diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index 6963714437..6b7654bf12 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -133,12 +133,12 @@ class Gateway extends StaticModel return [GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true]]; //Payfast case 7: return [ - GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true, 'webhooks' => ['all']], // Mollie - GatewayType::BANK_TRANSFER => ['refund' => false, 'token_billing' => true, 'webhooks' => ['all']], + GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => ['all']], + GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['all']], GatewayType::KBC => ['refund' => false, 'token_billing' => false, 'webhooks' => ['all']], GatewayType::BANCONTACT => ['refund' => false, 'token_billing' => false, 'webhooks' => ['all']], - GatewayType::IDEAL => ['refund' => false, 'token_billing' => false, 'webhooks' => ['all']], - ]; + GatewayType::IDEAL => ['refund' => true, 'token_billing' => false, 'webhooks' => ['all']], + ]; // Mollie case 15: return [ GatewayType::PAYPAL => ['refund' => false, 'token_billing' => false], diff --git a/app/Models/GatewayType.php b/app/Models/GatewayType.php index 0f82b5b29b..db17678c40 100644 --- a/app/Models/GatewayType.php +++ b/app/Models/GatewayType.php @@ -29,7 +29,6 @@ namespace App\Models; * @method static \Illuminate\Database\Eloquent\Builder|GatewayType whereAlias($value) * @method static \Illuminate\Database\Eloquent\Builder|GatewayType whereId($value) * @method static \Illuminate\Database\Eloquent\Builder|GatewayType whereName($value) - * @property-read \Illuminate\Database\Eloquent\Collection $payment_methods * @mixin \Eloquent */ class GatewayType extends StaticModel diff --git a/app/PaymentDrivers/Mollie/Bancontact.php b/app/PaymentDrivers/Mollie/Bancontact.php index 26a4ae9082..9a9f1738f2 100644 --- a/app/PaymentDrivers/Mollie/Bancontact.php +++ b/app/PaymentDrivers/Mollie/Bancontact.php @@ -37,33 +37,22 @@ class Bancontact implements MethodInterface, LivewireMethodInterface $this->mollie->init(); } - /** - * Show the authorization page for Bancontact. - * - * @param array $data - * @return \Illuminate\View\View - */ + /** @inheritDoc */ public function authorizeView(array $data): View { return render('gateways.mollie.bancontact.authorize', $data); } - /** - * Handle the authorization for Bancontact. - * - * @param Request $request - * @return \Illuminate\Http\RedirectResponse - */ + /** @inheritDoc */ public function authorizeResponse(Request $request): RedirectResponse { return redirect()->route('client.payment_methods.index'); } /** - * Show the payment page for Bancontact. - * - * @param array $data - * @return \Illuminate\Http\RedirectResponseor|RedirectResponse + * @throws \Exception + * @throws PaymentFailed + * @inheritDoc */ public function paymentView(array $data) { @@ -95,44 +84,17 @@ class Bancontact implements MethodInterface, LivewireMethodInterface $this->mollie->payment_hash->withData('payment_id', $payment->id); - return redirect( - $payment->getCheckoutUrl() - ); - } catch (\Mollie\Api\Exceptions\ApiException | \Exception $exception) { + return redirect($payment->getCheckoutUrl()); + } catch (\Exception $exception) { return $this->processUnsuccessfulPayment($exception); } } /** - * Handle unsuccessful payment. - * - * @param Exception $exception - * @throws PaymentFailed - * @return void + * @throws PaymentFailed When the payment fails + * @inheritDoc */ - public function processUnsuccessfulPayment(\Exception $exception): void - { - $this->mollie->sendFailureMail($exception->getMessage()); - - SystemLogger::dispatch( - $exception->getMessage(), - SystemLog::CATEGORY_GATEWAY_RESPONSE, - SystemLog::EVENT_GATEWAY_FAILURE, - SystemLog::TYPE_MOLLIE, - $this->mollie->client, - $this->mollie->client->company, - ); - - throw new PaymentFailed($exception->getMessage(), $exception->getCode()); - } - - /** - * Handle the payments for the KBC. - * - * @param PaymentResponseRequest $request - * @return mixed - */ - public function paymentResponse(PaymentResponseRequest $request) + public function paymentResponse(PaymentResponseRequest $request): \Illuminate\Http\Response|RedirectResponse { if (! \property_exists($this->mollie->payment_hash->data, 'payment_id')) { return $this->processUnsuccessfulPayment( @@ -141,19 +103,19 @@ class Bancontact implements MethodInterface, LivewireMethodInterface } try { - $payment = $this->mollie->gateway->payments->get( + $molliePayment = $this->mollie->gateway->payments->get( $this->mollie->payment_hash->data->payment_id ); - if ($payment->status === 'paid') { - return $this->processSuccessfulPayment($payment); + if ($molliePayment->status === 'paid') { + return $this->processSuccessfulPayment($molliePayment); } - if ($payment->status === 'open') { - return $this->processOpenPayment($payment); + if ($molliePayment->status === 'open') { + return $this->processOpenPayment($molliePayment); } - if ($payment->status === 'failed') { + if ($molliePayment->status === 'failed') { return $this->processUnsuccessfulPayment( new PaymentFailed(ctrans('texts.status_failed')) ); @@ -162,7 +124,7 @@ class Bancontact implements MethodInterface, LivewireMethodInterface return $this->processUnsuccessfulPayment( new PaymentFailed(ctrans('texts.status_voided')) ); - } catch (\Mollie\Api\Exceptions\ApiException | \Exception $exception) { + } catch (\Exception $exception) { return $this->processUnsuccessfulPayment($exception); } } @@ -170,17 +132,17 @@ class Bancontact implements MethodInterface, LivewireMethodInterface /** * Handle the successful payment for Bancontact. * - * @param string $status - * @param ResourcesPayment $payment + * @param \Mollie\Api\Resources\Payment $molliePayment The Mollie payment object + * @param string $status The payment status (default: 'paid') * @return \Illuminate\Http\RedirectResponse */ - public function processSuccessfulPayment(\Mollie\Api\Resources\Payment $payment, string $status = 'paid'): RedirectResponse + public function processSuccessfulPayment(\Mollie\Api\Resources\Payment $molliePayment, string $status = 'paid'): RedirectResponse { $data = [ 'gateway_type_id' => GatewayType::BANCONTACT, 'amount' => array_sum(array_column($this->mollie->payment_hash->invoices(), 'amount')) + $this->mollie->payment_hash->fee_total, 'payment_type' => PaymentType::BANCONTACT, - 'transaction_reference' => $payment->id, + 'transaction_reference' => $molliePayment->id, ]; $payment_record = $this->mollie->createPayment( @@ -189,7 +151,7 @@ class Bancontact implements MethodInterface, LivewireMethodInterface ); SystemLogger::dispatch( - ['response' => $payment, 'data' => $data], + ['response' => $molliePayment, 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_MOLLIE, @@ -203,17 +165,45 @@ class Bancontact implements MethodInterface, LivewireMethodInterface /** * Handle 'open' payment status for Bancontact. * - * @param ResourcesPayment $payment + * @param \Mollie\Api\Resources\Payment $molliePayment The Mollie payment object * @return \Illuminate\Http\RedirectResponse */ - public function processOpenPayment(\Mollie\Api\Resources\Payment $payment): RedirectResponse + public function processOpenPayment(\Mollie\Api\Resources\Payment $molliePayment): RedirectResponse { - return $this->processSuccessfulPayment($payment, 'open'); + return $this->processSuccessfulPayment($molliePayment, 'open'); } /** - * @inheritDoc + * Handle unsuccessful payment. + * + * @param \Exception $exception The exception that was thrown + * @throws PaymentFailed When the payment fails + * @return \Illuminate\Http\Response */ + public function processUnsuccessfulPayment(\Exception $exception): \Illuminate\Http\Response + { + $this->mollie->sendFailureMail($exception->getMessage()); + + SystemLogger::dispatch( + $exception->getMessage(), + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_FAILURE, + SystemLog::TYPE_MOLLIE, + $this->mollie->client, + $this->mollie->client->company, + ); + + $response = response([ + 'message' => $exception->getMessage(), + 'code' => $exception->getCode(), + ]); + + throw new PaymentFailed($exception->getMessage(), $exception->getCode()); + + return $response; + } + + /** @inheritDoc */ public function livewirePaymentView(array $data): string { // Doesn't support, it's offsite payment method. @@ -221,9 +211,7 @@ class Bancontact implements MethodInterface, LivewireMethodInterface return ''; } - /** - * @inheritDoc - */ + /** @inheritDoc */ public function paymentData(array $data): array { $this->paymentView($data); diff --git a/app/PaymentDrivers/Mollie/BankTransfer.php b/app/PaymentDrivers/Mollie/BankTransfer.php index 6b6aa49f2e..162403e988 100644 --- a/app/PaymentDrivers/Mollie/BankTransfer.php +++ b/app/PaymentDrivers/Mollie/BankTransfer.php @@ -25,7 +25,6 @@ use App\PaymentDrivers\MolliePaymentDriver; use Exception; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; -use Illuminate\Routing\Redirector; use Illuminate\View\View; use Mollie\Api\Resources\Payment as ResourcesPayment; @@ -40,33 +39,21 @@ class BankTransfer implements MethodInterface, LivewireMethodInterface $this->mollie->init(); } - /** - * Show the authorization page for bank transfer. - * - * @param array $data - * @return \Illuminate\View\View - */ + /** @inheritDoc */ public function authorizeView(array $data): View { return render('gateways.mollie.bank_transfer.authorize', $data); } - /** - * Handle the authorization for bank transfer. - * - * @param Request $request - * @return \Illuminate\Http\RedirectResponse - */ + /** @inheritDoc */ public function authorizeResponse(Request $request): RedirectResponse { return redirect()->route('client.payment_methods.index'); } /** - * Show the payment page for bank transfer. - * - * @param array $data - * @return \Illuminate\Http\RedirectResponseor|RedirectResponse + * @throws \Exception + * @inheritDoc */ public function paymentView(array $data) { @@ -98,44 +85,17 @@ class BankTransfer implements MethodInterface, LivewireMethodInterface $this->mollie->payment_hash->withData('payment_id', $payment->id); - return redirect( - $payment->getCheckoutUrl() - ); - } catch (\Mollie\Api\Exceptions\ApiException | \Exception $exception) { + return redirect($payment->getCheckoutUrl()); + } catch (\Exception $exception) { return $this->processUnsuccessfulPayment($exception); } } /** - * Handle unsuccessful payment. - * - * @param Exception $e - * @throws PaymentFailed - * @return void + * @throws PaymentFailed When the payment fails + * @inheritDoc */ - public function processUnsuccessfulPayment(Exception $e): void - { - $this->mollie->sendFailureMail($e->getMessage()); - - SystemLogger::dispatch( - $e->getMessage(), - SystemLog::CATEGORY_GATEWAY_RESPONSE, - SystemLog::EVENT_GATEWAY_FAILURE, - SystemLog::TYPE_MOLLIE, - $this->mollie->client, - $this->mollie->client->company, - ); - - throw new PaymentFailed($e->getMessage(), $e->getCode()); - } - - /** - * Handle the payments for the bank transfer. - * - * @param PaymentResponseRequest $request - * @return mixed - */ - public function paymentResponse(PaymentResponseRequest $request) + public function paymentResponse(PaymentResponseRequest $request): \Illuminate\Http\Response|RedirectResponse { if (! \property_exists($this->mollie->payment_hash->data, 'payment_id')) { return $this->processUnsuccessfulPayment( @@ -144,22 +104,22 @@ class BankTransfer implements MethodInterface, LivewireMethodInterface } try { - $payment = $this->mollie->gateway->payments->get( + $molliePayment = $this->mollie->gateway->payments->get( $this->mollie->payment_hash->data->payment_id ); - if ($payment->status === 'paid') { - return $this->processSuccessfulPayment($payment); + if ($molliePayment->status === 'paid') { + return $this->processSuccessfulPayment($molliePayment); } - if ($payment->status === 'open') { - return $this->processOpenPayment($payment); + if ($molliePayment->status === 'open') { + return $this->processOpenPayment($molliePayment); } return $this->processUnsuccessfulPayment( new PaymentFailed(ctrans('texts.status_voided')) ); - } catch (\Mollie\Api\Exceptions\ApiException | \Exception $exception) { + } catch (\Exception $exception) { return $this->processUnsuccessfulPayment($exception); } } @@ -167,17 +127,17 @@ class BankTransfer implements MethodInterface, LivewireMethodInterface /** * Handle the successful payment for bank transfer. * - * @param ResourcesPayment $payment - * @param string $status + * @param \Mollie\Api\Resources\Payment $molliePayment The Mollie payment object + * @param string $status The payment status (default: 'paid') * @return \Illuminate\Http\RedirectResponse */ - public function processSuccessfulPayment(ResourcesPayment $payment, $status = 'paid'): RedirectResponse + public function processSuccessfulPayment(ResourcesPayment $molliePayment, $status = 'paid'): RedirectResponse { $data = [ 'gateway_type_id' => GatewayType::BANK_TRANSFER, 'amount' => array_sum(array_column($this->mollie->payment_hash->invoices(), 'amount')) + $this->mollie->payment_hash->fee_total, 'payment_type' => PaymentType::MOLLIE_BANK_TRANSFER, - 'transaction_reference' => $payment->id, + 'transaction_reference' => $molliePayment->id, ]; $payment_record = $this->mollie->createPayment( @@ -186,7 +146,7 @@ class BankTransfer implements MethodInterface, LivewireMethodInterface ); SystemLogger::dispatch( - ['response' => $payment, 'data' => $data], + ['response' => $molliePayment, 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_MOLLIE, @@ -200,17 +160,45 @@ class BankTransfer implements MethodInterface, LivewireMethodInterface /** * Handle 'open' payment status for bank transfer. * - * @param ResourcesPayment $payment + * @param \Mollie\Api\Resources\Payment $molliePayment The Mollie payment object * @return \Illuminate\Http\RedirectResponse */ - public function processOpenPayment(ResourcesPayment $payment): RedirectResponse + public function processOpenPayment(ResourcesPayment $molliePayment): RedirectResponse { - return $this->processSuccessfulPayment($payment, 'open'); + return $this->processSuccessfulPayment($molliePayment, 'open'); } /** - * @inheritDoc + * Handle unsuccessful payment. + * + * @param \Exception $e The exception that was thrown + * @throws PaymentFailed When the payment fails + * @return \Illuminate\Http\Response */ + public function processUnsuccessfulPayment(Exception $e): \Illuminate\Http\Response + { + $this->mollie->sendFailureMail($e->getMessage()); + + SystemLogger::dispatch( + $e->getMessage(), + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_FAILURE, + SystemLog::TYPE_MOLLIE, + $this->mollie->client, + $this->mollie->client->company, + ); + + $response = response([ + 'message' => $e->getMessage(), + 'code' => $e->getCode(), + ]); + + throw new PaymentFailed($e->getMessage(), $e->getCode()); + + return $response; + } + + /** @inheritDoc */ public function livewirePaymentView(array $data): string { // Doesn't support, it's offsite payment method. @@ -218,9 +206,7 @@ class BankTransfer implements MethodInterface, LivewireMethodInterface return ''; } - /** - * @inheritDoc - */ + /** @inheritDoc */ public function paymentData(array $data): array { $this->paymentView($data); diff --git a/app/PaymentDrivers/Mollie/CreditCard.php b/app/PaymentDrivers/Mollie/CreditCard.php index 58b844bf78..ae06dc36eb 100644 --- a/app/PaymentDrivers/Mollie/CreditCard.php +++ b/app/PaymentDrivers/Mollie/CreditCard.php @@ -11,12 +11,12 @@ use App\Models\Payment; use App\Models\PaymentType; use App\Models\SystemLog; use App\PaymentDrivers\Common\LivewireMethodInterface; +use App\PaymentDrivers\Common\MethodInterface; use App\PaymentDrivers\MolliePaymentDriver; -use Illuminate\Contracts\View\Factory; use Illuminate\Http\RedirectResponse; use Illuminate\View\View; -class CreditCard implements LivewireMethodInterface +class CreditCard implements MethodInterface, LivewireMethodInterface { /** * @var MolliePaymentDriver @@ -30,12 +30,19 @@ class CreditCard implements LivewireMethodInterface $this->mollie->init(); } - /** - * Show the page for credit card payments. - * - * @param array $data - * @return Factory|View - */ + /** @inheritDoc */ + public function authorizeView(array $data): View + { + return render('gateways.mollie.credit_card.authorize', $data); + } + + /** @inheritDoc */ + public function authorizeResponse($request): RedirectResponse + { + return redirect()->route('client.payment_methods.index'); + } + + /** @inheritDoc */ public function paymentView(array $data) { $data = $this->paymentData($data); @@ -44,12 +51,10 @@ class CreditCard implements LivewireMethodInterface } /** - * Create a payment object. - * - * @param PaymentResponseRequest $request - * @return mixed + * @throws PaymentFailed When the payment processing fails + * @inheritDoc */ - public function paymentResponse(PaymentResponseRequest $request) + public function paymentResponse(PaymentResponseRequest $request): \Illuminate\Http\Response|RedirectResponse { $amount = $this->mollie->convertToMollieAmount((float) $this->mollie->payment_hash->data->amount_with_fee); @@ -63,8 +68,8 @@ class CreditCard implements LivewireMethodInterface try { $cgt = ClientGatewayToken::where('token', $request->token)->firstOrFail(); - $payment = $this->mollie->gateway->payments->create([ - 'method' => 'creditcard', + $molliePayment = $this->mollie->gateway->payments->create([ + 'method' => 'creditcard', 'amount' => [ 'currency' => $this->mollie->client->currency()->code, 'value' => $amount, @@ -83,22 +88,22 @@ class CreditCard implements LivewireMethodInterface ], ]); - if ($payment->status === 'paid') { + if ($molliePayment->status === 'paid') { $this->mollie->logSuccessfulGatewayResponse( - ['response' => $payment, 'data' => $this->mollie->payment_hash->data], + ['response' => $molliePayment, 'data' => $this->mollie->payment_hash->data], SystemLog::TYPE_MOLLIE ); - return $this->processSuccessfulPayment($payment); + return $this->processSuccessfulPayment($molliePayment); } - if ($payment->status === 'open') { - $this->mollie->payment_hash->withData('payment_id', $payment->id); + if ($molliePayment->status === 'open') { + $this->mollie->payment_hash->withData('payment_id', $molliePayment->id); - if (!$payment->getCheckoutUrl()) { + if (!$molliePayment->getCheckoutUrl()) { return render('gateways.mollie.mollie_placeholder'); } else { - return redirect()->away($payment->getCheckoutUrl()); + return redirect()->away($molliePayment->getCheckoutUrl()); } } } catch (\Exception $e) { @@ -131,43 +136,50 @@ class CreditCard implements LivewireMethodInterface ]; if ($request->shouldStoreToken()) { - $customer = $this->mollie->gateway->customers->create([ - 'name' => $this->mollie->client->name, - 'email' => $this->mollie->client->present()->email(), - 'metadata' => [ - 'id' => $this->mollie->client->hashed_id, - ], - ]); + // Check if a mollie CustomerId already exists for this client, if so, use that + $gateway_customer_reference = null; + if ($this->mollie->client->gateway_tokens->count() > 0) { + $gateway_customer_reference = $this->mollie->client->gateway_tokens->first()->gateway_customer_reference; + } else { + $customer = $this->mollie->gateway->customers->create([ + 'name' => $this->mollie->client->name, + 'email' => $this->mollie->client->present()->email(), + 'metadata' => [ + 'id' => $this->mollie->client->hashed_id, + ], + ]); + $gateway_customer_reference = $customer->id; + } - $data['customerId'] = $customer->id; + $data['customerId'] = $gateway_customer_reference; $data['sequenceType'] = 'first'; $this->mollie->payment_hash - ->withData('mollieCustomerId', $customer->id) + ->withData('mollieCustomerId', $gateway_customer_reference) ->withData('shouldStoreToken', true); } - $payment = $this->mollie->gateway->payments->create($data); + $molliePayment = $this->mollie->gateway->payments->create($data); - if ($payment->status === 'paid') { + if ($molliePayment->status === 'paid') { $this->mollie->logSuccessfulGatewayResponse( - ['response' => $payment, 'data' => $this->mollie->payment_hash->data], + ['response' => $molliePayment, 'data' => $this->mollie->payment_hash->data], SystemLog::TYPE_MOLLIE ); - return $this->processSuccessfulPayment($payment); + return $this->processSuccessfulPayment($molliePayment); } - if ($payment->status === 'open') { - $this->mollie->payment_hash->withData('payment_id', $payment->id); + if ($molliePayment->status === 'open') { + $this->mollie->payment_hash->withData('payment_id', $molliePayment->id); nlog("Mollie"); - nlog($payment); + nlog($molliePayment); - if (!$payment->getCheckoutUrl()) { - return render('gateways.mollie.mollie_placeholder'); + if (!$molliePayment->getCheckoutUrl()) { + return response()->render('gateways.mollie.mollie_placeholder'); } else { - return redirect()->away($payment->getCheckoutUrl()); + return redirect()->away($molliePayment->getCheckoutUrl()); } } } catch (\Exception $e) { @@ -175,44 +187,32 @@ class CreditCard implements LivewireMethodInterface throw new PaymentFailed($e->getMessage(), $e->getCode()); } + return response()->render('gateways.mollie.mollie_placeholder'); } - public function processSuccessfulPayment(\Mollie\Api\Resources\Payment $payment) + /** + * Process a successful credit card payment. + * + * @param \Mollie\Api\Resources\Payment $molliePayment The Mollie payment object + * @return \Illuminate\Http\RedirectResponse + */ + public function processSuccessfulPayment(\Mollie\Api\Resources\Payment $molliePayment): RedirectResponse { $payment_hash = $this->mollie->payment_hash; - if (property_exists($payment_hash->data, 'shouldStoreToken') && $payment_hash->data->shouldStoreToken) { - try { - $mandates = \iterator_to_array($this->mollie->gateway->mandates->listForId($payment_hash->data->mollieCustomerId)); - } catch (\Mollie\Api\Exceptions\ApiException $e) { - return $this->processUnsuccessfulPayment($e); - } - - $payment_meta = new \stdClass(); - $payment_meta->exp_month = (string) $mandates[0]->details->cardExpiryDate; - $payment_meta->exp_year = (string) ''; - $payment_meta->brand = (string) $mandates[0]->details->cardLabel; - $payment_meta->last4 = (string) $mandates[0]->details->cardNumber; - $payment_meta->type = GatewayType::CREDIT_CARD; - - $this->mollie->storeGatewayToken([ - 'token' => $mandates[0]->id, - 'payment_method_id' => GatewayType::CREDIT_CARD, - 'payment_meta' => $payment_meta, - ], ['gateway_customer_reference' => $payment_hash->data->mollieCustomerId]); - } + $this->mollie->createClientGatewayTokenFromMolliePayment($molliePayment); $data = [ 'gateway_type_id' => GatewayType::CREDIT_CARD, 'amount' => array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total, 'payment_type' => PaymentType::CREDIT_CARD_OTHER, - 'transaction_reference' => $payment->id, + 'transaction_reference' => $molliePayment->id, ]; - $payment_record = $this->mollie->createPayment($data, $payment->status === 'paid' ? Payment::STATUS_COMPLETED : Payment::STATUS_PENDING); + $payment_record = $this->mollie->createPayment($data, $molliePayment->status === 'paid' ? Payment::STATUS_COMPLETED : Payment::STATUS_PENDING); SystemLogger::dispatch( - ['response' => $payment, 'data' => $data], + ['response' => $molliePayment, 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_MOLLIE, @@ -223,7 +223,14 @@ class CreditCard implements LivewireMethodInterface return redirect()->route('client.payments.show', ['payment' => $this->mollie->encodePrimaryKey($payment_record->id)]); } - public function processUnsuccessfulPayment(\Exception $e) + /** + * Handle an unsuccessful payment attempt. + * + * @param \Exception $e The exception that was thrown + * @throws PaymentFailed Always throws a PaymentFailed exception + * @return \Illuminate\Http\Response + */ + public function processUnsuccessfulPayment(\Exception $e): \Illuminate\Http\Response { $this->mollie->sendFailureMail($e->getMessage()); @@ -236,42 +243,23 @@ class CreditCard implements LivewireMethodInterface $this->mollie->client->company, ); + $response = response([ + 'message' => $e->getMessage(), + 'code' => $e->getCode(), + ]); + throw new PaymentFailed($e->getMessage(), $e->getCode()); + + return $response; } - /** - * Show authorization page. - * - * @param array $data - * @return Factory|View - */ - public function authorizeView(array $data) - { - return render('gateways.mollie.credit_card.authorize', $data); - } - - /** - * Handle authorization response. - * - * @param mixed $request - * @return \Illuminate\Http\RedirectResponse - */ - public function authorizeResponse($request): RedirectResponse - { - return redirect()->route('client.payment_methods.index'); - } - - /** - * @inheritDoc - */ + /** @inheritDoc */ public function livewirePaymentView(array $data): string { return 'gateways.mollie.credit_card.pay_livewire'; } - /** - * @inheritDoc - */ + /** @inheritDoc */ public function paymentData(array $data): array { $data['gateway'] = $this->mollie; diff --git a/app/PaymentDrivers/Mollie/IDEAL.php b/app/PaymentDrivers/Mollie/IDEAL.php index 21c92f7bcb..04bc9b2629 100644 --- a/app/PaymentDrivers/Mollie/IDEAL.php +++ b/app/PaymentDrivers/Mollie/IDEAL.php @@ -25,6 +25,7 @@ use App\PaymentDrivers\MolliePaymentDriver; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\View\View; +use Mollie\Api\Exceptions\ApiException; class IDEAL implements MethodInterface, LivewireMethodInterface { @@ -37,33 +38,21 @@ class IDEAL implements MethodInterface, LivewireMethodInterface $this->mollie->init(); } - /** - * Show the authorization page for iDEAL. - * - * @param array $data - * @return \Illuminate\View\View - */ + /** @inheritDoc */ public function authorizeView(array $data): View { return render('gateways.mollie.ideal.authorize', $data); } - /** - * Handle the authorization for iDEAL. - * - * @param Request $request - * @return \Illuminate\Http\RedirectResponse - */ + /** @inheritDoc */ public function authorizeResponse(Request $request): RedirectResponse { return redirect()->route('client.payment_methods.index'); } /** - * Show the payment page for iDEAL. - * - * @param array $data - * @return \Illuminate\Http\RedirectResponseor|RedirectResponse + * @throws \Exception + * @inheritDoc */ public function paymentView(array $data) { @@ -72,7 +61,7 @@ class IDEAL implements MethodInterface, LivewireMethodInterface ->withData('client_id', $this->mollie->client->id); try { - $payment = $this->mollie->gateway->payments->create([ + $data = [ 'method' => 'ideal', 'amount' => [ 'currency' => $this->mollie->client->currency()->code, @@ -91,48 +80,48 @@ class IDEAL implements MethodInterface, LivewireMethodInterface 'gateway_type_id' => GatewayType::IDEAL, 'payment_type_id' => PaymentType::IDEAL, ], - ]); + ]; + + if ($this->mollie->company_gateway->token_billing == 'always') { + // Check if a mollie CustomerId already exists for this client, if so, use that + $gateway_customer_reference = null; + if ($this->mollie->client->gateway_tokens->count() > 0) { + $gateway_customer_reference = $this->mollie->client->gateway_tokens->first()->gateway_customer_reference; + } + if (!$gateway_customer_reference) { + $customer = $this->mollie->gateway->customers->create([ + 'name' => $this->mollie->client->name, + 'email' => $this->mollie->client->present()->email(), + 'metadata' => [ + 'id' => $this->mollie->client->hashed_id, + ], + ]); + $gateway_customer_reference = $customer->id; + } + + $data['customerId'] = $gateway_customer_reference; + $data['sequenceType'] = 'first'; + + $this->mollie->payment_hash + ->withData('mollieCustomerId', $gateway_customer_reference) + ->withData('shouldStoreToken', true); + } + + $payment = $this->mollie->gateway->payments->create($data); $this->mollie->payment_hash->withData('payment_id', $payment->id); - return redirect( - $payment->getCheckoutUrl() - ); - } catch (\Mollie\Api\Exceptions\ApiException | \Exception $exception) { + return redirect($payment->getCheckoutUrl()); + } catch (\Exception $exception) { return $this->processUnsuccessfulPayment($exception); } } /** - * Handle unsuccessful payment. - * - * @param Exception $exception - * @throws PaymentFailed - * @return void + * @throws PaymentFailed When the payment fails + * @inheritDoc */ - public function processUnsuccessfulPayment(\Exception $exception): void - { - $this->mollie->sendFailureMail($exception->getMessage()); - - SystemLogger::dispatch( - $exception->getMessage(), - SystemLog::CATEGORY_GATEWAY_RESPONSE, - SystemLog::EVENT_GATEWAY_FAILURE, - SystemLog::TYPE_MOLLIE, - $this->mollie->client, - $this->mollie->client->company, - ); - - throw new PaymentFailed($exception->getMessage(), $exception->getCode()); - } - - /** - * Handle the payments for the iDEAL. - * - * @param PaymentResponseRequest $request - * @return mixed - */ - public function paymentResponse(PaymentResponseRequest $request) + public function paymentResponse(PaymentResponseRequest $request): \Illuminate\Http\Response|RedirectResponse { if (! \property_exists($this->mollie->payment_hash->data, 'payment_id')) { return $this->processUnsuccessfulPayment( @@ -141,19 +130,19 @@ class IDEAL implements MethodInterface, LivewireMethodInterface } try { - $payment = $this->mollie->gateway->payments->get( + $molliePayment = $this->mollie->gateway->payments->get( $this->mollie->payment_hash->data->payment_id ); - if ($payment->status === 'paid') { - return $this->processSuccessfulPayment($payment); + if ($molliePayment->status === 'paid') { + return $this->processSuccessfulPayment($molliePayment); } - if ($payment->status === 'open') { - return $this->processOpenPayment($payment); + if ($molliePayment->status === 'open') { + return $this->processOpenPayment($molliePayment); } - if ($payment->status === 'failed') { + if ($molliePayment->status === 'failed') { return $this->processUnsuccessfulPayment( new PaymentFailed(ctrans('texts.status_failed')) ); @@ -162,7 +151,7 @@ class IDEAL implements MethodInterface, LivewireMethodInterface return $this->processUnsuccessfulPayment( new PaymentFailed(ctrans('texts.status_voided')) ); - } catch (\Mollie\Api\Exceptions\ApiException | \Exception $exception) { + } catch (\Exception $exception) { return $this->processUnsuccessfulPayment($exception); } } @@ -170,16 +159,21 @@ class IDEAL implements MethodInterface, LivewireMethodInterface /** * Handle the successful payment for iDEAL. * - * @param string $status - * @param ResourcesPayment $payment + * @param \Mollie\Api\Resources\Payment $molliePayment The Mollie payment object + * @param string $status The payment status (default: 'paid') * @return \Illuminate\Http\RedirectResponse + * @throws ApiException */ - public function processSuccessfulPayment(\Mollie\Api\Resources\Payment $payment, string $status = 'paid'): RedirectResponse + public function processSuccessfulPayment(\Mollie\Api\Resources\Payment $molliePayment, string $status = 'paid'): RedirectResponse { - $p = \App\Models\Payment::query() - ->withTrashed() + $payment_hash = $this->mollie->payment_hash; + + $this->mollie->createClientGatewayTokenFromMolliePayment($molliePayment); + + /** @var \App\Models\Payment $p */ + $p = \App\Models\Payment::withTrashed() ->where('company_id', $this->mollie->client->company_id) - ->where('transaction_reference', $payment->id) + ->where('transaction_reference', $molliePayment->id) ->first(); if ($p) { @@ -193,8 +187,7 @@ class IDEAL implements MethodInterface, LivewireMethodInterface 'gateway_type_id' => GatewayType::IDEAL, 'amount' => array_sum(array_column($this->mollie->payment_hash->invoices(), 'amount')) + $this->mollie->payment_hash->fee_total, 'payment_type' => PaymentType::IDEAL, - 'transaction_reference' => $payment->id, - 'idempotency_key' => substr("{$payment->id}{$this->mollie->payment_hash->hash}", 0, 64) + 'transaction_reference' => $molliePayment->id, ]; $payment_record = $this->mollie->createPayment( @@ -203,7 +196,7 @@ class IDEAL implements MethodInterface, LivewireMethodInterface ); SystemLogger::dispatch( - ['response' => $payment, 'data' => $data], + ['response' => $molliePayment, 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_MOLLIE, @@ -215,19 +208,48 @@ class IDEAL implements MethodInterface, LivewireMethodInterface } /** - * Handle 'open' payment status for IDEAL. + * Handle 'open' payment status for iDEAL. * - * @param ResourcesPayment $payment + * @param \Mollie\Api\Resources\Payment $molliePayment The Mollie payment object * @return \Illuminate\Http\RedirectResponse + * @throws ApiException */ - public function processOpenPayment(\Mollie\Api\Resources\Payment $payment): RedirectResponse + public function processOpenPayment(\Mollie\Api\Resources\Payment $molliePayment): RedirectResponse { - return $this->processSuccessfulPayment($payment, 'open'); + return $this->processSuccessfulPayment($molliePayment, 'open'); } /** - * @inheritDoc + * Handle unsuccessful payment. + * + * @param \Exception $exception The exception that was thrown + * @throws PaymentFailed When the payment fails + * @return \Illuminate\Http\Response */ + public function processUnsuccessfulPayment(\Exception $exception): \Illuminate\Http\Response + { + $this->mollie->sendFailureMail($exception->getMessage()); + + SystemLogger::dispatch( + $exception->getMessage(), + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_FAILURE, + SystemLog::TYPE_MOLLIE, + $this->mollie->client, + $this->mollie->client->company, + ); + + $response = response([ + 'message' => $exception->getMessage(), + 'code' => $exception->getCode(), + ]); + + throw new PaymentFailed($exception->getMessage(), $exception->getCode()); + + return $response; + } + + /** @inheritDoc */ public function livewirePaymentView(array $data): string { // Doesn't support, it's offsite payment method. @@ -235,9 +257,7 @@ class IDEAL implements MethodInterface, LivewireMethodInterface return ''; } - /** - * @inheritDoc - */ + /** @inheritDoc */ public function paymentData(array $data): array { $this->paymentView($data); diff --git a/app/PaymentDrivers/Mollie/KBC.php b/app/PaymentDrivers/Mollie/KBC.php index ea251acb3d..694357674f 100644 --- a/app/PaymentDrivers/Mollie/KBC.php +++ b/app/PaymentDrivers/Mollie/KBC.php @@ -37,33 +37,21 @@ class KBC implements MethodInterface, LivewireMethodInterface $this->mollie->init(); } - /** - * Show the authorization page for KBC. - * - * @param array $data - * @return \Illuminate\View\View - */ + /** @inheritDoc */ public function authorizeView(array $data): View { return render('gateways.mollie.kbc.authorize', $data); } - /** - * Handle the authorization for KBC. - * - * @param Request $request - * @return \Illuminate\Http\RedirectResponse - */ + /** @inheritDoc */ public function authorizeResponse(Request $request): RedirectResponse { return redirect()->route('client.payment_methods.index'); } /** - * Show the payment page for KBC. - * - * @param array $data - * @return \Illuminate\Http\RedirectResponseor|RedirectResponse + * @throws \Exception + * @inheritDoc */ public function paymentView(array $data) { @@ -95,22 +83,87 @@ class KBC implements MethodInterface, LivewireMethodInterface $this->mollie->payment_hash->withData('payment_id', $payment->id); - return redirect( - $payment->getCheckoutUrl() - ); - } catch (\Mollie\Api\Exceptions\ApiException | \Exception $exception) { + return redirect($payment->getCheckoutUrl()); + } catch (\Exception $exception) { return $this->processUnsuccessfulPayment($exception); } } + /** + * @throws PaymentFailed When the payment fails + * @inheritDoc + */ + public function paymentResponse(PaymentResponseRequest $request): \Illuminate\Http\Response|RedirectResponse + { + if (! \property_exists($this->mollie->payment_hash->data, 'payment_id')) { + return $this->processUnsuccessfulPayment( + new PaymentFailed('Whoops, something went wrong. Missing required [payment_id] parameter. Please contact administrator. Reference hash: '.$this->mollie->payment_hash->hash) + ); + } + + try { + $molliePayment = $this->mollie->gateway->payments->get( + $this->mollie->payment_hash->data->payment_id + ); + + if ($molliePayment->status === 'paid') { + return $this->processSuccessfulPayment($molliePayment); + } + + if ($molliePayment->status === 'failed') { + return $this->processUnsuccessfulPayment( + new PaymentFailed(ctrans('texts.status_failed')) + ); + } + + return $this->processUnsuccessfulPayment( + new PaymentFailed(ctrans('texts.status_voided')) + ); + } catch (\Exception $exception) { + return $this->processUnsuccessfulPayment($exception); + } + } + + /** + * Handle the successful payment for KBC. + * + * @param \Mollie\Api\Resources\Payment $molliePayment The Mollie payment object + * @return \Illuminate\Http\RedirectResponse + */ + public function processSuccessfulPayment(\Mollie\Api\Resources\Payment $molliePayment): RedirectResponse + { + $data = [ + 'gateway_type_id' => GatewayType::KBC, + 'amount' => array_sum(array_column($this->mollie->payment_hash->invoices(), 'amount')) + $this->mollie->payment_hash->fee_total, + 'payment_type' => PaymentType::KBC, + 'transaction_reference' => $molliePayment->id, + ]; + + $payment_record = $this->mollie->createPayment( + $data, + $molliePayment->status === 'paid' ? Payment::STATUS_COMPLETED : Payment::STATUS_PENDING + ); + + SystemLogger::dispatch( + ['response' => $molliePayment, 'data' => $data], + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_SUCCESS, + SystemLog::TYPE_MOLLIE, + $this->mollie->client, + $this->mollie->client->company, + ); + + return redirect()->route('client.payments.show', ['payment' => $this->mollie->encodePrimaryKey($payment_record->id)]); + } + /** * Handle unsuccessful payment. * - * @param Exception $exception - * @throws PaymentFailed - * @return void + * @param \Exception $exception The exception that was thrown + * @throws PaymentFailed When the payment fails + * @return \Illuminate\Http\Response */ - public function processUnsuccessfulPayment(\Exception $exception): void + public function processUnsuccessfulPayment(\Exception $exception): \Illuminate\Http\Response { $this->mollie->sendFailureMail($exception->getMessage()); @@ -123,81 +176,17 @@ class KBC implements MethodInterface, LivewireMethodInterface $this->mollie->client->company, ); + $response = response([ + 'message' => $exception->getMessage(), + 'code' => $exception->getCode(), + ]); + throw new PaymentFailed($exception->getMessage(), $exception->getCode()); + + return $response; } - /** - * Handle the payments for the KBC. - * - * @param PaymentResponseRequest $request - * @return mixed - */ - public function paymentResponse(PaymentResponseRequest $request) - { - if (! \property_exists($this->mollie->payment_hash->data, 'payment_id')) { - return $this->processUnsuccessfulPayment( - new PaymentFailed('Whoops, something went wrong. Missing required [payment_id] parameter. Please contact administrator. Reference hash: '.$this->mollie->payment_hash->hash) - ); - } - - try { - $payment = $this->mollie->gateway->payments->get( - $this->mollie->payment_hash->data->payment_id - ); - - if ($payment->status === 'paid') { - return $this->processSuccessfulPayment($payment); - } - - if ($payment->status === 'failed') { - return $this->processUnsuccessfulPayment( - new PaymentFailed(ctrans('texts.status_failed')) - ); - } - - return $this->processUnsuccessfulPayment( - new PaymentFailed(ctrans('texts.status_voided')) - ); - } catch (\Mollie\Api\Exceptions\ApiException | \Exception $exception) { - return $this->processUnsuccessfulPayment($exception); - } - } - - /** - * Handle the successful payment for KBC. - * - * @param ResourcesPayment $payment - * @return \Illuminate\Http\RedirectResponse - */ - public function processSuccessfulPayment(\Mollie\Api\Resources\Payment $payment): RedirectResponse - { - $data = [ - 'gateway_type_id' => GatewayType::KBC, - 'amount' => array_sum(array_column($this->mollie->payment_hash->invoices(), 'amount')) + $this->mollie->payment_hash->fee_total, - 'payment_type' => PaymentType::KBC, - 'transaction_reference' => $payment->id, - ]; - - $payment_record = $this->mollie->createPayment( - $data, - $payment->status === 'paid' ? Payment::STATUS_COMPLETED : Payment::STATUS_PENDING - ); - - SystemLogger::dispatch( - ['response' => $payment, 'data' => $data], - SystemLog::CATEGORY_GATEWAY_RESPONSE, - SystemLog::EVENT_GATEWAY_SUCCESS, - SystemLog::TYPE_MOLLIE, - $this->mollie->client, - $this->mollie->client->company, - ); - - return redirect()->route('client.payments.show', ['payment' => $this->mollie->encodePrimaryKey($payment_record->id)]); - } - - /** - * @inheritDoc - */ + /** @inheritDoc */ public function livewirePaymentView(array $data): string { // Doesn't support, it's offsite payment method. @@ -205,9 +194,7 @@ class KBC implements MethodInterface, LivewireMethodInterface return ''; } - /** - * @inheritDoc - */ + /** @inheritDoc */ public function paymentData(array $data): array { $this->paymentView($data); diff --git a/app/PaymentDrivers/MolliePaymentDriver.php b/app/PaymentDrivers/MolliePaymentDriver.php index 035bc3c0aa..b7e0fafc07 100644 --- a/app/PaymentDrivers/MolliePaymentDriver.php +++ b/app/PaymentDrivers/MolliePaymentDriver.php @@ -30,6 +30,8 @@ use App\PaymentDrivers\Mollie\CreditCard; use App\PaymentDrivers\Mollie\IDEAL; use App\PaymentDrivers\Mollie\KBC; use App\Utils\Traits\MakesHash; +use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Validator; use Mollie\Api\Exceptions\ApiException; use Mollie\Api\MollieApiClient; @@ -76,6 +78,11 @@ class MolliePaymentDriver extends BaseDriver public const SYSTEM_LOG_TYPE = SystemLog::TYPE_MOLLIE; + /** + * Initialize the Mollie API client with the API key from the company gateway. + * + * @return self Returns the current instance for method chaining. + */ public function init(): self { $this->gateway = new MollieApiClient(); @@ -87,6 +94,11 @@ class MolliePaymentDriver extends BaseDriver return $this; } + /** + * Get the list of supported gateway types for Mollie. + * + * @return array Array of supported gateway type constants. + */ public function gatewayTypes(): array { $types = []; @@ -100,30 +112,65 @@ class MolliePaymentDriver extends BaseDriver return $types; } + /** + * Set the payment method for the current transaction. + * + * @param string $payment_method_id The payment method identifier + * @return self Returns the current instance for method chaining. + * @throws \Exception When the payment method is not supported + */ public function setPaymentMethod($payment_method_id) { - $class = self::$methods[$payment_method_id]; + if (!isset(self::$methods[$payment_method_id])) { + throw new \Exception("Payment method not supported: " . $payment_method_id); + } + $class = self::$methods[$payment_method_id]; $this->payment_method = new $class($this); return $this; } + /** + * Show the authorization page for the payment method. + * + * @param array $data Payment method data + * @return mixed The response from the payment method's authorizeView method + */ public function authorizeView(array $data) { return $this->payment_method->authorizeView($data); } + /** + * Handle the authorization response from the payment gateway. + * + * @param mixed $request The authorization request + * @return mixed The response from the payment method's authorizeResponse method + */ public function authorizeResponse($request) { return $this->payment_method->authorizeResponse($request); } + /** + * Show the payment page for the payment method. + * + * @param array $data Payment data + * @return mixed The response from the payment method's paymentView method + */ public function processPaymentView(array $data) { return $this->payment_method->paymentView($data); } + /** + * Handle the payment response from the payment gateway. + * + * @param mixed $request The payment response request + * @return mixed The response from the payment method's paymentResponse method + * @throws \Exception When the payment processing fails + */ public function processPaymentResponse($request) { return $this->payment_method->paymentResponse($request); @@ -134,33 +181,23 @@ class MolliePaymentDriver extends BaseDriver $this->init(); try { - $payment = $this->gateway->payments->get($payment->transaction_reference); + $molliePayment = $this->gateway->payments->get($payment->transaction_reference); - $refund = $this->gateway->payments->refund($payment, [ + $refund = $this->gateway->payments->refund($molliePayment, [ 'amount' => [ 'currency' => $this->client->currency()->code, 'value' => $this->convertToMollieAmount((float) $amount), ], ]); - if ($refund->status === 'refunded') { - SystemLogger::dispatch( - ['server_response' => $refund, 'data' => request()->all()], - SystemLog::CATEGORY_GATEWAY_RESPONSE, - SystemLog::EVENT_GATEWAY_SUCCESS, - SystemLog::TYPE_MOLLIE, - $this->client, - $this->client->company - ); - - return [ - 'transaction_reference' => $refund->id, - 'transaction_response' => json_encode($refund), - 'success' => $refund->status === 'refunded' ? true : false, //@phpstan-ignore-line - 'description' => $refund->description, - 'code' => 200, - ]; - } + SystemLogger::dispatch( + ['server_response' => $refund, 'data' => request()->all()], + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_SUCCESS, + SystemLog::TYPE_MOLLIE, + $this->client, + $this->client->company + ); return [ 'transaction_reference' => $refund->id, @@ -191,7 +228,15 @@ class MolliePaymentDriver extends BaseDriver } } - public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash) + /** + * Process a payment using a stored payment token. + * + * @param ClientGatewayToken $cgt The client gateway token containing payment method details + * @param PaymentHash $payment_hash The payment hash containing payment details + * @return Payment|null The created payment or null if payment fails + * @throws \Exception When there's an error processing the payment + */ + public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash): ?Payment { $amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total; $invoice = Invoice::query()->whereIn('id', $this->transformKeys(array_column($payment_hash->invoices(), 'invoice_id')))->withTrashed()->first(); @@ -209,7 +254,7 @@ class MolliePaymentDriver extends BaseDriver $this->init(); try { - $payment = $this->gateway->payments->create([ + $molliePayment = $this->gateway->payments->create([ 'amount' => [ 'currency' => $this->client->currency()->code, 'value' => $this->convertToMollieAmount($amount), @@ -218,56 +263,8 @@ class MolliePaymentDriver extends BaseDriver 'customerId' => $cgt->gateway_customer_reference, 'sequenceType' => 'recurring', 'description' => $description, - 'idempotencyKey' => uniqid("st", true), 'webhookUrl' => $this->company_gateway->webhookUrl(), ]); - - if ($payment->status === 'paid') { - - - $data = [ - 'payment_method' => $cgt->token, - 'payment_type' => PaymentType::CREDIT_CARD_OTHER, - 'amount' => $amount, - 'transaction_reference' => $payment->id, - 'gateway_type_id' => GatewayType::CREDIT_CARD, - ]; - - $this->confirmGatewayFee($data); - - $payment = $this->createPayment($data, Payment::STATUS_COMPLETED); - - SystemLogger::dispatch( - ['response' => $payment, 'data' => $data], - SystemLog::CATEGORY_GATEWAY_RESPONSE, - SystemLog::EVENT_GATEWAY_SUCCESS, - SystemLog::TYPE_MOLLIE, - $this->client, - $this->client->company - ); - - return $payment; - } - - $this->unWindGatewayFees($payment_hash); - - $this->sendFailureMail($payment->details); - - $message = [ - 'server_response' => $payment, - 'data' => $payment_hash->data, - ]; - - SystemLogger::dispatch( - $message, - SystemLog::CATEGORY_GATEWAY_RESPONSE, - SystemLog::EVENT_GATEWAY_FAILURE, - SystemLog::TYPE_CHECKOUT, - $this->client, - $this->client->company - ); - - return false; } catch (ApiException $e) { $this->unWindGatewayFees($payment_hash); @@ -280,113 +277,261 @@ class MolliePaymentDriver extends BaseDriver ]; SystemLogger::dispatch($data, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_MOLLIE, $this->client, $this->client->company); + return null; } + + try { + $data = [ + 'payment_method' => $cgt->token, + 'payment_type' => self::convertFromMolliePaymentType($molliePayment->method), + 'amount' => $amount, + 'transaction_reference' => $molliePayment->id, + 'gateway_type_id' => self::convertFromMollieGatewayType($molliePayment->method), + ]; + + $payment = $this->createPayment($data, self::convertFromMollieStatus($molliePayment->status)); + + SystemLogger::dispatch( + ['response' => $payment, 'data' => $data], + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_SUCCESS, + SystemLog::TYPE_MOLLIE, + $this->client, + $this->client->company + ); + return $payment; + } catch (\Exception $e) { + $this->unWindGatewayFees($payment_hash); + + $this->sendFailureMail($molliePayment->details); + + $message = [ + 'server_response' => $molliePayment, + 'data' => $payment_hash->data, + 'exception' => $e->getMessage() + ]; + + SystemLogger::dispatch( + $message, + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_FAILURE, + SystemLog::TYPE_CHECKOUT, + $this->client, + $this->client->company + ); + } + return null; } - public function processWebhookRequest(PaymentWebhookRequest $request) + /** + * Process an incoming webhook request from Mollie. + * + * @param PaymentWebhookRequest $request The webhook request + * @return JsonResponse JSON response indicating success or failure + * @throws \Exception When there's an error processing the webhook + */ + public function processWebhookRequest(PaymentWebhookRequest $request): JsonResponse { - // Allow app to catch up with webhook request. - // sleep(4); + // Sometimes the webhook is called before the Client is sent back to InvoiceNinja, + // since we first want the client to execute `$this->payment_method->paymentResponse()` + // before processing the webhook, we wait a bit here. usleep(rand(1500000, 4000000)); $validator = Validator::make($request->all(), [ - 'id' => ['required', 'starts_with:tr'], + 'id' => ['required', 'string', 'starts_with:tr', 'max:255'], ]); if ($validator->fails()) { return response()->json($validator->errors(), 422); } + nlog("Mollie webhook called with id: {$request->id}", $request->toArray()); + $this->init(); - - $codes = [ - 'open' => Payment::STATUS_PENDING, - 'canceled' => Payment::STATUS_CANCELLED, - 'pending' => Payment::STATUS_PENDING, - 'expired' => Payment::STATUS_CANCELLED, - 'failed' => Payment::STATUS_FAILED, - 'paid' => Payment::STATUS_COMPLETED, - ]; - - nlog($request->id); - try { - $payment = $this->gateway->payments->get($request->id); - $record = Payment::withTrashed()->where('transaction_reference', $request->id)->first(); + $molliePayment = $this->gateway->payments->get($request->id); + if (!$molliePayment) { + throw new \Exception('Mollie payment not found in the webhook request'); + } - if ($record) { - $client = $record->client; + $payment = Payment::withTrashed()->where('transaction_reference', $request->id)->first(); + + if (!$payment) { + // Sometimes the user is not returned to the site with a response from Mollie + // so we may not have a payment record. For these cases we need to re-construct the payment + // record from the metadata in the payment hash. + + if (!$molliePayment->metadata?->client_id) { + throw new \Exception('No client_id found in Mollie payment metadata'); + } + $client = Client::withTrashed()->find($this->decodePrimaryKey($molliePayment->metadata->client_id)); $this->client = $client; + + if (!$molliePayment->metadata?->hash) { + throw new \Exception('No payment hash found in Mollie payment metadata'); + } + $payment_hash = PaymentHash::where('hash', $molliePayment->metadata->hash)->firstOrFail(); + // If we are here, then we do not have access to the class payment hash, so lets set it here + $this->payment_hash = $payment_hash; + + $data = [ + 'gateway_type_id' => $molliePayment->metadata->gateway_type_id, + 'amount' => array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total, + 'payment_type' => $molliePayment->metadata->payment_type_id, + 'transaction_reference' => $molliePayment->id, + 'idempotency_key' => substr("{$molliePayment->id}{$payment_hash->hash}", 0, 64) + ]; + + // Uses $this->payment_hash + $this->confirmGatewayFee($data); + + // Uses $this->payment_hash + $payment = $this->createPayment( + $data, + self::convertFromMollieStatus($molliePayment->status) + ); } else { - $client = Client::withTrashed()->find($this->decodePrimaryKey($payment->metadata->client_id)); + $client = $payment->client; $this->client = $client; - // sometimes if the user is not returned to the site with a response from Mollie - // we may not have a payment record - in these cases we need to re-construct the payment - // record from the meta data in the payment hash. + } - if ($payment && property_exists($payment->metadata, 'hash') && $payment->metadata->hash) { - /* Harvest Payment Hash*/ - $payment_hash = PaymentHash::where('hash', $payment->metadata->hash)->first(); + $this->createClientGatewayTokenFromMolliePayment($molliePayment); - /* If we are here, then we do not have access to the class payment hash, so lets set it here*/ - $this->payment_hash = $payment_hash; + $status = self::convertFromMollieStatus($molliePayment->status); + if (in_array($status, [Payment::STATUS_CANCELLED, Payment::STATUS_FAILED])) { + if ($molliePayment->metadata?->hash) { + $payment_hash = PaymentHash::where('hash', $molliePayment->metadata->hash)->firstOrFail(); + $this->handlePendingGatewayFeeRemoval($payment_hash); + } - $data = [ - 'gateway_type_id' => $payment->metadata->gateway_type_id, - 'amount' => $amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total, - 'payment_type' => $payment->metadata->payment_type_id, - 'transaction_reference' => $payment->id, - 'idempotency_key' => substr("{$payment->id}{$payment_hash->hash}", 0, 64) - ]; + // Sets payment status to cancelled synchronously and handles other consequences + $payment->service()->deletePayment(false); + } + $payment->status_id = $status; // Set or overwrite payment status to the mollie status + $payment->date = $molliePayment->paidAt ?: null; - $this->confirmGatewayFee($data); + // Handle refunded amounts + if ($molliePayment->amountRefunded?->currency === $payment->currency->code) { + $payment->refunded = self::convertFromMollieAmount($molliePayment->amountRefunded->value); + } - $record = $this->createPayment( - $data, - $codes[$payment->status] - ); + // Handle remaining amount (applied amount) + if ($molliePayment->amountRemaining?->currency === $payment->currency->code) { + $payment->applied = self::convertFromMollieAmount($molliePayment->amountRemaining->value); + } else { + // If no remaining amount, use the full amount as applied for completed payments + if ($molliePayment->isPaid() || $molliePayment->isAuthorized()) { + $payment->applied = $payment->amount - ($payment->refunded ?? 0); } } - $message = [ - 'server_response' => $payment, - 'data' => $request->all(), - ]; - - $response = SystemLog::EVENT_GATEWAY_FAILURE; - - if ($record) { - if (in_array($payment->status, ['canceled', 'expired', 'failed'])) { - - if(property_exists($payment->metadata, 'hash') && $payment->metadata->hash){ - $payment_hash = PaymentHash::where('hash', $payment->metadata->hash)->first(); - $this->handlePendingGatewayFeeRemoval($payment_hash); - } - - $record->service()->deletePayment(false); - - } - - $record->status_id = $codes[$payment->status]; - $record->save(); - $response = SystemLog::EVENT_GATEWAY_SUCCESS; + // Add description to private notes if not already present + $private_notes = "description: " . $molliePayment->description; + if (!str_contains($payment->private_notes, $private_notes)) { + $payment->private_notes .= $private_notes; } - SystemLogger::dispatch( - $message, + $payment->save(); + + SystemLogger::dispatch([ + 'request' => $request->toArray(), + 'mollie_payment' => $molliePayment, + 'payment' => $payment + ], SystemLog::CATEGORY_GATEWAY_RESPONSE, - $response, + SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_MOLLIE, - $client, - $client->company + $this->client, + $this->company_gateway->company ); return response()->json([], 200); - } catch (ApiException $e) { - return response()->json(['message' => $e->getMessage(), 'gatewayStatusCode' => $e->getCode()], 500); + } catch (\Exception $e) { + $ctx = [ + 'request' => $request->toArray(), + 'exception' => $e + ]; + nlog("Mollie webhook call failed", $ctx); + SystemLogger::dispatch($ctx, + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_FAILURE, + SystemLog::TYPE_MOLLIE, + $this->client, + $this->company_gateway->company + ); } + return response()->json([], 500); } + /** + * Stores a Mollie mandateId as ClientGatewayToken based on the mollie customerId attached to a given Mollie Payment. + * @param \Mollie\Api\Resources\Payment $payment + * @return ClientGatewayToken|null Returns new ClientGatewayToken on success, null on failure or if token already exists + * @throws ApiException + */ + public function createClientGatewayTokenFromMolliePayment(\Mollie\Api\Resources\Payment $payment): ?ClientGatewayToken + { + if (!in_array($payment->status, ['paid'])) { + return null; + } + + if (!$payment->metadata?->hash) { + return null; + } + + $payment_hash = PaymentHash::where('hash', $payment->metadata->hash)->first(); + + if (!$payment_hash || !property_exists($payment_hash->data, 'shouldStoreToken') || !$payment_hash->data->shouldStoreToken) { + return null; + } + + $mandates = \iterator_to_array($this->gateway->mandates->listForId($payment_hash->data->mollieCustomerId)); + $mandate = !empty($mandates) ? $mandates[0] : null; + + if (!$mandate) { + return null; + } + + $token_already_exists = $this->client->gateway_tokens + ->where('token', $mandate->id) + ->where('company_gateway_id', $this->company_gateway->id) + ->first(); + + if ($token_already_exists) { + return null; + } + + $payment_method_id = self::convertFromMollieGatewayType($mandate->method); + $payment_meta = new \stdClass(); + $payment_meta->type = $payment_method_id; + + if ($payment_method_id == GatewayType::CREDIT_CARD) { + // Parse the card expiry date (format: YYYY-MM-DD) + $dateParts = explode('-', $mandate->details->cardExpiryDate); + if (count($dateParts) >= 2) { + $payment_meta->exp_year = substr($dateParts[0], -2); // Last 2 digits of YYYY + $payment_meta->exp_month = ltrim($dateParts[1], '0'); // MM (remove leading zero) + } + $payment_meta->brand = $mandate->details->cardLabel; + $payment_meta->last4 = $mandate->details->cardNumber; + } elseif ($payment_method_id == GatewayType::DIRECT_DEBIT) { + $payment_meta->last4 = substr($mandate->details->consumerAccount, -4); // Last 4 characters + $payment_meta->brand = "mollie"; + } + + return $this->storeGatewayToken([ + 'token' => $mandate->id, + 'payment_method_id' => $payment_method_id, + 'payment_meta' => $payment_meta, + ], ['gateway_customer_reference' => $payment_hash->data->mollieCustomerId]); + } + + /** + * Remove pending gateway fees from an invoice. + * + * @param PaymentHash $payment_hash The payment hash containing fee information + * @return void + */ private function handlePendingGatewayFeeRemoval(PaymentHash $payment_hash) { $invoice = $payment_hash->fee_invoice; @@ -402,27 +547,35 @@ class MolliePaymentDriver extends BaseDriver })->toArray(); $invoice->line_items = array_values($line_items); - $invoice = $invoice->calc()->getInvoice(); - } } + /** + * Process 3D Secure confirmation for a payment. + * + * @param Mollie3dsRequest $request The 3DS confirmation request + * @return mixed The result of the payment processing + */ public function process3dsConfirmation(Mollie3dsRequest $request) { $this->init(); - $this->setPaymentHash($request->getPaymentHash()); try { $payment = $this->gateway->payments->get($request->getPaymentId()); - return (new CreditCard($this))->processSuccessfulPayment($payment); } catch (\Mollie\Api\Exceptions\ApiException $e) { return (new CreditCard($this))->processUnsuccessfulPayment($e); } } + /** + * Detach a payment method by revoking the mandate from Mollie. + * + * @param ClientGatewayToken $token The client gateway token to detach + * @return void + */ public function detach(ClientGatewayToken $token) { $this->init(); @@ -455,18 +608,141 @@ class MolliePaymentDriver extends BaseDriver return \number_format((float) $amount, 2, '.', ''); } + /** + * Convert a Mollie amount string back to a float. + * + * @param string $amount The amount string from Mollie (e.g., "123.45") + * @return float The converted amount as a float + * @throws \InvalidArgumentException If the input is not a valid numeric string + */ + public static function convertFromMollieAmount(string $amount): float + { + if (!is_numeric($amount)) { + throw new \InvalidArgumentException("Invalid amount format. Expected a numeric string, got: " . $amount); + } + + return (float) $amount; + } + + /** + * Convert ISO 4217 currency code to InvoiceNinja currency ID + * + * @param string $currencyCode ISO 4217 currency code (e.g., 'EUR', 'USD') + * @return int Returns the currency ID + * @throws ModelNotFoundException When the currency is not found + */ + public static function convertFromMollieCurrency(string $currencyCode): int + { + $currency = \App\Models\Currency::where('code', strtoupper($currencyCode))->firstOrFail(); + return $currency->id; + } + + /** + * Convert Mollie payment method to PaymentType ID + * + * @param string $type + * @return int + * @throws \Exception When the payment method is not supported + */ + static public function convertFromMolliePaymentType(string $type): int + { + $types = [ + 'banktransfer' => PaymentType::BANK_TRANSFER, + 'creditcard' => PaymentType::CREDIT_CARD_OTHER, + 'directdebit' => PaymentType::DIRECT_DEBIT, + 'ideal' => PaymentType::IDEAL, + 'bancontact' => PaymentType::BANCONTACT, + 'sofort' => PaymentType::SOFORT, + 'klarnapaylater' => PaymentType::KLARNA, + 'klarnasliceit' => PaymentType::KLARNA, + 'klarnapaynow' => PaymentType::KLARNA, + 'kbc' => PaymentType::KBC, + 'eps' => PaymentType::EPS, + 'giropay' => PaymentType::GIROPAY, + 'p24' => PaymentType::PRZELEWY24, + 'applepay' => PaymentType::CREDIT_CARD_OTHER, + 'paypal' => PaymentType::PAYPAL, + 'belfius' => PaymentType::BANK_TRANSFER, + 'inghomepay' => PaymentType::BANK_TRANSFER, + 'giftcard' => PaymentType::CREDIT, + 'paysafecard' => PaymentType::CREDIT, + 'przelewy24' => PaymentType::PRZELEWY24, + 'mybank' => PaymentType::BANK_TRANSFER, + 'billet' => PaymentType::BANK_TRANSFER, + 'tikkiepayment' => PaymentType::BANK_TRANSFER, + ]; + + if (!array_key_exists($type, $types)) { + throw new \Exception("Unsupported Mollie payment method: " . $type); + } + + return $types[$type]; + } + + /** + * Convert Mollie payment method to GatewayType ID + * + * @param string $type + * @return int + * @throws \Exception When the payment method is not supported + */ + static public function convertFromMollieGatewayType(string $type): int + { + $types = [ + 'creditcard' => GatewayType::CREDIT_CARD, + 'directdebit' => GatewayType::DIRECT_DEBIT, + 'paypal' => GatewayType::PAYPAL, + 'bancontact' => GatewayType::BANCONTACT, + 'banktransfer' => GatewayType::BANK_TRANSFER, + 'kbc' => GatewayType::KBC, + 'ideal' => GatewayType::IDEAL, + ]; + + if (!isset($types[strtolower($type)])) { + throw new \Exception("Unsupported Mollie payment method: " . $type); + } + + return $types[strtolower($type)]; + } + + /** + * Convert Mollie payment status to InvoiceNinja payment status + * + * @param string $mollieStatus + * @return int + */ + private static function convertFromMollieStatus(string $mollieStatus): int + { + $statusMap = [ + 'paid' => Payment::STATUS_COMPLETED, + 'authorized' => Payment::STATUS_PENDING, + 'pending' => Payment::STATUS_PENDING, + 'failed' => Payment::STATUS_FAILED, + 'expired' => Payment::STATUS_FAILED, + 'canceled' => Payment::STATUS_CANCELLED, + 'refunded' => Payment::STATUS_REFUNDED, + 'partially_refunded' => Payment::STATUS_PARTIALLY_REFUNDED, + ]; + + return $statusMap[strtolower($mollieStatus)] ?? Payment::STATUS_FAILED; + } + + /** + * Test the connection to the Mollie API. + * + * @return string 'ok' if the connection is successful, 'error' otherwise + */ public function auth(): string { $this->init(); try { + // Attempt to fetch a page of payments to test the connection $p = $this->gateway->payments->page(); return 'ok'; } catch (\Exception $e) { - + // Log the error or handle it as needed + return 'error'; } - - return 'error'; - } }