@final-commerce/command-frame 0.14.1-online.2 → 0.15.0-staging.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.
Files changed (86) hide show
  1. package/README.md +10 -0
  2. package/dist/actions/add-booking-to-cart/mock.js +1 -1
  3. package/dist/actions/add-booking-to-cart/types.d.ts +33 -4
  4. package/dist/actions/attach-checkout-contact/types.d.ts +2 -14
  5. package/dist/actions/delete-custom-table-data/mock.js +2 -0
  6. package/dist/actions/get-active-user/mock.js +3 -1
  7. package/dist/actions/get-cash-change/action.d.ts +2 -0
  8. package/dist/actions/get-cash-change/action.js +8 -0
  9. package/dist/actions/get-cash-change/mock.d.ts +2 -0
  10. package/dist/actions/get-cash-change/mock.js +6 -0
  11. package/dist/actions/get-cash-change/types.d.ts +23 -0
  12. package/dist/actions/get-cash-change/types.js +1 -0
  13. package/dist/actions/get-checkout-status/types.d.ts +0 -9
  14. package/dist/actions/get-custom-table-data/mock.d.ts +5 -0
  15. package/dist/actions/get-custom-table-data/mock.js +16 -27
  16. package/dist/actions/get-orders-summary/action.d.ts +2 -0
  17. package/dist/actions/get-orders-summary/action.js +5 -0
  18. package/dist/actions/get-orders-summary/mock.d.ts +2 -0
  19. package/dist/actions/get-orders-summary/mock.js +5 -0
  20. package/dist/actions/get-orders-summary/types.d.ts +24 -0
  21. package/dist/actions/get-orders-summary/types.js +1 -0
  22. package/dist/actions/hold-booking/mock.js +27 -13
  23. package/dist/actions/hold-booking/types.d.ts +35 -6
  24. package/dist/actions/partial-payment/types.d.ts +5 -0
  25. package/dist/actions/process-partial-refund/mock.js +5 -0
  26. package/dist/actions/tap-to-pay-payment/mock.js +2 -1
  27. package/dist/actions/terminal-payment/mock.js +2 -1
  28. package/dist/actions/upsert-custom-table-data/mock.d.ts +6 -0
  29. package/dist/actions/upsert-custom-table-data/mock.js +25 -9
  30. package/dist/demo/database.d.ts +16 -0
  31. package/dist/demo/database.js +18 -2
  32. package/dist/demo/mocks/custom-tables.d.ts +4 -1
  33. package/dist/demo/mocks/custom-tables.js +1 -0
  34. package/dist/index.d.ts +4 -16
  35. package/dist/index.js +4 -17
  36. package/dist/projects/render/mocks.js +4 -16
  37. package/dist/projects/render/types.d.ts +3 -10
  38. package/package.json +2 -2
  39. package/dist/actions/get-address-details/action.d.ts +0 -6
  40. package/dist/actions/get-address-details/action.js +0 -8
  41. package/dist/actions/get-address-details/mock.d.ts +0 -2
  42. package/dist/actions/get-address-details/mock.js +0 -7
  43. package/dist/actions/get-address-details/types.d.ts +0 -22
  44. package/dist/actions/get-address-details/types.js +0 -3
  45. package/dist/actions/get-shopper-account/action.d.ts +0 -6
  46. package/dist/actions/get-shopper-account/action.js +0 -8
  47. package/dist/actions/get-shopper-account/mock.d.ts +0 -3
  48. package/dist/actions/get-shopper-account/mock.js +0 -12
  49. package/dist/actions/get-shopper-account/types.d.ts +0 -46
  50. package/dist/actions/get-shopper-account/types.js +0 -6
  51. package/dist/actions/get-shopper-order/action.d.ts +0 -6
  52. package/dist/actions/get-shopper-order/action.js +0 -8
  53. package/dist/actions/get-shopper-order/mock.d.ts +0 -2
  54. package/dist/actions/get-shopper-order/mock.js +0 -24
  55. package/dist/actions/get-shopper-order/types.d.ts +0 -32
  56. package/dist/actions/get-shopper-order/types.js +0 -6
  57. package/dist/actions/get-shopper-orders/action.d.ts +0 -6
  58. package/dist/actions/get-shopper-orders/action.js +0 -8
  59. package/dist/actions/get-shopper-orders/mock.d.ts +0 -2
  60. package/dist/actions/get-shopper-orders/mock.js +0 -18
  61. package/dist/actions/get-shopper-orders/types.d.ts +0 -29
  62. package/dist/actions/get-shopper-orders/types.js +0 -6
  63. package/dist/actions/request-shopper-code/action.d.ts +0 -6
  64. package/dist/actions/request-shopper-code/action.js +0 -8
  65. package/dist/actions/request-shopper-code/mock.d.ts +0 -2
  66. package/dist/actions/request-shopper-code/mock.js +0 -4
  67. package/dist/actions/request-shopper-code/types.d.ts +0 -17
  68. package/dist/actions/request-shopper-code/types.js +0 -6
  69. package/dist/actions/search-addresses/action.d.ts +0 -6
  70. package/dist/actions/search-addresses/action.js +0 -8
  71. package/dist/actions/search-addresses/mock.d.ts +0 -3
  72. package/dist/actions/search-addresses/mock.js +0 -24
  73. package/dist/actions/search-addresses/types.d.ts +0 -31
  74. package/dist/actions/search-addresses/types.js +0 -5
  75. package/dist/actions/sign-in-shopper/action.d.ts +0 -6
  76. package/dist/actions/sign-in-shopper/action.js +0 -8
  77. package/dist/actions/sign-in-shopper/mock.d.ts +0 -2
  78. package/dist/actions/sign-in-shopper/mock.js +0 -7
  79. package/dist/actions/sign-in-shopper/types.d.ts +0 -19
  80. package/dist/actions/sign-in-shopper/types.js +0 -6
  81. package/dist/actions/sign-out-shopper/action.d.ts +0 -6
  82. package/dist/actions/sign-out-shopper/action.js +0 -8
  83. package/dist/actions/sign-out-shopper/mock.d.ts +0 -2
  84. package/dist/actions/sign-out-shopper/mock.js +0 -4
  85. package/dist/actions/sign-out-shopper/types.d.ts +0 -7
  86. package/dist/actions/sign-out-shopper/types.js +0 -6
package/README.md CHANGED
@@ -147,6 +147,16 @@ Each client comes with built-in mock data for local development.
147
147
  const client = new RenderClient({ mockMode: true, debug: true });
148
148
  ```
149
149
 
150
+ **Declines — mock only.** A test that needs to see how a flow treats a refusal sets `declines` in the dataset; a real till never reads it:
151
+
152
+ ```typescript
153
+ setMockDatabase({ declines: { payments: ['terminal', 'tapToPay'], refunds: true, customTableWrites: true } });
154
+ ```
155
+
156
+ - `payments`: those card commands reject with `CLOUD_PAYMENT_FAILED: Card declined` (cloud terminal) or `NATIVE_PAYMENT_FAILED: Card declined`.
157
+ - `refunds: true`: `processPartialRefund` rejects with `Cloud refund failed`.
158
+ - `customTableWrites: true`: `upsertCustomTableData` and `deleteCustomTableData` reject with `Table "<name>" not found`.
159
+
150
160
  ### Debugging
151
161
 
152
162
  Enable debug logging to see all message passing activity in the console:
@@ -37,7 +37,7 @@ export const mockAddBookingToCart = async (params) => {
37
37
  bookingId: booking.id,
38
38
  productId: params.productId,
39
39
  variantId: variant?._id ?? null,
40
- resourceId: params.resourceId,
40
+ resourceId: booking.resourceId,
41
41
  name: product.name,
42
42
  resourceName: booking.resourceName,
43
43
  price,
@@ -2,16 +2,45 @@ import { CFBooking } from '../../CommonTypes';
2
2
  export interface AddBookingToCartParams {
3
3
  /** The bookable product (`productType: 'booking'`). */
4
4
  productId: string;
5
- /** Which resource serves it: a member of staff, a room, a scooter. */
6
- resourceId: string;
5
+ /**
6
+ * Which resource serves it: a member of staff, a room, a scooter. Omit for "any free one": the host picks the first
7
+ * resource, in availability order, with `qty` places free for the whole window (or every session of a course).
8
+ * A storefront still needs it.
9
+ */
10
+ resourceId?: string;
7
11
  /** Must be the START of a slot getBookingAvailability offered, ISO 8601. */
8
12
  startAt: string;
9
- /** End of the window — the slot's own `endAt`, or the last slot's for a stay. */
10
- endAt: string;
13
+ /** End of the window — the slot's own `endAt`, or the last slot's for a stay. Omit it only when you pass `periods`. */
14
+ endAt?: string;
15
+ /**
16
+ * Instead of `endAt`, for date-range rules: how many periods from `startAt` — hours, nights, days, or whole weeks /
17
+ * months. The host reads the end off its own grid and refuses what the rules refuse (`minStayLength` /
18
+ * `maxStayLength` from `getBookingAvailability`, each with its own unit — "2 nights", "1 week" — and whole periods).
19
+ * Pass exactly one of `endAt` and `periods`. A storefront still needs `endAt`.
20
+ */
21
+ periods?: number;
11
22
  /** The variant being sold. Defaults to the product's only variant. */
12
23
  variantId?: string;
13
24
  /** Whom the booking is for. Omit for a walk-in. */
14
25
  customerId?: string;
26
+ /**
27
+ * How many places of the resource this booking takes (seats in a class, spots in a session). A whole
28
+ * number, default 1. Each place is charged: the reservation's `quantity` counts them and its `total` is
29
+ * `price × quantity`. Refused above the resource's capacity and above the rule's `maxQtyPerBooking`.
30
+ * A window of several nights or days (a date range on a till) is charged per period too: `quantity = qty × periods`.
31
+ */
32
+ qty?: number;
33
+ /**
34
+ * How many people the booking is for (a room for 3, a table for 4). A whole number. Recorded on the booking; it takes
35
+ * no extra places and does not change the price. Refused above the rule's `maxPartySize`. Omit when not asked.
36
+ */
37
+ partySize?: number;
38
+ /**
39
+ * Book the whole course (session rules sold as a course): one hold per session, all or nothing. Pass the course's own
40
+ * `startAt` / `endAt` from `getBookingAvailability` (`availability.course`); other values are refused. Refused on rules
41
+ * that do not sell a course; a course rule refuses a booking without it.
42
+ */
43
+ course?: boolean;
15
44
  }
16
45
  export interface AddBookingToCartResponse {
17
46
  /** False when the command was refused — a taken window, a rule, a booking that is not yours. */
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Contact for the order. `email` is where the receipt goes and is required. The two boxes are the
3
- * shopper account's (see `getShopperAccount`); a box never stops the payment, whatever happens to it.
2
+ * Guest contact for the order. There is no shopper account, so this is the only
3
+ * way to reach them — `email` is where the receipt goes and is required.
4
4
  */
5
5
  export interface AttachCheckoutContactParams {
6
6
  email: string;
@@ -8,18 +8,6 @@ export interface AttachCheckoutContactParams {
8
8
  phone?: string;
9
9
  /** Defaults to the outlet the storefront booted against. */
10
10
  outletId?: string;
11
- /**
12
- * The checkout's account box ("Track this order and check out faster next time", from
13
- * `getShopperAccount`): links this email to a shopper account at this store. Omitted = not ticked.
14
- */
15
- accountOptIn?: boolean;
16
- /** The news box ("Email me news and offers"): this store's consent, true or false. Omitted = not asked. */
17
- marketingOptIn?: boolean;
18
- /**
19
- * The "Save this address" box (signed-in shoppers): once the order is paid, its shipping address is
20
- * saved to the shopper's account. Omitted = not ticked.
21
- */
22
- saveAddress?: boolean;
23
11
  }
24
12
  export interface AttachCheckoutContactResponse {
25
13
  /**
@@ -1,5 +1,7 @@
1
+ import { assertMockCustomTableWriteNotDeclined } from "../../demo/database";
1
2
  export const mockDeleteCustomTableData = async (params) => {
2
3
  console.log("[Mock] deleteCustomTableData called", params);
4
+ assertMockCustomTableWriteNotDeclined(params?.tableName ?? "");
3
5
  return {
4
6
  success: true,
5
7
  timestamp: new Date().toISOString()
@@ -1,6 +1,8 @@
1
- import { MOCK_USER_MARIO } from "../../demo/database";
1
+ import { MOCK_USER_MARIO, mockPublishEvent } from "../../demo/database";
2
2
  export const mockGetActiveUser = async () => {
3
3
  console.log("[Mock] getActiveUser called");
4
+ // Same as the host (kaching get-active-user handler): reading the user publishes `users` / `get-active-user`.
5
+ mockPublishEvent("users", "get-active-user", { user: MOCK_USER_MARIO });
4
6
  return {
5
7
  success: true,
6
8
  user: MOCK_USER_MARIO,
@@ -0,0 +1,2 @@
1
+ import type { GetCashChange } from './types';
2
+ export declare const getCashChange: GetCashChange;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Get cash change action — what a cash tender would give back, computed by the till exactly as `cashPayment` computes
3
+ * it (after cash rounding). Read-only: nothing is paid.
4
+ */
5
+ import { commandFrameClient } from '../../client';
6
+ export const getCashChange = async (params) => {
7
+ return await commandFrameClient.call('getCashChange', params);
8
+ };
@@ -0,0 +1,2 @@
1
+ import type { GetCashChange } from './types';
2
+ export declare const mockGetCashChange: GetCashChange;
@@ -0,0 +1,6 @@
1
+ // The mock does no money: change is the till's arithmetic (cash rounding included), so a screen sees the host's
2
+ // refusal instead of a number the mock made up.
3
+ export const mockGetCashChange = async (params) => {
4
+ console.log('[Mock] getCashChange called', params);
5
+ throw new Error('Cash change is computed by the till');
6
+ };
@@ -0,0 +1,23 @@
1
+ export interface GetCashChangeParams {
2
+ /** Cash handed over, integer MINOR units (e.g. from common `parseAmountToMinorUnits`). */
3
+ tenderedAmount: number;
4
+ /** What it is tendered against, MINOR units. Default: the cart's balance due — the same default as `getCashRoundingAmount`. */
5
+ amount?: number;
6
+ }
7
+ export interface GetCashChangeResponse {
8
+ success: boolean;
9
+ /** The amount tendered against, before cash rounding (MINOR units). */
10
+ amountDue: number;
11
+ /** After the company's cash-rounding setting — what `cashPayment` charges for cash (MINOR units). */
12
+ roundedAmountDue: number;
13
+ /** `roundedAmountDue − amountDue`, signed (MINOR units). 0 without a cash-rounding setting. */
14
+ cashRounding: number;
15
+ /** Echo of the tender (MINOR units). */
16
+ tenderedAmount: number;
17
+ /** ≥ 0: the change `cashPayment({ tenderedAmount })` returns for this tender. 0 while the tender is short. */
18
+ change: number;
19
+ /** ≥ 0: how much the tender is short of `roundedAmountDue`. 0 when it covers it. */
20
+ remaining: number;
21
+ timestamp: string;
22
+ }
23
+ export type GetCashChange = (params: GetCashChangeParams) => Promise<GetCashChangeResponse>;
@@ -0,0 +1 @@
1
+ export {};
@@ -21,15 +21,6 @@ export interface GetCheckoutStatusResponse {
21
21
  */
22
22
  cancelReason?: CheckoutCancelReason;
23
23
  receiptId: string | null;
24
- /**
25
- * The shopper account line for the confirmation page: "Added to your orders" when signed in, else
26
- * (any guest who gave an email) a prompt to get a code; for `sign-in`, call requestShopperCode() with
27
- * no params and it goes to that email. Ready to display.
28
- */
29
- account?: {
30
- label: string;
31
- action: 'none' | 'sign-in';
32
- };
33
24
  timestamp: string;
34
25
  }
35
26
  /** Never throws for an unknown order: answers `status: null`. */
@@ -1,2 +1,7 @@
1
1
  import { GetCustomTableData } from "./types";
2
+ /**
3
+ * The host's read (kaching get-custom-table-data/handler.ts, customTables.ts:99-113): the table's rows, offset 0 and
4
+ * limit 100 by default, so a row upsertCustomTableData wrote is read back. The mock filters by plain field equality only
5
+ * and refuses query operators rather than guess them.
6
+ */
2
7
  export declare const mockGetCustomTableData: GetCustomTableData;
@@ -1,34 +1,23 @@
1
+ import { MOCK_CUSTOM_TABLE_DATA } from "../../demo/mocks/custom-tables";
2
+ /**
3
+ * The host's read (kaching get-custom-table-data/handler.ts, customTables.ts:99-113): the table's rows, offset 0 and
4
+ * limit 100 by default, so a row upsertCustomTableData wrote is read back. The mock filters by plain field equality only
5
+ * and refuses query operators rather than guess them.
6
+ */
1
7
  export const mockGetCustomTableData = async (params) => {
2
8
  console.log("[Mock] getCustomTableData called", params);
3
- const mockData = [
4
- {
5
- _id: "user_data_1",
6
- fullName: "John Doe",
7
- email: "john.doe@example.com",
8
- isActive: true,
9
- createdAt: "2024-01-10T10:00:00.000Z",
10
- updatedAt: "2024-01-10T10:00:00.000Z"
11
- },
12
- {
13
- _id: "user_data_2",
14
- fullName: "Jane Smith",
15
- email: "jane.smith@example.com",
16
- isActive: true,
17
- createdAt: "2024-01-11T11:00:00.000Z",
18
- updatedAt: "2024-01-11T11:00:00.000Z"
19
- },
20
- {
21
- _id: "user_data_3",
22
- fullName: "Bob Johnson",
23
- email: "bob.johnson@example.com",
24
- isActive: false,
25
- createdAt: "2024-01-12T09:00:00.000Z",
26
- updatedAt: "2024-01-15T14:30:00.000Z"
27
- }
28
- ];
9
+ if (!params?.tableName)
10
+ throw new Error("tableName is required");
11
+ const query = params.query ?? {};
12
+ const operator = Object.entries(query).find(([key, value]) => key.startsWith("$") || (typeof value === "object" && value !== null));
13
+ if (operator) {
14
+ throw new Error(`The mock filters custom-table rows by plain field equality only; "${operator[0]}" is not modelled`);
15
+ }
16
+ const rows = (MOCK_CUSTOM_TABLE_DATA[params.tableName] ?? []).filter(row => Object.entries(query).every(([key, value]) => row[key] === value));
17
+ const offset = params.offset || 0;
29
18
  return {
30
19
  success: true,
31
- data: mockData,
20
+ data: rows.slice(offset, offset + (params.limit || 100)),
32
21
  timestamp: new Date().toISOString()
33
22
  };
34
23
  };
@@ -0,0 +1,2 @@
1
+ import type { GetOrdersSummary } from './types';
2
+ export declare const getOrdersSummary: GetOrdersSummary;
@@ -0,0 +1,5 @@
1
+ /** Get orders summary action — counts and the money total of the orders `getOrders` would match, from the till. */
2
+ import { commandFrameClient } from '../../client';
3
+ export const getOrdersSummary = async (params) => {
4
+ return await commandFrameClient.call('getOrdersSummary', params);
5
+ };
@@ -0,0 +1,2 @@
1
+ import type { GetOrdersSummary } from './types';
2
+ export declare const mockGetOrdersSummary: GetOrdersSummary;
@@ -0,0 +1,5 @@
1
+ // The mock does no money: totals and shares are the till's, so a screen sees the host's refusal, not mock maths.
2
+ export const mockGetOrdersSummary = async (params) => {
3
+ console.log('[Mock] getOrdersSummary called', params);
4
+ throw new Error('The orders summary is computed by the till');
5
+ };
@@ -0,0 +1,24 @@
1
+ import type { GetOrdersParams } from '../get-orders/types';
2
+ /** The same filters as `getOrders`; paging and sorting do not apply to a summary. */
3
+ export type GetOrdersSummaryParams = Omit<GetOrdersParams, 'limit' | 'offset' | 'sortBy' | 'sortDirection'>;
4
+ export interface GetOrdersSummaryResponse {
5
+ success: boolean;
6
+ /** Matched orders, leaving out those still in a cart, parked or deleted from park (legacy `status`). */
7
+ orderCount: number;
8
+ /** Σ `summary.total` of those orders, integer MINOR units. */
9
+ ordersTotal: number;
10
+ currency: string;
11
+ minorUnits: number;
12
+ /** Legacy `status === 'completed'`; `percentOfOrders` rounded to a whole percent by the till. */
13
+ completed: {
14
+ count: number;
15
+ percentOfOrders: number;
16
+ };
17
+ /** Legacy `status` 'refunded' or 'partial-refund'. */
18
+ refunded: {
19
+ count: number;
20
+ percentOfOrders: number;
21
+ };
22
+ timestamp: string;
23
+ }
24
+ export type GetOrdersSummary = (params?: GetOrdersSummaryParams) => Promise<GetOrdersSummaryResponse>;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,35 +1,49 @@
1
- import { MOCK_BOOKINGS, MOCK_BOOKING_RESOURCES, mockLiveBookings } from '../../demo/database';
2
- import { ReservationStatus } from '@final-commerce/common';
1
+ import { MOCK_BOOKINGS, MOCK_BOOKING_RESOURCES, mockBookingAvailability, mockLiveBookings } from '../../demo/database';
2
+ import { ReservationStatus, pickFreeResource } from '@final-commerce/common';
3
3
  const BUFFER_MINUTES = 5;
4
4
  const HOLD_MINUTES = 10;
5
+ const TAKEN = 'That window has just been taken — pick another slot.';
6
+ const MOCK_ONLY_SINGLE_SLOTS = "[Mock] the mock shop sells single 30-minute slots — pass the slot's endAt";
5
7
  let held = 0;
8
+ const refused = (reason) => ({
9
+ success: false,
10
+ reason,
11
+ timestamp: new Date().toISOString(),
12
+ });
13
+ const offeredSlot = (productId, startAt) => mockBookingAvailability(productId, new Date(startAt), new Date(Date.parse(startAt) + 60000)).slots.find((slot) => slot.startAt === new Date(startAt).toISOString());
14
+ // The mock refuses a taken window like the host does. A mock that always said yes would
15
+ // let a booking screen ship without ever handling the one error it will actually meet.
16
+ const windowIsTaken = (resourceId, startAt, bufferEndAt) => mockLiveBookings().some((entry) => entry.resourceId === resourceId && new Date(entry.startAt) < bufferEndAt && new Date(entry.bufferEndAt) > startAt);
6
17
  export const mockHoldBooking = async (params) => {
7
18
  console.log('[Mock] holdBooking called', params);
19
+ if (params.course || (params.periods !== undefined && params.periods !== 1))
20
+ return refused(MOCK_ONLY_SINGLE_SLOTS);
21
+ const slot = offeredSlot(params.productId, params.startAt);
22
+ const resourceId = params.resourceId ?? (slot && pickFreeResource(slot, params.qty)?.resourceId);
23
+ const end = params.endAt ?? slot?.endAt;
24
+ if (!resourceId || !end)
25
+ return refused(TAKEN);
8
26
  const startAt = new Date(params.startAt);
9
- const endAt = new Date(params.endAt);
27
+ const endAt = new Date(end);
10
28
  const bufferEndAt = new Date(endAt.getTime() + BUFFER_MINUTES * 60000);
11
- // The mock refuses a taken window like the host does. A mock that always said yes would
12
- // let a booking screen ship without ever handling the one error it will actually meet.
13
- const clash = mockLiveBookings().some((entry) => entry.resourceId === params.resourceId &&
14
- new Date(entry.startAt) < bufferEndAt &&
15
- new Date(entry.bufferEndAt) > startAt);
16
- if (clash) {
17
- return { success: false, reason: 'That window has just been taken — pick another slot.', timestamp: new Date().toISOString() };
18
- }
29
+ if (windowIsTaken(resourceId, startAt, bufferEndAt))
30
+ return refused(TAKEN);
19
31
  const booking = {
20
32
  // A counter, not a clock: two holds in the same millisecond used to share an id, and cancel
21
33
  // or remove then acted on the wrong row. Scripted tests hit that routinely.
22
34
  id: `bk_mock_${(held += 1)}`,
23
35
  productId: params.productId,
24
- resourceId: params.resourceId,
36
+ resourceId,
25
37
  variantId: params.variantId,
26
38
  startAt: startAt.toISOString(),
27
39
  endAt: endAt.toISOString(),
28
40
  bufferEndAt: bufferEndAt.toISOString(),
29
41
  customerId: params.customerId,
42
+ qty: params.qty,
43
+ partySize: params.partySize,
30
44
  status: ReservationStatus.HELD,
31
45
  expiresAt: new Date(Date.now() + HOLD_MINUTES * 60000).toISOString(),
32
- resourceName: MOCK_BOOKING_RESOURCES.find(({ id }) => id === params.resourceId)?.name,
46
+ resourceName: MOCK_BOOKING_RESOURCES.find(({ id }) => id === resourceId)?.name,
33
47
  };
34
48
  // Held in the same list availability reads, so the slot really does disappear.
35
49
  MOCK_BOOKINGS.push(booking);
@@ -1,20 +1,46 @@
1
1
  import { CFBooking } from '../../CommonTypes';
2
2
  export interface HoldBookingParams {
3
3
  productId: string;
4
- /** Which resource serves it: a member of staff, a room, a scooter. */
5
- resourceId: string;
4
+ /**
5
+ * Which resource serves it: a member of staff, a room, a scooter. Omit for "any free one": the host picks the first
6
+ * resource, in availability order, with `qty` places free for the whole window (or every session of a course).
7
+ * A storefront still needs it.
8
+ */
9
+ resourceId?: string;
6
10
  /** Must be the START of a slot the host offered, ISO 8601. */
7
11
  startAt: string;
8
12
  /**
9
- * End of the window — the `endAt` of the slot you picked, or of the last slot in a stay.
10
- * Required: a window that cannot be lined up with the grid is refused, and the till
11
- * has no local copy of the rules to derive it from.
13
+ * End of the window — the `endAt` of the slot you picked, or of the last slot in a stay. A window that cannot be
14
+ * lined up with the grid is refused. Omit it only when you pass `periods`.
12
15
  */
13
- endAt: string;
16
+ endAt?: string;
17
+ /**
18
+ * Instead of `endAt`, for date-range rules: how many periods from `startAt` — hours, nights, days, or whole weeks /
19
+ * months. The host reads the end off its own grid and refuses what the rules refuse (`minStayLength` /
20
+ * `maxStayLength` from `getBookingAvailability`, each with its own unit — "2 nights", "1 week" — and whole periods).
21
+ * Pass exactly one of `endAt` and `periods`. A storefront still needs `endAt`.
22
+ */
23
+ periods?: number;
14
24
  /** The variant being sold, when the flow already knows which one. */
15
25
  variantId?: string;
16
26
  /** Whom the booking is for. Omit for a walk-in. */
17
27
  customerId?: string;
28
+ /**
29
+ * How many places of the resource this booking takes (seats in a class, spots in a session). A whole
30
+ * number, default 1. Refused above the resource's capacity and above the rule's `maxQtyPerBooking`.
31
+ */
32
+ qty?: number;
33
+ /**
34
+ * How many people the booking is for (a room for 3, a table for 4). A whole number. Recorded on the booking; it takes
35
+ * no extra places and does not change the price. Refused above the rule's `maxPartySize`. Omit when not asked.
36
+ */
37
+ partySize?: number;
38
+ /**
39
+ * Book the whole course (session rules sold as a course): one hold per session, all or nothing. Pass the course's own
40
+ * `startAt` / `endAt` from `getBookingAvailability` (`availability.course`); other values are refused. Refused on rules
41
+ * that do not sell a course; a course rule refuses a booking without it.
42
+ */
43
+ course?: boolean;
18
44
  }
19
45
  export interface HoldBookingResponse {
20
46
  /** False when the command was refused — a taken window, a rule, a booking that is not yours. */
@@ -23,6 +49,9 @@ export interface HoldBookingResponse {
23
49
  reason?: string;
24
50
  /** The claim. `expiresAt` says how long it survives unpaid; `id` is what you confirm or cancel. */
25
51
  booking?: CFBooking;
52
+ /** A course: the id every session's hold shares, and how many sessions it holds. `booking` is the first session. */
53
+ groupId?: string;
54
+ sessions?: number;
26
55
  timestamp: string;
27
56
  }
28
57
  export type HoldBooking = (params: HoldBookingParams) => Promise<HoldBookingResponse>;
@@ -6,6 +6,11 @@ export interface PartialPaymentParams {
6
6
  isPercent?: boolean;
7
7
  /** If true, opens the split payment UI. */
8
8
  openUI?: boolean;
9
+ /**
10
+ * true: the host refuses an amount above the balance due (the same balance `getCashRoundingAmount()` defaults to) or a
11
+ * percent above 100, with its own reason. Absent / false: no ceiling, exactly as before.
12
+ */
13
+ refuseAboveBalance?: boolean;
9
14
  }
10
15
  export interface PartialPaymentResponse {
11
16
  success: boolean;
@@ -1,3 +1,4 @@
1
+ import { MOCK_DECLINES } from '../../demo/database';
1
2
  export const mockProcessPartialRefund = async (params) => {
2
3
  // The mock has no split-payment modal or order engine, so `openUI` (default
3
4
  // true on the real command), `legs` (the headless per-tender allocation), a
@@ -10,6 +11,10 @@ export const mockProcessPartialRefund = async (params) => {
10
11
  if (params?.legs && params?.giftCard) {
11
12
  throw new Error('refund.giftCardAndLegs: pass either `legs` (you allocate) or `giftCard` (the engine allocates), not both');
12
13
  }
14
+ if (MOCK_DECLINES.refunds) {
15
+ console.warn('[Mock] processPartialRefund failed — the mock dataset sets declines.refunds');
16
+ throw new Error('Cloud refund failed');
17
+ }
13
18
  console.log('[Mock] processPartialRefund called', {
14
19
  ...params,
15
20
  openUI: params?.openUI ?? true,
@@ -1,6 +1,7 @@
1
- import { applyMockPayment, MOCK_CART } from '../../demo/database';
1
+ import { applyMockPayment, assertMockPaymentNotDeclined, MOCK_CART } from '../../demo/database';
2
2
  export const mockTapToPayPayment = async (params) => {
3
3
  console.log('[Mock] tapToPayPayment called', params);
4
+ assertMockPaymentNotDeclined('tapToPay', false);
4
5
  // Simulate Tap to Pay interaction
5
6
  window.alert('Demo: Processing Tap to Pay...\n(Please tap card or device on screen)');
6
7
  const due = params?.amount ?? MOCK_CART.amountToBeCharged ?? MOCK_CART.total;
@@ -1,7 +1,8 @@
1
- import { applyMockPayment, MOCK_CART } from '../../demo/database';
1
+ import { applyMockPayment, assertMockPaymentNotDeclined, MOCK_CART } from '../../demo/database';
2
2
  export const mockTerminalPayment = async (params) => {
3
3
  console.log('[Mock] terminalPayment called', params);
4
4
  const connectionType = params?.paymentType || 'Cloud';
5
+ assertMockPaymentNotDeclined('terminal', connectionType === 'Cloud');
5
6
  // Simulate terminal interaction
6
7
  window.alert(`Demo: Processing ${connectionType} Terminal Payment...\n(Please tap, insert, or swipe card on terminal)`);
7
8
  const due = params?.amount ?? MOCK_CART.amountToBeCharged ?? MOCK_CART.total;
@@ -1,2 +1,8 @@
1
1
  import { UpsertCustomTableData } from './types';
2
+ /**
3
+ * The host's upsert (kaching upsert-custom-table-data/handler.ts, customTables.ts:120-271): an `_id` of an existing row
4
+ * updates it, anything else inserts (a given `_id` kept as-is); `createdAt`/`updatedAt` are the host's; the row is
5
+ * stored, so getCustomTableData reads it back. Not modelled: the table's field schema — the host drops keys that are
6
+ * not fields of the table and refuses an unknown table; the mock stores every key.
7
+ */
2
8
  export declare const mockUpsertCustomTableData: UpsertCustomTableData;
@@ -1,17 +1,33 @@
1
+ import { MOCK_CUSTOM_TABLE_DATA } from '../../demo/mocks/custom-tables';
2
+ import { assertMockCustomTableWriteNotDeclined, mockPublishEvent } from '../../demo/database';
3
+ import { generateMongoID } from '@final-commerce/common';
4
+ /**
5
+ * The host's upsert (kaching upsert-custom-table-data/handler.ts, customTables.ts:120-271): an `_id` of an existing row
6
+ * updates it, anything else inserts (a given `_id` kept as-is); `createdAt`/`updatedAt` are the host's; the row is
7
+ * stored, so getCustomTableData reads it back. Not modelled: the table's field schema — the host drops keys that are
8
+ * not fields of the table and refuses an unknown table; the mock stores every key.
9
+ */
1
10
  export const mockUpsertCustomTableData = async (params) => {
11
+ var _a;
2
12
  console.log('[Mock] upsertCustomTableData called', params);
3
- const data = params?.data;
13
+ if (!params?.tableName)
14
+ throw new Error('tableName is required');
15
+ assertMockCustomTableWriteNotDeclined(params.tableName);
16
+ const { _id: givenId, ...fields } = (params.data ?? {});
17
+ delete fields.createdAt;
18
+ delete fields.updatedAt;
19
+ const rows = (MOCK_CUSTOM_TABLE_DATA[_a = params.tableName] ?? (MOCK_CUSTOM_TABLE_DATA[_a] = []));
4
20
  const now = new Date().toISOString();
5
- // Return the data with _id and timestamps added
6
- const resultData = {
7
- ...data,
8
- _id: data?._id || `mock_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
9
- createdAt: data?.createdAt || now,
10
- updatedAt: now,
11
- };
21
+ const existing = typeof givenId === 'string' ? rows.find((row) => row._id === givenId) : undefined;
22
+ const row = existing
23
+ ? Object.assign(existing, fields, { updatedAt: now })
24
+ : { _id: typeof givenId === 'string' ? givenId : generateMongoID(), ...fields, createdAt: now, updatedAt: now };
25
+ if (!existing)
26
+ rows.push(row);
27
+ mockPublishEvent('custom-tables', existing ? 'row-updated' : 'row-created', { tableName: params.tableName, ...row });
12
28
  return {
13
29
  success: true,
14
- data: resultData,
30
+ data: { ...row },
15
31
  timestamp: now,
16
32
  };
17
33
  };
@@ -23,6 +23,17 @@ export interface MockDatabaseConfig {
23
23
  orderStatuses?: CFOrderStatusDefinition[];
24
24
  /** Products hidden at an outlet (the host's `catalog-visibility`, product-level rows). Empty = sold everywhere. */
25
25
  catalogVisibility?: MockCatalogVisibility[];
26
+ /** MOCK ONLY — the switch a test flips to make the host refuse. Absent = everything succeeds. */
27
+ declines?: MockDeclines;
28
+ }
29
+ /** MOCK ONLY: what the mock host refuses, so a test can see how a flow treats a failure. A real till never reads this. */
30
+ export interface MockDeclines {
31
+ /** Card payments that are declined: `terminalPayment` (cloud or reader) and `tapToPayPayment`. Cash never declines. */
32
+ payments?: ('terminal' | 'tapToPay')[];
33
+ /** Every `processPartialRefund` fails. */
34
+ refunds?: boolean;
35
+ /** Every `upsertCustomTableData` / `deleteCustomTableData` rejects with the till's only refusal text for a table write. */
36
+ customTableWrites?: boolean;
26
37
  }
27
38
  export interface MockCatalogVisibility {
28
39
  productId: string;
@@ -124,6 +135,11 @@ export declare const MOCK_ORDER_STATUSES: CFOrderStatusDefinition[];
124
135
  export declare function setMockDatabase(config: Partial<MockDatabaseConfig>): void;
125
136
  export declare const safeSerialize: <T>(data: T) => T;
126
137
  type MockEventCallback = (event: any) => void;
138
+ export declare let MOCK_DECLINES: MockDeclines;
139
+ /** MOCK ONLY: the dataset's `declines.payments` switch, failing with the till processor's text (kaching `dispatchPay.ts:208-220`). */
140
+ export declare const assertMockPaymentNotDeclined: (paymentType: "terminal" | "tapToPay", cloud: boolean) => void;
141
+ /** MOCK ONLY: the dataset's `declines.customTableWrites` switch, refusing with the till's text (kaching `customTables.ts:131,291`). */
142
+ export declare const assertMockCustomTableWriteNotDeclined: (tableName: string) => void;
127
143
  export declare const mockPublishEvent: (topic: string, eventType: string, data: any) => void;
128
144
  export declare const mockSubscribeToTopic: (topic: string, callback: MockEventCallback) => void;
129
145
  export declare const createOrderFromCart: (paymentType: string, amount: number, processor?: string) => CFActiveOrder;
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { CFProductType, CFUserTypes, CurrencyCode, } from '../CommonTypes';
6
6
  import { cheapestCompositeItem, cheapestCompositePrice, extendPrice, resolveUnit, toBase, } from '@final-commerce/common';
7
- import { BookingRatePeriod, BookingResourceKind, BookingType, ReservationStatus } from '@final-commerce/common';
7
+ import { BookingRatePeriod, BookingResourceKind, ReservationStatus } from '@final-commerce/common';
8
8
  export * from './mocks';
9
9
  // Asset Imports - Using Remote URLs to avoid build complexity with asset copying
10
10
  const ASSETS_BASE_URL = 'https://raw.githubusercontent.com/Final-Commerce/command-frame/refs/heads/main/src/demo/assets';
@@ -1006,6 +1006,8 @@ export function setMockDatabase(config) {
1006
1006
  if (config.catalogVisibility !== undefined) {
1007
1007
  MOCK_CATALOG_VISIBILITY.splice(0, MOCK_CATALOG_VISIBILITY.length, ...config.catalogVisibility);
1008
1008
  }
1009
+ if (config.declines !== undefined)
1010
+ MOCK_DECLINES = config.declines;
1009
1011
  if (MOCK_OUTLETS.length > 0) {
1010
1012
  Object.assign(MOCK_OUTLET_MAIN, MOCK_OUTLETS[0]);
1011
1013
  if (MOCK_OUTLET_MAIN.id === undefined && MOCK_OUTLET_MAIN._id !== undefined) {
@@ -1028,6 +1030,21 @@ export const safeSerialize = (data) => {
1028
1030
  return JSON.parse(JSON.stringify(data));
1029
1031
  };
1030
1032
  const mockTopicSubscribers = {};
1033
+ export let MOCK_DECLINES = {};
1034
+ /** MOCK ONLY: the dataset's `declines.payments` switch, failing with the till processor's text (kaching `dispatchPay.ts:208-220`). */
1035
+ export const assertMockPaymentNotDeclined = (paymentType, cloud) => {
1036
+ if (!MOCK_DECLINES.payments?.includes(paymentType))
1037
+ return;
1038
+ console.warn(`[Mock] ${paymentType}Payment declined — the mock dataset sets declines.payments`);
1039
+ throw new Error(`${cloud ? 'CLOUD_PAYMENT_FAILED' : 'NATIVE_PAYMENT_FAILED'}: Card declined`);
1040
+ };
1041
+ /** MOCK ONLY: the dataset's `declines.customTableWrites` switch, refusing with the till's text (kaching `customTables.ts:131,291`). */
1042
+ export const assertMockCustomTableWriteNotDeclined = (tableName) => {
1043
+ if (!MOCK_DECLINES.customTableWrites)
1044
+ return;
1045
+ console.warn('[Mock] custom table write refused — the mock dataset sets declines.customTableWrites');
1046
+ throw new Error(`Table "${tableName}" not found`);
1047
+ };
1031
1048
  export const mockPublishEvent = (topic, eventType, data) => {
1032
1049
  const subscribers = mockTopicSubscribers[topic] || [];
1033
1050
  const event = {
@@ -1338,7 +1355,6 @@ export const mockBookingAvailability = (productId, from, to, resourceId) => {
1338
1355
  return {
1339
1356
  productId,
1340
1357
  bookingRulesId: MOCK_BOOKING_RULES_ID,
1341
- bookingType: BookingType.APPOINTMENT,
1342
1358
  ratePeriod: BookingRatePeriod.SLOT,
1343
1359
  slots,
1344
1360
  timeZone: MOCK_SHOP_TIME_ZONE,
@@ -10,5 +10,8 @@ export interface CustomTableDataRow {
10
10
  createdAt: string;
11
11
  updatedAt: string;
12
12
  }
13
- export declare const MOCK_CUSTOM_TABLE_DATA: Record<string, CustomTableDataRow[]>;
13
+ /** Rows by table name — what getCustomTableData reads and upsertCustomTableData writes. */
14
+ export declare const MOCK_CUSTOM_TABLE_DATA: Record<string, (Record<string, unknown> & {
15
+ _id: string;
16
+ })[]>;
14
17
  export declare const MOCK_CUSTOM_EXTENSIONS: CustomExtension[];