@final-commerce/command-frame 0.3.2-preprod.1 → 0.4.1-preprod.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -50
- package/dist/actions/apply-transition/mock.d.ts +1 -1
- package/dist/actions/apply-transition/mock.js +8 -18
- package/dist/actions/apply-transition/types.d.ts +6 -4
- package/dist/actions/extension-payment/types.d.ts +2 -2
- package/dist/actions/get-available-transitions/mock.d.ts +5 -3
- package/dist/actions/get-available-transitions/mock.js +24 -12
- package/dist/actions/get-refund-plan/action.d.ts +6 -0
- package/dist/actions/get-refund-plan/action.js +8 -0
- package/dist/actions/get-refund-plan/mock.d.ts +14 -0
- package/dist/actions/get-refund-plan/mock.js +59 -0
- package/dist/actions/get-refund-plan/types.d.ts +32 -0
- package/dist/actions/get-refund-plan/types.js +7 -0
- package/dist/actions/integration-payment/types.d.ts +2 -0
- package/dist/actions/partial-payment/types.d.ts +2 -2
- package/dist/actions/process-partial-refund/mock.js +9 -1
- package/dist/actions/process-partial-refund/types.d.ts +99 -1
- package/dist/actions/redeem-payment/types.d.ts +2 -0
- package/dist/actions/redeem-refund/action.d.ts +6 -0
- package/dist/actions/redeem-refund/action.js +8 -0
- package/dist/actions/redeem-refund/mock.d.ts +2 -0
- package/dist/actions/redeem-refund/mock.js +59 -0
- package/dist/actions/redeem-refund/types.d.ts +39 -0
- package/dist/actions/redeem-refund/types.js +2 -0
- package/dist/actions/terminal-payment/types.d.ts +3 -3
- package/dist/demo/database.d.ts +2 -2
- package/dist/demo/database.js +271 -264
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/projects/render/mocks.js +6 -0
- package/dist/projects/render/types.d.ts +4 -1
- package/dist/pubsub/topics/orders/index.d.ts +2 -2
- package/dist/pubsub/topics/orders/index.js +27 -27
- package/dist/pubsub/topics/orders/types.d.ts +15 -15
- package/dist/pubsub/topics/orders/types.js +7 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,13 +10,13 @@ Command Frame provides a structured way to build integrations that run inside Fi
|
|
|
10
10
|
|
|
11
11
|
The library provides three main capabilities:
|
|
12
12
|
|
|
13
|
-
| Capability
|
|
14
|
-
|
|
|
15
|
-
| **Commands**
|
|
16
|
-
| **Pub/Sub**
|
|
17
|
-
| **Hooks**
|
|
18
|
-
| **Interceptors**
|
|
19
|
-
| **
|
|
13
|
+
| Capability | Purpose | Scope |
|
|
14
|
+
| ------------------- | ------------------------------------------------------------------------- | ----------------------------------------- |
|
|
15
|
+
| **Commands** | Call host functions from the iframe (e.g. get products, open cash drawer) | Request/response per call |
|
|
16
|
+
| **Pub/Sub** | Subscribe to real-time events from the host (e.g. cart changes, payments) | Page-scoped (while iframe is mounted) |
|
|
17
|
+
| **Hooks** | Register business-logic callbacks that persist across all pages | Session-scoped (survives page navigation) |
|
|
18
|
+
| **Interceptors** | Gate POS flows (approve / modify / block) at named points | Blocking; host waits for your response |
|
|
19
|
+
| **Refund commands** | Refund payments to gift cards or redeem tenders via `redeemRefund`, or mixed-destination legs on `processPartialRefund`; query engine capacity with `getRefundPlan` | Request/response per call |
|
|
20
20
|
|
|
21
21
|
Domain models (orders, cart, customers, products, and related types) are documented in **[Types reference](./src/types/README.md)**.
|
|
22
22
|
|
|
@@ -71,7 +71,7 @@ For building applications that run inside the Render Point of Sale interface.
|
|
|
71
71
|
- **Features:** Order management, Product catalog, Customer management, Payments, Hardware integration (Cash drawer, Printer), Custom tables, Secrets storage.
|
|
72
72
|
|
|
73
73
|
```typescript
|
|
74
|
-
import { RenderClient } from
|
|
74
|
+
import { RenderClient } from '@final-commerce/command-frame';
|
|
75
75
|
|
|
76
76
|
const client = new RenderClient();
|
|
77
77
|
const products = await client.getProducts();
|
|
@@ -85,7 +85,7 @@ For building applications that run inside the Final Commerce Management Dashboar
|
|
|
85
85
|
- **Features:** Context, catalog, entities, custom tables, secrets, and optional host-specific commands (navigation, media, tax, branding, notifications) when the dashboard implements them.
|
|
86
86
|
|
|
87
87
|
```typescript
|
|
88
|
-
import { ManageClient } from
|
|
88
|
+
import { ManageClient } from '@final-commerce/command-frame';
|
|
89
89
|
|
|
90
90
|
const client = new ManageClient();
|
|
91
91
|
const context = await client.getContext();
|
|
@@ -99,14 +99,14 @@ The pub/sub system allows iframe extensions to subscribe to topics and receive r
|
|
|
99
99
|
- **Topics:** Cart (16), Customers (8), Orders (5), Payments (2), Products (4), Refunds (4), Print (3), Custom Tables (3), Outlet (2), Station (2), Session (2), Users (2).
|
|
100
100
|
|
|
101
101
|
```typescript
|
|
102
|
-
import { topics } from
|
|
102
|
+
import { topics } from '@final-commerce/command-frame';
|
|
103
103
|
|
|
104
|
-
const subscriptionId = topics.subscribe(
|
|
105
|
-
|
|
104
|
+
const subscriptionId = topics.subscribe('cart', (event) => {
|
|
105
|
+
console.log('Cart event:', event.type, event.data);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
// Unsubscribe when done
|
|
109
|
-
topics.unsubscribe(
|
|
109
|
+
topics.unsubscribe('cart', subscriptionId);
|
|
110
110
|
```
|
|
111
111
|
|
|
112
112
|
## Hooks
|
|
@@ -118,21 +118,21 @@ Hooks are **session-scoped** event callbacks that run in the host (Render) conte
|
|
|
118
118
|
- A stable `hookId` is required for deduplication (safe on iframe reload).
|
|
119
119
|
|
|
120
120
|
```typescript
|
|
121
|
-
import { hooks } from
|
|
121
|
+
import { hooks } from '@final-commerce/command-frame';
|
|
122
122
|
|
|
123
123
|
hooks.register(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
124
|
+
'cart',
|
|
125
|
+
async (event, hostCommands) => {
|
|
126
|
+
await hostCommands.upsertCustomTableData({
|
|
127
|
+
tableName: 'cart-events-log',
|
|
128
|
+
data: { eventType: event.type, payload: event.data, timestamp: event.timestamp },
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
{ hookId: 'my-extension:cart-log' },
|
|
132
132
|
);
|
|
133
133
|
|
|
134
134
|
// Unregister when no longer needed
|
|
135
|
-
hooks.unregister(
|
|
135
|
+
hooks.unregister('my-extension:cart-log');
|
|
136
136
|
```
|
|
137
137
|
|
|
138
138
|
## Interceptors
|
|
@@ -147,43 +147,40 @@ Interceptors let an extension **gate a POS flow** (approve / modify / block) at
|
|
|
147
147
|
import { interceptors } from '@final-commerce/command-frame';
|
|
148
148
|
|
|
149
149
|
interceptors.register(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
150
|
+
'refund_start',
|
|
151
|
+
async (payload, cmds) => {
|
|
152
|
+
if (payload.paymentTypes.includes('redeem')) {
|
|
153
|
+
return cmds.openExtensionOverlay({ point: 'refund_start', payload });
|
|
154
|
+
}
|
|
155
|
+
return true; // nothing for us to do
|
|
156
|
+
},
|
|
157
|
+
{ interceptorId: 'my-extension:refund-guard' },
|
|
158
158
|
);
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
-
##
|
|
161
|
+
## Refunding redeem / extension payments
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
When staff refund an order that was paid with `paymentType: "redeem"` (via `redeemPayment` or `extensionPayment`), use the **`redeemRefund`** command to refund the amount onto a gift card or redeem tender.
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
1. **Recommended:** call **`installExtensionRefundListener`** once when your extension boots (e.g. next to your `RenderClient` setup). Pass an `async` handler that calls your provider (gift card API, wallet, etc.) and returns an **`ExtensionRefundResponse`** (`success`, optional `error`, optional `extensionTransactionId` for receipts / support).
|
|
168
|
-
2. The helper validates `event.source === window.top`, parses params, and replies with the same **`PostMessageResponse`** envelope as the rest of Command Frame (`requestId`, `success`, `data` / `error`).
|
|
169
|
-
3. **Alternative:** implement a `window` `message` listener yourself using the same contract (action name: **`extensionRefundRequest`**, or import **`EXTENSION_REFUND_REQUEST_ACTION`** from this package).
|
|
170
|
-
|
|
171
|
-
Exported APIs: `installExtensionRefundListener`, `EXTENSION_REFUND_REQUEST_ACTION`, types **`ExtensionRefundParams`** / **`ExtensionRefundResponse`**.
|
|
165
|
+
**Key point:** Plain refunds on redeem sources still fail by design (`REDEEM_REFUND_UNSUPPORTED`). Use `redeemRefund` to refund onto a gift card when your extension credits the card first.
|
|
172
166
|
|
|
173
167
|
```typescript
|
|
174
|
-
import {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
168
|
+
import { command } from '@final-commerce/command-frame';
|
|
169
|
+
|
|
170
|
+
// Refund a redeem order back onto a gift card
|
|
171
|
+
const result = await command.redeemRefund({
|
|
172
|
+
orderId: 'order_123',
|
|
173
|
+
amount: 2500, // $25.00
|
|
174
|
+
referenceId: 'GIFTCARD-456', // destination card
|
|
175
|
+
processor: 'giftCard',
|
|
176
|
+
label: 'Gift Card Refund',
|
|
177
|
+
reason: 'Customer requested return',
|
|
180
178
|
});
|
|
181
|
-
|
|
182
|
-
// on teardown (optional)
|
|
183
|
-
// unsubscribe();
|
|
184
179
|
```
|
|
185
180
|
|
|
186
|
-
**Full
|
|
181
|
+
**Full documentation:** **[redeemRefund](./src/actions/redeem-refund/README.md)**.
|
|
182
|
+
|
|
183
|
+
Before prompting the cashier for an amount, query **[getRefundPlan](./src/actions/get-refund-plan/README.md)** (read-only) for the order's own per-source caps (`maxRefundable`, `cardNumber` for same-card prefill) and order-level `remainingRefundable` — don't recompute this client-side, and always handle a `REFUND_AMOUNT_EXCEEDS_CAPACITY` rejection from the mutating call since the plan is only an advisory snapshot.
|
|
187
184
|
|
|
188
185
|
## Development & Testing
|
|
189
186
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ApplyTransitionParams, ApplyTransitionResponse } from "./types";
|
|
2
2
|
/**
|
|
3
|
-
* Mock implementation: applies all transitions except a
|
|
3
|
+
* Mock implementation: applies all transitions except a known-invalid one
|
|
4
4
|
* so the demo app can show both successful and blocked responses.
|
|
5
5
|
*/
|
|
6
6
|
export declare const applyTransitionMock: (params: ApplyTransitionParams) => Promise<ApplyTransitionResponse>;
|
|
@@ -1,33 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Mock implementation: applies all transitions except a
|
|
2
|
+
* Mock implementation: applies all transitions except a known-invalid one
|
|
3
3
|
* so the demo app can show both successful and blocked responses.
|
|
4
4
|
*/
|
|
5
5
|
export const applyTransitionMock = (params) => {
|
|
6
|
-
const {
|
|
7
|
-
if (
|
|
6
|
+
const { targetFulfillmentState } = params;
|
|
7
|
+
if (targetFulfillmentState === "cancelled") {
|
|
8
8
|
return Promise.resolve({
|
|
9
9
|
result: {
|
|
10
10
|
allowed: false,
|
|
11
|
-
blockedBy: "
|
|
12
|
-
guard: "no-
|
|
13
|
-
reason: "Cannot
|
|
14
|
-
}
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
if (to.payment === "paid" && to.fulfillment === "cancelled") {
|
|
18
|
-
return Promise.resolve({
|
|
19
|
-
result: {
|
|
20
|
-
allowed: false,
|
|
21
|
-
blockedBy: "cross_axis_rule",
|
|
22
|
-
guard: "no-pay-cancelled",
|
|
23
|
-
reason: "Cannot mark a cancelled order as paid"
|
|
11
|
+
blockedBy: "condition",
|
|
12
|
+
guard: "no-cancel-open-order",
|
|
13
|
+
reason: "Cannot cancel an order with open items"
|
|
24
14
|
}
|
|
25
15
|
});
|
|
26
16
|
}
|
|
27
17
|
return Promise.resolve({
|
|
28
18
|
result: { allowed: true },
|
|
29
19
|
from: { payment: "unpaid", fulfillment: "draft" },
|
|
30
|
-
to,
|
|
31
|
-
displayState:
|
|
20
|
+
to: { payment: "unpaid", fulfillment: targetFulfillmentState },
|
|
21
|
+
displayState: `unpaid / ${targetFulfillmentState}`
|
|
32
22
|
});
|
|
33
23
|
};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { CFStatePair, CFTransitionResult } from "../../common-types/order-state";
|
|
2
2
|
export interface ApplyTransitionParams {
|
|
3
|
-
/** Order to transition. */
|
|
4
|
-
orderId
|
|
5
|
-
/** Target state
|
|
6
|
-
|
|
3
|
+
/** Order to transition. Omit to target the active order — materializing one from the live cart if none exists. */
|
|
4
|
+
orderId?: string;
|
|
5
|
+
/** Target fulfillment state. The payment axis is never client-settable. */
|
|
6
|
+
targetFulfillmentState: string;
|
|
7
|
+
/** Clear the terminal after transitioning the ACTIVE order (park parity). Default true. */
|
|
8
|
+
clearTerminal?: boolean;
|
|
7
9
|
}
|
|
8
10
|
export interface ApplyTransitionResponse {
|
|
9
11
|
result: CFTransitionResult;
|
|
@@ -15,8 +15,8 @@ export interface ExtensionPaymentParams {
|
|
|
15
15
|
referenceId?: string;
|
|
16
16
|
extensionId?: string;
|
|
17
17
|
metadata?: Record<string, unknown>;
|
|
18
|
-
/** Override the fulfillment
|
|
19
|
-
|
|
18
|
+
/** Override the fulfillment landing on full payment. Omitted: preserve advanced fulfillment, auto-fulfill from draft/pending/on_hold. */
|
|
19
|
+
targetFulfillmentState?: string;
|
|
20
20
|
/** EMV data when the underlying payment carries one (typed as `IntegrationEmvData` by the integration wrapper). */
|
|
21
21
|
emvData?: unknown;
|
|
22
22
|
/** Processor fee in integer MINOR currency units; recorded on the order's paymentMethod.processorFee. */
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { GetAvailableTransitions } from "./types";
|
|
2
2
|
/**
|
|
3
3
|
* Mock implementation: returns a fixed set of plausible transitions
|
|
4
|
-
* so the demo app has data to render.
|
|
4
|
+
* so the demo app has data to render. Mirrors an UNPAID order under the
|
|
5
|
+
* default (empty cross-axis rules) config: fulfillment can advance without
|
|
6
|
+
* payment (FI-6383), plus the classic park/void moves.
|
|
5
7
|
*/
|
|
6
|
-
export declare const getAvailableTransitionsMock:
|
|
8
|
+
export declare const getAvailableTransitionsMock: GetAvailableTransitions;
|
|
@@ -1,26 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mock implementation: returns a fixed set of plausible transitions
|
|
3
|
-
* so the demo app has data to render.
|
|
3
|
+
* so the demo app has data to render. Mirrors an UNPAID order under the
|
|
4
|
+
* default (empty cross-axis rules) config: fulfillment can advance without
|
|
5
|
+
* payment (FI-6383), plus the classic park/void moves.
|
|
4
6
|
*/
|
|
5
|
-
export const getAvailableTransitionsMock =
|
|
7
|
+
export const getAvailableTransitionsMock = () => Promise.resolve({
|
|
6
8
|
transitions: [
|
|
7
9
|
{
|
|
8
|
-
to: { payment: "
|
|
9
|
-
displayLabel: "
|
|
10
|
-
conditions: [{ met: true, description: "Order
|
|
10
|
+
to: { payment: "unpaid", fulfillment: "in_progress" },
|
|
11
|
+
displayLabel: "Start Preparing",
|
|
12
|
+
conditions: [{ met: true, description: "Order has items" }]
|
|
11
13
|
},
|
|
12
14
|
{
|
|
13
|
-
to: { payment: "
|
|
14
|
-
displayLabel: "
|
|
15
|
-
conditions: [{ met: true, description: "Order
|
|
15
|
+
to: { payment: "unpaid", fulfillment: "fulfilled" },
|
|
16
|
+
displayLabel: "Mark Fulfilled",
|
|
17
|
+
conditions: [{ met: true, description: "Order has items" }]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
to: { payment: "unpaid", fulfillment: "on_hold" },
|
|
21
|
+
displayLabel: "Park Order",
|
|
22
|
+
conditions: [{ met: true, description: "Order is open" }]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
to: { payment: "paid", fulfillment: "fulfilled" },
|
|
26
|
+
displayLabel: "Complete Payment",
|
|
27
|
+
conditions: [{ met: true, description: "Balance due > 0" }]
|
|
16
28
|
},
|
|
17
29
|
{
|
|
18
30
|
to: { payment: "voided", fulfillment: "cancelled" },
|
|
19
31
|
displayLabel: "Void Order",
|
|
20
32
|
conditions: [
|
|
21
33
|
{ met: true, description: "Order exists" },
|
|
22
|
-
{ met: false, description: "No payments captured (mock: skipped)" }
|
|
23
|
-
]
|
|
24
|
-
}
|
|
25
|
-
]
|
|
34
|
+
{ met: false, description: "No payments captured (mock: skipped)" }
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
]
|
|
26
38
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { GetRefundPlan } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Demo derivation of the runtime `getRefundPlan`. Builds the per-source rows
|
|
4
|
+
* from the mock order's `paymentMethods`, mirroring the runtime's capacity
|
|
5
|
+
* definition (principal + captured tip).
|
|
6
|
+
*
|
|
7
|
+
* HONEST INERTNESS: the demo DB records no prior-refund ledger and no `emv`
|
|
8
|
+
* blocks on its mock captures, so this mock reports `refundedAmount: 0` /
|
|
9
|
+
* `totalRefunded: 0` and `cardNumber: undefined` for every source, and treats
|
|
10
|
+
* `maxRefundable` as the full captured amount. Against real kaching those
|
|
11
|
+
* numbers come from `order.refund[]` and the capture's `emv` JSON. Use this
|
|
12
|
+
* only to shape UI in local/standalone mode — never to assert real capacity.
|
|
13
|
+
*/
|
|
14
|
+
export declare const mockGetRefundPlan: GetRefundPlan;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { MOCK_ORDERS } from '../../demo/database';
|
|
2
|
+
/**
|
|
3
|
+
* Demo derivation of the runtime `getRefundPlan`. Builds the per-source rows
|
|
4
|
+
* from the mock order's `paymentMethods`, mirroring the runtime's capacity
|
|
5
|
+
* definition (principal + captured tip).
|
|
6
|
+
*
|
|
7
|
+
* HONEST INERTNESS: the demo DB records no prior-refund ledger and no `emv`
|
|
8
|
+
* blocks on its mock captures, so this mock reports `refundedAmount: 0` /
|
|
9
|
+
* `totalRefunded: 0` and `cardNumber: undefined` for every source, and treats
|
|
10
|
+
* `maxRefundable` as the full captured amount. Against real kaching those
|
|
11
|
+
* numbers come from `order.refund[]` and the capture's `emv` JSON. Use this
|
|
12
|
+
* only to shape UI in local/standalone mode — never to assert real capacity.
|
|
13
|
+
*/
|
|
14
|
+
export const mockGetRefundPlan = async (params) => {
|
|
15
|
+
console.log('[Mock] getRefundPlan called', params);
|
|
16
|
+
const order = params?.orderId ? MOCK_ORDERS.find((o) => o._id === params.orderId) : MOCK_ORDERS[0];
|
|
17
|
+
if (!order) {
|
|
18
|
+
throw new Error(`Order with ID ${params?.orderId} not found`);
|
|
19
|
+
}
|
|
20
|
+
const capturedOf = (pm) => Math.round(pm.amount ?? 0) + Math.round(pm.tip?.amount ?? 0);
|
|
21
|
+
const sources = (order.paymentMethods ?? []).map((pm) => {
|
|
22
|
+
const captured = capturedOf(pm);
|
|
23
|
+
let cardNumber;
|
|
24
|
+
// Demo captures carry no emv, so this is always undefined here; kept to
|
|
25
|
+
// document the runtime's redeem-source card-number extraction.
|
|
26
|
+
if (pm.paymentType === 'redeem' && pm.emv) {
|
|
27
|
+
try {
|
|
28
|
+
cardNumber = JSON.parse(pm.emv)['Card Number'];
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
cardNumber = undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
transactionId: pm.transactionId,
|
|
36
|
+
paymentType: pm.paymentType,
|
|
37
|
+
processor: pm.processor ?? undefined,
|
|
38
|
+
capturedAmount: captured,
|
|
39
|
+
// Demo DB has no refund ledger — nothing has been refunded yet here.
|
|
40
|
+
refundedAmount: 0,
|
|
41
|
+
maxRefundable: captured,
|
|
42
|
+
refundableToSource: pm.paymentType !== 'redeem',
|
|
43
|
+
cardNumber,
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
const totalCaptured = sources.reduce((sum, s) => sum + s.capturedAmount, 0);
|
|
47
|
+
const nonRefundableLiability = order.summary?.nonRevenueTotal ?? 0;
|
|
48
|
+
return {
|
|
49
|
+
success: true,
|
|
50
|
+
orderId: order._id,
|
|
51
|
+
sources,
|
|
52
|
+
// Demo: no prior refunds, so remaining = captured minus the non-revenue load.
|
|
53
|
+
remainingRefundable: Math.max(0, totalCaptured - nonRefundableLiability),
|
|
54
|
+
nonRefundableLiability,
|
|
55
|
+
totalCaptured,
|
|
56
|
+
totalRefunded: 0,
|
|
57
|
+
timestamp: new Date().toISOString(),
|
|
58
|
+
};
|
|
59
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface GetRefundPlanParams {
|
|
2
|
+
/** Order to inspect; defaults to the active order. */
|
|
3
|
+
orderId?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface RefundPlanSource {
|
|
6
|
+
transactionId: string;
|
|
7
|
+
paymentType: string;
|
|
8
|
+
processor?: string;
|
|
9
|
+
/** Captured on this payment (minor units). */
|
|
10
|
+
capturedAmount: number;
|
|
11
|
+
/** Already refunded against this source (minor units). */
|
|
12
|
+
refundedAmount: number;
|
|
13
|
+
/** Remaining refundable on this source (minor units) — the engine's own per-source cap. */
|
|
14
|
+
maxRefundable: number;
|
|
15
|
+
/** False for sources the engine cannot refund to directly (redeem without a gift-card destination). */
|
|
16
|
+
refundableToSource: boolean;
|
|
17
|
+
/** For redeem sources: the card number from the payment entry's emv, when present. */
|
|
18
|
+
cardNumber?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface GetRefundPlanResponse {
|
|
21
|
+
success: boolean;
|
|
22
|
+
orderId: string;
|
|
23
|
+
sources: RefundPlanSource[];
|
|
24
|
+
/** Order-level remaining refundable (minor units) — non-revenue liability already excluded. */
|
|
25
|
+
remainingRefundable: number;
|
|
26
|
+
/** Non-refundable liability (gift-card loads etc., minor units). */
|
|
27
|
+
nonRefundableLiability: number;
|
|
28
|
+
totalCaptured: number;
|
|
29
|
+
totalRefunded: number;
|
|
30
|
+
timestamp: string;
|
|
31
|
+
}
|
|
32
|
+
export type GetRefundPlan = (params?: GetRefundPlanParams) => Promise<GetRefundPlanResponse>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Get Refund Plan Types
|
|
2
|
+
//
|
|
3
|
+
// READ-ONLY capacity query. Exposes the refund engine's OWN per-source and
|
|
4
|
+
// order-level math so flows can PRESENT accurate refund options without
|
|
5
|
+
// re-deriving the numbers client-side (the mutating commands —
|
|
6
|
+
// `processPartialRefund` / `redeemRefund` — re-validate at submit time).
|
|
7
|
+
export {};
|
|
@@ -51,6 +51,8 @@ export interface IntegrationPaymentParams {
|
|
|
51
51
|
metadata?: Record<string, unknown>;
|
|
52
52
|
/** Provider fee in integer MINOR currency units — stored on paymentMethod.processorFee. */
|
|
53
53
|
processorFee?: number;
|
|
54
|
+
/** Override the fulfillment landing on full payment. Omitted: preserve advanced fulfillment, auto-fulfill from draft/pending/on_hold. */
|
|
55
|
+
targetFulfillmentState?: string;
|
|
54
56
|
}
|
|
55
57
|
export type IntegrationPaymentResponse = ExtensionPaymentResponse;
|
|
56
58
|
export type IntegrationPayment = (params: IntegrationPaymentParams) => Promise<IntegrationPaymentResponse>;
|
|
@@ -7,8 +7,8 @@ export interface PartialPaymentParams {
|
|
|
7
7
|
isPercent?: boolean;
|
|
8
8
|
/** If true, opens the split payment UI. */
|
|
9
9
|
openUI?: boolean;
|
|
10
|
-
/** Override the fulfillment
|
|
11
|
-
|
|
10
|
+
/** Override the fulfillment landing on full payment. Omitted: preserve advanced fulfillment, auto-fulfill from draft/pending/on_hold. */
|
|
11
|
+
targetFulfillmentState?: string;
|
|
12
12
|
}
|
|
13
13
|
export interface PartialPaymentResponse {
|
|
14
14
|
success: boolean;
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
export const mockProcessPartialRefund = async (params) => {
|
|
2
|
-
|
|
2
|
+
// The mock has no split-payment modal or order engine, so `openUI` (default
|
|
3
|
+
// true on the real command), `legs` (the headless per-tender allocation) and
|
|
4
|
+
// any per-leg `giftCard` destination (mixed returns) are inert here — the
|
|
5
|
+
// shape is accepted and echoed, nothing else. No gift card is credited.
|
|
6
|
+
console.log("[Mock] processPartialRefund called", {
|
|
7
|
+
...params,
|
|
8
|
+
openUI: params?.openUI ?? true,
|
|
9
|
+
legs: params?.legs ?? null,
|
|
10
|
+
});
|
|
3
11
|
return {
|
|
4
12
|
success: true,
|
|
5
13
|
refundId: 'mock_refund_' + Date.now(),
|
|
@@ -1,15 +1,113 @@
|
|
|
1
1
|
import type { CFTransitionResult } from "../../common-types/order-state";
|
|
2
2
|
export interface ProcessPartialRefundParams {
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Optional refund reason.
|
|
5
|
+
*
|
|
6
|
+
* KNOWN LIMITATION: not currently persisted on the `Refund` doc or the
|
|
7
|
+
* state-event audit row via this command — the runtime falls back to a
|
|
8
|
+
* fixed 'partial-refund' label instead. Unlike `redeemRefund`, whose
|
|
9
|
+
* `reason` IS recorded. See the README's "Known limitation" section.
|
|
10
|
+
*/
|
|
4
11
|
reason?: string;
|
|
5
12
|
/** Optional: specify which order to refund (sets it as active). */
|
|
6
13
|
orderId?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Controls the refund UI for a MULTI-TENDER order (one paid across more
|
|
16
|
+
* than one payment method). Defaults to `true`.
|
|
17
|
+
*
|
|
18
|
+
* - `true` (default, back-compat): the POS raises its split-payment refund
|
|
19
|
+
* modal so the cashier allocates the refund across the original payment
|
|
20
|
+
* sources; `processPartialRefund` returns without committing and the
|
|
21
|
+
* modal drives the commit.
|
|
22
|
+
* - `false`: no modal is raised — the refund is committed headlessly
|
|
23
|
+
* against the planner's default proportional allocation across those
|
|
24
|
+
* sources (all cash-rounding invariants preserved). Use this when your
|
|
25
|
+
* flow renders its own refund UI and needs a fully headless multi-tender
|
|
26
|
+
* partial refund.
|
|
27
|
+
*
|
|
28
|
+
* Has no effect on single-tender orders (already headless — there is
|
|
29
|
+
* nothing to allocate).
|
|
30
|
+
*/
|
|
31
|
+
openUI?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Explicit per-tender allocation for the refund — the headless replacement
|
|
34
|
+
* for choosing, in the split-payment refund modal, WHICH original payment
|
|
35
|
+
* each refunded dollar returns to. Each entry names an original payment by
|
|
36
|
+
* its `transactionId` and the amount, **in minor units** (cents), to return
|
|
37
|
+
* to that source.
|
|
38
|
+
*
|
|
39
|
+
* Requires `openUI: false` — with the modal path (`openUI` omitted/`true`)
|
|
40
|
+
* the modal owns allocation and `legs` are ignored. Validation:
|
|
41
|
+
* - Σ of all `amount`s **must equal the allocatable refund budget** —
|
|
42
|
+
* `min(the refund total computed from the selected items, Σ of each
|
|
43
|
+
* source's remaining refundable capacity)`; on a full selection this is
|
|
44
|
+
* the captured total and the cash-rounding gap is auto-stamped (a
|
|
45
|
+
* mismatch throws; nothing is committed);
|
|
46
|
+
* - the amounts **aggregated per source** must be ≤ that source's remaining
|
|
47
|
+
* refundable capacity (over-cap throws, naming the source);
|
|
48
|
+
* - a **zero** `amount` entry is IGNORED — dropped like an omitted row,
|
|
49
|
+
* matching the modal (which let a cashier leave a tender at 0 and filtered
|
|
50
|
+
* it at commit) — while a **negative** `amount` is rejected;
|
|
51
|
+
* - an unknown `transactionId` throws, naming it.
|
|
52
|
+
*
|
|
53
|
+
* MIXED RETURNS — set `giftCard` on a leg to land that leg's amount on a
|
|
54
|
+
* gift-card / store-credit tender instead of returning it to the source. A
|
|
55
|
+
* single `legs` array may freely mix source-return legs and `giftCard` legs
|
|
56
|
+
* (some money back to the original tenders, the rest onto a card). An
|
|
57
|
+
* all-`giftCard` staging is the `redeemRefund` equivalent through this path.
|
|
58
|
+
* **Credit-first:** the flow must credit the card for the sum of all
|
|
59
|
+
* `giftCard` legs BEFORE calling; on any throw nothing was recorded — reverse
|
|
60
|
+
* the credit.
|
|
61
|
+
*
|
|
62
|
+
* Omit `legs` to keep the default proportional allocation across sources.
|
|
63
|
+
* See "Choosing which payments to refund to" and "Mixed returns" in the README.
|
|
64
|
+
*/
|
|
65
|
+
legs?: {
|
|
66
|
+
/** `transactionId` of the original payment this leg draws from. */
|
|
67
|
+
transactionId: string;
|
|
68
|
+
/** Amount for this leg, in minor units (cents). `0` is ignored (dropped
|
|
69
|
+
* like an omitted row); a negative value is rejected. */
|
|
70
|
+
amount: number;
|
|
71
|
+
/**
|
|
72
|
+
* When set, this leg's amount lands on the gift-card / store-credit
|
|
73
|
+
* ("redeem") tender instead of returning to the source. The leg still
|
|
74
|
+
* draws from `transactionId` for capacity/audit — only the landing
|
|
75
|
+
* tender changes.
|
|
76
|
+
*
|
|
77
|
+
* CREDIT-FIRST: the flow must have already credited the card for the sum
|
|
78
|
+
* of all `giftCard` legs before calling; on any throw nothing was
|
|
79
|
+
* recorded and the caller must reverse that credit. `referenceId` is
|
|
80
|
+
* required when `giftCard` is present.
|
|
81
|
+
*/
|
|
82
|
+
giftCard?: {
|
|
83
|
+
/** Card/account id the flow already credited (stored raw). */
|
|
84
|
+
referenceId: string;
|
|
85
|
+
/** Provider/program name. Defaults to `giftCard`. */
|
|
86
|
+
processor?: string;
|
|
87
|
+
/** Human label for the destination tender. */
|
|
88
|
+
label?: string;
|
|
89
|
+
};
|
|
90
|
+
}[];
|
|
7
91
|
/** Optional items to refund. */
|
|
8
92
|
items?: {
|
|
9
93
|
/** internalId or variantId or customSaleId. */
|
|
10
94
|
itemKey: string;
|
|
11
95
|
quantity: number;
|
|
12
96
|
type?: 'product' | 'customSale' | 'fee' | 'tip';
|
|
97
|
+
/**
|
|
98
|
+
* Per-item stock disposition for a refunded **product** line — the
|
|
99
|
+
* headless equivalent of the old refund popup's per-row restock/damaged
|
|
100
|
+
* dropdown. Recorded on the persisted refund line so hub-side inventory
|
|
101
|
+
* ingest knows whether the returned units go back on the shelf.
|
|
102
|
+
*
|
|
103
|
+
* - `'RESTOCK'` (default when omitted): units return to sellable stock
|
|
104
|
+
* — the popup's default first option.
|
|
105
|
+
* - `'REFUND_DAMAGE'`: units are written off as damaged, not restocked.
|
|
106
|
+
*
|
|
107
|
+
* Ignored for non-`product` items (custom sales / fees / tips carry no
|
|
108
|
+
* stock action, exactly as the popup only offered it on line items).
|
|
109
|
+
*/
|
|
110
|
+
stockAction?: 'RESTOCK' | 'REFUND_DAMAGE';
|
|
13
111
|
}[];
|
|
14
112
|
}
|
|
15
113
|
export interface ProcessPartialRefundResponse {
|
|
@@ -12,6 +12,8 @@ export interface RedeemPaymentParams {
|
|
|
12
12
|
processor?: string;
|
|
13
13
|
referenceId?: string;
|
|
14
14
|
metadata?: Record<string, unknown>;
|
|
15
|
+
/** Override the fulfillment landing on full payment. Omitted: preserve advanced fulfillment, auto-fulfill from draft/pending/on_hold. */
|
|
16
|
+
targetFulfillmentState?: string;
|
|
15
17
|
}
|
|
16
18
|
export type RedeemPaymentResponse = ExtensionPaymentResponse;
|
|
17
19
|
export type RedeemPayment = (params: RedeemPaymentParams) => Promise<RedeemPaymentResponse>;
|