@final-commerce/command-frame 0.10.0-staging.1 → 0.10.0-staging.3

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.
@@ -2,14 +2,6 @@ import { MOCK_CART, MOCK_PRODUCTS, mockPublishEvent } from '../../demo/database'
2
2
  import { mockHoldBooking } from '../hold-booking/mock';
3
3
  export const mockAddBookingToCart = async (params) => {
4
4
  console.log('[Mock] addBookingToCart called', params);
5
- // One call, two effects — the window is claimed AND the service is in the cart. The claim goes
6
- // first and its refusal propagates: a cart line for a window somebody else took is worse than
7
- // no line at all.
8
- const held = await mockHoldBooking(params);
9
- if (!held.success || !held.booking) {
10
- return { success: false, reason: held.reason, timestamp: new Date().toISOString() };
11
- }
12
- const booking = held.booking;
13
5
  // An unknown product used to be sold as "Booking" at zero. The host refuses it, and a mock that
14
6
  // quietly sells something no catalogue contains teaches a screen a case that cannot happen.
15
7
  const product = MOCK_PRODUCTS.find(({ _id }) => _id === params.productId);
@@ -27,6 +19,18 @@ export const mockAddBookingToCart = async (params) => {
27
19
  timestamp: new Date().toISOString(),
28
20
  };
29
21
  }
22
+ // Same refusal and text as the host (kaching add-booking-to-cart).
23
+ if (product.notSoldInPos || variant?.notSoldInPos) {
24
+ return { success: false, reason: `${product.name} is not sold in POS`, timestamp: new Date().toISOString() };
25
+ }
26
+ // One call, two effects — the window is claimed AND the service is in the cart. Like the host, the
27
+ // claim comes after the product checks and before the line; its refusal propagates: a cart line for
28
+ // a window somebody else took is worse than no line at all.
29
+ const held = await mockHoldBooking(params);
30
+ if (!held.success || !held.booking) {
31
+ return { success: false, reason: held.reason, timestamp: new Date().toISOString() };
32
+ }
33
+ const booking = held.booking;
30
34
  const price = variant?.price ?? product.minPrice ?? 0;
31
35
  const reservation = {
32
36
  internalId: `res_${booking.id}`,
@@ -79,6 +79,10 @@ export const mockAddProductToCart = async (params) => {
79
79
  }
80
80
  compositeLine = line;
81
81
  }
82
+ // Same refusal and text as the host (kaching add-product-to-cart).
83
+ if (product.notSoldInPos || variant.notSoldInPos) {
84
+ throw new Error(`${product.name} is not sold in POS`);
85
+ }
82
86
  // A measured variant carries a resolved unit; the host does the same. What the unit can
83
87
  // express is its precision — 1.5 of a kilogram is a sale, 1.5 of a piece is a typo.
84
88
  const unit = variant.unitId ? resolveUnit(variant.unitId) : undefined;
@@ -182,6 +186,10 @@ export function compositePicksRefusal(composite, picks) {
182
186
  if (!composite.parts.length)
183
187
  return 'This composite has no parts on this till yet';
184
188
  const blocking = composite.parts.find((part) => part.min > 0 && !part.items.some((item) => !item.unavailable || item.unavailable === 'by_date'));
189
+ // FT-151: a part blocked by the sold-out mark says so, as the host does.
190
+ const marked = blocking?.items.find((item) => item.unavailable === 'marked_sold_out');
191
+ if (marked)
192
+ return `${marked.name}: marked sold out`;
185
193
  return blocking
186
194
  ? `${blocking.name ?? 'Choose'}: nothing can be picked at this outlet`
187
195
  : 'This composite is unavailable: a part it needs has nothing to pick';
@@ -199,6 +207,8 @@ export function compositePicksRefusal(composite, picks) {
199
207
  return `${item.name}: choose a date and time`;
200
208
  if (item.unavailable === 'fully_booked')
201
209
  return `${item.name} is fully booked at that time`;
210
+ if (item.unavailable === 'marked_sold_out')
211
+ return `${item.name}: marked sold out`;
202
212
  if (item.unavailable)
203
213
  return `${item.name} is not available`;
204
214
  const choice = item.choices.find((candidate) => candidate.variantId === pick.variantId);
@@ -15,6 +15,19 @@ export const mockGetProducts = async (params) => {
15
15
  if (!JSON.stringify(query).includes('productType')) {
16
16
  products = products.filter((p) => p.productType !== 'booking');
17
17
  }
18
+ // Same as the host: `true` = flagged, `false` = not flagged (missing counts).
19
+ for (const flag of ['notSoldInPos', 'notSoldOnline']) {
20
+ if (typeof query[flag] === 'boolean')
21
+ products = products.filter((p) => !!p[flag] === query[flag]);
22
+ }
23
+ if (!params?.includeUnlisted) {
24
+ // Same as the host: flagged variants are stripped, a product left with none is dropped.
25
+ if (query.notSoldInPos === undefined)
26
+ products = products.filter((p) => !p.notSoldInPos);
27
+ products = products
28
+ .filter((p) => !p.variants.length || p.variants.some((v) => !v.notSoldInPos))
29
+ .map((p) => ({ ...p, variants: p.variants.filter((v) => !v.notSoldInPos) }));
30
+ }
18
31
  if (query.searchValue) {
19
32
  const search = String(query.searchValue).toLowerCase();
20
33
  products = products.filter((p) => p.name.toLowerCase().includes(search) || p.sku?.toLowerCase().includes(search));
@@ -42,8 +42,24 @@ export interface GetProductsParams {
42
42
  };
43
43
  supplier?: string;
44
44
  externalId?: string;
45
+ /**
46
+ * Product-level flag filter. `true` = only products flagged `notSoldInPos`; `false` = only
47
+ * products NOT flagged (a missing field counts as not flagged). Naming it switches off the
48
+ * default `notSoldInPos` exclusion, so `true` works without `includeUnlisted`.
49
+ */
50
+ notSoldInPos?: boolean;
51
+ /** Product-level flag filter: `true` = flagged `notSoldOnline`; `false` = sold online (missing counts). */
52
+ notSoldOnline?: boolean;
45
53
  [key: string]: any;
46
54
  };
55
+ /**
56
+ * Products flagged `notSoldInPos` are left out unless this is `true` (default `false`) or the
57
+ * query names `notSoldInPos`. They still cannot be sold: `addProductToCart` / `addBookingToCart`
58
+ * refuse a product or a variant flagged `notSoldInPos`. Without it, flagged variants are also
59
+ * stripped from `variants[]` and a product whose every variant is flagged is left out — whatever
60
+ * `query.notSoldInPos` says.
61
+ */
62
+ includeUnlisted?: boolean;
47
63
  /** Defaults to 0. */
48
64
  offset?: number;
49
65
  /** Defaults to 100. */
@@ -1,2 +1,2 @@
1
- import { ResumeParkedOrder } from "./types";
1
+ import { ResumeParkedOrder } from './types';
2
2
  export declare const mockResumeParkedOrder: ResumeParkedOrder;
@@ -1,11 +1,11 @@
1
- import { MOCK_PARKED_ORDERS, MOCK_CART, resetMockCart, mockPublishEvent } from "../../demo/database";
1
+ import { MOCK_PARKED_ORDERS, MOCK_CART, MOCK_PRODUCTS, resetMockCart, mockPublishEvent } from '../../demo/database';
2
2
  export const mockResumeParkedOrder = async (params) => {
3
- console.log("[Mock] resumeParkedOrder called", params);
3
+ console.log('[Mock] resumeParkedOrder called', params);
4
4
  const orderId = params?.orderId;
5
5
  let orderToResume = null;
6
6
  let index = -1;
7
7
  if (orderId) {
8
- index = MOCK_PARKED_ORDERS.findIndex(o => o._id === orderId);
8
+ index = MOCK_PARKED_ORDERS.findIndex((o) => o._id === orderId);
9
9
  if (index !== -1)
10
10
  orderToResume = MOCK_PARKED_ORDERS[index];
11
11
  }
@@ -17,8 +17,14 @@ export const mockResumeParkedOrder = async (params) => {
17
17
  }
18
18
  }
19
19
  if (!orderToResume) {
20
- throw new Error("Parked order not found");
20
+ throw new Error('Parked order not found');
21
21
  }
22
+ // Same refusal and text as the host, before anything is restored: the parked order stays.
23
+ const notSoldNames = orderToResume.lineItems
24
+ .filter((li) => MOCK_PRODUCTS.some((p) => p.variants.some((v) => v._id === li.variantId && (p.notSoldInPos || v.notSoldInPos))))
25
+ .map((li) => li.name);
26
+ if (notSoldNames.length)
27
+ throw new Error(`${notSoldNames.join(', ')} is not sold in POS`);
22
28
  // Restore to MOCK_CART
23
29
  // Logic to convert Order back to Cart (simplified)
24
30
  resetMockCart();
@@ -26,7 +32,7 @@ export const mockResumeParkedOrder = async (params) => {
26
32
  MOCK_CART.customer = orderToResume.customer; // Cast if needed
27
33
  // MOCK_CART.products = orderToResume.lineItems... (Mapping needed)
28
34
  // For Mock demo, we'll do a best effort mapping
29
- MOCK_CART.products = orderToResume.lineItems.map(li => ({
35
+ MOCK_CART.products = orderToResume.lineItems.map((li) => ({
30
36
  id: li.productId,
31
37
  name: li.name,
32
38
  quantity: li.quantity,
@@ -36,8 +42,8 @@ export const mockResumeParkedOrder = async (params) => {
36
42
  sku: li.sku,
37
43
  images: [li.image],
38
44
  stock: 100,
39
- taxTableId: "",
40
- attributes: li.attributes
45
+ taxTableId: '',
46
+ attributes: li.attributes,
41
47
  }));
42
48
  MOCK_CART.subtotal = orderToResume.summary.subtotalAfterFees;
43
49
  MOCK_CART.total = orderToResume.summary.total;
@@ -46,11 +52,11 @@ export const mockResumeParkedOrder = async (params) => {
46
52
  // Remove from parked
47
53
  MOCK_PARKED_ORDERS.splice(index, 1);
48
54
  // Refresh the restored cart and the order lists.
49
- mockPublishEvent("cart", "parked-order-resumed", { orderId });
50
- mockPublishEvent("orders", "parked-order-resumed", { orderId });
55
+ mockPublishEvent('cart', 'parked-order-resumed', { orderId });
56
+ mockPublishEvent('orders', 'parked-order-resumed', { orderId });
51
57
  return {
52
58
  success: true,
53
59
  order: orderToResume,
54
- timestamp: new Date().toISOString()
60
+ timestamp: new Date().toISOString(),
55
61
  };
56
62
  };
@@ -1,4 +1,4 @@
1
- import { MOCK_CART, mockPublishEvent } from '../../demo/database';
1
+ import { MOCK_CART, MOCK_PRODUCTS, mockPublishEvent } from '../../demo/database';
2
2
  import { extendPrice, isValidQuantity } from '@final-commerce/common';
3
3
  export const mockUpdateCartItemQuantity = (params) => {
4
4
  console.log('[Mock] updateCartItemQuantity called', params);
@@ -44,6 +44,12 @@ export const mockUpdateCartItemQuantity = (params) => {
44
44
  timestamp: new Date().toISOString(),
45
45
  });
46
46
  }
47
+ // Same refusal and text as the host: raising a line not sold in POS (lowering always works).
48
+ const source = MOCK_PRODUCTS.find((p) => p.variants.some((v) => v._id === product.variantId));
49
+ const variant = source?.variants.find((v) => v._id === product.variantId);
50
+ if (quantity > previousQuantity && (source?.notSoldInPos || variant?.notSoldInPos)) {
51
+ throw new Error(`${source?.name} is not sold in POS`);
52
+ }
47
53
  // Update quantity
48
54
  product.quantity = quantity;
49
55
  // Difference of the two ROUNDED extensions — rounding the delta itself would let the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@final-commerce/command-frame",
3
- "version": "0.10.0-staging.1",
3
+ "version": "0.10.0-staging.3",
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.5.0-staging.1"
60
+ "@final-commerce/common": "2.6.0-staging.2"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@commitlint/cli": "^19.0.0",