@fayz-ai/shop 0.9.0 → 0.10.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.
Files changed (144) hide show
  1. package/README.md +18 -1
  2. package/dist/admin.d.ts +2 -0
  3. package/dist/admin.d.ts.map +1 -0
  4. package/dist/catalog.d.ts +1 -3
  5. package/dist/catalog.d.ts.map +1 -1
  6. package/dist/catalog.js +2 -2
  7. package/dist/chunk-3GERSY5F.js +526 -0
  8. package/dist/chunk-3GERSY5F.js.map +1 -0
  9. package/dist/chunk-FQPGE3YN.js +3 -0
  10. package/dist/{chunk-JR25TISO.js.map → chunk-FQPGE3YN.js.map} +1 -1
  11. package/dist/{chunk-CTKZ6O2J.js → chunk-IKNGCRUM.js} +41 -15
  12. package/dist/chunk-IKNGCRUM.js.map +1 -0
  13. package/dist/{chunk-IYQHKMLB.js → chunk-IL6GA4ER.js} +4 -4
  14. package/dist/chunk-IL6GA4ER.js.map +1 -0
  15. package/dist/chunk-KLDS7IPU.js +17 -0
  16. package/dist/chunk-KLDS7IPU.js.map +1 -0
  17. package/dist/chunk-RGTIJQJL.js +93 -0
  18. package/dist/chunk-RGTIJQJL.js.map +1 -0
  19. package/dist/{chunk-SVTCJ6HP.js → chunk-U3O2I3RM.js} +395 -27
  20. package/dist/chunk-U3O2I3RM.js.map +1 -0
  21. package/dist/chunk-XERLK5LD.js +1415 -0
  22. package/dist/chunk-XERLK5LD.js.map +1 -0
  23. package/dist/chunk-XOP6SHPB.js +142 -0
  24. package/dist/chunk-XOP6SHPB.js.map +1 -0
  25. package/dist/client.d.ts +2 -0
  26. package/dist/client.d.ts.map +1 -0
  27. package/dist/{mock-catalog.d.ts → data/mock-catalog.d.ts} +21 -1
  28. package/dist/data/mock-catalog.d.ts.map +1 -0
  29. package/dist/data/mock-provider.d.ts +133 -0
  30. package/dist/data/mock-provider.d.ts.map +1 -0
  31. package/dist/data/mock.d.ts +3 -0
  32. package/dist/data/mock.d.ts.map +1 -0
  33. package/dist/data/provider.d.ts +142 -0
  34. package/dist/data/provider.d.ts.map +1 -0
  35. package/dist/data/supabase-provider.d.ts +100 -0
  36. package/dist/data/supabase-provider.d.ts.map +1 -0
  37. package/dist/data/tables.d.ts +35 -0
  38. package/dist/data/tables.d.ts.map +1 -0
  39. package/dist/data/tenant.d.ts.map +1 -0
  40. package/dist/entities/entity-reconcile.d.ts +32 -0
  41. package/dist/entities/entity-reconcile.d.ts.map +1 -0
  42. package/dist/entities/entity-sql.d.ts +50 -0
  43. package/dist/entities/entity-sql.d.ts.map +1 -0
  44. package/dist/entities/entity-store.d.ts +58 -0
  45. package/dist/entities/entity-store.d.ts.map +1 -0
  46. package/dist/entities/index.d.ts +11 -0
  47. package/dist/entities/index.d.ts.map +1 -0
  48. package/dist/entities/mock-entity-store.d.ts +17 -0
  49. package/dist/entities/mock-entity-store.d.ts.map +1 -0
  50. package/dist/entities/sidecar.d.ts +9 -0
  51. package/dist/entities/sidecar.d.ts.map +1 -0
  52. package/dist/entities/supabase-entity-store.d.ts +8 -0
  53. package/dist/entities/supabase-entity-store.d.ts.map +1 -0
  54. package/dist/entities.js +159 -0
  55. package/dist/entities.js.map +1 -0
  56. package/dist/index.d.ts +17 -10
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +728 -41
  59. package/dist/index.js.map +1 -1
  60. package/dist/inventory/allocation.d.ts +98 -0
  61. package/dist/inventory/allocation.d.ts.map +1 -0
  62. package/dist/inventory/index.d.ts +13 -0
  63. package/dist/inventory/index.d.ts.map +1 -0
  64. package/dist/inventory/mock-stock-store.d.ts +89 -0
  65. package/dist/inventory/mock-stock-store.d.ts.map +1 -0
  66. package/dist/inventory/postgrest-double.d.ts +65 -0
  67. package/dist/inventory/postgrest-double.d.ts.map +1 -0
  68. package/dist/inventory/shopify-mapping.d.ts +67 -0
  69. package/dist/inventory/shopify-mapping.d.ts.map +1 -0
  70. package/dist/inventory/stock-contract.d.ts +46 -0
  71. package/dist/inventory/stock-contract.d.ts.map +1 -0
  72. package/dist/inventory/stock-store.d.ts +96 -0
  73. package/dist/inventory/stock-store.d.ts.map +1 -0
  74. package/dist/inventory/stock-types.d.ts +292 -0
  75. package/dist/inventory/stock-types.d.ts.map +1 -0
  76. package/dist/inventory/supabase-stock-store.d.ts +59 -0
  77. package/dist/inventory/supabase-stock-store.d.ts.map +1 -0
  78. package/dist/inventory/vocabulary.d.ts +86 -0
  79. package/dist/inventory/vocabulary.d.ts.map +1 -0
  80. package/dist/inventory.js +4 -0
  81. package/dist/inventory.js.map +1 -0
  82. package/dist/lib/catalog.d.ts +4 -0
  83. package/dist/lib/catalog.d.ts.map +1 -0
  84. package/dist/{commerce.d.ts → lib/commerce.d.ts} +2 -2
  85. package/dist/lib/commerce.d.ts.map +1 -0
  86. package/dist/lib/emails.d.ts +50 -0
  87. package/dist/lib/emails.d.ts.map +1 -0
  88. package/dist/lib/message-events.d.ts +4 -0
  89. package/dist/lib/message-events.d.ts.map +1 -0
  90. package/dist/lib/runtime.d.ts +5 -0
  91. package/dist/lib/runtime.d.ts.map +1 -0
  92. package/dist/mock.d.ts +1 -2
  93. package/dist/mock.d.ts.map +1 -1
  94. package/dist/mock.js +4 -2
  95. package/dist/provider.d.ts +1 -88
  96. package/dist/provider.d.ts.map +1 -1
  97. package/dist/rules/adapter.d.ts +36 -0
  98. package/dist/rules/adapter.d.ts.map +1 -0
  99. package/dist/rules/attributes.d.ts +64 -0
  100. package/dist/rules/attributes.d.ts.map +1 -0
  101. package/dist/rules/context.d.ts +43 -0
  102. package/dist/rules/context.d.ts.map +1 -0
  103. package/dist/rules/evaluator.d.ts +37 -0
  104. package/dist/rules/evaluator.d.ts.map +1 -0
  105. package/dist/rules/index.d.ts +13 -0
  106. package/dist/rules/index.d.ts.map +1 -0
  107. package/dist/rules/prices.d.ts +25 -0
  108. package/dist/rules/prices.d.ts.map +1 -0
  109. package/dist/rules/rule-types.d.ts +160 -0
  110. package/dist/rules/rule-types.d.ts.map +1 -0
  111. package/dist/rules.js +22 -0
  112. package/dist/rules.js.map +1 -0
  113. package/dist/runtime.d.ts +1 -4
  114. package/dist/runtime.d.ts.map +1 -1
  115. package/dist/runtime.js +5 -3
  116. package/dist/types.d.ts +231 -0
  117. package/dist/types.d.ts.map +1 -1
  118. package/package.json +26 -20
  119. package/dist/chunk-CTKZ6O2J.js.map +0 -1
  120. package/dist/chunk-IYQHKMLB.js.map +0 -1
  121. package/dist/chunk-JR25TISO.js +0 -3
  122. package/dist/chunk-SVTCJ6HP.js.map +0 -1
  123. package/dist/commerce.d.ts.map +0 -1
  124. package/dist/mock-catalog.d.ts.map +0 -1
  125. package/dist/mock-provider.d.ts +0 -60
  126. package/dist/mock-provider.d.ts.map +0 -1
  127. package/dist/supabase-provider.d.ts +0 -56
  128. package/dist/supabase-provider.d.ts.map +0 -1
  129. package/dist/tables.d.ts +0 -12
  130. package/dist/tables.d.ts.map +0 -1
  131. package/dist/tenant.d.ts.map +0 -1
  132. package/src/catalog.ts +0 -3
  133. package/src/commerce.ts +0 -120
  134. package/src/index.ts +0 -40
  135. package/src/mock-catalog.ts +0 -266
  136. package/src/mock-provider.ts +0 -480
  137. package/src/mock.ts +0 -2
  138. package/src/provider.ts +0 -117
  139. package/src/runtime.ts +0 -20
  140. package/src/supabase-provider.ts +0 -754
  141. package/src/tables.ts +0 -17
  142. package/src/tenant.ts +0 -21
  143. package/src/types.ts +0 -623
  144. /package/dist/{tenant.d.ts → data/tenant.d.ts} +0 -0
package/README.md CHANGED
@@ -17,6 +17,7 @@ It's provider-first by design. Ship a demo on `createMockShopProvider`, then dro
17
17
  - **Runtime** — `getShopProvider` / `setShopProvider` singleton wiring
18
18
  - **Tenancy** — `setShopTenantResolver` / `getShopTenantId` for multi-store isolation
19
19
  - **Public domain types** — products, orders, customers, discounts, shipping
20
+ - **`StockStore` contract** (`@fayz-ai/shop/inventory`) — stock as an append-only ledger rather than a scalar, with a Supabase and an in-memory implementation held to one shared contract suite
20
21
 
21
22
  ## Install
22
23
  ```bash
@@ -31,8 +32,24 @@ import { setShopProvider, createSupabaseShopProvider } from '@fayz-ai/shop'
31
32
  setShopProvider(createSupabaseShopProvider({ /* supabase + storeId */ }))
32
33
  ```
33
34
 
35
+ Stock is a ledger: movements are appended, balances are derived from them, and `available = on_hand − reserved`. The same code runs against either implementation.
36
+
37
+ ```ts
38
+ import { createSupabaseStockStore } from '@fayz-ai/shop/inventory'
39
+
40
+ const stock = createSupabaseStockStore()
41
+ await stock.recordMovement({
42
+ variantId,
43
+ kind: 'receipt', // vocabulary rows, not a hardcoded enum
44
+ reasonCode: 'purchase',
45
+ quantity: 12, // signed; the kind's direction decides the sign
46
+ idempotencyKey: `po-1180:${variantId}`, // a retry returns the same movement
47
+ })
48
+ await stock.getAvailable(variantId)
49
+ ```
50
+
34
51
  ## Part of the Fayz SDK
35
52
  The commerce engine `@fayz-ai/storefront` renders and `@fayz-ai/plugin-shop` administers. Pair with `@fayz-ai/sdk`'s shop client for generated storefronts.
36
53
 
37
54
  ## Roadmap & contributing
38
- Built and evolving in the open. See the [Fayz SDK roadmap](../../docs/ROADMAP.md).
55
+ Built and evolving in the open. See the [Fayz SDK roadmap](../../docs/platform/ROADMAP.md).
@@ -0,0 +1,2 @@
1
+ export * from './index';
2
+ //# sourceMappingURL=admin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin.d.ts","sourceRoot":"","sources":["../src/admin.ts"],"names":[],"mappings":"AACA,cAAc,SAAS,CAAA"}
package/dist/catalog.d.ts CHANGED
@@ -1,4 +1,2 @@
1
- export { MOCK_PRODUCTS, MOCK_CATEGORIES, MOCK_DISCOUNTS, svgPlaceholder } from './mock-catalog';
2
- export { buildMockCatalog } from './mock-catalog';
3
- export type { CatalogInput, CatalogProductInput, CatalogCategoryInput } from './mock-catalog';
1
+ export * from './lib/catalog';
4
2
  //# sourceMappingURL=catalog.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC/F,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACjD,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA"}
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AACA,cAAc,eAAe,CAAA"}
package/dist/catalog.js CHANGED
@@ -1,4 +1,4 @@
1
- import './chunk-JR25TISO.js';
2
- export { MOCK_CATEGORIES, MOCK_DISCOUNTS, MOCK_PRODUCTS, buildMockCatalog, svgPlaceholder } from './chunk-CTKZ6O2J.js';
1
+ import './chunk-FQPGE3YN.js';
2
+ export { MOCK_CATEGORIES, MOCK_DISCOUNTS, MOCK_PRODUCTS, buildMockCatalog, svgPlaceholder } from './chunk-IKNGCRUM.js';
3
3
  //# sourceMappingURL=catalog.js.map
4
4
  //# sourceMappingURL=catalog.js.map
@@ -0,0 +1,526 @@
1
+ import { Registry } from '@fayz-ai/core';
2
+
3
+ // src/rules/rule-types.ts
4
+ var RULE_OPERATORS = ["gte", "lte", "gt", "lt", "eq", "ne", "in"];
5
+ var APPLICATION_METHOD_TYPES = ["percentage", "fixed"];
6
+ var APPLICATION_METHOD_TARGET_TYPES = ["order", "items", "shipping"];
7
+ var APPLICATION_METHOD_ALLOCATIONS = ["each", "across", "once"];
8
+ var PROMOTION_TYPES = ["standard", "buyget"];
9
+ var PROMOTION_STATUSES = ["draft", "active", "inactive"];
10
+ function cartSubtotal(cart) {
11
+ return roundCents(cart.items.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0));
12
+ }
13
+ function roundCents(value) {
14
+ return Math.round(value * 100) / 100;
15
+ }
16
+ var RULE_ATTRIBUTE_FIELD_TYPES = ["text", "number", "boolean"];
17
+ var ID_OPERATORS = ["eq", "ne", "in"];
18
+ var NUMBER_OPERATORS = ["gte", "lte", "gt", "lt", "eq", "ne"];
19
+ var ITEM_PATH_PREFIX = "items.";
20
+ var CORE_RULE_ATTRIBUTES = [
21
+ {
22
+ key: "code",
23
+ path: "codes",
24
+ label: "Cupom informado",
25
+ fieldType: "text",
26
+ valueSource: null,
27
+ operators: ["eq", "in"]
28
+ },
29
+ {
30
+ key: "cart.subtotal",
31
+ path: "subtotal",
32
+ label: "Subtotal do carrinho",
33
+ fieldType: "number",
34
+ valueSource: null,
35
+ operators: NUMBER_OPERATORS
36
+ },
37
+ {
38
+ key: "item.quantity",
39
+ path: "items.quantity",
40
+ label: "Quantidade do item",
41
+ fieldType: "number",
42
+ valueSource: null,
43
+ operators: NUMBER_OPERATORS
44
+ },
45
+ {
46
+ key: "product.id",
47
+ path: "items.product_id",
48
+ label: "Produto",
49
+ fieldType: "text",
50
+ valueSource: "products",
51
+ operators: ID_OPERATORS
52
+ },
53
+ {
54
+ key: "product.category_id",
55
+ path: "items.category_id",
56
+ label: "Categoria do produto",
57
+ fieldType: "text",
58
+ valueSource: "categories",
59
+ operators: ID_OPERATORS
60
+ },
61
+ {
62
+ key: "product.collection_id",
63
+ path: "items.collection_id",
64
+ label: "Cole\xE7\xE3o do produto",
65
+ fieldType: "text",
66
+ valueSource: "collections",
67
+ operators: ID_OPERATORS
68
+ },
69
+ {
70
+ key: "customer.email",
71
+ path: "customer.email",
72
+ label: "E-mail do cliente",
73
+ fieldType: "text",
74
+ valueSource: null,
75
+ operators: ID_OPERATORS
76
+ },
77
+ {
78
+ key: "customer.tag",
79
+ path: "customer.tags",
80
+ label: "Tag do cliente",
81
+ fieldType: "text",
82
+ valueSource: "customer_tags",
83
+ operators: ["eq", "ne", "in"]
84
+ },
85
+ {
86
+ key: "shipping.region",
87
+ path: "shipping.region",
88
+ label: "Regi\xE3o de entrega",
89
+ fieldType: "text",
90
+ valueSource: "regions",
91
+ operators: ID_OPERATORS
92
+ }
93
+ ];
94
+ var ruleAttributeRegistry = new Registry("ruleAttribute");
95
+ var coreRegistered = false;
96
+ function registerCoreRuleAttributes() {
97
+ if (coreRegistered && ruleAttributeRegistry.has("code")) return;
98
+ for (const attribute of CORE_RULE_ATTRIBUTES) {
99
+ ruleAttributeRegistry.register(attribute.key, attribute, {
100
+ title: attribute.label,
101
+ description: `Rule attribute (${attribute.fieldType}) \u2014 operators: ${attribute.operators.join(", ")}`,
102
+ source: "sdk"
103
+ });
104
+ }
105
+ coreRegistered = true;
106
+ }
107
+ function registerRuleAttribute(attribute, options = {}) {
108
+ registerCoreRuleAttributes();
109
+ if (!attribute.key) throw new Error("registerRuleAttribute: attribute.key is required");
110
+ if (!RULE_ATTRIBUTE_FIELD_TYPES.includes(attribute.fieldType)) {
111
+ throw new Error(
112
+ `registerRuleAttribute: "${attribute.key}" has unknown fieldType "${attribute.fieldType}". Valid field types: ${RULE_ATTRIBUTE_FIELD_TYPES.join(", ")}`
113
+ );
114
+ }
115
+ for (const op of attribute.operators) {
116
+ if (!RULE_OPERATORS.includes(op)) {
117
+ throw new Error(
118
+ `registerRuleAttribute: "${attribute.key}" declares unknown operator "${op}". Valid operators: ${RULE_OPERATORS.join(", ")}`
119
+ );
120
+ }
121
+ }
122
+ if (ruleAttributeRegistry.has(attribute.key) && !options.replace) {
123
+ throw new Error(
124
+ `registerRuleAttribute: attribute "${attribute.key}" is already registered. Registered attributes: ${listRuleAttributeKeys().join(", ")}`
125
+ );
126
+ }
127
+ ruleAttributeRegistry.register(attribute.key, attribute, {
128
+ title: attribute.label,
129
+ description: `Rule attribute (${attribute.fieldType}) \u2014 operators: ${attribute.operators.join(", ")}`,
130
+ source: "app",
131
+ ...options.meta
132
+ });
133
+ }
134
+ function resolveRuleAttribute(key) {
135
+ registerCoreRuleAttributes();
136
+ const attribute = ruleAttributeRegistry.get(key);
137
+ if (!attribute) {
138
+ throw new Error(`Unknown rule attribute "${key}". Registered attributes: ${listRuleAttributeKeys().join(", ")}`);
139
+ }
140
+ return attribute;
141
+ }
142
+ function hasRuleAttribute(key) {
143
+ registerCoreRuleAttributes();
144
+ return ruleAttributeRegistry.has(key);
145
+ }
146
+ function listRuleAttributeKeys() {
147
+ registerCoreRuleAttributes();
148
+ return ruleAttributeRegistry.ids().sort();
149
+ }
150
+ function listRuleAttributes() {
151
+ registerCoreRuleAttributes();
152
+ return ruleAttributeRegistry.list().map((entry) => entry.value).sort((a, b) => a.key.localeCompare(b.key));
153
+ }
154
+ function isItemScopedAttribute(attribute) {
155
+ return attribute.path.startsWith(ITEM_PATH_PREFIX);
156
+ }
157
+ function clearRuleAttributes() {
158
+ ruleAttributeRegistry.clear();
159
+ coreRegistered = false;
160
+ }
161
+
162
+ // src/rules/evaluator.ts
163
+ var ITEM_FIELDS = {
164
+ quantity: "quantity",
165
+ unit_price: "unitPrice",
166
+ product_id: "productId",
167
+ category_id: "categoryId",
168
+ collection_id: "collectionId"
169
+ };
170
+ function itemValue(item, attribute) {
171
+ const field = ITEM_FIELDS[attribute.path.slice("items.".length)];
172
+ return field ? item[field] : void 0;
173
+ }
174
+ function cartValues(cart, attribute, context) {
175
+ if (attribute.key in context) return flatten(context[attribute.key]);
176
+ const root = {
177
+ subtotal: cartSubtotal(cart),
178
+ codes: (cart.codes ?? []).map((code) => code.trim().toUpperCase()),
179
+ customer: { email: cart.customer?.email ?? null, tags: cart.customer?.tags ?? [] },
180
+ shipping: { region: cart.region ?? null, total: cart.shippingTotal ?? null }
181
+ };
182
+ let value = root;
183
+ for (const segment of attribute.path.split(".")) {
184
+ if (value === null || typeof value !== "object") return [];
185
+ value = value[segment];
186
+ }
187
+ return flatten(value);
188
+ }
189
+ function flatten(value) {
190
+ if (value === null || value === void 0) return [];
191
+ return Array.isArray(value) ? value.filter((v) => v !== null && v !== void 0) : [value];
192
+ }
193
+ function scalarEq(a, b) {
194
+ if (typeof a === "string" && typeof b === "string") return a.toUpperCase() === b.toUpperCase();
195
+ return a === b;
196
+ }
197
+ function numeric(op, values, ruleValues) {
198
+ const bound = Number(ruleValues[0]);
199
+ if (!Number.isFinite(bound)) return false;
200
+ return values.some((value) => {
201
+ const n = Number(value);
202
+ return Number.isFinite(n) && op(n, bound);
203
+ });
204
+ }
205
+ function matchRuleValues(rule, values) {
206
+ switch (rule.operator) {
207
+ case "eq":
208
+ case "in":
209
+ return values.some((value) => rule.values.some((rv) => scalarEq(value, rv)));
210
+ case "ne":
211
+ return !values.some((value) => rule.values.some((rv) => scalarEq(value, rv)));
212
+ case "gt":
213
+ return numeric((a, b) => a > b, values, rule.values);
214
+ case "gte":
215
+ return numeric((a, b) => a >= b, values, rule.values);
216
+ case "lt":
217
+ return numeric((a, b) => a < b, values, rule.values);
218
+ case "lte":
219
+ return numeric((a, b) => a <= b, values, rule.values);
220
+ }
221
+ }
222
+ function ruleMatchesCart(rule, cart, context) {
223
+ const attribute = resolveRuleAttribute(rule.attribute);
224
+ if (isItemScopedAttribute(attribute) && !(attribute.key in context)) {
225
+ return cart.items.some((item) => matchRuleValues(rule, flatten(itemValue(item, attribute))));
226
+ }
227
+ return matchRuleValues(rule, cartValues(cart, attribute, context));
228
+ }
229
+ function ruleMatchesItem(rule, item, cart, context) {
230
+ const attribute = resolveRuleAttribute(rule.attribute);
231
+ if (isItemScopedAttribute(attribute) && !(attribute.key in context)) {
232
+ return matchRuleValues(rule, flatten(itemValue(item, attribute)));
233
+ }
234
+ return matchRuleValues(rule, cartValues(cart, attribute, context));
235
+ }
236
+ function rulesMatchCart(rules, cart, context = {}) {
237
+ return (rules ?? []).every((rule) => ruleMatchesCart(rule, cart, context));
238
+ }
239
+ function itemsMatching(rules, cart, context) {
240
+ const list = rules ?? [];
241
+ return cart.items.filter((item) => list.every((rule) => ruleMatchesItem(rule, item, cart, context)));
242
+ }
243
+ function promotionGateReason(promotion, cart, options = {}) {
244
+ const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
245
+ if (promotion.status !== void 0 && promotion.status !== "active") return "inactive";
246
+ if (promotion.startsAt && promotion.startsAt > now) return "not_started";
247
+ if (promotion.endsAt && promotion.endsAt < now) return "expired";
248
+ if (promotion.usageLimit != null && (promotion.timesUsed ?? 0) >= promotion.usageLimit) return "usage_limit";
249
+ if (promotion.code) {
250
+ const entered = (cart.codes ?? []).map((code) => code.trim().toUpperCase());
251
+ if (!entered.includes(promotion.code.trim().toUpperCase())) return "code_not_entered";
252
+ } else if (promotion.isAutomatic === false) {
253
+ return "code_not_entered";
254
+ }
255
+ if (!rulesMatchCart(promotion.rules, cart, options.context ?? {})) return "rules_not_matched";
256
+ return null;
257
+ }
258
+ function clampPercent(value) {
259
+ return Math.min(Math.max(value, 0), 100);
260
+ }
261
+ function splitProportionally(total, weights) {
262
+ const sum = weights.reduce((acc, weight) => acc + weight, 0);
263
+ if (sum <= 0 || total <= 0) return weights.map(() => 0);
264
+ const cents = Math.round(total * 100);
265
+ const raw = weights.map((weight) => cents * weight / sum);
266
+ const floors = raw.map(Math.floor);
267
+ let remainder = cents - floors.reduce((acc, value) => acc + value, 0);
268
+ const order = raw.map((value, index) => ({ index, frac: value - Math.floor(value) })).sort((a, b) => b.frac - a.frac || a.index - b.index);
269
+ for (const { index } of order) {
270
+ if (remainder <= 0) break;
271
+ floors[index] += 1;
272
+ remainder -= 1;
273
+ }
274
+ return floors.map((value) => value / 100);
275
+ }
276
+ function lineId(item) {
277
+ return item.id ?? item.productId;
278
+ }
279
+ function unitDiscount(method, unitPrice) {
280
+ if (method.type === "percentage") return roundCents(unitPrice * clampPercent(method.value) / 100);
281
+ return Math.min(method.value, unitPrice);
282
+ }
283
+ function isBuyget(promotion) {
284
+ const min = promotion.applicationMethod.buyRulesMinQuantity;
285
+ return promotion.type === "buyget" || min != null && min >= 1;
286
+ }
287
+ function applyBuyget(promotion, cart, context) {
288
+ const method = promotion.applicationMethod;
289
+ const min = method.buyRulesMinQuantity ?? 1;
290
+ const per = method.applyToQuantity ?? 1;
291
+ const buyQty = itemsMatching(method.buyRules, cart, context).reduce((sum, item) => sum + item.quantity, 0);
292
+ const times = Math.floor(buyQty / Math.max(min, 1));
293
+ if (times <= 0 || per <= 0) return [];
294
+ let grantUnits = times * per;
295
+ if (method.maxQuantity != null) grantUnits = Math.min(grantUnits, method.maxQuantity);
296
+ if (grantUnits <= 0) return [];
297
+ const targets = itemsMatching(method.targetRules, cart, context).map((item) => ({ item, id: lineId(item) })).sort((a, b) => a.item.unitPrice - b.item.unitPrice || a.id.localeCompare(b.id));
298
+ if (targets.length === 0) {
299
+ const pins = (method.targetRules ?? []).filter((rule) => rule.attribute === "product.id" && (rule.operator === "eq" || rule.operator === "in")).flatMap((rule) => rule.values);
300
+ if (pins.length === 1 && typeof pins[0] === "string") {
301
+ return [{
302
+ action: "addFreeItem",
303
+ promotionId: promotion.id,
304
+ code: promotion.code ?? null,
305
+ productId: pins[0],
306
+ quantity: grantUnits
307
+ }];
308
+ }
309
+ return [];
310
+ }
311
+ const actions = [];
312
+ let remaining = grantUnits;
313
+ for (const target of targets) {
314
+ if (remaining <= 0) break;
315
+ const units = Math.min(target.item.quantity, remaining);
316
+ const amount = roundCents(unitDiscount(method, target.item.unitPrice) * units);
317
+ remaining -= units;
318
+ if (amount <= 0) continue;
319
+ actions.push({
320
+ action: "addItemDiscount",
321
+ promotionId: promotion.id,
322
+ code: promotion.code ?? null,
323
+ itemId: target.id,
324
+ amount
325
+ });
326
+ }
327
+ return actions;
328
+ }
329
+ function applyStandard(promotion, cart, context) {
330
+ const method = promotion.applicationMethod;
331
+ const code = promotion.code ?? null;
332
+ if (method.targetType === "shipping") {
333
+ const shipping = cart.shippingTotal ?? 0;
334
+ if (shipping <= 0) return [];
335
+ const amount = method.type === "percentage" ? roundCents(shipping * clampPercent(method.value) / 100) : roundCents(Math.min(method.value, shipping));
336
+ if (amount <= 0) return [];
337
+ return [{ action: "addShippingDiscount", promotionId: promotion.id, code, amount }];
338
+ }
339
+ const eligible = method.targetType === "order" ? cart.items.map((item) => ({ item, id: lineId(item) })) : itemsMatching(method.targetRules, cart, context).map((item) => ({ item, id: lineId(item) }));
340
+ if (eligible.length === 0) return [];
341
+ const lineTotals = eligible.map(({ item }) => roundCents(item.unitPrice * item.quantity));
342
+ const eligibleTotal = roundCents(lineTotals.reduce((sum, value) => sum + value, 0));
343
+ const emit = (amounts) => amounts.flatMap(
344
+ (amount, index) => amount > 0 ? [{ action: "addItemDiscount", promotionId: promotion.id, code, itemId: eligible[index].id, amount }] : []
345
+ );
346
+ const allocation = method.targetType === "order" ? "across" : method.allocation ?? "across";
347
+ if (allocation === "each") {
348
+ return emit(
349
+ eligible.map(({ item }) => {
350
+ const units = method.maxQuantity != null ? Math.min(item.quantity, method.maxQuantity) : item.quantity;
351
+ return roundCents(unitDiscount(method, item.unitPrice) * units);
352
+ })
353
+ );
354
+ }
355
+ if (allocation === "once") {
356
+ let cheapest = 0;
357
+ for (let i = 1; i < eligible.length; i++) {
358
+ if (eligible[i].item.unitPrice < eligible[cheapest].item.unitPrice) cheapest = i;
359
+ }
360
+ const amounts = eligible.map(() => 0);
361
+ amounts[cheapest] = unitDiscount(method, eligible[cheapest].item.unitPrice);
362
+ return emit(amounts);
363
+ }
364
+ const total = method.type === "percentage" ? roundCents(eligibleTotal * clampPercent(method.value) / 100) : roundCents(Math.min(method.value, eligibleTotal));
365
+ return emit(splitProportionally(total, lineTotals));
366
+ }
367
+ function totalRulesCount(promotion) {
368
+ return (promotion.rules?.length ?? 0) + (promotion.applicationMethod.targetRules?.length ?? 0) + (promotion.applicationMethod.buyRules?.length ?? 0);
369
+ }
370
+ function orderPromotions(promotions) {
371
+ return [...promotions].sort((a, b) => {
372
+ const buyget = Number(isBuyget(b)) - Number(isBuyget(a));
373
+ if (buyget !== 0) return buyget;
374
+ const specificity = totalRulesCount(b) - totalRulesCount(a);
375
+ if (specificity !== 0) return specificity;
376
+ return a.id.localeCompare(b.id);
377
+ });
378
+ }
379
+ function computePromotionActions(cart, promotions, options = {}) {
380
+ const context = options.context ?? {};
381
+ const actions = [];
382
+ for (const promotion of orderPromotions(promotions)) {
383
+ if (promotionGateReason(promotion, cart, options) !== null) continue;
384
+ actions.push(
385
+ ...isBuyget(promotion) ? applyBuyget(promotion, cart, context) : applyStandard(promotion, cart, context)
386
+ );
387
+ }
388
+ return actions;
389
+ }
390
+ function actionsDiscountTotal(actions) {
391
+ return roundCents(
392
+ actions.reduce((sum, action) => sum + ("amount" in action ? action.amount : 0), 0)
393
+ );
394
+ }
395
+
396
+ // src/rules/adapter.ts
397
+ function storefrontDiscountToPromotion(discount) {
398
+ const code = discount.code.trim().toUpperCase();
399
+ return {
400
+ id: `config-discount:${code}`,
401
+ code,
402
+ type: "standard",
403
+ status: "active",
404
+ rules: [{ attribute: "code", operator: "eq", values: [code] }],
405
+ applicationMethod: { type: "percentage", value: discount.percent, targetType: "order" }
406
+ };
407
+ }
408
+ function discountToPromotion(discount) {
409
+ if (discount.type === "buy_x_get_y") return null;
410
+ const rules = [];
411
+ const code = discount.code ? discount.code.trim().toUpperCase() : null;
412
+ if (discount.method !== "automatic" && code) {
413
+ rules.push({ attribute: "code", operator: "eq", values: [code] });
414
+ }
415
+ if (discount.minSubtotal != null) {
416
+ rules.push({ attribute: "cart.subtotal", operator: "gte", values: [discount.minSubtotal] });
417
+ }
418
+ let targetRules;
419
+ if (discount.appliesTo === "products" && discount.productIds.length > 0) {
420
+ targetRules = [{ attribute: "product.id", operator: "in", values: [...discount.productIds] }];
421
+ } else if (discount.appliesTo === "categories" && discount.categoryIds.length > 0) {
422
+ targetRules = [{ attribute: "product.category_id", operator: "in", values: [...discount.categoryIds] }];
423
+ }
424
+ let applicationMethod;
425
+ if (discount.type === "free_shipping") {
426
+ applicationMethod = { type: "percentage", value: 100, targetType: "shipping" };
427
+ } else {
428
+ applicationMethod = {
429
+ type: discount.type === "percentage" ? "percentage" : "fixed",
430
+ value: discount.value,
431
+ // A scoped legacy coupon discounts its eligible lines; an unscoped one
432
+ // is order-wide. Same split 0027 makes in shop_place_order.
433
+ targetType: targetRules ? "items" : "order",
434
+ ...targetRules ? { allocation: "across", targetRules } : {}
435
+ };
436
+ }
437
+ return {
438
+ id: `discount:${discount.id}`,
439
+ code,
440
+ isAutomatic: discount.method === "automatic",
441
+ type: "standard",
442
+ status: discount.status === "active" ? "active" : "inactive",
443
+ rules,
444
+ applicationMethod,
445
+ startsAt: discount.startsAt || null,
446
+ endsAt: discount.endsAt || null,
447
+ usageLimit: discount.usageLimit,
448
+ timesUsed: discount.timesUsed
449
+ };
450
+ }
451
+ function previewCouponPercent(promotions, code, cart = { items: [] }, options = {}) {
452
+ const normalized = code.trim().toUpperCase();
453
+ if (!normalized) return { valid: false, percent: 0, reason: "not_found" };
454
+ const promotion = promotions.find((p) => (p.code ?? "").trim().toUpperCase() === normalized);
455
+ if (!promotion) return { valid: false, percent: 0, reason: "not_found" };
456
+ const reason = promotionGateReason(promotion, { ...cart, codes: [normalized] }, options);
457
+ if (reason === "inactive") return { valid: false, percent: 0, reason: "not_found" };
458
+ if (reason !== null) return { valid: false, percent: 0, reason };
459
+ const method = promotion.applicationMethod;
460
+ if (method.type !== "percentage" || method.targetType !== "order" || method.buyRulesMinQuantity != null) {
461
+ return { valid: false, percent: 0, reason: "unsupported" };
462
+ }
463
+ return { valid: true, percent: method.value };
464
+ }
465
+ var ruleContextRegistry = new Registry("ruleContext");
466
+ function defineRuleContextProvider(id, provider, meta = {}) {
467
+ if (!id) throw new Error("defineRuleContextProvider: id is required");
468
+ if (!provider.attributes.length) {
469
+ throw new Error(`defineRuleContextProvider: "${id}" declares no attributes \u2014 a provider that contributes nothing cannot widen the vocabulary`);
470
+ }
471
+ const replacing = ruleContextRegistry.has(id);
472
+ for (const input of provider.attributes) {
473
+ registerRuleAttribute(
474
+ { ...input, path: input.path ?? `context.${input.key}` },
475
+ { meta: { source: "app", ...meta }, replace: replacing }
476
+ );
477
+ }
478
+ ruleContextRegistry.register(id, provider, {
479
+ description: `Rule context provider \u2014 contributes: ${provider.attributes.map((a) => a.key).join(", ")}`,
480
+ source: "app",
481
+ ...meta
482
+ });
483
+ }
484
+ function typeOk(value, fieldType) {
485
+ if (fieldType === "number") return typeof value === "number" && Number.isFinite(value);
486
+ if (fieldType === "boolean") return typeof value === "boolean";
487
+ return typeof value === "string";
488
+ }
489
+ function validateRuleContextResult(id, provider, result) {
490
+ const declared = new Map(provider.attributes.map((attribute) => [attribute.key, attribute]));
491
+ const validated = {};
492
+ for (const [key, value] of Object.entries(result)) {
493
+ const attribute = declared.get(key);
494
+ if (!attribute) {
495
+ throw new Error(
496
+ `Rule context provider "${id}" returned undeclared attribute "${key}". Declared attributes: ${[...declared.keys()].sort().join(", ")}`
497
+ );
498
+ }
499
+ if (value === null || value === void 0) continue;
500
+ const values = Array.isArray(value) ? value : [value];
501
+ for (const entry of values) {
502
+ if (!typeOk(entry, attribute.fieldType)) {
503
+ throw new Error(
504
+ `Rule context provider "${id}" returned ${JSON.stringify(entry)} for "${key}", which is declared as ${attribute.fieldType}`
505
+ );
506
+ }
507
+ }
508
+ validated[key] = value;
509
+ }
510
+ return validated;
511
+ }
512
+ async function buildRuleContext(cart) {
513
+ const context = {};
514
+ for (const entry of ruleContextRegistry.list()) {
515
+ const result = await entry.value.resolve(cart);
516
+ Object.assign(context, validateRuleContextResult(entry.id, entry.value, result));
517
+ }
518
+ return context;
519
+ }
520
+ function clearRuleContextProviders() {
521
+ ruleContextRegistry.clear();
522
+ }
523
+
524
+ export { APPLICATION_METHOD_ALLOCATIONS, APPLICATION_METHOD_TARGET_TYPES, APPLICATION_METHOD_TYPES, CORE_RULE_ATTRIBUTES, ITEM_PATH_PREFIX, PROMOTION_STATUSES, PROMOTION_TYPES, RULE_ATTRIBUTE_FIELD_TYPES, RULE_OPERATORS, actionsDiscountTotal, buildRuleContext, cartSubtotal, clearRuleAttributes, clearRuleContextProviders, computePromotionActions, defineRuleContextProvider, discountToPromotion, hasRuleAttribute, isItemScopedAttribute, listRuleAttributeKeys, listRuleAttributes, matchRuleValues, orderPromotions, previewCouponPercent, promotionGateReason, registerCoreRuleAttributes, registerRuleAttribute, resolveRuleAttribute, roundCents, ruleAttributeRegistry, ruleContextRegistry, rulesMatchCart, splitProportionally, storefrontDiscountToPromotion, validateRuleContextResult };
525
+ //# sourceMappingURL=chunk-3GERSY5F.js.map
526
+ //# sourceMappingURL=chunk-3GERSY5F.js.map