@fayz-ai/shop 0.9.0 → 0.11.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 (163) 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-IYQHKMLB.js → chunk-2WACO2TG.js} +4 -4
  8. package/dist/chunk-2WACO2TG.js.map +1 -0
  9. package/dist/chunk-3GERSY5F.js +526 -0
  10. package/dist/chunk-3GERSY5F.js.map +1 -0
  11. package/dist/chunk-6BO2TMZ5.js +93 -0
  12. package/dist/chunk-6BO2TMZ5.js.map +1 -0
  13. package/dist/chunk-FQPGE3YN.js +3 -0
  14. package/dist/{chunk-JR25TISO.js.map → chunk-FQPGE3YN.js.map} +1 -1
  15. package/dist/{chunk-CTKZ6O2J.js → chunk-IKNGCRUM.js} +41 -15
  16. package/dist/chunk-IKNGCRUM.js.map +1 -0
  17. package/dist/chunk-KLDS7IPU.js +17 -0
  18. package/dist/chunk-KLDS7IPU.js.map +1 -0
  19. package/dist/chunk-NWW3WJ7Y.js +136 -0
  20. package/dist/chunk-NWW3WJ7Y.js.map +1 -0
  21. package/dist/chunk-STRNMFC4.js +1438 -0
  22. package/dist/chunk-STRNMFC4.js.map +1 -0
  23. package/dist/chunk-U6HV3LVP.js +1170 -0
  24. package/dist/chunk-U6HV3LVP.js.map +1 -0
  25. package/dist/client.d.ts +2 -0
  26. package/dist/client.d.ts.map +1 -0
  27. package/dist/data/customer-search.d.ts +3 -0
  28. package/dist/data/customer-search.d.ts.map +1 -0
  29. package/dist/data/functions.d.ts +9 -0
  30. package/dist/data/functions.d.ts.map +1 -0
  31. package/dist/data/media.d.ts +37 -0
  32. package/dist/data/media.d.ts.map +1 -0
  33. package/dist/{mock-catalog.d.ts → data/mock-catalog.d.ts} +21 -1
  34. package/dist/data/mock-catalog.d.ts.map +1 -0
  35. package/dist/data/mock-provider.d.ts +165 -0
  36. package/dist/data/mock-provider.d.ts.map +1 -0
  37. package/dist/data/mock.d.ts +3 -0
  38. package/dist/data/mock.d.ts.map +1 -0
  39. package/dist/data/provider.d.ts +335 -0
  40. package/dist/data/provider.d.ts.map +1 -0
  41. package/dist/data/supabase-provider.d.ts +208 -0
  42. package/dist/data/supabase-provider.d.ts.map +1 -0
  43. package/dist/data/tables.d.ts +38 -0
  44. package/dist/data/tables.d.ts.map +1 -0
  45. package/dist/data/tenant.d.ts.map +1 -0
  46. package/dist/entities/entity-reconcile.d.ts +32 -0
  47. package/dist/entities/entity-reconcile.d.ts.map +1 -0
  48. package/dist/entities/entity-sql.d.ts +50 -0
  49. package/dist/entities/entity-sql.d.ts.map +1 -0
  50. package/dist/entities/entity-store.d.ts +58 -0
  51. package/dist/entities/entity-store.d.ts.map +1 -0
  52. package/dist/entities/index.d.ts +10 -0
  53. package/dist/entities/index.d.ts.map +1 -0
  54. package/dist/entities/mock-entity-store.d.ts +17 -0
  55. package/dist/entities/mock-entity-store.d.ts.map +1 -0
  56. package/dist/entities/supabase-entity-store.d.ts +8 -0
  57. package/dist/entities/supabase-entity-store.d.ts.map +1 -0
  58. package/dist/entities.js +159 -0
  59. package/dist/entities.js.map +1 -0
  60. package/dist/index.d.ts +28 -12
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +1558 -40
  63. package/dist/index.js.map +1 -1
  64. package/dist/inventory/allocation.d.ts +98 -0
  65. package/dist/inventory/allocation.d.ts.map +1 -0
  66. package/dist/inventory/index.d.ts +13 -0
  67. package/dist/inventory/index.d.ts.map +1 -0
  68. package/dist/inventory/mock-stock-store.d.ts +89 -0
  69. package/dist/inventory/mock-stock-store.d.ts.map +1 -0
  70. package/dist/inventory/postgrest-double.d.ts +65 -0
  71. package/dist/inventory/postgrest-double.d.ts.map +1 -0
  72. package/dist/inventory/shopify-mapping.d.ts +67 -0
  73. package/dist/inventory/shopify-mapping.d.ts.map +1 -0
  74. package/dist/inventory/stock-contract.d.ts +46 -0
  75. package/dist/inventory/stock-contract.d.ts.map +1 -0
  76. package/dist/inventory/stock-store.d.ts +96 -0
  77. package/dist/inventory/stock-store.d.ts.map +1 -0
  78. package/dist/inventory/stock-types.d.ts +351 -0
  79. package/dist/inventory/stock-types.d.ts.map +1 -0
  80. package/dist/inventory/supabase-stock-store.d.ts +60 -0
  81. package/dist/inventory/supabase-stock-store.d.ts.map +1 -0
  82. package/dist/inventory/vocabulary.d.ts +86 -0
  83. package/dist/inventory/vocabulary.d.ts.map +1 -0
  84. package/dist/inventory.js +4 -0
  85. package/dist/inventory.js.map +1 -0
  86. package/dist/lib/catalog.d.ts +4 -0
  87. package/dist/lib/catalog.d.ts.map +1 -0
  88. package/dist/{commerce.d.ts → lib/commerce.d.ts} +2 -2
  89. package/dist/lib/commerce.d.ts.map +1 -0
  90. package/dist/lib/document.d.ts +13 -0
  91. package/dist/lib/document.d.ts.map +1 -0
  92. package/dist/lib/emails.d.ts +50 -0
  93. package/dist/lib/emails.d.ts.map +1 -0
  94. package/dist/lib/message-events.d.ts +4 -0
  95. package/dist/lib/message-events.d.ts.map +1 -0
  96. package/dist/lib/runtime.d.ts +5 -0
  97. package/dist/lib/runtime.d.ts.map +1 -0
  98. package/dist/lib/variant-matrix.d.ts +120 -0
  99. package/dist/lib/variant-matrix.d.ts.map +1 -0
  100. package/dist/mock.d.ts +1 -2
  101. package/dist/mock.d.ts.map +1 -1
  102. package/dist/mock.js +4 -2
  103. package/dist/provider.d.ts +1 -88
  104. package/dist/provider.d.ts.map +1 -1
  105. package/dist/providers/checkout.d.ts +129 -0
  106. package/dist/providers/checkout.d.ts.map +1 -0
  107. package/dist/providers/index.d.ts +5 -0
  108. package/dist/providers/index.d.ts.map +1 -0
  109. package/dist/providers/shipping.d.ts +43 -0
  110. package/dist/providers/shipping.d.ts.map +1 -0
  111. package/dist/providers.js +70 -0
  112. package/dist/providers.js.map +1 -0
  113. package/dist/rules/adapter.d.ts +36 -0
  114. package/dist/rules/adapter.d.ts.map +1 -0
  115. package/dist/rules/attributes.d.ts +64 -0
  116. package/dist/rules/attributes.d.ts.map +1 -0
  117. package/dist/rules/context.d.ts +43 -0
  118. package/dist/rules/context.d.ts.map +1 -0
  119. package/dist/rules/evaluator.d.ts +37 -0
  120. package/dist/rules/evaluator.d.ts.map +1 -0
  121. package/dist/rules/index.d.ts +13 -0
  122. package/dist/rules/index.d.ts.map +1 -0
  123. package/dist/rules/prices.d.ts +25 -0
  124. package/dist/rules/prices.d.ts.map +1 -0
  125. package/dist/rules/rule-types.d.ts +160 -0
  126. package/dist/rules/rule-types.d.ts.map +1 -0
  127. package/dist/rules.js +22 -0
  128. package/dist/rules.js.map +1 -0
  129. package/dist/runtime.d.ts +1 -4
  130. package/dist/runtime.d.ts.map +1 -1
  131. package/dist/runtime.js +5 -3
  132. package/dist/shipping/label-provider.d.ts +43 -0
  133. package/dist/shipping/label-provider.d.ts.map +1 -0
  134. package/dist/types.d.ts +688 -0
  135. package/dist/types.d.ts.map +1 -1
  136. package/package.json +30 -20
  137. package/dist/chunk-CTKZ6O2J.js.map +0 -1
  138. package/dist/chunk-IYQHKMLB.js.map +0 -1
  139. package/dist/chunk-JR25TISO.js +0 -3
  140. package/dist/chunk-SVTCJ6HP.js +0 -440
  141. package/dist/chunk-SVTCJ6HP.js.map +0 -1
  142. package/dist/commerce.d.ts.map +0 -1
  143. package/dist/mock-catalog.d.ts.map +0 -1
  144. package/dist/mock-provider.d.ts +0 -60
  145. package/dist/mock-provider.d.ts.map +0 -1
  146. package/dist/supabase-provider.d.ts +0 -56
  147. package/dist/supabase-provider.d.ts.map +0 -1
  148. package/dist/tables.d.ts +0 -12
  149. package/dist/tables.d.ts.map +0 -1
  150. package/dist/tenant.d.ts.map +0 -1
  151. package/src/catalog.ts +0 -3
  152. package/src/commerce.ts +0 -120
  153. package/src/index.ts +0 -40
  154. package/src/mock-catalog.ts +0 -266
  155. package/src/mock-provider.ts +0 -480
  156. package/src/mock.ts +0 -2
  157. package/src/provider.ts +0 -117
  158. package/src/runtime.ts +0 -20
  159. package/src/supabase-provider.ts +0 -754
  160. package/src/tables.ts +0 -17
  161. package/src/tenant.ts +0 -21
  162. package/src/types.ts +0 -623
  163. /package/dist/{tenant.d.ts → data/tenant.d.ts} +0 -0
package/dist/index.js CHANGED
@@ -1,17 +1,28 @@
1
- import { getShopProvider } from './chunk-IYQHKMLB.js';
2
- export { getShopProvider, resetShopProvider, setShopProvider } from './chunk-IYQHKMLB.js';
3
- import './chunk-JR25TISO.js';
4
- export { MockShopProvider, createMockShopProvider } from './chunk-SVTCJ6HP.js';
5
- export { MOCK_CATEGORIES, MOCK_DISCOUNTS, MOCK_PRODUCTS, buildMockCatalog, svgPlaceholder } from './chunk-CTKZ6O2J.js';
1
+ import { T, resolveStockStore, createSupabaseStockStore, OPENING_BALANCE_REASON, rowToStockLocation } from './chunk-STRNMFC4.js';
2
+ export { DEFAULT_STOCK_TRACKING, MockStockStore, OWN_FULFILLMENT, STOCK_TRACKING_MODES, StockMovementError, SupabaseStockStore, createMockStockStore, createSupabaseStockStore, listStockMovementKinds, listStockReasonCodes, mirroredInventoryCount, registerStockMovementKind, registerStockReasonCode, registerStockStore, replayStockLedger, resolveStockStore, validateStockMovement } from './chunk-STRNMFC4.js';
3
+ import { getShopProvider } from './chunk-2WACO2TG.js';
4
+ export { getShopProvider, resetShopProvider, setShopProvider } from './chunk-2WACO2TG.js';
5
+ import './chunk-FQPGE3YN.js';
6
+ import { normalizeVariantAxes, validateVariantAxes, variantOptionsKey, storagePathOf, mergeMediaFiles, productImageToMediaFile, displayNameOf, mediaKindOf } from './chunk-U6HV3LVP.js';
7
+ export { MAX_VARIANT_AXES, MAX_VARIANT_COMBINATIONS, MockShopProvider, applyVariantMatrix, axesFromOptions, createMockShopProvider, displayNameOf, formatVariantCombination, hasOnlyDefaultVariant, leavesNothingSellable, mediaKindOf, mergeMediaFiles, normalizeVariantAxes, planVariantMatrix, productImageToMediaFile, storagePathOf, validateVariantAxes, variantCombinations, variantOptionsKey } from './chunk-U6HV3LVP.js';
8
+ export { MOCK_CATEGORIES, MOCK_DISCOUNTS, MOCK_PRODUCTS, buildMockCatalog, svgPlaceholder } from './chunk-IKNGCRUM.js';
9
+ import { createSupabaseEntityStore } from './chunk-6BO2TMZ5.js';
10
+ import { getShopTenantId } from './chunk-KLDS7IPU.js';
11
+ export { getShopTenantId, setShopTenantResolver } from './chunk-KLDS7IPU.js';
12
+ import { resolveEntityStore } from './chunk-NWW3WJ7Y.js';
13
+ import './chunk-3GERSY5F.js';
6
14
  import { getSupabaseClientOptional } from '@fayz-ai/core';
7
15
 
8
- // src/commerce.ts
16
+ // src/lib/commerce.ts
9
17
  function round2(n) {
10
18
  return Math.round(n * 100) / 100;
11
19
  }
12
20
  async function validateDiscount(input, provider = getShopProvider()) {
13
21
  const code = input.code.trim();
14
22
  if (!code) return { valid: false, code: null, type: null, value: 0, reason: "empty" };
23
+ if (typeof provider.validateDiscountCode === "function") {
24
+ return await provider.validateDiscountCode(code, input.subtotal);
25
+ }
15
26
  const list = await provider.listDiscounts({ status: "active" });
16
27
  const discount = list.find((d) => (d.code ?? "").toUpperCase() === code.toUpperCase());
17
28
  if (!discount) return { valid: false, code: null, type: null, value: 0, reason: "not_found" };
@@ -65,7 +76,7 @@ async function computeCartTotals(input, provider = getShopProvider()) {
65
76
  let discountCode = null;
66
77
  if (input.discountCode) {
67
78
  const applied = applyDiscount(
68
- await validateDiscount({ code: input.discountCode, customerId: input.customerId }, provider),
79
+ await validateDiscount({ code: input.discountCode, subtotal, customerId: input.customerId }, provider),
69
80
  subtotal
70
81
  );
71
82
  discountTotal = applied.discountTotal;
@@ -80,37 +91,51 @@ async function getCustomerOrders(customerId, provider = getShopProvider()) {
80
91
  return provider.listOrders({ customerId, limit: 100 });
81
92
  }
82
93
 
83
- // src/tenant.ts
84
- var _resolver = null;
85
- function setShopTenantResolver(resolver) {
86
- _resolver = resolver;
94
+ // src/data/functions.ts
95
+ var _functionsUrl = null;
96
+ function setShopFunctionsUrl(url) {
97
+ _functionsUrl = url ? url.replace(/\/+$/u, "") : null;
98
+ }
99
+ function getShopFunctionsUrl() {
100
+ return _functionsUrl;
87
101
  }
88
- function getShopTenantId() {
89
- if (!_resolver) return void 0;
102
+ async function callShopFunction(db, slug, body) {
103
+ const override = getShopFunctionsUrl();
104
+ if (override) {
105
+ let token = null;
106
+ try {
107
+ token = (await db?.auth?.getSession?.())?.data?.session?.access_token ?? null;
108
+ } catch {
109
+ }
110
+ const response = await fetch(`${override}/${slug}`, {
111
+ method: "POST",
112
+ headers: {
113
+ "Content-Type": "application/json",
114
+ ...token ? { Authorization: `Bearer ${token}` } : {}
115
+ },
116
+ body: JSON.stringify(body)
117
+ });
118
+ const payload2 = await response.json().catch(() => null);
119
+ return { ok: response.ok, status: response.status, payload: payload2 };
120
+ }
121
+ const { data, error } = await db.functions.invoke(slug, { body });
122
+ if (!error) return { ok: true, status: 200, payload: data };
123
+ const status = Number(error?.context?.status ?? 502);
124
+ let payload = null;
90
125
  try {
91
- return _resolver();
126
+ payload = await error?.context?.json?.();
92
127
  } catch {
93
- return void 0;
94
128
  }
129
+ return { ok: false, status, payload: payload ?? { error: error.message } };
95
130
  }
96
131
 
97
- // src/tables.ts
98
- var T = {
99
- products: "plg_shop_products",
100
- productImages: "plg_shop_product_images",
101
- categories: "plg_shop_categories",
102
- orders: "plg_shop_orders",
103
- orderItems: "plg_shop_order_items",
104
- customers: "plg_shop_customers",
105
- discounts: "plg_shop_discounts",
106
- salesChannels: "plg_shop_channels",
107
- // Core, not plg_shop_*: a delivery zone is a fact about the business, and
108
- // fulfilment and financial read it too. Same reasoning as addresses and
109
- // orders — see migration 0021.
110
- shippingZones: "shipping_zones"
111
- };
132
+ // src/data/customer-search.ts
133
+ var COLUMNS = ["first_name", "last_name", "email", "phone"];
134
+ function customerSearchFilters(term) {
135
+ return String(term ?? "").trim().split(/\s+/u).map((word) => word.replace(/[%,()]/gu, "")).filter(Boolean).map((word) => COLUMNS.map((column) => `${column}.ilike.%${word}%`).join(","));
136
+ }
112
137
 
113
- // src/supabase-provider.ts
138
+ // src/data/supabase-provider.ts
114
139
  function getDb() {
115
140
  const supabase = getSupabaseClientOptional();
116
141
  if (!supabase) throw new Error("@fayz-ai/shop: Supabase client not initialized. Call setGlobalSupabaseClient() first.");
@@ -128,6 +153,95 @@ function toPostalDigits(value) {
128
153
  const digits = String(value ?? "").replace(/\D/g, "");
129
154
  return digits ? digits.padStart(8, "0").slice(0, 8) : void 0;
130
155
  }
156
+ function rowToVariant(row) {
157
+ return {
158
+ id: row.id,
159
+ productId: row.product_id,
160
+ options: row.options && typeof row.options === "object" ? row.options : {},
161
+ sku: row.sku ?? null,
162
+ // numeric(12,2) arrives as a string on the wire; Number() once, here.
163
+ price: row.price == null ? null : Number(row.price),
164
+ compareAtPrice: row.compare_at_price == null ? null : Number(row.compare_at_price),
165
+ inventoryCount: row.inventory_count ?? 0,
166
+ position: row.position ?? 0,
167
+ status: row.status ?? "active",
168
+ imageId: row.image_id ?? null,
169
+ isDefaultVariant: row.is_default_variant ?? false,
170
+ // Null inherits the product's, the way `price` has since 0028.
171
+ weight: numOrNull(row.weight),
172
+ weightUnit: row.weight_unit ?? null,
173
+ heightCm: numOrNull(row.height_cm),
174
+ widthCm: numOrNull(row.width_cm),
175
+ lengthCm: numOrNull(row.length_cm)
176
+ };
177
+ }
178
+ function variantPatch(input) {
179
+ const patch = {};
180
+ const map = {
181
+ sku: "sku",
182
+ price: "price",
183
+ compareAtPrice: "compare_at_price",
184
+ position: "position",
185
+ status: "status",
186
+ imageId: "image_id",
187
+ weight: "weight",
188
+ weightUnit: "weight_unit",
189
+ heightCm: "height_cm",
190
+ widthCm: "width_cm",
191
+ lengthCm: "length_cm"
192
+ };
193
+ for (const [key, column] of Object.entries(map)) {
194
+ const value = input[key];
195
+ if (value === void 0) continue;
196
+ patch[column] = key === "sku" && typeof value === "string" && value.trim() === "" ? null : value;
197
+ }
198
+ return patch;
199
+ }
200
+ function rowToStoreSettings(row) {
201
+ const bool = (v) => v == null ? void 0 : Boolean(v);
202
+ const methods = Array.isArray(row.payment_methods) && row.payment_methods.length > 0 ? row.payment_methods : void 0;
203
+ const fields = row.checkout_fields && typeof row.checkout_fields === "object" && Object.keys(row.checkout_fields).length > 0 ? row.checkout_fields : void 0;
204
+ return {
205
+ currency: row.currency ?? void 0,
206
+ locale: row.locale ?? void 0,
207
+ showOutOfStock: bool(row.show_out_of_stock),
208
+ requireSku: bool(row.require_sku),
209
+ stockOwnership: row.stock_ownership ?? void 0,
210
+ paymentMethods: methods,
211
+ paymentProvider: row.payment_provider ?? void 0,
212
+ guestCheckout: bool(row.guest_checkout),
213
+ requireConfirmedEmail: bool(row.require_confirmed_email),
214
+ marketingOptIn: bool(row.marketing_opt_in),
215
+ discountsEnabled: bool(row.discounts_enabled),
216
+ checkoutFields: fields,
217
+ autoFulfill: row.auto_fulfill ?? void 0,
218
+ notifyNewOrder: bool(row.notify_new_order),
219
+ notifyLowStock: bool(row.notify_low_stock),
220
+ handlingNote: row.handling_note ?? void 0,
221
+ carrierQuotes: bool(row.carrier_quotes)
222
+ };
223
+ }
224
+ function storeSettingsPayload(input) {
225
+ const payload = {};
226
+ if (input.currency !== void 0) payload.currency = input.currency;
227
+ if (input.locale !== void 0) payload.locale = input.locale;
228
+ if (input.showOutOfStock !== void 0) payload.show_out_of_stock = input.showOutOfStock;
229
+ if (input.requireSku !== void 0) payload.require_sku = input.requireSku;
230
+ if (input.stockOwnership !== void 0) payload.stock_ownership = input.stockOwnership;
231
+ if (input.paymentMethods !== void 0) payload.payment_methods = input.paymentMethods;
232
+ if (input.paymentProvider !== void 0) payload.payment_provider = input.paymentProvider ?? null;
233
+ if (input.guestCheckout !== void 0) payload.guest_checkout = input.guestCheckout;
234
+ if (input.requireConfirmedEmail !== void 0) payload.require_confirmed_email = input.requireConfirmedEmail;
235
+ if (input.marketingOptIn !== void 0) payload.marketing_opt_in = input.marketingOptIn;
236
+ if (input.discountsEnabled !== void 0) payload.discounts_enabled = input.discountsEnabled;
237
+ if (input.checkoutFields !== void 0) payload.checkout_fields = input.checkoutFields ?? {};
238
+ if (input.autoFulfill !== void 0) payload.auto_fulfill = input.autoFulfill;
239
+ if (input.notifyNewOrder !== void 0) payload.notify_new_order = input.notifyNewOrder;
240
+ if (input.notifyLowStock !== void 0) payload.notify_low_stock = input.notifyLowStock;
241
+ if (input.handlingNote !== void 0) payload.handling_note = input.handlingNote || null;
242
+ if (input.carrierQuotes !== void 0) payload.carrier_quotes = input.carrierQuotes;
243
+ return payload;
244
+ }
131
245
  function rowToShippingZone(row) {
132
246
  return {
133
247
  id: row.id,
@@ -141,9 +255,74 @@ function rowToShippingZone(row) {
141
255
  etaMinDays: row.eta_min_days ?? null,
142
256
  etaMaxDays: row.eta_max_days ?? null,
143
257
  active: row.active ?? true,
144
- sortOrder: row.sort_order ?? 0
258
+ sortOrder: row.sort_order ?? 0,
259
+ excluded: row.excluded ?? false,
260
+ profileId: row.profile_id ?? null
261
+ };
262
+ }
263
+ function rowToShippingProfile(row) {
264
+ return {
265
+ id: row.id,
266
+ tenantId: row.tenant_id,
267
+ name: row.name,
268
+ // undefined, never false: NULL means "inherit the store's decision", and a
269
+ // mapper that coalesced would turn every new profile into one that refuses
270
+ // the gateway.
271
+ carrierQuotes: row.carrier_quotes == null ? void 0 : Boolean(row.carrier_quotes),
272
+ isDefault: row.is_default ?? false
145
273
  };
146
274
  }
275
+ function shippingProfilePayload(input) {
276
+ const payload = {};
277
+ if (input.name !== void 0) payload.name = input.name;
278
+ if (input.carrierQuotes !== void 0) payload.carrier_quotes = input.carrierQuotes ?? null;
279
+ if (input.isDefault !== void 0) payload.is_default = input.isDefault;
280
+ return payload;
281
+ }
282
+ function toOriginPostalCode(raw) {
283
+ const digits = String(raw ?? "").replace(/\D/g, "");
284
+ if (!digits) return null;
285
+ if (digits.length !== 8) throw new Error("O CEP do local precisa ter 8 d\xEDgitos.");
286
+ return digits;
287
+ }
288
+ function toLocationCode(name) {
289
+ const slug = name.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
290
+ return slug || "local";
291
+ }
292
+ function locationWriteError(error) {
293
+ if (error?.code === "23505") return new Error("J\xE1 existe um local com esse nome.");
294
+ if (error?.code === "23503") {
295
+ return new Error(
296
+ "Este local j\xE1 tem movimenta\xE7\xF5es de estoque. Arquive-o em vez de excluir \u2014 o hist\xF3rico aponta para ele."
297
+ );
298
+ }
299
+ return new Error(error?.message ?? "N\xE3o foi poss\xEDvel salvar o local.");
300
+ }
301
+ function stockLocationPayload(input) {
302
+ const payload = {};
303
+ if (input.name !== void 0) payload.name = input.name;
304
+ if (input.code !== void 0) payload.code = input.code;
305
+ if (input.kind !== void 0) payload.kind = input.kind;
306
+ if (input.isSellable !== void 0) payload.is_sellable = input.isSellable;
307
+ if (input.isDefault !== void 0) payload.is_default = input.isDefault;
308
+ if (input.status !== void 0) payload.status = input.status;
309
+ if (input.postalCode !== void 0) payload.postal_code = toOriginPostalCode(input.postalCode);
310
+ if (input.city !== void 0) payload.city = input.city || null;
311
+ if (input.state !== void 0) payload.state = input.state || null;
312
+ if (input.street !== void 0) payload.street = input.street || null;
313
+ if (input.number !== void 0) payload.number = input.number || null;
314
+ if (input.district !== void 0) payload.district = input.district || null;
315
+ if (input.complement !== void 0) payload.complement = input.complement || null;
316
+ if (input.document !== void 0) payload.document = String(input.document ?? "").replace(/\D/gu, "") || null;
317
+ if (input.phone !== void 0) payload.phone = String(input.phone ?? "").replace(/\D/gu, "") || null;
318
+ if (input.email !== void 0) payload.email = input.email || null;
319
+ if (input.carrierAgencies !== void 0) payload.carrier_agencies = input.carrierAgencies ?? {};
320
+ if (input.shipsOnline !== void 0) payload.ships_online = input.shipsOnline;
321
+ if (input.fulfillmentProvider !== void 0) {
322
+ payload.fulfillment_provider = input.fulfillmentProvider || null;
323
+ }
324
+ return payload;
325
+ }
147
326
  function shippingZonePayload(input) {
148
327
  const payload = {};
149
328
  if (input.name !== void 0) payload.name = input.name;
@@ -156,6 +335,34 @@ function shippingZonePayload(input) {
156
335
  if (input.etaMaxDays !== void 0) payload.eta_max_days = input.etaMaxDays ?? null;
157
336
  if (input.active !== void 0) payload.active = input.active;
158
337
  if (input.sortOrder !== void 0) payload.sort_order = input.sortOrder;
338
+ if (input.excluded !== void 0) payload.excluded = input.excluded;
339
+ if (input.profileId !== void 0) payload.profile_id = input.profileId ?? null;
340
+ return payload;
341
+ }
342
+ function rowToShippingPackage(row) {
343
+ return {
344
+ id: row.id,
345
+ tenantId: row.tenant_id,
346
+ name: row.name,
347
+ kind: row.kind ?? "box",
348
+ heightCm: Number(row.height_cm ?? 0),
349
+ widthCm: Number(row.width_cm ?? 0),
350
+ lengthCm: Number(row.length_cm ?? 0),
351
+ emptyWeightGrams: Number(row.empty_weight_grams ?? 0),
352
+ isDefault: row.is_default ?? false,
353
+ active: row.active ?? true
354
+ };
355
+ }
356
+ function shippingPackagePayload(input) {
357
+ const payload = {};
358
+ if (input.name !== void 0) payload.name = input.name;
359
+ if (input.kind !== void 0) payload.kind = input.kind;
360
+ if (input.heightCm !== void 0) payload.height_cm = input.heightCm;
361
+ if (input.widthCm !== void 0) payload.width_cm = input.widthCm;
362
+ if (input.lengthCm !== void 0) payload.length_cm = input.lengthCm;
363
+ if (input.emptyWeightGrams !== void 0) payload.empty_weight_grams = input.emptyWeightGrams;
364
+ if (input.isDefault !== void 0) payload.is_default = input.isDefault;
365
+ if (input.active !== void 0) payload.active = input.active;
159
366
  return payload;
160
367
  }
161
368
  function rowToProduct(row) {
@@ -179,10 +386,21 @@ function rowToProduct(row) {
179
386
  isPhysical: row.is_physical ?? true,
180
387
  weight: row.weight,
181
388
  weightUnit: row.weight_unit ?? "kg",
389
+ heightCm: numOrNull(row.height_cm),
390
+ widthCm: numOrNull(row.width_cm),
391
+ lengthCm: numOrNull(row.length_cm),
392
+ packageId: row.package_id ?? null,
393
+ handling: Array.isArray(row.handling) ? row.handling : [],
394
+ shippingProfileId: row.shipping_profile_id ?? null,
182
395
  createdAt: row.created_at,
183
396
  updatedAt: row.updated_at
184
397
  };
185
398
  }
399
+ function numOrNull(value) {
400
+ if (value == null || value === "") return null;
401
+ const n = Number(value);
402
+ return Number.isFinite(n) ? n : null;
403
+ }
186
404
  function rowToImage(row) {
187
405
  return {
188
406
  id: row.id,
@@ -254,6 +472,7 @@ function rowToCustomer(row) {
254
472
  lastName: row.last_name ?? "",
255
473
  email: row.email,
256
474
  phone: row.phone,
475
+ document: row.document ?? null,
257
476
  notes: row.notes,
258
477
  ordersCount: row.orders_count ?? 0,
259
478
  totalSpent: row.total_spent ?? 0,
@@ -297,6 +516,113 @@ function rowToSalesChannel(row) {
297
516
  updatedAt: row.updated_at
298
517
  };
299
518
  }
519
+ function segmentPayload(input) {
520
+ const p = {};
521
+ if (input.name !== void 0) p.name = input.name;
522
+ if (input.description !== void 0) p.description = input.description;
523
+ if (input.minOrders !== void 0) p.min_orders = nullableNumber(input.minOrders);
524
+ if (input.maxOrders !== void 0) p.max_orders = nullableNumber(input.maxOrders);
525
+ if (input.minSpent !== void 0) p.min_spent = nullableNumber(input.minSpent);
526
+ if (input.maxSpent !== void 0) p.max_spent = nullableNumber(input.maxSpent);
527
+ if (input.joinedAfter !== void 0) p.joined_after = input.joinedAfter || null;
528
+ if (input.joinedBefore !== void 0) p.joined_before = input.joinedBefore || null;
529
+ return p;
530
+ }
531
+ var GIFT_CODE_ALPHABET = "ABCDEFGHJKLMNPQRTUVWXY2346789";
532
+ function generateGiftCardCode() {
533
+ const bytes = new Uint8Array(12);
534
+ globalThis.crypto.getRandomValues(bytes);
535
+ const chars = Array.from(bytes, (b) => GIFT_CODE_ALPHABET[b % GIFT_CODE_ALPHABET.length]);
536
+ return `${chars.slice(0, 4).join("")}-${chars.slice(4, 8).join("")}-${chars.slice(8, 12).join("")}`;
537
+ }
538
+ var LIBRARY_FOLDER = "library";
539
+ function nullableNumber(value) {
540
+ if (value === null || value === void 0 || value === "") return null;
541
+ const n = Number(value);
542
+ return Number.isFinite(n) ? n : null;
543
+ }
544
+ function rowToCollection(row) {
545
+ const members = row.members ?? [];
546
+ return {
547
+ id: row.id,
548
+ tenantId: row.tenant_id,
549
+ title: row.title,
550
+ handle: row.handle ?? slugify(row.title ?? ""),
551
+ description: row.description ?? null,
552
+ imageUrl: row.image_url ?? null,
553
+ seoTitle: row.seo_title ?? null,
554
+ seoDescription: row.seo_description ?? null,
555
+ published: row.published ?? true,
556
+ sortOrder: row.sort_order ?? 0,
557
+ productIds: [...members].sort((a, b) => (a.position ?? 0) - (b.position ?? 0)).map((m) => m.product_id),
558
+ createdAt: row.created_at,
559
+ updatedAt: row.updated_at ?? row.created_at
560
+ };
561
+ }
562
+ function rowToCustomerGroup(row) {
563
+ const members = row.members ?? [];
564
+ return {
565
+ id: row.id,
566
+ tenantId: row.tenant_id,
567
+ name: row.name,
568
+ description: row.description ?? null,
569
+ color: row.color ?? null,
570
+ customerIds: members.map((m) => m.customer_id),
571
+ createdAt: row.created_at,
572
+ updatedAt: row.updated_at ?? row.created_at
573
+ };
574
+ }
575
+ function rowToCustomerSegment(row) {
576
+ return {
577
+ id: row.id,
578
+ tenantId: row.tenant_id,
579
+ name: row.name,
580
+ description: row.description ?? null,
581
+ minOrders: nullableNumber(row.min_orders),
582
+ maxOrders: nullableNumber(row.max_orders),
583
+ minSpent: nullableNumber(row.min_spent),
584
+ maxSpent: nullableNumber(row.max_spent),
585
+ joinedAfter: row.joined_after ?? null,
586
+ joinedBefore: row.joined_before ?? null,
587
+ createdAt: row.created_at,
588
+ updatedAt: row.updated_at ?? row.created_at
589
+ };
590
+ }
591
+ var SHOP_EVENT_PREFIX = "shop.";
592
+ var EMAIL_CHANNEL = "email";
593
+ function rowToEmailTemplate(row) {
594
+ return {
595
+ id: row.id,
596
+ tenantId: row.tenant_id,
597
+ event: String(row.event_key ?? "").slice(SHOP_EVENT_PREFIX.length),
598
+ // NULL stays NULL: the catalog resolves the default (resolveEmailTemplates),
599
+ // not this mapping. Collapsing to `false` here would silently disable every
600
+ // template whose row exists only because its text was edited.
601
+ enabled: row.enabled ?? null,
602
+ // Empty string and NULL both mean "no override" — a subject the merchant
603
+ // cleared should fall back to the default, not send a blank subject line.
604
+ subject: row.subject || null,
605
+ body: row.body || null,
606
+ updatedAt: row.updated_at ?? row.created_at
607
+ };
608
+ }
609
+ function rowToGiftCard(row) {
610
+ return {
611
+ id: row.id,
612
+ tenantId: row.tenant_id,
613
+ code: String(row.code ?? "").toUpperCase(),
614
+ initialBalance: Number(row.initial_balance ?? 0),
615
+ balance: Number(row.balance ?? 0),
616
+ currency: row.currency ?? "BRL",
617
+ status: row.status ?? "active",
618
+ expiresAt: row.expires_at ?? null,
619
+ customerId: row.customer_id ?? null,
620
+ customerEmail: row.customer_email ?? null,
621
+ note: row.note ?? null,
622
+ createdAt: row.created_at,
623
+ updatedAt: row.updated_at ?? row.created_at
624
+ };
625
+ }
300
626
  var SupabaseShopProvider = class {
301
627
  // ---------------------------------------------------------------------------
302
628
  // Products
@@ -323,6 +649,123 @@ var SupabaseShopProvider = class {
323
649
  if (error) throw error;
324
650
  return data ? rowToProduct(data) : null;
325
651
  }
652
+ /** Option axes + variants from the 0028 tables (spine M5). The storefront
653
+ * RLS on both tables already restricts anon reads to active variants of
654
+ * publicly readable products; the status filter here keeps the admin path
655
+ * (authenticated tenant member, who CAN see archived rows) consistent. */
656
+ async getProductVariants(productId, opts) {
657
+ const db = getDb();
658
+ let variantQuery = db.from(T.productVariants).select("*").eq("product_id", productId);
659
+ if (!opts?.includeArchived) variantQuery = variantQuery.eq("status", "active");
660
+ const [optionsRes, variantsRes] = await Promise.all([
661
+ db.from(T.productOptions).select("*").eq("product_id", productId).order("position", { ascending: true }),
662
+ variantQuery.order("position", { ascending: true })
663
+ ]);
664
+ if (optionsRes.error) throw optionsRes.error;
665
+ if (variantsRes.error) throw variantsRes.error;
666
+ const options = (optionsRes.data ?? []).map((row) => ({
667
+ id: row.id,
668
+ name: row.name,
669
+ values: Array.isArray(row.values) ? row.values.map(String) : [],
670
+ position: row.position ?? 0
671
+ }));
672
+ const variants = (variantsRes.data ?? []).map(rowToVariant);
673
+ return { options, variants };
674
+ }
675
+ // --- The variant admin ------------------------------------------------------
676
+ async saveProductOptions(productId, input) {
677
+ const db = getDb();
678
+ const tenantId = getTenantId();
679
+ const clean = normalizeVariantAxes(input);
680
+ const refusal = validateVariantAxes(clean);
681
+ if (refusal) throw new Error(refusal);
682
+ const { data: existing, error: readError } = await db.from(T.productOptions).select("*").eq("product_id", productId);
683
+ if (readError) throw readError;
684
+ const byName = new Map((existing ?? []).map((row) => [row.name, row]));
685
+ const wanted = new Set(clean.map((axis) => axis.name));
686
+ const stale = (existing ?? []).filter((row) => !wanted.has(row.name)).map((row) => row.id);
687
+ if (stale.length > 0) {
688
+ const { error: error2 } = await db.from(T.productOptions).delete().in("id", stale);
689
+ if (error2) throw error2;
690
+ }
691
+ for (const [position, axis] of clean.entries()) {
692
+ const row = byName.get(axis.name);
693
+ if (row) {
694
+ const { error: error2 } = await db.from(T.productOptions).update({ values: axis.values, position }).eq("id", row.id);
695
+ if (error2) throw error2;
696
+ } else {
697
+ const { error: error2 } = await db.from(T.productOptions).insert({
698
+ product_id: productId,
699
+ tenant_id: tenantId,
700
+ name: axis.name,
701
+ values: axis.values,
702
+ position
703
+ });
704
+ if (error2) throw error2;
705
+ }
706
+ }
707
+ const { data, error } = await db.from(T.productOptions).select("*").eq("product_id", productId).order("position", { ascending: true });
708
+ if (error) throw error;
709
+ return (data ?? []).map((row) => ({
710
+ id: row.id,
711
+ name: row.name,
712
+ values: Array.isArray(row.values) ? row.values.map(String) : [],
713
+ position: row.position ?? 0
714
+ }));
715
+ }
716
+ async createProductVariant(productId, input) {
717
+ const db = getDb();
718
+ const tenantId = getTenantId();
719
+ if (Object.keys(input.options).length === 0) {
720
+ throw new Error("Uma variante sem op\xE7\xF5es \xE9 a variante padr\xE3o (0037), criada pelo banco. Declare ao menos um eixo.");
721
+ }
722
+ const { data: existing, error: readError } = await db.from(T.productVariants).select("*").eq("product_id", productId);
723
+ if (readError) throw readError;
724
+ const wantedKey = variantOptionsKey(input.options);
725
+ const match = (existing ?? []).find((row) => variantOptionsKey(row.options ?? {}) === wantedKey);
726
+ if (match) return this.updateProductVariant(match.id, { ...input, status: input.status ?? "active" });
727
+ const { data, error } = await db.from(T.productVariants).insert({
728
+ product_id: productId,
729
+ tenant_id: tenantId,
730
+ options: input.options,
731
+ ...variantPatch(input),
732
+ // NOT copied from the product: 0028 defines a null price as "inherit the
733
+ // product's", and 0037's header spells out why a copy is a bug — the
734
+ // storefront would show the new product price and the server charge the
735
+ // frozen variant one.
736
+ status: input.status ?? "active"
737
+ }).select("*").single();
738
+ if (error) throw error;
739
+ return rowToVariant(data);
740
+ }
741
+ async updateProductVariant(variantId, input) {
742
+ const db = getDb();
743
+ const { data, error } = await db.from(T.productVariants).update(variantPatch(input)).eq("id", variantId).select("*").single();
744
+ if (error) throw error;
745
+ return rowToVariant(data);
746
+ }
747
+ async archiveProductVariant(variantId) {
748
+ const db = getDb();
749
+ const { error } = await db.from(T.productVariants).update({ status: "archived" }).eq("id", variantId);
750
+ if (error) throw error;
751
+ }
752
+ async setVariantStock(variantId, count, note) {
753
+ const target = Math.max(0, Math.trunc(count));
754
+ const store = resolveStockStore(getTenantId() ?? "", () => createSupabaseStockStore());
755
+ const current = await store.getAvailable(variantId);
756
+ const delta = target - current;
757
+ if (delta === 0) return current;
758
+ const priorMovements = await store.listMovements({ variantId, limit: 1 });
759
+ const opening = priorMovements.length === 0 && delta > 0;
760
+ await store.recordMovement({
761
+ variantId,
762
+ kind: opening ? "receipt" : "adjustment",
763
+ reasonCode: opening ? OPENING_BALANCE_REASON : "correction",
764
+ quantity: delta,
765
+ note: note ?? "Estoque definido na matriz de variantes."
766
+ });
767
+ return target;
768
+ }
326
769
  async createProduct(input) {
327
770
  const db = getDb();
328
771
  const tenantId = getTenantId();
@@ -342,7 +785,17 @@ var SupabaseShopProvider = class {
342
785
  category_id: input.categoryId ?? null,
343
786
  is_physical: input.isPhysical ?? true,
344
787
  weight: input.weight ?? null,
345
- weight_unit: input.weightUnit ?? "kg"
788
+ weight_unit: input.weightUnit ?? "kg",
789
+ height_cm: input.heightCm ?? null,
790
+ width_cm: input.widthCm ?? null,
791
+ length_cm: input.lengthCm ?? null,
792
+ // Null = a padrão da loja (0047). É o estado normal, não um campo vazio.
793
+ package_id: input.packageId ?? null,
794
+ // NULL and not []: "nothing declared" and "declared as needing nothing"
795
+ // are the same fact about a box, and one representation of it keeps the
796
+ // `&&` overlap test in the 0048 trigger reading one way.
797
+ handling: input.handling?.length ? input.handling : null,
798
+ shipping_profile_id: input.shippingProfileId ?? null
346
799
  }).select(`*, images:${T.productImages}(*)`).single();
347
800
  if (error) throw error;
348
801
  return rowToProduct(data);
@@ -365,6 +818,13 @@ var SupabaseShopProvider = class {
365
818
  if (input.categoryId !== void 0) updates.category_id = input.categoryId;
366
819
  if (input.isPhysical !== void 0) updates.is_physical = input.isPhysical;
367
820
  if (input.weight !== void 0) updates.weight = input.weight;
821
+ if (input.weightUnit !== void 0) updates.weight_unit = input.weightUnit;
822
+ if (input.heightCm !== void 0) updates.height_cm = input.heightCm;
823
+ if (input.widthCm !== void 0) updates.width_cm = input.widthCm;
824
+ if (input.lengthCm !== void 0) updates.length_cm = input.lengthCm;
825
+ if (input.packageId !== void 0) updates.package_id = input.packageId || null;
826
+ if (input.handling !== void 0) updates.handling = input.handling.length ? input.handling : null;
827
+ if (input.shippingProfileId !== void 0) updates.shipping_profile_id = input.shippingProfileId ?? null;
368
828
  const { data, error } = await db.from(T.products).update(updates).eq("id", id).select(`*, images:${T.productImages}(*)`).single();
369
829
  if (error) throw error;
370
830
  return rowToProduct(data);
@@ -409,9 +869,43 @@ var SupabaseShopProvider = class {
409
869
  if (error) throw error;
410
870
  return rowToImage(data);
411
871
  }
872
+ async replaceProductImage(imageId, file) {
873
+ const db = getDb();
874
+ const tenantId = getTenantId();
875
+ const { data: row, error: readErr } = await db.from(T.productImages).select("id,url,product_id").eq("id", imageId).single();
876
+ if (readErr) throw readErr;
877
+ const path = `${tenantId}/products/${row.product_id}/${Date.now()}-${file.name}`;
878
+ const bucket = db.storage.from(T.mediaBucket);
879
+ const { error: uploadErr } = await bucket.upload(path, file);
880
+ if (uploadErr) throw uploadErr;
881
+ const { data: { publicUrl } } = bucket.getPublicUrl(path);
882
+ const { data, error } = await db.from(T.productImages).update({ url: publicUrl }).eq("id", imageId).select().single();
883
+ if (error) throw error;
884
+ await this.removeUnreferencedObject(row.url);
885
+ return rowToImage(data);
886
+ }
412
887
  async deleteProductImage(imageId) {
413
- const { error } = await getDb().from(T.productImages).delete().eq("id", imageId);
888
+ const db = getDb();
889
+ const { data: row } = await db.from(T.productImages).select("url").eq("id", imageId).maybeSingle();
890
+ const { error } = await db.from(T.productImages).delete().eq("id", imageId);
414
891
  if (error) throw error;
892
+ if (row?.url) await this.removeUnreferencedObject(row.url);
893
+ }
894
+ /**
895
+ * Drop a stored file once nothing points at it anymore.
896
+ *
897
+ * The row was the only thing keeping the object reachable, so leaving it
898
+ * behind is a file nobody can see and nobody can delete — and the bill for it
899
+ * arrives all the same. Two rows can share one URL (a duplicated product), and
900
+ * the survivor has to keep working, so the count decides.
901
+ */
902
+ async removeUnreferencedObject(url) {
903
+ const path = url ? storagePathOf(url) : null;
904
+ if (!path) return;
905
+ const db = getDb();
906
+ const { count } = await db.from(T.productImages).select("id", { count: "exact", head: true }).eq("url", url);
907
+ if (count) return;
908
+ await db.storage.from(T.mediaBucket).remove([path]);
415
909
  }
416
910
  // ---------------------------------------------------------------------------
417
911
  // Categories
@@ -520,10 +1014,188 @@ var SupabaseShopProvider = class {
520
1014
  if (error) throw error;
521
1015
  return rowToShippingZone(data);
522
1016
  }
1017
+ // -------------------------------------------------------------------------
1018
+ // Embalagens (0046)
1019
+ //
1020
+ // `is_default` has a partial unique index behind it, so promoting a box has
1021
+ // to demote the previous one in the same round trip — otherwise the second
1022
+ // default is refused by the database and the merchant sees a save that
1023
+ // failed for a reason the screen cannot explain.
1024
+ // -------------------------------------------------------------------------
1025
+ async listShippingPackages() {
1026
+ const tenantId = getTenantId();
1027
+ const { data, error } = await getDb().from(T.shippingPackages).select("*").eq("tenant_id", tenantId).order("is_default", { ascending: false }).order("name", { ascending: true });
1028
+ if (error) throw error;
1029
+ return (data ?? []).map(rowToShippingPackage);
1030
+ }
1031
+ async createShippingPackage(input) {
1032
+ const tenantId = getTenantId();
1033
+ if (!tenantId) throw new Error("@fayz-ai/shop: createShippingPackage requires a tenant id.");
1034
+ if (input.isDefault) await this.clearDefaultPackage(tenantId);
1035
+ const { data, error } = await getDb().from(T.shippingPackages).insert({ tenant_id: tenantId, ...shippingPackagePayload(input) }).select().single();
1036
+ if (error) throw error;
1037
+ return rowToShippingPackage(data);
1038
+ }
1039
+ async updateShippingPackage(id, input) {
1040
+ const tenantId = getTenantId();
1041
+ if (input.isDefault && tenantId) await this.clearDefaultPackage(tenantId, id);
1042
+ const { data, error } = await getDb().from(T.shippingPackages).update(shippingPackagePayload(input)).eq("id", id).select().single();
1043
+ if (error) throw error;
1044
+ return rowToShippingPackage(data);
1045
+ }
1046
+ async deleteShippingPackage(id) {
1047
+ const { error } = await getDb().from(T.shippingPackages).delete().eq("id", id);
1048
+ if (error) throw error;
1049
+ }
1050
+ async clearDefaultPackage(tenantId, exceptId) {
1051
+ let query = getDb().from(T.shippingPackages).update({ is_default: false }).eq("tenant_id", tenantId).eq("is_default", true);
1052
+ if (exceptId) query = query.neq("id", exceptId);
1053
+ const { error } = await query;
1054
+ if (error) throw error;
1055
+ }
523
1056
  async deleteShippingZone(id) {
524
1057
  const { error } = await getDb().from(T.shippingZones).delete().eq("id", id);
525
1058
  if (error) throw error;
526
1059
  }
1060
+ // -------------------------------------------------------------------------
1061
+ // Store policy + delivery fallback (0044b / 0015)
1062
+ //
1063
+ // Both are ONE ROW PER TENANT, and both are upserted rather than
1064
+ // insert-or-update: a store that never opened these screens has no row, and
1065
+ // making the first save conditional on discovering that is two round trips to
1066
+ // learn something the primary key already knows.
1067
+ // -------------------------------------------------------------------------
1068
+ async getShippingSettings() {
1069
+ const tenantId = getTenantId();
1070
+ const { data, error } = await getDb().from(T.shippingSettings).select("flat_rate, free_above, currency").eq("tenant_id", tenantId).maybeSingle();
1071
+ if (error) throw error;
1072
+ if (!data) return null;
1073
+ return {
1074
+ flatRate: Number(data.flat_rate ?? 0),
1075
+ freeAbove: data.free_above == null ? null : Number(data.free_above),
1076
+ currency: data.currency ?? "BRL"
1077
+ };
1078
+ }
1079
+ async updateShippingSettings(input) {
1080
+ const tenantId = getTenantId();
1081
+ if (!tenantId) throw new Error("@fayz-ai/shop: updateShippingSettings requires a tenant id.");
1082
+ const payload = { tenant_id: tenantId };
1083
+ if (input.flatRate !== void 0) payload.flat_rate = input.flatRate;
1084
+ if (input.freeAbove !== void 0) payload.free_above = input.freeAbove;
1085
+ if (input.currency !== void 0) payload.currency = input.currency;
1086
+ const { data, error } = await getDb().from(T.shippingSettings).upsert(payload, { onConflict: "tenant_id" }).select("flat_rate, free_above, currency").single();
1087
+ if (error) throw error;
1088
+ return {
1089
+ flatRate: Number(data.flat_rate ?? 0),
1090
+ freeAbove: data.free_above == null ? null : Number(data.free_above),
1091
+ currency: data.currency ?? "BRL"
1092
+ };
1093
+ }
1094
+ /**
1095
+ * "Does this store deliver here?" — asked with the address, before the card.
1096
+ *
1097
+ * Degrades to "yes, and not our own zone" on any failure. A store that cannot
1098
+ * reach the check must not stop selling: the order itself is still refused by
1099
+ * the 0048 trigger if the region really is excluded, so the worst case is the
1100
+ * shopper learning it one screen later instead of never selling at all.
1101
+ */
1102
+ async listShippingProfiles() {
1103
+ const { data, error } = await getDb().from(T.shippingProfiles).select("*").eq("tenant_id", getTenantId()).order("is_default", { ascending: false }).order("name");
1104
+ if (error) throw error;
1105
+ return (data ?? []).map(rowToShippingProfile);
1106
+ }
1107
+ async createShippingProfile(input) {
1108
+ const tenantId = getTenantId();
1109
+ if (!tenantId) throw new Error("@fayz-ai/shop: createShippingProfile requires a tenant id.");
1110
+ const { data, error } = await getDb().from(T.shippingProfiles).insert({ tenant_id: tenantId, ...shippingProfilePayload(input) }).select("*").single();
1111
+ if (error) throw error;
1112
+ return rowToShippingProfile(data);
1113
+ }
1114
+ async updateShippingProfile(id, input) {
1115
+ const { data, error } = await getDb().from(T.shippingProfiles).update(shippingProfilePayload(input)).eq("id", id).select("*").single();
1116
+ if (error) throw error;
1117
+ return rowToShippingProfile(data);
1118
+ }
1119
+ async deleteShippingProfile(id) {
1120
+ const { error } = await getDb().from(T.shippingProfiles).delete().eq("id", id);
1121
+ if (error) throw error;
1122
+ }
1123
+ // Places the store ships from (0058). Default first, then by name — the same
1124
+ // order the ledger's listLocations() uses, so the two screens agree.
1125
+ async listStockLocations() {
1126
+ const { data, error } = await getDb().from(T.stockLocations).select("*").eq("tenant_id", getTenantId()).order("is_default", { ascending: false }).order("name");
1127
+ if (error) throw error;
1128
+ return (data ?? []).map(rowToStockLocation);
1129
+ }
1130
+ async createStockLocation(input) {
1131
+ const tenantId = getTenantId();
1132
+ if (!tenantId) throw new Error("@fayz-ai/shop: createStockLocation requires a tenant id.");
1133
+ if (input.isDefault) await this.clearDefaultStockLocation(tenantId);
1134
+ const payload = stockLocationPayload(input);
1135
+ if (payload.code === void 0) payload.code = toLocationCode(input.name);
1136
+ const { data, error } = await getDb().from(T.stockLocations).insert({ tenant_id: tenantId, ...payload }).select("*").single();
1137
+ if (error) throw locationWriteError(error);
1138
+ return rowToStockLocation(data);
1139
+ }
1140
+ async updateStockLocation(id, input) {
1141
+ const tenantId = getTenantId();
1142
+ if (input.isDefault && tenantId) await this.clearDefaultStockLocation(tenantId, id);
1143
+ const { data, error } = await getDb().from(T.stockLocations).update(stockLocationPayload(input)).eq("id", id).select("*").single();
1144
+ if (error) throw locationWriteError(error);
1145
+ return rowToStockLocation(data);
1146
+ }
1147
+ async deleteStockLocation(id) {
1148
+ const { error } = await getDb().from(T.stockLocations).delete().eq("id", id);
1149
+ if (error) throw locationWriteError(error);
1150
+ }
1151
+ async clearDefaultStockLocation(tenantId, exceptId) {
1152
+ let query = getDb().from(T.stockLocations).update({ is_default: false }).eq("tenant_id", tenantId).eq("is_default", true);
1153
+ if (exceptId) query = query.neq("id", exceptId);
1154
+ const { error } = await query;
1155
+ if (error) throw error;
1156
+ }
1157
+ async checkDelivery(postalCode) {
1158
+ const tenantId = getTenantId();
1159
+ const digits = String(postalCode ?? "").replace(/\D/g, "");
1160
+ if (!tenantId || digits.length !== 8) return { refused: false, ownZone: false };
1161
+ const { data, error } = await getDb().rpc("shop_delivery_check", {
1162
+ p_tenant_id: tenantId,
1163
+ p_postal_code: digits
1164
+ });
1165
+ if (error) return { refused: false, ownZone: false };
1166
+ const row = Array.isArray(data) ? data[0] : data;
1167
+ return {
1168
+ refused: Boolean(row?.refused),
1169
+ refusedZone: row?.refused_zone ?? null,
1170
+ ownZone: Boolean(row?.own_zone)
1171
+ };
1172
+ }
1173
+ async getStoreSettings() {
1174
+ const tenantId = getTenantId();
1175
+ const { data, error } = await getDb().from(T.settings).select("*").eq("tenant_id", tenantId).maybeSingle();
1176
+ if (error) throw error;
1177
+ return data ? rowToStoreSettings(data) : null;
1178
+ }
1179
+ async updateStoreSettings(input) {
1180
+ const tenantId = getTenantId();
1181
+ if (!tenantId) throw new Error("@fayz-ai/shop: updateStoreSettings requires a tenant id.");
1182
+ const { data, error } = await getDb().from(T.settings).upsert({ tenant_id: tenantId, ...storeSettingsPayload(input) }, { onConflict: "tenant_id" }).select("*").single();
1183
+ if (error) throw error;
1184
+ return rowToStoreSettings(data);
1185
+ }
1186
+ /**
1187
+ * The shopper's read. Through the RPC, not the table: the table is closed to
1188
+ * anon on purpose (0029) and shop_store_settings() is the narrow window —
1189
+ * storefront fields only, and only for a published store.
1190
+ */
1191
+ async getPublicStoreSettings() {
1192
+ const tenantId = getTenantId();
1193
+ if (!tenantId) return null;
1194
+ const { data, error } = await getDb().rpc("shop_store_settings", { p_tenant_id: tenantId });
1195
+ if (error) throw error;
1196
+ const row = Array.isArray(data) ? data[0] : data;
1197
+ return row ? rowToStoreSettings(row) : null;
1198
+ }
527
1199
  /**
528
1200
  * Settle a pending order: the merchant confirming that the money arrived.
529
1201
  *
@@ -541,6 +1213,208 @@ var SupabaseShopProvider = class {
541
1213
  if (error) throw error;
542
1214
  return this.getOrder(id);
543
1215
  }
1216
+ // ---------------------------------------------------------------------------
1217
+ // Carrier labels. Everything goes through the function: the token never
1218
+ // reaches the browser, and `buy` is the one call that spends money.
1219
+ // ---------------------------------------------------------------------------
1220
+ async labelCall(orderId, action, extra = {}) {
1221
+ const tenantId = getTenantId();
1222
+ if (!tenantId) throw new Error("@fayz-ai/shop: shipping labels require a tenant id.");
1223
+ const { ok, payload } = await callShopFunction(getDb(), "shop-shipping-label", {
1224
+ tenantId,
1225
+ orderId,
1226
+ action,
1227
+ ...extra
1228
+ });
1229
+ if (!ok) throw new Error(payload?.error ?? "A transportadora n\xE3o respondeu agora.");
1230
+ return payload;
1231
+ }
1232
+ async quoteShippingLabel(orderId, carrier) {
1233
+ const payload = await this.labelCall(orderId, "quote", carrier ? { carrier } : {});
1234
+ return {
1235
+ options: (payload?.options ?? []).map((option) => ({
1236
+ serviceId: Number(option.service_id ?? option.serviceId),
1237
+ name: String(option.name ?? "Entrega"),
1238
+ carrier: String(option.carrier ?? ""),
1239
+ price: Number(option.price ?? 0),
1240
+ etaMinDays: option.eta_min_days ?? null,
1241
+ etaMaxDays: option.eta_max_days ?? null
1242
+ })),
1243
+ label: payload?.label ? {
1244
+ labelId: String(payload.label.external_id),
1245
+ labelUrl: payload.label.url ?? null,
1246
+ status: payload.label.status ?? null,
1247
+ tracking: payload.label.payload?.tracking ?? null
1248
+ } : null
1249
+ };
1250
+ }
1251
+ /** Spends the merchant's balance at the carrier. Never call it on a whim. */
1252
+ async buyShippingLabel(orderId, serviceId, carrier) {
1253
+ const payload = await this.labelCall(orderId, "buy", { serviceId, ...carrier ? { carrier } : {} });
1254
+ return {
1255
+ labelId: String(payload?.labelId ?? ""),
1256
+ labelUrl: payload?.labelUrl ?? null,
1257
+ status: "bought",
1258
+ tracking: payload?.tracking ?? null
1259
+ };
1260
+ }
1261
+ async printShippingLabel(orderId, carrier) {
1262
+ const payload = await this.labelCall(orderId, "print", carrier ? { carrier } : {});
1263
+ return payload?.labelUrl ?? null;
1264
+ }
1265
+ /** Drop-off points near a place. Store configuration — no order involved. */
1266
+ async listShippingAgencies(query) {
1267
+ const tenantId = getTenantId();
1268
+ if (!tenantId) return [];
1269
+ const { ok, payload } = await callShopFunction(getDb(), "shop-shipping-label", {
1270
+ tenantId,
1271
+ action: "agencies",
1272
+ state: query.state ?? "",
1273
+ city: query.city ?? "",
1274
+ ...query.carrier ? { carrier: query.carrier } : {}
1275
+ });
1276
+ if (!ok) throw new Error(payload?.error ?? "A transportadora n\xE3o respondeu agora.");
1277
+ return payload?.agencies ?? [];
1278
+ }
1279
+ async trackShippingLabel(orderId, carrier) {
1280
+ const payload = await this.labelCall(orderId, "track", carrier ? { carrier } : {});
1281
+ return payload?.tracking ?? null;
1282
+ }
1283
+ /**
1284
+ * The customer's address book, for the console. `shop_place_order` files a
1285
+ * new one here on every delivered order, and until now only the storefront
1286
+ * read them back — so a repeat customer had their address retyped by hand.
1287
+ */
1288
+ async listCustomerAddresses(customerId) {
1289
+ if (!asUuid(customerId)) return [];
1290
+ const { data, error } = await getDb().from("addresses").select("id,label,recipient,phone,postal_code,street,number,complement,district,city,state,country,is_default").eq("owner_type", "shop_customer").eq("owner_id", customerId).order("is_default", { ascending: false }).order("created_at", { ascending: false });
1291
+ if (error) return [];
1292
+ return (data ?? []).map((row) => ({
1293
+ id: row.id,
1294
+ label: row.label ?? void 0,
1295
+ recipient: row.recipient ?? void 0,
1296
+ phone: row.phone ?? void 0,
1297
+ postalCode: row.postal_code ?? "",
1298
+ street: row.street ?? "",
1299
+ number: row.number ?? void 0,
1300
+ complement: row.complement ?? void 0,
1301
+ district: row.district ?? void 0,
1302
+ city: row.city ?? "",
1303
+ state: row.state ?? "",
1304
+ country: row.country ?? void 0,
1305
+ isDefault: row.is_default ?? false
1306
+ }));
1307
+ }
1308
+ /**
1309
+ * What delivery costs to a postcode — the same answer the shopper gets.
1310
+ *
1311
+ * The console needs it to quote a price out loud while the customer is on the
1312
+ * line. Carrier first (when the merchant did not turn gateway quotes off),
1313
+ * zones as the fallback, exactly as the storefront resolves it. Advisory
1314
+ * either way: shop_place_order requotes and ignores whatever a client sends.
1315
+ */
1316
+ async quoteShipping(postalCode, subtotal) {
1317
+ const digits = toPostalDigits(postalCode);
1318
+ if (!digits) return [];
1319
+ const tenantId = getTenantId();
1320
+ if (!tenantId) return [];
1321
+ const settings = await this.getStoreSettings().catch(() => null);
1322
+ if (settings?.carrierQuotes !== false) {
1323
+ try {
1324
+ const { ok, payload } = await callShopFunction(getDb(), "shop-shipping-quote", {
1325
+ tenantId,
1326
+ postalCode: digits,
1327
+ subtotal
1328
+ });
1329
+ const rows = ok ? payload?.options ?? [] : [];
1330
+ if (rows.length > 0) {
1331
+ return rows.map((row) => ({
1332
+ zoneId: String(row.service_id ?? ""),
1333
+ name: String(row.name ?? "Entrega"),
1334
+ carrier: row.carrier ?? void 0,
1335
+ rate: Number(row.price ?? 0),
1336
+ baseRate: Number(row.price ?? 0),
1337
+ freeAbove: null,
1338
+ etaMinDays: row.eta_min_days ?? void 0,
1339
+ etaMaxDays: row.eta_max_days ?? void 0,
1340
+ free: Number(row.price ?? 0) === 0
1341
+ }));
1342
+ }
1343
+ } catch {
1344
+ }
1345
+ }
1346
+ const { data, error } = await getDb().rpc("shop_quote_shipping", {
1347
+ p_tenant_id: tenantId,
1348
+ p_postal_code: digits,
1349
+ p_subtotal: subtotal
1350
+ });
1351
+ if (error) return [];
1352
+ return (data ?? []).map((row) => ({
1353
+ zoneId: row.zone_id,
1354
+ name: row.name,
1355
+ carrier: row.carrier ?? void 0,
1356
+ rate: Number(row.rate ?? 0),
1357
+ baseRate: Number(row.base_rate ?? row.rate ?? 0),
1358
+ freeAbove: row.free_above == null ? null : Number(row.free_above),
1359
+ etaMinDays: row.eta_min_days ?? void 0,
1360
+ etaMaxDays: row.eta_max_days ?? void 0,
1361
+ free: Boolean(row.free)
1362
+ }));
1363
+ }
1364
+ /**
1365
+ * Charge a cart that is not an order yet — the phone sale, from the counter's
1366
+ * side. Same door the storefront uses, on purpose: `shop_price_cart` inside
1367
+ * the function reprices everything, so the console sends products and
1368
+ * quantities and never money. The order is born in `settleCheckoutSession`,
1369
+ * once the acquirer says the Pix landed, so an abandoned charge leaves no
1370
+ * ghost order holding stock.
1371
+ */
1372
+ async openCheckoutSession(input) {
1373
+ const tenantId = getTenantId();
1374
+ if (!tenantId) throw new Error("@fayz-ai/shop: openCheckoutSession requires a tenant id.");
1375
+ const { ok, status, payload } = await callShopFunction(getDb(), "shop-checkout-open", {
1376
+ tenantId,
1377
+ items: input.items.map((item) => ({
1378
+ productId: item.productId,
1379
+ optionsLabel: item.optionsLabel ?? null,
1380
+ quantity: item.quantity
1381
+ })),
1382
+ customer: input.customer,
1383
+ shippingAddress: input.shippingAddress ?? null,
1384
+ discountCode: input.discountCode ?? null
1385
+ });
1386
+ if (!ok || !payload?.sessionId) {
1387
+ throw new Error(payload?.error ?? (status === 404 ? "Esta loja ainda n\xE3o tem um meio de cobran\xE7a conectado." : "N\xE3o conseguimos abrir a cobran\xE7a agora."));
1388
+ }
1389
+ return {
1390
+ sessionId: payload.sessionId,
1391
+ total: Number(payload.total ?? 0),
1392
+ state: "open",
1393
+ charge: payload.brCode ? {
1394
+ emv: payload.brCode,
1395
+ // AbacatePay embeds the `data:` prefix; the type promises bare
1396
+ // base64. Left in, the view prefixes it again and the QR is a
1397
+ // silently broken <img>.
1398
+ qrcodeBase64: String(payload.brCodeBase64 ?? "").replace(/^data:image\/\w+;base64,/u, "") || null,
1399
+ expiresAt: payload.expiresAt ?? ""
1400
+ } : null
1401
+ };
1402
+ }
1403
+ /**
1404
+ * Ask whether the money landed. Runs on a timer, so a network blip must read
1405
+ * as "still waiting" (`unknown`) and never as a refused payment.
1406
+ */
1407
+ async settleCheckoutSession(sessionId) {
1408
+ const tenantId = getTenantId();
1409
+ if (!tenantId) return { state: "unknown" };
1410
+ try {
1411
+ const { ok, payload } = await callShopFunction(getDb(), "shop-checkout-settle", { tenantId, sessionId });
1412
+ if (!ok || !payload?.state) return { state: "unknown" };
1413
+ return { state: payload.state, orderId: payload.orderId ?? null, reason: payload.reason ?? null };
1414
+ } catch {
1415
+ return { state: "unknown" };
1416
+ }
1417
+ }
544
1418
  // Trusted placement — delegates all price/discount/inventory authority to the
545
1419
  // shop_place_order RPC (SECURITY DEFINER). The browser never sets a price.
546
1420
  async placeOrder(input) {
@@ -549,7 +1423,15 @@ var SupabaseShopProvider = class {
549
1423
  if (!tenantId) throw new Error("@fayz-ai/shop: placeOrder requires a tenant id (configure the shop tenant resolver).");
550
1424
  const { data, error } = await db.rpc("shop_place_order", {
551
1425
  p_tenant_id: tenantId,
552
- p_items: input.items.map((i) => ({ product_id: i.productId, quantity: i.quantity, options_label: i.optionsLabel ?? null })),
1426
+ // variant_id rides on each line (0029): the RPC prices and decrements
1427
+ // the variant when present, product-level otherwise. Pre-0029 servers
1428
+ // ignore unknown jsonb keys, so this stays compatible either way.
1429
+ p_items: input.items.map((i) => ({
1430
+ product_id: i.productId,
1431
+ variant_id: asUuid(i.variantId) ?? void 0,
1432
+ quantity: i.quantity,
1433
+ options_label: i.optionsLabel ?? null
1434
+ })),
553
1435
  p_customer_id: asUuid(input.customerId),
554
1436
  p_customer_name: input.customer?.name ?? null,
555
1437
  p_customer_email: input.customer?.email ?? null,
@@ -569,10 +1451,19 @@ var SupabaseShopProvider = class {
569
1451
  country: input.shippingAddress.country ?? "BR",
570
1452
  recipient: input.shippingAddress.recipient ?? null,
571
1453
  phone: input.shippingAddress.phone ?? null,
572
- label: input.shippingAddress.label ?? null
1454
+ label: input.shippingAddress.label ?? null,
1455
+ // Not an address field: it rides here because shop_place_order's
1456
+ // signature cannot grow without a DROP, and 0050's trigger lifts it to
1457
+ // plg_shop_orders.customer_document. The storefront already sent it;
1458
+ // the console did not, so a phone sale was born with no CPF.
1459
+ document: input.shippingAddress.document ?? null
573
1460
  } : null,
574
1461
  p_payment_method: input.paymentMethod ?? null,
575
- p_notes: input.notes ?? null
1462
+ p_notes: input.notes ?? null,
1463
+ // Only when something contributed: p_metadata exists from 0029 on, and
1464
+ // a pre-0029 server rejects unknown NAMED arguments (unlike unknown
1465
+ // jsonb keys). Absent metadata therefore must not send the key at all.
1466
+ ...input.metadata !== void 0 ? { p_metadata: input.metadata } : {}
576
1467
  });
577
1468
  if (error) throw error;
578
1469
  const orderId = typeof data === "string" ? data : data?.id ?? data?.order_id ?? data;
@@ -588,7 +1479,7 @@ var SupabaseShopProvider = class {
588
1479
  const tenantId = getTenantId();
589
1480
  let q = db.from(T.customers).select("*").order("first_name", { ascending: true });
590
1481
  if (tenantId) q = q.eq("tenant_id", tenantId);
591
- if (options?.search) q = q.or(`first_name.ilike.%${options.search}%,last_name.ilike.%${options.search}%,email.ilike.%${options.search}%`);
1482
+ for (const filter of customerSearchFilters(options?.search)) q = q.or(filter);
592
1483
  if (options?.limit) q = q.limit(options.limit);
593
1484
  const { data, error } = await q;
594
1485
  if (error) throw error;
@@ -600,7 +1491,15 @@ var SupabaseShopProvider = class {
600
1491
  return data ? rowToCustomer(data) : null;
601
1492
  }
602
1493
  async createCustomer(input) {
603
- const { data, error } = await getDb().from(T.customers).insert({ tenant_id: getTenantId(), first_name: input.firstName, last_name: input.lastName ?? "", email: input.email ?? null, phone: input.phone ?? null, notes: input.notes ?? null }).select().single();
1494
+ const { data, error } = await getDb().from(T.customers).insert({
1495
+ tenant_id: getTenantId(),
1496
+ first_name: input.firstName,
1497
+ last_name: input.lastName ?? "",
1498
+ email: input.email ?? null,
1499
+ phone: input.phone ?? null,
1500
+ document: input.document ?? null,
1501
+ notes: input.notes ?? null
1502
+ }).select().single();
604
1503
  if (error) throw error;
605
1504
  return rowToCustomer(data);
606
1505
  }
@@ -610,6 +1509,7 @@ var SupabaseShopProvider = class {
610
1509
  if (input.lastName !== void 0) updates.last_name = input.lastName;
611
1510
  if (input.email !== void 0) updates.email = input.email;
612
1511
  if (input.phone !== void 0) updates.phone = input.phone;
1512
+ if (input.document !== void 0) updates.document = input.document;
613
1513
  if (input.notes !== void 0) updates.notes = input.notes;
614
1514
  const { data, error } = await getDb().from(T.customers).update(updates).eq("id", id).select().single();
615
1515
  if (error) throw error;
@@ -636,6 +1536,50 @@ var SupabaseShopProvider = class {
636
1536
  return rowToCustomer(row);
637
1537
  }
638
1538
  // ---------------------------------------------------------------------------
1539
+ // Promotions (spine M6) — evaluation happens INSIDE the database via the
1540
+ // SECURITY DEFINER function (migrations/0033), so an anonymous storefront
1541
+ // never reads promotion rows or codes directly: it sends the cart snapshot
1542
+ // and receives ordered actions. The function mirrors the TS reference
1543
+ // evaluator (src/rules/evaluator.ts), which is the tested spec.
1544
+ // ---------------------------------------------------------------------------
1545
+ async evaluatePromotions(input) {
1546
+ const db = getDb();
1547
+ const tenantId = getTenantId();
1548
+ if (!tenantId) throw new Error("@fayz-ai/shop: evaluatePromotions requires a tenant id (configure the shop tenant resolver).");
1549
+ const cart = input.cart;
1550
+ const { data, error } = await db.rpc("shop_compute_promotion_actions", {
1551
+ p_tenant_id: tenantId,
1552
+ // The serializable RuleCartSnapshot, snake_case on the wire — the same
1553
+ // contract 0033 documents for p_cart.
1554
+ p_cart: {
1555
+ items: cart.items.map((item) => ({
1556
+ id: item.id ?? item.productId,
1557
+ product_id: item.productId,
1558
+ quantity: item.quantity,
1559
+ unit_price: item.unitPrice,
1560
+ category_id: item.categoryId ?? null,
1561
+ collection_id: item.collectionId ?? null
1562
+ })),
1563
+ shipping_total: cart.shippingTotal ?? null,
1564
+ region: cart.region ?? null,
1565
+ customer: cart.customer ? { email: cart.customer.email ?? null, tags: cart.customer.tags ?? [] } : null,
1566
+ codes: cart.codes ?? []
1567
+ }
1568
+ });
1569
+ if (error) throw error;
1570
+ const rows = Array.isArray(data) ? data : [];
1571
+ return rows.map((row) => {
1572
+ const code = row.code ?? null;
1573
+ if (row.action === "addShippingDiscount") {
1574
+ return { action: "addShippingDiscount", promotionId: String(row.promotion_id), code, amount: Number(row.amount) };
1575
+ }
1576
+ if (row.action === "addFreeItem") {
1577
+ return { action: "addFreeItem", promotionId: String(row.promotion_id), code, productId: String(row.product_id), quantity: Number(row.quantity) };
1578
+ }
1579
+ return { action: "addItemDiscount", promotionId: String(row.promotion_id), code, itemId: String(row.item_id), amount: Number(row.amount) };
1580
+ });
1581
+ }
1582
+ // ---------------------------------------------------------------------------
639
1583
  // Discounts
640
1584
  // ---------------------------------------------------------------------------
641
1585
  async listDiscounts(options) {
@@ -714,11 +1658,585 @@ var SupabaseShopProvider = class {
714
1658
  const { error } = await getDb().from(T.salesChannels).delete().eq("id", id);
715
1659
  if (error) throw error;
716
1660
  }
1661
+ // ---------------------------------------------------------------------------
1662
+ // Custom entities (spine M4). Storage resolves through the EntityStore
1663
+ // indirection (I5): a promoted entity's registered store wins; everything
1664
+ // else reads/writes the generic jsonb table (migration 0030), whose row
1665
+ // access is the RLS generated from each declaration (I6).
1666
+ // ---------------------------------------------------------------------------
1667
+ entityStore(entity) {
1668
+ return resolveEntityStore(entity, createSupabaseEntityStore);
1669
+ }
1670
+ async listEntityRecords(entity, options) {
1671
+ return this.entityStore(entity).list(options);
1672
+ }
1673
+ async getEntityRecord(entity, id) {
1674
+ return this.entityStore(entity).get(id);
1675
+ }
1676
+ async getEntityRecordBySlug(entity, slug) {
1677
+ return this.entityStore(entity).getBySlug(slug);
1678
+ }
1679
+ async createEntityRecord(entity, input) {
1680
+ return this.entityStore(entity).create(input);
1681
+ }
1682
+ async updateEntityRecord(entity, id, input) {
1683
+ return this.entityStore(entity).update(id, input);
1684
+ }
1685
+ async deleteEntityRecord(entity, id) {
1686
+ return this.entityStore(entity).delete(id);
1687
+ }
1688
+ // -------------------------------------------------------------------------
1689
+ // Coleções, grupos/segmentos, vale-presente, e-mails e mídia.
1690
+ // -------------------------------------------------------------------------
1691
+ async listCollections() {
1692
+ const db = getDb();
1693
+ const tenantId = getTenantId();
1694
+ let q = db.from(T.collections).select(`*, members:${T.collectionProducts}(product_id, position)`).order("sort_order", { ascending: true });
1695
+ if (tenantId) q = q.eq("tenant_id", tenantId);
1696
+ const { data, error } = await q;
1697
+ if (error) throw error;
1698
+ return (data ?? []).map(rowToCollection);
1699
+ }
1700
+ async getCollection(id) {
1701
+ const { data, error } = await getDb().from(T.collections).select(`*, members:${T.collectionProducts}(product_id, position)`).eq("id", id).maybeSingle();
1702
+ if (error) throw error;
1703
+ return data ? rowToCollection(data) : null;
1704
+ }
1705
+ async createCollection(input) {
1706
+ const db = getDb();
1707
+ const tenantId = getTenantId();
1708
+ const { data, error } = await db.from(T.collections).insert({
1709
+ tenant_id: tenantId,
1710
+ title: input.title,
1711
+ handle: input.handle?.trim() || slugify(input.title),
1712
+ description: input.description ?? null,
1713
+ image_url: input.imageUrl ?? null,
1714
+ seo_title: input.seoTitle ?? null,
1715
+ seo_description: input.seoDescription ?? null,
1716
+ published: input.published ?? true,
1717
+ sort_order: input.sortOrder ?? 0
1718
+ }).select().single();
1719
+ if (error) throw error;
1720
+ await this.syncCollectionProducts(data.id, tenantId, input.productIds);
1721
+ return await this.getCollection(data.id) ?? rowToCollection(data);
1722
+ }
1723
+ async updateCollection(id, input) {
1724
+ const db = getDb();
1725
+ const updates = {};
1726
+ if (input.title !== void 0) updates.title = input.title;
1727
+ if (input.handle !== void 0) updates.handle = input.handle?.trim() || slugify(String(input.title ?? ""));
1728
+ if (input.description !== void 0) updates.description = input.description;
1729
+ if (input.imageUrl !== void 0) updates.image_url = input.imageUrl;
1730
+ if (input.seoTitle !== void 0) updates.seo_title = input.seoTitle;
1731
+ if (input.seoDescription !== void 0) updates.seo_description = input.seoDescription;
1732
+ if (input.published !== void 0) updates.published = input.published;
1733
+ if (input.sortOrder !== void 0) updates.sort_order = input.sortOrder;
1734
+ if (Object.keys(updates).length > 0) {
1735
+ const { error } = await db.from(T.collections).update(updates).eq("id", id);
1736
+ if (error) throw error;
1737
+ }
1738
+ if (input.productIds !== void 0) {
1739
+ await this.syncCollectionProducts(id, getTenantId(), input.productIds);
1740
+ }
1741
+ const updated = await this.getCollection(id);
1742
+ if (!updated) throw new Error("Collection not found");
1743
+ return updated;
1744
+ }
1745
+ async deleteCollection(id) {
1746
+ const { error } = await getDb().from(T.collections).delete().eq("id", id);
1747
+ if (error) throw error;
1748
+ }
1749
+ async listCustomerGroups() {
1750
+ const db = getDb();
1751
+ const tenantId = getTenantId();
1752
+ let q = db.from(T.customerGroups).select(`*, members:${T.customerGroupMembers}(customer_id)`).order("name", { ascending: true });
1753
+ if (tenantId) q = q.eq("tenant_id", tenantId);
1754
+ const { data, error } = await q;
1755
+ if (error) throw error;
1756
+ return (data ?? []).map(rowToCustomerGroup);
1757
+ }
1758
+ async createCustomerGroup(input) {
1759
+ const tenantId = getTenantId();
1760
+ const { data, error } = await getDb().from(T.customerGroups).insert({
1761
+ tenant_id: tenantId,
1762
+ name: input.name,
1763
+ description: input.description ?? null,
1764
+ color: input.color ?? null
1765
+ }).select().single();
1766
+ if (error) throw error;
1767
+ await this.syncGroupMembers(data.id, tenantId, input.customerIds);
1768
+ return { ...rowToCustomerGroup(data), customerIds: input.customerIds ?? [] };
1769
+ }
1770
+ async updateCustomerGroup(id, input) {
1771
+ const db = getDb();
1772
+ const updates = {};
1773
+ if (input.name !== void 0) updates.name = input.name;
1774
+ if (input.description !== void 0) updates.description = input.description;
1775
+ if (input.color !== void 0) updates.color = input.color;
1776
+ if (Object.keys(updates).length > 0) {
1777
+ const { error: error2 } = await db.from(T.customerGroups).update(updates).eq("id", id);
1778
+ if (error2) throw error2;
1779
+ }
1780
+ if (input.customerIds !== void 0) {
1781
+ await this.syncGroupMembers(id, getTenantId(), input.customerIds);
1782
+ }
1783
+ const { data, error } = await db.from(T.customerGroups).select(`*, members:${T.customerGroupMembers}(customer_id)`).eq("id", id).single();
1784
+ if (error) throw error;
1785
+ return rowToCustomerGroup(data);
1786
+ }
1787
+ async deleteCustomerGroup(id) {
1788
+ const { error } = await getDb().from(T.customerGroups).delete().eq("id", id);
1789
+ if (error) throw error;
1790
+ }
1791
+ async listCustomerSegments() {
1792
+ const db = getDb();
1793
+ const tenantId = getTenantId();
1794
+ let q = db.from(T.customerSegments).select("*").order("name", { ascending: true });
1795
+ if (tenantId) q = q.eq("tenant_id", tenantId);
1796
+ const { data, error } = await q;
1797
+ if (error) throw error;
1798
+ return (data ?? []).map(rowToCustomerSegment);
1799
+ }
1800
+ async createCustomerSegment(input) {
1801
+ const { data, error } = await getDb().from(T.customerSegments).insert({ tenant_id: getTenantId(), ...segmentPayload(input), name: input.name }).select().single();
1802
+ if (error) throw error;
1803
+ return rowToCustomerSegment(data);
1804
+ }
1805
+ async updateCustomerSegment(id, input) {
1806
+ const { data, error } = await getDb().from(T.customerSegments).update(segmentPayload(input)).eq("id", id).select().single();
1807
+ if (error) throw error;
1808
+ return rowToCustomerSegment(data);
1809
+ }
1810
+ async deleteCustomerSegment(id) {
1811
+ const { error } = await getDb().from(T.customerSegments).delete().eq("id", id);
1812
+ if (error) throw error;
1813
+ }
1814
+ async listGiftCards() {
1815
+ const db = getDb();
1816
+ const tenantId = getTenantId();
1817
+ let q = db.from(T.giftCards).select("*").order("created_at", { ascending: false });
1818
+ if (tenantId) q = q.eq("tenant_id", tenantId);
1819
+ const { data, error } = await q;
1820
+ if (error) throw error;
1821
+ return (data ?? []).map(rowToGiftCard);
1822
+ }
1823
+ async createGiftCard(input) {
1824
+ const amount = Number(input.initialBalance) || 0;
1825
+ const { data, error } = await getDb().from(T.giftCards).insert({
1826
+ tenant_id: getTenantId(),
1827
+ code: (input.code?.trim() || generateGiftCardCode()).toUpperCase(),
1828
+ initial_balance: amount,
1829
+ // A new card is worth its face value. The admin never types the
1830
+ // remaining balance at issue — that is what redemption is for.
1831
+ balance: amount,
1832
+ currency: input.currency ?? "BRL",
1833
+ status: input.status ?? "active",
1834
+ expires_at: input.expiresAt ?? null,
1835
+ customer_id: asUuid(input.customerId),
1836
+ customer_email: input.customerEmail ?? null,
1837
+ note: input.note ?? null
1838
+ }).select().single();
1839
+ if (error) throw error;
1840
+ return rowToGiftCard(data);
1841
+ }
1842
+ async updateGiftCard(id, input) {
1843
+ const updates = {};
1844
+ if (input.code !== void 0) updates.code = input.code.trim().toUpperCase();
1845
+ if (input.balance !== void 0) updates.balance = input.balance;
1846
+ if (input.status !== void 0) updates.status = input.status;
1847
+ if (input.expiresAt !== void 0) updates.expires_at = input.expiresAt;
1848
+ if (input.customerId !== void 0) updates.customer_id = asUuid(input.customerId);
1849
+ if (input.customerEmail !== void 0) updates.customer_email = input.customerEmail;
1850
+ if (input.note !== void 0) updates.note = input.note;
1851
+ const { data, error } = await getDb().from(T.giftCards).update(updates).eq("id", id).select().single();
1852
+ if (error) throw error;
1853
+ return rowToGiftCard(data);
1854
+ }
1855
+ async deleteGiftCard(id) {
1856
+ const { error } = await getDb().from(T.giftCards).delete().eq("id", id);
1857
+ if (error) throw error;
1858
+ }
1859
+ async listEmailTemplates() {
1860
+ const db = getDb();
1861
+ const tenantId = getTenantId();
1862
+ let q = db.from(T.messageTemplates).select("*").like("event_key", `${SHOP_EVENT_PREFIX}%`).eq("channel", EMAIL_CHANNEL);
1863
+ if (tenantId) q = q.eq("tenant_id", tenantId);
1864
+ const { data, error } = await q;
1865
+ if (error) throw error;
1866
+ return (data ?? []).map(rowToEmailTemplate);
1867
+ }
1868
+ async upsertEmailTemplate(input) {
1869
+ const payload = {
1870
+ tenant_id: getTenantId(),
1871
+ event_key: `${SHOP_EVENT_PREFIX}${input.event}`,
1872
+ channel: EMAIL_CHANNEL
1873
+ };
1874
+ if (input.enabled !== void 0) payload.enabled = input.enabled;
1875
+ if (input.subject !== void 0) payload.subject = input.subject;
1876
+ if (input.body !== void 0) payload.body = input.body;
1877
+ const { data, error } = await getDb().from(T.messageTemplates).upsert(payload, { onConflict: "tenant_id,event_key,channel" }).select().single();
1878
+ if (error) throw error;
1879
+ return rowToEmailTemplate(data);
1880
+ }
1881
+ async listMediaFiles() {
1882
+ const tenantId = getTenantId();
1883
+ if (!tenantId) return [];
1884
+ const [attached, library] = await Promise.all([
1885
+ this.listAttachedMedia(tenantId),
1886
+ this.listLibraryFiles(tenantId)
1887
+ ]);
1888
+ return mergeMediaFiles(attached, library);
1889
+ }
1890
+ /** Every product photo in the store, each one carrying its product. */
1891
+ async listAttachedMedia(tenantId) {
1892
+ const db = getDb();
1893
+ const { data: rows, error } = await db.from(T.productImages).select("id,product_id,url,alt_text,sort_order,is_primary,created_at").eq("tenant_id", tenantId).order("created_at", { ascending: false }).limit(1e3);
1894
+ if (error || !rows?.length) return [];
1895
+ const productIds = [...new Set(rows.map((r) => r.product_id).filter(Boolean))];
1896
+ const { data: products } = await db.from(T.products).select("id,name").in("id", productIds);
1897
+ const nameById = new Map((products ?? []).map((p) => [p.id, p.name]));
1898
+ return rows.map((row) => productImageToMediaFile(rowToImage(row), nameById.get(row.product_id)));
1899
+ }
1900
+ /** What the merchant uploaded straight to the library — owned by nothing. */
1901
+ async listLibraryFiles(tenantId) {
1902
+ const bucket = getDb().storage.from(T.mediaBucket);
1903
+ const listAt = async (prefix) => {
1904
+ const { data, error } = await bucket.list(prefix, {
1905
+ limit: 1e3,
1906
+ sortBy: { column: "created_at", order: "desc" }
1907
+ });
1908
+ if (error) return [];
1909
+ return (data ?? []).map((entry) => ({ prefix, entry }));
1910
+ };
1911
+ const top = await listAt(tenantId);
1912
+ const folders = top.filter(({ entry }) => !entry.id && entry.name === LIBRARY_FOLDER).map(({ entry }) => `${tenantId}/${entry.name}`);
1913
+ const nested = (await Promise.all(folders.map(listAt))).flat();
1914
+ return [...top, ...nested].filter(({ entry }) => entry.id).map(({ prefix, entry }) => {
1915
+ const path = `${prefix}/${entry.name}`;
1916
+ const { data: { publicUrl } } = bucket.getPublicUrl(path);
1917
+ const name = displayNameOf(entry.name);
1918
+ const file = {
1919
+ id: path,
1920
+ name,
1921
+ url: publicUrl,
1922
+ kind: mediaKindOf(entry.name, entry.metadata?.mimetype),
1923
+ size: Number(entry.metadata?.size ?? 0),
1924
+ extension: (entry.name.split(".").pop() ?? "").toUpperCase(),
1925
+ createdAt: entry.created_at ?? entry.updated_at ?? ""
1926
+ };
1927
+ return file;
1928
+ });
1929
+ }
1930
+ async uploadMediaFile(file) {
1931
+ const db = getDb();
1932
+ const tenantId = getTenantId();
1933
+ if (!tenantId) throw new Error("@fayz-ai/shop: no active tenant for media upload");
1934
+ const path = `${tenantId}/${LIBRARY_FOLDER}/${Date.now()}-${file.name}`;
1935
+ const bucket = db.storage.from(T.mediaBucket);
1936
+ const { error } = await bucket.upload(path, file);
1937
+ if (error) throw error;
1938
+ const { data: { publicUrl } } = bucket.getPublicUrl(path);
1939
+ return {
1940
+ id: path,
1941
+ name: file.name,
1942
+ url: publicUrl,
1943
+ kind: mediaKindOf(file.name, file.type),
1944
+ size: file.size,
1945
+ extension: (file.name.split(".").pop() ?? "").toUpperCase(),
1946
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1947
+ };
1948
+ }
1949
+ async deleteMediaFile(id) {
1950
+ const { error } = await getDb().storage.from(T.mediaBucket).remove([id]);
1951
+ if (error) throw error;
1952
+ }
1953
+ async syncCollectionProducts(collectionId, tenantId, productIds) {
1954
+ if (productIds === void 0) return;
1955
+ const db = getDb();
1956
+ const { error: delErr } = await db.from(T.collectionProducts).delete().eq("collection_id", collectionId);
1957
+ if (delErr) throw delErr;
1958
+ if (productIds.length === 0) return;
1959
+ const rows = productIds.map((productId, position) => ({
1960
+ collection_id: collectionId,
1961
+ product_id: productId,
1962
+ tenant_id: tenantId,
1963
+ position
1964
+ }));
1965
+ const { error } = await db.from(T.collectionProducts).insert(rows);
1966
+ if (error) throw error;
1967
+ }
1968
+ async syncGroupMembers(groupId, tenantId, customerIds) {
1969
+ if (customerIds === void 0) return;
1970
+ const db = getDb();
1971
+ const { error: delErr } = await db.from(T.customerGroupMembers).delete().eq("group_id", groupId);
1972
+ if (delErr) throw delErr;
1973
+ if (customerIds.length === 0) return;
1974
+ const rows = customerIds.map((customerId) => ({
1975
+ group_id: groupId,
1976
+ customer_id: customerId,
1977
+ tenant_id: tenantId
1978
+ }));
1979
+ const { error } = await db.from(T.customerGroupMembers).insert(rows);
1980
+ if (error) throw error;
1981
+ }
717
1982
  };
718
1983
  function createSupabaseShopProvider() {
719
1984
  return new SupabaseShopProvider();
720
1985
  }
721
1986
 
722
- export { SupabaseShopProvider, applyDiscount, checkInventory, computeCartTotals, createSupabaseShopProvider, getCustomerOrders, getShopTenantId, setShopTenantResolver, validateDiscount };
1987
+ // src/shipping/label-provider.ts
1988
+ var registry = /* @__PURE__ */ new Map();
1989
+ function registerShippingLabelProvider(provider) {
1990
+ registry.set(provider.id, provider);
1991
+ }
1992
+ function listShippingLabelProviders() {
1993
+ return [...registry.values()];
1994
+ }
1995
+ function resetShippingLabelProviders() {
1996
+ registry.clear();
1997
+ }
1998
+ function resolveShippingLabelProvider(options) {
1999
+ const { connected, preferred } = options;
2000
+ if (preferred === "") return null;
2001
+ if (preferred) {
2002
+ const chosen = registry.get(preferred);
2003
+ return chosen && connected.includes(preferred) ? chosen : null;
2004
+ }
2005
+ for (const id of connected) {
2006
+ const found = registry.get(id);
2007
+ if (found) return found;
2008
+ }
2009
+ return null;
2010
+ }
2011
+
2012
+ // src/lib/document.ts
2013
+ var documentDigits = (value) => String(value ?? "").replace(/\D/g, "");
2014
+ function checkDigit(digits, weights) {
2015
+ const sum = weights.reduce((acc, weight, index) => acc + Number(digits[index]) * weight, 0);
2016
+ const rest = sum % 11;
2017
+ return rest < 2 ? 0 : 11 - rest;
2018
+ }
2019
+ var CPF_FIRST = [10, 9, 8, 7, 6, 5, 4, 3, 2];
2020
+ var CPF_SECOND = [11, 10, 9, 8, 7, 6, 5, 4, 3, 2];
2021
+ var CNPJ_FIRST = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
2022
+ var CNPJ_SECOND = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
2023
+ function isValidCpf(value) {
2024
+ const digits = documentDigits(value);
2025
+ if (digits.length !== 11 || /^(\d)\1{10}$/.test(digits)) return false;
2026
+ return checkDigit(digits, CPF_FIRST) === Number(digits[9]) && checkDigit(digits, CPF_SECOND) === Number(digits[10]);
2027
+ }
2028
+ function isValidCnpj(value) {
2029
+ const digits = documentDigits(value);
2030
+ if (digits.length !== 14 || /^(\d)\1{13}$/.test(digits)) return false;
2031
+ return checkDigit(digits, CNPJ_FIRST) === Number(digits[12]) && checkDigit(digits, CNPJ_SECOND) === Number(digits[13]);
2032
+ }
2033
+ function documentKind(value) {
2034
+ const length = documentDigits(value).length;
2035
+ if (length === 11) return "cpf";
2036
+ if (length === 14) return "cnpj";
2037
+ return null;
2038
+ }
2039
+ function isValidDocument(value) {
2040
+ const kind = documentKind(value);
2041
+ return kind === "cpf" ? isValidCpf(value) : kind === "cnpj" ? isValidCnpj(value) : false;
2042
+ }
2043
+ function formatDocument(value) {
2044
+ const digits = documentDigits(value).slice(0, 14);
2045
+ if (digits.length <= 11) {
2046
+ return digits.replace(/^(\d{3})(\d)/, "$1.$2").replace(/^(\d{3})\.(\d{3})(\d)/, "$1.$2.$3").replace(/\.(\d{3})(\d{1,2})$/, ".$1-$2");
2047
+ }
2048
+ return digits.replace(/^(\d{2})(\d)/, "$1.$2").replace(/^(\d{2})\.(\d{3})(\d)/, "$1.$2.$3").replace(/\.(\d{3})(\d)/, ".$1/$2").replace(/(\d{4})(\d{1,2})$/, "$1-$2");
2049
+ }
2050
+
2051
+ // src/lib/emails.ts
2052
+ var EMAIL_PLACEHOLDERS = [
2053
+ "{{store_name}}",
2054
+ "{{customer_name}}",
2055
+ "{{order_number}}",
2056
+ "{{order_total}}",
2057
+ "{{order_items}}",
2058
+ "{{tracking_code}}",
2059
+ "{{cart_url}}"
2060
+ ];
2061
+ var SHOP_EMAIL_EVENTS = [
2062
+ {
2063
+ event: "order_confirmation",
2064
+ kind: "transactional",
2065
+ icon: "CircleCheck",
2066
+ title: "Confirma\xE7\xE3o de pedido",
2067
+ description: "Enviado quando o cliente conclui a compra",
2068
+ trigger: "Imediatamente ap\xF3s o checkout",
2069
+ defaultSubject: "Pedido #{{order_number}} confirmado",
2070
+ defaultBody: "Ol\xE1 {{customer_name}},\n\nRecebemos seu pedido #{{order_number}}.\n\n{{order_items}}\n\nTotal: {{order_total}}\n\nObrigado por comprar na {{store_name}}!",
2071
+ defaultEnabled: true
2072
+ },
2073
+ {
2074
+ event: "order_paid",
2075
+ kind: "transactional",
2076
+ icon: "CreditCard",
2077
+ title: "Pagamento aprovado",
2078
+ description: "Enviado quando o pagamento \xE9 confirmado",
2079
+ trigger: "Quando o pagamento \xE9 aprovado",
2080
+ defaultSubject: "Pagamento aprovado \u2014 pedido #{{order_number}}",
2081
+ defaultBody: "Ol\xE1 {{customer_name}},\n\nSeu pagamento de {{order_total}} foi aprovado. J\xE1 estamos preparando o pedido #{{order_number}}.\n\n{{store_name}}",
2082
+ defaultEnabled: false
2083
+ },
2084
+ {
2085
+ event: "order_shipped",
2086
+ kind: "transactional",
2087
+ icon: "Truck",
2088
+ title: "Pedido enviado",
2089
+ description: "Enviado quando o pedido sai para entrega, com o rastreio",
2090
+ trigger: "Quando a entrega \xE9 marcada como enviada",
2091
+ defaultSubject: "Seu pedido saiu para entrega \u2014 #{{order_number}}",
2092
+ defaultBody: "Ol\xE1 {{customer_name}},\n\nSeu pedido #{{order_number}} est\xE1 a caminho.\n\nRastreio: {{tracking_code}}\n\n{{store_name}}",
2093
+ defaultEnabled: false
2094
+ },
2095
+ {
2096
+ event: "order_delivered",
2097
+ kind: "transactional",
2098
+ icon: "PackageCheck",
2099
+ title: "Pedido entregue",
2100
+ description: "Enviado quando a entrega \xE9 conclu\xEDda",
2101
+ trigger: "Quando a entrega \xE9 marcada como conclu\xEDda",
2102
+ defaultSubject: "Seu pedido chegou \u2014 #{{order_number}}",
2103
+ defaultBody: "Ol\xE1 {{customer_name}},\n\nSeu pedido #{{order_number}} foi entregue. Esperamos que goste!\n\n{{store_name}}",
2104
+ defaultEnabled: false
2105
+ },
2106
+ {
2107
+ event: "order_cancelled",
2108
+ kind: "transactional",
2109
+ icon: "CircleX",
2110
+ title: "Pedido cancelado",
2111
+ description: "Enviado quando um pedido \xE9 cancelado",
2112
+ trigger: "Quando o pedido \xE9 cancelado",
2113
+ defaultSubject: "Pedido #{{order_number}} cancelado",
2114
+ defaultBody: "Ol\xE1 {{customer_name}},\n\nSeu pedido #{{order_number}} foi cancelado. Se voc\xEA n\xE3o pediu isso, responda este e-mail.\n\n{{store_name}}",
2115
+ defaultEnabled: false
2116
+ },
2117
+ {
2118
+ event: "refund_confirmation",
2119
+ kind: "transactional",
2120
+ icon: "RotateCcw",
2121
+ title: "Reembolso processado",
2122
+ description: "Enviado quando um reembolso \xE9 processado",
2123
+ trigger: "Quando o pagamento \xE9 estornado",
2124
+ defaultSubject: "Reembolso emitido \u2014 pedido #{{order_number}}",
2125
+ defaultBody: "Ol\xE1 {{customer_name}},\n\nO reembolso de {{order_total}} do pedido #{{order_number}} foi processado. O prazo de cr\xE9dito depende do seu banco.\n\n{{store_name}}",
2126
+ defaultEnabled: false
2127
+ },
2128
+ {
2129
+ event: "customer_welcome",
2130
+ kind: "transactional",
2131
+ icon: "Mail",
2132
+ title: "Boas-vindas",
2133
+ description: "Enviado quando um cliente cria conta na loja",
2134
+ trigger: "Quando o cliente se cadastra",
2135
+ defaultSubject: "Bem-vindo(a) \xE0 {{store_name}}",
2136
+ defaultBody: "Ol\xE1 {{customer_name}},\n\nSua conta na {{store_name}} est\xE1 pronta. Bom proveito!\n\n{{store_name}}",
2137
+ defaultEnabled: false
2138
+ },
2139
+ {
2140
+ event: "abandoned_cart",
2141
+ kind: "marketing",
2142
+ icon: "ShoppingCart",
2143
+ title: "Carrinho abandonado",
2144
+ description: "Enviado quando o cliente deixa itens no carrinho sem finalizar",
2145
+ trigger: "1 hora ap\xF3s o abandono do carrinho",
2146
+ defaultSubject: "Voc\xEA esqueceu alguns itens no carrinho",
2147
+ defaultBody: "Ol\xE1 {{customer_name}},\n\nSeus itens ainda est\xE3o te esperando.\n\n{{cart_url}}\n\n{{store_name}}",
2148
+ defaultEnabled: false
2149
+ }
2150
+ ];
2151
+ function renderEmailTemplate(text, values) {
2152
+ return text.replace(/\{\{\s*(\w+)\s*\}\}/g, (whole, key) => values[key] ?? whole);
2153
+ }
2154
+ var EMAIL_PREVIEW_SAMPLE = {
2155
+ store_name: "Sua Loja",
2156
+ customer_name: "Maria Silva",
2157
+ order_number: "1042",
2158
+ order_total: "R$ 189,90",
2159
+ order_items: "1 \xD7 Camiseta Cl\xE1ssica \u2014 R$ 89,90\n2 \xD7 Caneca Esmaltada \u2014 R$ 100,00",
2160
+ tracking_code: "BR123456789BR",
2161
+ cart_url: "https://sualoja.com.br/carrinho"
2162
+ };
2163
+ function resolveEmailTemplates(overrides) {
2164
+ const byEvent = new Map(overrides.map((o) => [o.event, o]));
2165
+ return SHOP_EMAIL_EVENTS.map((def) => {
2166
+ const override = byEvent.get(def.event) ?? null;
2167
+ return {
2168
+ ...def,
2169
+ override,
2170
+ enabled: override?.enabled ?? def.defaultEnabled,
2171
+ subject: override?.subject ?? def.defaultSubject,
2172
+ body: override?.body ?? def.defaultBody,
2173
+ customized: Boolean(override?.subject || override?.body)
2174
+ };
2175
+ });
2176
+ }
2177
+
2178
+ // src/lib/message-events.ts
2179
+ var ORDER_FIELDS = [
2180
+ { name: "order_number", label: "N\xFAmero do pedido", type: "string", example: "1042" },
2181
+ { name: "order_total", label: "Total", type: "money", example: "R$ 189,90" },
2182
+ { name: "order_items", label: "Itens", type: "string", example: "1 \xD7 Camiseta Cl\xE1ssica \u2014 R$ 89,90\n2 \xD7 Caneca Esmaltada \u2014 R$ 100,00" },
2183
+ { name: "customer_name", label: "Cliente", type: "string", example: "Maria Silva" },
2184
+ { name: "store_name", label: "Loja", type: "string", example: "Sua Loja" }
2185
+ ];
2186
+ var TRACKING_FIELD = {
2187
+ name: "tracking_code",
2188
+ label: "C\xF3digo de rastreio",
2189
+ type: "string",
2190
+ example: "BR123456789BR"
2191
+ };
2192
+ var CART_FIELD = {
2193
+ name: "cart_url",
2194
+ label: "Link do carrinho",
2195
+ type: "url",
2196
+ example: "https://sualoja.com.br/carrinho"
2197
+ };
2198
+ var FIELDS_BY_EVENT = {
2199
+ order_shipped: [...ORDER_FIELDS, TRACKING_FIELD],
2200
+ order_delivered: [...ORDER_FIELDS, TRACKING_FIELD],
2201
+ abandoned_cart: [...ORDER_FIELDS, CART_FIELD]
2202
+ };
2203
+ var NOT_AUDITED = /* @__PURE__ */ new Set(["abandoned_cart"]);
2204
+ function buildShopDomainEvents() {
2205
+ return SHOP_EMAIL_EVENTS.map((def) => ({
2206
+ name: `shop.${def.event}`,
2207
+ title: def.title,
2208
+ description: def.description,
2209
+ trigger: def.trigger,
2210
+ icon: def.icon,
2211
+ fields: FIELDS_BY_EVENT[def.event] ?? ORDER_FIELDS,
2212
+ audit: !NOT_AUDITED.has(def.event)
2213
+ }));
2214
+ }
2215
+ var WHATSAPP_EVENTS = /* @__PURE__ */ new Set(["order_confirmation", "order_shipped", "abandoned_cart"]);
2216
+ var WHATSAPP_BODY = {
2217
+ order_confirmation: "Oi {{customer_name}}! Recebemos seu pedido #{{order_number}} \u2705\n\nTotal: {{order_total}}\n\nQualquer d\xFAvida \xE9 s\xF3 chamar aqui. \u2014 {{store_name}}",
2218
+ order_shipped: "Oi {{customer_name}}! Seu pedido #{{order_number}} saiu para entrega \u{1F69A}\n\nRastreio: {{tracking_code}}\n\n\u2014 {{store_name}}",
2219
+ abandoned_cart: "Oi {{customer_name}}! Vi que ficaram itens no seu carrinho \u{1F6D2}\n\nQuer que eu segure para voc\xEA?\n{{cart_url}}\n\n\u2014 {{store_name}}"
2220
+ };
2221
+ function buildShopMessageDefaults() {
2222
+ return SHOP_EMAIL_EVENTS.map((def) => {
2223
+ const whatsapp = WHATSAPP_EVENTS.has(def.event) ? WHATSAPP_BODY[def.event] : void 0;
2224
+ return {
2225
+ eventKey: `shop.${def.event}`,
2226
+ kind: def.kind,
2227
+ channels: whatsapp ? ["email", "whatsapp"].slice() : ["email"],
2228
+ defaults: {
2229
+ email: { subject: def.defaultSubject, body: def.defaultBody },
2230
+ ...whatsapp ? { whatsapp: { body: whatsapp } } : {}
2231
+ },
2232
+ // Only email inherits "on by default". WhatsApp bills per message and
2233
+ // needs an approved number — switching itself on would spend someone's
2234
+ // money unasked.
2235
+ defaultEnabled: { email: def.defaultEnabled }
2236
+ };
2237
+ });
2238
+ }
2239
+
2240
+ export { EMAIL_PLACEHOLDERS, EMAIL_PREVIEW_SAMPLE, SHOP_EMAIL_EVENTS, SupabaseShopProvider, applyDiscount, buildShopDomainEvents, buildShopMessageDefaults, checkInventory, computeCartTotals, createSupabaseShopProvider, documentDigits, documentKind, formatDocument, getCustomerOrders, getShopFunctionsUrl, isValidCnpj, isValidCpf, isValidDocument, listShippingLabelProviders, registerShippingLabelProvider, renderEmailTemplate, resetShippingLabelProviders, resolveEmailTemplates, resolveShippingLabelProvider, setShopFunctionsUrl, validateDiscount };
723
2241
  //# sourceMappingURL=index.js.map
724
2242
  //# sourceMappingURL=index.js.map