@final-commerce/command-frame 0.9.0-staging.1 → 0.9.0
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 +1 -25
- package/dist/actions/add-product-fee/mock.d.ts +1 -1
- package/dist/actions/add-product-fee/mock.js +9 -15
- package/dist/actions/add-product-to-cart/mock.d.ts +0 -23
- package/dist/actions/add-product-to-cart/mock.js +1 -300
- package/dist/actions/add-product-to-cart/types.d.ts +1 -27
- package/dist/actions/adjust-inventory/mock.js +20 -0
- package/dist/actions/adjust-inventory/types.d.ts +13 -0
- package/dist/actions/adjust-inventory/types.js +1 -0
- package/dist/actions/get-current-cart/mock.d.ts +1 -1
- package/dist/actions/get-current-cart/mock.js +3 -13
- package/dist/actions/get-current-cart/types.d.ts +1 -7
- package/dist/actions/get-custom-stock-actions/action.d.ts +5 -0
- package/dist/actions/get-custom-stock-actions/action.js +7 -0
- package/dist/actions/get-custom-stock-actions/mock.d.ts +2 -0
- package/dist/actions/get-custom-stock-actions/mock.js +11 -0
- package/dist/actions/get-custom-stock-actions/types.d.ts +12 -0
- package/dist/actions/get-custom-stock-actions/types.js +2 -0
- package/dist/actions/get-product-modifier-selections/mock.js +0 -12
- package/dist/actions/get-products/mock.js +3 -7
- package/dist/actions/get-products/types.d.ts +1 -1
- package/dist/actions/get-refund-plan/mock.d.ts +4 -0
- package/dist/actions/get-refund-plan/mock.js +8 -0
- package/dist/actions/get-refund-plan/mockRows.d.ts +6 -0
- package/dist/actions/get-refund-plan/mockRows.js +114 -0
- package/dist/actions/get-refund-plan/types.d.ts +89 -0
- package/dist/actions/set-product-modifier-selections/mock.js +0 -12
- package/dist/demo/database.d.ts +1 -39
- package/dist/demo/database.js +1 -214
- package/dist/index.d.ts +4 -6
- package/dist/index.js +2 -4
- package/dist/projects/render/mocks.js +2 -4
- package/dist/projects/render/types.d.ts +2 -3
- package/dist/pubsub/topics/products/index.d.ts +2 -2
- package/dist/pubsub/topics/products/index.js +18 -33
- package/dist/pubsub/topics/products/types.d.ts +10 -16
- package/dist/pubsub/topics/products/types.js +4 -7
- package/package.json +2 -2
- package/dist/actions/get-composite-availability/action.d.ts +0 -6
- package/dist/actions/get-composite-availability/action.js +0 -8
- package/dist/actions/get-composite-availability/mock.d.ts +0 -2
- package/dist/actions/get-composite-availability/mock.js +0 -45
- package/dist/actions/get-composite-availability/types.d.ts +0 -29
- package/dist/actions/get-composite-availability/types.js +0 -1
- package/dist/actions/get-composite-price/action.d.ts +0 -6
- package/dist/actions/get-composite-price/action.js +0 -8
- package/dist/actions/get-composite-price/mock.d.ts +0 -2
- package/dist/actions/get-composite-price/mock.js +0 -37
- package/dist/actions/get-composite-price/types.d.ts +0 -31
- package/dist/actions/get-composite-price/types.js +0 -1
- package/dist/pubsub/topics/products/catalog-visibility-changed/types.d.ts +0 -17
- package/dist/pubsub/topics/products/catalog-visibility-changed/types.js +0 -1
- package/dist/pubsub/topics/products/composite-changed/types.d.ts +0 -17
- package/dist/pubsub/topics/products/composite-changed/types.js +0 -1
- package/dist/pubsub/topics/products/inventory-changed/types.d.ts +0 -18
- package/dist/pubsub/topics/products/inventory-changed/types.js +0 -1
package/dist/CommonTypes.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * from './common-types';
|
|
2
|
-
import type { Tax, Tip, Address, MetadataItem, PosDataItem, CartFeeTaxEntry, CartFeeItem, CartDiscountItem, OrderNote, NonRevenueItem, DiscountDetail, FeeDetail, DiscountLineItem, FeeLineItem, LineItem, TipPayment, RefundedTipPayment, PaymentMethod, Discount, CustomFee, Summary, CustomSale, RefundedCustomSale, RefundItem, RefundedLineItem, ActiveStation, ActiveSession, ActiveOutlet, ActiveUser, ActiveUserRole, ActiveOrder, ActiveCart, ActivePark, ActiveCompany, ActiveCustomSales, ActiveProduct, ActiveCustomer, FullProduct, ProductVariant, Inventory, ResolvedModifier, ResolvedModifierChoice, ModifierSelectionType, ModifierSelection, ModifierChoiceSelection, CartLineModifier, ProdModifierBreakdown, OrderLineItemModifier, CustomerNote, Attribute, AttributeOption, Category, Transaction, ActiveSplitPayment, BookingAvailability, BookingSlot, BookingSlotResource, BookingResource, Booking, CartReservation, OrderReservation
|
|
2
|
+
import type { Tax, Tip, Address, MetadataItem, PosDataItem, CartFeeTaxEntry, CartFeeItem, CartDiscountItem, OrderNote, NonRevenueItem, DiscountDetail, FeeDetail, DiscountLineItem, FeeLineItem, LineItem, TipPayment, RefundedTipPayment, PaymentMethod, Discount, CustomFee, Summary, CustomSale, RefundedCustomSale, RefundItem, RefundedLineItem, ActiveStation, ActiveSession, ActiveOutlet, ActiveUser, ActiveUserRole, ActiveOrder, ActiveCart, ActivePark, ActiveCompany, ActiveCustomSales, ActiveProduct, ActiveCustomer, FullProduct, ProductVariant, Inventory, ResolvedModifier, ResolvedModifierChoice, ModifierSelectionType, ModifierSelection, ModifierChoiceSelection, CartLineModifier, ProdModifierBreakdown, OrderLineItemModifier, CustomerNote, Attribute, AttributeOption, Category, Transaction, ActiveSplitPayment, BookingAvailability, BookingSlot, BookingSlotResource, BookingResource, Booking, CartReservation, OrderReservation } from '@final-commerce/common/pos-types';
|
|
3
3
|
export { CurrencyCode, ProductType as CFProductType, UserTypes as CFUserTypes } from '@final-commerce/common';
|
|
4
4
|
/** Open record used as a base for active entities. */
|
|
5
5
|
export type CFActiveEntity = Record<string, unknown>;
|
|
@@ -18,30 +18,6 @@ export type CFTransaction = Transaction;
|
|
|
18
18
|
export type CFCategory = Category;
|
|
19
19
|
export type CFProductVariant = ProductVariant;
|
|
20
20
|
export type CFProduct = FullProduct;
|
|
21
|
-
/** Composite products (FT-83): `CFProduct.composite`, the picks `addProductToCart` takes, the cart/order line's
|
|
22
|
-
* components. The host decides availability, prices and tax; a flow renders them and sends picks. */
|
|
23
|
-
/**
|
|
24
|
-
* B41: `needsDate: true` — the composite holds a bookable item and CANNOT be sold without a time. Show "By date" and a
|
|
25
|
-
* date and time picker filled from `getCompositeAvailability`, then pass the chosen `compositeSlot` to `addProductToCart`.
|
|
26
|
-
* Until a time is chosen its bookable items read `unavailable: 'by_date'` — still pickable, never greyed out.
|
|
27
|
-
* INTERIM here until `common/pos-types` Composite carries it (ob_agent).
|
|
28
|
-
*/
|
|
29
|
-
export type CFComposite = Composite & {
|
|
30
|
-
needsDate: boolean;
|
|
31
|
-
};
|
|
32
|
-
export type CFCompositePart = CompositePart;
|
|
33
|
-
export type CFCompositeItem = CompositeItem;
|
|
34
|
-
export type CFCompositeChoice = CompositeChoice;
|
|
35
|
-
export type CFCompositeItemUnavailable = CompositeItemUnavailable;
|
|
36
|
-
/**
|
|
37
|
-
* B41: `resourceId` — on a bookable pick only: the resource that must serve it. Omitted = the host takes the first one
|
|
38
|
-
* free in that window. INTERIM here until `common/pos-types` CompositePick carries it (ob_agent).
|
|
39
|
-
*/
|
|
40
|
-
export type CFCompositePick = CompositePick & {
|
|
41
|
-
resourceId?: string;
|
|
42
|
-
};
|
|
43
|
-
export type CFCartLineComponent = CartLineComponent;
|
|
44
|
-
export type CFOrderLineItemComponent = OrderLineItemComponent;
|
|
45
21
|
export type CFBookingAvailability = BookingAvailability;
|
|
46
22
|
export type CFBookingSlot = BookingSlot;
|
|
47
23
|
export type CFBookingSlotResource = BookingSlotResource;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { AddProductFee } from
|
|
1
|
+
import { AddProductFee } from "./types";
|
|
2
2
|
export declare const mockAddProductFee: AddProductFee;
|
|
@@ -1,36 +1,30 @@
|
|
|
1
|
-
import { MOCK_CART } from
|
|
1
|
+
import { MOCK_CART } from "../../demo/database";
|
|
2
2
|
export const mockAddProductFee = async (params) => {
|
|
3
|
-
console.log(
|
|
3
|
+
console.log("[Mock] addProductFee called", params);
|
|
4
4
|
if (params) {
|
|
5
5
|
let item = null;
|
|
6
6
|
if (params.internalId) {
|
|
7
|
-
item = MOCK_CART.products.find(
|
|
7
|
+
item = MOCK_CART.products.find(p => p.internalId === params.internalId);
|
|
8
8
|
}
|
|
9
9
|
else if (MOCK_CART.products.length > 0) {
|
|
10
10
|
item = MOCK_CART.products[MOCK_CART.products.length - 1];
|
|
11
11
|
}
|
|
12
|
-
// Like the host (FT-83): a composite taxed by its items has no table a taxable fee could inherit.
|
|
13
|
-
if (item && params.applyTaxes && !item.taxTableId && item.components?.length) {
|
|
14
|
-
throw new Error(`${item.name} is taxed by its items: a taxable fee on it has no table to inherit`);
|
|
15
|
-
}
|
|
16
12
|
if (item) {
|
|
17
|
-
item.fees = [
|
|
18
|
-
|
|
19
|
-
label: params.label || 'Fee',
|
|
13
|
+
item.fees = [{
|
|
14
|
+
label: params.label || "Fee",
|
|
20
15
|
amount: params.amount,
|
|
21
16
|
isPercent: params.isPercent || false,
|
|
22
|
-
applyTaxes: params.applyTaxes || false
|
|
23
|
-
}
|
|
24
|
-
];
|
|
17
|
+
applyTaxes: params.applyTaxes || false
|
|
18
|
+
}];
|
|
25
19
|
}
|
|
26
20
|
}
|
|
27
21
|
return {
|
|
28
22
|
success: true,
|
|
29
23
|
amount: params?.amount || 0,
|
|
30
24
|
isPercent: params?.isPercent || false,
|
|
31
|
-
label: params?.label ||
|
|
25
|
+
label: params?.label || "",
|
|
32
26
|
applyTaxes: params?.applyTaxes || false,
|
|
33
27
|
internalId: params?.internalId,
|
|
34
|
-
timestamp: new Date().toISOString()
|
|
28
|
+
timestamp: new Date().toISOString()
|
|
35
29
|
};
|
|
36
30
|
};
|
|
@@ -1,25 +1,2 @@
|
|
|
1
1
|
import { AddProductToCart } from './types';
|
|
2
|
-
import { CFCartLineComponent, CFCartLineModifier, CFComposite, CFCompositePick } from '../../CommonTypes';
|
|
3
2
|
export declare const mockAddProductToCart: AddProductToCart;
|
|
4
|
-
/** The host's pick checks (kaching `validateCompositePicks`), same order and texts; null = valid. */
|
|
5
|
-
export declare function compositePicksRefusal(composite: CFComposite, picks: CFCompositePick[]): string | null;
|
|
6
|
-
/** The host's `picksStillNeeded` (kaching `resolveComposite.ts`): per part, what is left of its min; an Optional part
|
|
7
|
-
* left empty is not listed. A copy — change both in one step. */
|
|
8
|
-
export declare function mockPicksStillNeeded(composite: CFComposite, picks: CFCompositePick[]): {
|
|
9
|
-
partId: string;
|
|
10
|
-
needed: number;
|
|
11
|
-
}[];
|
|
12
|
-
type MockCompositeLine = {
|
|
13
|
-
price: number;
|
|
14
|
-
components: CFCartLineComponent[];
|
|
15
|
-
modifiers: CFCartLineModifier[];
|
|
16
|
-
};
|
|
17
|
-
/** kaching `buildCompositeLine` (B31): a component's share of ONE composite = the composite price split by the picked
|
|
18
|
-
* variant's price × units (all 0 → units) + its own upcharge; Σ = price. Item modifiers flat with `componentIndex`
|
|
19
|
-
* and quantity × the component's units. A copy — change both in one step. */
|
|
20
|
-
export declare function mockCompositeLine(composite: CFComposite, picks: CFCompositePick[], ownTax: boolean): MockCompositeLine | string;
|
|
21
|
-
/** The host's B41 refusal (kaching `loadCompositeLine`), word for word: a composite with a booking needs a slot. */
|
|
22
|
-
export declare function mockNeedsDateRefusal(name: string, composite: CFComposite, slot?: {
|
|
23
|
-
startAt: string;
|
|
24
|
-
}): string | null;
|
|
25
|
-
export {};
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { MOCK_CART,
|
|
2
|
-
import { mockHoldBooking } from '../hold-booking/mock';
|
|
3
|
-
import { mockCancelBooking } from '../cancel-booking/mock';
|
|
1
|
+
import { MOCK_CART, MOCK_PRODUCTS, buildCartLineModifiers, buildModifierRows, mockPublishEvent, } from '../../demo/database';
|
|
4
2
|
import { extendPrice, isValidQuantity, resolveUnit } from '@final-commerce/common';
|
|
5
3
|
export const mockAddProductToCart = async (params) => {
|
|
6
4
|
console.log('[Mock] addProductToCart called', params);
|
|
@@ -23,47 +21,6 @@ export const mockAddProductToCart = async (params) => {
|
|
|
23
21
|
}
|
|
24
22
|
}
|
|
25
23
|
}
|
|
26
|
-
// Composites: the host's refusals with its texts, then the same line it builds (kaching `buildCompositeLine`).
|
|
27
|
-
let compositeLine;
|
|
28
|
-
if (product.composite || params?.composite?.length) {
|
|
29
|
-
const ownTax = !product.taxTable;
|
|
30
|
-
const picks = params?.composite ?? [];
|
|
31
|
-
// What this outlet sells (the host's catalog-visibility rules).
|
|
32
|
-
const composite = product.composite && mockCompositeAtOutlet(product.composite, undefined, params?.compositeSlot);
|
|
33
|
-
const reason = !composite
|
|
34
|
-
? `${product.name} is not a composite: it takes no composite picks`
|
|
35
|
-
: mockHiddenProductIds().has(product._id)
|
|
36
|
-
? `${product.name} is not sold at this outlet`
|
|
37
|
-
: params?.modifiers?.length
|
|
38
|
-
? `${product.name} takes no modifiers of its own: send them on its picks (composite[].modifiers)`
|
|
39
|
-
: ownTax && params?.fees?.some((fee) => fee.applyTaxes)
|
|
40
|
-
? `${product.name} is taxed by its items: a taxable fee on it has no table to inherit`
|
|
41
|
-
: (mockNeedsDateRefusal(product.name, composite, params?.compositeSlot) ??
|
|
42
|
-
compositePicksRefusal(composite, picks) ??
|
|
43
|
-
undefined);
|
|
44
|
-
const built = reason ?? mockCompositeLine(composite, picks, ownTax);
|
|
45
|
-
const line = typeof built !== 'string'
|
|
46
|
-
? (mockCompositeStockRefusal([
|
|
47
|
-
...MOCK_CART.products,
|
|
48
|
-
{ variantId: variant._id, quantity, components: built.components },
|
|
49
|
-
]) ?? built)
|
|
50
|
-
: built;
|
|
51
|
-
if (typeof line === 'string') {
|
|
52
|
-
return {
|
|
53
|
-
success: false,
|
|
54
|
-
reason: line,
|
|
55
|
-
productId: product._id,
|
|
56
|
-
variantId: variant._id,
|
|
57
|
-
internalId: '',
|
|
58
|
-
name: product.name,
|
|
59
|
-
quantity: 0,
|
|
60
|
-
rows: [],
|
|
61
|
-
modifiersTotal: 0,
|
|
62
|
-
timestamp: new Date().toISOString(),
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
compositeLine = line;
|
|
66
|
-
}
|
|
67
24
|
// A measured variant carries a resolved unit; the host does the same. What the unit can
|
|
68
25
|
// express is its precision — 1.5 of a kilogram is a sale, 1.5 of a piece is a typo.
|
|
69
26
|
const unit = variant.unitId ? resolveUnit(variant.unitId) : undefined;
|
|
@@ -101,8 +58,6 @@ export const mockAddProductToCart = async (params) => {
|
|
|
101
58
|
// shapes, one name. Drop it before anything reads the line, or every surface renders the
|
|
102
59
|
// whole catalogue as if the cashier had picked all of it.
|
|
103
60
|
delete activeProduct.modifiers;
|
|
104
|
-
// Same for the catalogue's `composite` picker: a cart line carries `components`, never the menu.
|
|
105
|
-
delete activeProduct.composite;
|
|
106
61
|
// Keep the raw answers AND the priced rows, the same pair the real host writes. The cart
|
|
107
62
|
// TOTAL is deliberately left alone: like a product fee (see add-product-fee), per-line
|
|
108
63
|
// money is not accumulated by this mock — `total` stays Σ extendPrice(price, quantity).
|
|
@@ -110,31 +65,6 @@ export const mockAddProductToCart = async (params) => {
|
|
|
110
65
|
activeProduct.modifierSelections = params.modifiers;
|
|
111
66
|
activeProduct.modifiers = buildCartLineModifiers(product.modifiers, params.modifiers, product.taxTable);
|
|
112
67
|
}
|
|
113
|
-
if (compositeLine) {
|
|
114
|
-
activeProduct.price = compositeLine.price;
|
|
115
|
-
activeProduct.components = compositeLine.components;
|
|
116
|
-
if (compositeLine.modifiers.length) {
|
|
117
|
-
activeProduct.modifiers = [...(activeProduct.modifiers ?? []), ...compositeLine.modifiers];
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
// B41: every bookable component held in one step, or the add is refused and nothing is kept.
|
|
121
|
-
const booked = compositeLine && (await mockHoldCompositeBookings(compositeLine, quantity, internalId, params));
|
|
122
|
-
if (typeof booked === 'string') {
|
|
123
|
-
return {
|
|
124
|
-
success: false,
|
|
125
|
-
reason: booked,
|
|
126
|
-
productId: product._id,
|
|
127
|
-
variantId: variant._id,
|
|
128
|
-
internalId: '',
|
|
129
|
-
name: product.name,
|
|
130
|
-
quantity: 0,
|
|
131
|
-
rows: [],
|
|
132
|
-
modifiersTotal: 0,
|
|
133
|
-
timestamp: new Date().toISOString(),
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
if (booked?.length)
|
|
137
|
-
MOCK_CART.reservations = [...(MOCK_CART.reservations ?? []), ...booked];
|
|
138
68
|
MOCK_CART.products.push(activeProduct);
|
|
139
69
|
// Recalculate totals. extendPrice, not a raw multiply: a fractional quantity times an
|
|
140
70
|
// integer-minor price leaves fractional cents, which no order may carry.
|
|
@@ -161,232 +91,3 @@ export const mockAddProductToCart = async (params) => {
|
|
|
161
91
|
timestamp: new Date().toISOString(),
|
|
162
92
|
};
|
|
163
93
|
};
|
|
164
|
-
/** The host's pick checks (kaching `validateCompositePicks`), same order and texts; null = valid. */
|
|
165
|
-
export function compositePicksRefusal(composite, picks) {
|
|
166
|
-
if (!composite.available) {
|
|
167
|
-
if (!composite.parts.length)
|
|
168
|
-
return 'This composite has no parts on this till yet';
|
|
169
|
-
const blocking = composite.parts.find((part) => part.min > 0 && !part.items.some((item) => !item.unavailable || item.unavailable === 'by_date'));
|
|
170
|
-
return blocking
|
|
171
|
-
? `${blocking.name ?? 'Choose'}: nothing can be picked at this outlet`
|
|
172
|
-
: 'This composite is unavailable: a part it needs has nothing to pick';
|
|
173
|
-
}
|
|
174
|
-
const counts = new Map();
|
|
175
|
-
for (const pick of picks) {
|
|
176
|
-
const quantity = pick.quantity ?? 1;
|
|
177
|
-
const part = composite.parts.find((candidate) => candidate.items.some((item) => item._id === pick.itemId));
|
|
178
|
-
const item = part?.items.find((candidate) => candidate._id === pick.itemId);
|
|
179
|
-
if (!part || !item)
|
|
180
|
-
return `Composite item ${pick.itemId} is not offered here`;
|
|
181
|
-
if (!Number.isInteger(quantity) || quantity <= 0)
|
|
182
|
-
return `${item.name}: quantity must be a positive whole number`;
|
|
183
|
-
if (item.unavailable === 'by_date')
|
|
184
|
-
return `${item.name}: choose a date and time`;
|
|
185
|
-
if (item.unavailable === 'fully_booked')
|
|
186
|
-
return `${item.name} is fully booked at that time`;
|
|
187
|
-
if (item.unavailable)
|
|
188
|
-
return `${item.name} is not available`;
|
|
189
|
-
const choice = item.choices.find((candidate) => candidate.variantId === pick.variantId);
|
|
190
|
-
if (!choice)
|
|
191
|
-
return `${item.name}: that variant is not one of its choices`;
|
|
192
|
-
// The picked product's own modifiers, by its own rules (B28).
|
|
193
|
-
const modifiers = mockSelectionsRefusal(choice.modifiers ?? [], pick.modifiers ?? []);
|
|
194
|
-
if (modifiers)
|
|
195
|
-
return `${choice.name}: ${modifiers}`;
|
|
196
|
-
counts.set(part._id, (counts.get(part._id) ?? 0) + quantity);
|
|
197
|
-
}
|
|
198
|
-
for (const part of composite.parts) {
|
|
199
|
-
const count = counts.get(part._id) ?? 0;
|
|
200
|
-
if (count < part.min)
|
|
201
|
-
return `${part.name ?? 'Choose'}: pick ${part.min === part.max ? '' : 'at least '}${part.min}`;
|
|
202
|
-
if (count > part.max)
|
|
203
|
-
return `${part.name ?? 'Choose'}: pick at most ${part.max}`;
|
|
204
|
-
}
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
|
-
/** The host's modifier rules (kaching `validateModifiers.ts` `validateSelections`), same order and texts, at the demo
|
|
208
|
-
* outlet; null = valid. A copy — change both in one step. */
|
|
209
|
-
function mockSelectionsRefusal(modifiers, selections) {
|
|
210
|
-
const outletId = MOCK_OUTLET.id;
|
|
211
|
-
const sellable = (choice) => !(choice.unavailableOutletIds ?? []).includes(outletId);
|
|
212
|
-
if (new Set(selections.map((selection) => selection.modifierId)).size !== selections.length) {
|
|
213
|
-
return 'Duplicate selection for the same modifier';
|
|
214
|
-
}
|
|
215
|
-
if (selections.some((selection) => !modifiers.some((modifier) => modifier._id === selection.modifierId))) {
|
|
216
|
-
return 'A selected modifier does not apply to this product';
|
|
217
|
-
}
|
|
218
|
-
for (const modifier of modifiers) {
|
|
219
|
-
if (modifier.required && !modifier.choices.some(sellable)) {
|
|
220
|
-
return `${modifier.name} is required, but nothing is sold at this outlet`;
|
|
221
|
-
}
|
|
222
|
-
const picked = selections.find((selection) => selection.modifierId === modifier._id)?.choices ?? [];
|
|
223
|
-
for (const { choiceId, quantity } of picked) {
|
|
224
|
-
const choice = modifier.choices.find((candidate) => candidate._id === choiceId);
|
|
225
|
-
if (!choice)
|
|
226
|
-
return `Unknown choice for ${modifier.name}`;
|
|
227
|
-
if (!sellable(choice))
|
|
228
|
-
return `${choice.name} is not sold at this outlet`;
|
|
229
|
-
if (!Number.isInteger(quantity) || quantity < 1)
|
|
230
|
-
return `${choice.name}: quantity must be a positive whole number`;
|
|
231
|
-
const takesQuantity = modifier.selectionType === 'quantity' || (modifier.selectionType === 'multiple' && modifier.allowQuantity);
|
|
232
|
-
if (!takesQuantity && quantity !== 1)
|
|
233
|
-
return `${modifier.name} does not take quantities`;
|
|
234
|
-
}
|
|
235
|
-
if (new Set(picked.map((choice) => choice.choiceId)).size !== picked.length)
|
|
236
|
-
return `${modifier.name}: duplicate choice`;
|
|
237
|
-
const units = picked.reduce((sum, choice) => sum + choice.quantity, 0);
|
|
238
|
-
if (modifier.selectionType === 'single') {
|
|
239
|
-
if (units > 1)
|
|
240
|
-
return `${modifier.name}: pick one`;
|
|
241
|
-
if (modifier.required && units < 1)
|
|
242
|
-
return `${modifier.name} is required`;
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
const min = modifier.required ? Math.max(1, modifier.min ?? 1) : 0;
|
|
246
|
-
if (units < min)
|
|
247
|
-
return `${modifier.name}: pick at least ${min}`;
|
|
248
|
-
if (modifier.max != null && units > modifier.max)
|
|
249
|
-
return `${modifier.name}: at most ${modifier.max}`;
|
|
250
|
-
}
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
/** The host's `picksStillNeeded` (kaching `resolveComposite.ts`): per part, what is left of its min; an Optional part
|
|
254
|
-
* left empty is not listed. A copy — change both in one step. */
|
|
255
|
-
export function mockPicksStillNeeded(composite, picks) {
|
|
256
|
-
return composite.parts.flatMap((part) => {
|
|
257
|
-
const count = picks
|
|
258
|
-
.filter((pick) => part.items.some((item) => item._id === pick.itemId))
|
|
259
|
-
.reduce((sum, pick) => sum + (pick.quantity ?? 1), 0);
|
|
260
|
-
const needed = Math.max(0, part.min - count);
|
|
261
|
-
return needed > 0 ? [{ partId: part._id, needed }] : [];
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
/** kaching `buildCompositeLine` (B31): a component's share of ONE composite = the composite price split by the picked
|
|
265
|
-
* variant's price × units (all 0 → units) + its own upcharge; Σ = price. Item modifiers flat with `componentIndex`
|
|
266
|
-
* and quantity × the component's units. A copy — change both in one step. */
|
|
267
|
-
export function mockCompositeLine(composite, picks, ownTax) {
|
|
268
|
-
const rows = picks.map((pick) => {
|
|
269
|
-
const part = composite.parts.find((candidate) => candidate.items.some((item) => item._id === pick.itemId));
|
|
270
|
-
const item = part.items.find((candidate) => candidate._id === pick.itemId);
|
|
271
|
-
const choice = item.choices.find((candidate) => candidate.variantId === pick.variantId);
|
|
272
|
-
const product = MOCK_PRODUCTS.find((candidate) => candidate._id === choice.productId);
|
|
273
|
-
const variant = product?.variants.find((candidate) => candidate._id === choice.variantId);
|
|
274
|
-
const selected = variant?.isOnSale ? variant.salePrice : variant?.price;
|
|
275
|
-
return {
|
|
276
|
-
part,
|
|
277
|
-
item,
|
|
278
|
-
choice,
|
|
279
|
-
productName: product?.name ?? choice.name,
|
|
280
|
-
taxTable: product?.taxTable || undefined,
|
|
281
|
-
price: typeof selected === 'number' && Number.isFinite(selected) ? Math.round(selected) : null,
|
|
282
|
-
pickQuantity: pick.quantity ?? 1,
|
|
283
|
-
units: (pick.quantity ?? 1) * item.quantity,
|
|
284
|
-
};
|
|
285
|
-
});
|
|
286
|
-
const unreadable = rows.find((row) => row.price === null);
|
|
287
|
-
if (unreadable)
|
|
288
|
-
return `${unreadable.productName}: its price cannot be read, so the composite cannot be split`;
|
|
289
|
-
const base = composite.basePrice ?? 0;
|
|
290
|
-
if (ownTax && rows.length === 0 && base > 0)
|
|
291
|
-
return 'Pick at least one item: this composite is taxed by its items';
|
|
292
|
-
const priced = rows.map((row) => row.price * row.units);
|
|
293
|
-
const weights = priced.some((weight) => weight > 0) ? priced : rows.map((row) => row.units);
|
|
294
|
-
const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
|
|
295
|
-
// Largest remainder, ties to the first — the host's allocateRateAcrossLines.
|
|
296
|
-
const floors = weights.map((weight) => (totalWeight ? Math.floor((base * weight) / totalWeight) : 0));
|
|
297
|
-
const ranked = weights
|
|
298
|
-
.map((weight, index) => ({ index, remainder: totalWeight ? (base * weight) % totalWeight : 0 }))
|
|
299
|
-
.sort((a, b) => b.remainder - a.remainder || a.index - b.index);
|
|
300
|
-
for (let k = 0; k < (totalWeight ? base - floors.reduce((a, b) => a + b, 0) : 0); k++)
|
|
301
|
-
floors[ranked[k].index] += 1;
|
|
302
|
-
const components = rows.map((row, index) => ({
|
|
303
|
-
...(row.part.name ? { partName: row.part.name } : {}),
|
|
304
|
-
productId: row.choice.productId,
|
|
305
|
-
variantId: row.choice.variantId,
|
|
306
|
-
name: row.choice.name,
|
|
307
|
-
...(row.choice.attributes.length ? { attributes: row.choice.attributes } : {}),
|
|
308
|
-
quantity: row.units,
|
|
309
|
-
unitPrice: row.item.cost * row.pickQuantity + floors[index],
|
|
310
|
-
...(ownTax && row.taxTable ? { taxTableId: row.taxTable } : {}),
|
|
311
|
-
}));
|
|
312
|
-
const modifiers = rows.flatMap((row, index) => buildCartLineModifiers(row.choice.modifiers, picks[index].modifiers, row.taxTable).map((modifier) => ({
|
|
313
|
-
...modifier,
|
|
314
|
-
quantity: modifier.quantity * row.units,
|
|
315
|
-
componentIndex: index,
|
|
316
|
-
})));
|
|
317
|
-
return { price: rows.reduce((sum, row) => sum + row.item.cost * row.pickQuantity, base), components, modifiers };
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* The host's B41 step (kaching `holdCompositeBookings`): seats of one resource, else several, else refused; each hold a
|
|
321
|
-
* price-0 reservation linked to the line. Returns the rows, or the refusal after releasing what it took.
|
|
322
|
-
*/
|
|
323
|
-
async function mockHoldCompositeBookings(line, quantity, lineItemInternalId, params) {
|
|
324
|
-
const rows = [];
|
|
325
|
-
const refuse = async (reason) => {
|
|
326
|
-
for (const row of rows)
|
|
327
|
-
await mockCancelBooking({ bookingId: row.bookingId });
|
|
328
|
-
return reason;
|
|
329
|
-
};
|
|
330
|
-
for (const [componentIndex, component] of line.components.entries()) {
|
|
331
|
-
if (!mockIsBookable(component.productId))
|
|
332
|
-
continue;
|
|
333
|
-
if (!params?.compositeSlot)
|
|
334
|
-
return refuse(`${component.name}: choose a date and time`);
|
|
335
|
-
const seats = component.quantity * quantity;
|
|
336
|
-
const { slot } = mockBookableWindow(component.productId, params.compositeSlot.startAt);
|
|
337
|
-
const named = params.composite?.[componentIndex]?.resourceId;
|
|
338
|
-
const resources = (slot?.resources ?? []).filter((resource) => !named || resource.resourceId === named);
|
|
339
|
-
const whole = resources.find((resource) => resource.free >= seats);
|
|
340
|
-
const plan = whole
|
|
341
|
-
? [{ resourceId: whole.resourceId, qty: seats }]
|
|
342
|
-
: named
|
|
343
|
-
? []
|
|
344
|
-
: resources
|
|
345
|
-
.filter((resource) => resource.free > 0)
|
|
346
|
-
.map((resource) => ({ resourceId: resource.resourceId, qty: resource.free }));
|
|
347
|
-
let left = seats;
|
|
348
|
-
const taken = plan.flatMap((entry) => {
|
|
349
|
-
const qty = Math.min(entry.qty, left);
|
|
350
|
-
left -= qty;
|
|
351
|
-
return qty > 0 ? [{ ...entry, qty }] : [];
|
|
352
|
-
});
|
|
353
|
-
if (!slot || left > 0)
|
|
354
|
-
return refuse(`${component.name}: ${seats} seat(s) are not free at that time`);
|
|
355
|
-
for (const { resourceId, qty } of taken) {
|
|
356
|
-
const held = await mockHoldBooking({
|
|
357
|
-
productId: component.productId,
|
|
358
|
-
resourceId,
|
|
359
|
-
variantId: component.variantId,
|
|
360
|
-
startAt: slot.startAt,
|
|
361
|
-
endAt: slot.endAt,
|
|
362
|
-
});
|
|
363
|
-
if (!held.success || !held.booking)
|
|
364
|
-
return refuse(`${component.name}: ${held.reason}`);
|
|
365
|
-
rows.push({
|
|
366
|
-
internalId: `res_${held.booking.id}`,
|
|
367
|
-
bookingId: held.booking.id,
|
|
368
|
-
productId: component.productId,
|
|
369
|
-
variantId: component.variantId,
|
|
370
|
-
resourceId,
|
|
371
|
-
name: component.name,
|
|
372
|
-
resourceName: held.booking.resourceName,
|
|
373
|
-
price: 0,
|
|
374
|
-
quantity: qty,
|
|
375
|
-
total: 0,
|
|
376
|
-
startAt: held.booking.startAt,
|
|
377
|
-
endAt: held.booking.endAt,
|
|
378
|
-
bufferEndAt: held.booking.bufferEndAt,
|
|
379
|
-
expiresAt: held.booking.expiresAt,
|
|
380
|
-
lineItemInternalId,
|
|
381
|
-
componentIndex,
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
return rows;
|
|
386
|
-
}
|
|
387
|
-
/** The host's B41 refusal (kaching `loadCompositeLine`), word for word: a composite with a booking needs a slot. */
|
|
388
|
-
export function mockNeedsDateRefusal(name, composite, slot) {
|
|
389
|
-
return composite.needsDate && !slot
|
|
390
|
-
? `${name} includes a booking: show a date and time picker (getCompositeAvailability) and pass compositeSlot to addProductToCart`
|
|
391
|
-
: null;
|
|
392
|
-
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AddProductDiscountParams } from '../add-product-discount/types';
|
|
2
2
|
import type { AddProductFeeParams } from '../add-product-fee/types';
|
|
3
3
|
import type { ModifierSelection } from '../get-product-modifier-selections/types';
|
|
4
|
-
import type {
|
|
4
|
+
import type { CFProdModifierBreakdown } from '../../CommonTypes';
|
|
5
5
|
export interface AddProductToCartParams {
|
|
6
6
|
/** ID of the variant to add. */
|
|
7
7
|
variantId: string;
|
|
@@ -25,32 +25,6 @@ export interface AddProductToCartParams {
|
|
|
25
25
|
fees?: AddProductFeeParams[];
|
|
26
26
|
/** Modifier selections to apply immediately. */
|
|
27
27
|
modifiers?: ModifierSelection[];
|
|
28
|
-
/**
|
|
29
|
-
* Picks for a composite product (`CFProduct.composite`), validated by the host against each part's
|
|
30
|
-
* required / min / max, availability and each picked item's own modifier rules before the line exists; a
|
|
31
|
-
* rejected add returns `success: false` with a `reason`. Refused for a product that is not a composite.
|
|
32
|
-
*
|
|
33
|
-
* `composite[].modifiers` are the PICKED ITEM's modifiers (`choices[].modifiers`), taxed at that item's table
|
|
34
|
-
* (B28); send their quantity per one pick — the host multiplies it by the pick's units. The line comes back with
|
|
35
|
-
* `components[]` and those rows in its flat `modifiers[]` with `componentIndex`.
|
|
36
|
-
*
|
|
37
|
-
* A composite has no modifiers of its own (B43): `modifiers` sent with `composite` are refused — every modifier of a
|
|
38
|
-
* composite line is a picked item's and carries `componentIndex`. A taxable `fee` on a composite taxed by its items is
|
|
39
|
-
* refused too: there is no table for it to inherit.
|
|
40
|
-
*/
|
|
41
|
-
composite?: CFCompositePick[];
|
|
42
|
-
/**
|
|
43
|
-
* B41: REQUIRED when `composite.needsDate` is true — the ONE date and time every bookable pick of this composite is
|
|
44
|
-
* booked for: show a date and time picker and send the `startAt` of a slot `getCompositeAvailability` offered. Without
|
|
45
|
-
* it the add is refused with a `reason`; if a seat went meanwhile, ask availability again — never retry the same slot.
|
|
46
|
-
* The host checks goods stock, then seats, then holds each bookable in one step: any failure releases what it
|
|
47
|
-
* took and the add is refused with a `reason`. Each hold lands in `cart.reservations[]` at price 0, linked to the line
|
|
48
|
-
* (`lineItemInternalId`, `componentIndex`); the component carries the money. Such a line's quantity cannot be
|
|
49
|
-
* changed, and it is refunded whole only.
|
|
50
|
-
*/
|
|
51
|
-
compositeSlot?: {
|
|
52
|
-
startAt: string;
|
|
53
|
-
};
|
|
54
28
|
/** Note or array of notes to add immediately. */
|
|
55
29
|
notes?: string | string[];
|
|
56
30
|
}
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { MOCK_PRODUCTS } from '../../demo/database';
|
|
2
|
+
// Same rules and messages as the kaching handler.
|
|
3
|
+
const STOCK_TYPE_BY_REASON = {
|
|
4
|
+
STOCK_RECEIVED: 'add',
|
|
5
|
+
RESTOCK_RETURN: 'add',
|
|
6
|
+
DAMAGE: 'subtract',
|
|
7
|
+
THEFT: 'subtract',
|
|
8
|
+
LOSS: 'subtract',
|
|
9
|
+
INVENTORY_RECOUNT: 'set',
|
|
10
|
+
};
|
|
2
11
|
export const mockAdjustInventory = async (params) => {
|
|
3
12
|
console.log('[Mock] adjustInventory called', params);
|
|
13
|
+
if (params?.specificAction != null) {
|
|
14
|
+
const { specificAction, stockType, customActionId } = params;
|
|
15
|
+
if (customActionId)
|
|
16
|
+
throw new Error('Pass either customActionId or specificAction, not both');
|
|
17
|
+
const reasonStockType = STOCK_TYPE_BY_REASON[specificAction];
|
|
18
|
+
if (!reasonStockType)
|
|
19
|
+
throw new Error(`Unknown specificAction: ${specificAction}`);
|
|
20
|
+
if (reasonStockType !== stockType) {
|
|
21
|
+
throw new Error(`specificAction ${specificAction} requires stockType '${reasonStockType}', got '${stockType}'`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
4
24
|
let newStock = 0;
|
|
5
25
|
// Real handler keys off variantId (falling back to the active product's
|
|
6
26
|
// variant); mirror that here by finding the owning product for the variant.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** The manual reasons a person can pick, as hub-api's InventorySpecificActionType names them. */
|
|
2
|
+
export type ManualStockReason = 'STOCK_RECEIVED' | 'RESTOCK_RETURN' | 'DAMAGE' | 'THEFT' | 'LOSS' | 'INVENTORY_RECOUNT';
|
|
1
3
|
export interface AdjustInventoryParams {
|
|
2
4
|
/** String to preserve precision. */
|
|
3
5
|
amount: string;
|
|
@@ -5,6 +7,17 @@ export interface AdjustInventoryParams {
|
|
|
5
7
|
stockType: 'add' | 'subtract' | 'set';
|
|
6
8
|
/** Variant to adjust. Omit to use the active product's selected variant. */
|
|
7
9
|
variantId?: string;
|
|
10
|
+
/**
|
|
11
|
+
* A company action from getCustomStockActions. The movement is recorded under its name;
|
|
12
|
+
* `stockType` must match its baseAction (ADD → 'add', REMOVE → 'subtract', RECOUNT → 'set').
|
|
13
|
+
*/
|
|
14
|
+
customActionId?: string;
|
|
15
|
+
/**
|
|
16
|
+
* A built-in reason to record instead of the host's default for `stockType`. Must match it:
|
|
17
|
+
* STOCK_RECEIVED / RESTOCK_RETURN → 'add', DAMAGE / THEFT / LOSS → 'subtract', INVENTORY_RECOUNT → 'set'.
|
|
18
|
+
* Rejected together with `customActionId`.
|
|
19
|
+
*/
|
|
20
|
+
specificAction?: ManualStockReason;
|
|
8
21
|
}
|
|
9
22
|
export interface AdjustInventoryResponse {
|
|
10
23
|
success: boolean;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { GetCurrentCart } from
|
|
1
|
+
import { GetCurrentCart } from "./types";
|
|
2
2
|
export declare const mockGetCurrentCart: GetCurrentCart;
|
|
@@ -1,19 +1,9 @@
|
|
|
1
|
-
import { MOCK_CART,
|
|
2
|
-
import { extendPrice } from '@final-commerce/common';
|
|
1
|
+
import { MOCK_CART, safeSerialize } from "../../demo/database";
|
|
3
2
|
export const mockGetCurrentCart = async () => {
|
|
4
|
-
console.log(
|
|
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
|
-
]);
|
|
3
|
+
console.log("[Mock] getCurrentCart called");
|
|
13
4
|
return {
|
|
14
5
|
success: true,
|
|
15
6
|
cart: safeSerialize(MOCK_CART),
|
|
16
|
-
|
|
17
|
-
timestamp: new Date().toISOString(),
|
|
7
|
+
timestamp: new Date().toISOString()
|
|
18
8
|
};
|
|
19
9
|
};
|
|
@@ -1,13 +1,7 @@
|
|
|
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>;
|
|
11
5
|
timestamp: string;
|
|
12
6
|
}
|
|
13
7
|
export type GetCurrentCart = () => Promise<GetCurrentCartResponse>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const mockGetCustomStockActions = async () => {
|
|
2
|
+
console.log('[Mock] getCustomStockActions called');
|
|
3
|
+
return {
|
|
4
|
+
customStockActions: [
|
|
5
|
+
{ _id: 'mock_custom_action_1', name: 'Opening stock', baseAction: 'ADD' },
|
|
6
|
+
{ _id: 'mock_custom_action_2', name: 'Exhibition loan', baseAction: 'REMOVE' },
|
|
7
|
+
{ _id: 'mock_custom_action_3', name: 'Year-end count', baseAction: 'RECOUNT' },
|
|
8
|
+
],
|
|
9
|
+
timestamp: new Date().toISOString(),
|
|
10
|
+
};
|
|
11
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface CustomStockActionPayload {
|
|
2
|
+
_id: string;
|
|
3
|
+
/** Name the company gave the action, shown in pickers and stock history. */
|
|
4
|
+
name: string;
|
|
5
|
+
/** What the action does to stock — pass the matching `stockType` to adjustInventory. */
|
|
6
|
+
baseAction: 'ADD' | 'REMOVE' | 'RECOUNT';
|
|
7
|
+
}
|
|
8
|
+
export interface GetCustomStockActionsResponse {
|
|
9
|
+
customStockActions: CustomStockActionPayload[];
|
|
10
|
+
timestamp: string;
|
|
11
|
+
}
|
|
12
|
+
export type GetCustomStockActions = () => Promise<GetCustomStockActionsResponse>;
|