feat(pos-transactions-web): show payable payment progression
This commit is contained in:
parent
a626474c65
commit
0dfa200ebf
@ -23,7 +23,8 @@
|
||||
## UI Workflow Coverage
|
||||
|
||||
- POS transaction summary lookup with open balance visibility
|
||||
- Transaction detail inspection for a selected transaction id
|
||||
- Transaction detail inspection for a selected payable check or transaction id
|
||||
- Recent payment activity review
|
||||
- Payment capture with retry-ready local session history
|
||||
- Payment capture with retry-ready local session history and lifecycle-aware payment hints
|
||||
- Protected route shell for summary, payment capture, and session inspection
|
||||
- POS actions are presented as the final step after kitchen and floor service complete the restaurant order.
|
||||
|
||||
@ -22,6 +22,7 @@ npm run dev
|
||||
- Business calls are gated behind session checks.
|
||||
- Session cookies are sent with `credentials: include`.
|
||||
- Summary, detail, recent-payment, and capture actions all surface session-expired guidance before retry.
|
||||
- UI copy assumes POS only acts on checks that became payable after restaurant service completion.
|
||||
|
||||
## Build
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ npm run test:ci
|
||||
- `src/api/client.test.ts`: runtime-config precedence and fallback behavior.
|
||||
- `src/api/dashboardApi.test.ts`: endpoint path/query composition and payload mapping.
|
||||
- `src/auth/oidcLogin.test.ts`: OIDC start-url generation and safe return-url fallback.
|
||||
- `src/App.test.tsx`: central login screen, protected summary/detail flow, payment capture, and session-expired recovery guidance.
|
||||
- `src/App.test.tsx`: central login screen, lifecycle-aware payable-check summary/detail flow, payment capture, and session-expired recovery guidance.
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@ -56,7 +56,7 @@ describe('POS Transactions App', () => {
|
||||
});
|
||||
vi.mocked(loadDashboard).mockResolvedValue({
|
||||
contextId: 'demo-context',
|
||||
summary: '2 payments awaiting reconciliation',
|
||||
summary: '2 served orders are payable',
|
||||
openBalance: 52.3,
|
||||
currency: 'USD',
|
||||
recentPayments: []
|
||||
@ -86,7 +86,7 @@ describe('POS Transactions App', () => {
|
||||
paymentMethod: 'cash',
|
||||
amount: 10,
|
||||
currency: 'USD',
|
||||
status: 'Captured',
|
||||
status: 'Payable',
|
||||
capturedAtUtc: '2026-03-27T11:00:00Z'
|
||||
}
|
||||
]
|
||||
@ -103,9 +103,11 @@ describe('POS Transactions App', () => {
|
||||
await waitFor(() => expect(loadTransactionDetail).toHaveBeenCalledWith('demo-context', 'POS-9001'));
|
||||
await waitFor(() => expect(loadRecentPayments).toHaveBeenCalledWith('demo-context'));
|
||||
|
||||
expect(await screen.findByText('2 payments awaiting reconciliation')).toBeInTheDocument();
|
||||
expect(await screen.findByText('2 served orders are payable')).toBeInTheDocument();
|
||||
expect(screen.getByText('Only restaurant orders that have completed service should appear here as payable checks for capture.')).toBeInTheDocument();
|
||||
expect(await screen.findByText('POS-22')).toBeInTheDocument();
|
||||
expect(await screen.findByText('POS-21')).toBeInTheDocument();
|
||||
expect(await screen.findByText('ready for payment capture after service completion')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('captures payments and records retry-ready history', async () => {
|
||||
@ -154,6 +156,7 @@ describe('POS Transactions App', () => {
|
||||
|
||||
await waitFor(() => expect(capturePosPayment).toHaveBeenCalledTimes(1));
|
||||
expect(await screen.findAllByText('POS-2200')).toHaveLength(2);
|
||||
expect(screen.getAllByText('payment is complete and the restaurant workflow can close').length).toBeGreaterThan(0);
|
||||
expect(screen.getByRole('button', { name: 'Retry Last Capture' })).not.toBeDisabled();
|
||||
});
|
||||
|
||||
|
||||
67
src/App.tsx
67
src/App.tsx
@ -67,9 +67,10 @@ const paymentColumns = [
|
||||
{
|
||||
title: 'Status',
|
||||
render: (_: unknown, record: PosPaymentActivity) => (
|
||||
<Tag color={record.status.toLowerCase() === 'captured' ? 'green' : 'orange'}>{record.status}</Tag>
|
||||
<Tag color={paymentStatusColor(record.status)}>{record.status}</Tag>
|
||||
)
|
||||
},
|
||||
{ title: 'Lifecycle Step', render: (_: unknown, record: PosPaymentActivity) => paymentProgressHint(record.status) },
|
||||
{ title: 'Captured At', render: (_: unknown, record: PosPaymentActivity) => formatUtc(record.capturedAtUtc) }
|
||||
];
|
||||
|
||||
@ -240,7 +241,7 @@ function PosTransactionsShell() {
|
||||
<Layout.Content className="content">
|
||||
<Typography.Title level={3}>POS Transactions</Typography.Title>
|
||||
<Typography.Paragraph type="secondary">
|
||||
Protected POS workflows for summary lookup, transaction detail inspection, capture retries, and recent payment activity.
|
||||
Protected POS workflows for payable restaurant checks, capture retries, and recent payment activity once service is complete.
|
||||
</Typography.Paragraph>
|
||||
{session.error && <Alert className="stack-gap" type="warning" showIcon message={session.error} />}
|
||||
{workflowState.error && <Alert className="stack-gap" type="error" showIcon message={workflowState.error} />}
|
||||
@ -296,9 +297,12 @@ function PosTransactionsShell() {
|
||||
<Descriptions.Item label="Open Balance">
|
||||
{summaryPayload.openBalance.toFixed(2)} {summaryPayload.currency}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Lifecycle Note">
|
||||
Only restaurant orders that have completed service should appear here as payable checks for capture.
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
) : (
|
||||
<Empty description="Load a POS summary to inspect the current balance." />
|
||||
<Empty description="Load a POS summary to inspect which served orders are ready for payment capture." />
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
@ -313,11 +317,14 @@ function PosTransactionsShell() {
|
||||
<Descriptions.Item label="Amount">
|
||||
{detailPayload.transaction.amount.toFixed(2)} {detailPayload.transaction.currency}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Status">{detailPayload.transaction.status}</Descriptions.Item>
|
||||
<Descriptions.Item label="Status">
|
||||
<Tag color={paymentStatusColor(detailPayload.transaction.status)}>{detailPayload.transaction.status}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Next Step">{paymentProgressHint(detailPayload.transaction.status)}</Descriptions.Item>
|
||||
<Descriptions.Item label="Captured At">{formatUtc(detailPayload.transaction.capturedAtUtc)}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
) : (
|
||||
<Empty description="Load a transaction detail to inspect capture state and method." />
|
||||
<Empty description="Load a transaction detail to inspect payable-check state and capture readiness." />
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@ -381,7 +388,7 @@ function PosTransactionsShell() {
|
||||
</Space>
|
||||
</Form>
|
||||
<Typography.Text type="secondary">
|
||||
Use retry when a payment attempt fails after verifying the transaction payload and operator session.
|
||||
Capture should be used only after kitchen and floor service complete the order, making the check payable.
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</Card>
|
||||
@ -392,7 +399,10 @@ function PosTransactionsShell() {
|
||||
<Descriptions.Item label="Context Id">{paymentResponse.contextId}</Descriptions.Item>
|
||||
<Descriptions.Item label="Transaction Id">{paymentResponse.transactionId}</Descriptions.Item>
|
||||
<Descriptions.Item label="Succeeded">{String(paymentResponse.succeeded)}</Descriptions.Item>
|
||||
<Descriptions.Item label="Status">{paymentResponse.status}</Descriptions.Item>
|
||||
<Descriptions.Item label="Status">
|
||||
<Tag color={paymentStatusColor(paymentResponse.status)}>{paymentResponse.status}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Next Step">{paymentProgressHint(paymentResponse.status)}</Descriptions.Item>
|
||||
<Descriptions.Item label="Captured At">{formatUtc(paymentResponse.capturedAtUtc)}</Descriptions.Item>
|
||||
<Descriptions.Item label="Summary">{paymentResponse.summary}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
@ -417,10 +427,11 @@ function PosTransactionsShell() {
|
||||
{
|
||||
title: 'Status',
|
||||
render: (_, record) => (
|
||||
<Tag color={record.response.succeeded ? 'green' : 'red'}>{record.response.status}</Tag>
|
||||
<Tag color={paymentStatusColor(record.response.status)}>{record.response.status}</Tag>
|
||||
)
|
||||
},
|
||||
{ title: 'Summary', render: (_, record) => record.response.summary }
|
||||
{ title: 'Summary', render: (_, record) => record.response.summary },
|
||||
{ title: 'Lifecycle Step', render: (_, record) => paymentProgressHint(record.response.status) }
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
@ -455,6 +466,44 @@ function providerLabel(provider: IdentityProvider) {
|
||||
}
|
||||
}
|
||||
|
||||
function paymentStatusColor(status: string) {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'captured':
|
||||
case 'paid':
|
||||
return 'green';
|
||||
case 'payable':
|
||||
case 'ready':
|
||||
return 'blue';
|
||||
case 'pending':
|
||||
case 'authorized':
|
||||
return 'orange';
|
||||
case 'failed':
|
||||
case 'blocked':
|
||||
return 'red';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
|
||||
function paymentProgressHint(status: string) {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'payable':
|
||||
case 'ready':
|
||||
return 'ready for payment capture after service completion';
|
||||
case 'pending':
|
||||
case 'authorized':
|
||||
return 'payment is in progress and should be monitored before retrying';
|
||||
case 'captured':
|
||||
case 'paid':
|
||||
return 'payment is complete and the restaurant workflow can close';
|
||||
case 'failed':
|
||||
case 'blocked':
|
||||
return 'operator review is required before attempting another capture';
|
||||
default:
|
||||
return 'payment state is being resolved against the restaurant lifecycle';
|
||||
}
|
||||
}
|
||||
|
||||
function formatUtc(value: string) {
|
||||
return new Date(value).toLocaleString('en-US', {
|
||||
dateStyle: 'medium',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user