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,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];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user