Complete Payment module: initiate/webhook/confirm/refund actions, Filament resources (T5.8-T5.13)
- InitiatePaymentAction + POST /api/v1/payments/{booking}/initiate
- Generic KBZ webhook (POST /api/v1/webhooks/{method}/{encryptBookingId?}),
signature verification per KBZ's real callback spec, PaymentGatewayInterface::handleWebhook()
- ConfirmPaymentAction: idempotent confirmation, PaymentCompleted/PaymentFailed events,
MarkBookingPaid listener
- RefundBookingAction + POST /api/v1/bookings/{booking}/refund: partial refunds validated
against remaining balance, RefundProcessed event, MarkBookingRefunded listener
- CancelBookingAction now refunds confirmed bookings instead of rejecting; BookingPolicy::cancel
requires process_refunds for confirmed bookings
- PaymentPlugin + PaymentResource/RefundResource Filament admin UI (read-only payments,
refund list + Process action)
- Booking detail page now shows related payments
- Fix CACHE_STORE mismatch (database -> redis) so tagged route caching works
- CLAUDE.md: never run migrate:fresh/migrate:refresh/db:wipe on dev without being asked
This commit is contained in:
@@ -9,8 +9,10 @@ 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\Exceptions\InvalidWebhookSignatureException;
|
||||
use Modules\Payment\Support\KbzSignature;
|
||||
|
||||
/**
|
||||
@@ -157,6 +159,46 @@ class KbzMiniAppGateway implements PaymentGatewayInterface
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* bnf_event's equivalent (`OrderController::paymentComplete` /
|
||||
* `KBZMiniApp::save`) trusted the raw `trade_status` from the POST body
|
||||
* and only re-verified via `queryorder` afterward — it never checked
|
||||
* `sign` on the inbound payload at all. This closes that gap: KBZ signs
|
||||
* webhook notifications with the same scheme as our outbound calls
|
||||
* (confirmed against KBZ's "6 Callback Interface" spec), so the
|
||||
* signature is checked first, before any of the payload is trusted.
|
||||
*
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
public function handleWebhook(array $payload): PaymentResultData
|
||||
{
|
||||
/** @var array<string, mixed> $notification */
|
||||
$notification = (array) ($payload['Request'] ?? []);
|
||||
|
||||
if (! $this->hasValidSignature($notification)) {
|
||||
throw InvalidWebhookSignatureException::forGateway(PaymentMethod::KbzMiniApp);
|
||||
}
|
||||
|
||||
return new PaymentResultData(
|
||||
status: $this->mapTradeStatus($notification['trade_status'] ?? null),
|
||||
gatewayTransactionId: $notification['merch_order_id'] ?? null,
|
||||
gatewayPayload: $notification,
|
||||
message: $notification['trade_status'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $notification
|
||||
*/
|
||||
private function hasValidSignature(array $notification): bool
|
||||
{
|
||||
if (! isset($notification['sign']) || ! is_string($notification['sign']) || $this->merchantKey === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hash_equals(KbzSignature::sign($notification, $this->merchantKey), strtoupper($notification['sign']));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user