@final-commerce/command-frame 0.6.0-staging3.2 → 0.6.0-staging3.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.
@@ -1,5 +1,5 @@
1
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, ResolvedModifier, ResolvedModifierChoice, ModifierSelectionType, ModifierSelection, ModifierChoiceSelection, OrderLineItemModifier, CustomerNote, Attribute, AttributeOption, Category, Transaction, ActiveSplitPayment } from '@final-commerce/common/pos-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, ResolvedModifier, ResolvedModifierChoice, ModifierSelectionType, ModifierSelection, ModifierChoiceSelection, CartLineModifier, OrderLineItemModifier, CustomerNote, Attribute, AttributeOption, Category, Transaction, ActiveSplitPayment } from '@final-commerce/common/pos-types';
3
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>;
@@ -27,6 +27,8 @@ export type CFOrderLineItemModifier = OrderLineItemModifier;
27
27
  /** Cart-line modifier answer (pre-pricing) — `CFActiveProduct.modifierSelections`. */
28
28
  export type CFModifierSelection = ModifierSelection;
29
29
  export type CFModifierChoiceSelection = ModifierChoiceSelection;
30
+ /** One selected choice PRICED onto a cart line — `CFActiveProduct.modifiers` (not the resolved stack). */
31
+ export type CFCartLineModifier = CartLineModifier;
30
32
  export type CFActiveProduct = ActiveProduct;
31
33
  export type CFCustomer = ActiveCustomer;
32
34
  export type CFActiveCustomer = ActiveCustomer;
@@ -1,4 +1,4 @@
1
- import { MOCK_CART, MOCK_PRODUCTS, mockPublishEvent } from '../../demo/database';
1
+ import { MOCK_CART, MOCK_PRODUCTS, buildCartLineModifiers, mockPublishEvent } from '../../demo/database';
2
2
  import { extendPrice, isValidQuantity, resolveUnit } from '@final-commerce/common';
3
3
  export const mockAddProductToCart = async (params) => {
4
4
  console.log('[Mock] addProductToCart called', params);
@@ -53,11 +53,17 @@ export const mockAddProductToCart = async (params) => {
53
53
  note: note,
54
54
  // discount/fee could be added here to mock object if CFActiveProduct supports it
55
55
  };
56
- // Mock modifier handling: echo selections onto the line; a real host resolves the
57
- // product's modifiers, validates required/min/max in units, and prices each choice
58
- // (fee-level money: not in grossSales, not reduced by the product discount).
56
+ // The spread above copied the PRODUCT's `modifiers` a ResolvedModifier[] menu into a
57
+ // field that means "the choices this line carries" (CartLineModifier[]). Two different
58
+ // shapes, one name. Drop it before anything reads the line, or every surface renders the
59
+ // whole catalogue as if the cashier had picked all of it.
60
+ delete activeProduct.modifiers;
61
+ // Keep the raw answers AND the priced rows, the same pair the real host writes. The cart
62
+ // TOTAL is deliberately left alone: like a product fee (see add-product-fee), per-line
63
+ // money is not accumulated by this mock — `total` stays Σ extendPrice(price, quantity).
59
64
  if (params?.modifiers?.length) {
60
65
  activeProduct.modifierSelections = params.modifiers;
66
+ activeProduct.modifiers = buildCartLineModifiers(product.modifiers, params.modifiers, product.taxTable);
61
67
  }
62
68
  MOCK_CART.products.push(activeProduct);
63
69
  // Recalculate totals. extendPrice, not a raw multiply: a fractional quantity times an
@@ -1,4 +1,4 @@
1
- import { MOCK_CART } from "../../demo/database";
1
+ import { MOCK_CART, MOCK_PRODUCTS, buildCartLineModifiers } from "../../demo/database";
2
2
  export const mockSetProductModifierSelections = async (params) => {
3
3
  console.log("[Mock] setProductModifierSelections called", params);
4
4
  if (!params?.selections) {
@@ -11,7 +11,9 @@ export const mockSetProductModifierSelections = async (params) => {
11
11
  };
12
12
  }
13
13
  // Write the selections onto the mock cart line (last line when no internalId).
14
- // The mock does not re-run rule validation or repricing — the real host does.
14
+ // The mock does not re-run rule VALIDATION — the real host does — but it does reprice,
15
+ // because a replacement that left the old rows and the old money behind would make the
16
+ // cart disagree with itself the moment a cashier edits a line.
15
17
  const line = params.internalId
16
18
  ? MOCK_CART.products.find((p) => p.internalId === params.internalId)
17
19
  : MOCK_CART.products[MOCK_CART.products.length - 1];
@@ -24,7 +26,13 @@ export const mockSetProductModifierSelections = async (params) => {
24
26
  timestamp: new Date().toISOString()
25
27
  };
26
28
  }
29
+ // Full replacement — passing [] clears the rows. The line drops the product's resolved
30
+ // stack when it is built, so the menu is re-read from the catalogue to price against.
31
+ // Cart totals are not touched, for the same reason addProductToCart does not move them
32
+ // for modifiers: this mock does not accumulate per-line money.
33
+ const source = MOCK_PRODUCTS.find((product) => product._id === line.id);
27
34
  line.modifierSelections = params.selections;
35
+ line.modifiers = buildCartLineModifiers(source?.modifiers, params.selections, source?.taxTable);
28
36
  return {
29
37
  success: true,
30
38
  internalId: line.internalId,
@@ -2,7 +2,7 @@
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';
5
+ import { CFActiveCompany, CFActiveUser, CFActiveStation, CFActiveOutlet, CFActiveOrder, CFCustomer, CFProduct, CFActiveCart, CFCategory, CFActiveProduct, CFSession, CFActiveRefundDetails, CFSmartGridLayout, CFCartLineModifier, CFModifierSelection, CFResolvedModifier } from '../CommonTypes';
6
6
  export * from './mocks';
7
7
  /** Replace mock catalog / context data in place (same array references mock handlers use). */
8
8
  export interface MockDatabaseConfig {
@@ -87,3 +87,21 @@ export declare const createOrderFromCart: (paymentType: string, amount: number,
87
87
  * open with `amountToBeCharged` reset to what's left, and returns null.
88
88
  */
89
89
  export declare const applyMockPayment: (amount: number, paymentType: string, processor?: string) => CFActiveOrder | null;
90
+ /**
91
+ * Price the cashier's raw modifier answers into the rows a CART line carries (FT-0010).
92
+ *
93
+ * Here rather than in an action folder because two mocks need it — addProductToCart and
94
+ * setProductModifierSelections — and this module is already where the shared mock helpers
95
+ * live (`safeSerialize`, `resetMockCart`, `mockPublishEvent`).
96
+ *
97
+ * Note the two shapes that share the name `modifiers` and are NOT interchangeable:
98
+ * `FullProduct.modifiers` is `ResolvedModifier[]` (the menu — question + every choice),
99
+ * `ActiveProduct.modifiers` is `CartLineModifier[]` (one row per CHOSEN choice). The mock
100
+ * builds a cart line by spreading the product, so the menu must be stripped off the line
101
+ * before anything reads it, or every surface renders the catalogue as if it were picked.
102
+ *
103
+ * Names and unitPrice are snapshotted, as the real host does — a later rename never
104
+ * rewrites an open cart. No `total`: a cart row is per line UNIT, and the host extends it.
105
+ * Rule validation (required/min/max) is deliberately not mocked; the real host owns it.
106
+ */
107
+ export declare const buildCartLineModifiers: (menu: CFResolvedModifier[] | undefined, selections: CFModifierSelection[] | undefined, productTaxTableId?: string) => CFCartLineModifier[];
@@ -914,3 +914,51 @@ export const applyMockPayment = (amount, paymentType, processor = 'cash') => {
914
914
  mockPublishEvent('payments', 'payment-done', { order });
915
915
  return order;
916
916
  };
917
+ /**
918
+ * Price the cashier's raw modifier answers into the rows a CART line carries (FT-0010).
919
+ *
920
+ * Here rather than in an action folder because two mocks need it — addProductToCart and
921
+ * setProductModifierSelections — and this module is already where the shared mock helpers
922
+ * live (`safeSerialize`, `resetMockCart`, `mockPublishEvent`).
923
+ *
924
+ * Note the two shapes that share the name `modifiers` and are NOT interchangeable:
925
+ * `FullProduct.modifiers` is `ResolvedModifier[]` (the menu — question + every choice),
926
+ * `ActiveProduct.modifiers` is `CartLineModifier[]` (one row per CHOSEN choice). The mock
927
+ * builds a cart line by spreading the product, so the menu must be stripped off the line
928
+ * before anything reads it, or every surface renders the catalogue as if it were picked.
929
+ *
930
+ * Names and unitPrice are snapshotted, as the real host does — a later rename never
931
+ * rewrites an open cart. No `total`: a cart row is per line UNIT, and the host extends it.
932
+ * Rule validation (required/min/max) is deliberately not mocked; the real host owns it.
933
+ */
934
+ export const buildCartLineModifiers = (menu, selections, productTaxTableId) => {
935
+ if (!menu?.length || !selections?.length)
936
+ return [];
937
+ const byModifier = new Map(menu.map((modifier) => [modifier._id, modifier]));
938
+ const rows = [];
939
+ for (const selection of selections) {
940
+ const modifier = byModifier.get(selection.modifierId);
941
+ if (!modifier)
942
+ continue;
943
+ const byChoice = new Map(modifier.choices.map((choice) => [choice._id, choice]));
944
+ for (const picked of selection.choices ?? []) {
945
+ const choice = byChoice.get(picked.choiceId);
946
+ // `quantity` is units per ONE line unit; 0 means "not chosen" and carries no row.
947
+ if (!choice || !(picked.quantity > 0))
948
+ continue;
949
+ rows.push({
950
+ modifierId: modifier._id,
951
+ modifierName: modifier.name,
952
+ choiceId: choice._id,
953
+ choiceName: choice.name,
954
+ // The cart's display label, like CustomFee.label — "Toppings - Avocado".
955
+ label: `${modifier.name} - ${choice.name}`,
956
+ unitPrice: choice.price,
957
+ quantity: picked.quantity,
958
+ // Modifier tax follows the parent product's table, as a fee's does.
959
+ ...(productTaxTableId ? { taxTableId: productTaxTableId } : {}),
960
+ });
961
+ }
962
+ }
963
+ return rows;
964
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@final-commerce/command-frame",
3
- "version": "0.6.0-staging3.2",
3
+ "version": "0.6.0-staging3.4",
4
4
  "description": "Commands Frame library",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -57,7 +57,7 @@
57
57
  "ignoreBranchesMissingTickets": true
58
58
  },
59
59
  "dependencies": {
60
- "@final-commerce/common": "2.2.3-staging3.2"
60
+ "@final-commerce/common": "2.2.3-staging3.3"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@commitlint/cli": "^19.0.0",