@final-commerce/command-frame 0.3.2-beta.1 → 0.4.0-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 CHANGED
@@ -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 "@final-commerce/command-frame";
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 "@final-commerce/command-frame";
88
+ import { ManageClient } from '@final-commerce/command-frame';
89
89
 
90
90
  const client = new ManageClient();
91
91
  const context = await client.getContext();
@@ -96,17 +96,17 @@ const context = await client.getContext();
96
96
  The pub/sub system allows iframe extensions to subscribe to topics and receive real-time events published by the host (Render). Subscriptions are **page-scoped** -- they fire only while the iframe is mounted on the current page.
97
97
 
98
98
  - **[Pub/Sub Documentation](./src/pubsub/README.md)**
99
- - **Topics:** Cart (16), Customers (8), Orders (4), Payments (2), Products (4), Refunds (4), Print (3), Custom Tables (3), Outlet (2), Station (2), Session (2), Users (2).
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 "@final-commerce/command-frame";
102
+ import { topics } from '@final-commerce/command-frame';
103
103
 
104
- const subscriptionId = topics.subscribe("cart", event => {
105
- console.log("Cart event:", event.type, event.data);
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("cart", subscriptionId);
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 "@final-commerce/command-frame";
121
+ import { hooks } from '@final-commerce/command-frame';
122
122
 
123
123
  hooks.register(
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" }
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("my-extension:cart-log");
135
+ hooks.unregister('my-extension:cart-log');
136
136
  ```
137
137
 
138
138
  ## Interceptors
@@ -147,14 +147,14 @@ 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
- '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' }
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
 
@@ -171,13 +171,21 @@ interceptors.register(
171
171
  Exported APIs: `installExtensionRefundListener`, `EXTENSION_REFUND_REQUEST_ACTION`, types **`ExtensionRefundParams`** / **`ExtensionRefundResponse`**.
172
172
 
173
173
  ```typescript
174
- import { installExtensionRefundListener, type ExtensionRefundParams, type ExtensionRefundResponse } from "@final-commerce/command-frame";
175
-
176
- const unsubscribe = installExtensionRefundListener(async (params: ExtensionRefundParams): Promise<ExtensionRefundResponse> => {
174
+ import {
175
+ installExtensionRefundListener,
176
+ type ExtensionRefundParams,
177
+ type ExtensionRefundResponse,
178
+ } from '@final-commerce/command-frame';
179
+
180
+ const unsubscribe = installExtensionRefundListener(
181
+ async (params: ExtensionRefundParams): Promise<ExtensionRefundResponse> => {
177
182
  // params.paymentType === "redeem", params.amount in major currency units, params.saleId, params.processor, etc.
178
183
  const ok = await myGiftCardProvider.refund(params);
179
- return ok ? { success: true, extensionTransactionId: ok.providerRefundId } : { success: false, error: "Refund declined" };
180
- });
184
+ return ok
185
+ ? { success: true, extensionTransactionId: ok.providerRefundId }
186
+ : { success: false, error: 'Refund declined' };
187
+ },
188
+ );
181
189
 
182
190
  // on teardown (optional)
183
191
  // unsubscribe();
@@ -1,6 +1,6 @@
1
1
  import type { ApplyTransitionParams, ApplyTransitionResponse } from "./types";
2
2
  /**
3
- * Mock implementation: applies all transitions except a few known-invalid ones
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 few known-invalid ones
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 { to } = params;
7
- if (to.payment === "refunded" && to.fulfillment === "draft") {
6
+ const { targetFulfillmentState } = params;
7
+ if (targetFulfillmentState === "cancelled") {
8
8
  return Promise.resolve({
9
9
  result: {
10
10
  allowed: false,
11
- blockedBy: "financial_invariant",
12
- guard: "no-refund-in-draft",
13
- reason: "Cannot refund an order that is still in draft"
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: `${to.payment} / ${to.fulfillment}`
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: string;
5
- /** Target state pair. */
6
- to: CFStatePair;
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 state after full payment. Render resolves the cascade. */
19
- checkoutFulfillmentTarget?: string;
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 { GetAvailableTransitionsParams, GetAvailableTransitionsResponse } from "./types";
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: (_params: GetAvailableTransitionsParams) => Promise<GetAvailableTransitionsResponse>;
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 = async (_params) => ({
7
+ export const getAvailableTransitionsMock = () => Promise.resolve({
6
8
  transitions: [
7
9
  {
8
- to: { payment: "refunded", fulfillment: "returned" },
9
- displayLabel: "Full Refund",
10
- conditions: [{ met: true, description: "Order is paid" }],
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: "partially_refunded", fulfillment: "partially_returned" },
14
- displayLabel: "Partial Refund",
15
- conditions: [{ met: true, description: "Order is paid" }],
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
  });
@@ -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 state after full payment. Render resolves the cascade. */
11
- checkoutFulfillmentTarget?: string;
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;
@@ -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>;
@@ -12,9 +12,9 @@ export interface TerminalPaymentParams {
12
12
  */
13
13
  amount: number;
14
14
  /** "Bluetooth" or "Cloud". Defaults to "Cloud". */
15
- paymentType?: 'Bluetooth' | 'Cloud';
16
- /** Override the fulfillment state after full payment. Render resolves the cascade. */
17
- checkoutFulfillmentTarget?: string;
15
+ paymentType?: "Bluetooth" | "Cloud";
16
+ /** Override the fulfillment landing on full payment. Omitted: preserve advanced fulfillment, auto-fulfill from draft/pending/on_hold. */
17
+ targetFulfillmentState?: string;
18
18
  }
19
19
  export interface TerminalPaymentResponse {
20
20
  success: boolean;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Void order action
3
+ * Calls the voidOrder action on the parent window
4
+ */
5
+ import type { VoidOrder } from './types';
6
+ export declare const voidOrder: VoidOrder;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Void order action
3
+ * Calls the voidOrder action on the parent window
4
+ */
5
+ import { commandFrameClient } from '../../client';
6
+ export const voidOrder = async (params) => {
7
+ return await commandFrameClient.call('voidOrder', params);
8
+ };
@@ -0,0 +1,2 @@
1
+ import { VoidOrder } from './types';
2
+ export declare const mockVoidOrder: VoidOrder;
@@ -0,0 +1,23 @@
1
+ import { MOCK_ORDERS, mockPublishEvent } from '../../demo/database';
2
+ // Payment states in which an order is still open — mirrors the runtime gate
3
+ // (kaching's OPEN_PAYMENT_STATES / handler.ts). Anything outside this set
4
+ // (paid, partially_refunded, refunded, voided, or unknown) is ORDER_NOT_VOIDABLE.
5
+ const OPEN_PAYMENT_STATES = ['unpaid', 'payment_pending', 'partially_paid'];
6
+ export const mockVoidOrder = async (params) => {
7
+ console.log('[Mock] voidOrder called', params);
8
+ const order = params?.orderId ? MOCK_ORDERS.find((o) => o._id === params.orderId) : MOCK_ORDERS[0];
9
+ if (!order) {
10
+ throw new Error(`Order with ID ${params?.orderId} not found`);
11
+ }
12
+ const orderId = order._id;
13
+ // Only OPEN orders are voidable — mirror the runtime gate.
14
+ if (!OPEN_PAYMENT_STATES.includes(order.paymentState)) {
15
+ throw new Error(`ORDER_NOT_VOIDABLE: order ${orderId} is '${order.paymentState}' — use the refund flow for completed orders`);
16
+ }
17
+ const hasCapturedLegs = (order.paymentMethods?.length ?? 0) > 0;
18
+ const outcome = hasCapturedLegs ? 'refunded' : 'voided';
19
+ order.paymentState = outcome;
20
+ order.fulfillmentState = 'cancelled';
21
+ mockPublishEvent('orders', 'order-voided', { orderId, outcome, reason: params?.reason });
22
+ return { success: true, orderId, outcome, timestamp: new Date().toISOString() };
23
+ };
@@ -0,0 +1,27 @@
1
+ import type { CFTransitionResult } from '../../common-types/order-state';
2
+ export type VoidOrderOutcome = 'voided' | 'refunded';
3
+ export interface VoidOrderParams {
4
+ /** Order to void; defaults to the active order. */
5
+ orderId?: string;
6
+ /**
7
+ * Optional cashier-facing reason. On a pure void, recorded on the void audit
8
+ * row and carried on the `order-voided` event. On the refund branch it rides
9
+ * the event only — the refund dispatcher does not consume it.
10
+ */
11
+ reason?: string;
12
+ }
13
+ export interface VoidOrderResponse {
14
+ success: boolean;
15
+ orderId: string;
16
+ /**
17
+ * `voided` — nothing was captured; pure state transition to voided × cancelled.
18
+ * `refunded` — captured split legs were refunded to their original tenders;
19
+ * order lands refunded × cancelled (financially equivalent to a
20
+ * void, but the capture + payout stay on the audit trail).
21
+ */
22
+ outcome: VoidOrderOutcome;
23
+ timestamp: string;
24
+ /** Present when the state machine blocked or forced the transition. */
25
+ transitionResult?: CFTransitionResult;
26
+ }
27
+ export type VoidOrder = (params?: VoidOrderParams) => Promise<VoidOrderResponse>;
@@ -0,0 +1 @@
1
+ export {};
@@ -2,8 +2,8 @@
2
2
  * Mock Database for Standalone/Demo Mode
3
3
  * Stores mock data that mimics the Render environment
4
4
  */
5
- import { CFActiveCompany, CFActiveUser, CFActiveStation, CFActiveOutlet, CFActiveOrder, CFCustomer, CFProduct, CFActiveCart, CFCategory, CFActiveProduct, CFSession, CFActiveRefundDetails, CFSmartGridLayout } from "../CommonTypes";
6
- export * from "./mocks";
5
+ import { CFActiveCompany, CFActiveUser, CFActiveStation, CFActiveOutlet, CFActiveOrder, CFCustomer, CFProduct, CFActiveCart, CFCategory, CFActiveProduct, CFSession, CFActiveRefundDetails, CFSmartGridLayout } from '../CommonTypes';
6
+ export * from './mocks';
7
7
  /** Replace mock catalog / context data in place (same array references mock handlers use). */
8
8
  export interface MockDatabaseConfig {
9
9
  company?: Partial<CFActiveCompany>;
@@ -51,6 +51,7 @@ export declare const MOCK_PRODUCT_BLACK_GARLIC: import("@final-commerce/common/p
51
51
  export declare const MOCK_ORDER_1: CFActiveOrder;
52
52
  export declare const MOCK_ORDER_2: CFActiveOrder;
53
53
  export declare const MOCK_ORDER_3: CFActiveOrder;
54
+ export declare const MOCK_ORDER_4: CFActiveOrder;
54
55
  export declare const MOCK_PARKED_ORDER_1: CFActiveOrder;
55
56
  export declare const MOCK_PARKED_ORDER_2: CFActiveOrder;
56
57
  export declare const MOCK_USERS: import("@final-commerce/common/pos-types").ActiveUser[];