mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
Handle failed payments
This commit is contained in:
parent
c536bd8569
commit
ed1f2b6044
23
app/Events/PaymentCompleted.php
Normal file
23
app/Events/PaymentCompleted.php
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
23
app/Events/PaymentFailed.php
Normal file
23
app/Events/PaymentFailed.php
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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/*',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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);
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
@ -213,29 +213,15 @@ class PaymentService extends BaseService
|
|||||||
return 'Unsupported gateway';
|
return 'Unsupported gateway';
|
||||||
}
|
}
|
||||||
|
|
||||||
try{
|
|
||||||
// Omnipay doesn't support verifying payment methods
|
// Omnipay doesn't support verifying payment methods
|
||||||
// Also, it doesn't want to urlencode without putting numbers inside the brackets
|
// 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(
|
return $this->makeStripeCall(
|
||||||
|
$accountGateway,
|
||||||
'POST',
|
'POST',
|
||||||
'customers/'.$token.'/sources/'.$sourceId.'/verify',
|
'customers/'.$token.'/sources/'.$sourceId.'/verify',
|
||||||
[
|
'amounts[]='.intval($amount1).'&amounts[]='.intval($amount2)
|
||||||
'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) {
|
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
|
return $this->makeStripeCall(
|
||||||
$response = (new \GuzzleHttp\Client(['base_uri'=>'https://api.stripe.com/v1/']))->request(
|
$accountGateway,
|
||||||
'POST',
|
'POST',
|
||||||
'customers/'.$token,
|
'customers/'.$token,
|
||||||
[
|
'default_card='.$sourceId
|
||||||
'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) {
|
} 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
4
composer.lock
generated
@ -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",
|
||||||
|
@ -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');
|
||||||
|
@ -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;
|
||||||
|
@ -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() {
|
||||||
|
26
resources/views/emails/invoice_payment_failed_html.blade.php
Normal file
26
resources/views/emails/invoice_payment_failed_html.blade.php
Normal 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>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{ trans("texts.notification_invoice_payment_failed", ['amount' => $paymentAmount, 'client' => $clientName, 'invoice' => $invoiceNumber]) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{ $payment->gateway_error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{ trans('texts.email_signature') }} <br/>
|
||||||
|
{{ trans('texts.email_from') }}
|
||||||
|
</div>
|
||||||
|
@stop
|
@ -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') !!}
|
Loading…
x
Reference in New Issue
Block a user