@final-commerce/command-frame 0.5.1 → 0.6.0-staging.4

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.
Files changed (69) hide show
  1. package/README.md +0 -2
  2. package/dist/CommonTypes.d.ts +6 -6
  3. package/dist/CommonTypes.js +2 -2
  4. package/dist/actions/add-cart-discount/mock.d.ts +1 -1
  5. package/dist/actions/add-cart-discount/mock.js +8 -8
  6. package/dist/actions/add-cart-fee/mock.d.ts +1 -1
  7. package/dist/actions/add-cart-fee/mock.js +8 -8
  8. package/dist/actions/add-product-to-cart/mock.d.ts +1 -1
  9. package/dist/actions/add-product-to-cart/mock.js +24 -10
  10. package/dist/actions/add-product-to-cart/types.d.ts +13 -1
  11. package/dist/actions/adjust-inventory/mock.d.ts +1 -1
  12. package/dist/actions/adjust-inventory/mock.js +6 -6
  13. package/dist/actions/apply-transition/mock.d.ts +1 -1
  14. package/dist/actions/apply-transition/mock.js +8 -8
  15. package/dist/actions/apply-transition/types.d.ts +1 -1
  16. package/dist/actions/authenticate-user/mock.d.ts +1 -1
  17. package/dist/actions/authenticate-user/mock.js +3 -3
  18. package/dist/actions/calculate-refund-total/action.d.ts +1 -1
  19. package/dist/actions/calculate-refund-total/action.js +2 -2
  20. package/dist/actions/can-transition/types.d.ts +1 -1
  21. package/dist/actions/cash-payment/mock.d.ts +1 -1
  22. package/dist/actions/cash-payment/mock.js +10 -10
  23. package/dist/actions/cash-payment/types.d.ts +1 -1
  24. package/dist/actions/extension-payment/mock.d.ts +1 -1
  25. package/dist/actions/extension-payment/mock.js +3 -3
  26. package/dist/actions/extension-payment/types.d.ts +1 -1
  27. package/dist/actions/get-available-transitions/mock.d.ts +2 -7
  28. package/dist/actions/get-available-transitions/mock.js +9 -9
  29. package/dist/actions/get-final-context/mock.d.ts +1 -1
  30. package/dist/actions/get-final-context/mock.js +2 -2
  31. package/dist/actions/get-products/mock.d.ts +1 -1
  32. package/dist/actions/get-products/mock.js +26 -9
  33. package/dist/actions/get-refunds/types.d.ts +1 -1
  34. package/dist/actions/get-remaining-refundable-quantities/action.d.ts +1 -1
  35. package/dist/actions/get-remaining-refundable-quantities/action.js +2 -2
  36. package/dist/actions/get-remaining-refundable-quantities/mock.d.ts +1 -1
  37. package/dist/actions/get-remaining-refundable-quantities/mock.js +2 -2
  38. package/dist/actions/integration-payment/types.d.ts +1 -1
  39. package/dist/actions/park-order/types.d.ts +1 -1
  40. package/dist/actions/partial-payment/mock.d.ts +1 -1
  41. package/dist/actions/partial-payment/mock.js +8 -8
  42. package/dist/actions/partial-payment/types.d.ts +1 -1
  43. package/dist/actions/print/mock.d.ts +1 -1
  44. package/dist/actions/print/mock.js +9 -9
  45. package/dist/actions/print/types.d.ts +6 -6
  46. package/dist/actions/redeem-payment/types.d.ts +1 -1
  47. package/dist/actions/remove-cart-fee/mock.d.ts +1 -1
  48. package/dist/actions/remove-cart-fee/mock.js +7 -7
  49. package/dist/actions/remove-product-from-cart/mock.d.ts +1 -1
  50. package/dist/actions/remove-product-from-cart/mock.js +11 -8
  51. package/dist/actions/resume-parked-order/types.d.ts +1 -1
  52. package/dist/actions/select-all-refund-items/action.d.ts +1 -1
  53. package/dist/actions/select-all-refund-items/action.js +2 -2
  54. package/dist/actions/set-active-product/mock.d.ts +1 -1
  55. package/dist/actions/set-active-product/mock.js +19 -14
  56. package/dist/actions/set-active-refund/types.d.ts +1 -1
  57. package/dist/actions/tap-to-pay-payment/mock.d.ts +1 -1
  58. package/dist/actions/tap-to-pay-payment/mock.js +6 -6
  59. package/dist/actions/tap-to-pay-payment/types.d.ts +1 -1
  60. package/dist/actions/terminal-payment/mock.d.ts +1 -1
  61. package/dist/actions/terminal-payment/mock.js +6 -6
  62. package/dist/actions/terminal-payment/types.d.ts +2 -2
  63. package/dist/actions/update-cart-item-quantity/mock.d.ts +1 -1
  64. package/dist/actions/update-cart-item-quantity/mock.js +23 -15
  65. package/dist/actions/update-cart-item-quantity/types.d.ts +8 -1
  66. package/dist/actions/upsert-custom-table-data/mock.d.ts +1 -1
  67. package/dist/actions/upsert-custom-table-data/mock.js +3 -3
  68. package/dist/demo/database.js +30 -4
  69. package/package.json +2 -2
package/README.md CHANGED
@@ -18,8 +18,6 @@ The library provides three main capabilities:
18
18
 
19
19
  Domain models (orders, cart, customers, products, and related types) are documented in **[Types reference](./src/types/README.md)**.
20
20
 
21
- The order lifecycle — payment × fulfillment state pairs, display labels, the transition guard chain, and the financial invariants — is documented in **[Order state machine reference](./docs/order-state-machine.md)**. Read it before building anything that reads or moves order state (`canTransition`, `getAvailableTransitions`, `applyTransition`, park/void/resume, payments, refunds).
22
-
23
21
  ## Installation
24
22
 
25
23
  ### From npm (public registry)
@@ -1,6 +1,6 @@
1
- export * from "./common-types";
2
- import type { Tax, Tip, Address, MetadataItem, PosDataItem, CartFeeTaxEntry, CartFeeItem, CartDiscountItem, OrderNote, NonRevenueItem, DiscountDetail, FeeDetail, DiscountLineItem, FeeLineItem, LineItem, TipPayment, RefundedTipPayment, PaymentMethod, Discount, CustomFee, Summary, CustomSale, RefundedCustomSale, RefundItem, RefundedLineItem, ActiveStation, ActiveSession, ActiveOutlet, ActiveUser, ActiveUserRole, ActiveOrder, ActiveCart, ActivePark, ActiveCompany, ActiveCustomSales, ActiveProduct, ActiveCustomer, FullProduct, ProductVariant, Inventory, CustomerNote, Attribute, AttributeOption, Category, Transaction, ActiveSplitPayment } from "@final-commerce/common/pos-types";
3
- export { CurrencyCode, ProductType as CFProductType, UserTypes as CFUserTypes } from "@final-commerce/common";
1
+ export * from './common-types';
2
+ import type { Tax, Tip, Address, MetadataItem, PosDataItem, CartFeeTaxEntry, CartFeeItem, CartDiscountItem, OrderNote, NonRevenueItem, DiscountDetail, FeeDetail, DiscountLineItem, FeeLineItem, LineItem, TipPayment, RefundedTipPayment, PaymentMethod, Discount, CustomFee, Summary, CustomSale, RefundedCustomSale, RefundItem, RefundedLineItem, ActiveStation, ActiveSession, ActiveOutlet, ActiveUser, ActiveUserRole, ActiveOrder, ActiveCart, ActivePark, ActiveCompany, ActiveCustomSales, ActiveProduct, ActiveCustomer, FullProduct, ProductVariant, Inventory, CustomerNote, Attribute, AttributeOption, Category, Transaction, ActiveSplitPayment } from '@final-commerce/common/pos-types';
3
+ export { CurrencyCode, ProductType as CFProductType, UserTypes as CFUserTypes } from '@final-commerce/common';
4
4
  /** Open record used as a base for active entities. */
5
5
  export type CFActiveEntity = Record<string, unknown>;
6
6
  export type CFDiscount = Discount;
@@ -52,7 +52,7 @@ export type CFActiveCart = ActiveCart;
52
52
  export type CFActivePark = ActivePark;
53
53
  export type CFActiveCompany = ActiveCompany;
54
54
  /** Tile kinds a SmartGrid cell can represent. Mirrors common's `BaseTile['type']`. */
55
- export type CFTileType = "empty" | "product" | "action" | "category" | "folder" | "back";
55
+ export type CFTileType = 'empty' | 'product' | 'action' | 'category' | 'folder' | 'back';
56
56
  /**
57
57
  * A single positioned cell in a SmartGrid layout. The wire/extension-facing shape
58
58
  * (Render's adapter maps this to/from common's `BaseTile`).
@@ -96,7 +96,7 @@ export interface CFActiveRefundDetails {
96
96
  refundProcessingStatus?: CFRefundProcessingStatus | null;
97
97
  sessionRefundedTotal?: number;
98
98
  }
99
- export type CFProjectName = "kaching" | "Manage";
99
+ export type CFProjectName = 'kaching' | 'Manage';
100
100
  export interface CFContextRender {
101
101
  userId: string | null;
102
102
  companyId: string | null;
@@ -126,7 +126,7 @@ export interface CFContextRender {
126
126
  thousandSeparator: string | null;
127
127
  decimalSeparator: string | null;
128
128
  user: Record<string, unknown> | null;
129
- company: Omit<Record<string, unknown>, "settings"> | null;
129
+ company: Omit<Record<string, unknown>, 'settings'> | null;
130
130
  station: Record<string, unknown> | null;
131
131
  outlet: Record<string, unknown> | null;
132
132
  timestamp: string;
@@ -1,6 +1,6 @@
1
- export * from "./common-types";
1
+ export * from './common-types';
2
2
  // Enums — re-exported from common (single source). CurrencyCode keeps its name;
3
3
  // CFProductType/CFUserTypes alias common's ProductType/UserTypes. Note: common's
4
4
  // UserTypes uses `COMPANY_OWNER` where command-frame's enum previously had `OWNER`
5
5
  // (same value `'owner'`).
6
- export { CurrencyCode, ProductType as CFProductType, UserTypes as CFUserTypes } from "@final-commerce/common";
6
+ export { CurrencyCode, ProductType as CFProductType, UserTypes as CFUserTypes } from '@final-commerce/common';
@@ -1,2 +1,2 @@
1
- import { AddCartDiscount } from "./types";
1
+ import { AddCartDiscount } from './types';
2
2
  export declare const mockAddCartDiscount: AddCartDiscount;
@@ -1,18 +1,18 @@
1
- import { MOCK_CART, mockPublishEvent } from "../../demo/database";
2
- import { percentToFraction, requireMinorUnitsInteger } from "../../demo/units";
1
+ import { MOCK_CART, mockPublishEvent } from '../../demo/database';
2
+ import { percentToFraction, requireMinorUnitsInteger } from '../../demo/units';
3
3
  export const mockAddCartDiscount = async (params) => {
4
- console.log("[Mock] addCartDiscount called", params);
4
+ console.log('[Mock] addCartDiscount called', params);
5
5
  if (params) {
6
6
  // FI-6991: a fixed amount arrives as an INTEGER in MINOR units (500 =
7
7
  // $5.00) and is stored directly; a percent arrives raw (50 = 50%) and is
8
8
  // stored as a fraction (0.5). Same as the real handler.
9
9
  const value = params.isPercent
10
10
  ? percentToFraction(params.amount)
11
- : requireMinorUnitsInteger(params.amount, "Discount amount");
11
+ : requireMinorUnitsInteger(params.amount, 'Discount amount');
12
12
  MOCK_CART.discount = {
13
13
  value,
14
14
  isPercent: params.isPercent,
15
- label: params.label
15
+ label: params.label,
16
16
  };
17
17
  if (params.isPercent) {
18
18
  MOCK_CART.total = MOCK_CART.subtotal * (1 - value);
@@ -24,14 +24,14 @@ export const mockAddCartDiscount = async (params) => {
24
24
  MOCK_CART.remainingBalance = MOCK_CART.total;
25
25
  // Publish cart-discount-added event
26
26
  mockPublishEvent('cart', 'cart-discount-added', {
27
- discount: MOCK_CART.discount
27
+ discount: MOCK_CART.discount,
28
28
  });
29
29
  }
30
30
  return {
31
31
  success: true,
32
32
  amount: params?.amount || 0,
33
33
  isPercent: params?.isPercent || false,
34
- label: params?.label || "",
35
- timestamp: new Date().toISOString()
34
+ label: params?.label || '',
35
+ timestamp: new Date().toISOString(),
36
36
  };
37
37
  };
@@ -1,2 +1,2 @@
1
- import { AddCartFee } from "./types";
1
+ import { AddCartFee } from './types';
2
2
  export declare const mockAddCartFee: AddCartFee;
@@ -1,7 +1,7 @@
1
- import { MOCK_CART } from "../../demo/database";
2
- import { percentToFraction, requireMinorUnitsInteger } from "../../demo/units";
1
+ import { MOCK_CART } from '../../demo/database';
2
+ import { percentToFraction, requireMinorUnitsInteger } from '../../demo/units';
3
3
  export const mockAddCartFee = async (params) => {
4
- console.log("[Mock] addCartFee called", params);
4
+ console.log('[Mock] addCartFee called', params);
5
5
  if (params) {
6
6
  if (!MOCK_CART.customFee)
7
7
  MOCK_CART.customFee = [];
@@ -10,13 +10,13 @@ export const mockAddCartFee = async (params) => {
10
10
  // stored as a fraction (0.5). Same as the real handler.
11
11
  const storedAmount = params.isPercent
12
12
  ? percentToFraction(params.amount)
13
- : requireMinorUnitsInteger(params.amount, "Fee amount");
13
+ : requireMinorUnitsInteger(params.amount, 'Fee amount');
14
14
  MOCK_CART.customFee.push({
15
- label: params.label || "Fee",
15
+ label: params.label || 'Fee',
16
16
  amount: storedAmount,
17
17
  isPercent: params.isPercent || false,
18
18
  applyTaxes: params.applyTaxes || false,
19
- taxTableId: params.taxTableId
19
+ taxTableId: params.taxTableId,
20
20
  });
21
21
  const feeAmount = params.isPercent ? MOCK_CART.subtotal * storedAmount : storedAmount;
22
22
  MOCK_CART.total += feeAmount;
@@ -27,8 +27,8 @@ export const mockAddCartFee = async (params) => {
27
27
  success: true,
28
28
  amount: params?.amount || 0,
29
29
  isPercent: params?.isPercent || false,
30
- label: params?.label || "",
30
+ label: params?.label || '',
31
31
  applyTaxes: params?.applyTaxes || false,
32
- timestamp: new Date().toISOString()
32
+ timestamp: new Date().toISOString(),
33
33
  };
34
34
  };
@@ -1,2 +1,2 @@
1
- import { AddProductToCart } from "./types";
1
+ import { AddProductToCart } from './types';
2
2
  export declare const mockAddProductToCart: AddProductToCart;
@@ -1,15 +1,19 @@
1
- import { MOCK_CART, MOCK_PRODUCTS, mockPublishEvent } from "../../demo/database";
1
+ import { MOCK_CART, MOCK_PRODUCTS, mockPublishEvent } from '../../demo/database';
2
+ import { extendPrice, isValidQuantity, resolveUnit } from '@final-commerce/common';
2
3
  export const mockAddProductToCart = async (params) => {
3
- console.log("[Mock] addProductToCart called", params);
4
+ console.log('[Mock] addProductToCart called', params);
4
5
  const variantId = params?.variantId;
5
6
  const quantity = params?.quantity || 1;
7
+ if (!Number.isFinite(quantity) || quantity <= 0) {
8
+ throw new Error(`${quantity} is not a quantity`);
9
+ }
6
10
  let product = MOCK_PRODUCTS[0]; // Default fallback
7
11
  let variant = product.variants[0];
8
12
  // Determine variant and product from variantId
9
13
  if (variantId) {
10
14
  // Search through all products to find the one containing this variant
11
15
  for (const p of MOCK_PRODUCTS) {
12
- const foundVariant = p.variants.find(v => v._id === variantId);
16
+ const foundVariant = p.variants.find((v) => v._id === variantId);
13
17
  if (foundVariant) {
14
18
  product = p;
15
19
  variant = foundVariant;
@@ -17,12 +21,20 @@ export const mockAddProductToCart = async (params) => {
17
21
  }
18
22
  }
19
23
  }
24
+ // A measured variant carries a resolved unit; the host does the same. What the unit can
25
+ // express is its precision — 1.5 of a kilogram is a sale, 1.5 of a piece is a typo.
26
+ const unit = variant.unitId ? resolveUnit(variant.unitId) : undefined;
27
+ if (!isValidQuantity(quantity, unit ?? { unitId: 'piece', ratioToBase: 1, precision: 0 })) {
28
+ throw new Error(unit
29
+ ? `${quantity} is finer than ${unit.abbreviation} can express (${unit.precision} decimals)`
30
+ : `${product.name} is sold by the piece, so ${quantity} is not a quantity it can sell in`);
31
+ }
20
32
  // Add to MOCK_CART
21
- const internalId = product._id + "_" + Date.now();
33
+ const internalId = product._id + '_' + Date.now();
22
34
  // Process optional fields for mock
23
35
  let note = undefined;
24
36
  if (params?.notes) {
25
- note = Array.isArray(params.notes) ? params.notes.join(", ") : params.notes;
37
+ note = Array.isArray(params.notes) ? params.notes.join(', ') : params.notes;
26
38
  }
27
39
  const activeProduct = {
28
40
  ...product,
@@ -33,16 +45,18 @@ export const mockAddProductToCart = async (params) => {
33
45
  price: Number(variant.price),
34
46
  taxTableId: product.taxTable,
35
47
  stock: variant.inventory?.[0]?.stock || 0,
48
+ ...(unit ? { unit } : {}),
36
49
  images: product.images || [],
37
50
  localQuantity: quantity,
38
51
  sku: variant.sku,
39
- attributes: variant.attributes.map(a => `${a.name}: ${a.value}`).join(", "),
40
- note: note
52
+ attributes: variant.attributes.map((a) => `${a.name}: ${a.value}`).join(', '),
53
+ note: note,
41
54
  // discount/fee could be added here to mock object if CFActiveProduct supports it
42
55
  };
43
56
  MOCK_CART.products.push(activeProduct);
44
- // Recalculate totals
45
- const lineTotal = activeProduct.price * quantity;
57
+ // Recalculate totals. extendPrice, not a raw multiply: a fractional quantity times an
58
+ // integer-minor price leaves fractional cents, which no order may carry.
59
+ const lineTotal = extendPrice(activeProduct.price, quantity);
46
60
  MOCK_CART.subtotal += lineTotal;
47
61
  // Simple total calculation (ignoring taxes/fees for mock simplicity unless needed)
48
62
  MOCK_CART.total += lineTotal;
@@ -57,6 +71,6 @@ export const mockAddProductToCart = async (params) => {
57
71
  internalId: activeProduct.internalId,
58
72
  name: activeProduct.name,
59
73
  quantity: quantity,
60
- timestamp: new Date().toISOString()
74
+ timestamp: new Date().toISOString(),
61
75
  };
62
76
  };
@@ -3,7 +3,19 @@ import type { AddProductFeeParams } from "../add-product-fee/types";
3
3
  export interface AddProductToCartParams {
4
4
  /** ID of the variant to add. */
5
5
  variantId: string;
6
- /** Defaults to 1. */
6
+ /**
7
+ * Defaults to 1. **May be fractional** — a variant sold by weight, volume or length is priced
8
+ * per its own unit and keyed in that unit, so `0.456` kg is a quantity, not a typo.
9
+ *
10
+ * How many decimals are allowed is the variant's own business: `variant.unit.precision` —
11
+ * `3` for a litre, `0` for anything sold by the piece. The engine refuses a quantity finer
12
+ * than that and says which unit it was measured against; do not round, floor or clamp before
13
+ * sending, and never key a quantity in base units to work around it (a per-100g price with a
14
+ * gram count is explicitly not supported).
15
+ *
16
+ * A quantity field should take its step and its decimal count from `variant.unit.precision`,
17
+ * not from a constant.
18
+ */
7
19
  quantity?: number;
8
20
  /** Array of discounts to apply immediately. */
9
21
  discounts?: AddProductDiscountParams[];
@@ -1,2 +1,2 @@
1
- import { AdjustInventory } from "./types";
1
+ import { AdjustInventory } from './types';
2
2
  export declare const mockAdjustInventory: AdjustInventory;
@@ -1,6 +1,6 @@
1
- import { MOCK_PRODUCTS } from "../../demo/database";
1
+ import { MOCK_PRODUCTS } from '../../demo/database';
2
2
  export const mockAdjustInventory = async (params) => {
3
- console.log("[Mock] adjustInventory called", params);
3
+ console.log('[Mock] adjustInventory called', params);
4
4
  let newStock = 0;
5
5
  // Real handler keys off variantId (falling back to the active product's
6
6
  // variant); mirror that here by finding the owning product for the variant.
@@ -8,7 +8,7 @@ export const mockAdjustInventory = async (params) => {
8
8
  const variantId = params.variantId;
9
9
  let variant;
10
10
  for (const product of MOCK_PRODUCTS) {
11
- const match = product.variants.find(v => v._id === variantId);
11
+ const match = product.variants.find((v) => v._id === variantId);
12
12
  if (match) {
13
13
  variant = match;
14
14
  break;
@@ -32,9 +32,9 @@ export const mockAdjustInventory = async (params) => {
32
32
  }
33
33
  return {
34
34
  success: true,
35
- amount: params?.amount || "0",
36
- stockType: params?.stockType || "set",
35
+ amount: params?.amount || '0',
36
+ stockType: params?.stockType || 'set',
37
37
  newStock,
38
- timestamp: new Date().toISOString()
38
+ timestamp: new Date().toISOString(),
39
39
  };
40
40
  };
@@ -1,4 +1,4 @@
1
- import type { ApplyTransitionParams, ApplyTransitionResponse } from "./types";
1
+ import type { ApplyTransitionParams, ApplyTransitionResponse } from './types';
2
2
  /**
3
3
  * Mock implementation: applies all transitions except a known-invalid one
4
4
  * so the demo app can show both successful and blocked responses.
@@ -4,20 +4,20 @@
4
4
  */
5
5
  export const applyTransitionMock = (params) => {
6
6
  const { targetFulfillmentState } = params;
7
- if (targetFulfillmentState === "cancelled") {
7
+ if (targetFulfillmentState === 'cancelled') {
8
8
  return Promise.resolve({
9
9
  result: {
10
10
  allowed: false,
11
- blockedBy: "condition",
12
- guard: "no-cancel-open-order",
13
- reason: "Cannot cancel an order with open items"
14
- }
11
+ blockedBy: 'condition',
12
+ guard: 'no-cancel-open-order',
13
+ reason: 'Cannot cancel an order with open items',
14
+ },
15
15
  });
16
16
  }
17
17
  return Promise.resolve({
18
18
  result: { allowed: true },
19
- from: { payment: "unpaid", fulfillment: "draft" },
20
- to: { payment: "unpaid", fulfillment: targetFulfillmentState },
21
- displayState: `unpaid / ${targetFulfillmentState}`
19
+ from: { payment: 'unpaid', fulfillment: 'draft' },
20
+ to: { payment: 'unpaid', fulfillment: targetFulfillmentState },
21
+ displayState: `unpaid / ${targetFulfillmentState}`,
22
22
  });
23
23
  };
@@ -1,4 +1,4 @@
1
- import type { CFStatePair, CFTransitionResult } from "../../common-types/order-state";
1
+ import type { CFStatePair, CFTransitionResult } from '../../common-types/order-state';
2
2
  export interface ApplyTransitionParams {
3
3
  /** Order to transition. Omit to target the active order — materializing one from the live cart if none exists. */
4
4
  orderId?: string;
@@ -1,2 +1,2 @@
1
- import { AuthenticateUser } from "./types";
1
+ import { AuthenticateUser } from './types';
2
2
  export declare const mockAuthenticateUser: AuthenticateUser;
@@ -1,12 +1,12 @@
1
1
  export const mockAuthenticateUser = async (params) => {
2
- console.log("[Mock] authenticateUser called", params);
2
+ console.log('[Mock] authenticateUser called', params);
3
3
  // Headless path requires both userId and pin together (mirrors the handler guard).
4
4
  if ((params?.userId || params?.pin) && !(params?.userId && params?.pin)) {
5
- throw new Error("userId and pin must be provided together");
5
+ throw new Error('userId and pin must be provided together');
6
6
  }
7
7
  return {
8
8
  success: true,
9
9
  roleIds: params?.roleIds || [],
10
- timestamp: new Date().toISOString()
10
+ timestamp: new Date().toISOString(),
11
11
  };
12
12
  };
@@ -2,5 +2,5 @@
2
2
  * Calculate refund total action
3
3
  * Calls the calculateRefundTotal action on the parent window
4
4
  */
5
- import type { CalculateRefundTotal } from "./types";
5
+ import type { CalculateRefundTotal } from './types';
6
6
  export declare const calculateRefundTotal: CalculateRefundTotal;
@@ -2,7 +2,7 @@
2
2
  * Calculate refund total action
3
3
  * Calls the calculateRefundTotal action on the parent window
4
4
  */
5
- import { commandFrameClient } from "../../client";
5
+ import { commandFrameClient } from '../../client';
6
6
  export const calculateRefundTotal = async (params) => {
7
- return await commandFrameClient.call("calculateRefundTotal", params);
7
+ return await commandFrameClient.call('calculateRefundTotal', params);
8
8
  };
@@ -1,4 +1,4 @@
1
- import type { CFStatePair, CFTransitionResult } from "../../common-types/order-state";
1
+ import type { CFStatePair, CFTransitionResult } from '../../common-types/order-state';
2
2
  export interface CanTransitionParams {
3
3
  /** Order to evaluate. Defaults to the active order; if there is no active order (or none matches), evaluates as a brand-new order (from = null). */
4
4
  orderId?: string;
@@ -1,2 +1,2 @@
1
- import { CashPayment } from "./types";
1
+ import { CashPayment } from './types';
2
2
  export declare const mockCashPayment: CashPayment;
@@ -1,6 +1,6 @@
1
- import { applyMockPayment, MOCK_CART } from "../../demo/database";
1
+ import { applyMockPayment, MOCK_CART } from '../../demo/database';
2
2
  export const mockCashPayment = async (params) => {
3
- console.log("[Mock] cashPayment called", params);
3
+ console.log('[Mock] cashPayment called', params);
4
4
  const fail = (reason) => {
5
5
  console.warn(`[Mock] cashPayment rejected: ${reason}`);
6
6
  return {
@@ -9,17 +9,17 @@ export const mockCashPayment = async (params) => {
9
9
  openChangeCalculator: params?.openChangeCalculator ?? false,
10
10
  change: 0,
11
11
  cashRounding: 0,
12
- paymentType: "cash",
12
+ paymentType: 'cash',
13
13
  order: null,
14
14
  timestamp: new Date().toISOString(),
15
15
  saleFinalized: false,
16
- remainingBalance: 0
16
+ remainingBalance: 0,
17
17
  };
18
18
  };
19
19
  // Contract: `amount` is required, integer MINOR currency units — same
20
20
  // scale as the mock cart's totals, so comparisons are direct.
21
21
  if (params?.amount === undefined || params.amount === null) {
22
- return fail("amount is required (integer minor currency units)");
22
+ return fail('amount is required (integer minor currency units)');
23
23
  }
24
24
  const balanceDue = MOCK_CART.amountToBeCharged ?? MOCK_CART.total;
25
25
  if (params.amount > balanceDue) {
@@ -38,16 +38,16 @@ export const mockCashPayment = async (params) => {
38
38
  try {
39
39
  const input = window.prompt(`Amount due: $${(params.amount / 100).toFixed(2)}\nEnter amount tendered (dollars):`, (params.amount / 100).toFixed(2));
40
40
  if (input === null)
41
- return fail("cancelled");
41
+ return fail('cancelled');
42
42
  const tendered = parseFloat(input);
43
43
  if (!isNaN(tendered))
44
44
  change = Math.round(tendered * 100) - params.amount;
45
45
  }
46
46
  catch (e) {
47
- console.warn("Could not open prompt/alert (possibly in non-interactive environment)", e);
47
+ console.warn('Could not open prompt/alert (possibly in non-interactive environment)', e);
48
48
  }
49
49
  }
50
- const order = applyMockPayment(params.amount, "cash", "cash");
50
+ const order = applyMockPayment(params.amount, 'cash', 'cash');
51
51
  return {
52
52
  success: true,
53
53
  amount: params.amount,
@@ -55,10 +55,10 @@ export const mockCashPayment = async (params) => {
55
55
  change,
56
56
  tenderedAmount: params.tenderedAmount,
57
57
  cashRounding: 0,
58
- paymentType: "cash",
58
+ paymentType: 'cash',
59
59
  order,
60
60
  timestamp: new Date().toISOString(),
61
61
  saleFinalized: true,
62
- remainingBalance: 0
62
+ remainingBalance: 0,
63
63
  };
64
64
  };
@@ -1,4 +1,4 @@
1
- import { CFOrder } from "../../CommonTypes";
1
+ import { CFOrder } from '../../CommonTypes';
2
2
  export interface CashPaymentParams {
3
3
  /**
4
4
  * The amount to pay with this tender, in integer MINOR currency units
@@ -1,2 +1,2 @@
1
- import { ExtensionPayment } from "./types";
1
+ import { ExtensionPayment } from './types';
2
2
  export declare const mockExtensionPayment: ExtensionPayment;
@@ -1,6 +1,6 @@
1
- import { MOCK_ORDERS } from "../../demo/database";
1
+ import { MOCK_ORDERS } from '../../demo/database';
2
2
  export const mockExtensionPayment = async (params) => {
3
- const paymentType = params?.paymentType ?? "redeem";
3
+ const paymentType = params?.paymentType ?? 'redeem';
4
4
  return {
5
5
  success: true,
6
6
  amount: params?.amount ?? null,
@@ -10,6 +10,6 @@ export const mockExtensionPayment = async (params) => {
10
10
  change: 0,
11
11
  cashRounding: 0,
12
12
  saleFinalized: true,
13
- remainingBalance: 0
13
+ remainingBalance: 0,
14
14
  };
15
15
  };
@@ -1,4 +1,4 @@
1
- import { CFOrder } from "../../CommonTypes";
1
+ import { CFOrder } from '../../CommonTypes';
2
2
  /** Params for extension-initiated payments; host routes by `paymentType`. */
3
3
  export interface ExtensionPaymentParams {
4
4
  paymentType: string;
@@ -2,12 +2,7 @@ import type { GetAvailableTransitions } from './types';
2
2
  /**
3
3
  * Mock implementation: returns a fixed set of plausible transitions
4
4
  * so the demo app has data to render. Mirrors an UNPAID order under the
5
- * default (empty cross-axis rules) config.
6
- *
7
- * FULFILLMENT-AXIS ONLY (common 2.1.4 contract): the payment axis never
8
- * appears as an available transition — it moves exclusively through the
9
- * money operations (pay / refund / void), which derive their own landing
10
- * pairs. Every row keeps `to.payment` equal to the order's current payment
11
- * state, exactly like the host's `getAvailableTransitions`.
5
+ * default (empty cross-axis rules) config: fulfillment can advance without
6
+ * payment (FI-6383), plus the classic park/void moves.
12
7
  */
13
8
  export declare const getAvailableTransitionsMock: GetAvailableTransitions;
@@ -1,13 +1,8 @@
1
1
  /**
2
2
  * Mock implementation: returns a fixed set of plausible transitions
3
3
  * so the demo app has data to render. Mirrors an UNPAID order under the
4
- * default (empty cross-axis rules) config.
5
- *
6
- * FULFILLMENT-AXIS ONLY (common 2.1.4 contract): the payment axis never
7
- * appears as an available transition — it moves exclusively through the
8
- * money operations (pay / refund / void), which derive their own landing
9
- * pairs. Every row keeps `to.payment` equal to the order's current payment
10
- * state, exactly like the host's `getAvailableTransitions`.
4
+ * default (empty cross-axis rules) config: fulfillment can advance without
5
+ * payment (FI-6383), plus the classic park/void moves.
11
6
  */
12
7
  export const getAvailableTransitionsMock = () => Promise.resolve({
13
8
  transitions: [
@@ -27,8 +22,13 @@ export const getAvailableTransitionsMock = () => Promise.resolve({
27
22
  conditions: [{ met: true, description: 'Order is open' }],
28
23
  },
29
24
  {
30
- to: { payment: 'unpaid', fulfillment: 'cancelled' },
31
- displayLabel: 'Cancel Order',
25
+ to: { payment: 'paid', fulfillment: 'fulfilled' },
26
+ displayLabel: 'Complete Payment',
27
+ conditions: [{ met: true, description: 'Balance due > 0' }],
28
+ },
29
+ {
30
+ to: { payment: 'voided', fulfillment: 'cancelled' },
31
+ displayLabel: 'Void Order',
32
32
  conditions: [
33
33
  { met: true, description: 'Order exists' },
34
34
  { met: false, description: 'No payments captured (mock: skipped)' },
@@ -1,2 +1,2 @@
1
- import { GetFinalContext } from "./types";
1
+ import { GetFinalContext } from './types';
2
2
  export declare const mockGetFinalContext: GetFinalContext;
@@ -1,6 +1,6 @@
1
1
  export const mockGetFinalContext = async () => {
2
- console.log("[Mock] getFinalContext called");
2
+ console.log('[Mock] getFinalContext called');
3
3
  return {
4
- projectName: "kaching"
4
+ projectName: 'kaching',
5
5
  };
6
6
  };
@@ -1,2 +1,2 @@
1
- import { GetProducts } from "./types";
1
+ import { GetProducts } from './types';
2
2
  export declare const mockGetProducts: GetProducts;
@@ -1,36 +1,53 @@
1
- import { MOCK_PRODUCTS, safeSerialize } from "../../demo/database";
1
+ import { MOCK_PRODUCTS, safeSerialize } from '../../demo/database';
2
+ import { resolveUnit } from '@final-commerce/common';
2
3
  export const mockGetProducts = async (params) => {
3
- console.log("[Mock] getProducts called", params);
4
+ console.log('[Mock] getProducts called', params);
4
5
  // Simple filter simulation
5
6
  let products = MOCK_PRODUCTS;
6
7
  const query = params?.query || {};
7
8
  if (query.searchValue) {
8
9
  const search = String(query.searchValue).toLowerCase();
9
- products = products.filter(p => p.name.toLowerCase().includes(search) ||
10
- p.sku?.toLowerCase().includes(search));
10
+ products = products.filter((p) => p.name.toLowerCase().includes(search) || p.sku?.toLowerCase().includes(search));
11
11
  }
12
12
  if (query.categories) {
13
13
  // Handle categories filter: { $in: [...] } or direct string
14
14
  const catFilter = query.categories;
15
15
  if (typeof catFilter === 'string') {
16
- products = products.filter(p => (p.categories || []).some(c => c.name === catFilter));
16
+ products = products.filter((p) => (p.categories || []).some((c) => c.name === catFilter));
17
17
  }
18
18
  else if (typeof catFilter === 'object' && '$in' in catFilter) {
19
19
  const inList = catFilter.$in;
20
- products = products.filter(p => (p.categories || []).some(c => inList.includes(c.name)));
20
+ products = products.filter((p) => (p.categories || []).some((c) => inList.includes(c.name)));
21
21
  }
22
22
  else if (typeof catFilter === 'object' && '$contains' in catFilter) {
23
23
  const containsVal = catFilter.$contains;
24
- products = products.filter(p => (p.categories || []).some(c => c.name.includes(containsVal)));
24
+ products = products.filter((p) => (p.categories || []).some((c) => c.name.includes(containsVal)));
25
25
  }
26
26
  }
27
27
  const total = products.length;
28
28
  const offset = params?.offset ?? 0;
29
29
  const limit = params?.limit ?? 100;
30
30
  const paged = products.slice(offset, offset + limit);
31
+ // Mirror the real host's attachUnits: storage carries only `unitId`, the wire shape
32
+ // adds the RESOLVED unit (abbreviation, ratioToBase, precision) so a card can label
33
+ // stock and a quantity input can pick its step. Unresolvable ids pass through bare
34
+ // rather than killing the page — same stance as the host's per-variant catch.
35
+ const withUnits = safeSerialize(paged).map((product) => ({
36
+ ...product,
37
+ variants: (product.variants ?? []).map((variant) => {
38
+ if (!variant.unitId)
39
+ return variant;
40
+ try {
41
+ return { ...variant, unit: resolveUnit(variant.unitId) };
42
+ }
43
+ catch {
44
+ return variant;
45
+ }
46
+ }),
47
+ }));
31
48
  return {
32
- products: safeSerialize(paged),
49
+ products: withUnits,
33
50
  total,
34
- timestamp: new Date().toISOString()
51
+ timestamp: new Date().toISOString(),
35
52
  };
36
53
  };
@@ -1,4 +1,4 @@
1
- import { CFRefundItem } from "../../CommonTypes";
1
+ import { CFRefundItem } from '../../CommonTypes';
2
2
  export interface GetRefundsParams {
3
3
  orderId?: string;
4
4
  sessionId?: string;