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:
Nyan Lin Paing
2026-08-08 22:42:16 +07:00
parent e0bcc5f81a
commit 4737838021
28 changed files with 1407 additions and 1 deletions
@@ -0,0 +1,72 @@
<?php
namespace Modules\Payment\Support;
/**
* KBZ's signing scheme, ported verbatim from bnf_event's `KBZPay::joinKeyVal`/
* `signature` (domain.md §6): flatten the request array (excluding `sign`/
* `sign_type`, at any nesting level `biz_content` included) into sorted
* `key=val` pairs joined by `&`, append `&key={merchant_key}`, SHA-256 hash,
* uppercase. Shared by initiate()/verify()/refund() on every gateway.
*/
class KbzSignature
{
/**
* @param array<string, mixed> $data
* @param list<string> $skips Additional top-level/nested keys to exclude beyond sign/sign_type.
*/
public static function joinKeyVal(array $data, array $skips = []): string
{
$skips = [...$skips, 'sign', 'sign_type'];
$fields = [];
self::collect($data, $skips, $fields);
usort($fields, fn (array $a, array $b): int => strcmp($a['key'], $b['key']));
$pairs = [];
foreach ($fields as $field) {
if ($field['val'] !== null && trim((string) $field['val']) !== '') {
$pairs[] = $field['key'].'='.$field['val'];
}
}
return implode('&', $pairs);
}
/**
* @param array<string, mixed> $data
* @param list<string> $skips
*/
public static function sign(array $data, string $merchantKey, array $skips = []): string
{
$joined = self::joinKeyVal($data, $skips);
return strtoupper(hash('sha256', $joined.'&key='.$merchantKey));
}
/**
* @param list<string> $skips
* @param list<array{key: string, val: mixed}> $fields
*/
private static function collect(mixed $value, array $skips, array &$fields, string $key = ''): void
{
if (in_array($key, $skips, true)) {
return;
}
if (is_array($value)) {
foreach ($value as $subKey => $subVal) {
self::collect($subVal, $skips, $fields, (string) $subKey);
}
return;
}
if ($key === '') {
return;
}
$fields[] = ['key' => $key, 'val' => $value];
}
}