@fayz-ai/shop 0.9.0-next.1 → 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
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { MOCK_PRODUCTS, MOCK_CATEGORIES, MOCK_DISCOUNTS } from './chunk-
|
|
1
|
+
import { MOCK_PRODUCTS, MOCK_CATEGORIES, MOCK_DISCOUNTS, MOCK_VARIANTS } from './chunk-IKNGCRUM.js';
|
|
2
|
+
import { resolveEntityStore, createMockEntityStore, validateSidecarWrite } from './chunk-XOP6SHPB.js';
|
|
3
|
+
import { buildRuleContext, computePromotionActions, discountToPromotion } from './chunk-3GERSY5F.js';
|
|
4
|
+
import { resolveEntityDeclaration } from '@fayz-ai/core';
|
|
2
5
|
|
|
3
|
-
// src/mock-provider.ts
|
|
4
6
|
function uid() {
|
|
5
7
|
return Math.random().toString(36).slice(2);
|
|
6
8
|
}
|
|
@@ -30,8 +32,27 @@ function store(key, value) {
|
|
|
30
32
|
} catch {
|
|
31
33
|
}
|
|
32
34
|
}
|
|
35
|
+
var COLLECTIONS_KEY = "fayz.shop.mock.collections.v1";
|
|
36
|
+
var GIFT_CARDS_KEY = "fayz.shop.mock.gift-cards.v1";
|
|
37
|
+
var EMAIL_TEMPLATES_KEY = "fayz.shop.mock.email-templates.v1";
|
|
38
|
+
var CUSTOMER_GROUPS_KEY = "fayz.shop.mock.customer-groups.v1";
|
|
39
|
+
var CUSTOMER_SEGMENTS_KEY = "fayz.shop.mock.customer-segments.v1";
|
|
40
|
+
function mockGiftCardCode() {
|
|
41
|
+
const alphabet = "ABCDEFGHJKLMNPQRTUVWXY2346789";
|
|
42
|
+
const pick = () => alphabet[Math.floor(Math.random() * alphabet.length)];
|
|
43
|
+
const block = () => Array.from({ length: 4 }, pick).join("");
|
|
44
|
+
return `${block()}-${block()}-${block()}`;
|
|
45
|
+
}
|
|
46
|
+
function mockSlugify(name) {
|
|
47
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
48
|
+
}
|
|
33
49
|
var MockShopProvider = class {
|
|
34
50
|
constructor(seed) {
|
|
51
|
+
this.collections = loadStored(COLLECTIONS_KEY);
|
|
52
|
+
this.giftCards = loadStored(GIFT_CARDS_KEY);
|
|
53
|
+
this.emailTemplates = loadStored(EMAIL_TEMPLATES_KEY);
|
|
54
|
+
this.customerGroups = loadStored(CUSTOMER_GROUPS_KEY);
|
|
55
|
+
this.customerSegments = loadStored(CUSTOMER_SEGMENTS_KEY);
|
|
35
56
|
this.orders = loadStored(ORDERS_KEY);
|
|
36
57
|
this.customers = loadStored(CUSTOMERS_KEY);
|
|
37
58
|
this.enquiries = loadStored(ENQUIRIES_KEY);
|
|
@@ -39,9 +60,37 @@ var MockShopProvider = class {
|
|
|
39
60
|
const stored = loadStored(CHANNELS_KEY);
|
|
40
61
|
return stored.length ? stored : DEFAULT_CHANNELS.map((c) => ({ ...c }));
|
|
41
62
|
})();
|
|
63
|
+
this.entityStores = /* @__PURE__ */ new Map();
|
|
64
|
+
this.sidecars = /* @__PURE__ */ new Map();
|
|
42
65
|
this.products = (seed?.products ?? MOCK_PRODUCTS).map((p) => ({ ...p }));
|
|
43
66
|
this.categories = (seed?.categories ?? MOCK_CATEGORIES).map((c) => ({ ...c }));
|
|
44
67
|
this.discounts = (seed?.discounts ?? MOCK_DISCOUNTS).map((d) => ({ ...d }));
|
|
68
|
+
this.variantSets = Object.fromEntries(
|
|
69
|
+
Object.entries(seed?.variants ?? (seed?.products ? {} : MOCK_VARIANTS)).map(([productId, set]) => [
|
|
70
|
+
productId,
|
|
71
|
+
{ options: set.options.map((o) => ({ ...o, values: [...o.values] })), variants: set.variants.map((v) => ({ ...v, options: { ...v.options } })) }
|
|
72
|
+
])
|
|
73
|
+
);
|
|
74
|
+
this.promotions = (seed?.promotions ?? []).map((p) => ({ ...p }));
|
|
75
|
+
this.entitySeed = seed?.entities ?? {};
|
|
76
|
+
for (const [key, fields] of Object.entries(seed?.entityFields ?? {})) {
|
|
77
|
+
this.sidecars.set(key, { ...fields });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** The I5 indirection: a promoted (registered) store wins; otherwise the
|
|
81
|
+
* entity gets a lazily-built in-memory store seeded from the mock seed.
|
|
82
|
+
* Lazy on purpose — the seed is validated against the declaration, which
|
|
83
|
+
* the app may register after constructing the provider. */
|
|
84
|
+
entityStore(entity) {
|
|
85
|
+
resolveEntityDeclaration(entity);
|
|
86
|
+
return resolveEntityStore(entity, (key) => {
|
|
87
|
+
let store2 = this.entityStores.get(key);
|
|
88
|
+
if (!store2) {
|
|
89
|
+
store2 = createMockEntityStore(key, { seed: this.entitySeed[key] });
|
|
90
|
+
this.entityStores.set(key, store2);
|
|
91
|
+
}
|
|
92
|
+
return store2;
|
|
93
|
+
});
|
|
45
94
|
}
|
|
46
95
|
// Re-read before mutating: HMR can re-instantiate the provider mid-session
|
|
47
96
|
// and a stale in-memory copy must not clobber persisted records.
|
|
@@ -86,18 +135,24 @@ var MockShopProvider = class {
|
|
|
86
135
|
}
|
|
87
136
|
const orderBy = opts?.orderBy ?? "sort_order";
|
|
88
137
|
const dir = opts?.order === "desc" ? -1 : 1;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
138
|
+
if (opts?.search && !opts.orderBy) {
|
|
139
|
+
const q = opts.search.toLowerCase();
|
|
140
|
+
const rank = (p) => p.name.toLowerCase().startsWith(q) ? 0 : p.name.toLowerCase().includes(q) ? 1 : 2;
|
|
141
|
+
list.sort((a, b) => rank(a) - rank(b) || a.sortOrder - b.sortOrder);
|
|
142
|
+
} else {
|
|
143
|
+
list.sort((a, b) => {
|
|
144
|
+
switch (orderBy) {
|
|
145
|
+
case "name":
|
|
146
|
+
return a.name.localeCompare(b.name) * dir;
|
|
147
|
+
case "price":
|
|
148
|
+
return (a.price - b.price) * dir;
|
|
149
|
+
case "created_at":
|
|
150
|
+
return a.createdAt.localeCompare(b.createdAt) * dir;
|
|
151
|
+
default:
|
|
152
|
+
return (a.sortOrder - b.sortOrder) * dir;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
101
156
|
const offset = opts?.offset ?? 0;
|
|
102
157
|
if (offset || opts?.limit) list = list.slice(offset, opts?.limit ? offset + opts.limit : void 0);
|
|
103
158
|
return list;
|
|
@@ -105,6 +160,16 @@ var MockShopProvider = class {
|
|
|
105
160
|
async getProduct(id) {
|
|
106
161
|
return this.products.find((p) => p.id === id) ?? null;
|
|
107
162
|
}
|
|
163
|
+
/** Option axes + variants (spine M5) — active variants only, like the 0028
|
|
164
|
+
* storefront RLS. Answers empty sets for a variantless product. */
|
|
165
|
+
async getProductVariants(productId) {
|
|
166
|
+
const set = this.variantSets[productId];
|
|
167
|
+
if (!set) return { options: [], variants: [] };
|
|
168
|
+
return {
|
|
169
|
+
options: set.options.map((o) => ({ ...o, values: [...o.values] })).sort((a, b) => a.position - b.position),
|
|
170
|
+
variants: set.variants.filter((v) => v.status === "active").map((v) => ({ ...v, options: { ...v.options } })).sort((a, b) => a.position - b.position)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
108
173
|
async createProduct(input) {
|
|
109
174
|
const p = { id: uid(), tenantId: "mock", name: input.name, slug: input.slug ?? input.name.toLowerCase().replace(/\s+/g, "-"), description: input.description ?? null, price: input.price, compareAtPrice: input.compareAtPrice ?? null, currency: input.currency ?? "BRL", status: input.status ?? "active", inventoryCount: input.inventoryCount ?? 0, sku: input.sku ?? null, sortOrder: input.sortOrder ?? 0, metadata: input.metadata ?? {}, images: [], categoryId: input.categoryId ?? null, categoryName: null, isPhysical: input.isPhysical ?? true, weight: input.weight ?? null, weightUnit: input.weightUnit ?? "kg", createdAt: now(), updatedAt: now() };
|
|
110
175
|
this.products.push(p);
|
|
@@ -258,8 +323,9 @@ var MockShopProvider = class {
|
|
|
258
323
|
return this.orders[i];
|
|
259
324
|
}
|
|
260
325
|
// Trusted placement (mock parity with the shop_place_order RPC): re-reads
|
|
261
|
-
// prices + stock from the catalog,
|
|
262
|
-
//
|
|
326
|
+
// prices + stock from the catalog, EVALUATES THE DECLARED PROMOTIONS through
|
|
327
|
+
// the rule engine, validates the legacy discount code, decrements inventory.
|
|
328
|
+
// The browser-supplied prices and totals are never used.
|
|
263
329
|
async placeOrder(input) {
|
|
264
330
|
this.syncOrders();
|
|
265
331
|
this.syncCustomers();
|
|
@@ -279,22 +345,74 @@ var MockShopProvider = class {
|
|
|
279
345
|
}
|
|
280
346
|
const items = [];
|
|
281
347
|
const lineDetails = [];
|
|
348
|
+
const snapshotItems = [];
|
|
282
349
|
let subtotal = 0;
|
|
283
|
-
for (const line of input.items) {
|
|
350
|
+
for (const [index, line] of input.items.entries()) {
|
|
284
351
|
const product = this.products.find((p) => p.id === line.productId);
|
|
285
352
|
if (!product || product.status !== "active") throw new Error(`Product unavailable: ${line.productId}`);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
353
|
+
let variant;
|
|
354
|
+
if (line.variantId) {
|
|
355
|
+
variant = this.variantSets[line.productId]?.variants.find((v) => v.id === line.variantId && v.status === "active");
|
|
356
|
+
if (!variant) throw new Error(`Variant unavailable: ${line.variantId}`);
|
|
357
|
+
if (variant.inventoryCount < line.quantity) throw new Error(`Insufficient stock for ${product.name}`);
|
|
358
|
+
} else if (product.inventoryCount < line.quantity) {
|
|
359
|
+
throw new Error(`Insufficient stock for ${product.name}`);
|
|
360
|
+
}
|
|
361
|
+
const unitPrice = variant?.price ?? product.price;
|
|
362
|
+
const label = line.optionsLabel || (variant ? Object.entries(variant.options).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}: ${v}`).join(" \xB7 ") : "");
|
|
363
|
+
const name = label ? `${product.name} (${label})` : product.name;
|
|
364
|
+
const lineTotal = unitPrice * line.quantity;
|
|
289
365
|
subtotal += lineTotal;
|
|
290
366
|
lineDetails.push({ productId: product.id, categoryId: product.categoryId, lineTotal });
|
|
291
|
-
|
|
367
|
+
snapshotItems.push({
|
|
368
|
+
id: `${index}:${line.variantId ?? product.id}`,
|
|
369
|
+
productId: product.id,
|
|
370
|
+
quantity: line.quantity,
|
|
371
|
+
unitPrice,
|
|
372
|
+
categoryId: product.categoryId,
|
|
373
|
+
collectionId: typeof product.metadata?.collection === "string" ? product.metadata.collection : null
|
|
374
|
+
});
|
|
375
|
+
items.push({ productId: product.id, name, sku: variant?.sku ?? product.sku ?? void 0, quantity: line.quantity, unitPrice, imageUrl: product.images[0]?.url });
|
|
292
376
|
}
|
|
293
377
|
subtotal = Math.round(subtotal * 100) / 100;
|
|
294
378
|
let shippingTotal = Math.max(input.shippingTotal ?? 0, 0);
|
|
295
379
|
let discountTotal = 0;
|
|
296
380
|
let appliedCode = null;
|
|
297
|
-
|
|
381
|
+
const entered = input.discountCode ? [input.discountCode.trim()] : [];
|
|
382
|
+
const cart = {
|
|
383
|
+
items: snapshotItems,
|
|
384
|
+
shippingTotal,
|
|
385
|
+
customer: { email: input.customer?.email ?? null },
|
|
386
|
+
codes: entered,
|
|
387
|
+
currency: input.currency
|
|
388
|
+
};
|
|
389
|
+
const context = await buildRuleContext(cart);
|
|
390
|
+
const actions = computePromotionActions(cart, this.promotions, { context });
|
|
391
|
+
let promotionItemDiscount = 0;
|
|
392
|
+
let promotionShippingDiscount = 0;
|
|
393
|
+
const firedPromotionIds = /* @__PURE__ */ new Set();
|
|
394
|
+
const firedCodes = /* @__PURE__ */ new Set();
|
|
395
|
+
for (const action of actions) {
|
|
396
|
+
firedPromotionIds.add(action.promotionId);
|
|
397
|
+
if (action.code) firedCodes.add(action.code.trim().toUpperCase());
|
|
398
|
+
if (action.action === "addItemDiscount") promotionItemDiscount += action.amount;
|
|
399
|
+
else if (action.action === "addShippingDiscount") promotionShippingDiscount += action.amount;
|
|
400
|
+
else if (action.action === "addFreeItem") {
|
|
401
|
+
const granted = this.products.find((p) => p.id === action.productId);
|
|
402
|
+
if (granted) {
|
|
403
|
+
items.push({ productId: granted.id, name: granted.name, sku: granted.sku ?? void 0, quantity: action.quantity, unitPrice: 0, imageUrl: granted.images[0]?.url });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
for (const promotion of this.promotions) {
|
|
408
|
+
if (firedPromotionIds.has(promotion.id)) promotion.timesUsed = (promotion.timesUsed ?? 0) + 1;
|
|
409
|
+
}
|
|
410
|
+
discountTotal += Math.round(promotionItemDiscount * 100) / 100;
|
|
411
|
+
shippingTotal = Math.max(0, Math.round((shippingTotal - promotionShippingDiscount) * 100) / 100);
|
|
412
|
+
if (input.discountCode && firedCodes.has(input.discountCode.trim().toUpperCase())) {
|
|
413
|
+
appliedCode = input.discountCode.trim().toUpperCase();
|
|
414
|
+
}
|
|
415
|
+
if (input.discountCode && !appliedCode) {
|
|
298
416
|
const code = input.discountCode.trim().toLowerCase();
|
|
299
417
|
const d = this.discounts.find((x) => (x.code ?? "").toLowerCase() === code);
|
|
300
418
|
const ts = now();
|
|
@@ -309,8 +427,8 @@ var MockShopProvider = class {
|
|
|
309
427
|
eligible = lineDetails.filter((l) => l.categoryId && d.categoryIds.includes(l.categoryId)).reduce((s, l) => s + l.lineTotal, 0);
|
|
310
428
|
}
|
|
311
429
|
eligible = Math.round(eligible * 100) / 100;
|
|
312
|
-
if (d.type === "percentage") discountTotal
|
|
313
|
-
else if (d.type === "fixed_amount") discountTotal
|
|
430
|
+
if (d.type === "percentage") discountTotal += Math.round(eligible * d.value) / 100;
|
|
431
|
+
else if (d.type === "fixed_amount") discountTotal += Math.min(d.value, eligible);
|
|
314
432
|
else if (d.type === "free_shipping") shippingTotal = 0;
|
|
315
433
|
d.timesUsed += 1;
|
|
316
434
|
}
|
|
@@ -329,7 +447,14 @@ var MockShopProvider = class {
|
|
|
329
447
|
});
|
|
330
448
|
for (const line of input.items) {
|
|
331
449
|
const product = this.products.find((p) => p.id === line.productId);
|
|
332
|
-
if (product)
|
|
450
|
+
if (!product) continue;
|
|
451
|
+
const variant = line.variantId ? this.variantSets[line.productId]?.variants.find((v) => v.id === line.variantId) : void 0;
|
|
452
|
+
if (variant) {
|
|
453
|
+
variant.inventoryCount = Math.max(0, variant.inventoryCount - line.quantity);
|
|
454
|
+
product.inventoryCount = this.variantSets[line.productId].variants.filter((v) => v.status === "active").reduce((sum, v) => sum + v.inventoryCount, 0);
|
|
455
|
+
} else {
|
|
456
|
+
product.inventoryCount = Math.max(0, product.inventoryCount - line.quantity);
|
|
457
|
+
}
|
|
333
458
|
}
|
|
334
459
|
return order;
|
|
335
460
|
}
|
|
@@ -376,6 +501,20 @@ var MockShopProvider = class {
|
|
|
376
501
|
return this.createCustomer({ firstName: first || email, lastName: rest.join(" "), email });
|
|
377
502
|
}
|
|
378
503
|
// ---------------------------------------------------------------------------
|
|
504
|
+
// Promotions (spine M6) — the mock provider IS the reference evaluator, so
|
|
505
|
+
// tests and pilot fixtures exercise the rule engine without a database.
|
|
506
|
+
// Seeded promotions ride as-is; legacy discounts ride through the adapter
|
|
507
|
+
// (a percent code = one eligibility rule on code + percentage method).
|
|
508
|
+
// ---------------------------------------------------------------------------
|
|
509
|
+
async evaluatePromotions(input) {
|
|
510
|
+
const promotions = [
|
|
511
|
+
...this.promotions,
|
|
512
|
+
...this.discounts.map(discountToPromotion).filter((p) => p !== null)
|
|
513
|
+
];
|
|
514
|
+
const context = await buildRuleContext(input.cart);
|
|
515
|
+
return computePromotionActions(input.cart, promotions, { context });
|
|
516
|
+
}
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
379
518
|
// Discounts
|
|
380
519
|
// ---------------------------------------------------------------------------
|
|
381
520
|
async listDiscounts(opts) {
|
|
@@ -430,11 +569,240 @@ var MockShopProvider = class {
|
|
|
430
569
|
this.channels = this.channels.filter((c) => c.id !== id);
|
|
431
570
|
this.persistChannels();
|
|
432
571
|
}
|
|
572
|
+
// ---------------------------------------------------------------------------
|
|
573
|
+
// Custom entities (spine M4) — same EntityStore port as the live provider.
|
|
574
|
+
// ---------------------------------------------------------------------------
|
|
575
|
+
async listEntityRecords(entity, options) {
|
|
576
|
+
return this.entityStore(entity).list(options);
|
|
577
|
+
}
|
|
578
|
+
async getEntityRecord(entity, id) {
|
|
579
|
+
return this.entityStore(entity).get(id);
|
|
580
|
+
}
|
|
581
|
+
async getEntityRecordBySlug(entity, slug) {
|
|
582
|
+
return this.entityStore(entity).getBySlug(slug);
|
|
583
|
+
}
|
|
584
|
+
async createEntityRecord(entity, input) {
|
|
585
|
+
return this.entityStore(entity).create(input);
|
|
586
|
+
}
|
|
587
|
+
async updateEntityRecord(entity, id, input) {
|
|
588
|
+
return this.entityStore(entity).update(id, input);
|
|
589
|
+
}
|
|
590
|
+
async deleteEntityRecord(entity, id) {
|
|
591
|
+
return this.entityStore(entity).delete(id);
|
|
592
|
+
}
|
|
593
|
+
// Sidecar fields on core records — validated against defineSidecarFields.
|
|
594
|
+
async getEntityFields(reference, referenceId) {
|
|
595
|
+
return this.sidecars.get(`${reference}:${referenceId}`) ?? null;
|
|
596
|
+
}
|
|
597
|
+
async setEntityFields(reference, referenceId, fields) {
|
|
598
|
+
validateSidecarWrite(reference, fields);
|
|
599
|
+
const stored = { ...fields };
|
|
600
|
+
this.sidecars.set(`${reference}:${referenceId}`, stored);
|
|
601
|
+
return stored;
|
|
602
|
+
}
|
|
603
|
+
// Coleções, grupos/segmentos, vale-presente e e-mails — paridade com o
|
|
604
|
+
// provider Supabase. Mídia não entra: é storage, não tem mock honesto.
|
|
605
|
+
async listCollections() {
|
|
606
|
+
this.syncCollections();
|
|
607
|
+
return [...this.collections].sort((a, b) => a.sortOrder - b.sortOrder);
|
|
608
|
+
}
|
|
609
|
+
async getCollection(id) {
|
|
610
|
+
this.syncCollections();
|
|
611
|
+
return this.collections.find((c) => c.id === id) ?? null;
|
|
612
|
+
}
|
|
613
|
+
async createCollection(input) {
|
|
614
|
+
this.syncCollections();
|
|
615
|
+
const c = {
|
|
616
|
+
id: uid(),
|
|
617
|
+
tenantId: "mock",
|
|
618
|
+
title: input.title,
|
|
619
|
+
handle: input.handle?.trim() || mockSlugify(input.title),
|
|
620
|
+
description: input.description ?? null,
|
|
621
|
+
imageUrl: input.imageUrl ?? null,
|
|
622
|
+
seoTitle: input.seoTitle ?? null,
|
|
623
|
+
seoDescription: input.seoDescription ?? null,
|
|
624
|
+
published: input.published ?? true,
|
|
625
|
+
sortOrder: input.sortOrder ?? this.collections.length,
|
|
626
|
+
productIds: input.productIds ?? [],
|
|
627
|
+
createdAt: now(),
|
|
628
|
+
updatedAt: now()
|
|
629
|
+
};
|
|
630
|
+
this.collections.push(c);
|
|
631
|
+
this.persistCollections();
|
|
632
|
+
return c;
|
|
633
|
+
}
|
|
634
|
+
async updateCollection(id, input) {
|
|
635
|
+
this.syncCollections();
|
|
636
|
+
const i = this.collections.findIndex((c) => c.id === id);
|
|
637
|
+
if (i < 0) throw new Error("Collection not found");
|
|
638
|
+
this.collections[i] = { ...this.collections[i], ...input, updatedAt: now() };
|
|
639
|
+
this.persistCollections();
|
|
640
|
+
return this.collections[i];
|
|
641
|
+
}
|
|
642
|
+
async deleteCollection(id) {
|
|
643
|
+
this.syncCollections();
|
|
644
|
+
this.collections = this.collections.filter((c) => c.id !== id);
|
|
645
|
+
this.persistCollections();
|
|
646
|
+
}
|
|
647
|
+
async listCustomerGroups() {
|
|
648
|
+
const stored = loadStored(CUSTOMER_GROUPS_KEY);
|
|
649
|
+
if (stored.length) this.customerGroups = stored;
|
|
650
|
+
return [...this.customerGroups].sort((a, b) => a.name.localeCompare(b.name, "pt-BR"));
|
|
651
|
+
}
|
|
652
|
+
async createCustomerGroup(input) {
|
|
653
|
+
await this.listCustomerGroups();
|
|
654
|
+
const g = {
|
|
655
|
+
id: uid(),
|
|
656
|
+
tenantId: "mock",
|
|
657
|
+
name: input.name,
|
|
658
|
+
description: input.description ?? null,
|
|
659
|
+
color: input.color ?? null,
|
|
660
|
+
customerIds: input.customerIds ?? [],
|
|
661
|
+
createdAt: now(),
|
|
662
|
+
updatedAt: now()
|
|
663
|
+
};
|
|
664
|
+
this.customerGroups.push(g);
|
|
665
|
+
store(CUSTOMER_GROUPS_KEY, this.customerGroups);
|
|
666
|
+
return g;
|
|
667
|
+
}
|
|
668
|
+
async updateCustomerGroup(id, input) {
|
|
669
|
+
await this.listCustomerGroups();
|
|
670
|
+
const i = this.customerGroups.findIndex((g) => g.id === id);
|
|
671
|
+
if (i < 0) throw new Error("Customer group not found");
|
|
672
|
+
this.customerGroups[i] = { ...this.customerGroups[i], ...input, updatedAt: now() };
|
|
673
|
+
store(CUSTOMER_GROUPS_KEY, this.customerGroups);
|
|
674
|
+
return this.customerGroups[i];
|
|
675
|
+
}
|
|
676
|
+
async deleteCustomerGroup(id) {
|
|
677
|
+
await this.listCustomerGroups();
|
|
678
|
+
this.customerGroups = this.customerGroups.filter((g) => g.id !== id);
|
|
679
|
+
store(CUSTOMER_GROUPS_KEY, this.customerGroups);
|
|
680
|
+
}
|
|
681
|
+
async listCustomerSegments() {
|
|
682
|
+
const stored = loadStored(CUSTOMER_SEGMENTS_KEY);
|
|
683
|
+
if (stored.length) this.customerSegments = stored;
|
|
684
|
+
return [...this.customerSegments].sort((a, b) => a.name.localeCompare(b.name, "pt-BR"));
|
|
685
|
+
}
|
|
686
|
+
async createCustomerSegment(input) {
|
|
687
|
+
await this.listCustomerSegments();
|
|
688
|
+
const s = {
|
|
689
|
+
id: uid(),
|
|
690
|
+
tenantId: "mock",
|
|
691
|
+
name: input.name,
|
|
692
|
+
description: input.description ?? null,
|
|
693
|
+
minOrders: input.minOrders ?? null,
|
|
694
|
+
maxOrders: input.maxOrders ?? null,
|
|
695
|
+
minSpent: input.minSpent ?? null,
|
|
696
|
+
maxSpent: input.maxSpent ?? null,
|
|
697
|
+
joinedAfter: input.joinedAfter ?? null,
|
|
698
|
+
joinedBefore: input.joinedBefore ?? null,
|
|
699
|
+
createdAt: now(),
|
|
700
|
+
updatedAt: now()
|
|
701
|
+
};
|
|
702
|
+
this.customerSegments.push(s);
|
|
703
|
+
store(CUSTOMER_SEGMENTS_KEY, this.customerSegments);
|
|
704
|
+
return s;
|
|
705
|
+
}
|
|
706
|
+
async updateCustomerSegment(id, input) {
|
|
707
|
+
await this.listCustomerSegments();
|
|
708
|
+
const i = this.customerSegments.findIndex((s) => s.id === id);
|
|
709
|
+
if (i < 0) throw new Error("Customer segment not found");
|
|
710
|
+
this.customerSegments[i] = { ...this.customerSegments[i], ...input, updatedAt: now() };
|
|
711
|
+
store(CUSTOMER_SEGMENTS_KEY, this.customerSegments);
|
|
712
|
+
return this.customerSegments[i];
|
|
713
|
+
}
|
|
714
|
+
async deleteCustomerSegment(id) {
|
|
715
|
+
await this.listCustomerSegments();
|
|
716
|
+
this.customerSegments = this.customerSegments.filter((s) => s.id !== id);
|
|
717
|
+
store(CUSTOMER_SEGMENTS_KEY, this.customerSegments);
|
|
718
|
+
}
|
|
719
|
+
async listGiftCards() {
|
|
720
|
+
this.syncGiftCards();
|
|
721
|
+
return [...this.giftCards].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
722
|
+
}
|
|
723
|
+
async createGiftCard(input) {
|
|
724
|
+
this.syncGiftCards();
|
|
725
|
+
const amount = Number(input.initialBalance) || 0;
|
|
726
|
+
const g = {
|
|
727
|
+
id: uid(),
|
|
728
|
+
tenantId: "mock",
|
|
729
|
+
code: (input.code?.trim() || mockGiftCardCode()).toUpperCase(),
|
|
730
|
+
// Same rule the supabase provider enforces: a new card is worth its face
|
|
731
|
+
// value, and only redemption moves the remaining balance.
|
|
732
|
+
initialBalance: amount,
|
|
733
|
+
balance: amount,
|
|
734
|
+
currency: input.currency ?? "BRL",
|
|
735
|
+
status: input.status ?? "active",
|
|
736
|
+
expiresAt: input.expiresAt ?? null,
|
|
737
|
+
customerId: input.customerId ?? null,
|
|
738
|
+
customerEmail: input.customerEmail ?? null,
|
|
739
|
+
note: input.note ?? null,
|
|
740
|
+
createdAt: now(),
|
|
741
|
+
updatedAt: now()
|
|
742
|
+
};
|
|
743
|
+
this.giftCards.push(g);
|
|
744
|
+
this.persistGiftCards();
|
|
745
|
+
return g;
|
|
746
|
+
}
|
|
747
|
+
async updateGiftCard(id, input) {
|
|
748
|
+
this.syncGiftCards();
|
|
749
|
+
const i = this.giftCards.findIndex((g) => g.id === id);
|
|
750
|
+
if (i < 0) throw new Error("Gift card not found");
|
|
751
|
+
const code = input.code !== void 0 ? input.code.trim().toUpperCase() : this.giftCards[i].code;
|
|
752
|
+
this.giftCards[i] = { ...this.giftCards[i], ...input, code, updatedAt: now() };
|
|
753
|
+
this.persistGiftCards();
|
|
754
|
+
return this.giftCards[i];
|
|
755
|
+
}
|
|
756
|
+
async deleteGiftCard(id) {
|
|
757
|
+
this.syncGiftCards();
|
|
758
|
+
this.giftCards = this.giftCards.filter((g) => g.id !== id);
|
|
759
|
+
this.persistGiftCards();
|
|
760
|
+
}
|
|
761
|
+
async listEmailTemplates() {
|
|
762
|
+
this.syncEmailTemplates();
|
|
763
|
+
return [...this.emailTemplates];
|
|
764
|
+
}
|
|
765
|
+
async upsertEmailTemplate(input) {
|
|
766
|
+
this.syncEmailTemplates();
|
|
767
|
+
const i = this.emailTemplates.findIndex((t) => t.event === input.event);
|
|
768
|
+
const base = i >= 0 ? this.emailTemplates[i] : {
|
|
769
|
+
id: uid(),
|
|
770
|
+
tenantId: "mock",
|
|
771
|
+
event: input.event,
|
|
772
|
+
enabled: false,
|
|
773
|
+
subject: null,
|
|
774
|
+
body: null,
|
|
775
|
+
updatedAt: now()
|
|
776
|
+
};
|
|
777
|
+
const next = { ...base, ...input, updatedAt: now() };
|
|
778
|
+
if (i >= 0) this.emailTemplates[i] = next;
|
|
779
|
+
else this.emailTemplates.push(next);
|
|
780
|
+
store(EMAIL_TEMPLATES_KEY, this.emailTemplates);
|
|
781
|
+
return next;
|
|
782
|
+
}
|
|
783
|
+
syncCollections() {
|
|
784
|
+
const stored = loadStored(COLLECTIONS_KEY);
|
|
785
|
+
if (stored.length) this.collections = stored;
|
|
786
|
+
}
|
|
787
|
+
persistCollections() {
|
|
788
|
+
store(COLLECTIONS_KEY, this.collections);
|
|
789
|
+
}
|
|
790
|
+
syncGiftCards() {
|
|
791
|
+
const stored = loadStored(GIFT_CARDS_KEY);
|
|
792
|
+
if (stored.length) this.giftCards = stored;
|
|
793
|
+
}
|
|
794
|
+
persistGiftCards() {
|
|
795
|
+
store(GIFT_CARDS_KEY, this.giftCards);
|
|
796
|
+
}
|
|
797
|
+
syncEmailTemplates() {
|
|
798
|
+
const stored = loadStored(EMAIL_TEMPLATES_KEY);
|
|
799
|
+
if (stored.length) this.emailTemplates = stored;
|
|
800
|
+
}
|
|
433
801
|
};
|
|
434
802
|
function createMockShopProvider(seed) {
|
|
435
803
|
return new MockShopProvider(seed);
|
|
436
804
|
}
|
|
437
805
|
|
|
438
806
|
export { MockShopProvider, createMockShopProvider };
|
|
439
|
-
//# sourceMappingURL=chunk-
|
|
440
|
-
//# sourceMappingURL=chunk-
|
|
807
|
+
//# sourceMappingURL=chunk-U3O2I3RM.js.map
|
|
808
|
+
//# sourceMappingURL=chunk-U3O2I3RM.js.map
|