Merge branch 'develop' of github.com:hillelcoren/invoice-ninja into develop

This commit is contained in:
Hillel Coren 2016-05-11 09:28:55 +03:00
commit f03adc767e
99 changed files with 4071 additions and 627 deletions

View File

@ -0,0 +1,23 @@
<?php namespace App\Events;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class PaymentCompleted extends Event {
use SerializesModels;
public $payment;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($payment)
{
$this->payment = $payment;
}
}

View File

@ -0,0 +1,23 @@
<?php namespace App\Events;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class PaymentFailed extends Event {
use SerializesModels;
public $payment;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($payment)
{
$this->payment = $payment;
}
}

View File

@ -0,0 +1,25 @@
<?php namespace App\Events;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class PaymentWasRefunded extends Event {
use SerializesModels;
public $payment;
public $refundAmount;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($payment, $refundAmount)
{
$this->payment = $payment;
$this->refundAmount = $refundAmount;
}
}

View File

@ -0,0 +1,22 @@
<?php namespace App\Events;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class PaymentWasVoided extends Event {
use SerializesModels;
public $payment;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($payment)
{
$this->payment = $payment;
}
}

View File

@ -182,14 +182,7 @@ class AccountController extends BaseController
if ($account->company->payment) {
$payment = $account->company->payment;
$gateway = $this->paymentService->createGateway($payment->account_gateway);
$refund = $gateway->refund(array(
'transactionReference' => $payment->transaction_reference,
'amount' => $payment->amount
));
$refund->send();
$payment->delete();
$this->paymentService->refund($payment);
Session::flash('message', trans('texts.plan_refunded'));
\Log::info("Refunded Plan Payment: {$account->name} - {$user->email}");
} else {

View File

@ -86,6 +86,7 @@ class AccountGatewayController extends BaseController
->where('id', '!=', GATEWAY_BITPAY)
->where('id', '!=', GATEWAY_GOCARDLESS)
->where('id', '!=', GATEWAY_DWOLLA)
->where('id', '!=', GATEWAY_STRIPE)
->orderBy('name')->get();
$data['hiddenFields'] = Gateway::$hiddenFields;
@ -104,7 +105,31 @@ class AccountGatewayController extends BaseController
$paymentTypes = [];
foreach (Gateway::$paymentTypes as $type) {
if ($accountGateway || !$account->getGatewayByType($type)) {
$paymentTypes[$type] = trans('texts.'.strtolower($type));
if ($type == PAYMENT_TYPE_CREDIT_CARD && $account->getGatewayByType(PAYMENT_TYPE_STRIPE)) {
// Stripe is already handling credit card payments
continue;
}
if ($type == PAYMENT_TYPE_STRIPE && $account->getGatewayByType(PAYMENT_TYPE_CREDIT_CARD)) {
// Another gateway is already handling credit card payments
continue;
}
if ($type == PAYMENT_TYPE_DIRECT_DEBIT && $stripeGateway = $account->getGatewayByType(PAYMENT_TYPE_STRIPE)) {
if (!empty($stripeGateway->getAchEnabled())) {
// Stripe is already handling ACH payments
continue;
}
}
if ($type == PAYMENT_TYPE_PAYPAL && $braintreeGateway = $account->getGatewayConfig(GATEWAY_BRAINTREE)) {
if (!empty($braintreeGateway->getPayPalEnabled())) {
// PayPal is already enabled
continue;
}
}
$paymentTypes[$type] = $type == PAYMENT_TYPE_CREDIT_CARD ? trans('texts.other_providers'): trans('texts.'.strtolower($type));
if ($type == PAYMENT_TYPE_BITCOIN) {
$paymentTypes[$type] .= ' - BitPay';
@ -185,6 +210,8 @@ class AccountGatewayController extends BaseController
$gatewayId = GATEWAY_GOCARDLESS;
} elseif ($paymentType == PAYMENT_TYPE_DWOLLA) {
$gatewayId = GATEWAY_DWOLLA;
} elseif ($paymentType == PAYMENT_TYPE_STRIPE) {
$gatewayId = GATEWAY_STRIPE;
}
if (!$gatewayId) {
@ -204,6 +231,7 @@ class AccountGatewayController extends BaseController
// do nothing - we're unable to acceptance test with StripeJS
} else {
$rules['publishable_key'] = 'required';
$rules['enable_ach'] = 'boolean';
}
}
@ -259,6 +287,35 @@ class AccountGatewayController extends BaseController
$config->publishableKey = $oldConfig->publishableKey;
}
$plaidClientId = Input::get('plaid_client_id');
if ($plaidClientId = str_replace('*', '', $plaidClientId)) {
$config->plaidClientId = $plaidClientId;
} elseif ($oldConfig && property_exists($oldConfig, 'plaidClientId')) {
$config->plaidClientId = $oldConfig->plaidClientId;
}
$plaidSecret = Input::get('plaid_secret');
if ($plaidSecret = str_replace('*', '', $plaidSecret)) {
$config->plaidSecret = $plaidSecret;
} elseif ($oldConfig && property_exists($oldConfig, 'plaidSecret')) {
$config->plaidSecret = $oldConfig->plaidSecret;
}
$plaidPublicKey = Input::get('plaid_public_key');
if ($plaidPublicKey = str_replace('*', '', $plaidPublicKey)) {
$config->plaidPublicKey = $plaidPublicKey;
} elseif ($oldConfig && property_exists($oldConfig, 'plaidPublicKey')) {
$config->plaidPublicKey = $oldConfig->plaidPublicKey;
}
if ($gatewayId == GATEWAY_STRIPE) {
$config->enableAch = boolval(Input::get('enable_ach'));
}
if ($gatewayId == GATEWAY_BRAINTREE) {
$config->enablePayPal = boolval(Input::get('enable_paypal'));
}
$cardCount = 0;
if ($creditcards) {
foreach ($creditcards as $card => $value) {

View File

@ -33,8 +33,7 @@ class AuthController extends Controller {
$client = $invoice->client;
$account = $client->account;
$data['hideLogo'] = $account->hasFeature(FEATURE_WHITE_LABEL);
$data['clientViewCSS'] = $account->clientViewCSS();
$data['account'] = $account;
$data['clientFontUrl'] = $account->getFontsUrl();
}
}

View File

@ -49,9 +49,7 @@ class PasswordController extends Controller {
$invoice = $invitation->invoice;
$client = $invoice->client;
$account = $client->account;
$data['hideLogo'] = $account->hasFeature(FEATURE_WHITE_LABEL);
$data['clientViewCSS'] = $account->clientViewCSS();
$data['account'] = $account;
$data['clientFontUrl'] = $account->getFontsUrl();
}
}
@ -117,8 +115,7 @@ class PasswordController extends Controller {
$client = $invoice->client;
$account = $client->account;
$data['hideLogo'] = $account->hasFeature(FEATURE_WHITE_LABEL);
$data['clientViewCSS'] = $account->clientViewCSS();
$data['account'] = $account;
$data['clientFontUrl'] = $account->getFontsUrl();
}
}

View File

@ -135,7 +135,8 @@ class ClientController extends BaseController
'hasRecurringInvoices' => Invoice::scope()->where('is_recurring', '=', true)->whereClientId($client->id)->count() > 0,
'hasQuotes' => Invoice::scope()->where('is_quote', '=', true)->whereClientId($client->id)->count() > 0,
'hasTasks' => Task::scope()->whereClientId($client->id)->count() > 0,
'gatewayLink' => $client->getGatewayLink(),
'gatewayLink' => $client->getGatewayLink($accountGateway),
'gateway' => $accountGateway
);
return View::make('clients.show', $data);

View File

@ -15,14 +15,17 @@ use Cache;
use App\Models\Invoice;
use App\Models\Invitation;
use App\Models\Client;
use App\Models\Account;
use App\Models\PaymentType;
use App\Models\License;
use App\Models\Payment;
use App\Models\Affiliate;
use App\Models\PaymentMethod;
use App\Ninja\Repositories\PaymentRepository;
use App\Ninja\Repositories\InvoiceRepository;
use App\Ninja\Repositories\AccountRepository;
use App\Ninja\Mailers\ContactMailer;
use App\Ninja\Mailers\UserMailer;
use App\Services\PaymentService;
use App\Http\Requests\PaymentRequest;
@ -33,7 +36,7 @@ class PaymentController extends BaseController
{
protected $entityType = ENTITY_PAYMENT;
public function __construct(PaymentRepository $paymentRepo, InvoiceRepository $invoiceRepo, AccountRepository $accountRepo, ContactMailer $contactMailer, PaymentService $paymentService)
public function __construct(PaymentRepository $paymentRepo, InvoiceRepository $invoiceRepo, AccountRepository $accountRepo, ContactMailer $contactMailer, PaymentService $paymentService, UserMailer $userMailer)
{
// parent::__construct();
@ -42,6 +45,7 @@ class PaymentController extends BaseController
$this->accountRepo = $accountRepo;
$this->contactMailer = $contactMailer;
$this->paymentService = $paymentService;
$this->userMailer = $userMailer;
}
public function index()
@ -56,8 +60,10 @@ class PaymentController extends BaseController
'client',
'transaction_reference',
'method',
'source',
'payment_amount',
'payment_date',
'status',
''
]),
));
@ -128,7 +134,7 @@ class PaymentController extends BaseController
];
}
public function show_payment($invitationKey, $paymentType = false)
public function show_payment($invitationKey, $paymentType = false, $sourceId = false)
{
$invitation = Invitation::with('invoice.invoice_items', 'invoice.client.currency', 'invoice.client.account.account_gateways.gateway')->where('invitation_key', '=', $invitationKey)->firstOrFail();
@ -144,54 +150,84 @@ class PaymentController extends BaseController
$account->account_gateways[0]->getPaymentType();
}
if ($paymentType == PAYMENT_TYPE_TOKEN) {
$useToken = true;
$paymentType = PAYMENT_TYPE_CREDIT_CARD;
}
Session::put($invitation->id . 'payment_type', $paymentType);
$data = array();
$accountGateway = $invoice->client->account->getGatewayByType($paymentType);
$gateway = $accountGateway->gateway;
$acceptedCreditCardTypes = $accountGateway->getCreditcardTypes();
// Handle offsite payments
if ($useToken || $paymentType != PAYMENT_TYPE_CREDIT_CARD
|| $gateway->id == GATEWAY_EWAY
|| $gateway->id == GATEWAY_TWO_CHECKOUT
|| $gateway->id == GATEWAY_PAYFAST
|| $gateway->id == GATEWAY_MOLLIE) {
if (Session::has('error')) {
Session::reflash();
return Redirect::to('view/'.$invitationKey);
if ($paymentType != PAYMENT_TYPE_BRAINTREE_PAYPAL) {
if ($paymentType == PAYMENT_TYPE_TOKEN) {
$useToken = true;
$accountGateway = $invoice->client->account->getTokenGateway();
$paymentType = $accountGateway->getPaymentType();
} else {
return self::do_payment($invitationKey, false, $useToken);
$accountGateway = $invoice->client->account->getGatewayByType($paymentType);
}
Session::put($invitation->id . 'payment_type', $paymentType);
$gateway = $accountGateway->gateway;
$acceptedCreditCardTypes = $accountGateway->getCreditcardTypes();
$isOffsite = ($paymentType != PAYMENT_TYPE_CREDIT_CARD && $accountGateway->getPaymentType() != PAYMENT_TYPE_STRIPE)
|| $gateway->id == GATEWAY_EWAY
|| $gateway->id == GATEWAY_TWO_CHECKOUT
|| $gateway->id == GATEWAY_PAYFAST
|| $gateway->id == GATEWAY_MOLLIE;
// Handle offsite payments
if ($useToken || $isOffsite) {
if (Session::has('error')) {
Session::reflash();
return Redirect::to('view/' . $invitationKey);
} else {
return self::do_payment($invitationKey, false, $useToken, $sourceId);
}
}
$data += [
'accountGateway' => $accountGateway,
'acceptedCreditCardTypes' => $acceptedCreditCardTypes,
'gateway' => $gateway,
'showAddress' => $accountGateway->show_address,
];
if ($paymentType == PAYMENT_TYPE_STRIPE_ACH) {
$data['currencies'] = Cache::get('currencies');
}
if ($gateway->id == GATEWAY_BRAINTREE) {
$data['braintreeClientToken'] = $this->paymentService->getBraintreeClientToken($account);
}
} else {
if ($deviceData = Input::get('details')) {
Session::put($invitation->id . 'device_data', $deviceData);
}
Session::put($invitation->id . 'payment_type', PAYMENT_TYPE_BRAINTREE_PAYPAL);
$paypalDetails = json_decode(Input::get('details'));
if (!$sourceId || !$paypalDetails) {
return Redirect::to('view/'.$invitationKey);
}
$data['paypalDetails'] = $paypalDetails;
}
$data = [
$data += [
'showBreadcrumbs' => false,
'url' => 'payment/'.$invitationKey,
'amount' => $invoice->getRequestedAmount(),
'invoiceNumber' => $invoice->invoice_number,
'client' => $client,
'contact' => $invitation->contact,
'gateway' => $gateway,
'accountGateway' => $accountGateway,
'acceptedCreditCardTypes' => $acceptedCreditCardTypes,
'paymentType' => $paymentType,
'countries' => Cache::get('countries'),
'currencyId' => $client->getCurrencyId(),
'currencyCode' => $client->currency ? $client->currency->code : ($account->currency ? $account->currency->code : 'USD'),
'account' => $client->account,
'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL),
'hideHeader' => $account->isNinjaAccount(),
'clientViewCSS' => $account->clientViewCSS(),
'clientFontUrl' => $account->getFontsUrl(),
'showAddress' => $accountGateway->show_address,
'sourceId' => $sourceId,
'clientFontUrl' => $client->account->getFontsUrl(),
];
return View::make('payments.payment', $data);
return View::make('payments.add_paymentmethod', $data);
}
public function show_license_payment()
@ -222,7 +258,7 @@ class PaymentController extends BaseController
$account = $this->accountRepo->getNinjaAccount();
$account->load('account_gateways.gateway');
$accountGateway = $account->getGatewayByType(PAYMENT_TYPE_CREDIT_CARD);
$accountGateway = $account->getGatewayByType(PAYMENT_TYPE_STRIPE_CREDIT_CARD);
$gateway = $accountGateway->gateway;
$acceptedCreditCardTypes = $accountGateway->getCreditcardTypes();
@ -247,7 +283,7 @@ class PaymentController extends BaseController
'showAddress' => true,
];
return View::make('payments.payment', $data);
return View::make('payments.add_paymentmethod', $data);
}
public function do_license_payment()
@ -278,7 +314,7 @@ class PaymentController extends BaseController
$account = $this->accountRepo->getNinjaAccount();
$account->load('account_gateways.gateway');
$accountGateway = $account->getGatewayByType(PAYMENT_TYPE_CREDIT_CARD);
$accountGateway = $account->getGatewayByType(PAYMENT_TYPE_STRIPE_CREDIT_CARD);
try {
$affiliate = Affiliate::find(Session::get('affiliate_id'));
@ -363,21 +399,13 @@ class PaymentController extends BaseController
}
}
public function do_payment($invitationKey, $onSite = true, $useToken = false)
{
$invitation = Invitation::with('invoice.invoice_items', 'invoice.client.currency', 'invoice.client.account.currency', 'invoice.client.account.account_gateways.gateway')->where('invitation_key', '=', $invitationKey)->firstOrFail();
$invoice = $invitation->invoice;
$client = $invoice->client;
$account = $client->account;
$accountGateway = $account->getGatewayByType(Session::get($invitation->id . 'payment_type'));
public static function processPaymentClientDetails($client, $accountGateway, $paymentType, $onSite = true){
$rules = [
'first_name' => 'required',
'last_name' => 'required',
];
if ( ! Input::get('stripeToken')) {
if ( ! Input::get('stripeToken') && ! Input::get('payment_method_nonce') && !(Input::get('plaidPublicToken') && Input::get('plaidAccountId'))) {
$rules = array_merge(
$rules,
[
@ -389,7 +417,9 @@ class PaymentController extends BaseController
);
}
if ($accountGateway->show_address) {
$requireAddress = $accountGateway->show_address && $paymentType != PAYMENT_TYPE_STRIPE_ACH && $paymentType != PAYMENT_TYPE_BRAINTREE_PAYPAL;
if ($requireAddress) {
$rules = array_merge($rules, [
'address1' => 'required',
'city' => 'required',
@ -408,7 +438,7 @@ class PaymentController extends BaseController
->withInput(Request::except('cvv'));
}
if ($accountGateway->update_address) {
if ($requireAddress && $accountGateway->update_address) {
$client->address1 = trim(Input::get('address1'));
$client->address2 = trim(Input::get('address2'));
$client->city = trim(Input::get('city'));
@ -419,6 +449,37 @@ class PaymentController extends BaseController
}
}
return true;
}
public function do_payment($invitationKey, $onSite = true, $useToken = false, $sourceId = false)
{
$invitation = Invitation::with('invoice.invoice_items', 'invoice.client.currency', 'invoice.client.account.currency', 'invoice.client.account.account_gateways.gateway')->where('invitation_key', '=', $invitationKey)->firstOrFail();
$invoice = $invitation->invoice;
$client = $invoice->client;
$account = $client->account;
$paymentType = Session::get($invitation->id . 'payment_type');
$accountGateway = $account->getGatewayByType($paymentType);
$paymentMethod = null;
if ($useToken) {
if(!$sourceId) {
Session::flash('error', trans('texts.no_payment_method_specified'));
return Redirect::to('payment/' . $invitationKey)->withInput(Request::except('cvv'));
} else {
$customerReference = $client->getGatewayToken($accountGateway, $accountGatewayToken/* return parameter*/);
$paymentMethod = PaymentMethod::scope($sourceId, $account->id, $accountGatewayToken->id)->firstOrFail();
$sourceReference = $paymentMethod->source_reference;
}
}
$result = static::processPaymentClientDetails($client, $accountGateway, $paymentType, $onSite);
if ($result !== true) {
return $result;
}
try {
// For offsite payments send the client's details on file
// If we're using a token then we don't need to send any other data
@ -433,27 +494,65 @@ class PaymentController extends BaseController
// check if we're creating/using a billing token
if ($accountGateway->gateway_id == GATEWAY_STRIPE) {
if ($token = Input::get('stripeToken')) {
$details['token'] = $token;
unset($details['card']);
if ($paymentType == PAYMENT_TYPE_STRIPE_ACH && !Input::get('authorize_ach')) {
Session::flash('error', trans('texts.ach_authorization_required'));
return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv'));
}
if ($useToken) {
$details['customerReference'] = $client->getGatewayToken();
} elseif ($account->token_billing_type_id == TOKEN_BILLING_ALWAYS || Input::get('token_billing')) {
$token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id);
$details['customerReference'] = $customerReference;
unset($details['token']);
$details['cardReference'] = $sourceReference;
} elseif ($account->token_billing_type_id == TOKEN_BILLING_ALWAYS || Input::get('token_billing') || $paymentType == PAYMENT_TYPE_STRIPE_ACH) {
$token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id, $customerReference/* return parameter */, $paymentMethod/* return parameter */);
if ($token) {
$details['customerReference'] = $token;
$details['token'] = $token;
$details['customerReference'] = $customerReference;
if ($paymentType == PAYMENT_TYPE_STRIPE_ACH && empty(Input::get('plaidPublicToken')) ) {
// The user needs to complete verification
Session::flash('message', trans('texts.bank_account_verification_next_steps'));
return Redirect::to('/client/paymentmethods');
}
} else {
$this->error('Token-No-Ref', $this->paymentService->lastError, $accountGateway);
return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv'));
}
}
} elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) {
$deviceData = Input::get('device_data');
if (!$deviceData) {
$deviceData = Session::get($invitation->id . 'device_data');
}
if ($token = Input::get('payment_method_nonce')) {
$details['token'] = $token;
unset($details['card']);
}
if ($useToken) {
$details['customerId'] = $customerReference;
$details['paymentMethodToken'] = $sourceReference;
unset($details['token']);
} elseif ($account->token_billing_type_id == TOKEN_BILLING_ALWAYS || Input::get('token_billing')) {
$token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id, $customerReference/* return parameter */, $paymentMethod/* return parameter */);
if ($token) {
$details['paymentMethodToken'] = $token;
$details['customerId'] = $customerReference;
unset($details['token']);
} else {
$this->error('Token-No-Ref', $this->paymentService->lastError, $accountGateway);
return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv'));
}
}
if($deviceData) {
$details['deviceData'] = $deviceData;
}
}
$response = $gateway->purchase($details)->send();
if ($accountGateway->gateway_id == GATEWAY_EWAY) {
$ref = $response->getData()['AccessCode'];
} elseif ($accountGateway->gateway_id == GATEWAY_TWO_CHECKOUT) {
@ -480,7 +579,7 @@ class PaymentController extends BaseController
}
if ($response->isSuccessful()) {
$payment = $this->paymentService->createPayment($invitation, $accountGateway, $ref);
$payment = $this->paymentService->createPayment($invitation, $accountGateway, $ref, null, $details, $paymentMethod, $response);
Session::flash('message', trans('texts.applied_payment'));
if ($account->account_key == NINJA_ACCOUNT_KEY) {
@ -577,7 +676,7 @@ class PaymentController extends BaseController
if ($response->isCancelled()) {
// do nothing
} elseif ($response->isSuccessful()) {
$payment = $this->paymentService->createPayment($invitation, $accountGateway, $ref, $payerId);
$payment = $this->paymentService->createPayment($invitation, $accountGateway, $ref, $payerId, $details, null, $purchaseResponse);
Session::flash('message', trans('texts.applied_payment'));
} else {
$this->error('offsite', $response->getMessage(), $accountGateway);
@ -624,11 +723,12 @@ class PaymentController extends BaseController
public function bulk()
{
$action = Input::get('action');
$amount = Input::get('amount');
$ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
$count = $this->paymentService->bulk($ids, $action);
$count = $this->paymentService->bulk($ids, $action, array('amount'=>$amount));
if ($count > 0) {
$message = Utils::pluralize($action.'d_payment', $count);
$message = Utils::pluralize($action=='refund'?'refunded_payment':$action.'d_payment', $count);
Session::flash('message', $message);
}
@ -646,4 +746,136 @@ class PaymentController extends BaseController
Session::flash('error', $message);
Utils::logError("Payment Error [{$type}]: " . ($exception ? Utils::getErrorString($exception) : $message), 'PHP', true);
}
public function getBankInfo($routingNumber) {
if (strlen($routingNumber) != 9 || !preg_match('/\d{9}/', $routingNumber)) {
return response()->json([
'message' => 'Invalid routing number',
], 400);
}
$data = PaymentMethod::lookupBankData($routingNumber);
if (is_string($data)) {
return response()->json([
'message' => $data,
], 500);
} elseif (!empty($data)) {
return $data;
}
return response()->json([
'message' => 'Bank not found',
], 404);
}
public function handlePaymentWebhook($accountKey, $gatewayId)
{
$gatewayId = intval($gatewayId);
$account = Account::where('accounts.account_key', '=', $accountKey)->first();
if (!$account) {
return response()->json([
'message' => 'Unknown account',
], 404);
}
$accountGateway = $account->getGatewayConfig(intval($gatewayId));
if (!$accountGateway) {
return response()->json([
'message' => 'Unknown gateway',
], 404);
}
switch($gatewayId) {
case GATEWAY_STRIPE:
return $this->handleStripeWebhook($accountGateway);
default:
return response()->json([
'message' => 'Unsupported gateway',
], 404);
}
}
protected function handleStripeWebhook($accountGateway) {
$eventId = Input::get('id');
$eventType= Input::get('type');
$accountId = $accountGateway->account_id;
if (!$eventId) {
return response()->json(['message' => 'Missing event id'], 400);
}
if (!$eventType) {
return response()->json(['message' => 'Missing event type'], 400);
}
$supportedEvents = array(
'charge.failed',
'charge.succeeded',
'customer.source.updated',
'customer.source.deleted',
);
if (!in_array($eventType, $supportedEvents)) {
return array('message' => 'Ignoring event');
}
// Fetch the event directly from Stripe for security
$eventDetails = $this->paymentService->makeStripeCall($accountGateway, 'GET', 'events/'.$eventId);
if (is_string($eventDetails) || !$eventDetails) {
return response()->json([
'message' => $eventDetails ? $eventDetails : 'Could not get event details.',
], 500);
}
if ($eventType != $eventDetails['type']) {
return response()->json(['message' => 'Event type mismatch'], 400);
}
if (!$eventDetails['pending_webhooks']) {
return response()->json(['message' => 'This is not a pending event'], 400);
}
if ($eventType == 'charge.failed' || $eventType == 'charge.succeeded') {
$charge = $eventDetails['data']['object'];
$transactionRef = $charge['id'];
$payment = Payment::scope(false, $accountId)->where('transaction_reference', '=', $transactionRef)->first();
if (!$payment) {
return array('message' => 'Unknown payment');
}
if ($eventType == 'charge.failed') {
if (!$payment->isFailed()) {
$payment->markFailed($charge['failure_message']);
$this->userMailer->sendNotification($payment->user, $payment->invoice, 'payment_failed', $payment);
}
} elseif ($eventType == 'charge.succeeded') {
$payment->markComplete();
}
} elseif($eventType == 'customer.source.updated' || $eventType == 'customer.source.deleted') {
$source = $eventDetails['data']['object'];
$sourceRef = $source['id'];
$paymentMethod = PaymentMethod::scope(false, $accountId)->where('source_reference', '=', $sourceRef)->first();
if (!$paymentMethod) {
return array('message' => 'Unknown payment method');
}
if ($eventType == 'customer.source.deleted') {
$paymentMethod->delete();
} elseif ($eventType == 'customer.source.updated') {
$this->paymentService->convertPaymentMethodFromStripe($source, null, $paymentMethod)->save();
}
}
return array('message' => 'Processed successfully');
}
}

View File

@ -10,9 +10,13 @@ use Request;
use Response;
use Session;
use Datatable;
use Validator;
use Cache;
use Redirect;
use App\Models\Gateway;
use App\Models\Invitation;
use App\Models\Document;
use App\ModelsPaymentMethod;
use App\Ninja\Repositories\InvoiceRepository;
use App\Ninja\Repositories\PaymentRepository;
use App\Ninja\Repositories\ActivityRepository;
@ -92,15 +96,22 @@ class PublicClientController extends BaseController
'phone',
]);
$data = array();
$paymentTypes = $this->getPaymentTypes($client, $invitation);
$paymentURL = '';
if (count($paymentTypes)) {
if (count($paymentTypes) == 1) {
$paymentURL = $paymentTypes[0]['url'];
if (!$account->isGatewayConfigured(GATEWAY_PAYPAL_EXPRESS)) {
$paymentURL = URL::to($paymentURL);
}
}
if ($braintreeGateway = $account->getGatewayConfig(GATEWAY_BRAINTREE)){
if($braintreeGateway->getPayPalEnabled()) {
$data['braintreeClientToken'] = $this->paymentService->getBraintreeClientToken($account);
}
}
$showApprove = $invoice->quote_invoice_id ? false : true;
if ($invoice->due_date) {
$showApprove = time() < strtotime($invoice->due_date);
@ -122,15 +133,10 @@ class PublicClientController extends BaseController
}
}
$data = array(
$data += array(
'account' => $account,
'showApprove' => $showApprove,
'showBreadcrumbs' => false,
'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL),
'hideHeader' => $account->isNinjaAccount() || !$account->enable_client_portal,
'hideDashboard' => !$account->enable_client_portal_dashboard,
'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS),
'clientViewCSS' => $account->clientViewCSS(),
'clientFontUrl' => $account->getFontsUrl(),
'invoice' => $invoice->hidePrivateFields(),
'invitation' => $invitation,
@ -161,23 +167,84 @@ class PublicClientController extends BaseController
$paymentTypes = [];
$account = $client->account;
if ($client->getGatewayToken()) {
$paymentTypes[] = [
'url' => URL::to("payment/{$invitation->invitation_key}/token"), 'label' => trans('texts.use_card_on_file')
];
}
foreach(Gateway::$paymentTypes as $type) {
if ($account->getGatewayByType($type)) {
$typeLink = strtolower(str_replace('PAYMENT_TYPE_', '', $type));
$url = URL::to("/payment/{$invitation->invitation_key}/{$typeLink}");
$paymentMethods = $this->paymentService->getClientPaymentMethods($client);
// PayPal doesn't allow being run in an iframe so we need to open in new tab
if ($type === PAYMENT_TYPE_PAYPAL && $account->iframe_url) {
$url = 'javascript:window.open("'.$url.'", "_blank")';
if ($paymentMethods) {
foreach ($paymentMethods as $paymentMethod) {
if ($paymentMethod->payment_type_id != PAYMENT_TYPE_ACH || $paymentMethod->status == PAYMENT_METHOD_STATUS_VERIFIED) {
$code = htmlentities(str_replace(' ', '', strtolower($paymentMethod->payment_type->name)));
if ($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH) {
if($paymentMethod->bank_data) {
$html = '<div>' . htmlentities($paymentMethod->bank_data->name) . '</div>';
}
} elseif ($paymentMethod->payment_type_id == PAYMENT_TYPE_ID_PAYPAL) {
$html = '<img height="22" src="'.URL::to('/images/credit_cards/paypal.png').'" alt="'.trans("texts.card_".$code).'">';
} else {
$html = '<img height="22" src="'.URL::to('/images/credit_cards/'.$code.'.png').'" alt="'.trans("texts.card_".$code).'">';
}
$url = URL::to("/payment/{$invitation->invitation_key}/token/".$paymentMethod->public_id);
if ($paymentMethod->payment_type_id == PAYMENT_TYPE_ID_PAYPAL) {
$html .= '&nbsp;&nbsp;<span>'.$paymentMethod->email.'</span>';
$url .= '#braintree_paypal';
} elseif ($paymentMethod->payment_type_id != PAYMENT_TYPE_ACH) {
$html .= '<div class="pull-right" style="text-align:right">'.trans('texts.card_expiration', array('expires' => Utils::fromSqlDate($paymentMethod->expiration, false)->format('m/y'))).'<br>';
$html .= '&bull;&bull;&bull;'.$paymentMethod->last4.'</div>';
} else {
$html .= '<div style="text-align:right">';
$html .= '&bull;&bull;&bull;'.$paymentMethod->last4.'</div>';
}
$paymentTypes[] = [
'url' => $url,
'label' => $html,
];
}
}
}
foreach(Gateway::$paymentTypes as $type) {
if ($gateway = $account->getGatewayByType($type)) {
$types = array($type);
if ($type == PAYMENT_TYPE_STRIPE) {
$types = array(PAYMENT_TYPE_STRIPE_CREDIT_CARD);
if ($gateway->getAchEnabled()) {
$types[] = PAYMENT_TYPE_STRIPE_ACH;
}
}
foreach($types as $type) {
$typeLink = strtolower(str_replace('PAYMENT_TYPE_', '', $type));
$url = URL::to("/payment/{$invitation->invitation_key}/{$typeLink}");
// PayPal doesn't allow being run in an iframe so we need to open in new tab
if ($type === PAYMENT_TYPE_PAYPAL && $account->iframe_url) {
$url = 'javascript:window.open("' . $url . '", "_blank")';
}
if ($type == PAYMENT_TYPE_STRIPE_CREDIT_CARD) {
$label = trans('texts.' . strtolower(PAYMENT_TYPE_CREDIT_CARD));
} elseif ($type == PAYMENT_TYPE_STRIPE_ACH) {
$label = trans('texts.' . strtolower(PAYMENT_TYPE_DIRECT_DEBIT));
} else {
$label = trans('texts.' . strtolower($type));
}
$paymentTypes[] = [
'url' => $url, 'label' => $label
];
if($gateway->getPayPalEnabled()) {
$paymentTypes[] = [
'label' => trans('texts.paypal'),
'url' => $url = URL::to("/payment/{$invitation->invitation_key}/braintree_paypal"),
];
}
}
$paymentTypes[] = [
'url' => $url, 'label' => trans('texts.'.strtolower($type))
];
}
}
@ -224,12 +291,17 @@ class PublicClientController extends BaseController
'color' => $color,
'account' => $account,
'client' => $client,
'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL),
'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS),
'clientViewCSS' => $account->clientViewCSS(),
'clientFontUrl' => $account->getFontsUrl(),
'gateway' => $account->getTokenGateway(),
'paymentMethods' => $this->paymentService->getClientPaymentMethods($client),
];
if ($braintreeGateway = $account->getGatewayConfig(GATEWAY_BRAINTREE)){
if($braintreeGateway->getPayPalEnabled()) {
$data['braintreeClientToken'] = $this->paymentService->getBraintreeClientToken($account);
}
}
return response()->view('invited.dashboard', $data);
}
@ -252,6 +324,9 @@ class PublicClientController extends BaseController
'invoice' => trans('texts.invoice') . ' ' . $model->invoice,
'contact' => Utils::getClientDisplayName($model),
'payment' => trans('texts.payment') . ($model->payment ? ' ' . $model->payment : ''),
'credit' => $model->payment_amount ? Utils::formatMoney($model->credit, $model->currency_id, $model->country_id) : '',
'payment_amount' => $model->payment_amount ? Utils::formatMoney($model->payment_amount, $model->currency_id, $model->country_id) : null,
'adjustment' => $model->adjustment ? Utils::formatMoney($model->adjustment, $model->currency_id, $model->country_id) : null,
];
return trans("texts.activity_{$model->activity_type_id}", $data);
@ -261,6 +336,33 @@ class PublicClientController extends BaseController
->make();
}
public function recurringInvoiceIndex()
{
if (!$invitation = $this->getInvitation()) {
return $this->returnError();
}
$account = $invitation->account;
if (!$account->enable_client_portal) {
return $this->returnError();
}
$color = $account->primary_color ? $account->primary_color : '#0b4d78';
$data = [
'color' => $color,
'account' => $account,
'client' => $invitation->invoice->client,
'clientFontUrl' => $account->getFontsUrl(),
'title' => trans('texts.recurring_invoices'),
'entityType' => ENTITY_RECURRING_INVOICE,
'columns' => Utils::trans(['frequency', 'start_date', 'end_date', 'invoice_total', 'auto_bill']),
];
return response()->view('public_list', $data);
}
public function invoiceIndex()
{
if (!$invitation = $this->getInvitation()) {
@ -277,10 +379,8 @@ class PublicClientController extends BaseController
$data = [
'color' => $color,
'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL),
'hideDashboard' => !$account->enable_client_portal_dashboard,
'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS),
'clientViewCSS' => $account->clientViewCSS(),
'account' => $account,
'client' => $invitation->invoice->client,
'clientFontUrl' => $account->getFontsUrl(),
'title' => trans('texts.invoices'),
'entityType' => ENTITY_INVOICE,
@ -299,6 +399,15 @@ class PublicClientController extends BaseController
return $this->invoiceRepo->getClientDatatable($invitation->contact_id, ENTITY_INVOICE, Input::get('sSearch'));
}
public function recurringInvoiceDatatable()
{
if (!$invitation = $this->getInvitation()) {
return '';
}
return $this->invoiceRepo->getClientRecurringDatatable($invitation->contact_id);
}
public function paymentIndex()
{
@ -314,14 +423,11 @@ class PublicClientController extends BaseController
$color = $account->primary_color ? $account->primary_color : '#0b4d78';
$data = [
'color' => $color,
'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL),
'hideDashboard' => !$account->enable_client_portal_dashboard,
'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS),
'clientViewCSS' => $account->clientViewCSS(),
'account' => $account,
'clientFontUrl' => $account->getFontsUrl(),
'entityType' => ENTITY_PAYMENT,
'title' => trans('texts.payments'),
'columns' => Utils::trans(['invoice', 'transaction_reference', 'method', 'payment_amount', 'payment_date'])
'columns' => Utils::trans(['invoice', 'transaction_reference', 'method', 'source', 'payment_amount', 'payment_date', 'status'])
];
return response()->view('public_list', $data);
@ -337,12 +443,60 @@ class PublicClientController extends BaseController
return Datatable::query($payments)
->addColumn('invoice_number', function ($model) { return $model->invitation_key ? link_to('/view/'.$model->invitation_key, $model->invoice_number)->toHtml() : $model->invoice_number; })
->addColumn('transaction_reference', function ($model) { return $model->transaction_reference ? $model->transaction_reference : '<i>Manual entry</i>'; })
->addColumn('payment_type', function ($model) { return $model->payment_type ? $model->payment_type : ($model->account_gateway_id ? '<i>Online payment</i>' : ''); })
->addColumn('payment_type', function ($model) { return ($model->payment_type && !$model->last4) ? $model->payment_type : ($model->account_gateway_id ? '<i>Online payment</i>' : ''); })
->addColumn('payment_source', function ($model) {
$code = str_replace(' ', '', strtolower($model->payment_type));
$card_type = trans("texts.card_" . $code);
if ($model->payment_type_id != PAYMENT_TYPE_ACH) {
if($model->last4) {
$expiration = trans('texts.card_expiration', array('expires' => Utils::fromSqlDate($model->expiration, false)->format('m/y')));
return '<img height="22" src="' . URL::to('/images/credit_cards/' . $code . '.png') . '" alt="' . htmlentities($card_type) . '">&nbsp; &bull;&bull;&bull;' . $model->last4 . ' ' . $expiration;
} elseif ($model->email) {
return $model->email;
}
} elseif ($model->last4) {
$bankData = PaymentMethod::lookupBankData($model->routing_number);
if (is_object($bankData)) {
return $bankData->name.'&nbsp; &bull;&bull;&bull;' . $model->last4;
} elseif($model->last4) {
return '<img height="22" src="' . URL::to('/images/credit_cards/ach.png') . '" alt="' . htmlentities($card_type) . '">&nbsp; &bull;&bull;&bull;' . $model->last4;
}
}
})
->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id); })
->addColumn('payment_date', function ($model) { return Utils::dateToString($model->payment_date); })
->addColumn('status', function ($model) { return $this->getPaymentStatusLabel($model); })
->orderColumns( 'invoice_number', 'transaction_reference', 'payment_type', 'amount', 'payment_date')
->make();
}
private function getPaymentStatusLabel($model)
{
$label = trans("texts.status_" . strtolower($model->payment_status_name));
$class = 'default';
switch ($model->payment_status_id) {
case PAYMENT_STATUS_PENDING:
$class = 'info';
break;
case PAYMENT_STATUS_COMPLETED:
$class = 'success';
break;
case PAYMENT_STATUS_FAILED:
$class = 'danger';
break;
case PAYMENT_STATUS_PARTIALLY_REFUNDED:
$label = trans('texts.status_partially_refunded_amount', [
'amount' => Utils::formatMoney($model->refunded, $model->currency_id, $model->country_id),
]);
$class = 'primary';
break;
case PAYMENT_STATUS_REFUNDED:
$class = 'default';
break;
}
return "<h4><div class=\"label label-{$class}\">$label</div></h4>";
}
public function quoteIndex()
{
if (!$invitation = $this->getInvitation()) {
@ -358,10 +512,7 @@ class PublicClientController extends BaseController
$color = $account->primary_color ? $account->primary_color : '#0b4d78';
$data = [
'color' => $color,
'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL),
'hideDashboard' => !$account->enable_client_portal_dashboard,
'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS),
'clientViewCSS' => $account->clientViewCSS(),
'account' => $account,
'clientFontUrl' => $account->getFontsUrl(),
'title' => trans('texts.quotes'),
'entityType' => ENTITY_QUOTE,
@ -396,10 +547,7 @@ class PublicClientController extends BaseController
$color = $account->primary_color ? $account->primary_color : '#0b4d78';
$data = [
'color' => $color,
'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL),
'hideDashboard' => !$account->enable_client_portal_dashboard,
'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS),
'clientViewCSS' => $account->clientViewCSS(),
'account' => $account,
'clientFontUrl' => $account->getFontsUrl(),
'title' => trans('texts.documents'),
'entityType' => ENTITY_DOCUMENT,
@ -593,4 +741,241 @@ class PublicClientController extends BaseController
return DocumentController::getDownloadResponse($document);
}
public function paymentMethods()
{
if (!$invitation = $this->getInvitation()) {
return $this->returnError();
}
$client = $invitation->invoice->client;
$account = $client->account;
$paymentMethods = $this->paymentService->getClientPaymentMethods($client);
$data = array(
'account' => $account,
'color' => $account->primary_color ? $account->primary_color : '#0b4d78',
'client' => $client,
'clientViewCSS' => $account->clientViewCSS(),
'clientFontUrl' => $account->getFontsUrl(),
'paymentMethods' => $paymentMethods,
'gateway' => $account->getTokenGateway(),
'title' => trans('texts.payment_methods')
);
if ($braintreeGateway = $account->getGatewayConfig(GATEWAY_BRAINTREE)){
if($braintreeGateway->getPayPalEnabled()) {
$data['braintreeClientToken'] = $this->paymentService->getBraintreeClientToken($account);
}
}
return response()->view('payments.paymentmethods', $data);
}
public function verifyPaymentMethod()
{
$publicId = Input::get('source_id');
$amount1 = Input::get('verification1');
$amount2 = Input::get('verification2');
if (!$invitation = $this->getInvitation()) {
return $this->returnError();
}
$client = $invitation->invoice->client;
$result = $this->paymentService->verifyClientPaymentMethod($client, $publicId, $amount1, $amount2);
if (is_string($result)) {
Session::flash('error', $result);
} else {
Session::flash('message', trans('texts.payment_method_verified'));
}
return redirect()->to($client->account->enable_client_portal?'/client/dashboard':'/client/paymentmethods/');
}
public function removePaymentMethod($publicId)
{
if (!$invitation = $this->getInvitation()) {
return $this->returnError();
}
$client = $invitation->invoice->client;
$result = $this->paymentService->removeClientPaymentMethod($client, $publicId);
if (is_string($result)) {
Session::flash('error', $result);
} else {
Session::flash('message', trans('texts.payment_method_removed'));
}
return redirect()->to($client->account->enable_client_portal?'/client/dashboard':'/client/paymentmethods/');
}
public function addPaymentMethod($paymentType, $token=false)
{
if (!$invitation = $this->getInvitation()) {
return $this->returnError();
}
$invoice = $invitation->invoice;
$client = $invitation->invoice->client;
$account = $client->account;
$typeLink = $paymentType;
$paymentType = 'PAYMENT_TYPE_' . strtoupper($paymentType);
$accountGateway = $invoice->client->account->getTokenGateway();
$gateway = $accountGateway->gateway;
if ($token && $paymentType == PAYMENT_TYPE_BRAINTREE_PAYPAL) {
$sourceReference = $this->paymentService->createToken($this->paymentService->createGateway($accountGateway), array('token'=>$token), $accountGateway, $client, $invitation->contact_id);
if(empty($sourceReference)) {
$this->paymentMethodError('Token-No-Ref', $this->paymentService->lastError, $accountGateway);
} else {
Session::flash('message', trans('texts.payment_method_added'));
}
return redirect()->to($account->enable_client_portal?'/client/dashboard':'/client/paymentmethods/');
}
$acceptedCreditCardTypes = $accountGateway->getCreditcardTypes();
$data = [
'showBreadcrumbs' => false,
'client' => $client,
'contact' => $invitation->contact,
'gateway' => $gateway,
'accountGateway' => $accountGateway,
'acceptedCreditCardTypes' => $acceptedCreditCardTypes,
'paymentType' => $paymentType,
'countries' => Cache::get('countries'),
'currencyId' => $client->getCurrencyId(),
'currencyCode' => $client->currency ? $client->currency->code : ($account->currency ? $account->currency->code : 'USD'),
'account' => $account,
'url' => URL::to('client/paymentmethods/add/'.$typeLink),
'clientFontUrl' => $account->getFontsUrl(),
'showAddress' => $accountGateway->show_address,
'paymentTitle' => trans('texts.add_payment_method'),
];
if ($paymentType == PAYMENT_TYPE_STRIPE_ACH) {
$data['currencies'] = Cache::get('currencies');
}
if ($gateway->id == GATEWAY_BRAINTREE) {
$data['braintreeClientToken'] = $this->paymentService->getBraintreeClientToken($account);
}
return View::make('payments.add_paymentmethod', $data);
}
public function postAddPaymentMethod($paymentType)
{
if (!$invitation = $this->getInvitation()) {
return $this->returnError();
}
$typeLink = $paymentType;
$paymentType = 'PAYMENT_TYPE_' . strtoupper($paymentType);
$client = $invitation->invoice->client;
$account = $client->account;
$accountGateway = $account->getGatewayByType($paymentType);
$sourceToken = $accountGateway->gateway_id == GATEWAY_STRIPE ? Input::get('stripeToken'):Input::get('payment_method_nonce');
$result = PaymentController::processPaymentClientDetails($client, $accountGateway, $paymentType);
if ($result !== true) {
return $result;
}
if ($sourceToken) {
$details = array('token' => $sourceToken);
} elseif (Input::get('plaidPublicToken')) {
$usingPlaid = true;
$details = array('plaidPublicToken' => Input::get('plaidPublicToken'), 'plaidAccountId' => Input::get('plaidAccountId'));
}
if ($paymentType == PAYMENT_TYPE_STRIPE_ACH && !Input::get('authorize_ach')) {
Session::flash('error', trans('texts.ach_authorization_required'));
return Redirect::to('client/paymentmethods/add/' . $typeLink)->withInput(Request::except('cvv'));
}
if (!empty($details)) {
$gateway = $this->paymentService->createGateway($accountGateway);
$sourceReference = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id);
} else {
return Redirect::to('client/paymentmethods/add/' . $typeLink)->withInput(Request::except('cvv'));
}
if(empty($sourceReference)) {
$this->paymentMethodError('Token-No-Ref', $this->paymentService->lastError, $accountGateway);
return Redirect::to('client/paymentmethods/add/' . $typeLink)->withInput(Request::except('cvv'));
} else if ($paymentType == PAYMENT_TYPE_STRIPE_ACH && empty($usingPlaid) ) {
// The user needs to complete verification
Session::flash('message', trans('texts.bank_account_verification_next_steps'));
return Redirect::to($account->enable_client_portal?'/client/dashboard':'/client/paymentmethods/');
} else {
Session::flash('message', trans('texts.payment_method_added'));
return redirect()->to($account->enable_client_portal?'/client/dashboard':'/client/paymentmethods/');
}
}
public function setDefaultPaymentMethod(){
if (!$invitation = $this->getInvitation()) {
return $this->returnError();
}
$validator = Validator::make(Input::all(), array('source' => 'required'));
$client = $invitation->invoice->client;
if ($validator->fails()) {
return Redirect::to($client->account->enable_client_portal?'/client/dashboard':'/client/paymentmethods/');
}
$result = $this->paymentService->setClientDefaultPaymentMethod($client, Input::get('source'));
if (is_string($result)) {
Session::flash('error', $result);
} else {
Session::flash('message', trans('texts.payment_method_set_as_default'));
}
return redirect()->to($client->account->enable_client_portal?'/client/dashboard':'/client/paymentmethods/');
}
private function paymentMethodError($type, $error, $accountGateway = false, $exception = false)
{
$message = '';
if ($accountGateway && $accountGateway->gateway) {
$message = $accountGateway->gateway->name . ': ';
}
$message .= $error ?: trans('texts.payment_method_error');
Session::flash('error', $message);
Utils::logError("Payment Method Error [{$type}]: " . ($exception ? Utils::getErrorString($exception) : $message), 'PHP', true);
}
public function setAutoBill(){
if (!$invitation = $this->getInvitation()) {
return $this->returnError();
}
$validator = Validator::make(Input::all(), array('public_id' => 'required'));
$client = $invitation->invoice->client;
if ($validator->fails()) {
return Redirect::to('client/invoices/recurring');
}
$publicId = Input::get('public_id');
$enable = Input::get('enable');
$invoice = $client->invoices->where('public_id', intval($publicId))->first();
if ($invoice && $invoice->is_recurring && ($invoice->auto_bill == AUTO_BILL_OPT_IN || $invoice->auto_bill == AUTO_BILL_OPT_OUT)) {
$invoice->client_enable_auto_bill = $enable ? true : false;
$invoice->save();
}
return Redirect::to('client/invoices/recurring');
}
}

View File

@ -22,6 +22,7 @@ class VerifyCsrfToken extends BaseVerifier {
'hook/email_opened',
'hook/email_bounced',
'reseller_stats',
'paymenthook/*',
];
/**

View File

@ -41,11 +41,19 @@ Route::group(['middleware' => 'auth:client'], function() {
Route::get('download/{invitation_key}', 'PublicClientController@download');
Route::get('view', 'HomeController@viewLogo');
Route::get('approve/{invitation_key}', 'QuoteController@approve');
Route::get('payment/{invitation_key}/{payment_type?}', 'PaymentController@show_payment');
Route::get('payment/{invitation_key}/{payment_type?}/{source_id?}', 'PaymentController@show_payment');
Route::post('payment/{invitation_key}', 'PaymentController@do_payment');
Route::match(['GET', 'POST'], 'complete', 'PaymentController@offsite_payment');
Route::get('client/paymentmethods', 'PublicClientController@paymentMethods');
Route::post('client/paymentmethods/verify', 'PublicClientController@verifyPaymentMethod');
Route::get('client/paymentmethods/add/{payment_type}/{source_id?}', 'PublicClientController@addPaymentMethod');
Route::post('client/paymentmethods/add/{payment_type}', 'PublicClientController@postAddPaymentMethod');
Route::post('client/paymentmethods/default', 'PublicClientController@setDefaultPaymentMethod');
Route::post('client/paymentmethods/{source_id}/remove', 'PublicClientController@removePaymentMethod');
Route::get('client/quotes', 'PublicClientController@quoteIndex');
Route::get('client/invoices', 'PublicClientController@invoiceIndex');
Route::get('client/invoices/recurring', 'PublicClientController@recurringInvoiceIndex');
Route::post('client/invoices/auto_bill', 'PublicClientController@setAutoBill');
Route::get('client/documents', 'PublicClientController@documentIndex');
Route::get('client/payments', 'PublicClientController@paymentIndex');
Route::get('client/dashboard', 'PublicClientController@dashboard');
@ -55,12 +63,15 @@ Route::group(['middleware' => 'auth:client'], function() {
Route::get('api/client.quotes', array('as'=>'api.client.quotes', 'uses'=>'PublicClientController@quoteDatatable'));
Route::get('api/client.invoices', array('as'=>'api.client.invoices', 'uses'=>'PublicClientController@invoiceDatatable'));
Route::get('api/client.recurring_invoices', array('as'=>'api.client.recurring_invoices', 'uses'=>'PublicClientController@recurringInvoiceDatatable'));
Route::get('api/client.documents', array('as'=>'api.client.documents', 'uses'=>'PublicClientController@documentDatatable'));
Route::get('api/client.payments', array('as'=>'api.client.payments', 'uses'=>'PublicClientController@paymentDatatable'));
Route::get('api/client.activity', array('as'=>'api.client.activity', 'uses'=>'PublicClientController@activityDatatable'));
});
Route::get('bank/{routing_number}', 'PaymentController@getBankInfo');
Route::post('paymenthook/{accountKey}/{gatewayId}', 'PaymentController@handlePaymentWebhook');
Route::get('license', 'PaymentController@show_license_payment');
Route::post('license', 'PaymentController@do_license_payment');
Route::get('claim_license', 'PaymentController@claim_license');
@ -396,6 +407,9 @@ if (!defined('CONTACT_EMAIL')) {
//define('ACTIVITY_TYPE_UPDATE_PAYMENT', 11);
define('ACTIVITY_TYPE_ARCHIVE_PAYMENT', 12);
define('ACTIVITY_TYPE_DELETE_PAYMENT', 13);
define('ACTIVITY_TYPE_VOIDED_PAYMENT', 39);
define('ACTIVITY_TYPE_REFUNDED_PAYMENT', 40);
define('ACTIVITY_TYPE_FAILED_PAYMENT', 41);
define('ACTIVITY_TYPE_CREATE_CREDIT', 14);
//define('ACTIVITY_TYPE_UPDATE_CREDIT', 15);
@ -473,7 +487,13 @@ if (!defined('CONTACT_EMAIL')) {
define('INVOICE_STATUS_PARTIAL', 5);
define('INVOICE_STATUS_PAID', 6);
define('PAYMENT_TYPE_CREDIT', 1);
define('PAYMENT_STATUS_PENDING', 1);
define('PAYMENT_STATUS_VOIDED', 2);
define('PAYMENT_STATUS_FAILED', 3);
define('PAYMENT_STATUS_COMPLETED', 4);
define('PAYMENT_STATUS_PARTIALLY_REFUNDED', 5);
define('PAYMENT_STATUS_REFUNDED', 6);
define('CUSTOM_DESIGN', 11);
define('FREQUENCY_WEEKLY', 1);
@ -537,6 +557,8 @@ if (!defined('CONTACT_EMAIL')) {
define('GATEWAY_DWOLLA', 43);
define('GATEWAY_CHECKOUT_COM', 47);
define('GATEWAY_CYBERSOURCE', 49);
define('GATEWAY_WEPAY', 60);
define('GATEWAY_BRAINTREE', 61);
define('EVENT_CREATE_CLIENT', 1);
define('EVENT_CREATE_INVOICE', 2);
@ -608,7 +630,34 @@ if (!defined('CONTACT_EMAIL')) {
define('TOKEN_BILLING_OPT_OUT', 3);
define('TOKEN_BILLING_ALWAYS', 4);
define('PAYMENT_TYPE_CREDIT', 1);
define('PAYMENT_TYPE_ACH', 5);
define('PAYMENT_TYPE_VISA', 6);
define('PAYMENT_TYPE_MASTERCARD', 7);
define('PAYMENT_TYPE_AMERICAN_EXPRESS', 8);
define('PAYMENT_TYPE_DISCOVER', 9);
define('PAYMENT_TYPE_DINERS', 10);
define('PAYMENT_TYPE_EUROCARD', 11);
define('PAYMENT_TYPE_NOVA', 12);
define('PAYMENT_TYPE_CREDIT_CARD_OTHER', 13);
define('PAYMENT_TYPE_ID_PAYPAL', 14);
define('PAYMENT_TYPE_CARTE_BLANCHE', 17);
define('PAYMENT_TYPE_UNIONPAY', 18);
define('PAYMENT_TYPE_JCB', 19);
define('PAYMENT_TYPE_LASER', 20);
define('PAYMENT_TYPE_MAESTRO', 21);
define('PAYMENT_TYPE_SOLO', 22);
define('PAYMENT_TYPE_SWITCH', 23);
define('PAYMENT_METHOD_STATUS_NEW', 'new');
define('PAYMENT_METHOD_STATUS_VERIFICATION_FAILED', 'verification_failed');
define('PAYMENT_METHOD_STATUS_VERIFIED', 'verified');
define('PAYMENT_TYPE_PAYPAL', 'PAYMENT_TYPE_PAYPAL');
define('PAYMENT_TYPE_STRIPE', 'PAYMENT_TYPE_STRIPE');
define('PAYMENT_TYPE_STRIPE_CREDIT_CARD', 'PAYMENT_TYPE_STRIPE_CREDIT_CARD');
define('PAYMENT_TYPE_STRIPE_ACH', 'PAYMENT_TYPE_STRIPE_ACH');
define('PAYMENT_TYPE_BRAINTREE_PAYPAL', 'PAYMENT_TYPE_BRAINTREE_PAYPAL');
define('PAYMENT_TYPE_CREDIT_CARD', 'PAYMENT_TYPE_CREDIT_CARD');
define('PAYMENT_TYPE_DIRECT_DEBIT', 'PAYMENT_TYPE_DIRECT_DEBIT');
define('PAYMENT_TYPE_BITCOIN', 'PAYMENT_TYPE_BITCOIN');
@ -652,6 +701,11 @@ if (!defined('CONTACT_EMAIL')) {
define('RESELLER_REVENUE_SHARE', 'A');
define('RESELLER_LIMITED_USERS', 'B');
define('AUTO_BILL_OFF', 0);
define('AUTO_BILL_OPT_IN', 1);
define('AUTO_BILL_OPT_OUT', 2);
define('AUTO_BILL_ALWAYS', 3);
// These must be lowercase
define('PLAN_FREE', 'free');
define('PLAN_PRO', 'pro');

View File

@ -22,8 +22,11 @@ use App\Events\QuoteInvitationWasViewed;
use App\Events\QuoteInvitationWasApproved;
use App\Events\PaymentWasCreated;
use App\Events\PaymentWasDeleted;
use App\Events\PaymentWasRefunded;
use App\Events\PaymentWasVoided;
use App\Events\PaymentWasArchived;
use App\Events\PaymentWasRestored;
use App\Events\PaymentFailed;
use App\Events\CreditWasCreated;
use App\Events\CreditWasDeleted;
use App\Events\CreditWasArchived;
@ -309,6 +312,42 @@ class ActivityListener
);
}
public function refundedPayment(PaymentWasRefunded $event)
{
$payment = $event->payment;
$this->activityRepo->create(
$payment,
ACTIVITY_TYPE_REFUNDED_PAYMENT,
$event->refundAmount,
$event->refundAmount * -1
);
}
public function voidedPayment(PaymentWasVoided $event)
{
$payment = $event->payment;
$this->activityRepo->create(
$payment,
ACTIVITY_TYPE_VOIDED_PAYMENT,
$payment->amount,
$payment->amount * -1
);
}
public function failedPayment(PaymentFailed $event)
{
$payment = $event->payment;
$this->activityRepo->create(
$payment,
ACTIVITY_TYPE_FAILED_PAYMENT,
$payment->amount,
$payment->amount * -1
);
}
public function archivedPayment(PaymentWasArchived $event)
{
if ($event->payment->is_deleted) {

View File

@ -1,8 +1,10 @@
<?php namespace App\Listeners;
use App\Events\PaymentFailed;
use Carbon;
use App\Models\Credit;
use App\Events\PaymentWasDeleted;
use App\Events\PaymentWasRefunded;
use App\Ninja\Repositories\CreditRepository;
class CreditListener
@ -26,7 +28,24 @@ class CreditListener
$credit = Credit::createNew();
$credit->client_id = $payment->client_id;
$credit->credit_date = Carbon::now()->toDateTimeString();
$credit->balance = $credit->amount = $payment->amount;
$credit->balance = $credit->amount = $payment->amount - $payment->refunded;
$credit->private_notes = $payment->transaction_reference;
$credit->save();
}
public function refundedPayment(PaymentWasRefunded $event)
{
$payment = $event->payment;
// if the payment was from a credit we need to refund the credit
if ($payment->payment_type_id != PAYMENT_TYPE_CREDIT) {
return;
}
$credit = Credit::createNew();
$credit->client_id = $payment->client_id;
$credit->credit_date = Carbon::now()->toDateTimeString();
$credit->balance = $credit->amount = $event->refundAmount;
$credit->private_notes = $payment->transaction_reference;
$credit->save();
}

View File

@ -7,7 +7,10 @@ use App\Events\InvoiceWasUpdated;
use App\Events\InvoiceWasCreated;
use App\Events\PaymentWasCreated;
use App\Events\PaymentWasDeleted;
use App\Events\PaymentWasRefunded;
use App\Events\PaymentWasRestored;
use App\Events\PaymentWasVoided;
use App\Events\PaymentFailed;
use App\Events\InvoiceInvitationWasViewed;
class InvoiceListener
@ -55,6 +58,26 @@ class InvoiceListener
}
public function deletedPayment(PaymentWasDeleted $event)
{
$payment = $event->payment;
$invoice = $payment->invoice;
$adjustment = $payment->amount - $payment->refunded;
$invoice->updateBalances($adjustment);
$invoice->updatePaidStatus();
}
public function refundedPayment(PaymentWasRefunded $event)
{
$payment = $event->payment;
$invoice = $payment->invoice;
$adjustment = $event->refundAmount;
$invoice->updateBalances($adjustment);
$invoice->updatePaidStatus();
}
public function voidedPayment(PaymentWasVoided $event)
{
$payment = $event->payment;
$invoice = $payment->invoice;
@ -64,6 +87,16 @@ class InvoiceListener
$invoice->updatePaidStatus();
}
public function failedPayment(PaymentFailed $event)
{
$payment = $event->payment;
$invoice = $payment->invoice;
$adjustment = $payment->amount - $payment->refunded;
$invoice->updateBalances($adjustment);
$invoice->updatePaidStatus();
}
public function restoredPayment(PaymentWasRestored $event)
{
if ( ! $event->fromDeleted) {
@ -72,7 +105,7 @@ class InvoiceListener
$payment = $event->payment;
$invoice = $payment->invoice;
$adjustment = $payment->amount * -1;
$adjustment = ($payment->amount - $payment->refunded) * -1;
$invoice->updateBalances($adjustment);
$invoice->updatePaidStatus();

View File

@ -381,6 +381,19 @@ class Account extends Eloquent
public function getGatewayByType($type = PAYMENT_TYPE_ANY)
{
if ($type == PAYMENT_TYPE_STRIPE_ACH || $type == PAYMENT_TYPE_STRIPE_CREDIT_CARD) {
$type = PAYMENT_TYPE_STRIPE;
}
if ($type == PAYMENT_TYPE_BRAINTREE_PAYPAL) {
$gateway = $this->getGatewayConfig(GATEWAY_BRAINTREE);
if (!$gateway || !$gateway->getPayPalEnabled()){
return false;
}
return $gateway;
}
foreach ($this->account_gateways as $gateway) {
if (!$type || $type == PAYMENT_TYPE_ANY) {
return $gateway;
@ -1217,9 +1230,9 @@ class Account extends Eloquent
return false;
}
public function showTokenCheckbox()
public function showTokenCheckbox(&$storage_gateway = null)
{
if (!$this->isGatewayConfigured(GATEWAY_STRIPE)) {
if (!($storage_gateway = $this->getTokenGatewayId())) {
return false;
}
@ -1227,6 +1240,25 @@ class Account extends Eloquent
|| $this->token_billing_type_id == TOKEN_BILLING_OPT_OUT;
}
public function getTokenGatewayId() {
if ($this->isGatewayConfigured(GATEWAY_STRIPE)) {
return GATEWAY_STRIPE;
} elseif ($this->isGatewayConfigured(GATEWAY_BRAINTREE)) {
return GATEWAY_BRAINTREE;
} else {
return false;
}
}
public function getTokenGateway() {
$gatewayId = $this->getTokenGatewayId();
if (!$gatewayId) {
return;
}
return $this->getGatewayConfig($gatewayId);
}
public function selectTokenCheckbox()
{
return $this->token_billing_type_id == TOKEN_BILLING_OPT_OUT;

View File

@ -71,5 +71,58 @@ class AccountGateway extends EntityModel
return $this->getConfigField('publishableKey');
}
public function getAchEnabled()
{
return !empty($this->getConfigField('enableAch'));
}
public function getPayPAlEnabled()
{
return !empty($this->getConfigField('enablePayPal'));
}
public function getPlaidSecret()
{
if ( ! $this->isGateway(GATEWAY_STRIPE)) {
return false;
}
return $this->getConfigField('plaidSecret');
}
public function getPlaidClientId()
{
if ( ! $this->isGateway(GATEWAY_STRIPE)) {
return false;
}
return $this->getConfigField('plaidClientId');
}
public function getPlaidPublicKey()
{
if ( ! $this->isGateway(GATEWAY_STRIPE)) {
return false;
}
return $this->getConfigField('plaidPublicKey');
}
public function getPlaidEnabled()
{
return !empty($this->getPlaidClientId()) && $this->getAchEnabled();
}
public function getPlaidEnvironment()
{
if (!$this->getPlaidClientId()) {
return null;
}
$stripe_key = $this->getPublishableStripeKey();
return substr(trim($stripe_key), 0, 8) == 'pk_test_' ? 'tartan' : 'production';
}
}

View File

@ -8,4 +8,18 @@ class AccountGatewayToken extends Eloquent
use SoftDeletes;
protected $dates = ['deleted_at'];
public $timestamps = true;
protected $casts = [
'uses_local_payment_methods' => 'boolean',
];
public function payment_methods()
{
return $this->hasMany('App\Models\PaymentMethod');
}
public function default_payment_method()
{
return $this->hasOne('App\Models\PaymentMethod', 'id', 'default_payment_method_id');
}
}

View File

@ -70,6 +70,8 @@ class Activity extends Eloquent
'quote' => $invoice ? link_to($invoice->getRoute(), $invoice->getDisplayName()) : null,
'contact' => $contactId ? $client->getDisplayName() : $user->getDisplayName(),
'payment' => $payment ? $payment->transaction_reference : null,
'payment_amount' => $payment ? $account->formatMoney($payment->amount, $payment) : null,
'adjustment' => $this->adjustment ? $account->formatMoney($this->adjustment, $this) : asdf,
'credit' => $credit ? $account->formatMoney($credit->amount, $client) : null,
];

View File

@ -261,7 +261,7 @@ class Client extends EntityModel
}
public function getGatewayToken()
public function getGatewayToken(&$accountGateway = null, &$token = null)
{
$account = $this->account;
@ -273,7 +273,9 @@ class Client extends EntityModel
return false;
}
$accountGateway = $account->getGatewayConfig(GATEWAY_STRIPE);
if (!$accountGateway){
$accountGateway = $account->getTokenGateway();
}
if (!$accountGateway) {
return false;
@ -285,10 +287,22 @@ class Client extends EntityModel
return $token ? $token->token : false;
}
public function getGatewayLink()
public function getGatewayLink(&$accountGateway = null)
{
$token = $this->getGatewayToken();
return $token ? "https://dashboard.stripe.com/customers/{$token}" : false;
$token = $this->getGatewayToken($accountGateway);
if (!$token) {
return false;
}
if ($accountGateway->gateway_id == GATEWAY_STRIPE) {
return "https://dashboard.stripe.com/customers/{$token}";
} elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) {
$merchantId = $accountGateway->getConfig()->merchantId;
$testMode = $accountGateway->getConfig()->testMode;
return $testMode ? "https://sandbox.braintreegateway.com/merchants/{$merchantId}/customers/{$token}" : "https://www.braintreegateway.com/merchants/{$merchantId}/customers/{$token}";
} else {
return false;
}
}
public function getAmount()
@ -319,6 +333,10 @@ class Client extends EntityModel
$this->last_login = Carbon::now()->toDateTimeString();
$this->save();
}
public function hasAutoBillConfigurableInvoices(){
return $this->invoices()->whereIn('auto_bill', [AUTO_BILL_OPT_IN, AUTO_BILL_OPT_OUT])->count() > 0;
}
}
Client::creating(function ($client) {

View File

@ -9,6 +9,7 @@ class Gateway extends Eloquent
public $timestamps = true;
public static $paymentTypes = [
PAYMENT_TYPE_STRIPE,
PAYMENT_TYPE_CREDIT_CARD,
PAYMENT_TYPE_PAYPAL,
PAYMENT_TYPE_BITCOIN,
@ -97,8 +98,10 @@ class Gateway extends Eloquent
return PAYMENT_TYPE_BITCOIN;
} else if ($gatewayId == GATEWAY_DWOLLA) {
return PAYMENT_TYPE_DWOLLA;
}else if ($gatewayId == GATEWAY_GOCARDLESS) {
} else if ($gatewayId == GATEWAY_GOCARDLESS) {
return PAYMENT_TYPE_DIRECT_DEBIT;
} else if ($gatewayId == GATEWAY_STRIPE) {
return PAYMENT_TYPE_STRIPE;
} else {
return PAYMENT_TYPE_CREDIT_CARD;
}

View File

@ -35,7 +35,7 @@ class Invoice extends EntityModel implements BalanceAffecting
protected $casts = [
'is_recurring' => 'boolean',
'has_tasks' => 'boolean',
'auto_bill' => 'boolean',
'client_enable_auto_bill' => 'boolean',
'has_expenses' => 'boolean',
];

View File

@ -1,7 +1,14 @@
<?php namespace App\Models;
use Event;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Events\PaymentWasCreated;
use App\Events\PaymentWasRefunded;
use App\Events\PaymentWasVoided;
use App\Events\PaymentCompleted;
use App\Events\PaymentVoided;
use App\Events\PaymentFailed;
use App\Models\PaymentMethod;
use Laracasts\Presenter\PresentableTrait;
class Payment extends EntityModel
@ -52,6 +59,16 @@ class Payment extends EntityModel
return $this->belongsTo('App\Models\PaymentType');
}
public function payment_method()
{
return $this->belongsTo('App\Models\PaymentMethod');
}
public function payment_status()
{
return $this->belongsTo('App\Models\PaymentStatus');
}
public function getRoute()
{
return "/payments/{$this->public_id}/edit";
@ -69,10 +86,99 @@ class Payment extends EntityModel
return trim("payment {$this->transaction_reference}");
}
public function isPending()
{
return $this->payment_status_id = PAYMENT_STATUS_PENDING;
}
public function isFailed()
{
return $this->payment_status_id = PAYMENT_STATUS_FAILED;
}
public function isCompleted()
{
return $this->payment_status_id == PAYMENT_STATUS_COMPLETED;
}
public function isPartiallyRefunded()
{
return $this->payment_status_id == PAYMENT_STATUS_PARTIALLY_REFUNDED;
}
public function isRefunded()
{
return $this->payment_status_id == PAYMENT_STATUS_REFUNDED;
}
public function isVoided()
{
return $this->payment_status_id == PAYMENT_STATUS_VOIDED;
}
public function recordRefund($amount = null)
{
if (!$this->isRefunded() && !$this->isVoided()) {
if (!$amount) {
$amount = $this->amount;
}
$new_refund = min($this->amount, $this->refunded + $amount);
$refund_change = $new_refund - $this->refunded;
if ($refund_change) {
$this->refunded = $new_refund;
$this->payment_status_id = $this->refunded == $this->amount ? PAYMENT_STATUS_REFUNDED : PAYMENT_STATUS_PARTIALLY_REFUNDED;
$this->save();
Event::fire(new PaymentWasRefunded($this, $refund_change));
}
}
}
public function markVoided()
{
if (!$this->isVoided() && !$this->isPartiallyRefunded() && !$this->isRefunded()) {
$this->refunded = $this->amount;
$this->payment_status_id = PAYMENT_STATUS_VOIDED;
$this->save();
Event::fire(new PaymentWasVoided($this));
}
}
public function markComplete()
{
$this->payment_status_id = PAYMENT_STATUS_COMPLETED;
$this->save();
Event::fire(new PaymentCompleted($this));
}
public function markFailed($failureMessage)
{
$this->payment_status_id = PAYMENT_STATUS_FAILED;
$this->gateway_error = $failureMessage;
$this->save();
Event::fire(new PaymentFailed($this));
}
public function getEntityType()
{
return ENTITY_PAYMENT;
}
public function getBankData()
{
if (!$this->routing_number) {
return null;
}
return PaymentMethod::lookupBankData($this->routing_number);
}
public function getLast4Attribute($value)
{
return $value ? str_pad($value, 4, '0', STR_PAD_LEFT) : null;
}
}
Payment::creating(function ($payment) {

View File

@ -0,0 +1,162 @@
<?php namespace App\Models;
use Cache;
use Eloquent;
use Illuminate\Database\Eloquent\SoftDeletes;
class PaymentMethod extends EntityModel
{
use SoftDeletes;
protected $dates = ['deleted_at'];
public $timestamps = true;
protected $hidden = ['id'];
public static function createNew($accountGatewayToken = null)
{
$entity = new PaymentMethod();
$entity->account_id = $accountGatewayToken->account_id;
$entity->account_gateway_token_id = $accountGatewayToken->id;
$lastEntity = static::scope(false, $entity->account_id);
$lastEntity = $lastEntity->orderBy('public_id', 'DESC')
->first();
if ($lastEntity) {
$entity->public_id = $lastEntity->public_id + 1;
} else {
$entity->public_id = 1;
}
return $entity;
}
public function account()
{
return $this->belongsTo('App\Models\Account');
}
public function contact()
{
return $this->belongsTo('App\Models\Contact');
}
public function account_gateway_token()
{
return $this->belongsTo('App\Models\AccountGatewayToken');
}
public function payment_type()
{
return $this->belongsTo('App\Models\PaymentType');
}
public function currency()
{
return $this->belongsTo('App\Models\Currency');
}
public function payments()
{
return $this->hasMany('App\Models\Payments');
}
public function getBankData()
{
if (!$this->routing_number) {
return null;
}
return static::lookupBankData($this->routing_number);
}
public function getLast4Attribute($value)
{
return $value ? str_pad($value, 4, '0', STR_PAD_LEFT) : null;
}
public function scopeScope($query, $publicId = false, $accountId = false, $accountGatewayTokenId = false)
{
$query = parent::scopeScope($query, $publicId, $accountId);
if ($accountGatewayTokenId) {
$query->where($this->getTable() . '.account_gateway_token_id', '=', $accountGatewayTokenId);
}
return $query;
}
public static function lookupBankData($routingNumber) {
$cached = Cache::get('bankData:'.$routingNumber);
if ($cached != null) {
return $cached == false ? null : $cached;
}
$dataPath = base_path('vendor/gatepay/FedACHdir/FedACHdir.txt');
if (!file_exists($dataPath) || !$size = filesize($dataPath)) {
return 'Invalid data file';
}
$lineSize = 157;
$numLines = $size/$lineSize;
if ($numLines % 1 != 0) {
// The number of lines should be an integer
return 'Invalid data file';
}
// Format: http://www.sco.ca.gov/Files-21C/Bank_Master_Interface_Information_Package.pdf
$file = fopen($dataPath, 'r');
// Binary search
$low = 0;
$high = $numLines - 1;
while ($low <= $high) {
$mid = floor(($low + $high) / 2);
fseek($file, $mid * $lineSize);
$thisNumber = fread($file, 9);
if ($thisNumber > $routingNumber) {
$high = $mid - 1;
} else if ($thisNumber < $routingNumber) {
$low = $mid + 1;
} else {
$data = new \stdClass();
$data->routing_number = $thisNumber;
fseek($file, 26, SEEK_CUR);
$data->name = trim(fread($file, 36));
$data->address = trim(fread($file, 36));
$data->city = trim(fread($file, 20));
$data->state = fread($file, 2);
$data->zip = fread($file, 5).'-'.fread($file, 4);
$data->phone = fread($file, 10);
break;
}
}
if (!empty($data)) {
Cache::put('bankData:'.$routingNumber, $data, 5);
return $data;
} else {
Cache::put('bankData:'.$routingNumber, false, 5);
return null;
}
}
}
PaymentMethod::deleting(function($paymentMethod) {
$accountGatewayToken = $paymentMethod->account_gateway_token;
if ($accountGatewayToken->default_payment_method_id == $paymentMethod->id) {
$newDefault = $accountGatewayToken->payment_methods->first(function($i, $paymentMethdod) use ($accountGatewayToken){
return $paymentMethdod->id != $accountGatewayToken->default_payment_method_id;
});
$accountGatewayToken->default_payment_method_id = $newDefault ? $newDefault->id : null;
$accountGatewayToken->save();
}
});

View File

@ -0,0 +1,8 @@
<?php namespace App\Models;
use Eloquent;
class PaymentStatus extends Eloquent
{
public $timestamps = false;
}

View File

@ -57,6 +57,7 @@ class UserMailer extends Mailer
];
if ($payment) {
$data['payment'] = $payment;
$data['paymentAmount'] = $account->formatMoney($payment->amount, $client);
}

View File

@ -97,6 +97,7 @@ class ActivityRepository
'contacts.last_name as last_name',
'contacts.email as email',
'payments.transaction_reference as payment',
'payments.amount as payment_amount',
'credits.amount as credit'
);
}

View File

@ -147,6 +147,53 @@ class InvoiceRepository extends BaseRepository
return $query;
}
public function getClientRecurringDatatable($contactId)
{
$query = DB::table('invitations')
->join('accounts', 'accounts.id', '=', 'invitations.account_id')
->join('invoices', 'invoices.id', '=', 'invitations.invoice_id')
->join('clients', 'clients.id', '=', 'invoices.client_id')
->join('frequencies', 'frequencies.id', '=', 'invoices.frequency_id')
->where('invitations.contact_id', '=', $contactId)
->where('invitations.deleted_at', '=', null)
->where('invoices.is_quote', '=', false)
->where('invoices.is_deleted', '=', false)
->where('clients.deleted_at', '=', null)
->where('invoices.is_recurring', '=', true)
->whereIn('invoices.auto_bill', [AUTO_BILL_OPT_IN, AUTO_BILL_OPT_OUT])
//->where('invoices.start_date', '>=', date('Y-m-d H:i:s'))
->select(
DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'),
DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'),
'invitations.invitation_key',
'invoices.invoice_number',
'invoices.due_date',
'clients.public_id as client_public_id',
'clients.name as client_name',
'invoices.public_id',
'invoices.amount',
'invoices.start_date',
'invoices.end_date',
'invoices.client_enable_auto_bill',
'frequencies.name as frequency'
);
$table = \Datatable::query($query)
->addColumn('frequency', function ($model) { return $model->frequency; })
->addColumn('start_date', function ($model) { return Utils::fromSqlDate($model->start_date); })
->addColumn('end_date', function ($model) { return Utils::fromSqlDate($model->end_date); })
->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id); })
->addColumn('client_enable_auto_bill', function ($model) {
if ($model->client_enable_auto_bill) {
return trans('texts.enabled') . ' <a href="javascript:setAutoBill('.$model->public_id.',false)">('.trans('texts.disable').')</a>';
} else {
return trans('texts.disabled') . ' <a href="javascript:setAutoBill('.$model->public_id.',true)">('.trans('texts.enable').')</a>';
}
});
return $table->make();
}
public function getClientDatatable($contactId, $entityType, $search)
{
$query = DB::table('invitations')
@ -276,7 +323,12 @@ class InvoiceRepository extends BaseRepository
$invoice->frequency_id = $data['frequency_id'] ? $data['frequency_id'] : 0;
$invoice->start_date = Utils::toSqlDate($data['start_date']);
$invoice->end_date = Utils::toSqlDate($data['end_date']);
$invoice->auto_bill = isset($data['auto_bill']) && $data['auto_bill'] ? true : false;
$invoice->client_enable_auto_bill = isset($data['client_enable_auto_bill']) && $data['client_enable_auto_bill'] ? true : false;
$invoice->auto_bill = isset($data['auto_bill']) ? intval($data['auto_bill']) : 0;
if ($invoice->auto_bill < AUTO_BILL_OFF || $invoice->auto_bill > AUTO_BILL_ALWAYS ) {
$invoice->auto_bill = AUTO_BILL_OFF;
}
if (isset($data['recurring_due_date'])) {
$invoice->due_date = $data['recurring_due_date'];
@ -763,7 +815,7 @@ class InvoiceRepository extends BaseRepository
$recurInvoice->last_sent_date = date('Y-m-d');
$recurInvoice->save();
if ($recurInvoice->auto_bill) {
if ($recurInvoice->auto_bill == AUTO_BILL_ALWAYS || ($recurInvoice->auto_bill != AUTO_BILL_OFF && $recurInvoice->client_enable_auto_bill)) {
if ($this->paymentService->autoBillInvoice($invoice)) {
// update the invoice reference to match its actual state
// this is to ensure a 'payment received' email is sent

View File

@ -22,6 +22,7 @@ class PaymentRepository extends BaseRepository
->join('clients', 'clients.id', '=', 'payments.client_id')
->join('invoices', 'invoices.id', '=', 'payments.invoice_id')
->join('contacts', 'contacts.client_id', '=', 'clients.id')
->join('payment_statuses', 'payment_statuses.id', '=', 'payments.payment_status_id')
->leftJoin('payment_types', 'payment_types.id', '=', 'payments.payment_type_id')
->leftJoin('account_gateways', 'account_gateways.id', '=', 'payments.account_gateway_id')
->leftJoin('gateways', 'gateways.id', '=', 'account_gateways.gateway_id')
@ -39,6 +40,8 @@ class PaymentRepository extends BaseRepository
'clients.user_id as client_user_id',
'payments.amount',
'payments.payment_date',
'payments.payment_status_id',
'payments.payment_type_id',
'invoices.public_id as invoice_public_id',
'invoices.user_id as invoice_user_id',
'invoices.invoice_number',
@ -50,8 +53,15 @@ class PaymentRepository extends BaseRepository
'payments.deleted_at',
'payments.is_deleted',
'payments.user_id',
'payments.refunded',
'payments.expiration',
'payments.last4',
'payments.email',
'payments.routing_number',
'invoices.is_deleted as invoice_is_deleted',
'gateways.name as gateway_name'
'gateways.name as gateway_name',
'gateways.id as gateway_id',
'payment_statuses.name as payment_status_name'
);
if (!\Session::get('show_trash:payment')) {
@ -85,6 +95,7 @@ class PaymentRepository extends BaseRepository
->join('clients', 'clients.id', '=', 'payments.client_id')
->join('invoices', 'invoices.id', '=', 'payments.invoice_id')
->join('contacts', 'contacts.client_id', '=', 'clients.id')
->join('payment_statuses', 'payment_statuses.id', '=', 'payments.payment_status_id')
->leftJoin('invitations', function ($join) {
$join->on('invitations.invoice_id', '=', 'invoices.id')
->on('invitations.contact_id', '=', 'contacts.id');
@ -105,13 +116,21 @@ class PaymentRepository extends BaseRepository
'clients.public_id as client_public_id',
'payments.amount',
'payments.payment_date',
'payments.payment_type_id',
'invoices.public_id as invoice_public_id',
'invoices.invoice_number',
'contacts.first_name',
'contacts.last_name',
'contacts.email',
'payment_types.name as payment_type',
'payments.account_gateway_id'
'payments.account_gateway_id',
'payments.refunded',
'payments.expiration',
'payments.last4',
'payments.email',
'payments.routing_number',
'payments.payment_status_id',
'payment_statuses.name as payment_status_name'
);
if ($filter) {
@ -199,6 +218,4 @@ class PaymentRepository extends BaseRepository
parent::restore($payment);
}
}

View File

@ -108,6 +108,19 @@ class EventServiceProvider extends ServiceProvider {
'App\Listeners\InvoiceListener@deletedPayment',
'App\Listeners\CreditListener@deletedPayment',
],
'App\Events\PaymentWasRefunded' => [
'App\Listeners\ActivityListener@refundedPayment',
'App\Listeners\InvoiceListener@refundedPayment',
'App\Listeners\CreditListener@refundedPayment',
],
'App\Events\PaymentWasVoided' => [
'App\Listeners\ActivityListener@voidedPayment',
'App\Listeners\InvoiceListener@voidedPayment',
],
'App\Events\PaymentFailed' => [
'App\Listeners\ActivityListener@failedPayment',
'App\Listeners\InvoiceListener@failedPayment',
],
'App\Events\PaymentWasRestored' => [
'App\Listeners\ActivityListener@restoredPayment',
'App\Listeners\InvoiceListener@restoredPayment',

View File

@ -44,7 +44,9 @@ class ActivityService extends BaseService
'quote' => $model->invoice ? link_to('/quotes/' . $model->invoice_public_id, $model->invoice)->toHtml() : null,
'contact' => $model->contact_id ? link_to('/clients/' . $model->client_public_id, Utils::getClientDisplayName($model))->toHtml() : Utils::getPersonDisplayName($model->user_first_name, $model->user_last_name, $model->user_email),
'payment' => $model->payment ?: '',
'credit' => Utils::formatMoney($model->credit, $model->currency_id, $model->country_id)
'credit' => $model->payment_amount ? Utils::formatMoney($model->credit, $model->currency_id, $model->country_id) : '',
'payment_amount' => $model->payment_amount ? Utils::formatMoney($model->payment_amount, $model->currency_id, $model->country_id) : null,
'adjustment' => $model->adjustment ? Utils::formatMoney($model->adjustment, $model->currency_id, $model->country_id) : null
];
return trans("texts.activity_{$model->activity_type_id}", $data);

View File

@ -30,12 +30,12 @@ class BaseService
return count($entities);
}
public function createDatatable($entityType, $query, $showCheckbox = true, $hideClient = false)
public function createDatatable($entityType, $query, $showCheckbox = true, $hideClient = false, $orderColumns = [])
{
$columns = $this->getDatatableColumns($entityType, !$showCheckbox);
$actions = $this->getDatatableActions($entityType);
return $this->datatableService->createDatatable($entityType, $query, $columns, $actions, $showCheckbox);
return $this->datatableService->createDatatable($entityType, $query, $columns, $actions, $showCheckbox, $orderColumns);
}
protected function getDatatableColumns($entityType, $hideClient)

View File

@ -7,10 +7,10 @@ use Auth;
class DatatableService
{
public function createDatatable($entityType, $query, $columns, $actions = null, $showCheckbox = true)
public function createDatatable($entityType, $query, $columns, $actions = null, $showCheckbox = true, $orderColumns = [])
{
$table = Datatable::query($query);
$orderColumns = [];
$calculateOrderColumns = empty($orderColumns);
if ($actions && $showCheckbox) {
$table->addColumn('checkbox', function ($model) {
@ -31,7 +31,9 @@ class DatatableService
if ($visible) {
$table->addColumn($field, $value);
$orderColumns[] = $field;
if ($calculateOrderColumns) {
$orderColumns[] = $field;
}
}
}

View File

@ -5,14 +5,18 @@ use Auth;
use URL;
use DateTime;
use Event;
use Cache;
use Omnipay;
use Session;
use CreditCard;
use App\Models\Payment;
use App\Models\PaymentMethod;
use App\Models\Account;
use App\Models\Country;
use App\Models\Client;
use App\Models\Invoice;
use App\Models\AccountGateway;
use App\Http\Controllers\PaymentController;
use App\Models\AccountGatewayToken;
use App\Ninja\Repositories\PaymentRepository;
use App\Ninja\Repositories\AccountRepository;
@ -24,6 +28,11 @@ class PaymentService extends BaseService
public $lastError;
protected $datatableService;
protected static $refundableGateways = array(
GATEWAY_STRIPE,
GATEWAY_BRAINTREE
);
public function __construct(PaymentRepository $paymentRepo, AccountRepository $accountRepo, DatatableService $datatableService)
{
$this->datatableService = $datatableService;
@ -39,7 +48,7 @@ class PaymentService extends BaseService
public function createGateway($accountGateway)
{
$gateway = Omnipay::create($accountGateway->gateway->provider);
$gateway->initialize((array) $accountGateway->getConfig());
$gateway->initialize((array)$accountGateway->getConfig());
if ($accountGateway->isGateway(GATEWAY_DWOLLA)) {
if ($gateway->getSandbox() && isset($_ENV['DWOLLA_SANDBOX_KEY']) && isset($_ENV['DWOLLA_SANSBOX_SECRET'])) {
@ -58,7 +67,7 @@ class PaymentService extends BaseService
{
$invoice = $invitation->invoice;
$account = $invoice->account;
$key = $invoice->account_id.'-'.$invoice->invoice_number;
$key = $invoice->account_id . '-' . $invoice->invoice_number;
$currencyCode = $invoice->client->currency ? $invoice->client->currency->code : ($invoice->account->currency ? $invoice->account->currency->code : 'USD');
if ($input) {
@ -70,7 +79,7 @@ class PaymentService extends BaseService
$data = $this->createDataForClient($invitation);
}
$card = new CreditCard($data);
$card = !empty($data['number']) ? new CreditCard($data) : null;
$data = [
'amount' => $invoice->getRequestedAmount(),
'card' => $card,
@ -86,6 +95,17 @@ class PaymentService extends BaseService
$data['ButtonSource'] = 'InvoiceNinja_SP';
};
if ($input && $accountGateway->isGateway(GATEWAY_STRIPE)) {
if (!empty($input['stripeToken'])) {
$data['token'] = $input['stripeToken'];
unset($data['card']);
} elseif (!empty($input['plaidPublicToken'])) {
$data['plaidPublicToken'] = $input['plaidPublicToken'];
$data['plaidAccountId'] = $input['plaidAccountId'];
unset($data['card']);
}
}
return $data;
}
@ -155,14 +175,212 @@ class PaymentService extends BaseService
];
}
public function createToken($gateway, $details, $accountGateway, $client, $contactId)
public function getClientPaymentMethods($client)
{
$tokenResponse = $gateway->createCard($details)->send();
$cardReference = $tokenResponse->getCustomerReference();
$token = $client->getGatewayToken($accountGateway/* return parameter */, $accountGatewayToken/* return parameter */);
if (!$token) {
return null;
}
if ($cardReference) {
if (!$accountGatewayToken->uses_local_payment_methods && $accountGateway->gateway_id == GATEWAY_STRIPE) {
// Migrate Stripe data
$gateway = $this->createGateway($accountGateway);
$response = $gateway->fetchCustomer(array('customerReference' => $token))->send();
if (!$response->isSuccessful()) {
return null;
}
$data = $response->getData();
$sources = isset($data['sources']) ? $data['sources']['data'] : $data['cards']['data'];
// Load
$accountGatewayToken->payment_methods();
foreach ($sources as $source) {
$paymentMethod = $this->convertPaymentMethodFromStripe($source, $accountGatewayToken);
if ($paymentMethod) {
$paymentMethod->save();
}
if ($data['default_source'] == $source['id']) {
$accountGatewayToken->default_payment_method_id = $paymentMethod->id;
}
}
$accountGatewayToken->uses_local_payment_methods = true;
$accountGatewayToken->save();
}
return $accountGatewayToken->payment_methods;
}
public function verifyClientPaymentMethod($client, $publicId, $amount1, $amount2)
{
$token = $client->getGatewayToken($accountGateway);
if ($accountGateway->gateway_id != GATEWAY_STRIPE) {
return 'Unsupported gateway';
}
$paymentMethod = PaymentMethod::scope($publicId, $client->account_id, $accountGatewayToken->id)->firstOrFail();
// Omnipay doesn't support verifying payment methods
// Also, it doesn't want to urlencode without putting numbers inside the brackets
$result = $this->makeStripeCall(
$accountGateway,
'POST',
'customers/' . $token . '/sources/' . $paymentMethod->source_reference . '/verify',
'amounts[]=' . intval($amount1) . '&amounts[]=' . intval($amount2)
);
if (!is_string($result)) {
$paymentMethod->status = PAYMENT_METHOD_STATUS_VERIFIED;
$paymentMethod->save();
if (!$paymentMethod->account_gateway_token->default_payment_method_id) {
$paymentMethod->account_gateway_token->default_payment_method_id = $paymentMethod->id;
$paymentMethod->account_gateway_token->save();
}
}
return true;
}
public function removeClientPaymentMethod($client, $publicId)
{
$token = $client->getGatewayToken($accountGateway/* return parameter */, $accountGatewayToken/* return parameter */);
if (!$token) {
return null;
}
$paymentMethod = PaymentMethod::scope($publicId, $client->account_id, $accountGatewayToken->id)->firstOrFail();
$gateway = $this->createGateway($accountGateway);
if ($accountGateway->gateway_id == GATEWAY_STRIPE) {
$response = $gateway->deleteCard(array('customerReference' => $token, 'cardReference' => $paymentMethod->source_reference))->send();
if (!$response->isSuccessful()) {
return $response->getMessage();
}
} elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) {
$response = $gateway->deletePaymentMethod(array('token' => $paymentMethod->source_reference))->send();
if (!$response->isSuccessful()) {
return $response->getMessage();
}
}
$paymentMethod->delete();
return true;
}
public function setClientDefaultPaymentMethod($client, $publicId)
{
$token = $client->getGatewayToken($accountGateway/* return parameter */, $accountGatewayToken/* return parameter */);
if (!$token) {
return null;
}
$paymentMethod = PaymentMethod::scope($publicId, $client->account_id, $accountGatewayToken->id)->firstOrFail();
$paymentMethod->account_gateway_token->default_payment_method_id = $paymentMethod->id;
$paymentMethod->account_gateway_token->save();
return true;
}
public function createToken($gateway, $details, $accountGateway, $client, $contactId, &$customerReference = null, &$paymentMethod = null)
{
$customerReference = $client->getGatewayToken($accountGateway, $accountGatewayToken/* return paramenter */);
if ($customerReference) {
$details['customerReference'] = $customerReference;
if ($accountGateway->gateway->id == GATEWAY_STRIPE) {
$customerResponse = $gateway->fetchCustomer(array('customerReference' => $customerReference))->send();
if (!$customerResponse->isSuccessful()) {
$customerReference = null; // The customer might not exist anymore
}
} elseif ($accountGateway->gateway->id == GATEWAY_BRAINTREE) {
$customer = $gateway->findCustomer($customerReference)->send()->getData();
if (!($customer instanceof \Braintree\Customer)) {
$customerReference = null; // The customer might not exist anymore
}
}
}
if ($accountGateway->gateway->id == GATEWAY_STRIPE) {
if (!empty($details['plaidPublicToken'])) {
$plaidResult = $this->getPlaidToken($accountGateway, $details['plaidPublicToken'], $details['plaidAccountId']);
if (is_string($plaidResult)) {
$this->lastError = $plaidResult;
return;
} elseif (!$plaidResult) {
$this->lastError = 'No token received from Plaid';
return;
}
unset($details['plaidPublicToken']);
unset($details['plaidAccountId']);
$details['token'] = $plaidResult['stripe_bank_account_token'];
}
$tokenResponse = $gateway->createCard($details)->send();
if ($tokenResponse->isSuccessful()) {
$sourceReference = $tokenResponse->getCardReference();
if (!$customerReference) {
$customerReference = $tokenResponse->getCustomerReference();
}
if (!$sourceReference) {
$responseData = $tokenResponse->getData();
if (!empty($responseData['object']) && ($responseData['object'] == 'bank_account' || $responseData['object'] == 'card')) {
$sourceReference = $responseData['id'];
}
}
if ($customerReference == $sourceReference) {
// This customer was just created; find the card
$data = $tokenResponse->getData();
if (!empty($data['default_source'])) {
$sourceReference = $data['default_source'];
}
}
} else {
$data = $tokenResponse->getData();
if ($data && $data['error'] && $data['error']['type'] == 'invalid_request_error') {
$this->lastError = $data['error']['message'];
return;
}
}
} elseif ($accountGateway->gateway->id == GATEWAY_BRAINTREE) {
if (!$customerReference) {
$tokenResponse = $gateway->createCustomer(array('customerData' => array()))->send();
if ($tokenResponse->isSuccessful()) {
$customerReference = $tokenResponse->getCustomerData()->id;
} else {
$this->lastError = $tokenResponse->getData()->message;
return;
}
}
if ($customerReference) {
$details['customerId'] = $customerReference;
$tokenResponse = $gateway->createPaymentMethod($details)->send();
if ($tokenResponse->isSuccessful()) {
$sourceReference = $tokenResponse->getData()->paymentMethod->token;
} else {
$this->lastError = $tokenResponse->getData()->message;
return;
}
}
}
if ($customerReference) {
$token = AccountGatewayToken::where('client_id', '=', $client->id)
->where('account_gateway_id', '=', $accountGateway->id)->first();
->where('account_gateway_id', '=', $accountGateway->id)->first();
if (!$token) {
$token = new AccountGatewayToken();
@ -172,13 +390,120 @@ class PaymentService extends BaseService
$token->client_id = $client->id;
}
$token->token = $cardReference;
$token->token = $customerReference;
$token->save();
$paymentMethod = $this->createPaymentMethodFromGatewayResponse($tokenResponse, $accountGateway, $accountGatewayToken, $contactId);
} else {
$this->lastError = $tokenResponse->getMessage();
}
return $cardReference;
return $sourceReference;
}
public function convertPaymentMethodFromStripe($source, $accountGatewayToken = null, $paymentMethod = null) {
// Creating a new one or updating an existing one
if (!$paymentMethod) {
$paymentMethod = $accountGatewayToken ? PaymentMethod::createNew($accountGatewayToken) : new PaymentMethod();
}
$paymentMethod->last4 = $source['last4'];
$paymentMethod->source_reference = $source['id'];
if ($source['object'] == 'bank_account') {
$paymentMethod->routing_number = $source['routing_number'];
$paymentMethod->payment_type_id = PAYMENT_TYPE_ACH;
$paymentMethod->status = $source['status'];
$currency = Cache::get('currencies')->where('code', strtoupper($source['currency']))->first();
if ($currency) {
$paymentMethod->currency_id = $currency->id;
$paymentMethod->setRelation('currency', $currency);
}
} elseif ($source['object'] == 'card') {
$paymentMethod->expiration = $source['exp_year'] . '-' . $source['exp_month'] . '-00';
$paymentMethod->payment_type_id = $this->parseCardType($source['brand']);
} else {
return null;
}
$paymentMethod->setRelation('payment_type', Cache::get('paymentTypes')->find($paymentMethod->payment_type_id));
return $paymentMethod;
}
public function convertPaymentMethodFromBraintree($source, $accountGatewayToken = null, $paymentMethod = null) {
// Creating a new one or updating an existing one
if (!$paymentMethod) {
$paymentMethod = $accountGatewayToken ? PaymentMethod::createNew($accountGatewayToken) : new PaymentMethod();
}
if ($source instanceof \Braintree\CreditCard) {
$paymentMethod->payment_type_id = $this->parseCardType($source->cardType);
$paymentMethod->last4 = $source->last4;
$paymentMethod->expiration = $source->expirationYear . '-' . $source->expirationMonth . '-00';
} elseif ($source instanceof \Braintree\PayPalAccount) {
$paymentMethod->email = $source->email;
$paymentMethod->payment_type_id = PAYMENT_TYPE_ID_PAYPAL;
} else {
return null;
}
$paymentMethod->setRelation('payment_type', Cache::get('paymentTypes')->find($paymentMethod->payment_type_id));
$paymentMethod->source_reference = $source->token;
return $paymentMethod;
}
public function createPaymentMethodFromGatewayResponse($gatewayResponse, $accountGateway, $accountGatewayToken = null, $contactId = null) {
if ($accountGateway->gateway_id == GATEWAY_STRIPE) {
$data = $gatewayResponse->getData();
if(!empty($data['object']) && ($data['object'] == 'card' || $data['object'] == 'bank_account')) {
$source = $data;
} else {
$source = !empty($data['source']) ? $data['source'] : $data['card'];
}
if ($source) {
$paymentMethod = $this->convertPaymentMethodFromStripe($source, $accountGatewayToken);
}
} elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) {
$data = $gatewayResponse->getData();
if (!empty($data->transaction)) {
$transaction = $data->transaction;
$paymentMethod = $accountGatewayToken ? PaymentMethod::createNew($accountGatewayToken) : new PaymentMethod();
if ($transaction->paymentInstrumentType == 'credit_card') {
$card = $transaction->creditCardDetails;
$paymentMethod->last4 = $card->last4;
$paymentMethod->expiration = $card->expirationYear . '-' . $card->expirationMonth . '-00';
$paymentMethod->payment_type_id = $this->parseCardType($card->cardType);
} elseif ($transaction->paymentInstrumentType == 'paypal_account') {
$paymentMethod->payment_type_id = PAYMENT_TYPE_ID_PAYPAL;
$paymentMethod->email = $transaction->paypalDetails->payerEmail;
}
$paymentMethod->setRelation('payment_type', Cache::get('paymentTypes')->find($paymentMethod->payment_type_id));
} elseif (!empty($data->paymentMethod)) {
$paymentMethod = $this->convertPaymentMethodFromBraintree($data->paymentMethod, $accountGatewayToken);
}
}
if (!empty($paymentMethod) && $accountGatewayToken && $contactId) {
$paymentMethod->account_gateway_token_id = $accountGatewayToken->id;
$paymentMethod->account_id = $accountGatewayToken->account_id;
$paymentMethod->contact_id = $contactId;
$paymentMethod->save();
if (!$paymentMethod->account_gateway_token->default_payment_method_id) {
$paymentMethod->account_gateway_token->default_payment_method_id = $paymentMethod->id;
$paymentMethod->account_gateway_token->save();
}
}
return $paymentMethod;
}
public function getCheckoutComToken($invitation)
@ -205,7 +530,19 @@ class PaymentService extends BaseService
return $token;
}
public function createPayment($invitation, $accountGateway, $ref, $payerId = null)
public function getBraintreeClientToken($account)
{
$token = false;
$accountGateway = $account->getGatewayConfig(GATEWAY_BRAINTREE);
$gateway = $this->createGateway($accountGateway);
$token = $gateway->clientToken()->send()->getToken();
return $token;
}
public function createPayment($invitation, $accountGateway, $ref, $payerId = null, $paymentDetails = null, $paymentMethod = null, $purchaseResponse = null)
{
$invoice = $invitation->invoice;
@ -219,10 +556,57 @@ class PaymentService extends BaseService
$payment->transaction_reference = $ref;
$payment->payment_date = date_create()->format('Y-m-d');
if (!empty($paymentDetails['card'])) {
$card = $paymentDetails['card'];
$payment->last4 = substr($card->number, -4);
$year = $card->expiryYear;
if (strlen($year) == 2) {
$year = '20' . $year;
}
$payment->expiration = $year . '-' . $card->expiryMonth . '-00';
$payment->payment_type_id = $this->detectCardType($card->number);
}
if ($accountGateway->gateway_id == GATEWAY_STRIPE) {
$data = $purchaseResponse->getData();
$source = !empty($data['source'])?$data['source']:$data['card'];
$payment->payment_status_id = $data['status'] == 'succeeded' ? PAYMENT_STATUS_COMPLETED : PAYMENT_STATUS_PENDING;
if ($source) {
$payment->last4 = $source['last4'];
if ($source['object'] == 'bank_account') {
$payment->routing_number = $source['routing_number'];
$payment->payment_type_id = PAYMENT_TYPE_ACH;
}
else{
$payment->expiration = $source['exp_year'] . '-' . $source['exp_month'] . '-00';
$payment->payment_type_id = $this->parseCardType($source['brand']);
}
}
} elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) {
$transaction = $purchaseResponse->getData()->transaction;
if ($transaction->paymentInstrumentType == 'credit_card') {
$card = $transaction->creditCardDetails;
$payment->last4 = $card->last4;
$payment->expiration = $card->expirationYear . '-' . $card->expirationMonth . '-00';
$payment->payment_type_id = $this->parseCardType($card->cardType);
} elseif ($transaction->paymentInstrumentType == 'paypal_account') {
$payment->payment_type_id = PAYMENT_TYPE_ID_PAYPAL;
$payment->email = $transaction->paypalDetails->payerEmail;
}
}
if ($payerId) {
$payment->payer_id = $payerId;
}
if ($paymentMethod) {
$payment->payment_method_id = $paymentMethod->id;
}
$payment->save();
// enable pro plan for hosted users
@ -281,6 +665,47 @@ class PaymentService extends BaseService
return $payment;
}
private function parseCardType($cardName) {
$cardTypes = array(
'Visa' => PAYMENT_TYPE_VISA,
'American Express' => PAYMENT_TYPE_AMERICAN_EXPRESS,
'MasterCard' => PAYMENT_TYPE_MASTERCARD,
'Discover' => PAYMENT_TYPE_DISCOVER,
'JCB' => PAYMENT_TYPE_JCB,
'Diners Club' => PAYMENT_TYPE_DINERS,
'Carte Blanche' => PAYMENT_TYPE_CARTE_BLANCHE,
'China UnionPay' => PAYMENT_TYPE_UNIONPAY,
'Laser' => PAYMENT_TYPE_LASER,
'Maestro' => PAYMENT_TYPE_MAESTRO,
'Solo' => PAYMENT_TYPE_SOLO,
'Switch' => PAYMENT_TYPE_SWITCH
);
if (!empty($cardTypes[$cardName])) {
return $cardTypes[$cardName];
} else {
return PAYMENT_TYPE_CREDIT_CARD_OTHER;
}
}
private function detectCardType($number)
{
if (preg_match('/^3[47][0-9]{13}$/',$number)) {
return PAYMENT_TYPE_AMERICAN_EXPRESS;
} elseif (preg_match('/^3(?:0[0-5]|[68][0-9])[0-9]{11}$/',$number)) {
return PAYMENT_TYPE_DINERS;
} elseif (preg_match('/^6(?:011|5[0-9][0-9])[0-9]{12}$/',$number)) {
return PAYMENT_TYPE_DISCOVER;
} elseif (preg_match('/^(?:2131|1800|35\d{3})\d{11}$/',$number)) {
return PAYMENT_TYPE_JCB;
} elseif (preg_match('/^5[1-5][0-9]{14}$/',$number)) {
return PAYMENT_TYPE_MASTERCARD;
} elseif (preg_match('/^4[0-9]{12}(?:[0-9]{3})?$/',$number)) {
return PAYMENT_TYPE_VISA;
}
return PAYMENT_TYPE_CREDIT_CARD_OTHER;
}
public function completePurchase($gateway, $accountGateway, $details, $token)
{
if ($accountGateway->isGateway(GATEWAY_MOLLIE)) {
@ -296,12 +721,15 @@ class PaymentService extends BaseService
public function autoBillInvoice($invoice)
{
$client = $invoice->client;
$account = $invoice->account;
$invitation = $invoice->invitations->first();
$accountGateway = $account->getGatewayConfig(GATEWAY_STRIPE);
$token = $client->getGatewayToken();
if (!$invitation || !$accountGateway || !$token) {
// Make sure we've migrated in data from Stripe
$this->getClientPaymentMethods($client);
$invitation = $invoice->invitations->first();
$token = $client->getGatewayToken($accountGateway/* return parameter */, $accountGatewayToken/* return parameter */);
$defaultPaymentMethod = $accountGatewayToken->default_payment_method;
if (!$invitation || !$token || !$defaultPaymentMethod) {
return false;
}
@ -310,12 +738,18 @@ class PaymentService extends BaseService
$details = $this->getPaymentDetails($invitation, $accountGateway);
$details['customerReference'] = $token;
if ($accountGateway->gateway_id == GATEWAY_STRIPE) {
$details['cardReference'] = $defaultPaymentMethod->source_reference;
} elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) {
$details['paymentMethodToken'] = $defaultPaymentMethod->source_reference;
}
// submit purchase/get response
$response = $gateway->purchase($details)->send();
if ($response->isSuccessful()) {
$ref = $response->getTransactionReference();
return $this->createPayment($invitation, $accountGateway, $ref);
return $this->createPayment($invitation, $accountGateway, $ref, null, $details, $defaultPaymentMethod, $response);
} else {
return false;
}
@ -329,7 +763,8 @@ class PaymentService extends BaseService
$query->where('payments.user_id', '=', Auth::user()->id);
}
return $this->createDatatable(ENTITY_PAYMENT, $query, !$clientPublicId);
return $this->createDatatable(ENTITY_PAYMENT, $query, !$clientPublicId, false,
['invoice_number', 'transaction_reference', 'payment_type', 'amount', 'payment_date']);
}
protected function getDatatableColumns($entityType, $hideClient)
@ -365,7 +800,29 @@ class PaymentService extends BaseService
[
'payment_type',
function ($model) {
return $model->payment_type ? $model->payment_type : ($model->account_gateway_id ? $model->gateway_name : '');
return ($model->payment_type && !$model->last4) ? $model->payment_type : ($model->account_gateway_id ? $model->gateway_name : '');
}
],
[
'source',
function ($model) {
$code = str_replace(' ', '', strtolower($model->payment_type));
$card_type = trans("texts.card_" . $code);
if ($model->payment_type_id != PAYMENT_TYPE_ACH) {
if($model->last4) {
$expiration = trans('texts.card_expiration', array('expires' => Utils::fromSqlDate($model->expiration, false)->format('m/y')));
return '<img height="22" src="' . URL::to('/images/credit_cards/' . $code . '.png') . '" alt="' . htmlentities($card_type) . '">&nbsp; &bull;&bull;&bull;' . $model->last4 . ' ' . $expiration;
} elseif ($model->email) {
return $model->email;
}
} elseif ($model->last4) {
$bankData = PaymentMethod::lookupBankData($model->routing_number);
if (is_object($bankData)) {
return $bankData->name.'&nbsp; &bull;&bull;&bull;' . $model->last4;
} elseif($model->last4) {
return '<img height="22" src="' . URL::to('/images/credit_cards/ach.png') . '" alt="' . htmlentities($card_type) . '">&nbsp; &bull;&bull;&bull;' . $model->last4;
}
}
}
],
[
@ -379,6 +836,12 @@ class PaymentService extends BaseService
function ($model) {
return Utils::dateToString($model->payment_date);
}
],
[
'payment_status_name',
function ($model) use ($entityType) {
return self::getStatusLabel($entityType, $model);
}
]
];
}
@ -394,9 +857,226 @@ class PaymentService extends BaseService
function ($model) {
return Auth::user()->can('editByOwner', [ENTITY_PAYMENT, $model->user_id]);
}
],
[
trans('texts.refund_payment'),
function ($model) {
$max_refund = number_format($model->amount - $model->refunded, 2);
$formatted = Utils::formatMoney($max_refund, $model->currency_id, $model->country_id);
$symbol = Utils::getFromCache($model->currency_id, 'currencies')->symbol;
return "javascript:showRefundModal({$model->public_id}, '{$max_refund}', '{$formatted}', '{$symbol}')";
},
function ($model) {
return Auth::user()->can('editByOwner', [ENTITY_PAYMENT, $model->user_id]) && $model->payment_status_id >= PAYMENT_STATUS_COMPLETED &&
$model->refunded < $model->amount &&
(
($model->transaction_reference && in_array($model->gateway_id , static::$refundableGateways))
|| $model->payment_type_id == PAYMENT_TYPE_CREDIT
);
}
]
];
}
public function bulk($ids, $action, $params = array())
{
if ($action == 'refund') {
if ( ! $ids ) {
return 0;
}
$payments = $this->getRepo()->findByPublicIdsWithTrashed($ids);
$successful = 0;
foreach ($payments as $payment) {
if(Auth::user()->can('edit', $payment)){
$amount = !empty($params['amount']) ? floatval($params['amount']) : null;
if ($this->refund($payment, $amount)) {
$successful++;
}
}
}
return $successful;
} else {
return parent::bulk($ids, $action);
}
}
private function getStatusLabel($entityType, $model)
{
$label = trans("texts.status_" . strtolower($model->payment_status_name));
$class = 'default';
switch ($model->payment_status_id) {
case PAYMENT_STATUS_PENDING:
$class = 'info';
break;
case PAYMENT_STATUS_COMPLETED:
$class = 'success';
break;
case PAYMENT_STATUS_FAILED:
$class = 'danger';
break;
case PAYMENT_STATUS_PARTIALLY_REFUNDED:
$label = trans('texts.status_partially_refunded_amount', [
'amount' => Utils::formatMoney($model->refunded, $model->currency_id, $model->country_id),
]);
$class = 'primary';
break;
case PAYMENT_STATUS_VOIDED:
case PAYMENT_STATUS_REFUNDED:
$class = 'default';
break;
}
return "<h4><div class=\"label label-{$class}\">$label</div></h4>";
}
public function refund($payment, $amount = null) {
if (!$amount) {
$amount = $payment->amount;
}
$amount = min($amount, $payment->amount - $payment->refunded);
$accountGateway = $payment->account_gateway;
if (!$accountGateway) {
$accountGateway = AccountGateway::withTrashed()->find($payment->account_gateway_id);
}
if (!$amount || !$accountGateway) {
return;
}
if ($payment->payment_type_id != PAYMENT_TYPE_CREDIT) {
$gateway = $this->createGateway($accountGateway);
$refund = $gateway->refund(array(
'transactionReference' => $payment->transaction_reference,
'amount' => $amount,
));
$response = $refund->send();
if ($response->isSuccessful()) {
$payment->recordRefund($amount);
} else {
$data = $response->getData();
if ($data instanceof \Braintree\Result\Error) {
$error = $data->errors->deepAll()[0];
if ($error && $error->code == 91506) {
if ($amount == $payment->amount) {
// This is an unsettled transaction; try to void it
$void = $gateway->void(array(
'transactionReference' => $payment->transaction_reference,
));
$response = $void->send();
if ($response->isSuccessful()) {
$payment->markVoided();
}
} else {
$this->error('Unknown', 'Partial refund not allowed for unsettled transactions.', $accountGateway);
return false;
}
}
}
if (!$response->isSuccessful()) {
$this->error('Unknown', $response->getMessage(), $accountGateway);
return false;
}
}
} else {
$payment->recordRefund($amount);
}
return true;
}
private function error($type, $error, $accountGateway = false, $exception = false)
{
$message = '';
if ($accountGateway && $accountGateway->gateway) {
$message = $accountGateway->gateway->name . ': ';
}
$message .= $error ?: trans('texts.payment_error');
Session::flash('error', $message);
Utils::logError("Payment Error [{$type}]: " . ($exception ? Utils::getErrorString($exception) : $message), 'PHP', true);
}
public function makeStripeCall($accountGateway, $method, $url, $body = null) {
$apiKey = $accountGateway->getConfig()->apiKey;
if (!$apiKey) {
return 'No API key set';
}
try{
$options = [
'headers' => ['content-type' => 'application/x-www-form-urlencoded'],
'auth' => [$accountGateway->getConfig()->apiKey,''],
];
if ($body) {
$options['body'] = $body;
}
$response = (new \GuzzleHttp\Client(['base_uri'=>'https://api.stripe.com/v1/']))->request(
$method,
$url,
$options
);
return json_decode($response->getBody(), true);
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
$response = $e->getResponse();
$body = json_decode($response->getBody(), true);
if ($body && $body['error'] && $body['error']['type'] == 'invalid_request_error') {
return $body['error']['message'];
}
return $e->getMessage();
}
}
private function getPlaidToken($accountGateway, $publicToken, $accountId) {
$clientId = $accountGateway->getPlaidClientId();
$secret = $accountGateway->getPlaidSecret();
if (!$clientId) {
return 'No client ID set';
}
if (!$secret) {
return 'No secret set';
}
try{
$subdomain = $accountGateway->getPlaidEnvironment() == 'production' ? 'api' : 'tartan';
$response = (new \GuzzleHttp\Client(['base_uri'=>"https://{$subdomain}.plaid.com"]))->request(
'POST',
'exchange_token',
[
'allow_redirects' => false,
'headers' => ['content-type' => 'application/x-www-form-urlencoded'],
'body' => http_build_query(array(
'client_id' => $clientId,
'secret' => $secret,
'public_token' => $publicToken,
'account_id' => $accountId,
))
]
);
return json_decode($response->getBody(), true);
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
$response = $e->getResponse();
$body = json_decode($response->getBody(), true);
if ($body && !empty($body['message'])) {
return $body['message'];
}
return $e->getMessage();
}
}
}

View File

@ -17,7 +17,7 @@
"omnipay/mollie": "dev-master#22956c1a62a9662afa5f5d119723b413770ac525",
"omnipay/2checkout": "dev-master#e9c079c2dde0d7ba461903b3b7bd5caf6dee1248",
"omnipay/gocardless": "dev-master",
"omnipay/stripe": "2.3.0",
"omnipay/stripe": "dev-master",
"laravel/framework": "5.2.*",
"laravelcollective/html": "5.2.*",
"laravelcollective/bus": "5.2.*",
@ -72,7 +72,9 @@
"asgrim/ofxparser": "^1.1",
"league/flysystem-aws-s3-v3": "~1.0",
"league/flysystem-rackspace": "~1.0",
"barracudanetworks/archivestream-php": "^1.0"
"barracudanetworks/archivestream-php": "^1.0",
"omnipay/braintree": "~2.0@dev",
"gatepay/FedACHdir": "dev-master@dev"
},
"require-dev": {
"phpunit/phpunit": "~4.0",
@ -121,5 +123,23 @@
},
"config": {
"preferred-install": "dist"
}
},
"repositories": [
{
"type": "package",
"package": {
"name": "gatepay/FedACHdir",
"version": "dev-master",
"dist": {
"url": "https://github.com/gatepay/FedACHdir/archive/master.zip",
"type": "zip"
},
"source": {
"url": "git@github.com:gatepay/FedACHdir.git",
"type": "git",
"reference": "origin/master"
}
}
}
]
}

146
composer.lock generated
View File

@ -4,8 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "cf82d2ddb25cb1a7d6b4867bcc8692b8",
"content-hash": "481a95753b873249aebceb99e7426421",
"hash": "7139e4aedb2ac151079c50ee5c17f93c",
"content-hash": "a314d6c0a16785dd2395a7fd73cdc76d",
"packages": [
{
"name": "agmscode/omnipay-agms",
@ -127,7 +127,7 @@
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/formers/former/zipball/78ae8c65b1f8134e2db1c9491c251c03638823ca",
"url": "https://api.github.com/repos/formers/former/zipball/37f6876a5d211427b5c445cd64f0eb637f42f685",
"reference": "d97f907741323b390f43954a90a227921ecc6b96",
"shasum": ""
},
@ -558,6 +558,53 @@
],
"time": "2016-03-03 14:38:04"
},
{
"name": "braintree/braintree_php",
"version": "3.11.0",
"source": {
"type": "git",
"url": "https://github.com/braintree/braintree_php.git",
"reference": "2ab7e41d8d31286fa64bae7279a8e785a40b1be4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/braintree/braintree_php/zipball/2ab7e41d8d31286fa64bae7279a8e785a40b1be4",
"reference": "2ab7e41d8d31286fa64bae7279a8e785a40b1be4",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-dom": "*",
"ext-hash": "*",
"ext-openssl": "*",
"ext-xmlwriter": "*",
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "3.7.*"
},
"type": "library",
"autoload": {
"psr-0": {
"Braintree": "lib/"
},
"psr-4": {
"Braintree\\": "lib/Braintree"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Braintree",
"homepage": "http://www.braintreepayments.com"
}
],
"description": "Braintree PHP Client Library",
"time": "2016-04-12 20:39:36"
},
{
"name": "cardgate/omnipay-cardgate",
"version": "v2.0.0",
@ -699,7 +746,7 @@
"laravel"
],
"abandoned": "OpenSkill/Datatable",
"time": "2015-04-29 07:00:36"
"time": "2015-11-23 21:33:41"
},
{
"name": "classpreloader/classpreloader",
@ -1964,6 +2011,17 @@
],
"time": "2015-01-16 08:41:13"
},
{
"name": "gatepay/FedACHdir",
"version": "dev-master",
"source": {
"type": "git",
"url": "git@github.com:gatepay/FedACHdir.git",
"reference": "origin/master"
},
"type": "library",
"time": "2016-04-29 12:01:22"
},
{
"name": "guzzle/guzzle",
"version": "v3.8.1",
@ -2338,7 +2396,7 @@
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Intervention/image/zipball/e368d262887dbb2fdfaf710880571ede51e9c0e6",
"url": "https://api.github.com/repos/Intervention/image/zipball/22088b04728a039bd1fc32f7e79a89a118b78698",
"reference": "e368d262887dbb2fdfaf710880571ede51e9c0e6",
"shasum": ""
},
@ -4277,6 +4335,69 @@
],
"time": "2016-03-10 03:16:04"
},
{
"name": "omnipay/braintree",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/omnipay-braintree.git",
"reference": "e4b4027c6a9e6443833490d0d51fd530f0a19f62"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/omnipay-braintree/zipball/e4b4027c6a9e6443833490d0d51fd530f0a19f62",
"reference": "e4b4027c6a9e6443833490d0d51fd530f0a19f62",
"shasum": ""
},
"require": {
"braintree/braintree_php": "^2.39|^3.0",
"omnipay/common": "~2.0"
},
"require-dev": {
"omnipay/tests": "~2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Omnipay\\Braintree\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Barry vd. Heuvel",
"email": "barryvdh@gmail.com"
},
{
"name": "Kayla Daniels",
"email": "kayladnls@gmail.com"
},
{
"name": "Omnipay Contributors",
"homepage": "https://github.com/thephpleague/omnipay-braintree/contributors"
}
],
"description": "Braintree gateway for Omnipay payment processing library",
"homepage": "https://github.com/thephpleague/omnipay-braintree",
"keywords": [
"braintree",
"gateway",
"merchant",
"omnipay",
"pay",
"payment",
"purchase"
],
"time": "2016-02-25 20:54:09"
},
{
"name": "omnipay/buckaroo",
"version": "v2.0.1",
@ -5661,16 +5782,16 @@
},
{
"name": "omnipay/stripe",
"version": "v2.3.0",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/omnipay-stripe.git",
"reference": "54b816a5e95e34c988d71fb805b0232cfd7c1ce5"
"reference": "0ea7a647ee01e29c152814e11c2ea6307e5b0db9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/omnipay-stripe/zipball/54b816a5e95e34c988d71fb805b0232cfd7c1ce5",
"reference": "54b816a5e95e34c988d71fb805b0232cfd7c1ce5",
"url": "https://api.github.com/repos/thephpleague/omnipay-stripe/zipball/0ea7a647ee01e29c152814e11c2ea6307e5b0db9",
"reference": "0ea7a647ee01e29c152814e11c2ea6307e5b0db9",
"shasum": ""
},
"require": {
@ -5714,7 +5835,7 @@
"payment",
"stripe"
],
"time": "2015-11-10 16:17:35"
"time": "2016-04-26 08:34:50"
},
{
"name": "omnipay/targetpay",
@ -9875,6 +9996,7 @@
"omnipay/mollie": 20,
"omnipay/2checkout": 20,
"omnipay/gocardless": 20,
"omnipay/stripe": 20,
"anahkiasen/former": 20,
"chumper/datatable": 20,
"intervention/image": 20,
@ -9894,7 +10016,9 @@
"meebio/omnipay-secure-trading": 20,
"labs7in0/omnipay-wechat": 20,
"laracasts/presenter": 20,
"jlapp/swaggervel": 20
"jlapp/swaggervel": 20,
"omnipay/braintree": 20,
"gatepay/fedachdir": 20
},
"prefer-stable": false,
"prefer-lowest": false,

View File

@ -0,0 +1,145 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class PaymentsChanges extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::dropIfExists('payment_statuses');
Schema::create('payment_statuses', function($table)
{
$table->increments('id');
$table->string('name');
});
(new \PaymentStatusSeeder())->run();
Schema::dropIfExists('payment_methods');
Schema::create('payment_methods', function($table)
{
$table->increments('id');
$table->unsignedInteger('account_id');
$table->unsignedInteger('contact_id')->nullable();
$table->unsignedInteger('account_gateway_token_id');
$table->unsignedInteger('payment_type_id');
$table->string('source_reference');
$table->unsignedInteger('routing_number')->nullable();
$table->smallInteger('last4')->unsigned()->nullable();
$table->date('expiration')->nullable();
$table->string('email')->nullable();
$table->unsignedInteger('currency_id')->nullable();
$table->string('status')->nullable();
$table->timestamps();
$table->softDeletes();
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
$table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade');
$table->foreign('account_gateway_token_id')->references('id')->on('account_gateway_tokens');
$table->foreign('payment_type_id')->references('id')->on('payment_types');
$table->foreign('currency_id')->references('id')->on('currencies');
$table->unsignedInteger('public_id')->index();
$table->unique( array('account_id','public_id') );
});
Schema::table('payments', function($table)
{
$table->decimal('refunded', 13, 2);
$table->unsignedInteger('payment_status_id')->default(PAYMENT_STATUS_COMPLETED);
$table->foreign('payment_status_id')->references('id')->on('payment_statuses');
$table->unsignedInteger('routing_number')->nullable();
$table->smallInteger('last4')->unsigned()->nullable();
$table->date('expiration')->nullable();
$table->text('gateway_error')->nullable();
$table->string('email')->nullable();
$table->unsignedInteger('payment_method_id')->nullable();
$table->foreign('payment_method_id')->references('id')->on('payment_methods');
});
Schema::table('invoices', function($table)
{
$table->boolean('client_enable_auto_bill')->default(false);
});
\DB::table('invoices')
->where('auto_bill', '=', 1)
->update(array('client_enable_auto_bill' => 1, 'auto_bill' => AUTO_BILL_OPT_OUT));
Schema::table('account_gateway_tokens', function($table)
{
$table->unsignedInteger('default_payment_method_id')->nullable();
$table->foreign('default_payment_method_id')->references('id')->on('payment_methods');
$table->boolean('uses_local_payment_methods')->defalut(true);
});
\DB::table('account_gateway_tokens')->update(array('uses_local_payment_methods' => false));
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('payments', function($table)
{
$table->dropColumn('refunded');
$table->dropForeign('payments_payment_status_id_foreign');
$table->dropColumn('payment_status_id');
$table->dropColumn('routing_number');
$table->dropColumn('last4');
$table->dropColumn('expiration');
$table->dropColumn('gateway_error');
$table->dropColumn('email');
$table->dropForeign('payments_payment_method_id_foreign');
$table->dropColumn('payment_method_id');
});
\DB::table('invoices')
->where(function($query){
$query->where('auto_bill', '=', AUTO_BILL_ALWAYS);
$query->orwhere(function($query){
$query->where('auto_bill', '!=', AUTO_BILL_OFF);
$query->where('client_enable_auto_bill', '=', 1);
});
})
->update(array('auto_bill' => 1));
\DB::table('invoices')
->where('auto_bill', '!=', 1)
->update(array('auto_bill' => 0));
Schema::table('invoices', function ($table) {
$table->dropColumn('client_enable_auto_bill');
});
Schema::dropIfExists('payment_statuses');
Schema::table('account_gateway_tokens', function($table)
{
$table->dropForeign('account_gateway_tokens_default_payment_method_id_foreign');
$table->dropColumn('default_payment_method_id');
$table->dropColumn('uses_local_payment_methods');
});
Schema::dropIfExists('payment_methods');
}
}

View File

@ -20,23 +20,6 @@ class ConstantsSeeder extends Seeder
public function run()
{
PaymentType::create(array('name' => 'Apply Credit'));
PaymentType::create(array('name' => 'Bank Transfer'));
PaymentType::create(array('name' => 'Cash'));
PaymentType::create(array('name' => 'Debit'));
PaymentType::create(array('name' => 'ACH'));
PaymentType::create(array('name' => 'Visa Card'));
PaymentType::create(array('name' => 'MasterCard'));
PaymentType::create(array('name' => 'American Express'));
PaymentType::create(array('name' => 'Discover Card'));
PaymentType::create(array('name' => 'Diners Card'));
PaymentType::create(array('name' => 'EuroCard'));
PaymentType::create(array('name' => 'Nova'));
PaymentType::create(array('name' => 'Credit Card Other'));
PaymentType::create(array('name' => 'PayPal'));
PaymentType::create(array('name' => 'Google Wallet'));
PaymentType::create(array('name' => 'Check'));
Theme::create(array('name' => 'amelia'));
Theme::create(array('name' => 'cerulean'));
Theme::create(array('name' => 'cosmo'));

View File

@ -19,10 +19,12 @@ class DatabaseSeeder extends Seeder
$this->call('FontsSeeder');
$this->call('BanksSeeder');
$this->call('InvoiceStatusSeeder');
$this->call('PaymentStatusSeeder');
$this->call('CurrenciesSeeder');
$this->call('DateFormatsSeeder');
$this->call('InvoiceDesignsSeeder');
$this->call('PaymentTermsSeeder');
$this->call('PaymentTypesSeeder');
$this->call('LanguageSeeder');
}
}

View File

@ -48,6 +48,7 @@ class PaymentLibrariesSeeder extends Seeder
['name' => 'SecPay', 'provider' => 'SecPay', 'payment_library_id' => 1],
['name' => 'WeChat Express', 'provider' => 'WeChat_Express', 'payment_library_id' => 1],
['name' => 'WePay', 'provider' => 'WePay', 'payment_library_id' => 1],
['name' => 'Braintree', 'provider' => 'Braintree', 'payment_library_id' => 1],
];
foreach ($gateways as $gateway) {

View File

@ -0,0 +1,37 @@
<?php
use App\Models\PaymentStatus;
class PaymentStatusSeeder extends Seeder
{
public function run()
{
Eloquent::unguard();
$this->createPaymentStatuses();
Eloquent::reguard();
}
private function createPaymentStatuses()
{
$statuses = [
['id' => '1', 'name' => 'Pending'],
['id' => '2', 'name' => 'Voided'],
['id' => '3', 'name' => 'Failed'],
['id' => '4', 'name' => 'Completed'],
['id' => '5', 'name' => 'Partially Refunded'],
['id' => '6', 'name' => 'Refunded'],
];
foreach ($statuses as $status) {
$record = PaymentStatus::find($status['id']);
if ($record) {
$record->name = $status['name'];
$record->save();
} else {
PaymentStatus::create($status);
}
}
}
}

View File

@ -0,0 +1,44 @@
<?php
use App\Models\PaymentType;
class PaymentTypesSeeder extends Seeder
{
public function run()
{
Eloquent::unguard();
$paymentTypes = [
array('name' => 'Apply Credit'),
array('name' => 'Bank Transfer'),
array('name' => 'Cash'),
array('name' => 'Debit'),
array('name' => 'ACH'),
array('name' => 'Visa Card'),
array('name' => 'MasterCard'),
array('name' => 'American Express'),
array('name' => 'Discover Card'),
array('name' => 'Diners Card'),
array('name' => 'EuroCard'),
array('name' => 'Nova'),
array('name' => 'Credit Card Other'),
array('name' => 'PayPal'),
array('name' => 'Google Wallet'),
array('name' => 'Check'),
array('name' => 'Carte Blanche'),
array('name' => 'UnionPay'),
array('name' => 'JCB'),
array('name' => 'Laser'),
array('name' => 'Maestro'),
array('name' => 'Solo'),
array('name' => 'Switch'),
];
foreach ($paymentTypes as $paymentType) {
if (!DB::table('payment_types')->where('name', '=', $paymentType['name'])->get()) {
PaymentType::create($paymentType);
}
}
}
}

View File

@ -15,10 +15,12 @@ class UpdateSeeder extends Seeder
$this->call('FontsSeeder');
$this->call('BanksSeeder');
$this->call('InvoiceStatusSeeder');
$this->call('PaymentStatusSeeder');
$this->call('CurrenciesSeeder');
$this->call('DateFormatsSeeder');
$this->call('InvoiceDesignsSeeder');
$this->call('PaymentTermsSeeder');
$this->call('PaymentTypesSeeder');
$this->call('LanguageSeeder');
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" enable-background="new 0 0 30 30" xml:space="preserve">
<path id="XMLID_14_" fill="#275A76" d="M15.1,3.2l-3.2-3.3L2.5,2.3L0,11.6l3.2,3.3l-3.3,3.2l2.4,9.3l9.3,2.6l3.3-3.2l3.2,3.3
l9.3-2.4l2.6-9.3l-3.2-3.3l3.3-3.2l-2.4-9.3L18.4,0L15.1,3.2z M13.2,5l-3.1,3.1l-3.9-4l4.9-1.3L13.2,5z M15,16.9l3.1,3.1l-3.1,3.1
L11.8,20L15,16.9z M10,18.1l-3.1-3.1l3.1-3.1l3.1,3.1L10,18.1z M16.9,15l3.1-3.1l3.1,3.1L20,18.2L16.9,15z M15,13.1L11.9,10l3.1-3.1
l3.1,3.1L15,13.1z M2.9,10.9L4.3,6l3.9,4l-3.1,3.1L2.9,10.9z M5,16.8l3.1,3.1l-4,3.9l-1.3-4.9L5,16.8z M10.9,27.1L6,25.7l4-3.9
l3.1,3.1L10.9,27.1z M16.8,25l3.1-3.1l3.9,4l-4.9,1.3L16.8,25z M27.1,19.1L25.7,24l-3.9-4l3.1-3.1L27.1,19.1z M25,13.2l-3.1-3.1
l4-3.9l1.3,4.9L25,13.2z M24,4.3l-4,3.9l-3.1-3.1l2.2-2.2L24,4.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" enable-background="new 0 0 30 30" xml:space="preserve">
<path id="XMLID_14_" fill="#FFFFFF" d="M15.1,3.2l-3.2-3.3L2.5,2.3L0,11.6l3.2,3.3l-3.3,3.2l2.4,9.3l9.3,2.6l3.3-3.2l3.2,3.3
l9.3-2.4l2.6-9.3l-3.2-3.3l3.3-3.2l-2.4-9.3L18.4,0L15.1,3.2z M13.2,5l-3.1,3.1l-3.9-4l4.9-1.3L13.2,5z M15,16.9l3.1,3.1l-3.1,3.1
L11.8,20L15,16.9z M10,18.1l-3.1-3.1l3.1-3.1l3.1,3.1L10,18.1z M16.9,15l3.1-3.1l3.1,3.1L20,18.2L16.9,15z M15,13.1L11.9,10l3.1-3.1
l3.1,3.1L15,13.1z M2.9,10.9L4.3,6l3.9,4l-3.1,3.1L2.9,10.9z M5,16.8l3.1,3.1l-4,3.9l-1.3-4.9L5,16.8z M10.9,27.1L6,25.7l4-3.9
l3.1,3.1L10.9,27.1z M16.8,25l3.1-3.1l3.9,4l-4.9,1.3L16.8,25z M27.1,19.1L25.7,24l-3.9-4l3.1-3.1L27.1,19.1z M25,13.2l-3.1-3.1
l4-3.9l1.3,4.9L25,13.2z M24,4.3l-4,3.9l-3.1-3.1l2.2-2.2L24,4.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -17,7 +17,6 @@
| loading any of our classes later on. It feels nice to relax.
|
*/
require __DIR__.'/../bootstrap/autoload.php';
/*

View File

@ -516,11 +516,11 @@ return array(
'token_billing_3' => 'Fravalg - checkboks er vist og valgt',
'token_billing_4' => 'Altid',
'token_billing_checkbox' => 'Opbevar kreditkort oplysninger',
'view_in_stripe' => 'Vis i Stripe ',
'view_in_gateway' => 'Vis i :gateway',
'use_card_on_file' => 'Brug allerede gemt kort',
'edit_payment_details' => 'Redigér betalings detaljer',
'token_billing' => 'Gem kort detaljer',
'token_billing_secure' => 'Kort data er opbevaret sikkert hos :stripe_link',
'token_billing_secure' => 'Kort data er opbevaret sikkert hos :link',
'support' => 'Kundeservice',
'contact_information' => 'Kontakt information',

View File

@ -516,11 +516,11 @@ return array(
'token_billing_3' => 'Opt-out - Kontrollkästchen wird angezeigt und ist ausgewählt',
'token_billing_4' => 'Immer',
'token_billing_checkbox' => 'Kreditkarteninformationen speichern',
'view_in_stripe' => 'Auf Stripe anzeigen',
'view_in_gateway' => 'Auf :gateway anzeigen',
'use_card_on_file' => 'Verwende gespeicherte Kreditkarte',
'edit_payment_details' => 'Zahlungsdetails bearbeiten',
'token_billing' => 'Kreditkarte merken',
'token_billing_secure' => 'Die Daten werden sicher von :stripe_link gespeichert.',
'token_billing_secure' => 'Die Daten werden sicher von :link gespeichert.',
'support' => 'Support',
'contact_information' => 'Kontakt-Informationen',

View File

@ -444,11 +444,11 @@ $LANG = array(
'token_billing_3' => 'Opt-out - checkbox is shown and selected',
'token_billing_4' => 'Always',
'token_billing_checkbox' => 'Store credit card details',
'view_in_stripe' => 'View in Stripe',
'view_in_gateway' => 'View in :gateway',
'use_card_on_file' => 'Use card on file',
'edit_payment_details' => 'Edit payment details',
'token_billing' => 'Save card details',
'token_billing_secure' => 'The data is stored securely by :stripe_link',
'token_billing_secure' => 'The data is stored securely by :link',
'support' => 'Support',
'contact_information' => 'Contact Information',
'256_encryption' => '256-Bit Encryption',
@ -1182,6 +1182,117 @@ $LANG = array(
'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.',
'return_to_app' => 'Return to app',
// Payment updates
'refund_payment' => 'Refund Payment',
'refund_max' => 'Max:',
'refund' => 'Refund',
'are_you_sure_refund' => 'Refund selected payments?',
'status_pending' => 'Pending',
'status_completed' => 'Completed',
'status_failed' => 'Failed',
'status_partially_refunded' => 'Partially Refunded',
'status_partially_refunded_amount' => ':amount Refunded',
'status_refunded' => 'Refunded',
'status_voided' => 'Cancelled',
'refunded_payment' => 'Refunded Payment',
'activity_39' => ':user cancelled a :payment_amount payment (:payment)',
'activity_40' => ':user refunded :adjustment of a :payment_amount payment (:payment)',
'card_expiration' => 'Exp:&nbsp:expires',
'card_creditcardother' => 'Unknown',
'card_americanexpress' => 'American Express',
'card_carteblanche' => 'Carte Blanche',
'card_unionpay' => 'UnionPay',
'card_diners' => 'Diners Club',
'card_discover' => 'Discover',
'card_jcb' => 'JCB',
'card_laser' => 'Laser',
'card_maestro' => 'Maestro',
'card_mastercard' => 'MasterCard',
'card_solo' => 'Solo',
'card_switch' => 'Switch',
'card_visacard' => 'Visa',
'card_ach' => 'ACH',
'payment_type_stripe' => 'Stripe',
'ach' => 'ACH',
'enable_ach' => 'Enable ACH',
'stripe_ach_help' => 'ACH support must also be enabled at Stripe.',
'stripe_ach_disabled' => 'Another gateway is already configured for direct debit.',
'plaid' => 'Plaid',
'client_id' => 'Client Id',
'secret' => 'Secret',
'public_key' => 'Public Key',
'plaid_optional' => '(optional)',
'plaid_environment_help' => 'When a Stripe test key is given, Plaid\'s development environement (tartan) will be used.',
'other_providers' => 'Other Providers',
'country_not_supported' => 'That country is not supported.',
'invalid_routing_number' => 'The routing number is not valid.',
'invalid_account_number' => 'The account number is not valid.',
'account_number_mismatch' => 'The account numbers do not match.',
'missing_account_holder_type' => 'Please select an individual or company account.',
'missing_account_holder_name' => 'Please enter the account holder\'s name.',
'routing_number' => 'Routing Number',
'confirm_account_number' => 'Confirm Account Number',
'individual_account' => 'Individual Account',
'company_account' => 'Company Account',
'account_holder_name' => 'Account Holder Name',
'add_account' => 'Add Account',
'payment_methods' => 'Payment Methods',
'complete_verification' => 'Complete Verification',
'verification_amount1' => 'Amount 1',
'verification_amount2' => 'Amount 2',
'payment_method_verified' => 'Verification completed successfully',
'verification_failed' => 'Verification Failed',
'remove_payment_method' => 'Remove Payment Method',
'confirm_remove_payment_method' => 'Are you sure you want to remove this payment method?',
'remove' => 'Remove',
'payment_method_removed' => 'Removed payment method.',
'bank_account_verification_help' => 'We have made two deposits into your account with the description "VERIFICATION". These deposits will take 1-2 business days to appear on your statement. Please enter the amounts below.',
'bank_account_verification_next_steps' => 'We have made two deposits into your account with the description "VERIFICATION". These deposits will take 1-2 business days to appear on your statement.
Once you have the amounts, come back to this payment methods page and click "Complete Verification" next to the account.',
'unknown_bank' => 'Unknown Bank',
'ach_verification_delay_help' => 'You will be able to use the account after completing verification. Verification usually takes 1-2 business days.',
'add_credit_card' => 'Add Credit Card',
'payment_method_added' => 'Added payment method.',
'use_for_auto_bill' => 'Use For Autobill',
'used_for_auto_bill' => 'Autobill Payment Method',
'payment_method_set_as_default' => 'Set Autobill payment method.',
'activity_41' => ':payment_amount payment (:payment) failed',
'webhook_url' => 'Webhook URL',
'stripe_webhook_help' => 'You must :link.',
'stripe_webhook_help_link_text' => 'add this URL as an endpoint at Stripe',
'payment_method_error' => 'There was an error adding your payment methd. Please try again later.',
'notification_invoice_payment_failed_subject' => 'Payment failed for Invoice :invoice',
'notification_invoice_payment_failed' => 'A payment made by client :client towards Invoice :invoice failed. The payment has been marked as failed and :amount has been added to the client\'s balance.',
'link_with_plaid' => 'Link Account Instantly with Plaid',
'link_manually' => 'Link Manually',
'secured_by_plaid' => 'Secured by Plaid',
'plaid_linked_status' => 'Your bank account at :bank',
'add_payment_method' => 'Add Payment Method',
'account_holder_type' => 'Account Holder Type',
'ach_authorization' => 'I authorize :company to electronically debit my account and, if necessary, electronically credit my account to correct erroneous debits.',
'ach_authorization_required' => 'You must consent to ACH transactions.',
'off' => 'Off',
'opt_in' => 'Opt-in',
'opt_out' => 'Opt-out',
'always' => 'Always',
'opted_out' => 'Opted out',
'opted_in' => 'Opted in',
'manage_auto_bill' => 'Manage Auto-bill',
'enabled' => 'Enabled',
'paypal' => 'PayPal',
'braintree_enable_paypal' => 'Enable PayPal payments through BrainTree',
'braintree_paypal_disabled_help' => 'The PayPal gateway is processing PayPal payments',
'braintree_paypal_help' => 'You must also :link.',
'braintree_paypal_help_link_text' => 'link PayPal to your BrainTree account',
'token_billing_braintree_paypal' => 'Save payment details',
'add_paypal_account' => 'Add PayPal Account',
'no_payment_method_specified' => 'No payment method specified',
);
return $LANG;

View File

@ -489,11 +489,11 @@ return array(
'token_billing_3' => 'Opt-out - el checkbox es mostrado y seleccionado',
'token_billing_4' => 'Siempre',
'token_billing_checkbox' => 'Almacenar detalles de la tarjeta de crédito',
'view_in_stripe' => 'Ver en Stripe',
'view_in_gateway' => 'Ver en :gateway',
'use_card_on_file' => 'Usar la tarjeta en el archivo',
'edit_payment_details' => 'Editar detalles del pago',
'token_billing' => 'Guardar detalles de la tarjeta',
'token_billing_secure' => 'La información es almacenada de manera segura por :stripe_link',
'token_billing_secure' => 'La información es almacenada de manera segura por :link',
'support' => 'Soporte',
'contact_information' => 'Información de Contacto',

View File

@ -509,11 +509,11 @@ return array(
'token_billing_3' => 'La casilla Opt-Out se muestra y se selecciona',
'token_billing_4' => 'Siempre',
'token_billing_checkbox' => 'Almacenar datos de la tarjeta de crédito',
'view_in_stripe' => 'Ver en Stripe',
'view_in_gateway' => 'Ver en :gateway',
'use_card_on_file' => 'Usar tarjeta en fichero', //??
'edit_payment_details' => 'Editar detalles de pago',
'token_billing' => 'Guardar datos de la tarjeta',
'token_billing_secure' => 'Los datos serán almacenados de forma segura por :stripe_link',
'token_billing_secure' => 'Los datos serán almacenados de forma segura por :link',
'support' => 'Soporte',
'contact_information' => 'Información de Contacto',

View File

@ -509,11 +509,11 @@ return array(
'token_billing_3' => 'Opt-out - Case à cocher affichée et sélectionné',
'token_billing_4' => 'Toujours',
'token_billing_checkbox' => 'Enregistrer les informations de paiement',
'view_in_stripe' => 'Voir sur Stripe',
'view_in_gateway' => 'Voir sur :gateway',
'use_card_on_file' => 'Use card on file',
'edit_payment_details' => 'Editer les détails de pauement',
'token_billing' => 'Enregister les détails de paiement',
'token_billing_secure' => 'Les données sont enregistrées de manière sécurisée par :stripe_link',
'token_billing_secure' => 'Les données sont enregistrées de manière sécurisée par :link',
'support' => 'Support',
'contact_information' => 'Information de contact',

View File

@ -510,11 +510,11 @@ return array(
'token_billing_3' => 'Désengagement - case à cocher affichée avec sélection',
'token_billing_4' => 'Toujours',
'token_billing_checkbox' => 'Mémoriser les informations de carte de crédit',
'view_in_stripe' => 'Visualiser dans Stripe',
'view_in_gateway' => 'Visualiser dans :gateway',
'use_card_on_file' => 'Mémoriser les informations de la carte pour usage ultérieur',
'edit_payment_details' => 'Éditer les informations de paiement',
'token_billing' => 'Sauvegarder les informations de carte de crédit',
'token_billing_secure' => 'Les données sont mémorisées de façon sécuritaire avec :stripe_link',
'token_billing_secure' => 'Les données sont mémorisées de façon sécuritaire avec :link',
'support' => 'Support',
'contact_information' => 'Contact',

View File

@ -512,11 +512,11 @@ return array(
'token_billing_3' => 'Opt-out - checkbox is shown and selected',
'token_billing_4' => 'Sempre',
'token_billing_checkbox' => 'Salva dettagli carta di credito',
'view_in_stripe' => 'Vedi transazione in Stripe',
'view_in_gateway' => 'Vedi transazione in :gateway',
'use_card_on_file' => 'Carta di credito salvata',
'edit_payment_details' => 'Modifica dettagli pagamento',
'token_billing' => 'Salva carta di credito',
'token_billing_secure' => 'I dati sono memorizzati su piattaforma sicura mediante :stripe_link',
'token_billing_secure' => 'I dati sono memorizzati su piattaforma sicura mediante :link',
'support' => 'Support',
'contact_information' => 'Informazioni di contatto',

View File

@ -445,11 +445,11 @@ $LANG = array(
'token_billing_3' => 'Opt-out - checkbox is shown and selected',
'token_billing_4' => 'Always',
'token_billing_checkbox' => 'Store credit card details',
'view_in_stripe' => 'View in Stripe',
'view_in_gateway' => 'View in :gateway',
'use_card_on_file' => 'Use card on file',
'edit_payment_details' => 'Edit payment details',
'token_billing' => 'Save card details',
'token_billing_secure' => 'The data is stored securely by :stripe_link',
'token_billing_secure' => 'The data is stored securely by :link',
'support' => 'Support',
'contact_information' => 'Contact Information',
'256_encryption' => '256-Bit Encryption',

View File

@ -520,11 +520,11 @@ return array(
'token_billing_3' => 'Opt-out - checkbox is shown and selected',
'token_billing_4' => 'Always',
'token_billing_checkbox' => 'Store credit card details',
'view_in_stripe' => 'View in Stripe',
'view_in_gateway' => 'View in :gateway',
'use_card_on_file' => 'Use card on file',
'edit_payment_details' => 'Edit payment details',
'token_billing' => 'Save card details',
'token_billing_secure' => 'The data is stored securely by :stripe_link',
'token_billing_secure' => 'The data is stored securely by :link',
'support' => 'Support',
'contact_information' => 'Contact information',

View File

@ -516,11 +516,11 @@ return array(
'token_billing_3' => 'Aktivert - valgboks er vist og valgt',
'token_billing_4' => 'Alltid',
'token_billing_checkbox' => 'Lagre bankkort detaljer',
'view_in_stripe' => 'Vis i Stripe',
'view_in_gateway' => 'Vis i :gateway',
'use_card_on_file' => 'Bruk lagret kort',
'edit_payment_details' => 'Rediger betalings detaljer',
'token_billing' => 'Lagre kort detaljer',
'token_billing_secure' => 'Dataene er trygt lagret av :stripe_link',
'token_billing_secure' => 'Dataene er trygt lagret av :link',
'support' => 'Brukerstøtte',
'contact_information' => 'Kontaktinformasjon',

View File

@ -512,11 +512,11 @@ return array(
'token_billing_3' => 'Opt-out - checkbox is getoond en geselecteerd',
'token_billing_4' => 'Altijd',
'token_billing_checkbox' => 'Sla carditcard gegevens op',
'view_in_stripe' => 'In Stripe bekijken',
'view_in_gateway' => 'In :gateway bekijken',
'use_card_on_file' => 'Gebruik opgeslagen kaart',
'edit_payment_details' => 'Betalingsdetails aanpassen',
'token_billing' => 'Kaartgegevens opslaan',
'token_billing_secure' => 'Kaartgegevens worden veilig opgeslagen door :stripe_link',
'token_billing_secure' => 'Kaartgegevens worden veilig opgeslagen door :link',
'support' => 'Ondersteuning',
'contact_information' => 'Contact informatie',

View File

@ -514,7 +514,7 @@ return array(
'use_card_on_file' => 'Usar cartão no arquivo',
'edit_payment_details' => 'Editar detalhes do pagamento',
'token_billing' => 'Salvar detalhes do cartão',
'token_billing_secure' => 'Dados armazenados com seguração por :stripe_link',
'token_billing_secure' => 'Dados armazenados com seguração por :link',
'support' => 'Suporte',
'contact_information' => 'Informações de Contato',

View File

@ -515,11 +515,11 @@ return array(
'token_billing_3' => 'Opt-out - Checkbox visas och är förvald',
'token_billing_4' => 'alltid',
'token_billing_checkbox' => 'Spara betalkortsinformation',
'view_in_stripe' => 'Visa i Stripe',
'view_in_gateway' => 'Visa i :gateway',
'use_card_on_file' => 'Använd kort på fil',
'edit_payment_details' => 'Ändra betalningsdetaljer',
'token_billing' => 'Spara kortinformation',
'token_billing_secure' => 'Data sparas säkert med :stripe_link',
'token_billing_secure' => 'Data sparas säkert med :link',
'support' => 'Support',
'contact_information' => 'Kontaktinformation',

View File

@ -22,6 +22,11 @@
{!! Former::populateField('show_address', intval($accountGateway->show_address)) !!}
{!! Former::populateField('update_address', intval($accountGateway->update_address)) !!}
{!! Former::populateField('publishable_key', $accountGateway->getPublishableStripeKey() ? str_repeat('*', strlen($accountGateway->getPublishableStripeKey())) : '') !!}
{!! Former::populateField('enable_ach', $accountGateway->getAchEnabled() ? '1' : null) !!}
{!! Former::populateField('enable_paypal', $accountGateway->getPayPalEnabled() ? '1' : null) !!}
{!! Former::populateField('plaid_client_id', $accountGateway->getPlaidClientId() ? str_repeat('*', strlen($accountGateway->getPlaidClientId())) : '') !!}
{!! Former::populateField('plaid_secret', $accountGateway->getPlaidSecret() ? str_repeat('*', strlen($accountGateway->getPlaidSecret())) : '') !!}
{!! Former::populateField('plaid_public_key', $accountGateway->getPlaidPublicKey() ? str_repeat('*', strlen($accountGateway->getPlaidPublicKey())) : '') !!}
@if ($config)
@foreach ($accountGateway->fields as $field => $junk)
@ -45,7 +50,7 @@
{!! Former::select('gateway_id')
->dataClass('gateway-dropdown')
->addGroupClass('gateway-option')
->addGroupClass('gateway-option gateway-choice')
->fromQuery($selectGateways, 'name', 'id')
->onchange('setFieldsShown()') !!}
@ -84,11 +89,44 @@
@if ($gateway->id == GATEWAY_STRIPE)
{!! Former::text('publishable_key') !!}
@endif
@if ($gateway->id == GATEWAY_STRIPE || $gateway->id == GATEWAY_BRAINTREE)
{!! Former::select('token_billing_type_id')
->options($tokenBillingOptions)
->help(trans('texts.token_billing_help')) !!}
@endif
@if ($gateway->id == GATEWAY_STRIPE)
<div class="form-group">
<label class="control-label col-lg-4 col-sm-4">{{ trans('texts.webhook_url') }}</label>
<div class="col-lg-8 col-sm-8 help-block">
<input type="text" class="form-control" onfocus="$(this).select()" readonly value="{{ URL::to('/paymenthook/'.$account->account_key.'/'.GATEWAY_STRIPE) }}">
<div class="help-block"><strong>{!! trans('texts.stripe_webhook_help', [
'link'=>'<a href="https://dashboard.stripe.com/account/webhooks" target="_blank">'.trans('texts.stripe_webhook_help_link_text').'</a>'
]) !!}</strong></div>
</div>
</div>
@endif
@if ($gateway->id == GATEWAY_BRAINTREE)
@if ($account->getGatewayByType(PAYMENT_TYPE_PAYPAL))
{!! Former::checkbox('enable_paypal')
->label(trans('texts.paypal'))
->text(trans('texts.braintree_enable_paypal'))
->value(null)
->disabled(true)
->help(trans('texts.braintree_paypal_disabled_help')) !!}
@else
{!! Former::checkbox('enable_paypal')
->label(trans('texts.paypal'))
->help(trans('texts.braintree_paypal_help', [
'link'=>'<a href="https://articles.braintreepayments.com/guides/paypal/setup-guide" target="_blank">'.
trans('texts.braintree_paypal_help_link_text').'</a>'
]))
->text(trans('texts.braintree_enable_paypal')) !!}
@endif
@endif
</div>
@endforeach
@ -108,6 +146,33 @@
->class('creditcard-types')
->addGroupClass('gateway-option')
!!}
<div class="stripe-ach">
@if ($account->getGatewayByType(PAYMENT_TYPE_DIRECT_DEBIT))
{!! Former::checkbox('enable_ach')
->label(trans('texts.ach'))
->text(trans('texts.enable_ach'))
->value(null)
->disabled(true)
->help(trans('texts.stripe_ach_disabled')) !!}
@else
{!! Former::checkbox('enable_ach')
->label(trans('texts.ach'))
->text(trans('texts.enable_ach'))
->help(trans('texts.stripe_ach_help')) !!}
<div class="stripe-ach-options">
<div class="form-group">
<div class="col-sm-8 col-sm-offset-4">
<h4>{{trans('texts.plaid')}}</h4>
<div class="help-block">{{trans('texts.plaid_optional')}}</div>
</div>
</div>
{!! Former::text('plaid_client_id')->label(trans('texts.client_id')) !!}
{!! Former::text('plaid_secret')->label(trans('texts.secret')) !!}
{!! Former::text('plaid_public_key')->label(trans('texts.public_key'))
->help(trans('texts.plaid_environment_help')) !!}
</div>
@endif
</div>
</div>
</div>
@ -126,9 +191,11 @@
var val = $('#payment_type_id').val();
if (val == 'PAYMENT_TYPE_CREDIT_CARD') {
$('.gateway-option').show();
$('.stripe-ach').hide();
setFieldsShown();
} else {
$('.gateway-option').hide();
$('.stripe-ach').hide();
if (val == 'PAYMENT_TYPE_PAYPAL') {
setFieldsShown({{ GATEWAY_PAYPAL_EXPRESS }});
@ -136,6 +203,10 @@
setFieldsShown({{ GATEWAY_DWOLLA }});
} else if (val == 'PAYMENT_TYPE_DIRECT_DEBIT') {
setFieldsShown({{ GATEWAY_GOCARDLESS }});
} else if (val == 'PAYMENT_TYPE_STRIPE') {
$('.gateway-option:not(.gateway-choice)').show();
$('.stripe-ach').show();
setFieldsShown({{ GATEWAY_STRIPE }});
} else {
setFieldsShown({{ GATEWAY_BITPAY }});
}
@ -169,14 +240,22 @@
}
}
function enablePlaidSettings() {
var visible = $('#enable_ach').is(':checked');
$('.stripe-ach-options').toggle(visible);
}
$(function() {
setPaymentType();
enablePlaidSettings();
@if ($accountGateway)
$('.payment-type-option').hide();
@endif
$('#show_address').change(enableUpdateAddress);
enableUpdateAddress();
$('#enable_ach').change(enablePlaidSettings)
})
</script>

View File

@ -69,7 +69,7 @@
{{ Former::populateField('remember', 'true') }}
<div class="modal-header">
@if (!isset($hideLogo) || !$hideLogo)
@if (!isset($account) || !$account->hasFeature(FEATURE_WHITE_LABEL))
<a href="{{ NINJA_WEB_URL }}" target="_blank">
<img src="{{ asset('images/icon-login.png') }}" />
<h4>Invoice Ninja | {{ trans('texts.account_login') }}</h4>

View File

@ -54,7 +54,7 @@
{!! Former::open('client/recover_password')->addClass('form-signin') !!}
<div class="modal-header">
@if (!isset($hideLogo) || !$hideLogo)
@if (!isset($account) || !$account->hasFeature(FEATURE_WHITE_LABEL))
<a href="{{ NINJA_WEB_URL }}" target="_blank">
<img src="{{ asset('images/icon-login.png') }}" />
<h4>Invoice Ninja | {{ trans('texts.password_recovery') }}</h4>

View File

@ -58,7 +58,7 @@
)) !!}
<div class="modal-header">
@if (!isset($hideLogo) || !$hideLogo)
@if (!isset($account) || !$account->hasFeature(FEATURE_WHITE_LABEL))
<a href="{{ NINJA_WEB_URL }}" target="_blank">
<img src="{{ asset('images/icon-login.png') }}" />
<h4>Invoice Ninja | {{ trans('texts.set_password') }}</h4>

View File

@ -39,7 +39,7 @@
</div>
@if ($gatewayLink)
{!! Button::normal(trans('texts.view_in_stripe'))->asLinkTo($gatewayLink)->withAttributes(['target' => '_blank']) !!}
{!! Button::normal(trans('texts.view_in_gateway', ['gateway'=>$gateway->gateway->name]))->asLinkTo($gatewayLink)->withAttributes(['target' => '_blank']) !!}
@endif
@if ($client->trashed())
@ -290,8 +290,10 @@
trans('texts.invoice'),
trans('texts.transaction_reference'),
trans('texts.method'),
trans('texts.source'),
trans('texts.payment_amount'),
trans('texts.payment_date'))
trans('texts.payment_date'),
trans('texts.status'))
->setUrl(url('api/payments/' . $client->public_id))
->setCustomValues('entityType', 'payments')
->setOptions('sPaginationType', 'bootstrap')

View File

@ -0,0 +1,26 @@
@extends('emails.master_user')
@section('markup')
@if ($account->enable_email_markup)
@include('emails.partials.user_view_action')
@endif
@stop
@section('body')
<div>
{{ trans('texts.email_salutation', ['name' => $userName]) }}
</div>
&nbsp;
<div>
{{ trans("texts.notification_invoice_payment_failed", ['amount' => $paymentAmount, 'client' => $clientName, 'invoice' => $invoiceNumber]) }}
</div>
&nbsp;
<div>
{{ $payment->gateway_error }}
</div>
&nbsp;
<div>
{{ trans('texts.email_signature') }} <br/>
{{ trans('texts.email_from') }}
</div>
@stop

View File

@ -0,0 +1,8 @@
{!! trans('texts.email_salutation', ['name' => $userName]) !!}
{!! trans("texts.notification_invoice_payment_failed", ['amount' => $paymentAmount, 'client' => $clientName, 'invoice' => $invoiceNumber]) !!}
{!! $payment->gateway_error !!}
{!! trans('texts.email_signature') !!}
{!! trans('texts.email_from') !!}

View File

@ -126,7 +126,6 @@
@endif
</div>
</div>
<div class="row">
<div class="col-md-4 col-md-4-left">
<div class="well">
@ -162,7 +161,14 @@
</div>
</div>
</div>
@if (!empty($account->getTokenGatewayId()))
<div class="row">
<div class="col-xs-12">
<h3>{{ trans('texts.payment_methods') }}</h3>
@include('payments.paymentmethods_list')
</div>
</div>
@endif
<div style="min-height: 550px">
{!! Datatable::table()
->addColumn(

View File

@ -164,12 +164,33 @@
->label(trans("texts.{$entityType}_number_short"))
->data_bind("value: invoice_number, valueUpdate: 'afterkeydown'") !!}
</span>
@if($account->getTokenGatewayId())
<span data-bind="visible: is_recurring()" style="display: none">
{!! Former::checkbox('auto_bill')
->label(trans('texts.auto_bill'))
->text(trans('texts.enable_with_stripe'))
->data_bind("checked: auto_bill, valueUpdate: 'afterkeydown'") !!}
<div data-bind="visible: !(auto_bill() == 1 &amp;&amp; client_enable_auto_bill()) &amp;&amp; !(auto_bill() == 2 &amp;&amp; !client_enable_auto_bill())" style="display: none">
{!! Former::select('auto_bill')
->data_bind("value: auto_bill, valueUpdate: 'afterkeydown', event:{change:function(){if(auto_bill()==1)client_enable_auto_bill(0);if(auto_bill()==2)client_enable_auto_bill(1)}}")
->options([
0 => trans('texts.off'),
1 => trans('texts.opt_in'),
2 => trans('texts.opt_out'),
3 => trans('texts.always'),
]) !!}
</div>
<input type="hidden" name="client_enable_auto_bill" data-bind="attr: { value: client_enable_auto_bill() }" />
<div class="form-group" data-bind="visible: auto_bill() == 1 &amp;&amp; client_enable_auto_bill()">
<div class="col-sm-4 control-label">{{trans('texts.auto_bill')}}</div>
<div class="col-sm-8" style="padding-top:10px;padding-bottom:9px">
{{trans('texts.opted_in')}} - <a href="#" data-bind="click:function(){client_enable_auto_bill(false)}">({{trans('texts.disable')}})</a>
</div>
</div>
<div class="form-group" data-bind="visible: auto_bill() == 2 &amp;&amp; !client_enable_auto_bill()">
<div class="col-sm-4 control-label">{{trans('texts.auto_bill')}}</div>
<div class="col-sm-8" style="padding-top:10px;padding-bottom:9px">
{{trans('texts.opted_out')}} - <a href="#" data-bind="click:function(){client_enable_auto_bill(true)}">({{trans('texts.enable')}})</a>
</div>
</div>
</span>
@endif
{!! Former::text('po_number')->label(trans('texts.po_number_short'))->data_bind("value: po_number, valueUpdate: 'afterkeydown'") !!}
{!! Former::text('discount')->data_bind("value: discount, valueUpdate: 'afterkeydown'")
->addGroupClass('discount-group')->type('number')->min('0')->step('any')->append(

View File

@ -188,7 +188,8 @@ function InvoiceModel(data) {
self.tax_rate2 = ko.observable();
self.is_recurring = ko.observable(0);
self.is_quote = ko.observable({{ $entityType == ENTITY_QUOTE ? '1' : '0' }});
self.auto_bill = ko.observable();
self.auto_bill = ko.observable(0);
self.client_enable_auto_bill = ko.observable(false);
self.invoice_status_id = ko.observable(0);
self.invoice_items = ko.observableArray();
self.documents = ko.observableArray();

View File

@ -14,7 +14,52 @@
body {
background-color: #f8f8f8;
}
.dropdown-menu li a{
overflow:hidden;
margin-top:5px;
margin-bottom:5px;
}
</style>
@if (!empty($braintreeClientToken))
<script type="text/javascript" src="https://js.braintreegateway.com/js/braintree-2.23.0.min.js"></script>
<script type="text/javascript" >
$(function() {
var paypalLink = $('.dropdown-menu a[href$="/braintree_paypal"]'),
paypalUrl = paypalLink.attr('href'),
checkout;
paypalLink.parent().attr('id', 'paypal-container');
braintree.setup("{{ $braintreeClientToken }}", "custom", {
onReady: function (integration) {
checkout = integration;
$('.dropdown-menu a[href$="#braintree_paypal"]').each(function(){
var el=$(this);
el.attr('href', el.attr('href').replace('#braintree_paypal','?device_data='+encodeURIComponent(integration.deviceData)))
})
},
paypal: {
container: "paypal-container",
singleUse: false,
enableShippingAddress: false,
enableBillingAddress: false,
headless: true,
locale: "{{$invoice->client->language?$invoice->client->language->locale:$invoice->account->language->locale}}"
},
dataCollector: {
paypal: true
},
onPaymentMethodReceived: function (obj) {
window.location.href = paypalUrl + '/' + encodeURIComponent(obj.nonce) + "?details=" + encodeURIComponent(JSON.stringify(obj.details))
}
});
paypalLink.click(function(e){
e.preventDefault();
checkout.paypal.initAuthFlow();
})
});
</script>
@endif
@stop
@section('content')

View File

@ -54,6 +54,40 @@
->setOptions('aaSorting', [[isset($sortCol) ? $sortCol : '1', 'desc']])
->render('datatable') !!}
@if ($entityType == ENTITY_PAYMENT)
<div class="modal fade" id="paymentRefundModal" tabindex="-1" role="dialog" aria-labelledby="paymentRefundModalLabel" aria-hidden="true">
<div class="modal-dialog" style="min-width:150px">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="paymentRefundModalLabel">{{ trans('texts.refund_payment') }}</h4>
</div>
<div class="modal-body">
<div class="form-horizontal">
<div class="form-group">
<label for="refundAmount" class="col-sm-offset-2 col-sm-2 control-label">{{ trans('texts.amount') }}</label>
<div class="col-sm-4">
<div class="input-group">
<span class="input-group-addon" id="refundCurrencySymbol"></span>
<input type="number" class="form-control" id="refundAmount" name="amount" step="0.01" min="0.01" placeholder="{{ trans('texts.amount') }}">
</div>
<div class="help-block">{{ trans('texts.refund_max') }} <span id="refundMax"></span></div>
</div>
</div>
</div>
</div>
<div class="modal-footer" style="margin-top: 0px">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ trans('texts.cancel') }}</button>
<button type="button" class="btn btn-primary" id="completeRefundButton">{{ trans('texts.refund') }}</button>
</div>
</div>
</div>
</div>
@endif
{!! Former::close() !!}
<script type="text/javascript">
@ -103,6 +137,22 @@
submitForm('invoice');
}
@if ($entityType == ENTITY_PAYMENT)
var paymentId = null;
function showRefundModal(id, amount, formatted, symbol){
paymentId = id;
$('#refundCurrencySymbol').text(symbol);
$('#refundMax').text(formatted);
$('#refundAmount').val(amount).attr('max', amount);
$('#paymentRefundModal').modal('show');
}
function handleRefundClicked(){
$('#public_id').val(paymentId);
submitForm('refund');
}
@endif
function setTrashVisible() {
var checked = $('#trashed').is(':checked');
var url = '{{ URL::to('view_archive/' . $entityType) }}' + (checked ? '/true' : '/false');
@ -157,6 +207,10 @@
actionListHandler();
}
@if ($entityType == ENTITY_PAYMENT)
$('#completeRefundButton').click(handleRefundClicked)
@endif
$('.archive, .invoice').prop('disabled', true);
$('.archive:not(.dropdown-toggle)').click(function() {
submitForm('archive');

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="{{App::getLocale()}}">
<head>
@if (isset($hideLogo) && $hideLogo)
@if (isset($account) && $account instanceof \App\Models\Account && $account->hasFeature(FEATURE_WHITE_LABEL))
<title>{{ trans('texts.client_portal') }}</title>
@else
<title>{{ isset($title) ? ($title . ' | Invoice Ninja') : ('Invoice Ninja | ' . trans('texts.app_title')) }}</title>

View File

@ -0,0 +1,696 @@
@extends('public.header')
@section('head')
@parent
@if (!empty($braintreeClientToken))
<script type="text/javascript" src="https://js.braintreegateway.com/js/braintree-2.23.0.min.js"></script>
<script type="text/javascript" >
$(function() {
braintree.setup("{{ $braintreeClientToken }}", "custom", {
id: "payment-form",
hostedFields: {
number: {
selector: "#card_number",
placeholder: "{{ trans('texts.card_number') }}"
},
cvv: {
selector: "#cvv",
placeholder: "{{ trans('texts.cvv') }}"
},
expirationMonth: {
selector: "#expiration_month",
placeholder: "{{ trans('texts.expiration_month') }}"
},
expirationYear: {
selector: "#expiration_year",
placeholder: "{{ trans('texts.expiration_year') }}"
},
styles: {
'input': {
'font-family': {!! json_encode(Utils::getFromCache($account->getBodyFontId(), 'fonts')['css_stack']) !!},
'font-weight': "{{ Utils::getFromCache($account->getBodyFontId(), 'fonts')['css_weight'] }}",
'font-size': '16px'
}
}
},
onError: function(e) {
var $form = $('.payment-form');
$form.find('button').prop('disabled', false);
// Show the errors on the form
if (e.details && e.details.invalidFieldKeys.length) {
var invalidField = e.details.invalidFieldKeys[0];
if (invalidField == 'number') {
$('#js-error-message').html('{{ trans('texts.invalid_card_number') }}').fadeIn();
}
else if (invalidField == 'expirationDate' || invalidField == 'expirationYear' || invalidField == 'expirationMonth') {
$('#js-error-message').html('{{ trans('texts.invalid_expiry') }}').fadeIn();
}
else if (invalidField == 'cvv') {
$('#js-error-message').html('{{ trans('texts.invalid_cvv') }}').fadeIn();
}
}
else {
$('#js-error-message').html(e.message).fadeIn();
}
}
});
$('.payment-form').submit(function(event) {
var $form = $(this);
// Disable the submit button to prevent repeated clicks
$form.find('button').prop('disabled', true);
$('#js-error-message').hide();
});
});
</script>
@elseif (isset($accountGateway) && $accountGateway->getPublishableStripeKey())
<script type="text/javascript" src="https://js.stripe.com/v2/"></script>
<script type="text/javascript">
Stripe.setPublishableKey('{{ $accountGateway->getPublishableStripeKey() }}');
$(function() {
$('.payment-form').submit(function(event) {
if($('[name=plaidAccountId]').length)return;
var $form = $(this);
var data = {
@if($paymentType == PAYMENT_TYPE_STRIPE_ACH)
account_holder_name: $('#account_holder_name').val(),
account_holder_type: $('[name=account_holder_type]:checked').val(),
currency: $("#currency").val(),
country: $("#country").val(),
routing_number: $('#routing_number').val().replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''),
account_number: $('#account_number').val().replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '')
@else
name: $('#first_name').val() + ' ' + $('#last_name').val(),
address_line1: $('#address1').val(),
address_line2: $('#address2').val(),
address_city: $('#city').val(),
address_state: $('#state').val(),
address_zip: $('#postal_code').val(),
address_country: $("#country_id option:selected").text(),
number: $('#card_number').val(),
cvc: $('#cvv').val(),
exp_month: $('#expiration_month').val(),
exp_year: $('#expiration_year').val()
@endif
};
@if($paymentType == PAYMENT_TYPE_STRIPE_ACH)
// Validate the account details
if (!data.account_holder_type) {
$('#js-error-message').html('{{ trans('texts.missing_account_holder_type') }}').fadeIn();
return false;
}
if (!data.account_holder_name) {
$('#js-error-message').html('{{ trans('texts.missing_account_holder_name') }}').fadeIn();
return false;
}
if (!data.routing_number || !Stripe.bankAccount.validateRoutingNumber(data.routing_number, data.country)) {
$('#js-error-message').html('{{ trans('texts.invalid_routing_number') }}').fadeIn();
return false;
}
if (data.account_number != $('#confirm_account_number').val().replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '')) {
$('#js-error-message').html('{{ trans('texts.account_number_mismatch') }}').fadeIn();
return false;
}
if (!data.account_number || !Stripe.bankAccount.validateAccountNumber(data.account_number, data.country)) {
$('#js-error-message').html('{{ trans('texts.invalid_account_number') }}').fadeIn();
return false;
}
@else
// Validate the card details
if (!Stripe.card.validateCardNumber(data.number)) {
$('#js-error-message').html('{{ trans('texts.invalid_card_number') }}').fadeIn();
return false;
}
if (!Stripe.card.validateExpiry(data.exp_month, data.exp_year)) {
$('#js-error-message').html('{{ trans('texts.invalid_expiry') }}').fadeIn();
return false;
}
if (!Stripe.card.validateCVC(data.cvc)) {
$('#js-error-message').html('{{ trans('texts.invalid_cvv') }}').fadeIn();
return false;
}
@endif
// Disable the submit button to prevent repeated clicks
$form.find('button').prop('disabled', true);
$('#js-error-message').hide();
@if($paymentType == PAYMENT_TYPE_STRIPE_ACH)
Stripe.bankAccount.createToken(data, stripeResponseHandler);
@else
Stripe.card.createToken(data, stripeResponseHandler);
@endif
// Prevent the form from submitting with the default action
return false;
});
@if($accountGateway->getPlaidEnabled())
var plaidHandler = Plaid.create({
selectAccount: true,
env: '{{ $accountGateway->getPlaidEnvironment() }}',
clientName: {!! json_encode($account->getDisplayName()) !!},
key: '{{ $accountGateway->getPlaidPublicKey() }}',
product: 'auth',
onSuccess: plaidSuccessHandler,
onExit : function(){$('#secured_by_plaid').hide()}
});
$('#plaid_link_button').click(function(){plaidHandler.open();$('#secured_by_plaid').fadeIn()});
$('#plaid_unlink').click(function(e){
e.preventDefault();
$('#manual_container').fadeIn();
$('#plaid_linked').hide();
$('#plaid_link_button').show();
$('#pay_now_button').hide();
$('#add_account_button').show();
$('[name=plaidPublicToken]').remove();
$('[name=plaidAccountId]').remove();
$('[name=account_holder_type],#account_holder_name').attr('required','required');
})
@endif
});
function stripeResponseHandler(status, response) {
var $form = $('.payment-form');
if (response.error) {
// Show the errors on the form
var error = response.error.message;
@if($paymentType == PAYMENT_TYPE_STRIPE_ACH)
if(response.error.param == 'bank_account[country]') {
error = "{{trans('texts.country_not_supported')}}";
}
@endif
$form.find('button').prop('disabled', false);
$('#js-error-message').html(error).fadeIn();
} else {
// response contains id and card, which contains additional card details
var token = response.id;
// Insert the token into the form so it gets submitted to the server
$form.append($('<input type="hidden" name="stripeToken"/>').val(token));
// and submit
$form.get(0).submit();
}
};
function plaidSuccessHandler(public_token, metadata) {
$('#secured_by_plaid').hide()
var $form = $('.payment-form');
$form.append($('<input type="hidden" name="plaidPublicToken"/>').val(public_token));
$form.append($('<input type="hidden" name="plaidAccountId"/>').val(metadata.account_id));
$('#plaid_linked_status').text('{{ trans('texts.plaid_linked_status') }}'.replace(':bank', metadata.institution.name));
$('#manual_container').fadeOut();
$('#plaid_linked').show();
$('#plaid_link_button').hide();
$('[name=account_holder_type],#account_holder_name').removeAttr('required');
var payNowBtn = $('#pay_now_button');
if(payNowBtn.length) {
payNowBtn.show();
$('#add_account_button').hide();
}
};
</script>
@else
<script type="text/javascript">
$(function() {
$('.payment-form').submit(function(event) {
var $form = $(this);
// Disable the submit button to prevent repeated clicks
$form.find('button').prop('disabled', true);
return true;
});
});
</script>
@endif
@stop
@section('content')
@include('payments.payment_css')
@if($paymentType == PAYMENT_TYPE_STRIPE_ACH)
{!! Former::open($url)
->autocomplete('on')
->addClass('payment-form')
->id('payment-form')
->rules(array(
'first_name' => 'required',
'last_name' => 'required',
'account_number' => 'required',
'routing_number' => 'required',
'account_holder_name' => 'required',
'account_holder_type' => 'required',
'authorize_ach' => 'required',
)) !!}
@else
{!! Former::vertical_open($url)
->autocomplete('on')
->addClass('payment-form')
->id('payment-form')
->rules(array(
'first_name' => 'required',
'last_name' => 'required',
'card_number' => 'required',
'expiration_month' => 'required',
'expiration_year' => 'required',
'cvv' => 'required',
'address1' => 'required',
'city' => 'required',
'state' => 'required',
'postal_code' => 'required',
'country_id' => 'required',
'phone' => 'required',
'email' => 'required|email'
)) !!}
@endif
@if ($client)
{{ Former::populate($client) }}
{{ Former::populateField('first_name', $contact->first_name) }}
{{ Former::populateField('last_name', $contact->last_name) }}
{{ Former::populateField('email', $contact->email) }}
@if (!$client->country_id && $client->account->country_id)
{{ Former::populateField('country_id', $client->account->country_id) }}
{{ Former::populateField('country', $client->account->country->iso_3166_2) }}
@endif
@if (!$client->currency_id && $client->account->currency_id)
{{ Former::populateField('currency_id', $client->account->currency_id) }}
{{ Former::populateField('currency', $client->account->currency->code) }}
@endif
@endif
@if (Utils::isNinjaDev())
{{ Former::populateField('first_name', 'Test') }}
{{ Former::populateField('last_name', 'Test') }}
{{ Former::populateField('address1', '350 5th Ave') }}
{{ Former::populateField('city', 'New York') }}
{{ Former::populateField('state', 'NY') }}
{{ Former::populateField('postal_code', '10118') }}
{{ Former::populateField('country_id', 840) }}
@endif
<div class="container">
<p>&nbsp;</p>
<div class="panel panel-default">
<div class="panel-body">
<div class="row">
<div class="col-md-7">
<header>
@if ($client && isset($invoiceNumber))
<h2>{{ $client->getDisplayName() }}</h2>
<h3>{{ trans('texts.invoice') . ' ' . $invoiceNumber }}<span>|&nbsp; {{ trans('texts.amount_due') }}: <em>{{ $account->formatMoney($amount, $client, true) }}</em></span></h3>
@elseif ($paymentTitle)
<h2>{{ $paymentTitle }}
@if(isset($paymentSubtitle))
<br/><small>{{ $paymentSubtitle }}</small>
@endif
</h2>
@endif
</header>
</div>
<div class="col-md-5">
@if (Request::secure() || Utils::isNinjaDev())
<div class="secure">
<h3>{{ trans('texts.secure_payment') }}</h3>
<div>{{ trans('texts.256_encryption') }}</div>
</div>
@endif
</div>
</div>
<p>&nbsp;<br/>&nbsp;</p>
<div>
<div id="paypal-container"></div>
@if($paymentType != PAYMENT_TYPE_STRIPE_ACH && $paymentType != PAYMENT_TYPE_BRAINTREE_PAYPAL)
<h3>{{ trans('texts.contact_information') }}</h3>
<div class="row">
<div class="col-md-6">
{!! Former::text('first_name')
->placeholder(trans('texts.first_name'))
->autocomplete('given-name')
->label('') !!}
</div>
<div class="col-md-6">
{!! Former::text('last_name')
->placeholder(trans('texts.last_name'))
->autocomplete('family-name')
->label('') !!}
</div>
</div>
<div class="row" style="display:{{ isset($paymentTitle) ? 'block' : 'none' }}">
<div class="col-md-12">
{!! Former::text('email')
->placeholder(trans('texts.email'))
->autocomplete('email')
->label('') !!}
</div>
</div>
<p>&nbsp;<br/>&nbsp;</p>
@if (!empty($showAddress))
<h3>{{ trans('texts.billing_address') }}&nbsp;<span class="help">{{ trans('texts.payment_footer1') }}</span></h3>
<div class="row">
<div class="col-md-6">
{!! Former::text('address1')
->autocomplete('address-line1')
->placeholder(trans('texts.address1'))
->label('') !!}
</div>
<div class="col-md-6">
{!! Former::text('address2')
->autocomplete('address-line2')
->placeholder(trans('texts.address2'))
->label('') !!}
</div>
</div>
<div class="row">
<div class="col-md-6">
{!! Former::text('city')
->autocomplete('address-level2')
->placeholder(trans('texts.city'))
->label('') !!}
</div>
<div class="col-md-6">
{!! Former::text('state')
->autocomplete('address-level1')
->placeholder(trans('texts.state'))
->label('') !!}
</div>
</div>
<div class="row">
<div class="col-md-6">
{!! Former::text('postal_code')
->autocomplete('postal-code')
->placeholder(trans('texts.postal_code'))
->label('') !!}
</div>
<div class="col-md-6">
{!! Former::select('country_id')
->placeholder(trans('texts.country_id'))
->fromQuery($countries, 'name', 'id')
->addGroupClass('country-select')
->label('') !!}
</div>
</div>
<p>&nbsp;<br/>&nbsp;</p>
@endif
<h3>{{ trans('texts.billing_method') }}</h3>
@endif
@if($paymentType == PAYMENT_TYPE_STRIPE_ACH)
@if($accountGateway->getPlaidEnabled())
<div id="plaid_container">
<a class="btn btn-default btn-lg" id="plaid_link_button">
<img src="{{ URL::to('images/plaid-logo.svg') }}">
<img src="{{ URL::to('images/plaid-logowhite.svg') }}" class="hoverimg">
{{ trans('texts.link_with_plaid') }}
</a>
<div id="plaid_linked">
<div id="plaid_linked_status"></div>
<a href="#" id="plaid_unlink">{{ trans('texts.unlink') }}</a>
</div>
</div>
@endif
<div id="manual_container">
@if($accountGateway->getPlaidEnabled())
<div id="plaid_or"><span>{{ trans('texts.or') }}</span></div>
<h4>{{ trans('texts.link_manually') }}</h4>
@endif
<p>{{ trans('texts.ach_verification_delay_help') }}</p>
{!! Former::radios('account_holder_type')->radios(array(
trans('texts.individual_account') => array('value' => 'individual'),
trans('texts.company_account') => array('value' => 'company'),
))->inline()->label(trans('texts.account_holder_type')); !!}
{!! Former::text('account_holder_name')
->label(trans('texts.account_holder_name')) !!}
{!! Former::select('country')
->label(trans('texts.country_id'))
->fromQuery($countries, 'name', 'iso_3166_2')
->addGroupClass('country-select') !!}
{!! Former::select('currency')
->label(trans('texts.currency_id'))
->fromQuery($currencies, 'name', 'code')
->addGroupClass('currency-select') !!}
{!! Former::text('')
->id('routing_number')
->label(trans('texts.routing_number')) !!}
<div class="form-group" style="margin-top:-15px">
<div class="col-md-8 col-md-offset-4">
<div id="bank_name"></div>
</div>
</div>
{!! Former::text('')
->id('account_number')
->label(trans('texts.account_number')) !!}
{!! Former::text('')
->id('confirm_account_number')
->label(trans('texts.confirm_account_number')) !!}
{!! Former::checkbox('authorize_ach')
->text(trans('texts.ach_authorization', ['company'=>$account->getDisplayName()]))
->label(' ') !!}
</div>
</div>
<div class="col-md-8 col-md-offset-4">
{!! Button::success(strtoupper(trans('texts.add_account')))
->submit()
->withAttributes(['id'=>'add_account_button'])
->large() !!}
@if($accountGateway->getPlaidEnabled() && !empty($amount))
{!! Button::success(strtoupper(trans('texts.pay_now') . ' - ' . $account->formatMoney($amount, $client, true) ))
->submit()
->withAttributes(['style'=>'display:none', 'id'=>'pay_now_button'])
->large() !!}
@endif
</div>
@elseif($paymentType == PAYMENT_TYPE_BRAINTREE_PAYPAL)
<h3>{{ trans('texts.paypal') }}</h3>
<div>{{$paypalDetails->firstName}} {{$paypalDetails->lastName}}</div>
<div>{{$paypalDetails->email}}</div>
<input type="hidden" name="payment_method_nonce" value="{{$sourceId}}">
<input type="hidden" name="first_name" value="{{$paypalDetails->firstName}}">
<input type="hidden" name="last_name" value="{{$paypalDetails->lastName}}">
<p>&nbsp;</p>
@if (isset($amount) && $client && $account->showTokenCheckbox())
<input id="token_billing" type="checkbox" name="token_billing" {{ $account->selectTokenCheckbox() ? 'CHECKED' : '' }} value="1" style="margin-left:0px; vertical-align:top">
<label for="token_billing" class="checkbox" style="display: inline;">{{ trans('texts.token_billing_braintree_paypal') }}</label>
<span class="help-block" style="font-size:15px">
{!! trans('texts.token_billing_secure', ['link' => link_to('https://www.braintreepayments.com/', 'Braintree', ['target' => '_blank'])]) !!}
</span>
@endif
<p>&nbsp;</p>
<center>
@if(isset($amount))
{!! Button::success(strtoupper(trans('texts.pay_now') . ' - ' . $account->formatMoney($amount, $client, true) ))
->submit()
->large() !!}
@else
{!! Button::success(strtoupper(trans('texts.add_credit_card') ))
->submit()
->large() !!}
@endif
</center>
@else
<div class="row">
<div class="col-md-9">
@if (!empty($braintreeClientToken))
<div id="card_number" class="braintree-hosted form-control"></div>
@else
{!! Former::text($accountGateway->getPublishableStripeKey() ? '' : 'card_number')
->id('card_number')
->placeholder(trans('texts.card_number'))
->autocomplete('cc-number')
->label('') !!}
@endif
</div>
<div class="col-md-3">
@if (!empty($braintreeClientToken))
<div id="cvv" class="braintree-hosted form-control"></div>
@else
{!! Former::text($accountGateway->getPublishableStripeKey() ? '' : 'cvv')
->id('cvv')
->placeholder(trans('texts.cvv'))
->autocomplete('off')
->label('') !!}
@endif
</div>
</div>
<div class="row">
<div class="col-md-6">
@if (!empty($braintreeClientToken))
<div id="expiration_month" class="braintree-hosted form-control"></div>
@else
{!! Former::select($accountGateway->getPublishableStripeKey() ? '' : 'expiration_month')
->id('expiration_month')
->autocomplete('cc-exp-month')
->placeholder(trans('texts.expiration_month'))
->addOption('01 - January', '1')
->addOption('02 - February', '2')
->addOption('03 - March', '3')
->addOption('04 - April', '4')
->addOption('05 - May', '5')
->addOption('06 - June', '6')
->addOption('07 - July', '7')
->addOption('08 - August', '8')
->addOption('09 - September', '9')
->addOption('10 - October', '10')
->addOption('11 - November', '11')
->addOption('12 - December', '12')->label('')
!!}
@endif
</div>
<div class="col-md-6">
@if (!empty($braintreeClientToken))
<div id="expiration_year" class="braintree-hosted form-control"></div>
@else
{!! Former::select($accountGateway->getPublishableStripeKey() ? '' : 'expiration_year')
->id('expiration_year')
->autocomplete('cc-exp-year')
->placeholder(trans('texts.expiration_year'))
->addOption('2016', '2016')
->addOption('2017', '2017')
->addOption('2018', '2018')
->addOption('2019', '2019')
->addOption('2020', '2020')
->addOption('2021', '2021')
->addOption('2022', '2022')
->addOption('2023', '2023')
->addOption('2024', '2024')
->addOption('2025', '2025')
->addOption('2026', '2026')->label('')
!!}
@endif
</div>
</div>
<div class="row" style="padding-top:18px">
<div class="col-md-5">
@if (isset($amount) && $client && $account->showTokenCheckbox($storageGateway/* will contain gateway id */))
<input id="token_billing" type="checkbox" name="token_billing" {{ $account->selectTokenCheckbox() ? 'CHECKED' : '' }} value="1" style="margin-left:0px; vertical-align:top">
<label for="token_billing" class="checkbox" style="display: inline;">{{ trans('texts.token_billing') }}</label>
<span class="help-block" style="font-size:15px">
@if ($storageGateway == GATEWAY_STRIPE)
{!! trans('texts.token_billing_secure', ['link' => link_to('https://stripe.com/', 'Stripe.com', ['target' => '_blank'])]) !!}
@elseif ($storageGateway == GATEWAY_BRAINTREE)
{!! trans('texts.token_billing_secure', ['link' => link_to('https://www.braintreepayments.com/', 'Braintree', ['target' => '_blank'])]) !!}
@endif
</span>
@endif
</div>
<div class="col-md-7">
@if (isset($acceptedCreditCardTypes))
<div class="pull-right">
@foreach ($acceptedCreditCardTypes as $card)
<img src="{{ $card['source'] }}" alt="{{ $card['alt'] }}" style="width: 70px; display: inline; margin-right: 6px;"/>
@endforeach
</div>
@endif
</div>
</div>
<p>&nbsp;</p>
<center>
@if(isset($amount))
{!! Button::success(strtoupper(trans('texts.pay_now') . ' - ' . $account->formatMoney($amount, $client, true) ))
->submit()
->large() !!}
@else
{!! Button::success(strtoupper(trans('texts.add_credit_card') ))
->submit()
->large() !!}
@endif
</center>
<p>&nbsp;</p>
@endif
<div id="js-error-message" style="display:none" class="alert alert-danger"></div>
</div>
</div>
</div>
<p>&nbsp;</p>
<p>&nbsp;</p>
</div>
{!! Former::close() !!}
<script type="text/javascript">
$(function() {
$('select').change(function() {
$(this).css({color:'#444444'});
});
$('#country_id').combobox();
$('#country').combobox();
$('#currency').combobox();
$('#first_name').focus();
@if($paymentType == PAYMENT_TYPE_STRIPE_ACH)
var routingNumberCache = {};
$('#routing_number, #country').on('change keypress keyup keydown paste', function(){setTimeout(function () {
var routingNumber = $('#routing_number').val().replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
if (routingNumber.length != 9 || $("#country").val() != 'US' || routingNumberCache[routingNumber] === false) {
$('#bank_name').hide();
} else if (routingNumberCache[routingNumber]) {
$('#bank_name').empty().append(routingNumberCache[routingNumber]).show();
} else {
routingNumberCache[routingNumber] = false;
$('#bank_name').hide();
$.ajax({
url:"{{ URL::to('/bank') }}/" + routingNumber,
success:function(data) {
var els = $().add(document.createTextNode(data.name + ", " + data.city + ", " + data.state));
routingNumberCache[routingNumber] = els;
// Still the same number?
if (routingNumber == $('#routing_number').val().replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '')) {
$('#bank_name').empty().append(els).show();
}
},
error:function(xhr) {
if (xhr.status == 404) {
var els = $(document.createTextNode('{{trans('texts.unknown_bank')}}'));
;
routingNumberCache[routingNumber] = els;
// Still the same number?
if (routingNumber == $('#routing_number').val().replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '')) {
$('#bank_name').empty().append(els).show();
}
}
}
})
}
},10)})
@endif
});
</script>
@if (isset($accountGateway) && $accountGateway->getPlaidEnabled())
<a href="https://plaid.com/products/auth/" target="_blank" style="display:none" id="secured_by_plaid"><img src="{{ URL::to('images/plaid-logowhite.svg') }}">{{ trans('texts.secured_by_plaid') }}</a>
<script src="https://cdn.plaid.com/link/stable/link-initialize.js"></script>
@endif
@stop

View File

@ -1,357 +0,0 @@
@extends('public.header')
@section('head')
@parent
@if ($accountGateway->getPublishableStripeKey())
<script type="text/javascript" src="https://js.stripe.com/v2/"></script>
<script type="text/javascript">
Stripe.setPublishableKey('{{ $accountGateway->getPublishableStripeKey() }}');
$(function() {
$('.payment-form').submit(function(event) {
var $form = $(this);
var data = {
name: $('#first_name').val() + ' ' + $('#last_name').val(),
email: $('#email').val(),
address_line1: $('#address1').val(),
address_line2: $('#address2').val(),
address_city: $('#city').val(),
address_state: $('#state').val(),
address_zip: $('#postal_code').val(),
address_country: $("#country_id option:selected").text(),
number: $('#card_number').val(),
exp_month: $('#expiration_month').val(),
exp_year: $('#expiration_year').val()
};
// allow space until there's a setting to disable
if ($('#cvv').val() != ' ') {
data.cvc = $('#cvv').val();
}
// Validate the card details
if (!Stripe.card.validateCardNumber(data.number)) {
$('#js-error-message').html('{{ trans('texts.invalid_card_number') }}').fadeIn();
return false;
}
if (!Stripe.card.validateExpiry(data.exp_month, data.exp_year)) {
$('#js-error-message').html('{{ trans('texts.invalid_expiry') }}').fadeIn();
return false;
}
if (data.hasOwnProperty('cvc') && !Stripe.card.validateCVC(data.cvc)) {
$('#js-error-message').html('{{ trans('texts.invalid_cvv') }}').fadeIn();
return false;
}
// Disable the submit button to prevent repeated clicks
$form.find('button').prop('disabled', true);
$('#js-error-message').hide();
Stripe.card.createToken(data, stripeResponseHandler);
// Prevent the form from submitting with the default action
return false;
});
});
function stripeResponseHandler(status, response) {
var $form = $('.payment-form');
if (response.error) {
// Show the errors on the form
var error = response.error.message;
$form.find('button').prop('disabled', false);
$('#js-error-message').html(error).fadeIn();
} else {
// response contains id and card, which contains additional card details
var token = response.id;
// Insert the token into the form so it gets submitted to the server
$form.append($('<input type="hidden" name="stripeToken"/>').val(token));
// and submit
$form.get(0).submit();
}
};
</script>
@else
<script type="text/javascript">
$(function() {
$('.payment-form').submit(function(event) {
var $form = $(this);
// Disable the submit button to prevent repeated clicks
$form.find('button').prop('disabled', true);
return true;
});
});
</script>
@endif
@stop
@section('content')
@include('payments.payment_css')
{!! Former::vertical_open($url)
->autocomplete('on')
->addClass('payment-form')
->rules(array(
'first_name' => 'required',
'last_name' => 'required',
'card_number' => 'required',
'expiration_month' => 'required',
'expiration_year' => 'required',
'cvv' => 'required',
'address1' => 'required',
'city' => 'required',
'state' => 'required',
'postal_code' => 'required',
'country_id' => 'required',
'phone' => 'required',
'email' => 'required|email'
)) !!}
@if ($client)
{{ Former::populate($client) }}
{{ Former::populateField('first_name', $contact->first_name) }}
{{ Former::populateField('last_name', $contact->last_name) }}
{{ Former::populateField('email', $contact->email) }}
@if (!$client->country_id && $client->account->country_id)
{{ Former::populateField('country_id', $client->account->country_id) }}
@endif
@endif
@if (Utils::isNinjaDev())
{{ Former::populateField('first_name', 'Test') }}
{{ Former::populateField('last_name', 'Test') }}
{{ Former::populateField('address1', '350 5th Ave') }}
{{ Former::populateField('city', 'New York') }}
{{ Former::populateField('state', 'NY') }}
{{ Former::populateField('postal_code', '10118') }}
{{ Former::populateField('country_id', 840) }}
@endif
<div class="container">
<p>&nbsp;</p>
<div class="panel panel-default">
<div class="panel-body">
<div class="row">
<div class="col-md-7">
<header>
@if ($client)
<h2>{{ $client->getDisplayName() }}</h2>
<h3>{{ trans('texts.invoice') . ' ' . $invoiceNumber }}<span>|&nbsp; {{ trans('texts.amount_due') }}: <em>{{ $account->formatMoney($amount, $client, true) }}</em></span></h3>
@elseif ($paymentTitle)
<h2>{{ $paymentTitle }}<br/><small>{{ $paymentSubtitle }}</small></h2>
@endif
</header>
</div>
<div class="col-md-5">
@if (Request::secure() || Utils::isNinjaDev())
<div class="secure">
<h3>{{ trans('texts.secure_payment') }}</h3>
<div>{{ trans('texts.256_encryption') }}</div>
</div>
@endif
</div>
</div>
<p>&nbsp;<br/>&nbsp;</p>
<div>
<h3>{{ trans('texts.contact_information') }}</h3>
<div class="row">
<div class="col-md-6">
{!! Former::text('first_name')
->placeholder(trans('texts.first_name'))
->autocomplete('given-name')
->label('') !!}
</div>
<div class="col-md-6">
{!! Former::text('last_name')
->placeholder(trans('texts.last_name'))
->autocomplete('family-name')
->label('') !!}
</div>
</div>
<div class="row" style="display:{{ isset($paymentTitle) ? 'block' : 'none' }}">
<div class="col-md-12">
{!! Former::text('email')
->placeholder(trans('texts.email'))
->autocomplete('email')
->label('') !!}
</div>
</div>
<p>&nbsp;<br/>&nbsp;</p>
@if ($showAddress)
<h3>{{ trans('texts.billing_address') }} &nbsp;<span class="help">{{ trans('texts.payment_footer1') }}</span></h3>
<div class="row">
<div class="col-md-6">
{!! Former::text('address1')
->autocomplete('address-line1')
->placeholder(trans('texts.address1'))
->label('') !!}
</div>
<div class="col-md-6">
{!! Former::text('address2')
->autocomplete('address-line2')
->placeholder(trans('texts.address2'))
->label('') !!}
</div>
</div>
<div class="row">
<div class="col-md-6">
{!! Former::text('city')
->autocomplete('address-level2')
->placeholder(trans('texts.city'))
->label('') !!}
</div>
<div class="col-md-6">
{!! Former::text('state')
->autocomplete('address-level1')
->placeholder(trans('texts.state'))
->label('') !!}
</div>
</div>
<div class="row">
<div class="col-md-6">
{!! Former::text('postal_code')
->autocomplete('postal-code')
->placeholder(trans('texts.postal_code'))
->label('') !!}
</div>
<div class="col-md-6">
{!! Former::select('country_id')
->placeholder(trans('texts.country_id'))
->fromQuery($countries, 'name', 'id')
->addGroupClass('country-select')
->label('') !!}
</div>
</div>
<p>&nbsp;<br/>&nbsp;</p>
@endif
<h3>{{ trans('texts.billing_method') }}</h3>
<div class="row">
<div class="col-md-9">
{!! Former::text($accountGateway->getPublishableStripeKey() ? '' : 'card_number')
->id('card_number')
->placeholder(trans('texts.card_number'))
->autocomplete('cc-number')
->label('') !!}
</div>
<div class="col-md-3">
{!! Former::text($accountGateway->getPublishableStripeKey() ? '' : 'cvv')
->id('cvv')
->placeholder(trans('texts.cvv'))
->autocomplete('off')
->label('') !!}
</div>
</div>
<div class="row">
<div class="col-md-6">
{!! Former::select($accountGateway->getPublishableStripeKey() ? '' : 'expiration_month')
->id('expiration_month')
->autocomplete('cc-exp-month')
->placeholder(trans('texts.expiration_month'))
->addOption('01 - January', '1')
->addOption('02 - February', '2')
->addOption('03 - March', '3')
->addOption('04 - April', '4')
->addOption('05 - May', '5')
->addOption('06 - June', '6')
->addOption('07 - July', '7')
->addOption('08 - August', '8')
->addOption('09 - September', '9')
->addOption('10 - October', '10')
->addOption('11 - November', '11')
->addOption('12 - December', '12')->label('')
!!}
</div>
<div class="col-md-6">
{!! Former::select($accountGateway->getPublishableStripeKey() ? '' : 'expiration_year')
->id('expiration_year')
->autocomplete('cc-exp-year')
->placeholder(trans('texts.expiration_year'))
->addOption('2016', '2016')
->addOption('2017', '2017')
->addOption('2018', '2018')
->addOption('2019', '2019')
->addOption('2020', '2020')
->addOption('2021', '2021')
->addOption('2022', '2022')
->addOption('2023', '2023')
->addOption('2024', '2024')
->addOption('2025', '2025')
->addOption('2026', '2026')->label('')
!!}
</div>
</div>
<div class="row" style="padding-top:18px">
<div class="col-md-5">
@if ($client && $account->showTokenCheckbox())
<input id="token_billing" type="checkbox" name="token_billing" {{ $account->selectTokenCheckbox() ? 'CHECKED' : '' }} value="1" style="margin-left:0px; vertical-align:top">
<label for="token_billing" class="checkbox" style="display: inline;">{{ trans('texts.token_billing') }}</label>
<span class="help-block" style="font-size:15px">{!! trans('texts.token_billing_secure', ['stripe_link' => link_to('https://stripe.com/', 'Stripe.com', ['target' => '_blank'])]) !!}</span>
@endif
</div>
<div class="col-md-7">
@if (isset($acceptedCreditCardTypes))
<div class="pull-right">
@foreach ($acceptedCreditCardTypes as $card)
<img src="{{ $card['source'] }}" alt="{{ $card['alt'] }}" style="width: 70px; display: inline; margin-right: 6px;"/>
@endforeach
</div>
@endif
</div>
</div>
<p>&nbsp;</p>
<center>
{!! Button::success(strtoupper(trans('texts.pay_now') . ' - ' . $account->formatMoney($amount, $client, true) ))
->submit()
->large() !!}
</center>
<p>&nbsp;</p>
<div id="js-error-message" style="display:none" class="alert alert-danger"></div>
</div>
</div>
</div>
<p>&nbsp;</p>
<p>&nbsp;</p>
</div>
{!! Former::close() !!}
<script type="text/javascript">
$(function() {
$('select').change(function() {
$(this).css({color:'#444444'});
});
$('#country_id').combobox();
$('#first_name').focus();
});
</script>
@stop

View File

@ -12,9 +12,14 @@ body {
.container input[type=text],
.container input[type=email],
.container select {
.container select,
.braintree-hosted {
@if(!empty($account))
{!! $account->getBodyFontCss() !!}
@else
font-weight: 300;
font-family: 'Roboto', sans-serif;
@endif
width: 100%;
padding: 11px;
color: #8c8c8c;
@ -26,6 +31,12 @@ body {
font-weight: 400;
}
.form-control.braintree-hosted-fields-focused{
border-color: #66afe9;
outline: 0;
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);
}
div.col-md-3,
div.col-md-5,
div.col-md-6,
@ -102,8 +113,6 @@ header h3 em {
color: #eb8039;
}
.secure {
text-align: right;
float: right;
@ -125,6 +134,84 @@ header h3 em {
text-transform: uppercase;
}
#plaid_link_button img {
height:30px;
vertical-align:-7px;
margin-right:5px;
}
#plaid_link_button:hover img,
#plaid_link_button .hoverimg{
display:none;
}
#plaid_link_button:hover .hoverimg{
display:inline;
}
#plaid_link_button {
width:425px;
border-color:#2A5A74;
color:#2A5A74;
}
#plaid_link_button:hover {
width:425px;
background-color:#2A5A74;
color:#fff;
}
#plaid_or,
#plaid_container {
text-align:center
}
#plaid_or span{
background:#fff;
position:relative;
bottom:-11px;
font-size:125%;
padding:0 10px;
}
#plaid_or {
border-bottom:1px solid #000;
margin:10px 0 30px;
}
#secured_by_plaid{
position:fixed;
z-index:999999999;
bottom:5px;
left:5px;
color:#fff;
border:1px solid #fff;
padding:3px 7px 3px 3px;
border-radius:3px;
vertical-align:-5px;
text-decoration: none!important;
}
#secured_by_plaid img{
height:20px;
margin-right:5px;
}
#secured_by_plaid:hover{
background-color:#2A5A74;
}
#plaid_linked{
margin:40px 0;
display:none;
}
#plaid_linked_status {
margin-bottom:10px;
font-size:150%;
}
#bank_name {
margin:5px 0 -5px;
}
</style>

View File

@ -0,0 +1,9 @@
@extends('public.header')
@section('content')
<div class="container main-container">
<h3>{{ $title }}</h3>
@include('payments.paymentmethods_list')
<p></p>
</div>
@stop

View File

@ -0,0 +1,185 @@
<style type="text/css">
.payment_method_img_container{
width:37px;
text-align: center;
display:inline-block;
margin-right:10px;
}
.payment_method{
margin:20px 0;
}
.payment_method_number{
margin-right:10px;
width:65px;
display:inline-block;
}
</style>
@if (!empty($braintreeClientToken))
<script type="text/javascript" src="https://js.braintreegateway.com/js/braintree-2.23.0.min.js"></script>
<script type="text/javascript" >
$(function() {
var paypalLink = $('#add-paypal'),
paypalUrl = paypalLink.attr('href'),
checkout;
braintree.setup("{{ $braintreeClientToken }}", "custom", {
onReady: function (integration) {
checkout = integration;
},
paypal: {
container: "paypal-container",
singleUse: false,
enableShippingAddress: false,
enableBillingAddress: false,
headless: true,
locale: "{{$client->language?$client->language->locale:$client->account->language->locale}}"
},
dataCollector: {
paypal: true
},
onPaymentMethodReceived: function (obj) {
window.location.href = paypalUrl + '/' + encodeURIComponent(obj.nonce)
}
});
paypalLink.click(function(e){
e.preventDefault();
checkout.paypal.initAuthFlow();
})
});
</script>
@endif
@if(!empty($paymentMethods))
@foreach ($paymentMethods as $paymentMethod)
<div class="payment_method">
<span class="payment_method_img_container">
<img height="22" src="{{URL::to('/images/credit_cards/'.str_replace(' ', '', strtolower($paymentMethod->payment_type->name).'.png'))}}" alt="{{trans("texts.card_" . str_replace(' ', '', strtolower($paymentMethod->payment_type->name)))}}">
</span>
@if(!empty($paymentMethod->last4))
<span class="payment_method_number">&bull;&bull;&bull;&bull;&bull;{{$paymentMethod->last4}}</span>
@endif
@if($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH)
@if($paymentMethod->bank())
{{ $paymentMethod->bank()->name }}
@endif
@if($paymentMethod->status == PAYMENT_METHOD_STATUS_NEW)
<a href="javasript::void" onclick="completeVerification('{{$paymentMethod->public_id}}','{{$paymentMethod->currency->symbol}}')">({{trans('texts.complete_verification')}})</a>
@elseif($paymentMethod->status == PAYMENT_METHOD_STATUS_VERIFICATION_FAILED)
({{trans('texts.verification_failed')}})
@endif
@elseif($paymentMethod->payment_type_id == PAYMENT_TYPE_ID_PAYPAL)
{{ $paymentMethod->email }}
@elseif($paymentMethod->expiration)
{!! trans('texts.card_expiration', array('expires'=>Utils::fromSqlDate($paymentMethod->expiration, false)->format('m/y'))) !!}
@endif
@if($paymentMethod->id == $paymentMethod->account_gateway_token->default_payment_method_id)
({{trans('texts.used_for_auto_bill')}})
@elseif($paymentMethod->payment_type_id != PAYMENT_TYPE_ACH || $paymentMethod->status == PAYMENT_METHOD_STATUS_VERIFIED)
<a href="#" onclick="setDefault('{{$paymentMethod->public_id}}')">({{trans('texts.use_for_auto_bill')}})</a>
@endif
<a href="javasript::void" class="payment_method_remove" onclick="removePaymentMethod('{{$paymentMethod->public_id}}')">&times;</a>
</div>
@endforeach
@endif
<center>
{!! Button::success(strtoupper(trans('texts.add_credit_card')))
->asLinkTo(URL::to('/client/paymentmethods/add/'.($gateway->getPaymentType() == PAYMENT_TYPE_STRIPE ? 'stripe_credit_card' : 'credit_card'))) !!}
@if($gateway->getACHEnabled())
&nbsp;
{!! Button::success(strtoupper(trans('texts.add_bank_account')))
->asLinkTo(URL::to('/client/paymentmethods/add/stripe_ach')) !!}
@endif
@if($gateway->getPayPalEnabled())
&nbsp;
{!! Button::success(strtoupper(trans('texts.add_paypal_account')))
->withAttributes(['id'=>'add-paypal'])
->asLinkTo(URL::to('/client/paymentmethods/add/braintree_paypal')) !!}
<div id="paypal-container"></div>
@endif
</center>
<div class="modal fade" id="completeVerificationModal" tabindex="-1" role="dialog" aria-labelledby="completeVerificationModalLabel" aria-hidden="true">
<div class="modal-dialog" style="min-width:150px">
<div class="modal-content">
{!! Former::open('/client/paymentmethods/verify') !!}
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="completeVerificationModalLabel">{{ trans('texts.complete_verification') }}</h4>
</div>
<div class="modal-body">
<div style="display:none">
{!! Former::text('source_id') !!}
</div>
<p>{{ trans('texts.bank_account_verification_help') }}</p>
<div class="form-group">
<label for="verification1" class="control-label col-sm-5">{{ trans('texts.verification_amount1') }}</label>
<div class="col-sm-3">
<div class="input-group">
<span class="input-group-addon"><span class="payment_method_currenct_symbol"></span>0.</span>
<input type="number" min="0" max="99" required class="form-control" id="verification1" name="verification1">
</div>
</div>
</div>
<div class="form-group">
<label for="verification2" class="control-label col-sm-5">{{ trans('texts.verification_amount2') }}</label>
<div class="col-sm-3">
<div class="input-group">
<span class="input-group-addon"><span class="payment_method_currenct_symbol"></span>0.</span>
<input type="number" min="0" max="99" required class="form-control" id="verification2" name="verification2">
</div>
</div>
</div>
</div>
<div class="modal-footer" style="margin-top: 0px">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ trans('texts.cancel') }}</button>
<button type="submit" class="btn btn-primary">{{ trans('texts.complete_verification') }}</button>
</div>
{!! Former::close() !!}
</div>
</div>
</div>
<div class="modal fade" id="removePaymentMethodModal" tabindex="-1" role="dialog" aria-labelledby="removePaymentMethodModalLabel" aria-hidden="true">
<div class="modal-dialog" style="min-width:150px">
<div class="modal-content">
{!! Former::open()->id('removeForm') !!}
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="removePaymentMethodModalLabel">{{ trans('texts.remove_payment_method') }}</h4>
</div>
<div class="modal-body">
<p>{{ trans('texts.confirm_remove_payment_method') }}</p>
</div>
<div class="modal-footer" style="margin-top: 0px">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ trans('texts.cancel') }}</button>
<button type="submit" class="btn btn-primary">{{ trans('texts.remove') }}</button>
</div>
{!! Former::close() !!}
</div>
</div>
</div>
{!! Former::open(URL::to('/client/paymentmethods/default'))->id('defaultSourceForm') !!}
<input type="hidden" name="source" id="default_id">
{!! Former::close() !!}
<script type="text/javascript">
function completeVerification(sourceId, currencySymbol) {
$('#source_id').val(sourceId);
$('.payment_method_currenct_symbol').text(currencySymbol);
$('#completeVerificationModal').modal('show');
}
function removePaymentMethod(sourceId) {
$('#removeForm').attr('action', '{{ URL::to('/client/paymentmethods/%s/remove') }}'.replace('%s', sourceId))
$('#removePaymentMethodModal').modal('show');
}
function setDefault(sourceId) {
$('#default_id').val(sourceId);
$('#defaultSourceForm').submit()
}
</script>

View File

@ -7,9 +7,7 @@
<link href="//fonts.googleapis.com/css?family=Roboto:400,700,900,100" rel="stylesheet" type="text/css">
@endif
<link href="{{ asset('css/built.public.css') }}?no_cache={{ NINJA_VERSION }}" rel="stylesheet" type="text/css"/>
@if (!empty($clientViewCSS))
<style type="text/css">{!! $clientViewCSS !!}</style>
@endif
<style type="text/css">{!! isset($account)?$account->clientViewCSS():'' !!}</style>
@stop
@section('body')
@ -68,15 +66,15 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
@if (!isset($hideLogo) || !$hideLogo)
@if (!isset($account) || !$account->hasFeature(FEATURE_WHITE_LABEL))
{{-- Per our license, please do not remove or modify this link. --}}
<a class="navbar-brand" href="{{ URL::to(NINJA_WEB_URL) }}" target="_blank"><img src="{{ asset('images/invoiceninja-logo.png') }}" style="height:20px"></a>
@endif
</div>
<div id="navbar" class="collapse navbar-collapse">
@if (!isset($hideHeader) || !$hideHeader)
@if (!isset($account) || $account->isNinjaAccount() || $account->enable_client_portal)
<ul class="nav navbar-nav navbar-right">
@if (!isset($hideDashboard) || !$hideDashboard)
@if (!isset($account) || $account->enable_client_portal_dashboard)
<li {{ Request::is('*client/dashboard') ? 'class="active"' : '' }}>
{!! link_to('/client/dashboard', trans('texts.dashboard') ) !!}
</li>
@ -87,11 +85,16 @@
<li {{ Request::is('*client/invoices') ? 'class="active"' : '' }}>
{!! link_to('/client/invoices', trans('texts.invoices') ) !!}
</li>
@if (!empty($showDocuments))
@if (isset($account) && $account->hasFeature(FEATURE_DOCUMENTS))
<li {{ Request::is('*client/documents') ? 'class="active"' : '' }}>
{!! link_to('/client/documents', trans('texts.documents') ) !!}
</li>
@endif
@if (isset($account) && $account->getTokenGatewayId() && !$account->enable_client_portal_dashboard)
<li {{ Request::is('*client/paymentmethods') ? 'class="active"' : '' }}>
{!! link_to('/client/paymentmethods', trans('texts.payment_methods') ) !!}
</li>
@endif
<li {{ Request::is('*client/payments') ? 'class="active"' : '' }}>
{!! link_to('/client/payments', trans('texts.payments') ) !!}
</li>
@ -123,7 +126,7 @@
<footer id="footer" role="contentinfo">
<div class="top">
<div class="wrap">
@if (!isset($hideLogo) || !$hideLogo)
@if (!isset($account) || !$account->hasFeature(FEATURE_WHITE_LABEL))
<div id="footer-menu" class="menu-wrap">
<ul id="menu-footer-menu" class="menu">
<li id="menu-item-31" class="menu-item-31">
@ -146,7 +149,7 @@
<div class="bottom">
<div class="wrap">
@if (!isset($hideLogo) || !$hideLogo)
@if (!isset($account) || !$account->hasFeature(FEATURE_WHITE_LABEL))
<div class="copy">Copyright &copy;{{ date('Y') }} <a href="{{ NINJA_WEB_URL }}" target="_blank">Invoice Ninja</a>. All rights reserved.</div>
@endif
</div><!-- .wrap -->

View File

@ -35,7 +35,12 @@
</div>
-->
<h3>{{ $title }}</h3>
@if($entityType == ENTITY_INVOICE && $account->getTokenGatewayId() && $client->hasAutoBillConfigurableInvoices())
<div class="pull-right" style="margin-top:5px">
{!! Button::info(trans("texts.manage_auto_bill"))->asLinkTo(URL::to('/client/invoices/recurring'))->appendIcon(Icon::create('repeat')) !!}
</div>
@endif
<h3>{{ $title }}</h3>
{!! Datatable::table()
->addColumn($columns)
@ -45,6 +50,22 @@
</div>
@if($entityType == ENTITY_RECURRING_INVOICE)
{!! Former::open(URL::to('/client/invoices/auto_bill'))->id('auto_bill_form') !!}
<input type="hidden" name="public_id" id="auto_bill_public_id">
<input type="hidden" name="enable" id="auto_bill_enable">
{!! Former::close() !!}
<script type="text/javascript">
function setAutoBill(publicId, enable){
$('#auto_bill_public_id').val(publicId);
$('#auto_bill_enable').val(enable?'1':'0');
$('#auto_bill_form').submit();
}
</script>
@endif
<p>&nbsp;</p>
@stop