feat(pos-transactions-web): show payable payment progression

This commit is contained in:
José René White Enciso 2026-03-31 19:08:40 -06:00
parent a626474c65
commit 0dfa200ebf
5 changed files with 69 additions and 15 deletions

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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();
});

View File

@ -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',