@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.
- package/README.md +18 -1
- package/dist/admin.d.ts +2 -0
- package/dist/admin.d.ts.map +1 -0
- package/dist/catalog.d.ts +1 -3
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +2 -2
- package/dist/chunk-3GERSY5F.js +526 -0
- package/dist/chunk-3GERSY5F.js.map +1 -0
- package/dist/chunk-FQPGE3YN.js +3 -0
- package/dist/{chunk-JR25TISO.js.map → chunk-FQPGE3YN.js.map} +1 -1
- package/dist/{chunk-CTKZ6O2J.js → chunk-IKNGCRUM.js} +41 -15
- package/dist/chunk-IKNGCRUM.js.map +1 -0
- package/dist/{chunk-IYQHKMLB.js → chunk-IL6GA4ER.js} +4 -4
- package/dist/chunk-IL6GA4ER.js.map +1 -0
- package/dist/chunk-KLDS7IPU.js +17 -0
- package/dist/chunk-KLDS7IPU.js.map +1 -0
- package/dist/chunk-RGTIJQJL.js +93 -0
- package/dist/chunk-RGTIJQJL.js.map +1 -0
- package/dist/{chunk-SVTCJ6HP.js → chunk-U3O2I3RM.js} +395 -27
- package/dist/chunk-U3O2I3RM.js.map +1 -0
- package/dist/chunk-XERLK5LD.js +1415 -0
- package/dist/chunk-XERLK5LD.js.map +1 -0
- package/dist/chunk-XOP6SHPB.js +142 -0
- package/dist/chunk-XOP6SHPB.js.map +1 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/{mock-catalog.d.ts → data/mock-catalog.d.ts} +21 -1
- package/dist/data/mock-catalog.d.ts.map +1 -0
- package/dist/data/mock-provider.d.ts +133 -0
- package/dist/data/mock-provider.d.ts.map +1 -0
- package/dist/data/mock.d.ts +3 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/provider.d.ts +142 -0
- package/dist/data/provider.d.ts.map +1 -0
- package/dist/data/supabase-provider.d.ts +100 -0
- package/dist/data/supabase-provider.d.ts.map +1 -0
- package/dist/data/tables.d.ts +35 -0
- package/dist/data/tables.d.ts.map +1 -0
- package/dist/data/tenant.d.ts.map +1 -0
- package/dist/entities/entity-reconcile.d.ts +32 -0
- package/dist/entities/entity-reconcile.d.ts.map +1 -0
- package/dist/entities/entity-sql.d.ts +50 -0
- package/dist/entities/entity-sql.d.ts.map +1 -0
- package/dist/entities/entity-store.d.ts +58 -0
- package/dist/entities/entity-store.d.ts.map +1 -0
- package/dist/entities/index.d.ts +11 -0
- package/dist/entities/index.d.ts.map +1 -0
- package/dist/entities/mock-entity-store.d.ts +17 -0
- package/dist/entities/mock-entity-store.d.ts.map +1 -0
- package/dist/entities/sidecar.d.ts +9 -0
- package/dist/entities/sidecar.d.ts.map +1 -0
- package/dist/entities/supabase-entity-store.d.ts +8 -0
- package/dist/entities/supabase-entity-store.d.ts.map +1 -0
- package/dist/entities.js +159 -0
- package/dist/entities.js.map +1 -0
- package/dist/index.d.ts +17 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +728 -41
- package/dist/index.js.map +1 -1
- package/dist/inventory/allocation.d.ts +98 -0
- package/dist/inventory/allocation.d.ts.map +1 -0
- package/dist/inventory/index.d.ts +13 -0
- package/dist/inventory/index.d.ts.map +1 -0
- package/dist/inventory/mock-stock-store.d.ts +89 -0
- package/dist/inventory/mock-stock-store.d.ts.map +1 -0
- package/dist/inventory/postgrest-double.d.ts +65 -0
- package/dist/inventory/postgrest-double.d.ts.map +1 -0
- package/dist/inventory/shopify-mapping.d.ts +67 -0
- package/dist/inventory/shopify-mapping.d.ts.map +1 -0
- package/dist/inventory/stock-contract.d.ts +46 -0
- package/dist/inventory/stock-contract.d.ts.map +1 -0
- package/dist/inventory/stock-store.d.ts +96 -0
- package/dist/inventory/stock-store.d.ts.map +1 -0
- package/dist/inventory/stock-types.d.ts +292 -0
- package/dist/inventory/stock-types.d.ts.map +1 -0
- package/dist/inventory/supabase-stock-store.d.ts +59 -0
- package/dist/inventory/supabase-stock-store.d.ts.map +1 -0
- package/dist/inventory/vocabulary.d.ts +86 -0
- package/dist/inventory/vocabulary.d.ts.map +1 -0
- package/dist/inventory.js +4 -0
- package/dist/inventory.js.map +1 -0
- package/dist/lib/catalog.d.ts +4 -0
- package/dist/lib/catalog.d.ts.map +1 -0
- package/dist/{commerce.d.ts → lib/commerce.d.ts} +2 -2
- package/dist/lib/commerce.d.ts.map +1 -0
- package/dist/lib/emails.d.ts +50 -0
- package/dist/lib/emails.d.ts.map +1 -0
- package/dist/lib/message-events.d.ts +4 -0
- package/dist/lib/message-events.d.ts.map +1 -0
- package/dist/lib/runtime.d.ts +5 -0
- package/dist/lib/runtime.d.ts.map +1 -0
- package/dist/mock.d.ts +1 -2
- package/dist/mock.d.ts.map +1 -1
- package/dist/mock.js +4 -2
- package/dist/provider.d.ts +1 -88
- package/dist/provider.d.ts.map +1 -1
- package/dist/rules/adapter.d.ts +36 -0
- package/dist/rules/adapter.d.ts.map +1 -0
- package/dist/rules/attributes.d.ts +64 -0
- package/dist/rules/attributes.d.ts.map +1 -0
- package/dist/rules/context.d.ts +43 -0
- package/dist/rules/context.d.ts.map +1 -0
- package/dist/rules/evaluator.d.ts +37 -0
- package/dist/rules/evaluator.d.ts.map +1 -0
- package/dist/rules/index.d.ts +13 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/prices.d.ts +25 -0
- package/dist/rules/prices.d.ts.map +1 -0
- package/dist/rules/rule-types.d.ts +160 -0
- package/dist/rules/rule-types.d.ts.map +1 -0
- package/dist/rules.js +22 -0
- package/dist/rules.js.map +1 -0
- package/dist/runtime.d.ts +1 -4
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +5 -3
- package/dist/types.d.ts +231 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +26 -20
- package/dist/chunk-CTKZ6O2J.js.map +0 -1
- package/dist/chunk-IYQHKMLB.js.map +0 -1
- package/dist/chunk-JR25TISO.js +0 -3
- package/dist/chunk-SVTCJ6HP.js.map +0 -1
- package/dist/commerce.d.ts.map +0 -1
- package/dist/mock-catalog.d.ts.map +0 -1
- package/dist/mock-provider.d.ts +0 -60
- package/dist/mock-provider.d.ts.map +0 -1
- package/dist/supabase-provider.d.ts +0 -56
- package/dist/supabase-provider.d.ts.map +0 -1
- package/dist/tables.d.ts +0 -12
- package/dist/tables.d.ts.map +0 -1
- package/dist/tenant.d.ts.map +0 -1
- package/src/catalog.ts +0 -3
- package/src/commerce.ts +0 -120
- package/src/index.ts +0 -40
- package/src/mock-catalog.ts +0 -266
- package/src/mock-provider.ts +0 -480
- package/src/mock.ts +0 -2
- package/src/provider.ts +0 -117
- package/src/runtime.ts +0 -20
- package/src/supabase-provider.ts +0 -754
- package/src/tables.ts +0 -17
- package/src/tenant.ts +0 -21
- package/src/types.ts +0 -623
- /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).
|
package/dist/admin.d.ts
ADDED
|
@@ -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
|
|
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
|
package/dist/catalog.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"
|
|
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-
|
|
2
|
-
export { MOCK_CATEGORIES, MOCK_DISCOUNTS, MOCK_PRODUCTS, buildMockCatalog, svgPlaceholder } from './chunk-
|
|
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
|