Changeset View
Changeset View
Standalone View
Standalone View
src/app/Providers/Payment/Mollie.php
<?php | <?php | ||||
namespace App\Providers\Payment; | namespace App\Providers\Payment; | ||||
use App\Payment; | use App\Payment; | ||||
use App\Utils; | use App\Utils; | ||||
use App\Wallet; | use App\Wallet; | ||||
use Illuminate\Support\Facades\DB; | |||||
class Mollie extends \App\Providers\PaymentProvider | class Mollie extends \App\Providers\PaymentProvider | ||||
{ | { | ||||
/** | /** | ||||
* Get a link to the customer in the provider's control panel | * Get a link to the customer in the provider's control panel | ||||
* | * | ||||
* @param \App\Wallet $wallet The wallet | * @param \App\Wallet $wallet The wallet | ||||
* | * | ||||
▲ Show 20 Lines • Show All 96 Lines • ▼ Show 20 Lines | public function getMandate(Wallet $wallet): ?array | ||||
if (empty($mandate)) { | if (empty($mandate)) { | ||||
return null; | return null; | ||||
} | } | ||||
$result = [ | $result = [ | ||||
'id' => $mandate->id, | 'id' => $mandate->id, | ||||
'isPending' => $mandate->isPending(), | 'isPending' => $mandate->isPending(), | ||||
'isValid' => $mandate->isValid(), | 'isValid' => $mandate->isValid(), | ||||
'method' => self::paymentMethod($mandate, 'Unknown method') | |||||
]; | ]; | ||||
$details = $mandate->details; | |||||
// Mollie supports 3 methods here | |||||
switch ($mandate->method) { | |||||
case 'creditcard': | |||||
// If the customer started, but never finished the 'first' payment | |||||
// card details will be empty, and mandate will be 'pending'. | |||||
if (empty($details->cardNumber)) { | |||||
$result['method'] = 'Credit Card'; | |||||
} else { | |||||
$result['method'] = sprintf( | |||||
'%s (**** **** **** %s)', | |||||
$details->cardLabel ?: 'Card', // @phpstan-ignore-line | |||||
$details->cardNumber | |||||
); | |||||
} | |||||
break; | |||||
case 'directdebit': | |||||
$result['method'] = sprintf( | |||||
'Direct Debit (%s)', | |||||
$details->customerAccount | |||||
); | |||||
break; | |||||
case 'paypal': | |||||
$result['method'] = sprintf('PayPal (%s)', $details->consumerAccount); | |||||
break; | |||||
default: | |||||
$result['method'] = 'Unknown method'; | |||||
} | |||||
return $result; | return $result; | ||||
} | } | ||||
/** | /** | ||||
* Get a provider name | * Get a provider name | ||||
* | * | ||||
* @return string Provider name | * @return string Provider name | ||||
*/ | */ | ||||
▲ Show 20 Lines • Show All 57 Lines • ▼ Show 20 Lines | public function payment(Wallet $wallet, array $payment): ?array | ||||
// Create the payment in Mollie | // Create the payment in Mollie | ||||
$response = mollie()->payments()->create($request); | $response = mollie()->payments()->create($request); | ||||
// Store the payment reference in database | // Store the payment reference in database | ||||
$payment['status'] = $response->status; | $payment['status'] = $response->status; | ||||
$payment['id'] = $response->id; | $payment['id'] = $response->id; | ||||
self::storePayment($payment, $wallet->id); | $this->storePayment($payment, $wallet->id); | ||||
return [ | return [ | ||||
'id' => $payment['id'], | 'id' => $payment['id'], | ||||
'redirectUrl' => $response->getCheckoutUrl(), | 'redirectUrl' => $response->getCheckoutUrl(), | ||||
]; | ]; | ||||
} | } | ||||
/** | /** | ||||
* Update payment status (and balance). | * Update payment status (and balance). | ||||
* | * | ||||
* @return int HTTP response code | * @return int HTTP response code | ||||
vanmeeuwen: Typo. | |||||
*/ | */ | ||||
public function webhook(): int | public function webhook(): int | ||||
{ | { | ||||
$payment_id = \request()->input('id'); | $payment_id = \request()->input('id'); | ||||
if (empty($payment_id)) { | if (empty($payment_id)) { | ||||
return 200; | return 200; | ||||
} | } | ||||
Show All 13 Lines | public function webhook(): int | ||||
return 200; | return 200; | ||||
} | } | ||||
if ($mollie_payment->isPaid()) { | if ($mollie_payment->isPaid()) { | ||||
if (!$mollie_payment->hasRefunds() && !$mollie_payment->hasChargebacks()) { | if (!$mollie_payment->hasRefunds() && !$mollie_payment->hasChargebacks()) { | ||||
// The payment is paid and isn't refunded or charged back. | // The payment is paid and isn't refunded or charged back. | ||||
// Update the balance, if it wasn't already | // Update the balance, if it wasn't already | ||||
if ($payment->status != self::STATUS_PAID && $payment->amount > 0) { | if ($payment->status != self::STATUS_PAID && $payment->amount > 0) { | ||||
$payment->wallet->credit($payment->amount); | $credit = true; | ||||
$notify = $payment->type == self::TYPE_RECURRING; | |||||
} | } | ||||
} elseif ($mollie_payment->hasRefunds()) { | } elseif ($mollie_payment->hasRefunds()) { | ||||
// The payment has been (partially) refunded. | // The payment has been (partially) refunded. | ||||
// The status of the payment is still "paid" | // The status of the payment is still "paid" | ||||
// TODO: Update balance | // TODO: Update balance | ||||
} elseif ($mollie_payment->hasChargebacks()) { | } elseif ($mollie_payment->hasChargebacks()) { | ||||
// The payment has been (partially) charged back. | // The payment has been (partially) charged back. | ||||
// The status of the payment is still "paid" | // The status of the payment is still "paid" | ||||
// TODO: Update balance | // TODO: Update balance | ||||
} | } | ||||
} elseif ($mollie_payment->isFailed()) { | } elseif ($mollie_payment->isFailed()) { | ||||
// Note: I didn't find a way to get any description of the problem with a payment | // Note: I didn't find a way to get any description of the problem with a payment | ||||
\Log::info(sprintf('Mollie payment failed (%s)', $payment->id)); | \Log::info(sprintf('Mollie payment failed (%s)', $payment->id)); | ||||
// Disable the mandate | |||||
if ($payment->type == self::TYPE_RECURRING) { | |||||
$notify = true; | |||||
$payment->wallet->setSetting('mandate_disabled', 1); | |||||
} | |||||
} | } | ||||
DB::beginTransaction(); | |||||
// This is a sanity check, just in case the payment provider api | // This is a sanity check, just in case the payment provider api | ||||
// sent us open -> paid -> open -> paid. So, we lock the payment after it's paid. | // sent us open -> paid -> open -> paid. So, we lock the payment after | ||||
if ($payment->status != self::STATUS_PAID) { | // recivied a "final" state. | ||||
$pending_states = [self::STATUS_OPEN, self::STATUS_PENDING, self::STATUS_AUTHORIZED]; | |||||
if (in_array($payment->status, $pending_states)) { | |||||
$payment->status = $mollie_payment->status; | $payment->status = $mollie_payment->status; | ||||
$payment->save(); | $payment->save(); | ||||
} | } | ||||
if (!empty($credit)) { | |||||
self::creditPayment($payment, $mollie_payment); | |||||
} | |||||
DB::commit(); | |||||
if (!empty($notify)) { | |||||
\App\Jobs\PaymentEmail::dispatch($payment); | |||||
} | |||||
return 200; | return 200; | ||||
} | } | ||||
/** | /** | ||||
* Get Mollie customer identifier for specified wallet. | * Get Mollie customer identifier for specified wallet. | ||||
* Create one if does not exist yet. | * Create one if does not exist yet. | ||||
* | * | ||||
* @param \App\Wallet $wallet The wallet | * @param \App\Wallet $wallet The wallet | ||||
Show All 40 Lines | protected static function mollieMandate(Wallet $wallet) | ||||
foreach ($customer->mandates() as $mandate) { | foreach ($customer->mandates() as $mandate) { | ||||
if ($mandate->isValid() || $mandate->isPending()) { | if ($mandate->isValid() || $mandate->isPending()) { | ||||
$wallet->setSetting('mollie_mandate_id', $mandate->id); | $wallet->setSetting('mollie_mandate_id', $mandate->id); | ||||
return $mandate; | return $mandate; | ||||
} | } | ||||
} | } | ||||
*/ | */ | ||||
} | } | ||||
/** | |||||
* Apply the successful payment's pecunia to the wallet | |||||
*/ | |||||
protected static function creditPayment($payment, $mollie_payment) | |||||
{ | |||||
// Extract the payment method for transaction description | |||||
$method = self::paymentMethod($mollie_payment, 'Mollie'); | |||||
// TODO: Localization? | |||||
$description = $payment->type == self::TYPE_RECURRING ? 'Auto-payment' : 'Payment'; | |||||
$description .= " transaction {$payment->id} using {$method}"; | |||||
$payment->wallet->credit($payment->amount, $description); | |||||
// Unlock the disabled auto-payment mandate | |||||
if ($payment->wallet->balance >= 0) { | |||||
$payment->wallet->setSetting('mandate_disabled', null); | |||||
} | |||||
} | |||||
/** | |||||
* Extract payment method description from Mollie payment/mandate details | |||||
*/ | |||||
protected static function paymentMethod($object, $default = ''): string | |||||
{ | |||||
$details = $object->details; | |||||
// Mollie supports 3 methods here | |||||
switch ($object->method) { | |||||
case 'creditcard': | |||||
// If the customer started, but never finished the 'first' payment | |||||
// card details will be empty, and mandate will be 'pending'. | |||||
if (empty($details->cardNumber)) { | |||||
return 'Credit Card'; | |||||
} | |||||
return sprintf( | |||||
'%s (**** **** **** %s)', | |||||
$details->cardLabel ?: 'Card', // @phpstan-ignore-line | |||||
$details->cardNumber | |||||
); | |||||
case 'directdebit': | |||||
return sprintf('Direct Debit (%s)', $details->customerAccount); | |||||
case 'paypal': | |||||
return sprintf('PayPal (%s)', $details->consumerAccount); | |||||
} | |||||
return $default; | |||||
} | |||||
} | } |
Typo.