1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-09 12:42:36 +01:00

Add basic ACH support

This commit is contained in:
Joshua Dwire 2016-04-29 17:50:21 -04:00
parent e5fb354643
commit c536bd8569
22 changed files with 1746 additions and 264 deletions

View File

@ -117,7 +117,7 @@ class AccountGatewayController extends BaseController
}
}
$paymentTypes[$type] = trans('texts.'.strtolower($type));
$paymentTypes[$type] = $type == PAYMENT_TYPE_CREDIT_CARD ? trans('texts.other_providers'): trans('texts.'.strtolower($type));
if ($type == PAYMENT_TYPE_BITCOIN) {
$paymentTypes[$type] .= ' - BitPay';

View File

@ -32,9 +32,8 @@ class AuthController 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();
}
}

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();
}
}
@ -116,9 +114,8 @@ 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();
}
}

View File

@ -137,7 +137,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();
@ -155,27 +155,31 @@ class PaymentController extends BaseController
if ($paymentType == PAYMENT_TYPE_TOKEN) {
$useToken = true;
$paymentType = PAYMENT_TYPE_CREDIT_CARD;
$accountGateway = $invoice->client->account->getTokenGateway();
$paymentType = $accountGateway->getPaymentType();
} else {
$accountGateway = $invoice->client->account->getGatewayByType($paymentType);
}
Session::put($invitation->id . 'payment_type', $paymentType);
$accountGateway = $invoice->client->account->getGatewayByType($paymentType);
$gateway = $accountGateway->gateway;
$acceptedCreditCardTypes = $accountGateway->getCreditcardTypes();
// Handle offsite payments
if ($useToken || $paymentType != PAYMENT_TYPE_CREDIT_CARD
$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) {
|| $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);
return self::do_payment($invitationKey, false, $useToken, $sourceId);
}
}
@ -189,22 +193,24 @@ class PaymentController extends BaseController
'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(),
'clientFontUrl' => $client->account->getFontsUrl(),
'showAddress' => $accountGateway->show_address,
];
if ($gateway->id = GATEWAY_BRAINTREE) {
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.payment', $data);
return View::make('payments.add_paymentmethod', $data);
}
public function show_license_payment()
@ -235,7 +241,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();
@ -260,7 +266,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()
@ -291,7 +297,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'));
@ -367,13 +373,14 @@ class PaymentController extends BaseController
}
}
public function do_payment($invitationKey, $onSite = true, $useToken = false)
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;
$accountGateway = $account->getGatewayByType(Session::get($invitation->id . 'payment_type'));
$paymentType = Session::get($invitation->id . 'payment_type');
$accountGateway = $account->getGatewayByType($paymentType);
$rules = [
@ -445,11 +452,20 @@ class PaymentController extends BaseController
if ($useToken) {
$details['customerReference'] = $client->getGatewayToken();
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);
if ($sourceId) {
$details['cardReference'] = $sourceId;
}
} 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 */);
if ($token) {
$details['token'] = $token;
$details['customerReference'] = $customerReference;
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('/client/paymentmethods');
}
} else {
$this->error('Token-No-Ref', $this->paymentService->lastError, $accountGateway);
return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv'));
@ -463,11 +479,15 @@ class PaymentController extends BaseController
if ($useToken) {
$details['customerId'] = $customerId = $client->getGatewayToken();
$customer = $gateway->findCustomer($customerId)->send();
$details['paymentMethodToken'] = $customer->getData()->paymentMethods[0]->token;
if (!$sourceId) {
$customer = $gateway->findCustomer($customerId)->send();
$details['paymentMethodToken'] = $customer->getData()->paymentMethods[0]->token;
} else {
$details['paymentMethodToken'] = $sourceId;
}
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);
$token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id, $customerReference/* return parameter */);
if ($token) {
$details['paymentMethodToken'] = $token;
$details['customerId'] = $customerReference;
@ -683,4 +703,85 @@ 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 = static::getBankData($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 static function getBankData($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 = array('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;
}
}
}

View File

@ -10,6 +10,8 @@ use Request;
use Response;
use Session;
use Datatable;
use Validator;
use Cache;
use App\Models\Gateway;
use App\Models\Invitation;
use App\Models\Document;
@ -94,7 +96,7 @@ class PublicClientController extends BaseController
$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);
@ -126,11 +128,6 @@ class PublicClientController extends BaseController
'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 +158,67 @@ 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['type']->id != PAYMENT_TYPE_ACH || $paymentMethod['status'] == 'verified') {
if ($paymentMethod['type']->id == PAYMENT_TYPE_ACH) {
$html = '<div>'.htmlentities($paymentMethod['bank_name']).'</div>';
} else {
$code = htmlentities(str_replace(' ', '', strtolower($paymentMethod['type']->name)));
$html = '<img height="22" src="'.URL::to('/images/credit_cards/'.$code.'.png').'" alt="'.trans("texts.card_".$code).'">';
}
if ($paymentMethod['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>';
} else {
$html .= '<div style="text-align:right">';
}
$html .= '&bull;&bull;&bull;'.$paymentMethod['last4'].'</div>';
$paymentTypes[] = [
'url' => URL::to("/payment/{$invitation->invitation_key}/token/".$paymentMethod['id']),
'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
];
}
$paymentTypes[] = [
'url' => $url, 'label' => trans('texts.'.strtolower($type))
];
}
}
@ -224,9 +265,6 @@ 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(),
];
@ -248,7 +286,7 @@ class PublicClientController extends BaseController
->addColumn('activity_type_id', function ($model) {
$data = [
'client' => Utils::getClientDisplayName($model),
'user' => $model->is_system ? ('<i>' . trans('texts.system') . '</i>') : ($model->user_first_name . ' ' . $model->user_last_name),
'user' => $model->is_system ? ('<i>' . trans('texts.system') . '</i>') : ($model->user_first_name . ' ' . $model->user_last_name),
'invoice' => trans('texts.invoice') . ' ' . $model->invoice,
'contact' => Utils::getClientDisplayName($model),
'payment' => trans('texts.payment') . ($model->payment ? ' ' . $model->payment : ''),
@ -280,10 +318,7 @@ 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,
'clientFontUrl' => $account->getFontsUrl(),
'title' => trans('texts.invoices'),
'entityType' => ENTITY_INVOICE,
@ -317,10 +352,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(),
'entityType' => ENTITY_PAYMENT,
'title' => trans('texts.payments'),
@ -345,8 +377,17 @@ class PublicClientController extends BaseController
if (!$model->last4) return '';
$code = str_replace(' ', '', strtolower($model->payment_type));
$card_type = trans("texts.card_" . $code);
$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;
if ($model->payment_type_id != PAYMENT_TYPE_ACH) {
$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;
} else {
$bankData = PaymentController::getBankData($model->routing_number);
if (is_array($bankData)) {
return $bankData['name'].'&nbsp; &bull;&bull;&bull;' . $model->last4;
} else {
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); })
@ -397,10 +438,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,
@ -435,10 +473,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,
@ -632,4 +667,169 @@ 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')
);
return response()->view('payments.paymentmethods', $data);
}
public function verifyPaymentMethod()
{
$sourceId = 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, $sourceId, $amount1, $amount2);
if (is_string($result)) {
Session::flash('error', $result);
} else {
Session::flash('message', trans('texts.payment_method_verified'));
}
return redirect()->to('/client/paymentmethods/');
}
public function removePaymentMethod($sourceId)
{
if (!$invitation = $this->getInvitation()) {
return $this->returnError();
}
$client = $invitation->invoice->client;
$result = $this->paymentService->removeClientPaymentMethod($client, $sourceId);
if (is_string($result)) {
Session::flash('error', $result);
} else {
Session::flash('message', trans('texts.payment_method_removed'));
}
return redirect()->to('/client/paymentmethods/');
}
public function addPaymentMethod($paymentType)
{
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;
$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,
];
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();
}
$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');
if ($sourceToken) {
$details = array('token' => $sourceToken);
$gateway = $this->paymentService->createGateway($accountGateway);
$sourceId = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id);
} else {
return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv'));
}
if(empty($sourceId)) {
$this->error('Token-No-Ref', $this->paymentService->lastError, $accountGateway);
return Redirect::to('payment/'.$invitationKey)->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('client/paymentmethods/add/' . $paymentType);
} else {
Session::flash('message', trans('texts.payment_method_added'));
return redirect()->to('/client/paymentmethods/');
}
}
public function setDefaultPaymentMethod(){
if (!$invitation = $this->getInvitation()) {
return $this->returnError();
}
$validator = Validator::make(Input::all(), array('source' => 'required'));
if ($validator->fails()) {
return Redirect::to('client/paymentmethods');
}
$client = $invitation->invoice->client;
$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/paymentmethods/');
}
}

View File

@ -40,9 +40,15 @@ 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}', '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/documents', 'PublicClientController@documentIndex');
@ -60,6 +66,7 @@ Route::group(['middleware' => 'auth:client'], function() {
});
Route::get('bank/{routing_number}', 'PaymentController@getBankInfo');
Route::get('license', 'PaymentController@show_license_payment');
Route::post('license', 'PaymentController@do_license_payment');
Route::get('claim_license', 'PaymentController@claim_license');
@ -615,6 +622,7 @@ if (!defined('CONTACT_EMAIL')) {
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);
@ -633,6 +641,8 @@ if (!defined('CONTACT_EMAIL')) {
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_CREDIT_CARD', 'PAYMENT_TYPE_CREDIT_CARD');
define('PAYMENT_TYPE_DIRECT_DEBIT', 'PAYMENT_TYPE_DIRECT_DEBIT');
define('PAYMENT_TYPE_BITCOIN', 'PAYMENT_TYPE_BITCOIN');

View File

@ -379,6 +379,10 @@ 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;
}
foreach ($this->account_gateways as $gateway) {
if (!$type || $type == PAYMENT_TYPE_ANY) {
return $gateway;

View File

@ -56,6 +56,7 @@ class PaymentRepository extends BaseRepository
'payments.refunded',
'payments.expiration',
'payments.last4',
'payments.routing_number',
'invoices.is_deleted as invoice_is_deleted',
'gateways.name as gateway_name',
'gateways.id as gateway_id',
@ -107,6 +108,7 @@ 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',
@ -117,6 +119,7 @@ class PaymentRepository extends BaseRepository
'payments.refunded',
'payments.expiration',
'payments.last4',
'payments.routing_number',
'payments.payment_status_id',
'payment_statuses.name as payment_status_name'
);

View File

@ -5,6 +5,7 @@ use Auth;
use URL;
use DateTime;
use Event;
use Cache;
use Omnipay;
use Session;
use CreditCard;
@ -13,6 +14,7 @@ use App\Models\Account;
use App\Models\Country;
use App\Models\Client;
use App\Models\Invoice;
use App\Http\Controllers\PaymentController;
use App\Models\AccountGatewayToken;
use App\Ninja\Repositories\PaymentRepository;
use App\Ninja\Repositories\AccountRepository;
@ -155,8 +157,159 @@ class PaymentService extends BaseService
];
}
public function getClientPaymentMethods($client) {
$token = $client->getGatewayToken($accountGateway);
if (!$token) {
return null;
}
$gateway = $this->createGateway($accountGateway);
$paymentMethods = array();
if ($accountGateway->gateway_id == GATEWAY_STRIPE) {
$response = $gateway->fetchCustomer(array('customerReference' => $token))->send();
if (!$response->isSuccessful()) {
return null;
}
$data = $response->getData();
$default_source = $data['default_source'];
$sources = isset($data['sources']) ? $data['sources']['data'] : $data['cards']['data'];
$paymentTypes = Cache::get('paymentTypes');
$currencies = Cache::get('currencies');
foreach ($sources as $source) {
if ($source['object'] == 'bank_account') {
$paymentMethods[] = array(
'id' => $source['id'],
'default' => $source['id'] == $default_source,
'type' => $paymentTypes->find(PAYMENT_TYPE_ACH),
'currency' => $currencies->where('code', strtoupper($source['currency']))->first(),
'last4' => $source['last4'],
'routing_number' => $source['routing_number'],
'bank_name' => $source['bank_name'],
'status' => $source['status'],
);
} elseif ($source['object'] == 'card') {
$paymentMethods[] = array(
'id' => $source['id'],
'default' => $source['id'] == $default_source,
'type' => $paymentTypes->find($this->parseCardType($source['brand'])),
'last4' => $source['last4'],
'expiration' => $source['exp_year'] . '-' . $source['exp_month'] . '-00',
);
}
}
} elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) {
}
return $paymentMethods;
}
public function verifyClientPaymentMethod($client, $sourceId, $amount1, $amount2) {
$token = $client->getGatewayToken($accountGateway);
if ($accountGateway->gateway_id != GATEWAY_STRIPE) {
return 'Unsupported gateway';
}
try{
// Omnipay doesn't support verifying payment methods
// Also, it doesn't want to urlencode without putting numbers inside the brackets
$response = (new \GuzzleHttp\Client(['base_uri'=>'https://api.stripe.com/v1/']))->request(
'POST',
'customers/'.$token.'/sources/'.$sourceId.'/verify',
[
'body' => 'amounts[]='.intval($amount1).'&amounts[]='.intval($amount2),
'headers' => ['content-type' => 'application/x-www-form-urlencoded'],
'auth' => [$accountGateway->getConfig()->apiKey,''],
]
);
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();
}
}
public function removeClientPaymentMethod($client, $sourceId) {
$token = $client->getGatewayToken($accountGateway/* return parameter */);
if (!$token) {
return null;
}
$gateway = $this->createGateway($accountGateway);
if ($accountGateway->gateway_id == GATEWAY_STRIPE) {
$response = $gateway->deleteCard(array('customerReference' => $token, 'cardReference'=>$sourceId))->send();
if (!$response->isSuccessful()) {
return $response->getMessage();
}
} elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) {
}
return true;
}
public function setClientDefaultPaymentMethod($client, $sourceId) {
$token = $client->getGatewayToken($accountGateway/* return parameter */);
if (!$token) {
return null;
}
$gateway = $this->createGateway($accountGateway);
if ($accountGateway->gateway_id == GATEWAY_STRIPE) {
try{
// Omnipay doesn't support setting a default source
$response = (new \GuzzleHttp\Client(['base_uri'=>'https://api.stripe.com/v1/']))->request(
'POST',
'customers/'.$token,
[
'body' => 'default_card='.$sourceId,
'headers' => ['content-type' => 'application/x-www-form-urlencoded'],
'auth' => [$accountGateway->getConfig()->apiKey,''],
]
);
return 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();
}
} elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) {
}
return true;
}
public function createToken($gateway, $details, $accountGateway, $client, $contactId, &$customerReference = null)
{
$customerReference = $client->getGatewayToken();
if ($customerReference) {
$details['customerReference'] = $customerReference;
$customerResponse = $gateway->fetchCustomer(array('customerReference'=>$customerReference))->send();
if (!$customerResponse->isSuccessful()){
$customerReference = null; // The customer might not exist anymore
}
}
if ($accountGateway->gateway->id == GATEWAY_STRIPE) {
$tokenResponse = $gateway->createCard($details)->send();
$cardReference = $tokenResponse->getCardReference();
@ -170,10 +323,15 @@ class PaymentService extends BaseService
}
}
} elseif ($accountGateway->gateway->id == GATEWAY_BRAINTREE) {
$tokenResponse = $gateway->createCustomer(array('customerData'=>array()))->send();
if (!$customerReference) {
$tokenResponse = $gateway->createCustomer(array('customerData' => array()))->send();
if ($tokenResponse->isSuccessful()) {
$customerReference = $tokenResponse->getCustomerData()->id;
}
}
if ($tokenResponse->isSuccessful()) {
$details['customerId'] = $customerReference = $tokenResponse->getCustomerData()->id;
if ($customerReference) {
$details['customerId'] = $customerReference;
$tokenResponse = $gateway->createPaymentMethod($details)->send();
$cardReference = $tokenResponse->getData()->paymentMethod->token;
@ -264,54 +422,26 @@ class PaymentService extends BaseService
}
if ($accountGateway->gateway_id == GATEWAY_STRIPE) {
$card = $purchaseResponse->getSource();
if (!$card) {
$card = $purchaseResponse->getCard();
}
$data = $purchaseResponse->getData();
$source = !empty($data['source'])?$data['source']:$data['card'];
if ($card) {
$payment->last4 = $card['last4'];
$payment->expiration = $card['exp_year'] . '-' . $card['exp_month'] . '-00';
$stripe_card_types = 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,
);
if ($source) {
$payment->last4 = $source['last4'];
if (!empty($stripe_card_types[$card['brand']])) {
$payment->payment_type_id = $stripe_card_types[$card['brand']];
} else {
$payment->payment_type_id = PAYMENT_TYPE_CREDIT_CARD_OTHER;
if ($source['object'] == 'bank_account') {
$payment->routing_number = $source['routing_number'];
$payment->payment_type_id = PAYMENT_TYPE_ACH;
}
else{
$payment->expiration = $card['exp_year'] . '-' . $card['exp_month'] . '-00';
$payment->payment_type_id = $this->parseCardType($card['brand']);
}
}
} elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) {
$card = $purchaseResponse->getData()->transaction->creditCardDetails;
$payment->last4 = $card->last4;
$payment->expiration = $card->expirationYear . '-' . $card->expirationMonth . '-00';
$braintree_card_types = 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($braintree_card_types[$card->cardType])) {
$payment->payment_type_id = $braintree_card_types[$card->cardType];
} else {
$payment->payment_type_id = PAYMENT_TYPE_CREDIT_CARD_OTHER;
}
$payment->payment_type_id = $this->parseCardType($card->cardType);
}
if ($payerId) {
@ -375,6 +505,29 @@ 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)
{
@ -493,12 +646,21 @@ class PaymentService extends BaseService
],
[
'source',
function ($model) {
function ($model) {
if (!$model->last4) return '';
$code = str_replace(' ', '', strtolower($model->payment_type));
$card_type = trans("texts.card_" . $code);
$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;
if ($model->payment_type_id != PAYMENT_TYPE_ACH) {
$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;
} else {
$bankData = PaymentController::getBankData($model->routing_number);
if (is_array($bankData)) {
return $bankData['name'].'&nbsp; &bull;&bull;&bull;' . $model->last4;
} else {
return '<img height="22" src="' . URL::to('/images/credit_cards/ach.png') . '" alt="' . htmlentities($card_type) . '">&nbsp; &bull;&bull;&bull;' . $model->last4;
}
}
}
],
[

View File

@ -17,7 +17,7 @@
"omnipay/mollie": "dev-master#22956c1a62a9662afa5f5d119723b413770ac525",
"omnipay/2checkout": "dev-master#e9c079c2dde0d7ba461903b3b7bd5caf6dee1248",
"omnipay/gocardless": "dev-master",
"omnipay/stripe": "2.3.2",
"omnipay/stripe": "dev-master",
"laravel/framework": "5.2.*",
"laravelcollective/html": "5.2.*",
"laravelcollective/bus": "5.2.*",
@ -73,7 +73,8 @@
"league/flysystem-aws-s3-v3": "~1.0",
"league/flysystem-rackspace": "~1.0",
"barracudanetworks/archivestream-php": "^1.0",
"omnipay/braintree": "~2.0@dev"
"omnipay/braintree": "~2.0@dev",
"gatepay/FedACHdir": "dev-master@dev"
},
"require-dev": {
"phpunit/phpunit": "~4.0",
@ -122,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"
}
}
}
]
}

29
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": "e2b43d6bd5e87dfb9b3be07e89a2bd9f",
"content-hash": "480134957ff37fd0f46b5d90909da660",
"hash": "039f9d8f2e342f6c05dadb3523883a47",
"content-hash": "fd558fd1e187969baf015eab8e288e5b",
"packages": [
{
"name": "agmscode/omnipay-agms",
@ -2011,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",
@ -5771,16 +5782,16 @@
},
{
"name": "omnipay/stripe",
"version": "v2.3.2",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/omnipay-stripe.git",
"reference": "fe05ddd73d9ae38ca026dbc270ca98aaf3f99da4"
"reference": "0ea7a647ee01e29c152814e11c2ea6307e5b0db9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/omnipay-stripe/zipball/fe05ddd73d9ae38ca026dbc270ca98aaf3f99da4",
"reference": "fe05ddd73d9ae38ca026dbc270ca98aaf3f99da4",
"url": "https://api.github.com/repos/thephpleague/omnipay-stripe/zipball/0ea7a647ee01e29c152814e11c2ea6307e5b0db9",
"reference": "0ea7a647ee01e29c152814e11c2ea6307e5b0db9",
"shasum": ""
},
"require": {
@ -5824,7 +5835,7 @@
"payment",
"stripe"
],
"time": "2016-03-19 08:35:06"
"time": "2016-04-26 08:34:50"
},
{
"name": "omnipay/targetpay",
@ -9985,6 +9996,7 @@
"omnipay/mollie": 20,
"omnipay/2checkout": 20,
"omnipay/gocardless": 20,
"omnipay/stripe": 20,
"anahkiasen/former": 20,
"chumper/datatable": 20,
"intervention/image": 20,
@ -10005,7 +10017,8 @@
"labs7in0/omnipay-wechat": 20,
"laracasts/presenter": 20,
"jlapp/swaggervel": 20,
"omnipay/braintree": 20
"omnipay/braintree": 20,
"gatepay/fedachdir": 20
},
"prefer-stable": false,
"prefer-lowest": false,

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -502,7 +502,7 @@ $LANG = array(
'resend_confirmation' => 'Resend confirmation email',
'confirmation_resent' => 'The confirmation email was resent',
'gateway_help_42' => ':link to sign up for BitPay.<br/>Note: use a Legacy API Key, not an API token.',
'payment_type_credit_card' => 'Other Providers',
'payment_type_credit_card' => 'Credit Card',
'payment_type_paypal' => 'PayPal',
'payment_type_bitcoin' => 'Bitcoin',
'knowledge_base' => 'Knowledge Base',
@ -1205,6 +1205,7 @@ $LANG = array(
'card_solo' => 'Solo',
'card_switch' => 'Switch',
'card_visacard' => 'Visa',
'card_ach' => 'ACH',
'payment_type_stripe' => 'Stripe',
'ach' => 'ACH',
@ -1218,6 +1219,39 @@ $LANG = array(
'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.'
);
return $LANG;

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

@ -14,6 +14,12 @@
body {
background-color: #f8f8f8;
}
.dropdown-menu li a{
overflow:hidden;
margin-top:5px;
margin-bottom:5px;
}
</style>
@stop

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="{{App::getLocale()}}">
<head>
@if (isset($hideLogo) && $hideLogo)
@if (isset($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,611 @@
@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) {
// 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) {
$('#js-error-message').hide();
});
});
</script>
@elseif ($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 = {
@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;
});
});
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();
}
};
</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::vertical_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'
)) !!}
@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) }}
@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)
<h2>{{ $client->getDisplayName() }}</h2>
@if(isset($invoiceNumber))
<h3>{{ trans('texts.invoice') . ' ' . $invoiceNumber }}<span>|&nbsp; {{ trans('texts.amount_due') }}: <em>{{ $account->formatMoney($amount, $client, true) }}</em></span></h3>
@endif
@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>
@if (isset($paymentTitle))
<div class="row">
<div class="col-md-12">
{!! Former::text('email')
->placeholder(trans('texts.email'))
->autocomplete('email')
->label('') !!}
</div>
</div>
@endif
<p>&nbsp;<br/>&nbsp;</p>
@if ($showAddress)
<h3>{{ trans('texts.billing_address') }}
@if($paymentType != PAYMENT_TYPE_STRIPE_ACH)
&nbsp;<span class="help">{{ trans('texts.payment_footer1') }}</span></h3>
@endif
<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>
@if($paymentType == PAYMENT_TYPE_STRIPE_ACH)
<p>{{ trans('texts.ach_verification_delay_help') }}</p>
<div class="row">
<div class="col-md-6">
<div class="radio">
{!! Former::radios('account_holder_type')->radios(array(
trans('texts.individual_account') => array('value' => 'individual'),
trans('texts.company_account') => array('value' => 'company'),
))->inline()->label(''); !!}
</div>
</div>
<div class="col-md-6">
{!! Former::text('account_holder_name')
->placeholder(trans('texts.account_holder_name'))
->label('') !!}
</div>
</div>
<div class="row">
<div class="col-md-6">
{!! Former::select('country')
->placeholder(trans('texts.country_id'))
->fromQuery($countries, 'name', 'iso_3166_2')
->addGroupClass('country-select')
->label('') !!}
</div>
<div class="col-md-6">
{!! Former::select('currency')
->placeholder(trans('texts.currency_id'))
->fromQuery($currencies, 'name', 'code')
->addGroupClass('currency-select')
->label('') !!}
</div>
</div>
<div class="row">
<div class="col-md-6">
{!! Former::text('')
->id('routing_number')
->placeholder(trans('texts.routing_number'))
->label('') !!}
</div>
<div class="col-md-6">
<div id="bank_name"></div>
</div>
</div>
<div class="row">
<div class="col-md-6">
{!! Former::text('')
->id('account_number')
->placeholder(trans('texts.account_number'))
->label('') !!}
</div>
<div class="col-md-6">
{!! Former::text('')
->id('confirm_account_number')
->placeholder(trans('texts.confirm_account_number'))
->label('') !!}
</div>
</div>
<center>
{!! Button::success(strtoupper(trans('texts.add_account')))
->submit()
->large() !!}
</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)).add('<br>').add(document.createTextNode(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>
@stop

View File

@ -67,6 +67,14 @@
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(),
@ -78,27 +86,56 @@
cvc: $('#cvv').val(),
exp_month: $('#expiration_month').val(),
exp_year: $('#expiration_year').val()
@endif
};
// 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;
}
@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;
@ -111,6 +148,11 @@
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 {
@ -144,6 +186,21 @@
@include('payments.payment_css')
@if($paymentType == PAYMENT_TYPE_STRIPE_ACH)
{!! Former::vertical_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'
)) !!}
@else
{!! Former::vertical_open($url)
->autocomplete('on')
->addClass('payment-form')
@ -163,6 +220,7 @@
'phone' => 'required',
'email' => 'required|email'
)) !!}
@endif
@if ($client)
{{ Former::populate($client) }}
@ -170,6 +228,11 @@
{{ Former::populateField('last_name', $contact->last_name) }}
@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
@ -243,7 +306,10 @@
<p>&nbsp;<br/>&nbsp;</p>
@if ($showAddress)
<h3>{{ trans('texts.billing_address') }} &nbsp;<span class="help">{{ trans('texts.payment_footer1') }}</span></h3>
<h3>{{ trans('texts.billing_address') }}
@if($paymentType != PAYMENT_TYPE_STRIPE_ACH)
&nbsp;<span class="help">{{ trans('texts.payment_footer1') }}</span></h3>
@endif
<div class="row">
<div class="col-md-6">
{!! Former::text('address1')
@ -292,113 +358,178 @@
@endif
<h3>{{ trans('texts.billing_method') }}</h3>
<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 ($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
@if($paymentType == PAYMENT_TYPE_STRIPE_ACH)
<p>{{ trans('texts.ach_verification_delay_help') }}</p>
<div class="row">
<div class="col-md-6">
<div class="radio">
{!! Former::radios('account_holder_type')->radios(array(
trans('texts.individual_account') => array('value' => 'individual'),
trans('texts.company_account') => array('value' => 'company'),
))->inline()->label(''); !!}
</div>
</div>
<div class="col-md-6">
{!! Former::text('account_holder_name')
->placeholder(trans('texts.account_holder_name'))
->label('') !!}
</div>
@endif
</div>
</div>
<div class="row">
<div class="col-md-6">
{!! Former::select('country')
->placeholder(trans('texts.country_id'))
->fromQuery($countries, 'name', 'iso_3166_2')
->addGroupClass('country-select')
->label('') !!}
</div>
<div class="col-md-6">
{!! Former::select('currency')
->placeholder(trans('texts.currency_id'))
->fromQuery($currencies, 'name', 'code')
->addGroupClass('currency-select')
->label('') !!}
</div>
</div>
<div class="row">
<div class="col-md-6">
{!! Former::text('')
->id('routing_number')
->placeholder(trans('texts.routing_number'))
->label('') !!}
</div>
<div class="col-md-6">
<div id="bank_name"></div>
</div>
</div>
<div class="row">
<div class="col-md-6">
{!! Former::text('')
->id('account_number')
->placeholder(trans('texts.account_number'))
->label('') !!}
</div>
<div class="col-md-6">
{!! Former::text('')
->id('confirm_account_number')
->placeholder(trans('texts.confirm_account_number'))
->label('') !!}
</div>
</div>
<center>
{!! Button::success(strtoupper(trans('texts.add_account')))
->submit()
->large() !!}
</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 ($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>
{!! Button::success(strtoupper(trans('texts.pay_now') . ' - ' . $account->formatMoney($amount, $client, true) ))
->submit()
->large() !!}
</center>
<p>&nbsp;</p>
<p>&nbsp;</p>
<center>
{!! Button::success(strtoupper(trans('texts.pay_now') . ' - ' . $account->formatMoney($amount, $client, true) ))
->submit()
->large() !!}
</center>
<p>&nbsp;</p>
@endif
<div id="js-error-message" style="display:none" class="alert alert-danger"></div>
</div>
@ -422,7 +553,47 @@
});
$('#country_id').combobox();
$('#country').combobox();
$('#currency').combobox();
$('#first_name').focus();
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)).add('<br>').add(document.createTextNode(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)})
});
</script>

View File

@ -0,0 +1,149 @@
@extends('public.header')
@section('content')
<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>
<div class="container main-container">
<h3>{{ $title }}</h3>
@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['type']->name).'.png'))}}" alt="{{trans("texts.card_" . str_replace(' ', '', strtolower($paymentMethod['type']->name)))}}">
</span>
<span class="payment_method_number">&bull;&bull;&bull;&bull;&bull;{{$paymentMethod['last4']}}</span>
@if($paymentMethod['type']->id == PAYMENT_TYPE_ACH)
{{ $paymentMethod['bank_name'] }}
@if($paymentMethod['status'] == 'new')
<a href="javasript::void" onclick="completeVerification('{{$paymentMethod['id']}}','{{$paymentMethod['currency']->symbol}}')">({{trans('texts.complete_verification')}})</a>
@elseif($paymentMethod['status'] == 'verification_failed')
({{trans('texts.verification_failed')}})
@endif
@else
{!! trans('texts.card_expiration', array('expires'=>Utils::fromSqlDate($paymentMethod['expiration'], false)->format('m/y'))) !!}
@endif
@if($paymentMethod['default'])
({{trans('texts.used_for_auto_bill')}})
@elseif($paymentMethod['type']->id != PAYMENT_TYPE_ACH || $paymentMethod['status'] == 'verified')
<a href="#" onclick="setDefault('{{$paymentMethod['id']}}')">({{trans('texts.use_for_auto_bill')}})</a>
@endif
<a href="javasript::void" class="payment_method_remove" onclick="removePaymentMethod('{{$paymentMethod['id']}}')">&times;</a>
</div>
@endforeach
<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
</center>
<p></p>
</div>
<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>
@stop

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 ($account->hasFeature(FEATURE_DOCUMENTS))
<li {{ Request::is('*client/documents') ? 'class="active"' : '' }}>
{!! link_to('/client/documents', trans('texts.documents') ) !!}
</li>
@endif
@if ($account->getTokenGatewayId())
<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 -->