@final-commerce/command-frame 0.14.1-online.3 → 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.
- package/README.md +10 -0
- package/dist/actions/add-booking-to-cart/mock.js +1 -1
- package/dist/actions/add-booking-to-cart/types.d.ts +33 -4
- package/dist/actions/attach-checkout-contact/types.d.ts +2 -14
- package/dist/actions/delete-custom-table-data/mock.js +2 -0
- package/dist/actions/get-active-user/mock.js +3 -1
- package/dist/actions/get-cash-change/action.d.ts +2 -0
- package/dist/actions/get-cash-change/action.js +8 -0
- package/dist/actions/get-cash-change/mock.d.ts +2 -0
- package/dist/actions/get-cash-change/mock.js +6 -0
- package/dist/actions/get-cash-change/types.d.ts +23 -0
- package/dist/actions/get-cash-change/types.js +1 -0
- package/dist/actions/get-checkout-status/types.d.ts +0 -9
- package/dist/actions/get-custom-table-data/mock.d.ts +5 -0
- package/dist/actions/get-custom-table-data/mock.js +16 -27
- package/dist/actions/get-orders-summary/action.d.ts +2 -0
- package/dist/actions/get-orders-summary/action.js +5 -0
- package/dist/actions/get-orders-summary/mock.d.ts +2 -0
- package/dist/actions/get-orders-summary/mock.js +5 -0
- package/dist/actions/get-orders-summary/types.d.ts +24 -0
- package/dist/actions/get-orders-summary/types.js +1 -0
- package/dist/actions/hold-booking/mock.js +27 -13
- package/dist/actions/hold-booking/types.d.ts +35 -6
- package/dist/actions/partial-payment/types.d.ts +5 -0
- package/dist/actions/process-partial-refund/mock.js +5 -0
- package/dist/actions/tap-to-pay-payment/mock.js +2 -1
- package/dist/actions/terminal-payment/mock.js +2 -1
- package/dist/actions/upsert-custom-table-data/mock.d.ts +6 -0
- package/dist/actions/upsert-custom-table-data/mock.js +25 -9
- package/dist/demo/database.d.ts +16 -0
- package/dist/demo/database.js +18 -2
- package/dist/demo/mocks/custom-tables.d.ts +4 -1
- package/dist/demo/mocks/custom-tables.js +1 -0
- package/dist/index.d.ts +4 -16
- package/dist/index.js +4 -17
- package/dist/projects/render/mocks.js +4 -16
- package/dist/projects/render/types.d.ts +3 -10
- package/package.json +2 -2
- package/dist/actions/get-address-details/action.d.ts +0 -6
- package/dist/actions/get-address-details/action.js +0 -8
- package/dist/actions/get-address-details/mock.d.ts +0 -2
- package/dist/actions/get-address-details/mock.js +0 -7
- package/dist/actions/get-address-details/types.d.ts +0 -22
- package/dist/actions/get-address-details/types.js +0 -3
- package/dist/actions/get-shopper-account/action.d.ts +0 -6
- package/dist/actions/get-shopper-account/action.js +0 -8
- package/dist/actions/get-shopper-account/mock.d.ts +0 -3
- package/dist/actions/get-shopper-account/mock.js +0 -12
- package/dist/actions/get-shopper-account/types.d.ts +0 -46
- package/dist/actions/get-shopper-account/types.js +0 -6
- package/dist/actions/get-shopper-order/action.d.ts +0 -6
- package/dist/actions/get-shopper-order/action.js +0 -8
- package/dist/actions/get-shopper-order/mock.d.ts +0 -2
- package/dist/actions/get-shopper-order/mock.js +0 -24
- package/dist/actions/get-shopper-order/types.d.ts +0 -32
- package/dist/actions/get-shopper-order/types.js +0 -6
- package/dist/actions/get-shopper-orders/action.d.ts +0 -6
- package/dist/actions/get-shopper-orders/action.js +0 -8
- package/dist/actions/get-shopper-orders/mock.d.ts +0 -2
- package/dist/actions/get-shopper-orders/mock.js +0 -18
- package/dist/actions/get-shopper-orders/types.d.ts +0 -29
- package/dist/actions/get-shopper-orders/types.js +0 -6
- package/dist/actions/request-shopper-code/action.d.ts +0 -6
- package/dist/actions/request-shopper-code/action.js +0 -8
- package/dist/actions/request-shopper-code/mock.d.ts +0 -2
- package/dist/actions/request-shopper-code/mock.js +0 -4
- package/dist/actions/request-shopper-code/types.d.ts +0 -17
- package/dist/actions/request-shopper-code/types.js +0 -6
- package/dist/actions/search-addresses/action.d.ts +0 -6
- package/dist/actions/search-addresses/action.js +0 -8
- package/dist/actions/search-addresses/mock.d.ts +0 -3
- package/dist/actions/search-addresses/mock.js +0 -24
- package/dist/actions/search-addresses/types.d.ts +0 -31
- package/dist/actions/search-addresses/types.js +0 -5
- package/dist/actions/sign-in-shopper/action.d.ts +0 -6
- package/dist/actions/sign-in-shopper/action.js +0 -8
- package/dist/actions/sign-in-shopper/mock.d.ts +0 -2
- package/dist/actions/sign-in-shopper/mock.js +0 -7
- package/dist/actions/sign-in-shopper/types.d.ts +0 -19
- package/dist/actions/sign-in-shopper/types.js +0 -6
- package/dist/actions/sign-out-shopper/action.d.ts +0 -6
- package/dist/actions/sign-out-shopper/action.js +0 -8
- package/dist/actions/sign-out-shopper/mock.d.ts +0 -2
- package/dist/actions/sign-out-shopper/mock.js +0 -4
- package/dist/actions/sign-out-shopper/types.d.ts +0 -7
- 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:
|
|
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
|
-
/**
|
|
6
|
-
|
|
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
|
|
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
|
-
*
|
|
3
|
-
*
|
|
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,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,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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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:
|
|
20
|
+
data: rows.slice(offset, offset + (params.limit || 100)),
|
|
32
21
|
timestamp: new Date().toISOString()
|
|
33
22
|
};
|
|
34
23
|
};
|
|
@@ -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,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(
|
|
27
|
+
const endAt = new Date(end);
|
|
10
28
|
const bufferEndAt = new Date(endAt.getTime() + BUFFER_MINUTES * 60000);
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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 ===
|
|
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
|
-
/**
|
|
5
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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:
|
|
30
|
+
data: { ...row },
|
|
15
31
|
timestamp: now,
|
|
16
32
|
};
|
|
17
33
|
};
|
package/dist/demo/database.d.ts
CHANGED
|
@@ -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;
|
package/dist/demo/database.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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[];
|