mirror of
				https://github.com/invoiceninja/invoiceninja.git
				synced 2025-11-03 22:07:33 -05:00 
			
		
		
		
	Merge branch 'stripe_ach_refactor' into v5-develop
This commit is contained in:
		
						commit
						6a17a58df3
					
				@ -41,7 +41,7 @@ class PaymentsTable extends Component
 | 
			
		||||
    {
 | 
			
		||||
        $query = Payment::query()
 | 
			
		||||
            ->with('type', 'client')
 | 
			
		||||
            ->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment::STATUS_PENDING, Payment::STATUS_REFUNDED, Payment::STATUS_PARTIALLY_REFUNDED])
 | 
			
		||||
            ->whereIn('status_id', [Payment::STATUS_FAILED, Payment::STATUS_COMPLETED, Payment::STATUS_PENDING, Payment::STATUS_REFUNDED, Payment::STATUS_PARTIALLY_REFUNDED])
 | 
			
		||||
            ->where('company_id', $this->company->id)
 | 
			
		||||
            ->where('client_id', auth()->guard('contact')->user()->client->id)
 | 
			
		||||
            ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
 | 
			
		||||
 | 
			
		||||
@ -79,7 +79,9 @@ class StoreInvoiceRequest extends Request
 | 
			
		||||
 | 
			
		||||
        $input = $this->decodePrimaryKeys($input);
 | 
			
		||||
 | 
			
		||||
        if (isset($input['line_items']) && is_array($input['line_items'])) 
 | 
			
		||||
            $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
 | 
			
		||||
        
 | 
			
		||||
        $input['amount'] = 0;
 | 
			
		||||
        $input['balance'] = 0;
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
@ -76,7 +76,7 @@ class UpdateInvoiceRequest extends Request
 | 
			
		||||
 | 
			
		||||
        $input['id'] = $this->invoice->id;
 | 
			
		||||
        
 | 
			
		||||
        if (isset($input['line_items'])) {
 | 
			
		||||
        if (isset($input['line_items']) && is_array($input['line_items'])) {
 | 
			
		||||
            $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
@ -72,7 +72,6 @@ class PaymentFailedMailer implements ShouldQueue
 | 
			
		||||
     */
 | 
			
		||||
    public function handle()
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        //Set DB
 | 
			
		||||
        MultiDB::setDb($this->company->db);
 | 
			
		||||
        App::setLocale($this->client->locale());
 | 
			
		||||
 | 
			
		||||
@ -103,7 +103,7 @@ class Gateway extends StaticModel
 | 
			
		||||
            case 20:
 | 
			
		||||
                return [
 | 
			
		||||
                    GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true],
 | 
			
		||||
                    GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable','charge.succeeded','payment_intent.succeeded']],
 | 
			
		||||
                    GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable','charge.succeeded','payment_intent.succeeded','charge.failed','payment_intent.payment_failed']],
 | 
			
		||||
                    GatewayType::ALIPAY => ['refund' => false, 'token_billing' => false],
 | 
			
		||||
                    GatewayType::APPLE_PAY => ['refund' => false, 'token_billing' => false],
 | 
			
		||||
                    GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], //Stripe
 | 
			
		||||
 | 
			
		||||
@ -30,8 +30,12 @@ use App\PaymentDrivers\StripePaymentDriver;
 | 
			
		||||
use App\Utils\Traits\MakesHash;
 | 
			
		||||
use Exception;
 | 
			
		||||
use Stripe\Customer;
 | 
			
		||||
use Stripe\Exception\ApiErrorException;
 | 
			
		||||
use Stripe\Exception\AuthenticationException;
 | 
			
		||||
use Stripe\Exception\CardException;
 | 
			
		||||
use Stripe\Exception\InvalidRequestException;
 | 
			
		||||
use Stripe\Exception\RateLimitException;
 | 
			
		||||
use Stripe\PaymentIntent;
 | 
			
		||||
 | 
			
		||||
class ACH
 | 
			
		||||
{
 | 
			
		||||
@ -45,6 +49,9 @@ class ACH
 | 
			
		||||
        $this->stripe = $stripe;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Authorize a bank account - requires microdeposit verification
 | 
			
		||||
     */
 | 
			
		||||
    public function authorizeView(array $data)
 | 
			
		||||
    {
 | 
			
		||||
        $data['gateway'] = $this->stripe;
 | 
			
		||||
@ -135,6 +142,10 @@ class ACH
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Make a payment WITH instant verification.
 | 
			
		||||
     */
 | 
			
		||||
    
 | 
			
		||||
    public function paymentView(array $data)
 | 
			
		||||
    {
 | 
			
		||||
        $data['gateway'] = $this->stripe;
 | 
			
		||||
@ -143,6 +154,23 @@ class ACH
 | 
			
		||||
        $data['customer'] = $this->stripe->findOrCreateCustomer();
 | 
			
		||||
        $data['amount'] = $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision, $this->stripe->client->currency());
 | 
			
		||||
 | 
			
		||||
        $intent = false;
 | 
			
		||||
 | 
			
		||||
        if(count($data['tokens']) == 0)
 | 
			
		||||
        {
 | 
			
		||||
            $intent = 
 | 
			
		||||
            $this->stripe->createPaymentIntent([
 | 
			
		||||
                'amount' => $data['amount'],
 | 
			
		||||
                'currency' => $data['currency'],
 | 
			
		||||
                'setup_future_usage' => 'off_session',
 | 
			
		||||
                'customer' => $data['customer']->id,
 | 
			
		||||
                'payment_method_types' => ['us_bank_account'],
 | 
			
		||||
              ]
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $data['client_secret'] = $intent ? $intent->client_secret : false;
 | 
			
		||||
 | 
			
		||||
        return render('gateways.stripe.ach.pay', $data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -160,6 +188,9 @@ class ACH
 | 
			
		||||
            $description = "Payment with no invoice for amount {$amount} for client {$this->stripe->client->present()->name()}";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if(substr($cgt->token, 0, 2) === "pm")
 | 
			
		||||
            return $this->paymentIntentTokenBilling($amount, $invoice, $description, $cgt, false);
 | 
			
		||||
 | 
			
		||||
        $this->stripe->init();
 | 
			
		||||
 | 
			
		||||
        $response = null;
 | 
			
		||||
@ -203,11 +234,179 @@ class ACH
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function paymentIntentTokenBilling($amount, $invoice, $description, $cgt, $client_present = true)
 | 
			
		||||
    {
 | 
			
		||||
        $this->stripe->init();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $data = [
 | 
			
		||||
              'amount' => $this->stripe->convertToStripeAmount($amount, $this->stripe->client->currency()->precision, $this->stripe->client->currency()),
 | 
			
		||||
              'currency' => $this->stripe->client->getCurrencyCode(),
 | 
			
		||||
              'payment_method' => $cgt->token,
 | 
			
		||||
              'customer' => $cgt->gateway_customer_reference,
 | 
			
		||||
              'confirm' => true,
 | 
			
		||||
              'description' => $description,
 | 
			
		||||
              'metadata' => [
 | 
			
		||||
                'payment_hash' => $this->stripe->payment_hash->hash,
 | 
			
		||||
                'gateway_type_id' => $cgt->gateway_type_id,
 | 
			
		||||
                ],
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            if($cgt->gateway_type_id == GatewayType::BANK_TRANSFER)
 | 
			
		||||
                $data['payment_method_types'] = ['us_bank_account'];
 | 
			
		||||
 | 
			
		||||
            $response = $this->stripe->createPaymentIntent($data, $this->stripe->stripe_connect_auth);
 | 
			
		||||
 | 
			
		||||
            SystemLogger::dispatch($response, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_STRIPE, $this->stripe->client, $this->stripe->client->company);
 | 
			
		||||
 | 
			
		||||
        }catch(\Exception $e) {
 | 
			
		||||
 | 
			
		||||
            $data =[
 | 
			
		||||
                'status' => '',
 | 
			
		||||
                'error_type' => '',
 | 
			
		||||
                'error_code' => '',
 | 
			
		||||
                'param' => '',
 | 
			
		||||
                'message' => '',
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            switch ($e) {
 | 
			
		||||
                case ($e instanceof CardException):
 | 
			
		||||
                    $data['status'] = $e->getHttpStatus();
 | 
			
		||||
                    $data['error_type'] = $e->getError()->type;
 | 
			
		||||
                    $data['error_code'] = $e->getError()->code;
 | 
			
		||||
                    $data['param'] = $e->getError()->param;
 | 
			
		||||
                    $data['message'] = $e->getError()->message;
 | 
			
		||||
                break;
 | 
			
		||||
                case ($e instanceof RateLimitException):
 | 
			
		||||
                    $data['message'] = 'Too many requests made to the API too quickly';
 | 
			
		||||
                break;
 | 
			
		||||
                case ($e instanceof InvalidRequestException):
 | 
			
		||||
                    $data['message'] = 'Invalid parameters were supplied to Stripe\'s API';
 | 
			
		||||
                break;
 | 
			
		||||
                case ($e instanceof AuthenticationException):
 | 
			
		||||
                    $data['message'] = 'Authentication with Stripe\'s API failed';
 | 
			
		||||
                break;
 | 
			
		||||
                case ($e instanceof ApiErrorException):
 | 
			
		||||
                    $data['message'] = 'Network communication with Stripe failed';
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
                default:
 | 
			
		||||
                    $data['message'] = $e->getMessage();
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $this->stripe->processInternallyFailedPayment($this->stripe, $e);
 | 
			
		||||
 | 
			
		||||
            SystemLogger::dispatch($data, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_STRIPE, $this->stripe->client, $this->stripe->client->company);
 | 
			
		||||
        }  
 | 
			
		||||
 | 
			
		||||
        if (! $response) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $payment_method_type = PaymentType::ACH;
 | 
			
		||||
 | 
			
		||||
        $data = [
 | 
			
		||||
            'gateway_type_id' => $cgt->gateway_type_id,
 | 
			
		||||
            'payment_type' => PaymentType::ACH,
 | 
			
		||||
            'transaction_reference' => $response->charges->data[0]->id,
 | 
			
		||||
            'amount' => $amount,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $payment = $this->stripe->createPayment($data, Payment::STATUS_PENDING);
 | 
			
		||||
        $payment->meta = $cgt->meta;
 | 
			
		||||
        $payment->save();
 | 
			
		||||
 | 
			
		||||
        $this->stripe->payment_hash->payment_id = $payment->id;
 | 
			
		||||
        $this->stripe->payment_hash->save();
 | 
			
		||||
 | 
			
		||||
        if($client_present){
 | 
			
		||||
            return redirect()->route('client.payments.show', ['payment' => $this->stripe->encodePrimaryKey($payment->id)]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $payment;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function handlePaymentIntentResponse($request)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        $response = json_decode($request->gateway_response);
 | 
			
		||||
        $bank_account_response = json_decode($request->bank_account_response);
 | 
			
		||||
        
 | 
			
		||||
        $method = $bank_account_response->payment_method->us_bank_account;
 | 
			
		||||
        $method->id = $response->payment_method;
 | 
			
		||||
        $method->state = 'authorized';
 | 
			
		||||
 | 
			
		||||
        $this->stripe->payment_hash = PaymentHash::where("hash", $request->input("payment_hash"))->first();
 | 
			
		||||
 | 
			
		||||
        if($response->id && $response->status === "processing") {
 | 
			
		||||
            $payment_intent = PaymentIntent::retrieve($response->id, $this->stripe->stripe_connect_auth);
 | 
			
		||||
 | 
			
		||||
            $state = [
 | 
			
		||||
                'gateway_type_id' => GatewayType::BANK_TRANSFER,
 | 
			
		||||
                'amount' => $response->amount,
 | 
			
		||||
                'currency' => $response->currency,
 | 
			
		||||
                'customer' => $request->customer,
 | 
			
		||||
                'source' => $response->payment_method,
 | 
			
		||||
                'charge' => $response
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            $this->stripe->payment_hash->data = array_merge((array)$this->stripe->payment_hash->data, $state);
 | 
			
		||||
            $this->stripe->payment_hash->save();
 | 
			
		||||
            
 | 
			
		||||
            $customer = $this->stripe->getCustomer($request->customer);
 | 
			
		||||
 | 
			
		||||
            $this->storePaymentMethod($method, GatewayType::BANK_TRANSFER, $customer);
 | 
			
		||||
 | 
			
		||||
            return $this->processPendingPayment($state, true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if($response->next_action){
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function processPendingPaymentIntent($state, $client_present = true)
 | 
			
		||||
    {
 | 
			
		||||
        $this->stripe->init();
 | 
			
		||||
 | 
			
		||||
        $data = [
 | 
			
		||||
            'payment_method' => $state['source'],
 | 
			
		||||
            'payment_type' => PaymentType::ACH,
 | 
			
		||||
            'amount' => $state['amount'],
 | 
			
		||||
            'transaction_reference' => $state['charge'],
 | 
			
		||||
            'gateway_type_id' => GatewayType::BANK_TRANSFER,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $payment = $this->stripe->createPayment($data, Payment::STATUS_PENDING);
 | 
			
		||||
 | 
			
		||||
        SystemLogger::dispatch(
 | 
			
		||||
            ['response' => $state, 'data' => $data],
 | 
			
		||||
            SystemLog::CATEGORY_GATEWAY_RESPONSE,
 | 
			
		||||
            SystemLog::EVENT_GATEWAY_SUCCESS,
 | 
			
		||||
            SystemLog::TYPE_STRIPE,
 | 
			
		||||
            $this->stripe->client,
 | 
			
		||||
            $this->stripe->client->company,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if(!$client_present)
 | 
			
		||||
            return $payment;
 | 
			
		||||
 | 
			
		||||
        return redirect()->route('client.payments.show', ['payment' => $this->stripe->encodePrimaryKey($payment->id)]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public function paymentResponse($request)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        $this->stripe->init();
 | 
			
		||||
 | 
			
		||||
        //it may be a payment intent here.
 | 
			
		||||
        if($request->input('client_secret') != '')
 | 
			
		||||
            return $this->handlePaymentIntentResponse($request);
 | 
			
		||||
 | 
			
		||||
        $source = ClientGatewayToken::query()
 | 
			
		||||
            ->where('id', $this->decodePrimaryKey($request->source))
 | 
			
		||||
            ->where('company_id', auth()->guard('contact')->user()->client->company->id)
 | 
			
		||||
@ -242,6 +441,9 @@ class ACH
 | 
			
		||||
            $description = "Payment with no invoice for amount {$amount} for client {$this->stripe->client->present()->name()}";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if(substr($source->token, 0, 2) === "pm")
 | 
			
		||||
            return $this->paymentIntentTokenBilling($amount, $invoice, $description, $source);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $state['charge'] = \Stripe\Charge::create([
 | 
			
		||||
                'amount' => $state['amount'],
 | 
			
		||||
@ -270,6 +472,7 @@ class ACH
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public function processPendingPayment($state, $client_present = true)
 | 
			
		||||
    {
 | 
			
		||||
        $this->stripe->init();
 | 
			
		||||
@ -321,12 +524,14 @@ class ACH
 | 
			
		||||
 | 
			
		||||
    private function storePaymentMethod($method, $payment_method_id, $customer)
 | 
			
		||||
    {
 | 
			
		||||
        $state = property_exists($method, 'state') ? $method->state : 'unauthorized';
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $payment_meta = new \stdClass;
 | 
			
		||||
            $payment_meta->brand = (string) \sprintf('%s (%s)', $method->bank_name, ctrans('texts.ach'));
 | 
			
		||||
            $payment_meta->last4 = (string) $method->last4;
 | 
			
		||||
            $payment_meta->type = GatewayType::BANK_TRANSFER;
 | 
			
		||||
            $payment_meta->state = 'unauthorized';
 | 
			
		||||
            $payment_meta->state = $state;
 | 
			
		||||
 | 
			
		||||
            $data = [
 | 
			
		||||
                'payment_meta' => $payment_meta,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										128
									
								
								app/PaymentDrivers/Stripe/Jobs/PaymentIntentFailureWebhook.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								app/PaymentDrivers/Stripe/Jobs/PaymentIntentFailureWebhook.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,128 @@
 | 
			
		||||
<?php
 | 
			
		||||
/**
 | 
			
		||||
 * Invoice Ninja (https://invoiceninja.com).
 | 
			
		||||
 *
 | 
			
		||||
 * @link https://github.com/invoiceninja/invoiceninja source repository
 | 
			
		||||
 *
 | 
			
		||||
 * @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
 | 
			
		||||
 *
 | 
			
		||||
 * @license https://www.elastic.co/licensing/elastic-license
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace App\PaymentDrivers\Stripe\Jobs;
 | 
			
		||||
 | 
			
		||||
use App\Jobs\Mail\PaymentFailedMailer;
 | 
			
		||||
use App\Jobs\Util\SystemLogger;
 | 
			
		||||
use App\Libraries\MultiDB;
 | 
			
		||||
use App\Models\Company;
 | 
			
		||||
use App\Models\CompanyGateway;
 | 
			
		||||
use App\Models\GatewayType;
 | 
			
		||||
use App\Models\Invoice;
 | 
			
		||||
use App\Models\Payment;
 | 
			
		||||
use App\Models\PaymentHash;
 | 
			
		||||
use App\Models\PaymentType;
 | 
			
		||||
use App\Models\SystemLog;
 | 
			
		||||
use App\PaymentDrivers\Stripe\Utilities;
 | 
			
		||||
use Illuminate\Bus\Queueable;
 | 
			
		||||
use Illuminate\Contracts\Queue\ShouldQueue;
 | 
			
		||||
use Illuminate\Foundation\Bus\Dispatchable;
 | 
			
		||||
use Illuminate\Queue\InteractsWithQueue;
 | 
			
		||||
use Illuminate\Queue\SerializesModels;
 | 
			
		||||
 | 
			
		||||
class PaymentIntentFailureWebhook implements ShouldQueue
 | 
			
		||||
{
 | 
			
		||||
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Utilities;
 | 
			
		||||
 | 
			
		||||
    public $tries = 1; //number of retries
 | 
			
		||||
 | 
			
		||||
    public $deleteWhenMissingModels = true;
 | 
			
		||||
 | 
			
		||||
    public $stripe_request;
 | 
			
		||||
 | 
			
		||||
    public $company_key;
 | 
			
		||||
 | 
			
		||||
    private $company_gateway_id;
 | 
			
		||||
 | 
			
		||||
    public $payment_completed = false;
 | 
			
		||||
 | 
			
		||||
    public function __construct($stripe_request, $company_key, $company_gateway_id)
 | 
			
		||||
    {
 | 
			
		||||
        $this->stripe_request = $stripe_request;
 | 
			
		||||
        $this->company_key = $company_key;
 | 
			
		||||
        $this->company_gateway_id = $company_gateway_id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function handle()
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        MultiDB::findAndSetDbByCompanyKey($this->company_key);
 | 
			
		||||
 | 
			
		||||
        $company = Company::where('company_key', $this->company_key)->first();
 | 
			
		||||
 | 
			
		||||
            foreach ($this->stripe_request as $transaction) {
 | 
			
		||||
 | 
			
		||||
                if(array_key_exists('payment_intent', $transaction))
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                    $payment = Payment::query()
 | 
			
		||||
                        ->where('company_id', $company->id)
 | 
			
		||||
                        ->where(function ($query) use ($transaction) {
 | 
			
		||||
                            $query->where('transaction_reference', $transaction['payment_intent'])
 | 
			
		||||
                                  ->orWhere('transaction_reference', $transaction['id']);
 | 
			
		||||
                                })
 | 
			
		||||
                        ->first();
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                     $payment = Payment::query()
 | 
			
		||||
                        ->where('company_id', $company->id)
 | 
			
		||||
                        ->where('transaction_reference', $transaction['id'])
 | 
			
		||||
                        ->first();
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if ($payment) {
 | 
			
		||||
 | 
			
		||||
                    $client = $payment->client;
 | 
			
		||||
 | 
			
		||||
                    if($payment->status_id == Payment::STATUS_PENDING)
 | 
			
		||||
                        $payment->service()->deletePayment();
 | 
			
		||||
        
 | 
			
		||||
                    $payment->status_id = Payment::STATUS_FAILED;
 | 
			
		||||
                    $payment->save();
 | 
			
		||||
 | 
			
		||||
                    $payment_hash = PaymentHash::where('payment_id', $payment->id)->first();
 | 
			
		||||
 | 
			
		||||
                    if($payment_hash)
 | 
			
		||||
                    {
 | 
			
		||||
 | 
			
		||||
                        $error = ctrans('texts.client_payment_failure_body', [
 | 
			
		||||
                            'invoice' => implode(",", $payment->invoices->pluck('number')->toArray()), 
 | 
			
		||||
                            'amount' => array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total]);
 | 
			
		||||
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                        $error = "Payment for " . $payment->client->present()->name(). " for {$payment->amount} failed";
 | 
			
		||||
 | 
			
		||||
                    if(array_key_exists('failure_message', $transaction)){
 | 
			
		||||
                        
 | 
			
		||||
                        $error .= "\n\n" .$transaction['failure_message'];
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    PaymentFailedMailer::dispatch(
 | 
			
		||||
                        $payment_hash,
 | 
			
		||||
                        $client->company,
 | 
			
		||||
                        $client,
 | 
			
		||||
                        $error
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -53,10 +53,6 @@ class PaymentIntentWebhook implements ShouldQueue
 | 
			
		||||
 | 
			
		||||
    public function handle()
 | 
			
		||||
    {
 | 
			
		||||
        // nlog($this->stripe_request);
 | 
			
		||||
        // nlog(optional($this->stripe_request['object']['charges']['data'][0]['metadata'])['gateway_type_id']);
 | 
			
		||||
        // nlog(optional($this->stripe_request['object']['charges']['data'][0]['metadata'])['payment_hash']);
 | 
			
		||||
        // nlog(optional($this->stripe_request['object']['charges']['data'][0]['payment_method_details']['card'])['brand']);
 | 
			
		||||
        
 | 
			
		||||
        MultiDB::findAndSetDbByCompanyKey($this->company_key);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -38,6 +38,7 @@ use App\PaymentDrivers\Stripe\EPS;
 | 
			
		||||
use App\PaymentDrivers\Stripe\FPX;
 | 
			
		||||
use App\PaymentDrivers\Stripe\GIROPAY;
 | 
			
		||||
use App\PaymentDrivers\Stripe\ImportCustomers;
 | 
			
		||||
use App\PaymentDrivers\Stripe\Jobs\PaymentIntentFailureWebhook;
 | 
			
		||||
use App\PaymentDrivers\Stripe\Jobs\PaymentIntentWebhook;
 | 
			
		||||
use App\PaymentDrivers\Stripe\PRZELEWY24;
 | 
			
		||||
use App\PaymentDrivers\Stripe\SEPA;
 | 
			
		||||
@ -47,6 +48,7 @@ use App\PaymentDrivers\Stripe\Utilities;
 | 
			
		||||
use App\PaymentDrivers\Stripe\iDeal;
 | 
			
		||||
use App\Utils\Traits\MakesHash;
 | 
			
		||||
use Exception;
 | 
			
		||||
use Google\Service\ServiceConsumerManagement\CustomError;
 | 
			
		||||
use Illuminate\Http\RedirectResponse;
 | 
			
		||||
use Illuminate\Support\Carbon;
 | 
			
		||||
use Laracasts\Presenter\Exceptions\PresenterException;
 | 
			
		||||
@ -409,6 +411,16 @@ class StripePaymentDriver extends BaseDriver
 | 
			
		||||
        return $this->company_gateway->getPublishableKey();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getCustomer($customer_id) :?Customer 
 | 
			
		||||
    {
 | 
			
		||||
        $customer = Customer::retrieve($customer_id, $this->stripe_connect_auth);
 | 
			
		||||
 | 
			
		||||
        if($customer)
 | 
			
		||||
            return $customer;
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Finds or creates a Stripe Customer object.
 | 
			
		||||
     *
 | 
			
		||||
@ -568,15 +580,21 @@ class StripePaymentDriver extends BaseDriver
 | 
			
		||||
 | 
			
		||||
        //payment_intent.succeeded - this will confirm or cancel the payment
 | 
			
		||||
        if($request->type === 'payment_intent.succeeded'){
 | 
			
		||||
            PaymentIntentWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(10));
 | 
			
		||||
            PaymentIntentWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(2,10)));
 | 
			
		||||
            return response()->json([], 200);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if(in_array($request->type, ['payment_intent.payment_failed','charge.failed'])){
 | 
			
		||||
            PaymentIntentFailureWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(2,10)));
 | 
			
		||||
            return response()->json([], 200);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        if ($request->type === 'charge.succeeded') {
 | 
			
		||||
 | 
			
		||||
            foreach ($request->data as $transaction) {
 | 
			
		||||
 | 
			
		||||
                if(array_key_exists('payment_intent', $transaction))
 | 
			
		||||
                if(array_key_exists('payment_intent', $transaction) && $transaction['payment_intent'])
 | 
			
		||||
                {
 | 
			
		||||
                    $payment = Payment::query()
 | 
			
		||||
                        // ->where('company_id', $request->getCompany()->id)
 | 
			
		||||
 | 
			
		||||
@ -46,14 +46,6 @@ class DeletePayment
 | 
			
		||||
            ->save();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //reverse paymentables->invoices
 | 
			
		||||
 | 
			
		||||
    //reverse paymentables->credits
 | 
			
		||||
 | 
			
		||||
    //set refunded to amount
 | 
			
		||||
 | 
			
		||||
    //set applied amount to 0
 | 
			
		||||
 | 
			
		||||
    private function cleanupPayment()
 | 
			
		||||
    {
 | 
			
		||||
        $this->payment->is_deleted = true;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								public/js/clients/payments/stripe-ach.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								public/js/clients/payments/stripe-ach.js
									
									
									
									
										vendored
									
									
								
							@ -1,2 +1,2 @@
 | 
			
		||||
/*! For license information please see stripe-ach.js.LICENSE.txt */
 | 
			
		||||
(()=>{function e(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function t(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}(new(function(){function n(){var e,r=this;!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,n),t(this,"setupStripe",(function(){return r.stripeConnect?r.stripe=Stripe(r.key,{stripeAccount:r.stripeConnect}):r.stripe=Stripe(r.key),r})),t(this,"getFormData",(function(){return{country:document.getElementById("country").value,currency:document.getElementById("currency").value,routing_number:document.getElementById("routing-number").value,account_number:document.getElementById("account-number").value,account_holder_name:document.getElementById("account-holder-name").value,account_holder_type:document.querySelector('input[name="account-holder-type"]:checked').value}})),t(this,"handleError",(function(e){document.getElementById("save-button").disabled=!1,document.querySelector("#save-button > svg").classList.add("hidden"),document.querySelector("#save-button > span").classList.remove("hidden"),r.errors.textContent="",r.errors.textContent=e,r.errors.hidden=!1})),t(this,"handleSuccess",(function(e){document.getElementById("gateway_response").value=JSON.stringify(e),document.getElementById("server_response").submit()})),t(this,"handleSubmit",(function(e){document.getElementById("save-button").disabled=!0,document.querySelector("#save-button > svg").classList.remove("hidden"),document.querySelector("#save-button > span").classList.add("hidden"),e.preventDefault(),r.errors.textContent="",r.errors.hidden=!0,r.stripe.createToken("bank_account",r.getFormData()).then((function(e){return e.hasOwnProperty("error")?r.handleError(e.error.message):r.handleSuccess(e)}))})),this.errors=document.getElementById("errors"),this.key=document.querySelector('meta[name="stripe-publishable-key"]').content,this.stripe_connect=null===(e=document.querySelector('meta[name="stripe-account-id"]'))||void 0===e?void 0:e.content}var r,o,u;return r=n,(o=[{key:"handle",value:function(){var e=this;document.getElementById("save-button").addEventListener("click",(function(t){return e.handleSubmit(t)}))}}])&&e(r.prototype,o),u&&e(r,u),n}())).setupStripe().handle()})();
 | 
			
		||||
(()=>{function e(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function t(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}(new(function(){function n(){var e,r=this;!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,n),t(this,"setupStripe",(function(){return r.stripeConnect?r.stripe=Stripe(r.key,{stripeAccount:r.stripeConnect}):r.stripe=Stripe(r.key),r})),t(this,"getFormData",(function(){return{country:document.getElementById("country").value,currency:document.getElementById("currency").value,routing_number:document.getElementById("routing-number").value,account_number:document.getElementById("account-number").value,account_holder_name:document.getElementById("account-holder-name").value,account_holder_type:document.querySelector('input[name="account-holder-type"]:checked').value}})),t(this,"handleError",(function(e){document.getElementById("save-button").disabled=!1,document.querySelector("#save-button > svg").classList.add("hidden"),document.querySelector("#save-button > span").classList.remove("hidden"),r.errors.textContent="",r.errors.textContent=e,r.errors.hidden=!1})),t(this,"handleSuccess",(function(e){document.getElementById("gateway_response").value=JSON.stringify(e),document.getElementById("server_response").submit()})),t(this,"handleSubmit",(function(e){if(!document.getElementById("accept-terms").checked)return errors.textContent="You must accept the mandate terms prior to making payment.",void(errors.hidden=!1);document.getElementById("save-button").disabled=!0,document.querySelector("#save-button > svg").classList.remove("hidden"),document.querySelector("#save-button > span").classList.add("hidden"),e.preventDefault(),r.errors.textContent="",r.errors.hidden=!0,r.stripe.createToken("bank_account",r.getFormData()).then((function(e){return e.hasOwnProperty("error")?r.handleError(e.error.message):r.handleSuccess(e)}))})),this.errors=document.getElementById("errors"),this.key=document.querySelector('meta[name="stripe-publishable-key"]').content,this.stripe_connect=null===(e=document.querySelector('meta[name="stripe-account-id"]'))||void 0===e?void 0:e.content}var r,o,u;return r=n,(o=[{key:"handle",value:function(){var e=this;document.getElementById("save-button").addEventListener("click",(function(t){return e.handleSubmit(t)}))}}])&&e(r.prototype,o),u&&e(r,u),n}())).setupStripe().handle()})();
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
    "/js/app.js": "/js/app.js?id=0e3959ab851d3350364d",
 | 
			
		||||
    "/js/clients/payment_methods/authorize-authorize-card.js": "/js/clients/payment_methods/authorize-authorize-card.js?id=de4468c682d6861847de",
 | 
			
		||||
    "/js/clients/payments/authorize-credit-card-payment.js": "/js/clients/payments/authorize-credit-card-payment.js?id=cfe5de1cf87a0b01568d",
 | 
			
		||||
    "/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=a5f14c885c3aeef6c744",
 | 
			
		||||
    "/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=50d964c4a3ffa7f2f99f",
 | 
			
		||||
    "/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=6b79265cbb8c963eef19",
 | 
			
		||||
    "/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=2cccf9e51b60a0ab17b8",
 | 
			
		||||
    "/js/clients/payments/stripe-sofort.js": "/js/clients/payments/stripe-sofort.js?id=22fc06e698dea2c3bdf3",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										7
									
								
								resources/js/clients/payments/stripe-ach.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								resources/js/clients/payments/stripe-ach.js
									
									
									
									
										vendored
									
									
								
							@ -70,6 +70,13 @@ class AuthorizeACH {
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    handleSubmit = (e) => {
 | 
			
		||||
 | 
			
		||||
        if (!document.getElementById('accept-terms').checked) {
 | 
			
		||||
                errors.textContent = "You must accept the mandate terms prior to making payment.";
 | 
			
		||||
                errors.hidden = false;
 | 
			
		||||
                return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        document.getElementById('save-button').disabled = true;
 | 
			
		||||
        document.querySelector('#save-button > svg').classList.remove('hidden');
 | 
			
		||||
        document.querySelector('#save-button > span').classList.add('hidden');
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,10 @@
 | 
			
		||||
 | 
			
		||||
    <div class="alert alert-failure mb-4" hidden id="errors"></div>
 | 
			
		||||
 | 
			
		||||
    <div class="alert alert-warning mb-4">
 | 
			
		||||
        <h2>Adding a bank account here requires verification, which may take several days. In order to use Instant Verification please pay an invoice first, this process will automatically verify your bank account.</h2>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_holder_type')])
 | 
			
		||||
        <span class="flex items-center mr-4">
 | 
			
		||||
            <input class="form-radio mr-2" type="radio" value="individual" name="account-holder-type" checked>
 | 
			
		||||
@ -39,14 +43,18 @@
 | 
			
		||||
    @endcomponent
 | 
			
		||||
 | 
			
		||||
    @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_holder_name')])
 | 
			
		||||
        <input class="input w-full" id="account-holder-name" type="text" placeholder="{{ ctrans('texts.name') }}" required>
 | 
			
		||||
        <input class="input w-full" id="account-holder-name" type="text" placeholder="{{ ctrans('texts.name') }}" required value="{{ auth()->guard('contact')->user()->client->present()->name() }}">
 | 
			
		||||
    @endcomponent
 | 
			
		||||
 | 
			
		||||
    @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.country')])
 | 
			
		||||
        <select name="countries" id="country" class="form-select input w-full">
 | 
			
		||||
            <option disabled selected></option>
 | 
			
		||||
            @foreach($countries as $country)
 | 
			
		||||
                @if($country->iso_3166_2 == 'US')
 | 
			
		||||
                <option value="{{ $country->iso_3166_2 }}" selected>{{ $country->iso_3166_2 }} ({{ $country->name }})</option>
 | 
			
		||||
                @else
 | 
			
		||||
                <option value="{{ $country->iso_3166_2 }}">{{ $country->iso_3166_2 }} ({{ $country->name }})</option>
 | 
			
		||||
                @endif
 | 
			
		||||
            @endforeach
 | 
			
		||||
        </select>
 | 
			
		||||
    @endcomponent
 | 
			
		||||
@ -55,7 +63,11 @@
 | 
			
		||||
        <select name="currencies" id="currency" class="form-select input w-full">
 | 
			
		||||
            <option disabled selected></option>
 | 
			
		||||
            @foreach($currencies as $currency)
 | 
			
		||||
                @if($currency->code == 'USD')
 | 
			
		||||
                    <option value="{{ $currency->code }}" selected>{{ $currency->code }} ({{ $currency->name }})</option>
 | 
			
		||||
                @else
 | 
			
		||||
                    <option value="{{ $currency->code }}">{{ $currency->code }} ({{ $currency->name }})</option>
 | 
			
		||||
                @endif
 | 
			
		||||
            @endforeach
 | 
			
		||||
        </select>
 | 
			
		||||
    @endcomponent
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,20 @@
 | 
			
		||||
@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'ACH', 'card_title' => 'ACH'])
 | 
			
		||||
 | 
			
		||||
@section('gateway_content')
 | 
			
		||||
    @if(count($tokens) > 0)
 | 
			
		||||
        <div class="alert alert-failure mb-4" hidden id="errors"></div>
 | 
			
		||||
@section('gateway_head')
 | 
			
		||||
    @if($gateway->company_gateway->getConfigField('account_id'))
 | 
			
		||||
        <meta name="stripe-account-id" content="{{ $gateway->company_gateway->getConfigField('account_id') }}">
 | 
			
		||||
        <meta name="stripe-publishable-key" content="{{ config('ninja.ninja_stripe_publishable_key') }}">
 | 
			
		||||
    @else
 | 
			
		||||
        <meta name="stripe-publishable-key" content="{{ $gateway->getPublishableKey() }}">
 | 
			
		||||
    @endif
 | 
			
		||||
 | 
			
		||||
        @include('portal.ninja2020.gateways.includes.payment_details')
 | 
			
		||||
        <meta name="client_secret" content="{{ $client_secret }}">
 | 
			
		||||
        <meta name="viewport" content="width=device-width, minimum-scale=1" />
 | 
			
		||||
 | 
			
		||||
@endsection
 | 
			
		||||
 | 
			
		||||
@section('gateway_content')
 | 
			
		||||
    <div class="alert alert-failure mb-4" hidden id="errors"></div>
 | 
			
		||||
 | 
			
		||||
    <form action="{{ route('client.payments.response') }}" method="post" id="server-response">
 | 
			
		||||
        @csrf
 | 
			
		||||
@ -15,50 +25,203 @@
 | 
			
		||||
        <input type="hidden" name="currency" value="{{ $currency }}">
 | 
			
		||||
        <input type="hidden" name="customer" value="{{ $customer->id }}">
 | 
			
		||||
        <input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
 | 
			
		||||
        <input type="hidden" name="client_secret" value="{{ $client_secret }}">
 | 
			
		||||
        <input type="hidden" name="gateway_response" id="gateway_response" value="">
 | 
			
		||||
        <input type="hidden" name="bank_account_response" id="bank_account_response" value="">
 | 
			
		||||
    </form>
 | 
			
		||||
    
 | 
			
		||||
    @if(count($tokens) > 0)
 | 
			
		||||
 | 
			
		||||
        @include('portal.ninja2020.gateways.includes.payment_details')
 | 
			
		||||
 | 
			
		||||
        @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')])
 | 
			
		||||
            @if(count($tokens) > 0)
 | 
			
		||||
            <ul class="list-none hover:list-disc">
 | 
			
		||||
                @foreach($tokens as $token)
 | 
			
		||||
                    <li class="py-1 hover:text-blue hover:bg-blue-600">
 | 
			
		||||
                        <label class="mr-4">
 | 
			
		||||
                            <input
 | 
			
		||||
                                type="radio"
 | 
			
		||||
                                data-token="{{ $token->hashed_id }}"
 | 
			
		||||
                                name="payment-type"
 | 
			
		||||
                            class="form-radio cursor-pointer toggle-payment-with-token"/>
 | 
			
		||||
                                class="form-check-input text-indigo-600 rounded-full cursor-pointer toggle-payment-with-token"/>
 | 
			
		||||
                            <span class="ml-1 cursor-pointer">{{ ctrans('texts.bank_transfer') }} (*{{ $token->meta->last4 }})</span>
 | 
			
		||||
                        </label>
 | 
			
		||||
                    </li>
 | 
			
		||||
                @endforeach
 | 
			
		||||
            @endisset
 | 
			
		||||
        @endcomponent
 | 
			
		||||
 | 
			
		||||
    @else
 | 
			
		||||
        @component('portal.ninja2020.components.general.card-element-single', ['title' => 'ACH', 'show_title' => false])
 | 
			
		||||
            <span>{{ ctrans('texts.bank_account_not_linked') }}</span>
 | 
			
		||||
            <a class="button button-link text-primary"
 | 
			
		||||
               href="{{ route('client.payment_methods.index') }}">{{ ctrans('texts.add_payment_method') }}</a>
 | 
			
		||||
        @endcomponent
 | 
			
		||||
            </ul>
 | 
			
		||||
            @endif
 | 
			
		||||
        @endcomponent
 | 
			
		||||
 | 
			
		||||
    @include('portal.ninja2020.gateways.includes.pay_now')
 | 
			
		||||
 | 
			
		||||
    @else
 | 
			
		||||
 | 
			
		||||
        @component('portal.ninja2020.components.general.card-element-single')
 | 
			
		||||
            <input type="checkbox" class="form-checkbox mr-1" id="accept-terms" required>
 | 
			
		||||
            <label for="accept-terms" class="cursor-pointer">{{ ctrans('texts.ach_authorization', ['company' => auth()->user()->company->present()->name, 'email' => auth()->guard('contact')->user()->client->company->settings->email]) }}</label>
 | 
			
		||||
        @endcomponent
 | 
			
		||||
 | 
			
		||||
        @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_holder_name')])
 | 
			
		||||
            <input class="input w-full" id="account-holder-name-field" type="text" placeholder="{{ ctrans('texts.name') }}" value="{{ $gateway->client->present()->first_name() }} {{ $gateway->client->present()->last_name(); }}"required>
 | 
			
		||||
        @endcomponent
 | 
			
		||||
        @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.email')])
 | 
			
		||||
            <input class="input w-full" id="email-field" type="text" placeholder="{{ ctrans('texts.email') }}" value="{{ $gateway->client->present()->email(); }}" required>
 | 
			
		||||
        @endcomponent
 | 
			
		||||
        <div class="px-4 py-5 sm:px-6 lg:grid lg:grid-cols-3 lg:gap-4 lg:flex lg:items-center">
 | 
			
		||||
            <dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
 | 
			
		||||
                Connect a bank account
 | 
			
		||||
            </dt>
 | 
			
		||||
            <dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
 | 
			
		||||
                <button type="button" class="button button-primary bg-primary" id="new-bank" type="button">
 | 
			
		||||
                    <svg class="animate-spin h-5 w-5 text-white hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
 | 
			
		||||
                        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
 | 
			
		||||
                        <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
 | 
			
		||||
                    </svg>
 | 
			
		||||
                    <span>{{ $slot ?? ctrans('texts.new_bank_account') }}</span>
 | 
			
		||||
                </button>
 | 
			
		||||
            </dd>
 | 
			
		||||
        </div>
 | 
			
		||||
    @endif
 | 
			
		||||
 | 
			
		||||
@endsection
 | 
			
		||||
 | 
			
		||||
@push('footer')
 | 
			
		||||
    <script>
 | 
			
		||||
<script src="https://js.stripe.com/v3/"></script>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
 | 
			
		||||
    let payNow = document.getElementById('pay-now');
 | 
			
		||||
 | 
			
		||||
    if(payNow)
 | 
			
		||||
    {
 | 
			
		||||
    
 | 
			
		||||
        Array
 | 
			
		||||
            .from(document.getElementsByClassName('toggle-payment-with-token'))
 | 
			
		||||
            .forEach((element) => element.addEventListener('click', (element) => {
 | 
			
		||||
                document.querySelector('input[name=source]').value = element.target.dataset.token;
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
        document.getElementById('pay-now').addEventListener('click', function () {
 | 
			
		||||
 | 
			
		||||
        payNow.addEventListener('click', function () {
 | 
			
		||||
                let payNowButton = document.getElementById('pay-now');
 | 
			
		||||
                payNowButton.disabled = true;
 | 
			
		||||
                payNowButton.querySelector('svg').classList.remove('hidden');
 | 
			
		||||
                payNowButton.querySelector('span').classList.add('hidden');
 | 
			
		||||
 | 
			
		||||
        document.getElementById('server-response').submit();
 | 
			
		||||
        });
 | 
			
		||||
    </script>
 | 
			
		||||
        
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    document.getElementById('new-bank').addEventListener('click', (ev) => {
 | 
			
		||||
 | 
			
		||||
        if (!document.getElementById('accept-terms').checked) {
 | 
			
		||||
                errors.textContent = "You must accept the mandate terms prior to making payment.";
 | 
			
		||||
                errors.hidden = false;
 | 
			
		||||
                return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        errors.hidden = true;
 | 
			
		||||
 | 
			
		||||
        let stripe;
 | 
			
		||||
 | 
			
		||||
        let publishableKey = document.querySelector('meta[name="stripe-publishable-key"]').content
 | 
			
		||||
        
 | 
			
		||||
        let stripeConnect = document.querySelector('meta[name="stripe-account-id"]')?.content
 | 
			
		||||
       
 | 
			
		||||
        if(stripeConnect){
 | 
			
		||||
           stripe = Stripe(publishableKey, { stripeAccount: stripeConnect}); 
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            stripe = Stripe(publishableKey);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let newBankButton = document.getElementById('new-bank');
 | 
			
		||||
        newBankButton.disabled = true;
 | 
			
		||||
        newBankButton.querySelector('svg').classList.remove('hidden');
 | 
			
		||||
        newBankButton.querySelector('span').classList.add('hidden');
 | 
			
		||||
    
 | 
			
		||||
        ev.preventDefault();
 | 
			
		||||
        const accountHolderNameField = document.getElementById('account-holder-name-field');
 | 
			
		||||
        const emailField = document.getElementById('email-field');
 | 
			
		||||
        const clientSecret = document.querySelector('meta[name="client_secret"]')?.content;
 | 
			
		||||
        // Calling this method will open the instant verification dialog.
 | 
			
		||||
        stripe.collectBankAccountForPayment({
 | 
			
		||||
        clientSecret: clientSecret,
 | 
			
		||||
        params: {
 | 
			
		||||
          payment_method_type: 'us_bank_account',
 | 
			
		||||
          payment_method_data: {
 | 
			
		||||
            billing_details: {
 | 
			
		||||
              name: accountHolderNameField.value,
 | 
			
		||||
              email: emailField.value,
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        expand: ['payment_method'],
 | 
			
		||||
        })
 | 
			
		||||
        .then(({paymentIntent, error}) => {
 | 
			
		||||
            if (error) {
 | 
			
		||||
 | 
			
		||||
              console.error(error.message);
 | 
			
		||||
              errors.textContent = error.message;
 | 
			
		||||
              errors.hidden = false;
 | 
			
		||||
              resetButtons();
 | 
			
		||||
 | 
			
		||||
              // PaymentMethod collection failed for some reason.
 | 
			
		||||
            } else if (paymentIntent.status === 'requires_payment_method') {
 | 
			
		||||
              // Customer canceled the hosted verification modal. Present them with other
 | 
			
		||||
              // payment method type options.
 | 
			
		||||
 | 
			
		||||
                  errors.textContent = "We were unable to process the payment with this account, please try another one.";
 | 
			
		||||
                  errors.hidden = false;
 | 
			
		||||
                  resetButtons();
 | 
			
		||||
                  return;
 | 
			
		||||
 | 
			
		||||
            } else if (paymentIntent.status === 'requires_confirmation') {
 | 
			
		||||
 | 
			
		||||
                let bank_account_response = document.getElementById('bank_account_response');
 | 
			
		||||
                bank_account_response.value = JSON.stringify(paymentIntent);
 | 
			
		||||
 | 
			
		||||
              confirmPayment(stripe, clientSecret);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    function confirmPayment(stripe, clientSecret){
 | 
			
		||||
        stripe.confirmUsBankAccountPayment(clientSecret)
 | 
			
		||||
          .then(({paymentIntent, error}) => {
 | 
			
		||||
            console.log(paymentIntent);
 | 
			
		||||
            if (error) {
 | 
			
		||||
              console.error(error.message);
 | 
			
		||||
              // The payment failed for some reason.
 | 
			
		||||
            } else if (paymentIntent.status === "requires_payment_method") {
 | 
			
		||||
              // Confirmation failed. Attempt again with a different payment method.
 | 
			
		||||
                  
 | 
			
		||||
                  errors.textContent = "We were unable to process the payment with this account, please try another one.";
 | 
			
		||||
                  errors.hidden = false;
 | 
			
		||||
                  resetButtons();
 | 
			
		||||
 | 
			
		||||
            } else if (paymentIntent.status === "processing") {
 | 
			
		||||
              // Confirmation succeeded! The account will be debited.
 | 
			
		||||
 | 
			
		||||
                let gateway_response = document.getElementById('gateway_response');
 | 
			
		||||
                gateway_response.value = JSON.stringify(paymentIntent);
 | 
			
		||||
                document.getElementById('server-response').submit();
 | 
			
		||||
 | 
			
		||||
            } else if (paymentIntent.next_action?.type === "verify_with_microdeposits") {
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
    
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function resetButtons()
 | 
			
		||||
    {
 | 
			
		||||
        
 | 
			
		||||
        let newBankButton = document.getElementById('new-bank');
 | 
			
		||||
        newBankButton.disabled = false;
 | 
			
		||||
        newBankButton.querySelector('svg').classList.add('hidden');
 | 
			
		||||
        newBankButton.querySelector('span').classList.remove('hidden');
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
</script>
 | 
			
		||||
@endpush
 | 
			
		||||
@ -0,0 +1,68 @@
 | 
			
		||||
@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'ACH', 'card_title' => 'ACH'])
 | 
			
		||||
 | 
			
		||||
@section('gateway_content')
 | 
			
		||||
    @if(count($tokens) > 0)
 | 
			
		||||
        <div class="alert alert-failure mb-4" hidden id="errors"></div>
 | 
			
		||||
 | 
			
		||||
        @include('portal.ninja2020.gateways.includes.payment_details')
 | 
			
		||||
 | 
			
		||||
        <form action="{{ route('client.payments.response') }}" method="post" id="server-response">
 | 
			
		||||
            @csrf
 | 
			
		||||
            <input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}">
 | 
			
		||||
            <input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}">
 | 
			
		||||
            <input type="hidden" name="source" value="">
 | 
			
		||||
            <input type="hidden" name="amount" value="{{ $amount }}">
 | 
			
		||||
            <input type="hidden" name="currency" value="{{ $currency }}">
 | 
			
		||||
            <input type="hidden" name="customer" value="{{ $customer->id }}">
 | 
			
		||||
            <input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
 | 
			
		||||
            <input type="hidden" name="client_secret" value="{{ $client_secret }}">
 | 
			
		||||
        </form>
 | 
			
		||||
 | 
			
		||||
        @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')])
 | 
			
		||||
            @if(count($tokens) > 0)
 | 
			
		||||
                @foreach($tokens as $token)
 | 
			
		||||
                    <label class="mr-4">
 | 
			
		||||
                        <input
 | 
			
		||||
                            type="radio"
 | 
			
		||||
                            data-token="{{ $token->hashed_id }}"
 | 
			
		||||
                            name="payment-type"
 | 
			
		||||
                            class="form-radio cursor-pointer toggle-payment-with-token"/>
 | 
			
		||||
                        <span class="ml-1 cursor-pointer">{{ ctrans('texts.bank_transfer') }} (*{{ $token->meta->last4 }})</span>
 | 
			
		||||
                    </label>
 | 
			
		||||
                @endforeach
 | 
			
		||||
            @endisset
 | 
			
		||||
        @endcomponent
 | 
			
		||||
 | 
			
		||||
    @include('portal.ninja2020.gateways.includes.pay_now')
 | 
			
		||||
 | 
			
		||||
    @else
 | 
			
		||||
 | 
			
		||||
    @component('portal.ninja2020.components.general.card-element-single', ['title' => 'ACH', 'show_title' => false])
 | 
			
		||||
        <span>Pay with a new bank account.</span>
 | 
			
		||||
        <button type="button" class="button button-primary bg-primary" id="new-bank">{{ ctrans('texts.new_bank_account') }}</button>
 | 
			
		||||
 | 
			
		||||
    @endcomponent
 | 
			
		||||
 | 
			
		||||
    @endif    
 | 
			
		||||
 | 
			
		||||
@endsection
 | 
			
		||||
 | 
			
		||||
@push('footer')
 | 
			
		||||
    <script>
 | 
			
		||||
        Array
 | 
			
		||||
            .from(document.getElementsByClassName('toggle-payment-with-token'))
 | 
			
		||||
            .forEach((element) => element.addEventListener('click', (element) => {
 | 
			
		||||
                document.querySelector('input[name=source]').value = element.target.dataset.token;
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
        document.getElementById('pay-now').addEventListener('click', function () {
 | 
			
		||||
 | 
			
		||||
                    let payNowButton = document.getElementById('pay-now');
 | 
			
		||||
                    payNowButton.disabled = true;
 | 
			
		||||
                    payNowButton.querySelector('svg').classList.remove('hidden');
 | 
			
		||||
                    payNowButton.querySelector('span').classList.add('hidden');
 | 
			
		||||
 | 
			
		||||
            document.getElementById('server-response').submit();
 | 
			
		||||
        });
 | 
			
		||||
    </script>
 | 
			
		||||
@endpush
 | 
			
		||||
@ -11,7 +11,6 @@
 | 
			
		||||
    
 | 
			
		||||
@endphp
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@section('gateway_head')
 | 
			
		||||
    @if($gateway->company_gateway->getConfigField('account_id'))
 | 
			
		||||
        <meta name="stripe-account-id" content="{{ $gateway->company_gateway->getConfigField('account_id') }}">
 | 
			
		||||
@ -48,32 +47,40 @@
 | 
			
		||||
    @include('portal.ninja2020.gateways.includes.payment_details')
 | 
			
		||||
 | 
			
		||||
    @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')])
 | 
			
		||||
        <ul class="list-none hover:list-disc">
 | 
			
		||||
        @if(count($tokens) > 0)
 | 
			
		||||
            @foreach($tokens as $token)
 | 
			
		||||
            <li class="py-2 hover:text-blue hover:bg-blue-600">
 | 
			
		||||
                <label class="mr-4">
 | 
			
		||||
                    <input
 | 
			
		||||
                        type="radio"
 | 
			
		||||
                        data-token="{{ $token->token }}"
 | 
			
		||||
                        name="payment-type"
 | 
			
		||||
                        class="form-radio cursor-pointer toggle-payment-with-token"/>
 | 
			
		||||
                        class="form-check-input text-indigo-600 rounded-full cursor-pointer toggle-payment-with-token toggle-payment-with-token"/>
 | 
			
		||||
                    <span class="ml-1 cursor-pointer">**** {{ optional($token->meta)->last4 }}</span>
 | 
			
		||||
                </label>
 | 
			
		||||
            </li>
 | 
			
		||||
            @endforeach
 | 
			
		||||
        @endisset
 | 
			
		||||
 | 
			
		||||
            <li class="py-2 hover:text-blue hover:bg-blue-600">
 | 
			
		||||
                <label>
 | 
			
		||||
                    <input
 | 
			
		||||
                        type="radio"
 | 
			
		||||
                        id="toggle-payment-with-credit-card"
 | 
			
		||||
                class="form-radio cursor-pointer"
 | 
			
		||||
                        class="form-check-input text-indigo-600 rounded-full cursor-pointer"
 | 
			
		||||
                        name="payment-type"
 | 
			
		||||
                        checked/>
 | 
			
		||||
                    <span class="ml-1 cursor-pointer">{{ __('texts.new_card') }}</span>
 | 
			
		||||
                </label>
 | 
			
		||||
            </li>    
 | 
			
		||||
        </ul>
 | 
			
		||||
        
 | 
			
		||||
    @endcomponent
 | 
			
		||||
 | 
			
		||||
    @include('portal.ninja2020.gateways.stripe.includes.card_widget')
 | 
			
		||||
    @include('portal.ninja2020.gateways.includes.pay_now')
 | 
			
		||||
    
 | 
			
		||||
@endsection
 | 
			
		||||
 | 
			
		||||
@section('gateway_footer')
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,57 @@
 | 
			
		||||
<button id="link-button">Link Account</button>
 | 
			
		||||
 | 
			
		||||
<script src="https://cdn.plaid.com/link/v2/stable/link-initialize.js"></script>
 | 
			
		||||
<script type="text/javascript">
 | 
			
		||||
(async function() {
 | 
			
		||||
 | 
			
		||||
  const configs = {
 | 
			
		||||
    // Pass the link_token generated in step 2.
 | 
			
		||||
    token: '{{ $link_token }}',
 | 
			
		||||
    onLoad: function() {
 | 
			
		||||
      // The Link module finished loading.
 | 
			
		||||
    },
 | 
			
		||||
    onSuccess: function(public_token, metadata) {
 | 
			
		||||
      // The onSuccess function is called when the user has
 | 
			
		||||
      // successfully authenticated and selected an account to
 | 
			
		||||
      // use.
 | 
			
		||||
      //
 | 
			
		||||
      // When called, you will send the public_token
 | 
			
		||||
      // and the selected account ID, metadata.accounts,
 | 
			
		||||
      // to your backend app server.
 | 
			
		||||
      //
 | 
			
		||||
      // sendDataToBackendServer({
 | 
			
		||||
      //   public_token: public_token,
 | 
			
		||||
      //   account_id: metadata.accounts[0].id
 | 
			
		||||
      // });
 | 
			
		||||
      console.log('Public Token: ' + public_token);
 | 
			
		||||
      switch (metadata.accounts.length) {
 | 
			
		||||
        case 0:
 | 
			
		||||
          // Select Account is disabled: https://dashboard.plaid.com/link/account-select
 | 
			
		||||
          break;
 | 
			
		||||
        case 1:
 | 
			
		||||
          console.log('Customer-selected account ID: ' + metadata.accounts[0].id);
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          // Multiple Accounts is enabled: https://dashboard.plaid.com/link/account-select
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    onExit: async function(err, metadata) {
 | 
			
		||||
      // The user exited the Link flow.
 | 
			
		||||
      if (err != null) {
 | 
			
		||||
          // The user encountered a Plaid API error
 | 
			
		||||
          // prior to exiting.
 | 
			
		||||
      }
 | 
			
		||||
      // metadata contains information about the institution
 | 
			
		||||
      // that the user selected and the most recent
 | 
			
		||||
      // API request IDs.
 | 
			
		||||
      // Storing this information can be helpful for support.
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  var linkHandler = Plaid.create(configs);
 | 
			
		||||
 | 
			
		||||
  document.getElementById('link-button').onclick = function() {
 | 
			
		||||
    linkHandler.open();
 | 
			
		||||
  };
 | 
			
		||||
})();
 | 
			
		||||
</script>
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user