phase 7 blockers
This commit is contained in:
@@ -778,6 +778,11 @@
|
|||||||
"total_payable_label": "Total to pay",
|
"total_payable_label": "Total to pay",
|
||||||
"secure_gateway_notice": "Secure payment via bank gateway",
|
"secure_gateway_notice": "Secure payment via bank gateway",
|
||||||
"cta_pay": "Continue to payment",
|
"cta_pay": "Continue to payment",
|
||||||
|
"gateway_title": "Redirecting to the payment gateway",
|
||||||
|
"gateway_hint": "This is a demo page standing in for the bank gateway.",
|
||||||
|
"gateway_reference_label": "Reference",
|
||||||
|
"gateway_pay_success": "Pay (demo)",
|
||||||
|
"gateway_pay_fail": "Cancel",
|
||||||
"bnpl_option": "Or pay in installments",
|
"bnpl_option": "Or pay in installments",
|
||||||
"state_initiating": "Starting payment…",
|
"state_initiating": "Starting payment…",
|
||||||
"state_redirecting": "Redirecting to the payment gateway…",
|
"state_redirecting": "Redirecting to the payment gateway…",
|
||||||
|
|||||||
@@ -778,6 +778,11 @@
|
|||||||
"total_payable_label": "مبلغ قابل پرداخت",
|
"total_payable_label": "مبلغ قابل پرداخت",
|
||||||
"secure_gateway_notice": "پرداخت امن از طریق درگاه بانکی",
|
"secure_gateway_notice": "پرداخت امن از طریق درگاه بانکی",
|
||||||
"cta_pay": "ادامه پرداخت",
|
"cta_pay": "ادامه پرداخت",
|
||||||
|
"gateway_title": "در حال انتقال به درگاه پرداخت",
|
||||||
|
"gateway_hint": "این صفحه نمایشی جایگزین درگاه بانکی است.",
|
||||||
|
"gateway_reference_label": "کد مرجع",
|
||||||
|
"gateway_pay_success": "پرداخت (نمایشی)",
|
||||||
|
"gateway_pay_fail": "انصراف",
|
||||||
"bnpl_option": "یا پرداخت اقساطی",
|
"bnpl_option": "یا پرداخت اقساطی",
|
||||||
"state_initiating": "در حال آغاز پرداخت…",
|
"state_initiating": "در حال آغاز پرداخت…",
|
||||||
"state_redirecting": "در حال انتقال به درگاه پرداخت…",
|
"state_redirecting": "در حال انتقال به درگاه پرداخت…",
|
||||||
|
|||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
'use client';
|
||||||
|
import { Suspense } from 'react';
|
||||||
|
import { useLocale, useTranslations } from 'next-intl';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import { Box, Stack, Typography } from '@mui/material';
|
||||||
|
import { AppButton, AppLoading, PaymentStateCard } from '@/components';
|
||||||
|
import { ROUTES } from '@/constants';
|
||||||
|
import {
|
||||||
|
CHECKOUT_QUERY_OUTCOME,
|
||||||
|
CHECKOUT_QUERY_REQUEST_ID,
|
||||||
|
CHECKOUT_QUERY_TRANSACTION_ID,
|
||||||
|
} from '@/services/payment/constants';
|
||||||
|
import type { GatewayReturnOutcome } from '@/services/payment/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock-gateway harness — a **test harness, not a product feature**, mirroring the BNPL provider-handoff
|
||||||
|
* harness (`checkout/bnpl/gateway/page.tsx`). It stands in for the PSP so the initiate → redirect → return
|
||||||
|
* round-trip is exercisable without a real gateway: `MockPaymentProvider.InitPaymentAsync` points its
|
||||||
|
* `redirectUrl` here, and the pay/cancel buttons drive both outcome branches of the return surface (a real
|
||||||
|
* PSP redirects back after the cardholder pays or cancels). Deliberately reachable in every build, including
|
||||||
|
* production — unlike the BNPL harness, this one is NOT env-gated: this deployment is a demo with no real
|
||||||
|
* gateway wired up (`mvp/blockers.md` §B.5), so the mock stays reachable until a real acquirer is switched
|
||||||
|
* on, at which point `redirectUrl` becomes the PSP's absolute URL and this page is never reached.
|
||||||
|
*/
|
||||||
|
export default function MockGatewayPage() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<AppLoading />}>
|
||||||
|
<MockGatewayScreen />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MockGatewayScreen() {
|
||||||
|
const t = useTranslations('payment');
|
||||||
|
const locale = useLocale();
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useSearchParams();
|
||||||
|
|
||||||
|
const requestId = params.get(CHECKOUT_QUERY_REQUEST_ID) ?? '';
|
||||||
|
const transactionId = params.get(CHECKOUT_QUERY_TRANSACTION_ID) ?? '';
|
||||||
|
|
||||||
|
const returnWith = (outcome: GatewayReturnOutcome) => {
|
||||||
|
const query = new URLSearchParams({
|
||||||
|
[CHECKOUT_QUERY_REQUEST_ID]: requestId,
|
||||||
|
[CHECKOUT_QUERY_TRANSACTION_ID]: transactionId,
|
||||||
|
[CHECKOUT_QUERY_OUTCOME]: outcome,
|
||||||
|
});
|
||||||
|
router.replace(`/${locale}${ROUTES.CHECKOUT_RETURN}?${query.toString()}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PaymentStateCard icon="payment" tone="var(--bal-secondary)" title={t('gateway_title')} body={t('gateway_hint')}>
|
||||||
|
{transactionId ? (
|
||||||
|
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||||||
|
{t('gateway_reference_label')}:{' '}
|
||||||
|
<Box component="span" dir="ltr">
|
||||||
|
#{transactionId}
|
||||||
|
</Box>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
<Stack sx={{ gap: 1.5, alignItems: 'center', width: '100%' }}>
|
||||||
|
<AppButton color="secondary" variant="contained" size="large" onClick={() => returnWith('success')}>
|
||||||
|
{t('gateway_pay_success')}
|
||||||
|
</AppButton>
|
||||||
|
<AppButton variant="text" color="error" onClick={() => returnWith('failure')}>
|
||||||
|
{t('gateway_pay_fail')}
|
||||||
|
</AppButton>
|
||||||
|
</Stack>
|
||||||
|
</PaymentStateCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -27,6 +27,8 @@ export const ROUTES = {
|
|||||||
BOOKING_REQUEST_STATUS: '/bookings/request',
|
BOOKING_REQUEST_STATUS: '/bookings/request',
|
||||||
// Checkout (f9) — C6 summary & pay; the C5 accept CTA hands off here with `?request_id=`.
|
// Checkout (f9) — C6 summary & pay; the C5 accept CTA hands off here with `?request_id=`.
|
||||||
CHECKOUT: '/bookings/checkout',
|
CHECKOUT: '/bookings/checkout',
|
||||||
|
// Mock-gateway harness (test-only stand-in for the PSP redirect; the mock initiate points here).
|
||||||
|
CHECKOUT_GATEWAY: '/bookings/checkout/gateway',
|
||||||
// Return-from-gateway surface — pending-callback poll → succeeded/failed states.
|
// Return-from-gateway surface — pending-callback poll → succeeded/failed states.
|
||||||
CHECKOUT_RETURN: '/bookings/checkout/return',
|
CHECKOUT_RETURN: '/bookings/checkout/return',
|
||||||
// Post-payment success screen — links to the booking detail + invoice.
|
// Post-payment success screen — links to the booking detail + invoice.
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ Effort is a rough size, not a schedule: **S** = small/contained, **M** = a real
|
|||||||
## A. The product is broken or unusable
|
## A. The product is broken or unusable
|
||||||
|
|
||||||
### Payments
|
### Payments
|
||||||
- **A card payment can never actually complete.** The test/demo payment page redirects to a page that
|
|
||||||
doesn't exist. Nothing about the money logic itself is wrong — it's the very last step, handing off to a
|
|
||||||
real payment provider, that isn't connected. *(Effort: M)*
|
|
||||||
- **The 30-minute payment countdown can lie.** Booking deadlines are stored without a timezone, so the timer
|
- **The 30-minute payment countdown can lie.** Booking deadlines are stored without a timezone, so the timer
|
||||||
a customer sees can silently show hours more time than they actually have, and expire while they still
|
a customer sees can silently show hours more time than they actually have, and expire while they still
|
||||||
think they're fine. *(Effort: S)*
|
think they're fine. *(Effort: S)*
|
||||||
|
|||||||
+10
-1
@@ -22,7 +22,7 @@ whatever order you prefer.
|
|||||||
| 04 | [payment-timezone](blocker-phases/04-payment-timezone.md) | The 30-minute payment countdown can lie | — | ✅ Done (follow-up filed below) |
|
| 04 | [payment-timezone](blocker-phases/04-payment-timezone.md) | The 30-minute payment countdown can lie | — | ✅ Done (follow-up filed below) |
|
||||||
| 05 | [bnpl-setup](blocker-phases/05-bnpl-setup.md) | Installments (BNPL) don't work at all | — | — |
|
| 05 | [bnpl-setup](blocker-phases/05-bnpl-setup.md) | Installments (BNPL) don't work at all | — | — |
|
||||||
| 06 | [catalog-admin-page](blocker-phases/06-catalog-admin-page.md) | No admin page for service categories/pricing | — | — |
|
| 06 | [catalog-admin-page](blocker-phases/06-catalog-admin-page.md) | No admin page for service categories/pricing | — | — |
|
||||||
| 07 | [card-payment-redirect](blocker-phases/07-card-payment-redirect.md) | Card payment can never complete | — | — |
|
| 07 | [card-payment-redirect](blocker-phases/07-card-payment-redirect.md) | Card payment can never complete | — | ✅ Done (follow-up filed below) |
|
||||||
| 08 | [refunds-demock](blocker-phases/08-refunds-demock.md) | Refunds are demo-only, off by 100× | — | — |
|
| 08 | [refunds-demock](blocker-phases/08-refunds-demock.md) | Refunds are demo-only, off by 100× | — | — |
|
||||||
| 09 | [nurse-verification-badge](blocker-phases/09-nurse-verification-badge.md) | Verification badge doesn't reflect reality | pairs with 10 | — |
|
| 09 | [nurse-verification-badge](blocker-phases/09-nurse-verification-badge.md) | Verification badge doesn't reflect reality | pairs with 10 | — |
|
||||||
| 10 | [search-dedup-and-trust](blocker-phases/10-search-dedup-and-trust.md) | Search isn't de-duplicated; trust info hardcoded | pairs with 09 | — |
|
| 10 | [search-dedup-and-trust](blocker-phases/10-search-dedup-and-trust.md) | Search isn't de-duplicated; trust info hardcoded | pairs with 09 | — |
|
||||||
@@ -52,6 +52,15 @@ whatever order you prefer.
|
|||||||
decision (booking-lifecycle's "today's visits" fix), since both are the same missing
|
decision (booking-lifecycle's "today's visits" fix), since both are the same missing
|
||||||
`Asia/Tehran`/UTC-boundary discipline.
|
`Asia/Tehran`/UTC-boundary discipline.
|
||||||
|
|
||||||
|
- **ZarinPal-callback translator, latent until a real gateway is switched on.** Phase 07 restored the
|
||||||
|
mock-gateway harness and pointed `MockPaymentProvider.InitPaymentAsync`'s `redirectUrl` at it (relative
|
||||||
|
URL, no more dead `mock-psp.local` host) — the card-payment dead end (blockers.md § "Payments") is closed
|
||||||
|
for the mock path, which is the only path reachable pre-e-namad (§B.5). Once a real acquirer is ever
|
||||||
|
switched on, ZarinPal's callback redirects the browser with its **own** query params (`Authority`,
|
||||||
|
`Status`) — nothing under `Controllers/V1` today translates that into what `checkout/return/page.tsx`
|
||||||
|
expects (`request_id`/`transaction_id`/`outcome`). Needs a translating endpoint/route before the real path
|
||||||
|
can work; deliberately out of scope of the mock fix, and moot until §B.5 clears.
|
||||||
|
|
||||||
## Suggested order
|
## Suggested order
|
||||||
|
|
||||||
1. **01 (admin RBAC)** — unlocks 02, and makes 09/10/11/14 independently testable via the seeded
|
1. **01 (admin RBAC)** — unlocks 02, and makes 09/10/11/14 independently testable via the seeded
|
||||||
|
|||||||
+11
-6
@@ -1,23 +1,28 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
using System.Net;
|
||||||
using Baya.Application.Contracts.Payments;
|
using Baya.Application.Contracts.Payments;
|
||||||
|
|
||||||
namespace Baya.Infrastructure.CrossCutting.Seams;
|
namespace Baya.Infrastructure.CrossCutting.Seams;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deterministic mock <see cref="IPaymentProvider"/> — no external call. <see cref="InitPaymentAsync"/> returns
|
/// Deterministic mock <see cref="IPaymentProvider"/> — no external call. <see cref="InitPaymentAsync"/> returns
|
||||||
/// a stable fake reference derived from the request + idempotency key and a fake redirect URL;
|
/// a stable fake reference derived from the request + idempotency key, and an app-relative <c>redirectUrl</c>
|
||||||
/// <see cref="VerifyAsync"/> instantly succeeds and echoes the expected amount (the server-side re-check always
|
/// pointing at the client's mock-gateway harness (<c>bookings/checkout/gateway</c>, `ROUTES.CHECKOUT_GATEWAY`) —
|
||||||
/// passes in the mock); <see cref="RefundAsync"/> always succeeds (so b11 can call it). A real
|
/// the client's `initiatePayment` success handler already branches on an absolute vs. relative URL, so this
|
||||||
/// ZarinPal/Sadad/Vandar/Jibit adapter (acquirer-with-تسهیم, Shaparak reference, sandbox flag from the
|
/// harness round-trips without a real PSP; <see cref="VerifyAsync"/> instantly succeeds and echoes the expected
|
||||||
/// gateway's encrypted config) replaces this registration only — no mock behaviour is baked into a handler.
|
/// amount (the server-side re-check always passes in the mock); <see cref="RefundAsync"/> always succeeds (so
|
||||||
|
/// b11 can call it). A real ZarinPal/Sadad/Vandar/Jibit adapter (acquirer-with-تسهیم, Shaparak reference,
|
||||||
|
/// sandbox flag from the gateway's encrypted config) replaces this registration only — no mock behaviour is
|
||||||
|
/// baked into a handler.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class MockPaymentProvider : IPaymentProvider
|
public sealed class MockPaymentProvider : IPaymentProvider
|
||||||
{
|
{
|
||||||
public ValueTask<PaymentInitResult> InitPaymentAsync(long bookingRequestId, long amountIrr, string idempotencyKey, CancellationToken cancellationToken = default)
|
public ValueTask<PaymentInitResult> InitPaymentAsync(long bookingRequestId, long amountIrr, string idempotencyKey, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var reference = $"mock-ref-{bookingRequestId}-{idempotencyKey}";
|
var reference = $"mock-ref-{bookingRequestId}-{idempotencyKey}";
|
||||||
|
var redirectUrl = $"/bookings/checkout/gateway?request_id={bookingRequestId}&transaction_id={WebUtility.UrlEncode(reference)}";
|
||||||
return ValueTask.FromResult(new PaymentInitResult(
|
return ValueTask.FromResult(new PaymentInitResult(
|
||||||
RedirectUrl: $"https://mock-psp.local/pay/{reference}",
|
RedirectUrl: redirectUrl,
|
||||||
GatewayReferenceCode: reference));
|
GatewayReferenceCode: reference));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user