@final-commerce/command-frame 0.8.0 → 0.9.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/dist/CommonTypes.d.ts +25 -1
- package/dist/actions/add-product-fee/mock.d.ts +1 -1
- package/dist/actions/add-product-fee/mock.js +15 -9
- package/dist/actions/add-product-to-cart/mock.d.ts +23 -0
- package/dist/actions/add-product-to-cart/mock.js +300 -1
- package/dist/actions/add-product-to-cart/types.d.ts +27 -1
- package/dist/actions/get-composite-availability/action.d.ts +6 -0
- package/dist/actions/get-composite-availability/action.js +8 -0
- package/dist/actions/get-composite-availability/mock.d.ts +2 -0
- package/dist/actions/get-composite-availability/mock.js +45 -0
- package/dist/actions/get-composite-availability/types.d.ts +29 -0
- package/dist/actions/get-composite-availability/types.js +1 -0
- package/dist/actions/get-composite-price/action.d.ts +6 -0
- package/dist/actions/get-composite-price/action.js +8 -0
- package/dist/actions/get-composite-price/mock.d.ts +2 -0
- package/dist/actions/get-composite-price/mock.js +37 -0
- package/dist/actions/get-composite-price/types.d.ts +31 -0
- package/dist/actions/get-composite-price/types.js +1 -0
- package/dist/actions/get-current-cart/mock.d.ts +1 -1
- package/dist/actions/get-current-cart/mock.js +13 -3
- package/dist/actions/get-current-cart/types.d.ts +7 -1
- package/dist/actions/get-product-modifier-selections/mock.js +12 -0
- package/dist/actions/get-products/mock.js +7 -3
- package/dist/actions/get-products/types.d.ts +1 -1
- package/dist/actions/get-refund-plan/mock.d.ts +0 -4
- package/dist/actions/get-refund-plan/mock.js +0 -8
- package/dist/actions/get-refund-plan/types.d.ts +0 -89
- package/dist/actions/set-product-modifier-selections/mock.js +12 -0
- package/dist/demo/database.d.ts +39 -1
- package/dist/demo/database.js +214 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -0
- package/dist/projects/render/mocks.js +4 -0
- package/dist/projects/render/types.d.ts +3 -1
- package/dist/pubsub/topics/products/catalog-visibility-changed/types.d.ts +17 -0
- package/dist/pubsub/topics/products/catalog-visibility-changed/types.js +1 -0
- package/dist/pubsub/topics/products/composite-changed/types.d.ts +17 -0
- package/dist/pubsub/topics/products/composite-changed/types.js +1 -0
- package/dist/pubsub/topics/products/index.d.ts +2 -2
- package/dist/pubsub/topics/products/index.js +33 -18
- package/dist/pubsub/topics/products/inventory-changed/types.d.ts +18 -0
- package/dist/pubsub/topics/products/inventory-changed/types.js +1 -0
- package/dist/pubsub/topics/products/types.d.ts +16 -10
- package/dist/pubsub/topics/products/types.js +7 -4
- package/package.json +2 -2
- package/dist/actions/get-refund-plan/mockRows.d.ts +0 -6
- package/dist/actions/get-refund-plan/mockRows.js +0 -114
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { CFCompositePick } from '../../CommonTypes';
|
|
2
|
+
export interface GetCompositePriceParams {
|
|
3
|
+
/** The composite's variant — the one `addProductToCart` takes. */
|
|
4
|
+
variantId: string;
|
|
5
|
+
/** The picks so far, exactly as `addProductToCart({ composite })` would send them. */
|
|
6
|
+
composite: CFCompositePick[];
|
|
7
|
+
/** B41: the date and time a bookable pick is for, as `addProductToCart` takes it; without one a bookable pick is refused. */
|
|
8
|
+
compositeSlot?: {
|
|
9
|
+
startAt: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface GetCompositePriceResponse {
|
|
13
|
+
success: boolean;
|
|
14
|
+
/** Why these picks are refused — the same refusal and text `addProductToCart` would give. */
|
|
15
|
+
reason?: string;
|
|
16
|
+
/**
|
|
17
|
+
* What ONE composite with these picks adds to the cart, minor units: the line price the host would build plus the
|
|
18
|
+
* picked items' modifiers. Before discounts, fees and tax. Show it; do not recompute it. Absent when refused.
|
|
19
|
+
*/
|
|
20
|
+
total?: number;
|
|
21
|
+
/**
|
|
22
|
+
* The parts that still need picks before the composite can be added, and how many ("Pick 2 more"); empty when every
|
|
23
|
+
* required part is filled. Decided by the host — keep Add to cart disabled while this is not empty.
|
|
24
|
+
*/
|
|
25
|
+
missing: {
|
|
26
|
+
partId: string;
|
|
27
|
+
needed: number;
|
|
28
|
+
}[];
|
|
29
|
+
timestamp: string;
|
|
30
|
+
}
|
|
31
|
+
export type GetCompositePrice = (params: GetCompositePriceParams) => Promise<GetCompositePriceResponse>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { GetCurrentCart } from
|
|
1
|
+
import { GetCurrentCart } from './types';
|
|
2
2
|
export declare const mockGetCurrentCart: GetCurrentCart;
|
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import { MOCK_CART, safeSerialize } from
|
|
1
|
+
import { MOCK_CART, buildModifierRows, safeSerialize } from '../../demo/database';
|
|
2
|
+
import { extendPrice } from '@final-commerce/common';
|
|
2
3
|
export const mockGetCurrentCart = async () => {
|
|
3
|
-
console.log(
|
|
4
|
+
console.log('[Mock] getCurrentCart called');
|
|
5
|
+
// The mock keeps no discounts or fees per line: a line is its price × quantity plus its modifier rows.
|
|
6
|
+
const lineTotals = Object.fromEntries([
|
|
7
|
+
...MOCK_CART.products.map((line) => [
|
|
8
|
+
line.internalId ?? line.id,
|
|
9
|
+
extendPrice(line.price, line.quantity) + buildModifierRows(line.modifiers, line.quantity).modifiersTotal,
|
|
10
|
+
]),
|
|
11
|
+
...(MOCK_CART.customSales ?? []).map((sale) => [sale.id, extendPrice(sale.price, sale.quantity)]),
|
|
12
|
+
]);
|
|
4
13
|
return {
|
|
5
14
|
success: true,
|
|
6
15
|
cart: safeSerialize(MOCK_CART),
|
|
7
|
-
|
|
16
|
+
lineTotals,
|
|
17
|
+
timestamp: new Date().toISOString(),
|
|
8
18
|
};
|
|
9
19
|
};
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import { CFActiveCart } from
|
|
1
|
+
import { CFActiveCart } from '../../CommonTypes';
|
|
2
2
|
export interface GetCurrentCartResponse {
|
|
3
3
|
success: boolean;
|
|
4
4
|
cart: CFActiveCart;
|
|
5
|
+
/**
|
|
6
|
+
* What each line adds to the cart, minor units, keyed by the line's `internalId` (products, bookings) or `id`
|
|
7
|
+
* (custom sales): price × quantity − its own discounts + its own fees + its modifiers. Before the cart discount
|
|
8
|
+
* and tax; Σ = the cart's subtotal before non-revenue items. Show it; do not recompute it.
|
|
9
|
+
*/
|
|
10
|
+
lineTotals?: Record<string, number>;
|
|
5
11
|
timestamp: string;
|
|
6
12
|
}
|
|
7
13
|
export type GetCurrentCart = () => Promise<GetCurrentCartResponse>;
|
|
@@ -16,6 +16,18 @@ export const mockGetProductModifierSelections = async (params) => {
|
|
|
16
16
|
timestamp: new Date().toISOString(),
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
+
// Like the host (FT-83): a composite line's modifiers belong to its components.
|
|
20
|
+
if (line.components?.length) {
|
|
21
|
+
return {
|
|
22
|
+
success: false,
|
|
23
|
+
reason: 'The modifiers of a composite line cannot be edited yet: remove the line and add it again with its picks',
|
|
24
|
+
internalId: line.internalId,
|
|
25
|
+
selections: [],
|
|
26
|
+
rows: [],
|
|
27
|
+
modifiersTotal: 0,
|
|
28
|
+
timestamp: new Date().toISOString(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
19
31
|
// The priced rows already sit on the line; extending them by the line quantity is
|
|
20
32
|
// the host's job, not the flow's.
|
|
21
33
|
const { rows, modifiersTotal } = buildModifierRows(line.modifiers, line.quantity);
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { MOCK_PRODUCTS, safeSerialize } from '../../demo/database';
|
|
1
|
+
import { MOCK_PRODUCTS, mockCompositeAtOutlet, mockHiddenProductIds, safeSerialize } from '../../demo/database';
|
|
2
2
|
import { resolveUnit } from '@final-commerce/common';
|
|
3
3
|
export const mockGetProducts = async (params) => {
|
|
4
4
|
console.log('[Mock] getProducts called', params);
|
|
5
|
-
// Simple filter simulation
|
|
6
|
-
|
|
5
|
+
// Simple filter simulation. Like the host: a product hidden at this outlet is left out, and a composite offers only
|
|
6
|
+
// what is sold here.
|
|
7
|
+
const hidden = mockHiddenProductIds();
|
|
8
|
+
let products = MOCK_PRODUCTS.filter((p) => !hidden.has(p._id)).map((p) =>
|
|
9
|
+
// B43: the host never hands a composite modifiers of its own.
|
|
10
|
+
p.composite ? { ...p, modifiers: [], composite: mockCompositeAtOutlet(p.composite, hidden) } : p);
|
|
7
11
|
const query = params?.query || {};
|
|
8
12
|
// Same rule as the real host: a bookable service is not catalogue stock and is left out unless
|
|
9
13
|
// the query names `productType`. Mirrored here on purpose — a mock that hands back services the
|
|
@@ -27,7 +27,7 @@ export interface GetProductsParams {
|
|
|
27
27
|
};
|
|
28
28
|
status?: string;
|
|
29
29
|
/**
|
|
30
|
-
* `simple` | `variable` | `booking`. Naming it at all switches OFF the default exclusion
|
|
30
|
+
* `simple` | `variable` | `booking` | `composite`. Naming it at all switches OFF the default exclusion
|
|
31
31
|
* of bookable services — including `{ $ne: 'booking' }`, which then means what it says.
|
|
32
32
|
*/
|
|
33
33
|
productType?: string | {
|
|
@@ -15,9 +15,5 @@ import { GetRefundPlan } from './types';
|
|
|
15
15
|
* and no company cash-rounding setting, so it always describes a FULL refund
|
|
16
16
|
* with `rounding: 0` and no cash `payout`. Against real kaching the allocation
|
|
17
17
|
* tracks the live selection and carries the drawer snap.
|
|
18
|
-
*
|
|
19
|
-
* `rows` / `allocation.breakdown` follow the engine's reading of the order
|
|
20
|
-
* (`line.total` includes the line's tax) but prorate by plain quantity share;
|
|
21
|
-
* the engine's remainder carry across earlier partial refunds is not modelled.
|
|
22
18
|
*/
|
|
23
19
|
export declare const mockGetRefundPlan: GetRefundPlan;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { MOCK_ORDERS } from '../../demo/database';
|
|
2
|
-
import { mockRefundBreakdown, mockRefundRows } from './mockRows';
|
|
3
2
|
/**
|
|
4
3
|
* Demo derivation of the runtime `getRefundPlan`. Builds the per-source rows
|
|
5
4
|
* from the mock order's `paymentMethods`, mirroring the runtime's capacity
|
|
@@ -16,10 +15,6 @@ import { mockRefundBreakdown, mockRefundRows } from './mockRows';
|
|
|
16
15
|
* and no company cash-rounding setting, so it always describes a FULL refund
|
|
17
16
|
* with `rounding: 0` and no cash `payout`. Against real kaching the allocation
|
|
18
17
|
* tracks the live selection and carries the drawer snap.
|
|
19
|
-
*
|
|
20
|
-
* `rows` / `allocation.breakdown` follow the engine's reading of the order
|
|
21
|
-
* (`line.total` includes the line's tax) but prorate by plain quantity share;
|
|
22
|
-
* the engine's remainder carry across earlier partial refunds is not modelled.
|
|
23
18
|
*/
|
|
24
19
|
export const mockGetRefundPlan = async (params) => {
|
|
25
20
|
console.log('[Mock] getRefundPlan called', params);
|
|
@@ -67,19 +62,16 @@ export const mockGetRefundPlan = async (params) => {
|
|
|
67
62
|
requiresGiftCardDestination: s.paymentType === 'redeem',
|
|
68
63
|
}));
|
|
69
64
|
const budget = legs.reduce((sum, l) => sum + l.amount, 0);
|
|
70
|
-
const rows = mockRefundRows(order);
|
|
71
65
|
return {
|
|
72
66
|
success: true,
|
|
73
67
|
orderId: order._id,
|
|
74
68
|
sources,
|
|
75
|
-
rows,
|
|
76
69
|
allocation: {
|
|
77
70
|
budget,
|
|
78
71
|
// No cash rounding in the demo, so the goods value and the budget agree.
|
|
79
72
|
itemTotal: budget,
|
|
80
73
|
rounding: 0,
|
|
81
74
|
legs,
|
|
82
|
-
breakdown: mockRefundBreakdown(rows, params?.items),
|
|
83
75
|
},
|
|
84
76
|
// Demo: no prior refunds, so remaining = captured minus the non-revenue load.
|
|
85
77
|
remainingRefundable: Math.max(0, totalCaptured - nonRefundableLiability),
|
|
@@ -79,81 +79,6 @@ export interface RefundPlanLeg {
|
|
|
79
79
|
rounding: number;
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
|
-
export type RefundPlanRowType = 'product' | 'customSale' | 'fee' | 'tip';
|
|
83
|
-
/** One tax rate's share of a row's refund (minor units). */
|
|
84
|
-
export interface RefundPlanTaxLine {
|
|
85
|
-
name: string;
|
|
86
|
-
/** Decimal rate as stored on the order (e.g. `0.15`), when the order recorded one. */
|
|
87
|
-
percentage?: number;
|
|
88
|
-
amount: number;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* The money a row refunds, split the way a receipt shows it. Every field is
|
|
92
|
-
* minor units and already rounded; `subtotal − itemDiscount − cartDiscount +
|
|
93
|
-
* tax === total` always holds. DISPLAY IT — never re-add or prorate it.
|
|
94
|
-
*/
|
|
95
|
-
export interface RefundPlanAmounts {
|
|
96
|
-
/** Before discounts, tax excluded. For a tip row, the tip itself. */
|
|
97
|
-
subtotal: number;
|
|
98
|
-
/** Per-item discounts on the refunded quantity (positive). */
|
|
99
|
-
itemDiscount: number;
|
|
100
|
-
/** Cart discount share on the refunded quantity (positive). */
|
|
101
|
-
cartDiscount: number;
|
|
102
|
-
/** Tax on the refunded quantity. Zero for a tip. */
|
|
103
|
-
tax: number;
|
|
104
|
-
/** `tax` per rate — the price breakdown's tax lines. */
|
|
105
|
-
taxes: RefundPlanTaxLine[];
|
|
106
|
-
/** What the row refunds. */
|
|
107
|
-
total: number;
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* One refundable row of the order — a line item, custom sale, cart fee or tip —
|
|
111
|
-
* served ready to render. Rows with nothing left to refund are not listed.
|
|
112
|
-
*/
|
|
113
|
-
export interface RefundPlanRow {
|
|
114
|
-
type: RefundPlanRowType;
|
|
115
|
-
/** The key `items[].itemKey` takes, for this call and for `processPartialRefund`. */
|
|
116
|
-
itemKey: string;
|
|
117
|
-
/** Line or fee name; `Tip` for a tip. */
|
|
118
|
-
label: string;
|
|
119
|
-
/** Product lines only, when the order recorded them. */
|
|
120
|
-
sku?: string;
|
|
121
|
-
attributes?: string;
|
|
122
|
-
/** Tip rows only: the tender that took the tip (`card`, `cash`, …). */
|
|
123
|
-
paymentType?: string;
|
|
124
|
-
/** Quantity originally sold. Fees and tips are `1` — all or nothing. */
|
|
125
|
-
quantity: number;
|
|
126
|
-
/** Quantity still refundable — the stepper's max. */
|
|
127
|
-
refundableQuantity: number;
|
|
128
|
-
/** The money for refunding ALL of `refundableQuantity`. */
|
|
129
|
-
amounts: RefundPlanAmounts;
|
|
130
|
-
}
|
|
131
|
-
/** One selected row and what refunding the selected quantity of it moves. */
|
|
132
|
-
export interface RefundPlanSelectedRow {
|
|
133
|
-
type: RefundPlanRowType;
|
|
134
|
-
itemKey: string;
|
|
135
|
-
quantity: number;
|
|
136
|
-
amounts: RefundPlanAmounts;
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* The selection's goods value broken down for display. Minor units;
|
|
140
|
-
* `items − discounts + fees + tax + tip === total === allocation.itemTotal`.
|
|
141
|
-
*/
|
|
142
|
-
export interface RefundPlanTotals {
|
|
143
|
-
/** Σ selected line subtotals (before discounts, tax excluded). */
|
|
144
|
-
items: number;
|
|
145
|
-
/** Σ item + cart discounts on the selected lines (positive). */
|
|
146
|
-
discounts: number;
|
|
147
|
-
/** Σ selected cart fees, tax excluded. */
|
|
148
|
-
fees: number;
|
|
149
|
-
tax: number;
|
|
150
|
-
tip: number;
|
|
151
|
-
total: number;
|
|
152
|
-
}
|
|
153
|
-
export interface RefundPlanBreakdown {
|
|
154
|
-
rows: RefundPlanSelectedRow[];
|
|
155
|
-
totals: RefundPlanTotals;
|
|
156
|
-
}
|
|
157
82
|
/**
|
|
158
83
|
* The engine's own allocation of the CURRENT refund selection across the
|
|
159
84
|
* order's captures — what a flow renders and submits instead of computing a
|
|
@@ -181,25 +106,11 @@ export interface RefundPlanAllocation {
|
|
|
181
106
|
rounding: number;
|
|
182
107
|
/** One leg per source that receives money. Submit as `legs`, unchanged. */
|
|
183
108
|
legs: RefundPlanLeg[];
|
|
184
|
-
/**
|
|
185
|
-
* What the selection refunds, per row and in total — the numbers a refund
|
|
186
|
-
* dialog's rows and footer render. Optional so an older runtime still
|
|
187
|
-
* type-checks; kaching 1.12.1+ always sends it.
|
|
188
|
-
*/
|
|
189
|
-
breakdown?: RefundPlanBreakdown;
|
|
190
109
|
}
|
|
191
110
|
export interface GetRefundPlanResponse {
|
|
192
111
|
success: boolean;
|
|
193
112
|
orderId: string;
|
|
194
113
|
sources: RefundPlanSource[];
|
|
195
|
-
/**
|
|
196
|
-
* Every row still refundable on the order, with the money for refunding all
|
|
197
|
-
* of it — what a refund dialog lists before anything is selected. Replaces
|
|
198
|
-
* reading line totals off the order and deciding whether they include tax.
|
|
199
|
-
* Optional so an older runtime still type-checks; kaching 1.12.1+ always
|
|
200
|
-
* sends it.
|
|
201
|
-
*/
|
|
202
|
-
rows?: RefundPlanRow[];
|
|
203
114
|
/**
|
|
204
115
|
* Ready-to-submit allocation of the current selection. Present only when a
|
|
205
116
|
* refund selection exists on the active order. See {@link RefundPlanAllocation}.
|
|
@@ -30,6 +30,18 @@ export const mockSetProductModifierSelections = async (params) => {
|
|
|
30
30
|
timestamp: new Date().toISOString(),
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
+
// Like the host (FT-83): a composite line's modifiers belong to its components.
|
|
34
|
+
if (line.components?.length) {
|
|
35
|
+
return {
|
|
36
|
+
success: false,
|
|
37
|
+
reason: 'The modifiers of a composite line cannot be edited yet: remove the line and add it again with its picks',
|
|
38
|
+
internalId: line.internalId,
|
|
39
|
+
selections: [],
|
|
40
|
+
rows: [],
|
|
41
|
+
modifiersTotal: 0,
|
|
42
|
+
timestamp: new Date().toISOString(),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
33
45
|
// Full replacement — passing [] clears the rows. The line drops the product's resolved
|
|
34
46
|
// stack when it is built, so the menu is re-read from the catalogue to price against.
|
|
35
47
|
// Cart totals are not touched, for the same reason addProductToCart does not move them
|
package/dist/demo/database.d.ts
CHANGED
|
@@ -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, CFBooking, CFBookingAvailability, CFBookingResource, CFCartLineModifier, CFProdModifierBreakdown, CFModifierSelection, CFResolvedModifier } from '../CommonTypes';
|
|
5
|
+
import { CFActiveCompany, CFActiveUser, CFActiveStation, CFActiveOutlet, CFActiveOrder, CFCustomer, CFProduct, CFComposite, CFCartLineComponent, CFActiveCart, CFCategory, CFActiveProduct, CFSession, CFActiveRefundDetails, CFSmartGridLayout, CFBooking, CFBookingAvailability, CFBookingResource, CFBookingSlot, CFCartLineModifier, CFProdModifierBreakdown, 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 {
|
|
@@ -19,6 +19,12 @@ export interface MockDatabaseConfig {
|
|
|
19
19
|
bookingResources?: CFBookingResource[];
|
|
20
20
|
/** Windows already taken when the dataset loads, so a calendar does not open empty. */
|
|
21
21
|
bookings?: CFBooking[];
|
|
22
|
+
/** Products hidden at an outlet (the host's `catalog-visibility`, product-level rows). Empty = sold everywhere. */
|
|
23
|
+
catalogVisibility?: MockCatalogVisibility[];
|
|
24
|
+
}
|
|
25
|
+
export interface MockCatalogVisibility {
|
|
26
|
+
productId: string;
|
|
27
|
+
outletId: string;
|
|
22
28
|
}
|
|
23
29
|
export declare const MOCK_COMPANY: CFActiveCompany;
|
|
24
30
|
export declare const MOCK_OUTLET_MAIN: CFActiveOutlet;
|
|
@@ -64,8 +70,40 @@ export declare const MOCK_OUTLETS: import("@final-commerce/common/pos-types").Ac
|
|
|
64
70
|
export declare const MOCK_CUSTOMERS: import("@final-commerce/common/pos-types").ActiveCustomer[];
|
|
65
71
|
export declare const MOCK_CATEGORIES: import("@final-commerce/common/pos-types").Category[];
|
|
66
72
|
export declare const MOCK_PRODUCT_HAIRCUT: CFProduct;
|
|
73
|
+
export declare const MOCK_PRODUCT_PASTE_TRIO: CFProduct;
|
|
67
74
|
export declare const MOCK_PRODUCTS: import("@final-commerce/common/pos-types").FullProduct[];
|
|
68
75
|
export declare const MOCK_ORDERS: import("@final-commerce/common/pos-types").ActiveOrder[];
|
|
76
|
+
/** Nothing hidden by default; `setMockDatabase({ catalogVisibility })` hides products at an outlet. */
|
|
77
|
+
export declare const MOCK_CATALOG_VISIBILITY: MockCatalogVisibility[];
|
|
78
|
+
/**
|
|
79
|
+
* The host's cart stock check for composites (kaching `compositeStockRefusal`), over the demo shelves — a copy: what the
|
|
80
|
+
* cart takes of each component's variant (plain lines and components × their line quantity) against its shelf here.
|
|
81
|
+
* The demo has no pools or units, so it counts per variant; untracked and backordered variants are never short.
|
|
82
|
+
*/
|
|
83
|
+
export declare function mockCompositeStockRefusal(products: {
|
|
84
|
+
variantId: string;
|
|
85
|
+
quantity: number;
|
|
86
|
+
components?: CFCartLineComponent[];
|
|
87
|
+
}[]): string | null;
|
|
88
|
+
/** Product ids hidden at the demo's active outlet. */
|
|
89
|
+
export declare const mockHiddenProductIds: () => Set<string>;
|
|
90
|
+
/**
|
|
91
|
+
* The host's outlet rules for a composite (kaching `resolveComposite`), over the demo's static view — a copy, change both
|
|
92
|
+
* in one step: an item whose product is hidden here is left out, a category item keeps only products sold here (none →
|
|
93
|
+
* `'empty'`), an Optional part with nothing to pick is not offered, a required one with nothing makes it unavailable,
|
|
94
|
+
* and the D36 default and "from" price are read off what is left.
|
|
95
|
+
*/
|
|
96
|
+
/** B41: the seats a bookable demo product has free in the slot starting at `startAt` — as the host reads them. */
|
|
97
|
+
export declare function mockBookableWindow(productId: string, startAt: string): {
|
|
98
|
+
free: number;
|
|
99
|
+
slot: CFBookingSlot | null;
|
|
100
|
+
};
|
|
101
|
+
export declare const mockIsBookable: (productId: string) => boolean;
|
|
102
|
+
export declare function mockCompositeAtOutlet(composite: Omit<CFComposite, 'needsDate'>, hidden?: Set<string>,
|
|
103
|
+
/** B41: the window bookable items are judged in; absent = `by_date`. */
|
|
104
|
+
slot?: {
|
|
105
|
+
startAt: string;
|
|
106
|
+
}): CFComposite;
|
|
69
107
|
export declare const MOCK_PARKED_ORDERS: CFActiveOrder[];
|
|
70
108
|
export declare let MOCK_USER: CFActiveUser;
|
|
71
109
|
export declare let MOCK_STATION: CFActiveStation;
|
package/dist/demo/database.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Stores mock data that mimics the Render environment
|
|
4
4
|
*/
|
|
5
5
|
import { CFProductType, CFUserTypes, CurrencyCode, } from '../CommonTypes';
|
|
6
|
-
import { extendPrice, resolveUnit, toBase } from '@final-commerce/common';
|
|
6
|
+
import { cheapestCompositeItem, cheapestCompositePrice, extendPrice, resolveUnit, toBase, } from '@final-commerce/common';
|
|
7
7
|
import { BookingRatePeriod, BookingResourceKind, BookingType, ReservationStatus } from '@final-commerce/common';
|
|
8
8
|
export * from './mocks';
|
|
9
9
|
// Asset Imports - Using Remote URLs to avoid build complexity with asset copying
|
|
@@ -228,6 +228,7 @@ const createSimpleProduct = (id, name, price, image, categories, description) =>
|
|
|
228
228
|
maxPrice: price,
|
|
229
229
|
status: 'active',
|
|
230
230
|
productType: CFProductType.SIMPLE,
|
|
231
|
+
composite: null,
|
|
231
232
|
taxTable: 'tax_standard',
|
|
232
233
|
description,
|
|
233
234
|
images: [image],
|
|
@@ -263,6 +264,7 @@ const createVariableProduct = (id, name, basePrice, largePrice, image, categorie
|
|
|
263
264
|
maxPrice: largePrice,
|
|
264
265
|
status: 'active',
|
|
265
266
|
productType: CFProductType.VARIABLE,
|
|
267
|
+
composite: null,
|
|
266
268
|
taxTable: 'tax_standard',
|
|
267
269
|
description,
|
|
268
270
|
images: [image],
|
|
@@ -658,7 +660,106 @@ export const MOCK_PRODUCT_HAIRCUT = {
|
|
|
658
660
|
productType: CFProductType.BOOKING,
|
|
659
661
|
bookingRulesId: 'rule_salon_30',
|
|
660
662
|
};
|
|
663
|
+
// A composite (FT-83) as the host serves it: the merchant's price 27.00, items add only their upcharges (B31); each item
|
|
664
|
+
// keeps its own tax (`taxTable` empty). One item points at a deleted variant (unavailable), one is a whole category.
|
|
665
|
+
export const MOCK_PRODUCT_PASTE_TRIO = {
|
|
666
|
+
...createSimpleProduct('prod_paste_trio', 'Paste Trio', 2700, basilAlmondImg, [MOCK_CATEGORY_PASTES], 'Pick a paste and a spicy one.'),
|
|
667
|
+
productType: CFProductType.COMPOSITE,
|
|
668
|
+
taxTable: '',
|
|
669
|
+
// B43: a composite has no modifiers of its own; its items carry theirs.
|
|
670
|
+
modifiers: [],
|
|
671
|
+
composite: {
|
|
672
|
+
available: true,
|
|
673
|
+
basePrice: 2700,
|
|
674
|
+
fromPrice: 2800,
|
|
675
|
+
parts: [
|
|
676
|
+
{
|
|
677
|
+
_id: 'part_paste',
|
|
678
|
+
name: 'Paste',
|
|
679
|
+
required: true,
|
|
680
|
+
min: 1,
|
|
681
|
+
max: 1,
|
|
682
|
+
defaultPick: { itemId: 'item_basil', variantId: 'prod_basil_almond_var_main' },
|
|
683
|
+
items: [
|
|
684
|
+
{
|
|
685
|
+
_id: 'item_basil',
|
|
686
|
+
variantId: 'prod_basil_almond_var_main',
|
|
687
|
+
categoryId: null,
|
|
688
|
+
name: 'Basil Almond Paste',
|
|
689
|
+
extraCharge: 0,
|
|
690
|
+
quantity: 1,
|
|
691
|
+
cost: 0,
|
|
692
|
+
unavailable: null,
|
|
693
|
+
choices: [
|
|
694
|
+
{
|
|
695
|
+
productId: 'prod_basil_almond',
|
|
696
|
+
variantId: 'prod_basil_almond_var_main',
|
|
697
|
+
name: 'Basil Almond Paste',
|
|
698
|
+
attributes: [],
|
|
699
|
+
// The picked product's own modifiers (B28): taxed at its table, sent in the pick.
|
|
700
|
+
modifiers: [
|
|
701
|
+
{
|
|
702
|
+
_id: 'mod_texture',
|
|
703
|
+
name: 'Texture',
|
|
704
|
+
selectionType: 'single',
|
|
705
|
+
required: false,
|
|
706
|
+
sortOrder: 0,
|
|
707
|
+
choices: [
|
|
708
|
+
{ _id: 'choice_smooth', name: 'Smooth', price: 0, sortOrder: 0 },
|
|
709
|
+
{ _id: 'choice_chunky', name: 'Chunky', price: 50, sortOrder: 1 },
|
|
710
|
+
],
|
|
711
|
+
},
|
|
712
|
+
],
|
|
713
|
+
},
|
|
714
|
+
],
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
_id: 'item_gone',
|
|
718
|
+
variantId: 'prod_gone_var_main',
|
|
719
|
+
categoryId: null,
|
|
720
|
+
name: '',
|
|
721
|
+
extraCharge: 0,
|
|
722
|
+
quantity: 1,
|
|
723
|
+
cost: 0,
|
|
724
|
+
unavailable: 'deleted',
|
|
725
|
+
choices: [],
|
|
726
|
+
},
|
|
727
|
+
],
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
_id: 'part_spicy',
|
|
731
|
+
name: null,
|
|
732
|
+
required: true,
|
|
733
|
+
min: 1,
|
|
734
|
+
max: 2,
|
|
735
|
+
// "Pick up to 2": no preselected pick (B29 = D36) — the till shows how many are still needed.
|
|
736
|
+
defaultPick: null,
|
|
737
|
+
items: [
|
|
738
|
+
{
|
|
739
|
+
_id: 'item_spicy',
|
|
740
|
+
variantId: null,
|
|
741
|
+
categoryId: MOCK_CATEGORY_SPICY.id,
|
|
742
|
+
name: 'Spicy',
|
|
743
|
+
extraCharge: 100,
|
|
744
|
+
quantity: 1,
|
|
745
|
+
cost: 100,
|
|
746
|
+
unavailable: null,
|
|
747
|
+
choices: [
|
|
748
|
+
{
|
|
749
|
+
productId: 'prod_habanero',
|
|
750
|
+
variantId: 'prod_habanero_var_main',
|
|
751
|
+
name: 'Habanero Paste',
|
|
752
|
+
attributes: [],
|
|
753
|
+
},
|
|
754
|
+
],
|
|
755
|
+
},
|
|
756
|
+
],
|
|
757
|
+
},
|
|
758
|
+
],
|
|
759
|
+
},
|
|
760
|
+
};
|
|
661
761
|
export const MOCK_PRODUCTS = [
|
|
762
|
+
MOCK_PRODUCT_PASTE_TRIO,
|
|
662
763
|
MOCK_PRODUCT_BASIL_ALMOND,
|
|
663
764
|
MOCK_PRODUCT_BEER,
|
|
664
765
|
MOCK_PRODUCT_BEET,
|
|
@@ -676,6 +777,114 @@ export const MOCK_PRODUCTS = [
|
|
|
676
777
|
MOCK_PRODUCT_HAIRCUT,
|
|
677
778
|
];
|
|
678
779
|
export const MOCK_ORDERS = [MOCK_ORDER_1, MOCK_ORDER_2, MOCK_ORDER_3, MOCK_ORDER_4];
|
|
780
|
+
/** Nothing hidden by default; `setMockDatabase({ catalogVisibility })` hides products at an outlet. */
|
|
781
|
+
export const MOCK_CATALOG_VISIBILITY = [];
|
|
782
|
+
/**
|
|
783
|
+
* The host's cart stock check for composites (kaching `compositeStockRefusal`), over the demo shelves — a copy: what the
|
|
784
|
+
* cart takes of each component's variant (plain lines and components × their line quantity) against its shelf here.
|
|
785
|
+
* The demo has no pools or units, so it counts per variant; untracked and backordered variants are never short.
|
|
786
|
+
*/
|
|
787
|
+
export function mockCompositeStockRefusal(products) {
|
|
788
|
+
const components = products.flatMap((product) => product.components ?? []);
|
|
789
|
+
const demand = new Map();
|
|
790
|
+
for (const product of products) {
|
|
791
|
+
if (product.components)
|
|
792
|
+
for (const component of product.components)
|
|
793
|
+
demand.set(component.variantId, (demand.get(component.variantId) ?? 0) + component.quantity * product.quantity);
|
|
794
|
+
else
|
|
795
|
+
demand.set(product.variantId, (demand.get(product.variantId) ?? 0) + product.quantity);
|
|
796
|
+
}
|
|
797
|
+
for (const component of components) {
|
|
798
|
+
const variant = MOCK_PRODUCTS.flatMap((product) => product.variants).find((candidate) => candidate._id === component.variantId);
|
|
799
|
+
if (!variant?.manageStock || variant.allowBackorder)
|
|
800
|
+
continue;
|
|
801
|
+
const left = variant.inventory?.find((row) => row.outletId === MOCK_OUTLET.id)?.stock ?? 0;
|
|
802
|
+
if ((demand.get(component.variantId) ?? 0) > left)
|
|
803
|
+
return `${component.name}: only ${left} left`;
|
|
804
|
+
}
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
/** Product ids hidden at the demo's active outlet. */
|
|
808
|
+
export const mockHiddenProductIds = () => new Set(MOCK_CATALOG_VISIBILITY.filter((row) => row.outletId === MOCK_OUTLET.id).map((row) => row.productId));
|
|
809
|
+
/**
|
|
810
|
+
* The host's outlet rules for a composite (kaching `resolveComposite`), over the demo's static view — a copy, change both
|
|
811
|
+
* in one step: an item whose product is hidden here is left out, a category item keeps only products sold here (none →
|
|
812
|
+
* `'empty'`), an Optional part with nothing to pick is not offered, a required one with nothing makes it unavailable,
|
|
813
|
+
* and the D36 default and "from" price are read off what is left.
|
|
814
|
+
*/
|
|
815
|
+
/** B41: the seats a bookable demo product has free in the slot starting at `startAt` — as the host reads them. */
|
|
816
|
+
export function mockBookableWindow(productId, startAt) {
|
|
817
|
+
const at = new Date(startAt);
|
|
818
|
+
const slot = mockBookingAvailability(productId, at, new Date(+at + 86400000)).slots.find((candidate) => +new Date(candidate.startAt) === +at && candidate.canStart) ?? null;
|
|
819
|
+
return { free: slot?.free ?? 0, slot };
|
|
820
|
+
}
|
|
821
|
+
export const mockIsBookable = (productId) => MOCK_PRODUCTS.find((product) => product._id === productId)?.productType === CFProductType.BOOKING;
|
|
822
|
+
export function mockCompositeAtOutlet(composite, hidden = mockHiddenProductIds(),
|
|
823
|
+
/** B41: the window bookable items are judged in; absent = `by_date`. */
|
|
824
|
+
slot) {
|
|
825
|
+
// B41: a bookable item is judged by seats in the window, never by stock; with no window it waits for a date.
|
|
826
|
+
const bookable = (item) => !!item.variantId && item.choices.some((choice) => mockIsBookable(choice.productId));
|
|
827
|
+
const seatsFor = (item) => !slot
|
|
828
|
+
? 'by_date'
|
|
829
|
+
: mockBookableWindow(item.choices[0].productId, slot.startAt).free >= item.quantity
|
|
830
|
+
? null
|
|
831
|
+
: 'fully_booked';
|
|
832
|
+
const offered = (item) => !item.unavailable || item.unavailable === 'by_date';
|
|
833
|
+
// B35: a tracked variant without `quantity` on the demo shelf here (and no backorder) is sold out.
|
|
834
|
+
const soldOut = (variantId, quantity) => {
|
|
835
|
+
const variant = MOCK_PRODUCTS.flatMap((product) => product.variants).find((candidate) => candidate._id === variantId);
|
|
836
|
+
if (!variant?.manageStock || variant.allowBackorder)
|
|
837
|
+
return false;
|
|
838
|
+
return (variant.inventory?.find((row) => row.outletId === MOCK_OUTLET.id)?.stock ?? 0) < quantity;
|
|
839
|
+
};
|
|
840
|
+
// B36e: "N left" on a unit-sold tracked choice — the demo shelf already reads in the variant's own unit.
|
|
841
|
+
const withStockLeft = (choice) => {
|
|
842
|
+
const variant = MOCK_PRODUCTS.flatMap((product) => product.variants).find((candidate) => candidate._id === choice.variantId);
|
|
843
|
+
const onHand = variant?.inventory?.find((row) => row.outletId === MOCK_OUTLET.id)?.stock;
|
|
844
|
+
return variant?.unitId && variant.manageStock && typeof onHand === 'number'
|
|
845
|
+
? { ...choice, stockLeft: onHand }
|
|
846
|
+
: choice;
|
|
847
|
+
};
|
|
848
|
+
const upcharge = (item) => ({
|
|
849
|
+
extraCharge: item.extraCharge,
|
|
850
|
+
quantity: item.quantity,
|
|
851
|
+
available: offered(item),
|
|
852
|
+
});
|
|
853
|
+
const parts = composite.parts
|
|
854
|
+
.map((part) => {
|
|
855
|
+
const items = part.items.flatMap((item) => {
|
|
856
|
+
if (item.variantId) {
|
|
857
|
+
if (item.choices.some((choice) => hidden.has(choice.productId)))
|
|
858
|
+
return [];
|
|
859
|
+
if (!item.unavailable && bookable(item))
|
|
860
|
+
return [{ ...item, unavailable: seatsFor(item) }];
|
|
861
|
+
return item.unavailable || !soldOut(item.variantId, item.quantity)
|
|
862
|
+
? [{ ...item, choices: item.choices.map(withStockLeft) }]
|
|
863
|
+
: [{ ...item, unavailable: 'out_of_stock', choices: [] }];
|
|
864
|
+
}
|
|
865
|
+
const sold = item.choices.filter((choice) => !hidden.has(choice.productId));
|
|
866
|
+
const choices = sold.filter((choice) => !soldOut(choice.variantId, item.quantity));
|
|
867
|
+
const unavailable = item.unavailable ?? (!sold.length ? 'empty' : !choices.length ? 'out_of_stock' : null);
|
|
868
|
+
return [{ ...item, choices: unavailable ? [] : choices.map(withStockLeft), unavailable }];
|
|
869
|
+
});
|
|
870
|
+
const index = part.min === 1 && part.max === 1 ? cheapestCompositeItem(items.map(upcharge)) : undefined;
|
|
871
|
+
const cheapest = index === undefined ? undefined : items[index];
|
|
872
|
+
return {
|
|
873
|
+
...part,
|
|
874
|
+
items,
|
|
875
|
+
defaultPick: cheapest?.variantId ? { itemId: cheapest._id, variantId: cheapest.variantId } : null,
|
|
876
|
+
};
|
|
877
|
+
})
|
|
878
|
+
.filter((part) => part.min > 0 || part.items.some(offered));
|
|
879
|
+
const cheapest = cheapestCompositePrice(parts.map((part) => ({ min: part.min, items: part.items.map(upcharge) })));
|
|
880
|
+
return {
|
|
881
|
+
...composite,
|
|
882
|
+
available: composite.available && parts.every((part) => part.min === 0 || part.items.some(offered)),
|
|
883
|
+
needsDate: parts.some((part) => part.items.some(bookable)),
|
|
884
|
+
fromPrice: composite.basePrice === null || cheapest === undefined ? null : composite.basePrice + cheapest,
|
|
885
|
+
parts,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
679
888
|
export const MOCK_PARKED_ORDERS = [MOCK_PARKED_ORDER_1, MOCK_PARKED_ORDER_2];
|
|
680
889
|
// Compatibility Exports (reassigned by setMockDatabase)
|
|
681
890
|
export let MOCK_USER = MOCK_USERS[0];
|
|
@@ -760,6 +969,9 @@ export function setMockDatabase(config) {
|
|
|
760
969
|
if (config.bookings !== undefined) {
|
|
761
970
|
MOCK_BOOKINGS.splice(0, MOCK_BOOKINGS.length, ...config.bookings);
|
|
762
971
|
}
|
|
972
|
+
if (config.catalogVisibility !== undefined) {
|
|
973
|
+
MOCK_CATALOG_VISIBILITY.splice(0, MOCK_CATALOG_VISIBILITY.length, ...config.catalogVisibility);
|
|
974
|
+
}
|
|
763
975
|
if (MOCK_OUTLETS.length > 0) {
|
|
764
976
|
Object.assign(MOCK_OUTLET_MAIN, MOCK_OUTLETS[0]);
|
|
765
977
|
if (MOCK_OUTLET_MAIN.id === undefined && MOCK_OUTLET_MAIN._id !== undefined) {
|
|
@@ -1132,6 +1344,7 @@ export const buildModifierRows = (modifiers, lineQuantity) => {
|
|
|
1132
1344
|
amount: extendPrice(modifier.unitPrice * modifier.quantity, quantity),
|
|
1133
1345
|
tax: 0,
|
|
1134
1346
|
...(modifier.taxTableId ? { taxTableId: modifier.taxTableId } : {}),
|
|
1347
|
+
...(modifier.componentIndex !== undefined ? { componentIndex: modifier.componentIndex } : {}),
|
|
1135
1348
|
}));
|
|
1136
1349
|
return { rows, modifiersTotal: rows.reduce((sum, row) => sum + row.amount, 0) };
|
|
1137
1350
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export declare const command: {
|
|
|
29
29
|
readonly addProductNote: import(".").AddProductNote;
|
|
30
30
|
readonly addProductFee: import(".").AddProductFee;
|
|
31
31
|
readonly getProductModifierSelections: import(".").GetProductModifierSelections;
|
|
32
|
+
readonly getCompositePrice: import(".").GetCompositePrice;
|
|
33
|
+
readonly getCompositeAvailability: import(".").GetCompositeAvailability;
|
|
32
34
|
readonly setProductModifierSelections: import(".").SetProductModifierSelections;
|
|
33
35
|
readonly setActiveProductFee: import(".").SetActiveProductFee;
|
|
34
36
|
readonly setActiveProductDiscount: import(".").SetActiveProductDiscount;
|
|
@@ -160,7 +162,7 @@ export type { CalculateRefundTotal, CalculateRefundTotalParams, CalculateRefundT
|
|
|
160
162
|
export type { GetRemainingRefundableQuantities, GetRemainingRefundableQuantitiesParams, GetRemainingRefundableQuantitiesResponse, } from './actions/get-remaining-refundable-quantities/types';
|
|
161
163
|
export type { ProcessPartialRefund, ProcessPartialRefundParams, ProcessPartialRefundResponse, } from './actions/process-partial-refund/types';
|
|
162
164
|
export type { RedeemRefund, RedeemRefundParams, RedeemRefundResponse } from './actions/redeem-refund/types';
|
|
163
|
-
export type { GetRefundPlan, GetRefundPlanParams, GetRefundPlanResponse, RefundPlanSource, RefundPlanAllocation, RefundPlanLeg,
|
|
165
|
+
export type { GetRefundPlan, GetRefundPlanParams, GetRefundPlanResponse, RefundPlanSource, RefundPlanAllocation, RefundPlanLeg, } from './actions/get-refund-plan/types';
|
|
164
166
|
export type { CheckPermission, CheckPermissionParams, CheckPermissionResponse } from './actions/check-permission/types';
|
|
165
167
|
export type { InitiateRefund, InitiateRefundParams, InitiateRefundResponse } from './actions/initiate-refund/types';
|
|
166
168
|
export type { GetCurrentCart, GetCurrentCartResponse } from './actions/get-current-cart/types';
|
|
@@ -174,6 +176,8 @@ export type { GetFinalContext, GetFinalContextResponse } from './actions/get-fin
|
|
|
174
176
|
export type { AddProductNote, AddProductNoteParams, AddProductNoteResponse } from './actions/add-product-note/types';
|
|
175
177
|
export type { AddProductFee, AddProductFeeParams, AddProductFeeResponse } from './actions/add-product-fee/types';
|
|
176
178
|
export type { GetProductModifierSelections, GetProductModifierSelectionsParams, GetProductModifierSelectionsResponse, ModifierSelection, ModifierChoiceSelection, } from './actions/get-product-modifier-selections/types';
|
|
179
|
+
export type { GetCompositePrice, GetCompositePriceParams, GetCompositePriceResponse, } from './actions/get-composite-price/types';
|
|
180
|
+
export type { GetCompositeAvailability, GetCompositeAvailabilityParams, GetCompositeAvailabilityResponse, CFCompositeSlot, } from './actions/get-composite-availability/types';
|
|
177
181
|
export type { SetProductModifierSelections, SetProductModifierSelectionsParams, SetProductModifierSelectionsResponse, } from './actions/set-product-modifier-selections/types';
|
|
178
182
|
export type { SetActiveProductFee, SetActiveProductFeeParams, SetActiveProductFeeResponse, } from './actions/set-active-product-fee/types';
|
|
179
183
|
export type { SetActiveProductDiscount, SetActiveProductDiscountParams, SetActiveProductDiscountResponse, } from './actions/set-active-product-discount/types';
|