Add Payment module: contracts, payments/refunds tables, KBZ gateway, factory, orchestrator (T5.1-T5.7)
- T5.1 PaymentGatewayInterface, DTOs, PaymentMethod/PaymentStatus/RefundStatus enums - T5.2 payments/refunds tables, models, factories - T5.3-T5.5 KbzMiniAppGateway: initiate()/verify()/refund(), ported KBZ signing scheme, wired refund_amount through for partial refunds, mTLS options for refund - T5.6 PaymentGatewayFactory resolving gateways by PaymentMethod - T5.7 PaymentService orchestrator delegating to the resolved gateway
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
use Modules\Payment\Support\KbzSignature;
|
||||
|
||||
/**
|
||||
* Fixture derived by running bnf_event's own `KBZPay::joinKeyVal`/`signature`
|
||||
* (App\Strategies\Payments\KBZPay) against this exact params array and key —
|
||||
* this pins the port to the old code's actual behavior (domain.md §6).
|
||||
*/
|
||||
$fixtureParams = [
|
||||
'timestamp' => '1700000000',
|
||||
'method' => 'kbz.payment.precreate',
|
||||
'notify_url' => 'https://example.com/api/v1/webhooks/kbz',
|
||||
'nonce_str' => 'fixture-nonce',
|
||||
'version' => '1.0',
|
||||
'biz_content' => [
|
||||
'appid' => 'APPID123',
|
||||
'merch_code' => 'MERCH001',
|
||||
'merch_order_id' => 'EVB-FIXTURE-001',
|
||||
'trade_type' => 'MINIAPP',
|
||||
'total_amount' => '15000',
|
||||
'trans_currency' => 'MMK',
|
||||
'callback_info' => 'urlencode',
|
||||
],
|
||||
];
|
||||
$fixtureKey = 'test-merchant-key';
|
||||
|
||||
test('joinKeyVal flattens nested biz_content and sorts keys, matching the old algorithm', function () use ($fixtureParams) {
|
||||
expect(KbzSignature::joinKeyVal($fixtureParams))->toBe(
|
||||
'appid=APPID123&callback_info=urlencode&merch_code=MERCH001&merch_order_id=EVB-FIXTURE-001'
|
||||
.'&method=kbz.payment.precreate&nonce_str=fixture-nonce¬ify_url=https://example.com/api/v1/webhooks/kbz'
|
||||
.'×tamp=1700000000&total_amount=15000&trade_type=MINIAPP&trans_currency=MMK&version=1.0'
|
||||
);
|
||||
});
|
||||
|
||||
test('sign matches the known fixture hash produced by the old KBZPay::signature', function () use ($fixtureParams, $fixtureKey) {
|
||||
expect(KbzSignature::sign($fixtureParams, $fixtureKey))
|
||||
->toBe('29F95FB3DCCEC866A68A07E9A1B25BEEE9F355529B5D37395A04B2282FC48BB1');
|
||||
});
|
||||
|
||||
test('sign is uppercase SHA-256 and stable for the same input', function () use ($fixtureParams, $fixtureKey) {
|
||||
$signature = KbzSignature::sign($fixtureParams, $fixtureKey);
|
||||
|
||||
expect($signature)->toBe(strtoupper($signature))
|
||||
->and($signature)->toHaveLength(64)
|
||||
->and(KbzSignature::sign($fixtureParams, $fixtureKey))->toBe($signature);
|
||||
});
|
||||
|
||||
test('sign and joinKeyVal ignore any pre-existing sign/sign_type values', function () use ($fixtureParams, $fixtureKey) {
|
||||
$withStaleSign = [...$fixtureParams, 'sign' => 'stale', 'sign_type' => 'SHA256'];
|
||||
|
||||
expect(KbzSignature::sign($withStaleSign, $fixtureKey))
|
||||
->toBe(KbzSignature::sign($fixtureParams, $fixtureKey));
|
||||
});
|
||||
|
||||
test('joinKeyVal drops null and empty-string values', function () {
|
||||
$params = ['a' => 'x', 'b' => null, 'c' => '', 'd' => ' '];
|
||||
|
||||
expect(KbzSignature::joinKeyVal($params))->toBe('a=x');
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
use Modules\Payment\Contracts\PaymentGatewayInterface;
|
||||
use Modules\Payment\Data\PaymentRequestData;
|
||||
use Modules\Payment\Data\PaymentResultData;
|
||||
use Modules\Payment\Data\RefundResultData;
|
||||
use Modules\Payment\Enums\PaymentMethod;
|
||||
use Modules\Payment\Enums\PaymentStatus;
|
||||
use Modules\Payment\Enums\RefundStatus;
|
||||
use Modules\Payment\Factories\PaymentGatewayFactory;
|
||||
use Modules\Payment\Services\PaymentService;
|
||||
|
||||
test('initiate resolves the gateway for the request method and delegates to its initiate()', function () {
|
||||
$gateway = Mockery::mock(PaymentGatewayInterface::class);
|
||||
$data = new PaymentRequestData(
|
||||
bookingId: 1,
|
||||
merchantOrderId: 'EVB-001',
|
||||
amount: '15000',
|
||||
currency: 'MMK',
|
||||
method: PaymentMethod::KbzMiniApp,
|
||||
);
|
||||
$expected = new PaymentResultData(status: PaymentStatus::Pending, gatewayTransactionId: 'EVB-001', gatewayPayload: []);
|
||||
|
||||
$gateway->shouldReceive('initiate')->once()->with($data)->andReturn($expected);
|
||||
|
||||
$factory = Mockery::mock(PaymentGatewayFactory::class);
|
||||
$factory->shouldReceive('make')->once()->with(PaymentMethod::KbzMiniApp)->andReturn($gateway);
|
||||
|
||||
$result = (new PaymentService($factory))->initiate($data);
|
||||
|
||||
expect($result)->toBe($expected);
|
||||
});
|
||||
|
||||
test('verify resolves the gateway for the given method and delegates to its verify()', function () {
|
||||
$gateway = Mockery::mock(PaymentGatewayInterface::class);
|
||||
$expected = new PaymentResultData(status: PaymentStatus::Completed, gatewayTransactionId: 'EVB-001', gatewayPayload: []);
|
||||
|
||||
$gateway->shouldReceive('verify')->once()->with('EVB-001')->andReturn($expected);
|
||||
|
||||
$factory = Mockery::mock(PaymentGatewayFactory::class);
|
||||
$factory->shouldReceive('make')->once()->with(PaymentMethod::KbzMiniApp)->andReturn($gateway);
|
||||
|
||||
$result = (new PaymentService($factory))->verify(PaymentMethod::KbzMiniApp, 'EVB-001');
|
||||
|
||||
expect($result)->toBe($expected);
|
||||
});
|
||||
|
||||
test('refund resolves the gateway for the given method and delegates to its refund()', function () {
|
||||
$gateway = Mockery::mock(PaymentGatewayInterface::class);
|
||||
$expected = new RefundResultData(status: RefundStatus::Completed, gatewayRefundId: 'REFUND-1', gatewayPayload: []);
|
||||
|
||||
$gateway->shouldReceive('refund')->once()->with('EVB-001', '8000', 'customer request')->andReturn($expected);
|
||||
|
||||
$factory = Mockery::mock(PaymentGatewayFactory::class);
|
||||
$factory->shouldReceive('make')->once()->with(PaymentMethod::KbzMiniApp)->andReturn($gateway);
|
||||
|
||||
$result = (new PaymentService($factory))->refund(PaymentMethod::KbzMiniApp, 'EVB-001', '8000', 'customer request');
|
||||
|
||||
expect($result)->toBe($expected);
|
||||
});
|
||||
Reference in New Issue
Block a user