'APPID123', 'merchant_code' => 'MERCH001', 'merchant_key' => 'test-merchant-key', 'base_url' => 'https://kbz.test/gateway', 'notify_url' => 'https://app.test/api/v1/webhooks/kbz', ]; test('refund posts a correctly signed refund request, wiring the partial amount through', function () use ($config) { Http::fake(['kbz.test/*' => Http::response(['Response' => ['result' => 'SUCCESS', 'refund_order_id' => 'REFUND123']])]); (new KbzMiniAppGateway($config))->refund('EVB-FIXTURE-001', '8000', 'customer requested partial refund'); Http::assertSent(function ($request) { $body = $request->data()['Request']; return $request->url() === 'https://kbz.test/gateway' && $body['method'] === 'kbz.payment.refund' && $body['sign_type'] === 'SHA256' && $body['biz_content']['appid'] === 'APPID123' && $body['biz_content']['merch_code'] === 'MERCH001' && $body['biz_content']['merch_order_id'] === 'EVB-FIXTURE-001' // unlike bnf_event (amount hardcoded/commented out), this is wired through && $body['biz_content']['refund_amount'] === '8000' && $body['biz_content']['refund_reason'] === 'customer requested partial refund' && ! empty($body['biz_content']['refund_request_no']) && $body['sign'] === KbzSignature::sign($body, 'test-merchant-key'); }); }); test('refund returns a completed RefundResultData on success', function () use ($config) { Http::fake(['kbz.test/*' => Http::response(['Response' => ['result' => 'SUCCESS', 'refund_order_id' => 'REFUND123']])]); $result = (new KbzMiniAppGateway($config))->refund('EVB-FIXTURE-001', '8000', 'customer request'); expect($result->status)->toBe(RefundStatus::Completed) ->and($result->gatewayRefundId)->toBe('REFUND123') ->and($result->gatewayPayload)->toBe(['result' => 'SUCCESS', 'refund_order_id' => 'REFUND123']); }); test('refund returns a failed RefundResultData when KBZ rejects the request', function () use ($config) { Http::fake(['kbz.test/*' => Http::response(['Response' => ['result' => 'FAIL', 'msg' => 'Refund window expired']])]); $result = (new KbzMiniAppGateway($config))->refund('EVB-FIXTURE-001', '8000', 'customer request'); expect($result->status)->toBe(RefundStatus::Failed) ->and($result->gatewayRefundId)->toBeNull() ->and($result->message)->toBe('Refund window expired'); }); test('refund returns a failed RefundResultData when the connection fails', function () use ($config) { Http::fake(['kbz.test/*' => fn () => throw new ConnectionException('Connection refused')]); $result = (new KbzMiniAppGateway($config))->refund('EVB-FIXTURE-001', '8000', 'customer request'); expect($result->status)->toBe(RefundStatus::Failed) ->and($result->gatewayRefundId)->toBeNull() ->and($result->gatewayPayload)->toBe([]) ->and($result->message)->toBe('Connection refused'); }); test('refund builds mTLS cert/ssl_key/verify options from config', function () { $gateway = new KbzMiniAppGateway([ 'app_id' => 'APPID123', 'merchant_code' => 'MERCH001', 'merchant_key' => 'test-merchant-key', 'base_url' => 'https://kbz.test/gateway', 'cert_path' => '/certs/merch.pem', 'cert_key_path' => '/certs/merch.key', 'ca_path' => '/certs/ca.crt', 'cert_password' => 'secret', ]); $options = (new ReflectionMethod($gateway, 'mtlsOptions'))->invoke($gateway); expect($options)->toBe([ 'cert' => ['/certs/merch.pem', 'secret'], 'ssl_key' => ['/certs/merch.key', 'secret'], 'verify' => '/certs/ca.crt', ]); }); test('refund omits mTLS options entirely when cert config is not set', function () use ($config) { $gateway = new KbzMiniAppGateway($config); $options = (new ReflectionMethod($gateway, 'mtlsOptions'))->invoke($gateway); expect($options)->toBe([]); });