Changeset View
Standalone View
src/app/Providers/Payment/Mollie.php
Show All 34 Lines | class Mollie extends \App\Providers\PaymentProvider | ||||
/** | /** | ||||
* Create a new auto-payment mandate for a wallet. | * Create a new auto-payment mandate for a wallet. | ||||
* | * | ||||
* @param \App\Wallet $wallet The wallet | * @param \App\Wallet $wallet The wallet | ||||
* @param array $payment Payment data: | * @param array $payment Payment data: | ||||
* - amount: Value in cents (optional) | * - amount: Value in cents (optional) | ||||
* - currency: The operation currency | * - currency: The operation currency | ||||
* - description: Operation desc. | * - description: Operation desc. | ||||
machniak: Mention `methodId` argument here. | |||||
* | * | ||||
* @return array Provider payment data: | * @return array Provider payment data: | ||||
* - id: Operation identifier | * - id: Operation identifier | ||||
* - redirectUrl: the location to redirect to | * - redirectUrl: the location to redirect to | ||||
*/ | */ | ||||
public function createMandate(Wallet $wallet, array $payment): ?array | public function createMandate(Wallet $wallet, array $payment): ?array | ||||
{ | { | ||||
// Register the user in Mollie, if not yet done | // Register the user in Mollie, if not yet done | ||||
Show All 9 Lines | public function createMandate(Wallet $wallet, array $payment): ?array | ||||
'value' => sprintf('%.2f', $payment['amount'] / 100), | 'value' => sprintf('%.2f', $payment['amount'] / 100), | ||||
], | ], | ||||
'customerId' => $customer_id, | 'customerId' => $customer_id, | ||||
'sequenceType' => 'first', | 'sequenceType' => 'first', | ||||
'description' => $payment['description'], | 'description' => $payment['description'], | ||||
'webhookUrl' => Utils::serviceUrl('/api/webhooks/payment/mollie'), | 'webhookUrl' => Utils::serviceUrl('/api/webhooks/payment/mollie'), | ||||
'redirectUrl' => Utils::serviceUrl('/wallet'), | 'redirectUrl' => Utils::serviceUrl('/wallet'), | ||||
'locale' => 'en_US', | 'locale' => 'en_US', | ||||
// 'method' => 'creditcard', | 'method' => $payment['methodId'] | ||||
]; | ]; | ||||
// Create the payment in Mollie | // Create the payment in Mollie | ||||
$response = mollie()->payments()->create($request); | $response = mollie()->payments()->create($request); | ||||
if ($response->mandateId) { | if ($response->mandateId) { | ||||
$wallet->setSetting('mollie_mandate_id', $response->mandateId); | $wallet->setSetting('mollie_mandate_id', $response->mandateId); | ||||
} | } | ||||
Show All 36 Lines | class Mollie extends \App\Providers\PaymentProvider | ||||
/** | /** | ||||
* Get a auto-payment mandate for the wallet. | * Get a auto-payment mandate for the wallet. | ||||
* | * | ||||
* @param \App\Wallet $wallet The wallet | * @param \App\Wallet $wallet The wallet | ||||
* | * | ||||
* @return array|null Mandate information: | * @return array|null Mandate information: | ||||
* - id: Mandate identifier | * - id: Mandate identifier | ||||
* - method: user-friendly payment method desc. | * - method: user-friendly payment method desc. | ||||
* - isPending: the process didn't complete yet | * - isPending: the process didn't complete yet | ||||
Done Inline ActionsMention methodId here. machniak: Mention `methodId` here. | |||||
* - isValid: the mandate is valid | * - isValid: the mandate is valid | ||||
*/ | */ | ||||
public function getMandate(Wallet $wallet): ?array | public function getMandate(Wallet $wallet): ?array | ||||
{ | { | ||||
// Get the Mandate info | // Get the Mandate info | ||||
$mandate = self::mollieMandate($wallet); | $mandate = self::mollieMandate($wallet); | ||||
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') | 'method' => self::paymentMethod($mandate, 'Unknown method'), | ||||
'methodId' => $mandate->method | |||||
]; | ]; | ||||
return $result; | return $result; | ||||
} | } | ||||
/** | /** | ||||
* Get a provider name | * Get a provider name | ||||
* | * | ||||
Show All 22 Lines | class Mollie extends \App\Providers\PaymentProvider | ||||
{ | { | ||||
if ($payment['type'] == self::TYPE_RECURRING) { | if ($payment['type'] == self::TYPE_RECURRING) { | ||||
return $this->paymentRecurring($wallet, $payment); | return $this->paymentRecurring($wallet, $payment); | ||||
} | } | ||||
// Register the user in Mollie, if not yet done | // Register the user in Mollie, if not yet done | ||||
$customer_id = self::mollieCustomerId($wallet, true); | $customer_id = self::mollieCustomerId($wallet, true); | ||||
// Note: Required fields: description, amount/currency, amount/value | $amount = $payment['amount'] * $this->exchangeRate($wallet->currency, $payment['currency']); | ||||
$payment['amountInCurrency'] = $amount; | |||||
// Note: Required fields: description, amount/currency, amount/value | |||||
$request = [ | $request = [ | ||||
'amount' => [ | 'amount' => [ | ||||
'currency' => $payment['currency'], | 'currency' => $payment['currency'], | ||||
// a number with two decimals is required | // a number with two decimals is required | ||||
'value' => sprintf('%.2f', $payment['amount'] / 100), | 'value' => sprintf('%.2f', $amount / 100), | ||||
Done Inline ActionsJust a note that there are two currencies (JPK, ISK) that require no decimal places. All other work with 2 so we're fine. machniak: Just a note that there are two currencies (JPK, ISK) that require no decimal places. All other… | |||||
], | ], | ||||
'customerId' => $customer_id, | 'customerId' => $customer_id, | ||||
'sequenceType' => $payment['type'], | 'sequenceType' => $payment['type'], | ||||
'description' => $payment['description'], | 'description' => $payment['description'], | ||||
'webhookUrl' => Utils::serviceUrl('/api/webhooks/payment/mollie'), | 'webhookUrl' => Utils::serviceUrl('/api/webhooks/payment/mollie'), | ||||
'locale' => 'en_US', | 'locale' => 'en_US', | ||||
// 'method' => 'creditcard', | 'method' => $payment['methodId'], | ||||
'redirectUrl' => Utils::serviceUrl('/wallet') // required for non-recurring payments | 'redirectUrl' => Utils::serviceUrl('/wallet') // required for non-recurring payments | ||||
]; | ]; | ||||
// TODO: Additional payment parameters for better fraud protection: | // TODO: Additional payment parameters for better fraud protection: | ||||
// billingEmail - for bank transfers, Przelewy24, but not creditcard | // billingEmail - for bank transfers, Przelewy24, but not creditcard | ||||
// billingAddress (it is a structured field not just text) | // billingAddress (it is a structured field not just text) | ||||
// 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; | ||||
$this->storePayment($payment, $wallet->id); | $this->storePayment($payment, $wallet->id); | ||||
return [ | return [ | ||||
'id' => $payment['id'], | 'id' => $payment['id'], | ||||
'redirectUrl' => $response->getCheckoutUrl(), | 'redirectUrl' => $response->getCheckoutUrl(), | ||||
]; | ]; | ||||
} | } | ||||
/** | |||||
* Cancel a pending payment. | |||||
* | |||||
* @param \App\Wallet $wallet The wallet | |||||
* @param array $paymentId Payment Id | |||||
* | |||||
* @return array Provider payment data: | |||||
* - id: Operation identifier | |||||
* - redirectUrl: the location to redirect to | |||||
*/ | |||||
public function cancel(Wallet $wallet, $paymentId): ?array | |||||
{ | |||||
$response = mollie()->payments()->delete($paymentId); | |||||
Done Inline ActionsRedundant new-line ;) machniak: Redundant new-line ;) | |||||
Done Inline ActionsThese two lines could be replaced with $db_payment = Payment::find($paymentId); machniak: These two lines could be replaced with `$db_payment = Payment::find($paymentId);`
| |||||
$payment['id'] = $response->id; | |||||
$payment['status'] = $response->status; | |||||
$this->storePayment($payment, $wallet->id); | |||||
Done Inline ActionsI would probably not use storePayment() just to update the payment status. And I think this method should return just bool. machniak: I would probably not use storePayment() just to update the payment status. And I think this… | |||||
return [ | |||||
'id' => $payment['id'] | |||||
]; | |||||
} | |||||
Done Inline ActionscheckoutUrl does not make sense here. machniak: checkoutUrl does not make sense here. | |||||
Done Inline ActionsI'm just using the wallet page now. mollekopf: I'm just using the wallet page now.
I don't think I've been using it at all before. | |||||
Done Inline ActionsMy point is that 'redirectUrl' does not make sense for cancelled payments. So, the API should not contain it here. BTW, yu added 3 new routes. I'd expect some new controller tests. machniak: My point is that 'redirectUrl' does not make sense for cancelled payments. So, the API should… | |||||
/** | /** | ||||
* Create a new automatic payment operation. | * Create a new automatic payment operation. | ||||
* | * | ||||
* @param \App\Wallet $wallet The wallet | * @param \App\Wallet $wallet The wallet | ||||
* @param array $payment Payment data (see self::payment()) | * @param array $payment Payment data (see self::payment()) | ||||
* | * | ||||
* @return array Provider payment/session data: | * @return array Provider payment/session data: | ||||
* - id: Operation identifier | * - id: Operation identifier | ||||
Show All 17 Lines | protected function paymentRecurring(Wallet $wallet, array $payment): ?array | ||||
// a number with two decimals is required | // a number with two decimals is required | ||||
'value' => sprintf('%.2f', $payment['amount'] / 100), | 'value' => sprintf('%.2f', $payment['amount'] / 100), | ||||
], | ], | ||||
'customerId' => $customer_id, | 'customerId' => $customer_id, | ||||
'sequenceType' => $payment['type'], | 'sequenceType' => $payment['type'], | ||||
'description' => $payment['description'], | 'description' => $payment['description'], | ||||
'webhookUrl' => Utils::serviceUrl('/api/webhooks/payment/mollie'), | 'webhookUrl' => Utils::serviceUrl('/api/webhooks/payment/mollie'), | ||||
'locale' => 'en_US', | 'locale' => 'en_US', | ||||
// 'method' => 'creditcard', | 'method' => $payment['methodId'], | ||||
'mandateId' => $mandate->id | 'mandateId' => $mandate->id | ||||
]; | ]; | ||||
// 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; | ||||
▲ Show 20 Lines • Show All 73 Lines • ▼ Show 20 Lines | public function webhook(): int | ||||
if ($mollie_payment->hasRefunds()) { | if ($mollie_payment->hasRefunds()) { | ||||
foreach ($mollie_payment->refunds() as $refund) { | foreach ($mollie_payment->refunds() as $refund) { | ||||
if ($refund->isTransferred() && $refund->amount->value) { | if ($refund->isTransferred() && $refund->amount->value) { | ||||
$refunds[] = [ | $refunds[] = [ | ||||
'id' => $refund->id, | 'id' => $refund->id, | ||||
'description' => $refund->description, | 'description' => $refund->description, | ||||
'amount' => round(floatval($refund->amount->value) * 100), | 'amount' => round(floatval($refund->amount->value) * 100), | ||||
'type' => self::TYPE_REFUND, | 'type' => self::TYPE_REFUND, | ||||
// Note: we assume this is the original payment/wallet currency | 'currency' => $refund->amount->currency | ||||
]; | ]; | ||||
} | } | ||||
} | } | ||||
} | } | ||||
// The payment has been (partially) charged back. | // The payment has been (partially) charged back. | ||||
// Let's process chargebacks (they have no states as refunds) | // Let's process chargebacks (they have no states as refunds) | ||||
if ($mollie_payment->hasChargebacks()) { | if ($mollie_payment->hasChargebacks()) { | ||||
foreach ($mollie_payment->chargebacks() as $chargeback) { | foreach ($mollie_payment->chargebacks() as $chargeback) { | ||||
if ($chargeback->amount->value) { | if ($chargeback->amount->value) { | ||||
$refunds[] = [ | $refunds[] = [ | ||||
'id' => $chargeback->id, | 'id' => $chargeback->id, | ||||
'amount' => round(floatval($chargeback->amount->value) * 100), | 'amount' => round(floatval($chargeback->amount->value) * 100), | ||||
'type' => self::TYPE_CHARGEBACK, | 'type' => self::TYPE_CHARGEBACK, | ||||
// Note: we assume this is the original payment/wallet currency | 'currency' => $chargeback->amount->currency | ||||
]; | ]; | ||||
} | } | ||||
} | } | ||||
} | } | ||||
// In case there were multiple auto-payment setup requests (e.g. caused by a double | // In case there were multiple auto-payment setup requests (e.g. caused by a double | ||||
// form submission) we end up with multiple payment records and mollie_mandate_id | // form submission) we end up with multiple payment records and mollie_mandate_id | ||||
// pointing to the one from the last payment not the successful one. | // pointing to the one from the last payment not the successful one. | ||||
▲ Show 20 Lines • Show All 123 Lines • ▼ Show 20 Lines | class Mollie extends \App\Providers\PaymentProvider | ||||
* Extract payment method description from Mollie payment/mandate details | * Extract payment method description from Mollie payment/mandate details | ||||
*/ | */ | ||||
protected static function paymentMethod($object, $default = ''): string | protected static function paymentMethod($object, $default = ''): string | ||||
{ | { | ||||
$details = $object->details; | $details = $object->details; | ||||
// Mollie supports 3 methods here | // Mollie supports 3 methods here | ||||
switch ($object->method) { | switch ($object->method) { | ||||
case 'creditcard': | case self::METHOD_CREDITCARD: | ||||
// If the customer started, but never finished the 'first' payment | // If the customer started, but never finished the 'first' payment | ||||
// card details will be empty, and mandate will be 'pending'. | // card details will be empty, and mandate will be 'pending'. | ||||
if (empty($details->cardNumber)) { | if (empty($details->cardNumber)) { | ||||
return 'Credit Card'; | return 'Credit Card'; | ||||
} | } | ||||
return sprintf( | return sprintf( | ||||
'%s (**** **** **** %s)', | '%s (**** **** **** %s)', | ||||
$details->cardLabel ?: 'Card', // @phpstan-ignore-line | $details->cardLabel ?: 'Card', // @phpstan-ignore-line | ||||
$details->cardNumber | $details->cardNumber | ||||
); | ); | ||||
case 'directdebit': | case self::METHOD_DIRECTDEBIT: | ||||
return sprintf('Direct Debit (%s)', $details->customerAccount); | return sprintf('Direct Debit (%s)', $details->customerAccount); | ||||
case 'paypal': | case self::METHOD_PAYPAL: | ||||
return sprintf('PayPal (%s)', $details->consumerAccount); | return sprintf('PayPal (%s)', $details->consumerAccount); | ||||
} | } | ||||
return $default; | return $default; | ||||
} | } | ||||
/** | |||||
* List supported payment methods. | |||||
* | |||||
* @param string $type type: oneoff/recurring | |||||
* | |||||
* @return array Array of array with available payment methods: | |||||
* - id: id of the method | |||||
* - name: User readable name of the payment method | |||||
* - minimumAmount: Minimum amount to be charged in cents | |||||
* - currency: Currency used for the method | |||||
* - exchangeRate: The projected exchange rate (actual rate is determined during payment) | |||||
* - icon: An icon (icon name) representing the method | |||||
*/ | |||||
public function providerPaymentMethods($type): ?array | |||||
{ | |||||
$availableMethods = []; | |||||
$mapResult = function ($result) use (&$availableMethods) { | |||||
foreach ($result as $method) { | |||||
$availableMethods[$method->id] = [ | |||||
'id' => $method->id, | |||||
'name' => $method->description, | |||||
'minimumAmount' => round(floatval($method->minimumAmount->value) * 100), // Converted to cents | |||||
'currency' => $method->minimumAmount->currency, | |||||
'exchangeRate' => $this->exchangeRate('CHF', $method->minimumAmount->currency) | |||||
]; | |||||
} | |||||
}; | |||||
// Fallback to EUR methods | |||||
// TODO enable to unlock payment using methods requiring EUR | |||||
//$mapResult(mollie()->methods()->allActive( | |||||
// [ | |||||
// 'sequenceType' => $type, | |||||
// 'amount' => [ | |||||
// 'value' => '1.00', | |||||
// 'currency' => 'EUR' | |||||
// ] | |||||
// ] | |||||
//)); | |||||
// Prefer CHF methods | |||||
$mapResult(mollie()->methods()->allActive( | |||||
Done Inline ActionsUse of lambda function does not make much sense here. You can just do foreach on the mollie()->methods()->allActive() result. machniak: Use of lambda function does not make much sense here. You can just do foreach on the mollie()… | |||||
[ | |||||
'sequenceType' => $type, | |||||
'amount' => [ | |||||
'value' => '1.00', | |||||
'currency' => 'CHF' | |||||
] | |||||
] | |||||
)); | |||||
Done Inline ActionsI think it would make sense to cache the list of available methods, so we don't fetch it all the time, but maybe once a day or something like that. machniak: I think it would make sense to cache the list of available methods, so we don't fetch it all… | |||||
Done Inline ActionsIn principle I suppose that is enough, but then we would have to start handling changes (if we assume it's static we could just hardcode it). Do you think this will in practice become a problem and needs addressing? mollekopf: In principle I suppose that is enough, but then we would have to start handling changes (if we… | |||||
return $availableMethods; | |||||
} | |||||
/** | |||||
* Get a payment. | |||||
* | |||||
* @param string $paymentId Payment identifier | |||||
* | |||||
* @return array|null Payment information: | |||||
* - id: Payment identifier | |||||
* - status: Payment status | |||||
* - isCancelable: The payment can be canceled | |||||
* - chceckoutUrl: The checkout url to complete the payment or null if none | |||||
Done Inline Actionsnon -> none machniak: non -> none | |||||
*/ | |||||
Done Inline ActionsLooks to me like it always returns an array and never null. machniak: Looks to me like it always returns an array and never null. | |||||
public function getPayment($paymentId): ?array | |||||
{ | |||||
$payment = mollie()->payments()->get($paymentId); | |||||
return [ | |||||
'id' => $payment->id, | |||||
'status' => $payment->status, | |||||
'isCancelable' => $payment->isCancelable, | |||||
'checkoutUrl' => $payment->getCheckoutUrl() | |||||
]; | |||||
} | |||||
} | } |
Mention methodId argument here.