Skip to content

Commit 7bb4201

Browse files
author
root
committed
feat: add extensibility points (Hooks, Validators, Fee Calculator Interface)
BREAKING CHANGES: - FeeCalculator now implements FeeCalculatorInterface - CheckoutService constructor now requires HookManager and ValidationManager New Features: - Hook system with 13 hook points for payment flow customization - Validation system with 9 validation contexts - FeeCalculatorInterface for custom fee logic - Built-in validators: CardDataValidator, PaymentAmountValidator Hook Points: - BEFORE_PAYMENT, AFTER_PAYMENT - BEFORE_GATEWAY, AFTER_GATEWAY, ON_GATEWAY_SWITCH - BEFORE_FEE_CALCULATION, AFTER_FEE_CALCULATION - BEFORE_VALIDATION, AFTER_VALIDATION - ON_PAYMENT_SUCCESS, ON_PAYMENT_FAILURE - BEFORE_WEBHOOK, AFTER_WEBHOOK Validation Contexts: - PAYMENT_REQUEST, CARD_DATA, CUSTOMER_DATA - PIX_DATA, BOLETO_DATA, WEBHOOK_PAYLOAD - REFUND_REQUEST, AMOUNT_VALIDATION, INSTALLMENTS Configuration: - Added validation.enabled config - Added hooks.enabled config - Added min/max amount validation configs Version: 1.0.17 -> 1.1.0
1 parent 4acef86 commit 7bb4201

18 files changed

Lines changed: 824 additions & 7 deletions

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "kaninstein/multi-acquirer-checkout",
3-
"version": "1.0.17",
3+
"version": "1.1.0",
44
"description": "Multi-acquirer checkout package with DDD architecture for Laravel",
55
"keywords": [
66
"laravel",

config/multi-acquirer.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,31 @@
179179
'dispatch_domain_events' => true,
180180
'async' => true, // Queue events for async processing
181181
],
182+
183+
/*
184+
|--------------------------------------------------------------------------
185+
| Validation Configuration
186+
|--------------------------------------------------------------------------
187+
|
188+
| Enable/disable validation and configure validation rules.
189+
|
190+
*/
191+
'validation' => [
192+
'enabled' => env('MULTI_ACQUIRER_VALIDATION_ENABLED', true),
193+
'min_amount_cents' => env('MULTI_ACQUIRER_MIN_AMOUNT_CENTS', 100), // R$ 1.00
194+
'max_amount_cents' => env('MULTI_ACQUIRER_MAX_AMOUNT_CENTS', 10000000), // R$ 100,000.00
195+
],
196+
197+
/*
198+
|--------------------------------------------------------------------------
199+
| Hooks Configuration
200+
|--------------------------------------------------------------------------
201+
|
202+
| Configure hooks/callbacks for payment flow customization.
203+
| Hooks are registered via HookManager in your application's ServiceProvider.
204+
|
205+
*/
206+
'hooks' => [
207+
'enabled' => env('MULTI_ACQUIRER_HOOKS_ENABLED', true),
208+
],
182209
];

src/Application/Services/CheckoutService.php

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,48 @@
55
use Illuminate\Contracts\Events\Dispatcher;
66
use Kaninstein\MultiAcquirerCheckout\Application\DTOs\CheckoutResult;
77
use Kaninstein\MultiAcquirerCheckout\Application\DTOs\PaymentRequest;
8-
use Kaninstein\MultiAcquirerCheckout\Domain\Fee\Services\FeeCalculator;
8+
use Kaninstein\MultiAcquirerCheckout\Domain\Fee\Contracts\FeeCalculatorInterface;
99
use Kaninstein\MultiAcquirerCheckout\Domain\Payment\Entities\Payment;
10+
use Kaninstein\MultiAcquirerCheckout\Domain\Validation\ValidationContexts;
11+
use Kaninstein\MultiAcquirerCheckout\Domain\Validation\ValidationManager;
1012
use Kaninstein\MultiAcquirerCheckout\Infrastructure\Repositories\Contracts\PaymentRepositoryInterface;
13+
use Kaninstein\MultiAcquirerCheckout\Support\Hooks\HookManager;
14+
use Kaninstein\MultiAcquirerCheckout\Support\Hooks\HookPoints;
1115
use Kaninstein\MultiAcquirerCheckout\Support\Pipelines\GatewayPipeline;
1216

1317
class CheckoutService
1418
{
1519
public function __construct(
1620
private readonly GatewayPipeline $pipeline,
17-
private readonly FeeCalculator $feeCalculator,
21+
private readonly FeeCalculatorInterface $feeCalculator,
1822
private readonly PaymentRepositoryInterface $payments,
1923
private readonly Dispatcher $events,
24+
private readonly HookManager $hooks,
25+
private readonly ValidationManager $validator,
2026
) {}
2127

2228
public function process(PaymentRequest $request): CheckoutResult
2329
{
30+
// Hook: Before payment
31+
$request = $this->hooks->execute(HookPoints::BEFORE_PAYMENT, $request);
32+
33+
// Validation: Payment request
34+
if (config('multi-acquirer.validation.enabled', true)) {
35+
$validationResult = $this->validator->validate(ValidationContexts::PAYMENT_REQUEST, $request);
36+
37+
if (!$validationResult->isValid) {
38+
throw new \InvalidArgumentException(
39+
'Payment validation failed: ' . implode(', ', $validationResult->getAllErrors())
40+
);
41+
}
42+
}
43+
44+
// Hook: Before validation
45+
$request = $this->hooks->execute(HookPoints::BEFORE_VALIDATION, $request);
46+
47+
// Hook: After validation
48+
$request = $this->hooks->execute(HookPoints::AFTER_VALIDATION, $request);
49+
2450
$payment = Payment::create(
2551
amount: $request->amount,
2652
method: $request->paymentMethod,
@@ -30,19 +56,32 @@ public function process(PaymentRequest $request): CheckoutResult
3056

3157
$platformRate = $request->platformFeeRate ?? (float) config('multi-acquirer.fees.platform.default_rate', 0.06);
3258

59+
// Hook: Before fee calculation
60+
$feeContext = ['request' => $request, 'platformRate' => $platformRate];
61+
$feeContext = $this->hooks->execute(HookPoints::BEFORE_FEE_CALCULATION, $feeContext);
62+
3363
// Fee calculation is based on the product/base price.
3464
$fees = $this->feeCalculator->calculate(
3565
productPriceCents: $request->amount->amountInCents,
3666
installments: $request->installments,
37-
platformFeeRate: $platformRate,
67+
platformFeeRate: $feeContext['platformRate'] ?? $platformRate,
3868
merchantAbsorbsFinancing: $request->merchantAbsorbsFinancing,
3969
paymentMethod: $request->paymentMethod->value,
4070
gatewayName: $request->preferredGateway !== '' ? $request->preferredGateway : 'pagarme',
4171
feeResponsibility: $request->feeResponsibility,
4272
);
4373

74+
// Hook: After fee calculation
75+
$fees = $this->hooks->execute(HookPoints::AFTER_FEE_CALCULATION, $fees);
76+
77+
// Hook: Before gateway processing
78+
$request = $this->hooks->execute(HookPoints::BEFORE_GATEWAY, $request);
79+
4480
$gatewayResponse = $this->pipeline->process($request);
4581

82+
// Hook: After gateway processing
83+
$gatewayResponse = $this->hooks->execute(HookPoints::AFTER_GATEWAY, $gatewayResponse);
84+
4685
if ($gatewayResponse->isSuccessful()) {
4786
if ($gatewayResponse->id) {
4887
$payment->authorize($gatewayResponse->id);
@@ -51,8 +90,16 @@ public function process(PaymentRequest $request): CheckoutResult
5190
if ($gatewayResponse->status === 'paid') {
5291
$payment->markPaid();
5392
}
93+
94+
// Hook: Payment success
95+
$successContext = ['payment' => $payment, 'response' => $gatewayResponse];
96+
$this->hooks->execute(HookPoints::ON_PAYMENT_SUCCESS, $successContext);
5497
} else {
5598
$payment->fail($gatewayResponse->errorMessage ?? 'Unknown error');
99+
100+
// Hook: Payment failure
101+
$failureContext = ['payment' => $payment, 'response' => $gatewayResponse];
102+
$this->hooks->execute(HookPoints::ON_PAYMENT_FAILURE, $failureContext);
56103
}
57104

58105
$this->payments->save($payment);
@@ -63,11 +110,16 @@ public function process(PaymentRequest $request): CheckoutResult
63110
}
64111
}
65112

66-
return new CheckoutResult(
113+
// Hook: After payment
114+
$result = new CheckoutResult(
67115
success: $gatewayResponse->isSuccessful(),
68116
payment: $payment,
69117
fees: $fees,
70118
gatewayResponse: $gatewayResponse,
71119
);
120+
121+
$result = $this->hooks->execute(HookPoints::AFTER_PAYMENT, $result);
122+
123+
return $result;
72124
}
73125
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
namespace Kaninstein\MultiAcquirerCheckout\Domain\Fee\Contracts;
4+
5+
use Kaninstein\MultiAcquirerCheckout\Domain\Fee\ValueObjects\FeeBreakdown;
6+
7+
interface FeeCalculatorInterface
8+
{
9+
/**
10+
* Calculate fees for a payment
11+
*
12+
* @param int $productPriceCents Base product price in cents
13+
* @param int $installments Number of installments
14+
* @param float $platformFeeRate Platform fee rate (e.g., 0.06 for 6%)
15+
* @param bool $merchantAbsorbsFinancing Whether merchant absorbs financing costs
16+
* @param string $paymentMethod Payment method (card, pix, boleto)
17+
* @param string $gatewayName Gateway name (pagarme, stripe, etc.)
18+
* @param string $feeResponsibility Who pays fees: merchant, customer, or split
19+
* @return FeeBreakdown
20+
*/
21+
public function calculate(
22+
int $productPriceCents,
23+
int $installments,
24+
float $platformFeeRate,
25+
bool $merchantAbsorbsFinancing,
26+
string $paymentMethod,
27+
string $gatewayName,
28+
string $feeResponsibility = 'merchant'
29+
): FeeBreakdown;
30+
31+
/**
32+
* Get gateway fee rate for a specific configuration
33+
*
34+
* @param string $gatewayName
35+
* @param string $paymentMethod
36+
* @param int $installments
37+
* @return float Fee rate (e.g., 0.0399 for 3.99%)
38+
*/
39+
public function getGatewayFeeRate(
40+
string $gatewayName,
41+
string $paymentMethod,
42+
int $installments
43+
): float;
44+
45+
/**
46+
* Calculate financing fee for installments
47+
*
48+
* @param int $amountCents
49+
* @param int $installments
50+
* @param float $monthlyRate Monthly interest rate
51+
* @return int Financing fee in cents
52+
*/
53+
public function calculateFinancingFee(
54+
int $amountCents,
55+
int $installments,
56+
float $monthlyRate
57+
): int;
58+
}

src/Domain/Fee/Services/FeeCalculator.php

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
namespace Kaninstein\MultiAcquirerCheckout\Domain\Fee\Services;
44

55
use Kaninstein\MultiAcquirerCheckout\Application\DTOs\FeeCalculationResult;
6+
use Kaninstein\MultiAcquirerCheckout\Domain\Fee\Contracts\FeeCalculatorInterface;
7+
use Kaninstein\MultiAcquirerCheckout\Domain\Fee\ValueObjects\FeeBreakdown;
68
use Kaninstein\MultiAcquirerCheckout\Infrastructure\Repositories\Contracts\FeeConfigRepositoryInterface;
79

8-
class FeeCalculator
10+
class FeeCalculator implements FeeCalculatorInterface
911
{
1012
public function __construct(
1113
private readonly FeeConfigRepositoryInterface $feeConfigs,
@@ -19,7 +21,7 @@ public function calculate(
1921
string $paymentMethod = 'card',
2022
string $gatewayName = 'pagarme',
2123
string $feeResponsibility = 'buyer',
22-
): FeeCalculationResult {
24+
): FeeBreakdown {
2325
$installments = max(1, min(12, $installments));
2426

2527
$platformFeeCents = $this->calculatePlatformFee($productPriceCents, $platformFeeRate);
@@ -143,5 +145,28 @@ private function getGatewayConfig(string $gatewayName, string $paymentMethod, in
143145
// card
144146
return ['percentage' => 0.0559, 'fixed_cents' => 99];
145147
}
148+
149+
public function getGatewayFeeRate(
150+
string $gatewayName,
151+
string $paymentMethod,
152+
int $installments
153+
): float {
154+
$config = $this->getGatewayConfig($gatewayName, $paymentMethod, $installments);
155+
return (float) $config['percentage'];
156+
}
157+
158+
public function calculateFinancingFee(
159+
int $amountCents,
160+
int $installments,
161+
float $monthlyRate
162+
): int {
163+
if ($installments <= 1) {
164+
return 0;
165+
}
166+
167+
// Simple interest calculation for financing
168+
$months = $installments - 1;
169+
return (int) bcmul((string) $amountCents, (string) ($monthlyRate * $months), 0);
170+
}
146171
}
147172

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Kaninstein\MultiAcquirerCheckout\Domain\Fee\ValueObjects;
4+
5+
use Kaninstein\MultiAcquirerCheckout\Application\DTOs\FeeCalculationResult;
6+
7+
/**
8+
* Type alias for FeeCalculationResult to maintain interface contract
9+
*/
10+
class FeeBreakdown extends FeeCalculationResult
11+
{
12+
// Inherits all properties and methods from FeeCalculationResult
13+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Kaninstein\MultiAcquirerCheckout\Domain\Validation;
4+
5+
use Kaninstein\MultiAcquirerCheckout\Domain\Validation\Contracts\ValidatorInterface;
6+
7+
abstract class AbstractValidator implements ValidatorInterface
8+
{
9+
public function stopOnFailure(): bool
10+
{
11+
return false;
12+
}
13+
14+
public function getName(): string
15+
{
16+
return static::class;
17+
}
18+
19+
/**
20+
* Helper to create success result
21+
*/
22+
protected function success(array $metadata = []): ValidationResult
23+
{
24+
return ValidationResult::success($metadata);
25+
}
26+
27+
/**
28+
* Helper to create failure result
29+
*/
30+
protected function failure(array $errors, array $metadata = []): ValidationResult
31+
{
32+
return ValidationResult::failure($errors, $metadata);
33+
}
34+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Kaninstein\MultiAcquirerCheckout\Domain\Validation\Contracts;
4+
5+
use Kaninstein\MultiAcquirerCheckout\Domain\Validation\ValidationResult;
6+
7+
interface ValidatorInterface
8+
{
9+
/**
10+
* Validate the given data
11+
*
12+
* @param mixed $data
13+
* @return ValidationResult
14+
*/
15+
public function validate(mixed $data): ValidationResult;
16+
17+
/**
18+
* Get validator name/identifier
19+
*/
20+
public function getName(): string;
21+
22+
/**
23+
* Whether this validator should stop validation chain on failure
24+
*/
25+
public function stopOnFailure(): bool;
26+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace Kaninstein\MultiAcquirerCheckout\Domain\Validation;
4+
5+
/**
6+
* Available validation contexts
7+
*/
8+
final class ValidationContexts
9+
{
10+
/** Validate payment request before processing */
11+
public const PAYMENT_REQUEST = 'payment.request';
12+
13+
/** Validate card data */
14+
public const CARD_DATA = 'card.data';
15+
16+
/** Validate customer data */
17+
public const CUSTOMER_DATA = 'customer.data';
18+
19+
/** Validate PIX payment data */
20+
public const PIX_DATA = 'pix.data';
21+
22+
/** Validate boleto payment data */
23+
public const BOLETO_DATA = 'boleto.data';
24+
25+
/** Validate webhook payload */
26+
public const WEBHOOK_PAYLOAD = 'webhook.payload';
27+
28+
/** Validate refund request */
29+
public const REFUND_REQUEST = 'refund.request';
30+
31+
/** Validate amount/monetary values */
32+
public const AMOUNT_VALIDATION = 'amount.validation';
33+
34+
/** Validate installments */
35+
public const INSTALLMENTS = 'installments';
36+
37+
/** Custom validation context */
38+
public const CUSTOM = 'custom';
39+
}

0 commit comments

Comments
 (0)