This commit is contained in:
TheNewSound 2025-11-29 04:27:54 +01:00 committed by GitHub
commit 2ee7e5321d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 799 additions and 555 deletions

View File

@ -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],

View File

@ -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<int, \App\Models\PaymentType> $payment_methods
* @mixin \Eloquent
*/
class GatewayType extends StaticModel

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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';
}
}