@feedmepos/order-plugin-gallery 0.0.10-beta.0 → 0.0.10-beta.2

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.
@@ -1,46 +1,5 @@
1
- const formatQty = (qty) => {
2
- if (Number.isInteger(qty))
3
- return `${qty}`;
4
- return qty.toFixed(2).replace(/\.?0+$/, "");
5
- };
6
- const addQuantity = (quantityMap, itemId, quantity) => {
7
- if (!itemId || typeof itemId !== "string")
8
- return;
9
- if (typeof quantity !== "number" || !Number.isFinite(quantity))
10
- return;
11
- if (quantity <= 0)
12
- return;
13
- quantityMap.set(itemId, (quantityMap.get(itemId) || 0) + quantity);
14
- };
15
- const getQuantityMap = (items) => {
16
- const quantityMap = new Map();
17
- for (const item of items) {
18
- addQuantity(quantityMap, item.productId, item.quantity);
19
- }
20
- return quantityMap;
21
- };
22
- const getNormalizedRules = (rawRules) => {
23
- if (!Array.isArray(rawRules))
24
- return [];
25
- const normalizedRules = [];
26
- for (const rule of rawRules) {
27
- const itemId = typeof rule?.itemId === "string"
28
- ? rule.itemId.trim()
29
- : "";
30
- const minQty = rule?.minQty;
31
- const maxQty = rule?.maxQty;
32
- if (!itemId || !Number.isFinite(minQty) || !Number.isFinite(maxQty)) {
33
- console.warn("[compulsory-items] Skip invalid rule. Expect { itemId, minQty, maxQty } with finite numbers.", rule);
34
- continue;
35
- }
36
- if (minQty < 0 || maxQty < minQty) {
37
- console.warn("[compulsory-items] Skip invalid rule. Expect minQty >= 0 and maxQty >= minQty.", rule);
38
- continue;
39
- }
40
- normalizedRules.push({ itemId, minQty, maxQty });
41
- }
42
- return normalizedRules;
43
- };
1
+ import { OrderItemUtil, } from "@feedmepos/ordering-sdk";
2
+ import { getAutoAddPlans, getNormalizedRules, getRuleQuantitySummary as getRuleQuantitySummaryShared, getViolationMessages, } from "./compulsory-items.shared.js";
44
3
  const getItemTitleMap = (sdk) => {
45
4
  const titleMap = new Map();
46
5
  const menuItems = (sdk.menuManager.state.value.menu?.items ||
@@ -63,39 +22,96 @@ const getItemTitleMap = (sdk) => {
63
22
  }
64
23
  return titleMap;
65
24
  };
66
- const compulsoryItemsPlugin = ({ sdk, onBeforeSubmitOrder, pluginParams, }) => {
67
- onBeforeSubmitOrder(() => {
25
+ const getRuleQuantitySummary = (sdk, rules, slotActiveBills, draftItems) => {
26
+ const itemTitleMap = getItemTitleMap(sdk);
27
+ return getRuleQuantitySummaryShared({
28
+ rules,
29
+ slotActiveBills,
30
+ draftItems,
31
+ itemTitleMap,
32
+ });
33
+ };
34
+ const getMenuItemByIdMap = (sdk) => {
35
+ const menuItems = (sdk.menuManager.state.value.menu?.items ||
36
+ []);
37
+ return new Map(menuItems.map((item) => [item.id, item]));
38
+ };
39
+ const isMenuItemAutoAddable = (menuItem) => {
40
+ return (menuItem.groups?.length || 0) === 0;
41
+ };
42
+ const buildDefaultOrderItem = (sdk, menuItem, quantity) => {
43
+ const order = sdk.orderManager.state.value.order;
44
+ const option = sdk.orderManager.state.value.option;
45
+ if (!order || !option)
46
+ return null;
47
+ const baseState = {
48
+ variantSelected: null,
49
+ subItems: [],
50
+ isTakeaway: Boolean(sdk.orderManager.state.value.isPickupTakeawayOrdering &&
51
+ menuItem.takeawayUsed),
52
+ takeawaySelected: sdk.orderManager.state.value.isPickupTakeawayOrdering &&
53
+ menuItem.takeawayUsed
54
+ ? menuItem.takeawayUsed
55
+ : null,
56
+ remark: "",
57
+ quantity,
58
+ };
59
+ return OrderItemUtil.buildOrderItem(baseState, menuItem, option.type);
60
+ };
61
+ const autoAddCompulsoryItems = async (sdk, rules, slotActiveBills) => {
62
+ const order = sdk.orderManager.state.value.order;
63
+ if (!order)
64
+ return;
65
+ const draftItems = (order.draft || []);
66
+ const { submittedQtyByItemId, draftQtyByItemId, itemTitleMap } = getRuleQuantitySummary(sdk, rules, slotActiveBills, draftItems);
67
+ const menuItemById = getMenuItemByIdMap(sdk);
68
+ const itemsToAdd = [];
69
+ const addPlans = getAutoAddPlans(rules, {
70
+ submittedQtyByItemId,
71
+ draftQtyByItemId,
72
+ itemTitleMap,
73
+ });
74
+ for (const plan of addPlans) {
75
+ const menuItem = menuItemById.get(plan.itemId);
76
+ const itemTitle = itemTitleMap.get(plan.itemId) || plan.itemId;
77
+ if (!menuItem) {
78
+ console.warn(`[compulsory-items] Skip auto add for "${itemTitle}". Menu item not found in current menu.`);
79
+ continue;
80
+ }
81
+ if (!isMenuItemAutoAddable(menuItem)) {
82
+ console.warn(`[compulsory-items] Skip auto add for "${itemTitle}". Only simple items without variants/addons are supported.`);
83
+ continue;
84
+ }
85
+ const orderItem = buildDefaultOrderItem(sdk, menuItem, plan.missingQty);
86
+ if (!orderItem) {
87
+ console.warn(`[compulsory-items] Skip auto add for "${itemTitle}". Failed to build a default cart item.`);
88
+ continue;
89
+ }
90
+ itemsToAdd.push(orderItem);
91
+ }
92
+ if (itemsToAdd.length > 0) {
93
+ await sdk.orderManager.updateItems(itemsToAdd);
94
+ }
95
+ };
96
+ const compulsoryItemsPlugin = ({ sdk, onOrderSessionReady, onBeforeSubmitOrder, pluginParams, }) => {
97
+ const getRules = () => {
68
98
  const params = (pluginParams || {});
69
- const rules = getNormalizedRules(params.itemLimitConfig);
99
+ return getNormalizedRules(params.itemLimitConfig);
100
+ };
101
+ onOrderSessionReady(async (params) => {
102
+ if (params.isPreviewMode)
103
+ return;
104
+ const rules = getRules();
105
+ if (rules.length === 0)
106
+ return;
107
+ await autoAddCompulsoryItems(sdk, rules, params.slotActiveBills);
108
+ });
109
+ onBeforeSubmitOrder(() => {
110
+ const rules = getRules();
70
111
  if (rules.length === 0)
71
112
  return true;
72
- const submittedItems = (sdk.orderManager.state.value.slotActiveBills?.[0]?.items ||
73
- []);
74
- const draftItems = (sdk.orderManager.state.value.order?.draft || []);
75
- const submittedQtyByItemId = getQuantityMap(submittedItems);
76
- const draftQtyByItemId = getQuantityMap(draftItems);
77
- const itemTitleMap = getItemTitleMap(sdk);
78
- const violationMessages = [];
79
- for (const rule of rules) {
80
- const submittedQty = submittedQtyByItemId.get(rule.itemId) || 0;
81
- const draftQty = draftQtyByItemId.get(rule.itemId) || 0;
82
- const totalQty = submittedQty + draftQty;
83
- const itemTitle = itemTitleMap.get(rule.itemId) || rule.itemId;
84
- if (totalQty < rule.minQty) {
85
- const delta = rule.minQty - totalQty;
86
- violationMessages.push(`Add ${formatQty(delta)} more "${itemTitle}" (minimum ${formatQty(rule.minQty)}, current total ${formatQty(totalQty)}).`);
87
- }
88
- if (submittedQty > rule.maxQty) {
89
- if (draftQty > 0) {
90
- violationMessages.push(`"${itemTitle}" is already above the limit in submitted orders. Remove ${formatQty(draftQty)} from your current cart to continue.`);
91
- }
92
- continue;
93
- }
94
- if (totalQty > rule.maxQty) {
95
- const delta = totalQty - rule.maxQty;
96
- violationMessages.push(`Remove ${formatQty(delta)} "${itemTitle}" from your current cart (maximum ${formatQty(rule.maxQty)}, current total ${formatQty(totalQty)}).`);
97
- }
98
- }
113
+ const quantitySummary = getRuleQuantitySummary(sdk, rules, sdk.orderManager.state.value.slotActiveBills || [], (sdk.orderManager.state.value.order?.draft || []));
114
+ const violationMessages = getViolationMessages(rules, quantitySummary);
99
115
  if (violationMessages.length === 0) {
100
116
  return true;
101
117
  }
@@ -0,0 +1,35 @@
1
+ import type { CCdtoBillSummary } from "@feedmepos/ordering-sdk";
2
+ export type CompulsoryItemRule = {
3
+ itemId: string;
4
+ minQty: number;
5
+ maxQty: number;
6
+ };
7
+ export type CompulsoryItemsPluginParams = {
8
+ itemLimitConfig: CompulsoryItemRule[];
9
+ };
10
+ export type QuantityItem = {
11
+ productId?: string | null;
12
+ name?: string | null;
13
+ title?: string | null;
14
+ quantity?: number | null;
15
+ status?: string | null;
16
+ };
17
+ export type RuleQuantitySummary = {
18
+ submittedQtyByItemId: Map<string, number>;
19
+ draftQtyByItemId: Map<string, number>;
20
+ itemTitleMap: Map<string, string>;
21
+ };
22
+ export declare const formatQty: (qty: number) => string;
23
+ export declare const getNormalizedRules: (rawRules: unknown) => CompulsoryItemRule[];
24
+ export declare const getFallbackRuleItemIdByNameMap: (rules: CompulsoryItemRule[], itemTitleMap: Map<string, string>) => Map<string, string>;
25
+ export declare const getRuleQuantitySummary: (params: {
26
+ rules: CompulsoryItemRule[];
27
+ slotActiveBills: CCdtoBillSummary[];
28
+ draftItems: QuantityItem[];
29
+ itemTitleMap: Map<string, string>;
30
+ }) => RuleQuantitySummary;
31
+ export declare const getAutoAddPlans: (rules: CompulsoryItemRule[], quantitySummary: RuleQuantitySummary) => Array<{
32
+ itemId: string;
33
+ missingQty: number;
34
+ }>;
35
+ export declare const getViolationMessages: (rules: CompulsoryItemRule[], quantitySummary: RuleQuantitySummary) => string[];
@@ -0,0 +1,131 @@
1
+ const INCLUDED_BILL_ITEM_STATUSES = new Set(["SENT", "DRAFT"]);
2
+ export const formatQty = (qty) => {
3
+ if (Number.isInteger(qty))
4
+ return `${qty}`;
5
+ return qty.toFixed(2).replace(/\.?0+$/, "");
6
+ };
7
+ const normalizeLookupKey = (value) => {
8
+ if (typeof value !== "string")
9
+ return null;
10
+ const normalizedValue = value.trim().replace(/\s+/g, " ").toLowerCase();
11
+ return normalizedValue || null;
12
+ };
13
+ const addQuantity = (quantityMap, itemId, quantity) => {
14
+ if (!itemId || typeof itemId !== "string")
15
+ return;
16
+ if (typeof quantity !== "number" || !Number.isFinite(quantity))
17
+ return;
18
+ if (quantity <= 0)
19
+ return;
20
+ quantityMap.set(itemId, (quantityMap.get(itemId) || 0) + quantity);
21
+ };
22
+ const getResolvedItemId = (item, fallbackItemIdByName) => {
23
+ const productId = typeof item.productId === "string" ? item.productId.trim() : "";
24
+ if (productId)
25
+ return productId;
26
+ if (!fallbackItemIdByName)
27
+ return null;
28
+ const itemName = normalizeLookupKey(item.name || item.title);
29
+ if (!itemName)
30
+ return null;
31
+ return fallbackItemIdByName.get(itemName) || null;
32
+ };
33
+ const getQuantityMap = (items, fallbackItemIdByName) => {
34
+ const quantityMap = new Map();
35
+ for (const item of items) {
36
+ addQuantity(quantityMap, getResolvedItemId(item, fallbackItemIdByName), item.quantity);
37
+ }
38
+ return quantityMap;
39
+ };
40
+ export const getNormalizedRules = (rawRules) => {
41
+ if (!Array.isArray(rawRules))
42
+ return [];
43
+ const normalizedRules = [];
44
+ for (const rule of rawRules) {
45
+ const itemId = typeof rule?.itemId === "string"
46
+ ? rule.itemId.trim()
47
+ : "";
48
+ const minQty = rule?.minQty;
49
+ const maxQty = rule?.maxQty;
50
+ if (!itemId || !Number.isFinite(minQty) || !Number.isFinite(maxQty)) {
51
+ console.warn("[compulsory-items] Skip invalid rule. Expect { itemId, minQty, maxQty } with finite numbers.", rule);
52
+ continue;
53
+ }
54
+ if (minQty < 0 || maxQty < minQty) {
55
+ console.warn("[compulsory-items] Skip invalid rule. Expect minQty >= 0 and maxQty >= minQty.", rule);
56
+ continue;
57
+ }
58
+ normalizedRules.push({ itemId, minQty, maxQty });
59
+ }
60
+ return normalizedRules;
61
+ };
62
+ export const getFallbackRuleItemIdByNameMap = (rules, itemTitleMap) => {
63
+ const fallbackItemIdByName = new Map();
64
+ const ambiguousNames = new Set();
65
+ for (const rule of rules) {
66
+ const itemTitle = itemTitleMap.get(rule.itemId);
67
+ const normalizedTitle = normalizeLookupKey(itemTitle);
68
+ if (!normalizedTitle || ambiguousNames.has(normalizedTitle))
69
+ continue;
70
+ const existingItemId = fallbackItemIdByName.get(normalizedTitle);
71
+ if (!existingItemId) {
72
+ fallbackItemIdByName.set(normalizedTitle, rule.itemId);
73
+ continue;
74
+ }
75
+ if (existingItemId !== rule.itemId) {
76
+ fallbackItemIdByName.delete(normalizedTitle);
77
+ ambiguousNames.add(normalizedTitle);
78
+ }
79
+ }
80
+ return fallbackItemIdByName;
81
+ };
82
+ const getSubmittedItems = (slotActiveBills) => slotActiveBills.flatMap((bill) => (bill.items || []).filter((item) => INCLUDED_BILL_ITEM_STATUSES.has(String(item.status || ""))));
83
+ export const getRuleQuantitySummary = (params) => {
84
+ const fallbackRuleItemIdByName = getFallbackRuleItemIdByNameMap(params.rules, params.itemTitleMap);
85
+ return {
86
+ itemTitleMap: params.itemTitleMap,
87
+ submittedQtyByItemId: getQuantityMap(getSubmittedItems(params.slotActiveBills), fallbackRuleItemIdByName),
88
+ draftQtyByItemId: getQuantityMap(params.draftItems),
89
+ };
90
+ };
91
+ export const getAutoAddPlans = (rules, quantitySummary) => {
92
+ const plans = [];
93
+ for (const rule of rules) {
94
+ const submittedQty = quantitySummary.submittedQtyByItemId.get(rule.itemId) || 0;
95
+ if (submittedQty >= rule.minQty)
96
+ continue;
97
+ const draftQty = quantitySummary.draftQtyByItemId.get(rule.itemId) || 0;
98
+ const missingQty = Math.max(0, rule.minQty - submittedQty - draftQty);
99
+ if (missingQty <= 0)
100
+ continue;
101
+ plans.push({
102
+ itemId: rule.itemId,
103
+ missingQty,
104
+ });
105
+ }
106
+ return plans;
107
+ };
108
+ export const getViolationMessages = (rules, quantitySummary) => {
109
+ const violationMessages = [];
110
+ for (const rule of rules) {
111
+ const submittedQty = quantitySummary.submittedQtyByItemId.get(rule.itemId) || 0;
112
+ const draftQty = quantitySummary.draftQtyByItemId.get(rule.itemId) || 0;
113
+ const totalQty = submittedQty + draftQty;
114
+ const itemTitle = quantitySummary.itemTitleMap.get(rule.itemId) || rule.itemId;
115
+ if (totalQty < rule.minQty) {
116
+ const delta = rule.minQty - totalQty;
117
+ violationMessages.push(`Add ${formatQty(delta)} more "${itemTitle}" (minimum ${formatQty(rule.minQty)}, current total ${formatQty(totalQty)}).`);
118
+ }
119
+ if (submittedQty > rule.maxQty) {
120
+ if (draftQty > 0) {
121
+ violationMessages.push(`"${itemTitle}" is already above the limit in submitted orders. Remove ${formatQty(draftQty)} from your current cart to continue.`);
122
+ }
123
+ continue;
124
+ }
125
+ if (totalQty > rule.maxQty) {
126
+ const delta = totalQty - rule.maxQty;
127
+ violationMessages.push(`Remove ${formatQty(delta)} "${itemTitle}" from your current cart (maximum ${formatQty(rule.maxQty)}, current total ${formatQty(totalQty)}).`);
128
+ }
129
+ }
130
+ return violationMessages;
131
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feedmepos/order-plugin-gallery",
3
- "version": "0.0.10-beta.0",
3
+ "version": "0.0.10-beta.2",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"