@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.
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
@@ -1,6 +1,8 @@
1
- import { MOCK_PRODUCTS, MOCK_CATEGORIES, MOCK_DISCOUNTS } from './chunk-CTKZ6O2J.js';
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
- list.sort((a, b) => {
90
- switch (orderBy) {
91
- case "name":
92
- return a.name.localeCompare(b.name) * dir;
93
- case "price":
94
- return (a.price - b.price) * dir;
95
- case "created_at":
96
- return a.createdAt.localeCompare(b.createdAt) * dir;
97
- default:
98
- return (a.sortOrder - b.sortOrder) * dir;
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, validates the discount, decrements
262
- // inventory. The browser-supplied prices are never used.
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
- if (product.inventoryCount < line.quantity) throw new Error(`Insufficient stock for ${product.name}`);
287
- const name = line.optionsLabel ? `${product.name} (${line.optionsLabel})` : product.name;
288
- const lineTotal = product.price * line.quantity;
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
- items.push({ productId: product.id, name, sku: product.sku ?? void 0, quantity: line.quantity, unitPrice: product.price, imageUrl: product.images[0]?.url });
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
- if (input.discountCode) {
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 = Math.round(eligible * d.value) / 100;
313
- else if (d.type === "fixed_amount") discountTotal = Math.min(d.value, eligible);
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) product.inventoryCount = Math.max(0, product.inventoryCount - line.quantity);
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-SVTCJ6HP.js.map
440
- //# sourceMappingURL=chunk-SVTCJ6HP.js.map
807
+ //# sourceMappingURL=chunk-U3O2I3RM.js.map
808
+ //# sourceMappingURL=chunk-U3O2I3RM.js.map