Changeset View
Changeset View
Standalone View
Standalone View
src/app/Providers/Payment/Stripe.php
Show First 20 Lines • Show All 81 Lines • ▼ Show 20 Lines | public function createMandate(Wallet $wallet, array $payment): ?array | ||||
$session = StripeAPI\Checkout\Session::create($request); | $session = StripeAPI\Checkout\Session::create($request); | ||||
$payment['amount'] = 0; | $payment['amount'] = 0; | ||||
$payment['credit_amount'] = 0; | $payment['credit_amount'] = 0; | ||||
$payment['currency_amount'] = 0; | $payment['currency_amount'] = 0; | ||||
$payment['vat_rate_id'] = null; | $payment['vat_rate_id'] = null; | ||||
$payment['id'] = $session->setup_intent; | $payment['id'] = $session->setup_intent; | ||||
$payment['type'] = self::TYPE_MANDATE; | $payment['type'] = Payment::TYPE_MANDATE; | ||||
$this->storePayment($payment, $wallet->id); | $this->storePayment($payment, $wallet->id); | ||||
return [ | return [ | ||||
'id' => $session->id, | 'id' => $session->id, | ||||
]; | ]; | ||||
} | } | ||||
▲ Show 20 Lines • Show All 73 Lines • ▼ Show 20 Lines | class Stripe extends \App\Providers\PaymentProvider | ||||
* - type: first/oneoff/recurring | * - type: first/oneoff/recurring | ||||
* - description: Operation desc. | * - description: Operation desc. | ||||
* | * | ||||
* @return array Provider payment/session data: | * @return array Provider payment/session data: | ||||
* - id: Session identifier | * - id: Session identifier | ||||
*/ | */ | ||||
public function payment(Wallet $wallet, array $payment): ?array | public function payment(Wallet $wallet, array $payment): ?array | ||||
{ | { | ||||
if ($payment['type'] == self::TYPE_RECURRING) { | if ($payment['type'] == Payment::TYPE_RECURRING) { | ||||
return $this->paymentRecurring($wallet, $payment); | return $this->paymentRecurring($wallet, $payment); | ||||
} | } | ||||
// Register the user in Stripe, if not yet done | // Register the user in Stripe, if not yet done | ||||
$customer_id = self::stripeCustomerId($wallet, true); | $customer_id = self::stripeCustomerId($wallet, true); | ||||
$amount = $this->exchange($payment['amount'], $wallet->currency, $payment['currency']); | $amount = $this->exchange($payment['amount'], $wallet->currency, $payment['currency']); | ||||
$payment['currency_amount'] = $amount; | $payment['currency_amount'] = $amount; | ||||
▲ Show 20 Lines • Show All 98 Lines • ▼ Show 20 Lines | public function webhook(): int | ||||
switch ($event->type) { | switch ($event->type) { | ||||
case StripeAPI\Event::PAYMENT_INTENT_CANCELED: | case StripeAPI\Event::PAYMENT_INTENT_CANCELED: | ||||
case StripeAPI\Event::PAYMENT_INTENT_PAYMENT_FAILED: | case StripeAPI\Event::PAYMENT_INTENT_PAYMENT_FAILED: | ||||
case StripeAPI\Event::PAYMENT_INTENT_SUCCEEDED: | case StripeAPI\Event::PAYMENT_INTENT_SUCCEEDED: | ||||
$intent = $event->data->object; // @phpstan-ignore-line | $intent = $event->data->object; // @phpstan-ignore-line | ||||
$payment = Payment::find($intent->id); | $payment = Payment::find($intent->id); | ||||
if (empty($payment) || $payment->type == self::TYPE_MANDATE) { | if (empty($payment) || $payment->type == Payment::TYPE_MANDATE) { | ||||
return 404; | return 404; | ||||
} | } | ||||
switch ($intent->status) { | switch ($intent->status) { | ||||
case StripeAPI\PaymentIntent::STATUS_CANCELED: | case StripeAPI\PaymentIntent::STATUS_CANCELED: | ||||
$status = self::STATUS_CANCELED; | $status = Payment::STATUS_CANCELED; | ||||
break; | break; | ||||
case StripeAPI\PaymentIntent::STATUS_SUCCEEDED: | case StripeAPI\PaymentIntent::STATUS_SUCCEEDED: | ||||
$status = self::STATUS_PAID; | $status = Payment::STATUS_PAID; | ||||
break; | break; | ||||
default: | default: | ||||
$status = self::STATUS_FAILED; | $status = Payment::STATUS_FAILED; | ||||
} | } | ||||
DB::beginTransaction(); | DB::beginTransaction(); | ||||
if ($status == self::STATUS_PAID) { | if ($status == Payment::STATUS_PAID) { | ||||
// Update the balance, if it wasn't already | // Update the balance, if it wasn't already | ||||
if ($payment->status != self::STATUS_PAID) { | if ($payment->status != Payment::STATUS_PAID) { | ||||
$this->creditPayment($payment, $intent); | $this->creditPayment($payment, $intent); | ||||
} | } | ||||
} else { | } else { | ||||
if (!empty($intent->last_payment_error)) { | if (!empty($intent->last_payment_error)) { | ||||
// See https://stripe.com/docs/error-codes for more info | // See https://stripe.com/docs/error-codes for more info | ||||
\Log::info(sprintf( | \Log::info(sprintf( | ||||
'Stripe payment failed (%s): %s', | 'Stripe payment failed (%s): %s', | ||||
$payment->id, | $payment->id, | ||||
json_encode($intent->last_payment_error) | json_encode($intent->last_payment_error) | ||||
)); | )); | ||||
} | } | ||||
} | } | ||||
if ($payment->status != self::STATUS_PAID) { | if ($payment->status != Payment::STATUS_PAID) { | ||||
$payment->status = $status; | $payment->status = $status; | ||||
$payment->save(); | $payment->save(); | ||||
if ($status != self::STATUS_CANCELED && $payment->type == self::TYPE_RECURRING) { | if ($status != Payment::STATUS_CANCELED && $payment->type == Payment::TYPE_RECURRING) { | ||||
// Disable the mandate | // Disable the mandate | ||||
if ($status == self::STATUS_FAILED) { | if ($status == Payment::STATUS_FAILED) { | ||||
$payment->wallet->setSetting('mandate_disabled', 1); | $payment->wallet->setSetting('mandate_disabled', 1); | ||||
} | } | ||||
// Notify the user | // Notify the user | ||||
\App\Jobs\PaymentEmail::dispatch($payment); | \App\Jobs\PaymentEmail::dispatch($payment); | ||||
} | } | ||||
} | } | ||||
DB::commit(); | DB::commit(); | ||||
break; | break; | ||||
case StripeAPI\Event::SETUP_INTENT_SUCCEEDED: | case StripeAPI\Event::SETUP_INTENT_SUCCEEDED: | ||||
case StripeAPI\Event::SETUP_INTENT_SETUP_FAILED: | case StripeAPI\Event::SETUP_INTENT_SETUP_FAILED: | ||||
case StripeAPI\Event::SETUP_INTENT_CANCELED: | case StripeAPI\Event::SETUP_INTENT_CANCELED: | ||||
$intent = $event->data->object; // @phpstan-ignore-line | $intent = $event->data->object; // @phpstan-ignore-line | ||||
$payment = Payment::find($intent->id); | $payment = Payment::find($intent->id); | ||||
if (empty($payment) || $payment->type != self::TYPE_MANDATE) { | if (empty($payment) || $payment->type != Payment::TYPE_MANDATE) { | ||||
return 404; | return 404; | ||||
} | } | ||||
switch ($intent->status) { | switch ($intent->status) { | ||||
case StripeAPI\SetupIntent::STATUS_CANCELED: | case StripeAPI\SetupIntent::STATUS_CANCELED: | ||||
$status = self::STATUS_CANCELED; | $status = Payment::STATUS_CANCELED; | ||||
break; | break; | ||||
case StripeAPI\SetupIntent::STATUS_SUCCEEDED: | case StripeAPI\SetupIntent::STATUS_SUCCEEDED: | ||||
$status = self::STATUS_PAID; | $status = Payment::STATUS_PAID; | ||||
break; | break; | ||||
default: | default: | ||||
$status = self::STATUS_FAILED; | $status = Payment::STATUS_FAILED; | ||||
} | } | ||||
if ($status == self::STATUS_PAID) { | if ($status == Payment::STATUS_PAID) { | ||||
$payment->wallet->setSetting('stripe_mandate_id', $intent->id); | $payment->wallet->setSetting('stripe_mandate_id', $intent->id); | ||||
$threshold = intval((float) $payment->wallet->getSetting('mandate_balance') * 100); | $threshold = intval((float) $payment->wallet->getSetting('mandate_balance') * 100); | ||||
// Top-up the wallet if balance is below the threshold | // Top-up the wallet if balance is below the threshold | ||||
if ($payment->wallet->balance < $threshold && $payment->status != self::STATUS_PAID) { | if ($payment->wallet->balance < $threshold && $payment->status != Payment::STATUS_PAID) { | ||||
\App\Jobs\WalletCharge::dispatch($payment->wallet); | \App\Jobs\WalletCharge::dispatch($payment->wallet); | ||||
} | } | ||||
} | } | ||||
$payment->status = $status; | $payment->status = $status; | ||||
$payment->save(); | $payment->save(); | ||||
break; | break; | ||||
▲ Show 20 Lines • Show All 64 Lines • ▼ Show 20 Lines | protected static function creditPayment(Payment $payment, $intent) | ||||
if ( | if ( | ||||
!empty($intent->charges) | !empty($intent->charges) | ||||
&& ($charge = $intent->charges->data[0]) | && ($charge = $intent->charges->data[0]) | ||||
&& ($pm = $charge->payment_method_details) | && ($pm = $charge->payment_method_details) | ||||
) { | ) { | ||||
$method = self::paymentMethod($pm); | $method = self::paymentMethod($pm); | ||||
} | } | ||||
// TODO: Localization? | $payment->credit($method); | ||||
$description = $payment->type == self::TYPE_RECURRING ? 'Auto-payment' : 'Payment'; | |||||
$description .= " transaction {$payment->id} using {$method}"; | |||||
$payment->wallet->credit($payment, $description); | |||||
// Unlock the disabled auto-payment mandate | |||||
if ($payment->wallet->balance >= 0) { | |||||
$payment->wallet->setSetting('mandate_disabled', null); | |||||
} | |||||
} | } | ||||
/** | /** | ||||
* Extract payment method description from Stripe payment details | * Extract payment method description from Stripe payment details | ||||
*/ | */ | ||||
protected static function paymentMethod($details, $default = ''): string | protected static function paymentMethod($details, $default = ''): string | ||||
{ | { | ||||
switch ($details->type) { | switch ($details->type) { | ||||
Show All 23 Lines | class Stripe extends \App\Providers\PaymentProvider | ||||
* - exchangeRate: The projected exchange rate (actual rate is determined during payment) | * - exchangeRate: The projected exchange rate (actual rate is determined during payment) | ||||
* - icon: An icon (icon name) representing the method | * - icon: An icon (icon name) representing the method | ||||
*/ | */ | ||||
public function providerPaymentMethods(string $type, string $currency): array | public function providerPaymentMethods(string $type, string $currency): array | ||||
{ | { | ||||
//TODO get this from the stripe API? | //TODO get this from the stripe API? | ||||
$availableMethods = []; | $availableMethods = []; | ||||
switch ($type) { | switch ($type) { | ||||
case self::TYPE_ONEOFF: | case Payment::TYPE_ONEOFF: | ||||
$availableMethods = [ | $availableMethods = [ | ||||
self::METHOD_CREDITCARD => [ | self::METHOD_CREDITCARD => [ | ||||
'id' => self::METHOD_CREDITCARD, | 'id' => self::METHOD_CREDITCARD, | ||||
'name' => "Credit Card", | 'name' => "Credit Card", | ||||
'minimumAmount' => self::MIN_AMOUNT, | 'minimumAmount' => Payment::MIN_AMOUNT, | ||||
'currency' => $currency, | 'currency' => $currency, | ||||
'exchangeRate' => 1.0 | 'exchangeRate' => 1.0 | ||||
], | ], | ||||
self::METHOD_PAYPAL => [ | self::METHOD_PAYPAL => [ | ||||
'id' => self::METHOD_PAYPAL, | 'id' => self::METHOD_PAYPAL, | ||||
'name' => "PayPal", | 'name' => "PayPal", | ||||
'minimumAmount' => self::MIN_AMOUNT, | 'minimumAmount' => Payment::MIN_AMOUNT, | ||||
'currency' => $currency, | 'currency' => $currency, | ||||
'exchangeRate' => 1.0 | 'exchangeRate' => 1.0 | ||||
] | ] | ||||
]; | ]; | ||||
break; | break; | ||||
case self::TYPE_RECURRING: | case Payment::TYPE_RECURRING: | ||||
$availableMethods = [ | $availableMethods = [ | ||||
self::METHOD_CREDITCARD => [ | self::METHOD_CREDITCARD => [ | ||||
'id' => self::METHOD_CREDITCARD, | 'id' => self::METHOD_CREDITCARD, | ||||
'name' => "Credit Card", | 'name' => "Credit Card", | ||||
'minimumAmount' => self::MIN_AMOUNT, // Converted to cents, | 'minimumAmount' => Payment::MIN_AMOUNT, // Converted to cents, | ||||
'currency' => $currency, | 'currency' => $currency, | ||||
'exchangeRate' => 1.0 | 'exchangeRate' => 1.0 | ||||
] | ] | ||||
]; | ]; | ||||
break; | break; | ||||
} | } | ||||
return $availableMethods; | return $availableMethods; | ||||
Show All 26 Lines |