Handle failed payments

This commit is contained in:
Joshua Dwire 2016-04-30 17:54:56 -04:00
parent c536bd8569
commit ed1f2b6044
19 changed files with 317 additions and 57 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

@ -15,6 +15,7 @@ use Cache;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Invitation; use App\Models\Invitation;
use App\Models\Client; use App\Models\Client;
use App\Models\Account;
use App\Models\PaymentType; use App\Models\PaymentType;
use App\Models\License; use App\Models\License;
use App\Models\Payment; use App\Models\Payment;
@ -23,6 +24,7 @@ use App\Ninja\Repositories\PaymentRepository;
use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\InvoiceRepository;
use App\Ninja\Repositories\AccountRepository; use App\Ninja\Repositories\AccountRepository;
use App\Ninja\Mailers\ContactMailer; use App\Ninja\Mailers\ContactMailer;
use App\Ninja\Mailers\UserMailer;
use App\Services\PaymentService; use App\Services\PaymentService;
use App\Http\Requests\CreatePaymentRequest; use App\Http\Requests\CreatePaymentRequest;
@ -32,7 +34,7 @@ class PaymentController extends BaseController
{ {
protected $model = 'App\Models\Payment'; protected $model = 'App\Models\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(); // parent::__construct();
@ -41,6 +43,7 @@ class PaymentController extends BaseController
$this->accountRepo = $accountRepo; $this->accountRepo = $accountRepo;
$this->contactMailer = $contactMailer; $this->contactMailer = $contactMailer;
$this->paymentService = $paymentService; $this->paymentService = $paymentService;
$this->userMailer = $userMailer;
} }
public function index() public function index()
@ -784,4 +787,85 @@ class PaymentController extends BaseController
return null; return null;
} }
} }
public function handlePaymentWebhook($accountKey, $gatewayId)
{
$gatewayId = intval($gatewayId);
if ($gatewayId != GATEWAY_STRIPE) {
return response()->json([
'message' => 'Unsupported gateway',
], 404);
}
$account = Account::where('accounts.account_key', '=', $accountKey)->first();
if (!$account) {
return response()->json([
'message' => 'Unknown account',
], 404);
}
$eventId = Input::get('id');
$eventType= Input::get('type');
if (!$eventId) {
return response()->json([
'message' => 'Missing event id',
], 400);
}
if (!$eventType) {
return response()->json([
'message' => 'Missing event type',
], 400);
}
if (!in_array($eventType, array('charge.failed', 'charge.succeeded'))) {
return array('message' => 'Ignoring event');
}
$accountGateway = $account->getGatewayConfig(intval($gatewayId));
// 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'] && false) {
return response()->json([
'message' => 'This is not a pending event',
], 400);
}
$charge = $eventDetails['data']['object'];
$transactionRef = $charge['id'];
$payment = Payment::where('transaction_reference', '=', $transactionRef)->first();
if (!$payment) {
return array('message' => 'Unknown payment');
}
if ($eventType == 'charge.failed') {
if (!$payment->isFailed() || true) {
$payment->markFailed($charge['failure_message']);
$this->userMailer->sendNotification($payment->user, $payment->invoice, 'payment_failed', $payment);
}
} elseif ($eventType == 'charge.succeeded') {
$payment->markComplete();
}
return array('message' => 'Processed successfully');
}
} }

View File

@ -12,6 +12,7 @@ use Session;
use Datatable; use Datatable;
use Validator; use Validator;
use Cache; use Cache;
use Redirect;
use App\Models\Gateway; use App\Models\Gateway;
use App\Models\Invitation; use App\Models\Invitation;
use App\Models\Document; use App\Models\Document;
@ -783,6 +784,7 @@ class PublicClientController extends BaseController
return $this->returnError(); return $this->returnError();
} }
$typeLink = $paymentType;
$paymentType = 'PAYMENT_TYPE_' . strtoupper($paymentType); $paymentType = 'PAYMENT_TYPE_' . strtoupper($paymentType);
$client = $invitation->invoice->client; $client = $invitation->invoice->client;
$account = $client->account; $account = $client->account;
@ -799,12 +801,12 @@ class PublicClientController extends BaseController
} }
if(empty($sourceId)) { if(empty($sourceId)) {
$this->error('Token-No-Ref', $this->paymentService->lastError, $accountGateway); $this->paymentMethodError('Token-No-Ref', $this->paymentService->lastError, $accountGateway);
return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv')); return Redirect::to('client/paymentmethods/add/' . $typeLink)->withInput(Request::except('cvv'));
} else if ($paymentType == PAYMENT_TYPE_STRIPE_ACH && empty($usingPlaid) ) { } else if ($paymentType == PAYMENT_TYPE_STRIPE_ACH && empty($usingPlaid) ) {
// The user needs to complete verification // The user needs to complete verification
Session::flash('message', trans('texts.bank_account_verification_next_steps')); Session::flash('message', trans('texts.bank_account_verification_next_steps'));
return Redirect::to('client/paymentmethods/add/' . $paymentType); return Redirect::to('client/paymentmethods/');
} else { } else {
Session::flash('message', trans('texts.payment_method_added')); Session::flash('message', trans('texts.payment_method_added'));
return redirect()->to('/client/paymentmethods/'); return redirect()->to('/client/paymentmethods/');
@ -832,4 +834,16 @@ class PublicClientController extends BaseController
return redirect()->to('/client/paymentmethods/'); return redirect()->to('/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);
}
} }

View File

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

View File

@ -67,6 +67,7 @@ Route::group(['middleware' => 'auth:client'], function() {
Route::get('bank/{routing_number}', 'PaymentController@getBankInfo'); Route::get('bank/{routing_number}', 'PaymentController@getBankInfo');
Route::post('paymenthook/{accountKey}/{gatewayId}', 'PaymentController@handlePaymentWebhook');
Route::get('license', 'PaymentController@show_license_payment'); Route::get('license', 'PaymentController@show_license_payment');
Route::post('license', 'PaymentController@do_license_payment'); Route::post('license', 'PaymentController@do_license_payment');
Route::get('claim_license', 'PaymentController@claim_license'); Route::get('claim_license', 'PaymentController@claim_license');
@ -406,6 +407,7 @@ if (!defined('CONTACT_EMAIL')) {
define('ACTIVITY_TYPE_ARCHIVE_PAYMENT', 12); define('ACTIVITY_TYPE_ARCHIVE_PAYMENT', 12);
define('ACTIVITY_TYPE_DELETE_PAYMENT', 13); define('ACTIVITY_TYPE_DELETE_PAYMENT', 13);
define('ACTIVITY_TYPE_REFUNDED_PAYMENT', 39); define('ACTIVITY_TYPE_REFUNDED_PAYMENT', 39);
define('ACTIVITY_TYPE_FAILED_PAYMENT', 40);
define('ACTIVITY_TYPE_CREATE_CREDIT', 14); define('ACTIVITY_TYPE_CREATE_CREDIT', 14);
//define('ACTIVITY_TYPE_UPDATE_CREDIT', 15); //define('ACTIVITY_TYPE_UPDATE_CREDIT', 15);

View File

@ -25,6 +25,7 @@ use App\Events\PaymentWasDeleted;
use App\Events\PaymentWasRefunded; use App\Events\PaymentWasRefunded;
use App\Events\PaymentWasArchived; use App\Events\PaymentWasArchived;
use App\Events\PaymentWasRestored; use App\Events\PaymentWasRestored;
use App\Events\PaymentFailed;
use App\Events\CreditWasCreated; use App\Events\CreditWasCreated;
use App\Events\CreditWasDeleted; use App\Events\CreditWasDeleted;
use App\Events\CreditWasArchived; use App\Events\CreditWasArchived;
@ -322,6 +323,18 @@ class ActivityListener
); );
} }
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) public function archivedPayment(PaymentWasArchived $event)
{ {
if ($event->payment->is_deleted) { if ($event->payment->is_deleted) {

View File

@ -1,5 +1,6 @@
<?php namespace app\Listeners; <?php namespace app\Listeners;
use App\Events\PaymentFailed;
use Carbon; use Carbon;
use App\Models\Credit; use App\Models\Credit;
use App\Events\PaymentWasDeleted; use App\Events\PaymentWasDeleted;

View File

@ -9,6 +9,7 @@ use App\Events\PaymentWasCreated;
use App\Events\PaymentWasDeleted; use App\Events\PaymentWasDeleted;
use App\Events\PaymentWasRefunded; use App\Events\PaymentWasRefunded;
use App\Events\PaymentWasRestored; use App\Events\PaymentWasRestored;
use App\Events\PaymentFailed;
use App\Events\InvoiceInvitationWasViewed; use App\Events\InvoiceInvitationWasViewed;
class InvoiceListener class InvoiceListener
@ -75,6 +76,16 @@ class InvoiceListener
$invoice->updatePaidStatus(); $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) public function restoredPayment(PaymentWasRestored $event)
{ {
if ( ! $event->fromDeleted) { if ( ! $event->fromDeleted) {

View File

@ -4,6 +4,8 @@ use Event;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use App\Events\PaymentWasCreated; use App\Events\PaymentWasCreated;
use App\Events\PaymentWasRefunded; use App\Events\PaymentWasRefunded;
use App\Events\PaymentCompleted;
use App\Events\PaymentFailed;
use Laracasts\Presenter\PresentableTrait; use Laracasts\Presenter\PresentableTrait;
class Payment extends EntityModel class Payment extends EntityModel
@ -121,6 +123,21 @@ class Payment extends EntityModel
} }
} }
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() public function getEntityType()
{ {
return ENTITY_PAYMENT; return ENTITY_PAYMENT;

View File

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

View File

@ -112,6 +112,10 @@ class EventServiceProvider extends ServiceProvider {
'App\Listeners\InvoiceListener@refundedPayment', 'App\Listeners\InvoiceListener@refundedPayment',
'App\Listeners\CreditListener@refundedPayment', 'App\Listeners\CreditListener@refundedPayment',
], ],
'App\Events\PaymentFailed' => [
'App\Listeners\ActivityListener@failedPayment',
'App\Listeners\InvoiceListener@failedPayment',
],
'App\Events\PaymentWasRestored' => [ 'App\Events\PaymentWasRestored' => [
'App\Listeners\ActivityListener@restoredPayment', 'App\Listeners\ActivityListener@restoredPayment',
'App\Listeners\InvoiceListener@restoredPayment', 'App\Listeners\InvoiceListener@restoredPayment',

View File

@ -213,29 +213,15 @@ class PaymentService extends BaseService
return 'Unsupported gateway'; 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') { // Omnipay doesn't support verifying payment methods
return $body['error']['message']; // Also, it doesn't want to urlencode without putting numbers inside the brackets
} return $this->makeStripeCall(
$accountGateway,
return $e->getMessage(); 'POST',
} 'customers/'.$token.'/sources/'.$sourceId.'/verify',
'amounts[]='.intval($amount1).'&amounts[]='.intval($amount2)
);
} }
public function removeClientPaymentMethod($client, $sourceId) { public function removeClientPaymentMethod($client, $sourceId) {
@ -267,28 +253,13 @@ class PaymentService extends BaseService
$gateway = $this->createGateway($accountGateway); $gateway = $this->createGateway($accountGateway);
if ($accountGateway->gateway_id == GATEWAY_STRIPE) { 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 $this->makeStripeCall(
return $body['error']['message']; $accountGateway,
} 'POST',
'customers/'.$token,
return $e->getMessage(); 'default_card='.$sourceId
} );
} elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) {
} }
@ -312,14 +283,21 @@ class PaymentService extends BaseService
if ($accountGateway->gateway->id == GATEWAY_STRIPE) { if ($accountGateway->gateway->id == GATEWAY_STRIPE) {
$tokenResponse = $gateway->createCard($details)->send(); $tokenResponse = $gateway->createCard($details)->send();
$cardReference = $tokenResponse->getCardReference(); $sourceReference = $tokenResponse->getCardReference();
$customerReference = $tokenResponse->getCustomerReference(); $customerReference = $tokenResponse->getCustomerReference();
if ($customerReference == $cardReference) { if (!$sourceReference) {
$responseData = $tokenResponse->getData();
if ($responseData['object'] == 'bank_account' || $responseData['object'] == 'card') {
$sourceReference = $responseData['id'];
}
}
if ($customerReference == $sourceReference) {
// This customer was just created; find the card // This customer was just created; find the card
$data = $tokenResponse->getData(); $data = $tokenResponse->getData();
if (!empty($data['default_source'])) { if (!empty($data['default_source'])) {
$cardReference = $data['default_source']; $sourceReferebce = $data['default_source'];
} }
} }
} elseif ($accountGateway->gateway->id == GATEWAY_BRAINTREE) { } elseif ($accountGateway->gateway->id == GATEWAY_BRAINTREE) {
@ -356,7 +334,7 @@ class PaymentService extends BaseService
$this->lastError = $tokenResponse->getMessage(); $this->lastError = $tokenResponse->getMessage();
} }
return $cardReference; return $sourceReference;
} }
public function getCheckoutComToken($invitation) public function getCheckoutComToken($invitation)
@ -425,6 +403,8 @@ class PaymentService extends BaseService
$data = $purchaseResponse->getData(); $data = $purchaseResponse->getData();
$source = !empty($data['source'])?$data['source']:$data['card']; $source = !empty($data['source'])?$data['source']:$data['card'];
$payment->payment_status_id = $data['status'] == 'succeeded' ? PAYMENT_STATUS_COMPLETED : PAYMENT_STATUS_PENDING;
if ($source) { if ($source) {
$payment->last4 = $source['last4']; $payment->last4 = $source['last4'];
@ -794,4 +774,39 @@ class PaymentService extends BaseService
$payment->recordRefund($amount); $payment->recordRefund($amount);
} }
} }
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();
}
}
} }

4
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", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"hash": "039f9d8f2e342f6c05dadb3523883a47", "hash": "7139e4aedb2ac151079c50ee5c17f93c",
"content-hash": "fd558fd1e187969baf015eab8e288e5b", "content-hash": "a314d6c0a16785dd2395a7fd73cdc76d",
"packages": [ "packages": [
{ {
"name": "agmscode/omnipay-agms", "name": "agmscode/omnipay-agms",

View File

@ -31,6 +31,7 @@ class PaymentsChanges extends Migration
$table->unsignedInteger('routing_number')->nullable(); $table->unsignedInteger('routing_number')->nullable();
$table->smallInteger('last4')->unsigned()->nullable(); $table->smallInteger('last4')->unsigned()->nullable();
$table->date('expiration')->nullable(); $table->date('expiration')->nullable();
$table->text('gateway_error')->nullable();
}); });
} }
@ -50,6 +51,7 @@ class PaymentsChanges extends Migration
$table->dropColumn('routing_number'); $table->dropColumn('routing_number');
$table->dropColumn('last4'); $table->dropColumn('last4');
$table->dropColumn('expiration'); $table->dropColumn('expiration');
$table->dropColumn('gateway_error');
}); });
Schema::dropIfExists('payment_statuses'); Schema::dropIfExists('payment_statuses');

View File

@ -1251,7 +1251,14 @@ $LANG = array(
'payment_method_added' => 'Added payment method.', 'payment_method_added' => 'Added payment method.',
'use_for_auto_bill' => 'Use For Autobill', 'use_for_auto_bill' => 'Use For Autobill',
'used_for_auto_bill' => 'Autobill Payment Method', 'used_for_auto_bill' => 'Autobill Payment Method',
'payment_method_set_as_default' => 'Set Autobill payment method.' 'payment_method_set_as_default' => 'Set Autobill payment method.',
'activity_40' => ':payment_amount payment (:payment) failed',
'webhook_url' => 'Webhook URL',
'stripe_webhook_help' => 'You must :link for ACH payment status to be updated.',
'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.',
); );
return $LANG; return $LANG;

View File

@ -115,7 +115,6 @@
->addGroupClass('gateway-option') ->addGroupClass('gateway-option')
!!} !!}
<div class="stripe-ach"> <div class="stripe-ach">
@if ($account->getGatewayByType(PAYMENT_TYPE_DIRECT_DEBIT)) @if ($account->getGatewayByType(PAYMENT_TYPE_DIRECT_DEBIT))
{!! Former::checkbox('enable_ach') {!! Former::checkbox('enable_ach')
->label(trans('texts.ach')) ->label(trans('texts.ach'))
@ -128,7 +127,16 @@
->label(trans('texts.ach')) ->label(trans('texts.ach'))
->text(trans('texts.enable_ach')) ->text(trans('texts.enable_ach'))
->help(trans('texts.stripe_ach_help')) !!} ->help(trans('texts.stripe_ach_help')) !!}
<div class="stripe-plaid"> <div class="stripe-ach-options">
<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>
<div class="form-group"> <div class="form-group">
<div class="col-sm-8 col-sm-offset-4"> <div class="col-sm-8 col-sm-offset-4">
<h4>{{trans('texts.plaid')}}</h4> <h4>{{trans('texts.plaid')}}</h4>
@ -211,7 +219,7 @@
function enablePlaidSettings() { function enablePlaidSettings() {
var visible = $('#enable_ach').is(':checked'); var visible = $('#enable_ach').is(':checked');
$('.stripe-plaid').toggle(visible); $('.stripe-ach-options').toggle(visible);
} }
$(function() { $(function() {

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') !!}