@fayz-ai/shop 0.10.0 → 0.12.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 (65) hide show
  1. package/dist/{chunk-IL6GA4ER.js → chunk-2WACO2TG.js} +3 -3
  2. package/dist/{chunk-IL6GA4ER.js.map → chunk-2WACO2TG.js.map} +1 -1
  3. package/dist/{chunk-RGTIJQJL.js → chunk-6BO2TMZ5.js} +3 -3
  4. package/dist/{chunk-RGTIJQJL.js.map → chunk-6BO2TMZ5.js.map} +1 -1
  5. package/dist/{chunk-XOP6SHPB.js → chunk-NWW3WJ7Y.js} +4 -10
  6. package/dist/chunk-NWW3WJ7Y.js.map +1 -0
  7. package/dist/{chunk-XERLK5LD.js → chunk-STRNMFC4.js} +28 -5
  8. package/dist/chunk-STRNMFC4.js.map +1 -0
  9. package/dist/{chunk-U3O2I3RM.js → chunk-U6HV3LVP.js} +385 -23
  10. package/dist/chunk-U6HV3LVP.js.map +1 -0
  11. package/dist/data/checkout-gateways.d.ts +28 -0
  12. package/dist/data/checkout-gateways.d.ts.map +1 -0
  13. package/dist/data/customer-search.d.ts +3 -0
  14. package/dist/data/customer-search.d.ts.map +1 -0
  15. package/dist/data/functions.d.ts +9 -0
  16. package/dist/data/functions.d.ts.map +1 -0
  17. package/dist/data/media.d.ts +37 -0
  18. package/dist/data/media.d.ts.map +1 -0
  19. package/dist/data/mock-provider.d.ts +40 -8
  20. package/dist/data/mock-provider.d.ts.map +1 -1
  21. package/dist/data/provider.d.ts +205 -7
  22. package/dist/data/provider.d.ts.map +1 -1
  23. package/dist/data/supabase-provider.d.ts +118 -5
  24. package/dist/data/supabase-provider.d.ts.map +1 -1
  25. package/dist/data/tables.d.ts +3 -0
  26. package/dist/data/tables.d.ts.map +1 -1
  27. package/dist/entities/index.d.ts +0 -1
  28. package/dist/entities/index.d.ts.map +1 -1
  29. package/dist/entities.js +2 -2
  30. package/dist/index.d.ts +16 -5
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +925 -60
  33. package/dist/index.js.map +1 -1
  34. package/dist/inventory/index.d.ts +2 -2
  35. package/dist/inventory/index.d.ts.map +1 -1
  36. package/dist/inventory/stock-types.d.ts +60 -1
  37. package/dist/inventory/stock-types.d.ts.map +1 -1
  38. package/dist/inventory/supabase-stock-store.d.ts +1 -0
  39. package/dist/inventory/supabase-stock-store.d.ts.map +1 -1
  40. package/dist/inventory.js +1 -1
  41. package/dist/lib/commerce.d.ts.map +1 -1
  42. package/dist/lib/document.d.ts +13 -0
  43. package/dist/lib/document.d.ts.map +1 -0
  44. package/dist/lib/variant-matrix.d.ts +120 -0
  45. package/dist/lib/variant-matrix.d.ts.map +1 -0
  46. package/dist/mock.js +2 -2
  47. package/dist/providers/checkout.d.ts +129 -0
  48. package/dist/providers/checkout.d.ts.map +1 -0
  49. package/dist/providers/index.d.ts +5 -0
  50. package/dist/providers/index.d.ts.map +1 -0
  51. package/dist/providers/shipping.d.ts +43 -0
  52. package/dist/providers/shipping.d.ts.map +1 -0
  53. package/dist/providers.js +70 -0
  54. package/dist/providers.js.map +1 -0
  55. package/dist/runtime.js +3 -3
  56. package/dist/shipping/label-provider.d.ts +43 -0
  57. package/dist/shipping/label-provider.d.ts.map +1 -0
  58. package/dist/types.d.ts +475 -0
  59. package/dist/types.d.ts.map +1 -1
  60. package/package.json +8 -4
  61. package/dist/chunk-U3O2I3RM.js.map +0 -1
  62. package/dist/chunk-XERLK5LD.js.map +0 -1
  63. package/dist/chunk-XOP6SHPB.js.map +0 -1
  64. package/dist/entities/sidecar.d.ts +0 -9
  65. package/dist/entities/sidecar.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,14 +1,15 @@
1
- import { T } from './chunk-XERLK5LD.js';
2
- export { DEFAULT_STOCK_TRACKING, MockStockStore, STOCK_TRACKING_MODES, StockMovementError, SupabaseStockStore, createMockStockStore, createSupabaseStockStore, listStockMovementKinds, listStockReasonCodes, mirroredInventoryCount, registerStockMovementKind, registerStockReasonCode, registerStockStore, replayStockLedger, resolveStockStore, validateStockMovement } from './chunk-XERLK5LD.js';
3
- import { getShopProvider } from './chunk-IL6GA4ER.js';
4
- export { getShopProvider, resetShopProvider, setShopProvider } from './chunk-IL6GA4ER.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
5
  import './chunk-FQPGE3YN.js';
6
- export { MockShopProvider, createMockShopProvider } from './chunk-U3O2I3RM.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';
7
8
  export { MOCK_CATEGORIES, MOCK_DISCOUNTS, MOCK_PRODUCTS, buildMockCatalog, svgPlaceholder } from './chunk-IKNGCRUM.js';
8
- import { createSupabaseEntityStore } from './chunk-RGTIJQJL.js';
9
+ import { createSupabaseEntityStore } from './chunk-6BO2TMZ5.js';
9
10
  import { getShopTenantId } from './chunk-KLDS7IPU.js';
10
11
  export { getShopTenantId, setShopTenantResolver } from './chunk-KLDS7IPU.js';
11
- import { resolveEntityStore, SIDECAR_TABLE, validateSidecarWrite } from './chunk-XOP6SHPB.js';
12
+ import { resolveEntityStore } from './chunk-NWW3WJ7Y.js';
12
13
  import './chunk-3GERSY5F.js';
13
14
  import { getSupabaseClientOptional } from '@fayz-ai/core';
14
15
 
@@ -19,6 +20,9 @@ function round2(n) {
19
20
  async function validateDiscount(input, provider = getShopProvider()) {
20
21
  const code = input.code.trim();
21
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
+ }
22
26
  const list = await provider.listDiscounts({ status: "active" });
23
27
  const discount = list.find((d) => (d.code ?? "").toUpperCase() === code.toUpperCase());
24
28
  if (!discount) return { valid: false, code: null, type: null, value: 0, reason: "not_found" };
@@ -72,7 +76,7 @@ async function computeCartTotals(input, provider = getShopProvider()) {
72
76
  let discountCode = null;
73
77
  if (input.discountCode) {
74
78
  const applied = applyDiscount(
75
- await validateDiscount({ code: input.discountCode, customerId: input.customerId }, provider),
79
+ await validateDiscount({ code: input.discountCode, subtotal, customerId: input.customerId }, provider),
76
80
  subtotal
77
81
  );
78
82
  discountTotal = applied.discountTotal;
@@ -86,6 +90,67 @@ async function computeCartTotals(input, provider = getShopProvider()) {
86
90
  async function getCustomerOrders(customerId, provider = getShopProvider()) {
87
91
  return provider.listOrders({ customerId, limit: 100 });
88
92
  }
93
+
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;
101
+ }
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;
125
+ try {
126
+ payload = await error?.context?.json?.();
127
+ } catch {
128
+ }
129
+ return { ok: false, status, payload: payload ?? { error: error.message } };
130
+ }
131
+
132
+ // src/data/checkout-gateways.ts
133
+ var GATEWAYS = Object.freeze({
134
+ abacatepay: Object.freeze({ open: "shop-checkout-open", settle: "shop-checkout-settle" }),
135
+ vindi: Object.freeze({ open: "vindi-checkout-open", settle: "vindi-checkout-settle" })
136
+ });
137
+ var CHARGEABLE_PROVIDERS = Object.freeze(Object.keys(GATEWAYS));
138
+ function isChargeableProvider(provider) {
139
+ return checkoutGateway(provider) !== null;
140
+ }
141
+ function checkoutGateway(provider) {
142
+ const slug = String(provider ?? "").trim();
143
+ if (!slug) return null;
144
+ return GATEWAYS[slug] ?? null;
145
+ }
146
+
147
+ // src/data/customer-search.ts
148
+ var COLUMNS = ["first_name", "last_name", "email", "phone"];
149
+ function customerSearchFilters(term) {
150
+ return String(term ?? "").trim().split(/\s+/u).map((word) => word.replace(/[%,()]/gu, "")).filter(Boolean).map((word) => COLUMNS.map((column) => `${column}.ilike.%${word}%`).join(","));
151
+ }
152
+
153
+ // src/data/supabase-provider.ts
89
154
  function getDb() {
90
155
  const supabase = getSupabaseClientOptional();
91
156
  if (!supabase) throw new Error("@fayz-ai/shop: Supabase client not initialized. Call setGlobalSupabaseClient() first.");
@@ -103,6 +168,95 @@ function toPostalDigits(value) {
103
168
  const digits = String(value ?? "").replace(/\D/g, "");
104
169
  return digits ? digits.padStart(8, "0").slice(0, 8) : void 0;
105
170
  }
171
+ function rowToVariant(row) {
172
+ return {
173
+ id: row.id,
174
+ productId: row.product_id,
175
+ options: row.options && typeof row.options === "object" ? row.options : {},
176
+ sku: row.sku ?? null,
177
+ // numeric(12,2) arrives as a string on the wire; Number() once, here.
178
+ price: row.price == null ? null : Number(row.price),
179
+ compareAtPrice: row.compare_at_price == null ? null : Number(row.compare_at_price),
180
+ inventoryCount: row.inventory_count ?? 0,
181
+ position: row.position ?? 0,
182
+ status: row.status ?? "active",
183
+ imageId: row.image_id ?? null,
184
+ isDefaultVariant: row.is_default_variant ?? false,
185
+ // Null inherits the product's, the way `price` has since 0028.
186
+ weight: numOrNull(row.weight),
187
+ weightUnit: row.weight_unit ?? null,
188
+ heightCm: numOrNull(row.height_cm),
189
+ widthCm: numOrNull(row.width_cm),
190
+ lengthCm: numOrNull(row.length_cm)
191
+ };
192
+ }
193
+ function variantPatch(input) {
194
+ const patch = {};
195
+ const map = {
196
+ sku: "sku",
197
+ price: "price",
198
+ compareAtPrice: "compare_at_price",
199
+ position: "position",
200
+ status: "status",
201
+ imageId: "image_id",
202
+ weight: "weight",
203
+ weightUnit: "weight_unit",
204
+ heightCm: "height_cm",
205
+ widthCm: "width_cm",
206
+ lengthCm: "length_cm"
207
+ };
208
+ for (const [key, column] of Object.entries(map)) {
209
+ const value = input[key];
210
+ if (value === void 0) continue;
211
+ patch[column] = key === "sku" && typeof value === "string" && value.trim() === "" ? null : value;
212
+ }
213
+ return patch;
214
+ }
215
+ function rowToStoreSettings(row) {
216
+ const bool = (v) => v == null ? void 0 : Boolean(v);
217
+ const methods = Array.isArray(row.payment_methods) && row.payment_methods.length > 0 ? row.payment_methods : void 0;
218
+ const fields = row.checkout_fields && typeof row.checkout_fields === "object" && Object.keys(row.checkout_fields).length > 0 ? row.checkout_fields : void 0;
219
+ return {
220
+ currency: row.currency ?? void 0,
221
+ locale: row.locale ?? void 0,
222
+ showOutOfStock: bool(row.show_out_of_stock),
223
+ requireSku: bool(row.require_sku),
224
+ stockOwnership: row.stock_ownership ?? void 0,
225
+ paymentMethods: methods,
226
+ paymentProvider: row.payment_provider ?? void 0,
227
+ guestCheckout: bool(row.guest_checkout),
228
+ requireConfirmedEmail: bool(row.require_confirmed_email),
229
+ marketingOptIn: bool(row.marketing_opt_in),
230
+ discountsEnabled: bool(row.discounts_enabled),
231
+ checkoutFields: fields,
232
+ autoFulfill: row.auto_fulfill ?? void 0,
233
+ notifyNewOrder: bool(row.notify_new_order),
234
+ notifyLowStock: bool(row.notify_low_stock),
235
+ handlingNote: row.handling_note ?? void 0,
236
+ carrierQuotes: bool(row.carrier_quotes)
237
+ };
238
+ }
239
+ function storeSettingsPayload(input) {
240
+ const payload = {};
241
+ if (input.currency !== void 0) payload.currency = input.currency;
242
+ if (input.locale !== void 0) payload.locale = input.locale;
243
+ if (input.showOutOfStock !== void 0) payload.show_out_of_stock = input.showOutOfStock;
244
+ if (input.requireSku !== void 0) payload.require_sku = input.requireSku;
245
+ if (input.stockOwnership !== void 0) payload.stock_ownership = input.stockOwnership;
246
+ if (input.paymentMethods !== void 0) payload.payment_methods = input.paymentMethods;
247
+ if (input.paymentProvider !== void 0) payload.payment_provider = input.paymentProvider ?? null;
248
+ if (input.guestCheckout !== void 0) payload.guest_checkout = input.guestCheckout;
249
+ if (input.requireConfirmedEmail !== void 0) payload.require_confirmed_email = input.requireConfirmedEmail;
250
+ if (input.marketingOptIn !== void 0) payload.marketing_opt_in = input.marketingOptIn;
251
+ if (input.discountsEnabled !== void 0) payload.discounts_enabled = input.discountsEnabled;
252
+ if (input.checkoutFields !== void 0) payload.checkout_fields = input.checkoutFields ?? {};
253
+ if (input.autoFulfill !== void 0) payload.auto_fulfill = input.autoFulfill;
254
+ if (input.notifyNewOrder !== void 0) payload.notify_new_order = input.notifyNewOrder;
255
+ if (input.notifyLowStock !== void 0) payload.notify_low_stock = input.notifyLowStock;
256
+ if (input.handlingNote !== void 0) payload.handling_note = input.handlingNote || null;
257
+ if (input.carrierQuotes !== void 0) payload.carrier_quotes = input.carrierQuotes;
258
+ return payload;
259
+ }
106
260
  function rowToShippingZone(row) {
107
261
  return {
108
262
  id: row.id,
@@ -116,9 +270,74 @@ function rowToShippingZone(row) {
116
270
  etaMinDays: row.eta_min_days ?? null,
117
271
  etaMaxDays: row.eta_max_days ?? null,
118
272
  active: row.active ?? true,
119
- sortOrder: row.sort_order ?? 0
273
+ sortOrder: row.sort_order ?? 0,
274
+ excluded: row.excluded ?? false,
275
+ profileId: row.profile_id ?? null
276
+ };
277
+ }
278
+ function rowToShippingProfile(row) {
279
+ return {
280
+ id: row.id,
281
+ tenantId: row.tenant_id,
282
+ name: row.name,
283
+ // undefined, never false: NULL means "inherit the store's decision", and a
284
+ // mapper that coalesced would turn every new profile into one that refuses
285
+ // the gateway.
286
+ carrierQuotes: row.carrier_quotes == null ? void 0 : Boolean(row.carrier_quotes),
287
+ isDefault: row.is_default ?? false
120
288
  };
121
289
  }
290
+ function shippingProfilePayload(input) {
291
+ const payload = {};
292
+ if (input.name !== void 0) payload.name = input.name;
293
+ if (input.carrierQuotes !== void 0) payload.carrier_quotes = input.carrierQuotes ?? null;
294
+ if (input.isDefault !== void 0) payload.is_default = input.isDefault;
295
+ return payload;
296
+ }
297
+ function toOriginPostalCode(raw) {
298
+ const digits = String(raw ?? "").replace(/\D/g, "");
299
+ if (!digits) return null;
300
+ if (digits.length !== 8) throw new Error("O CEP do local precisa ter 8 d\xEDgitos.");
301
+ return digits;
302
+ }
303
+ function toLocationCode(name) {
304
+ const slug = name.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
305
+ return slug || "local";
306
+ }
307
+ function locationWriteError(error) {
308
+ if (error?.code === "23505") return new Error("J\xE1 existe um local com esse nome.");
309
+ if (error?.code === "23503") {
310
+ return new Error(
311
+ "Este local j\xE1 tem movimenta\xE7\xF5es de estoque. Arquive-o em vez de excluir \u2014 o hist\xF3rico aponta para ele."
312
+ );
313
+ }
314
+ return new Error(error?.message ?? "N\xE3o foi poss\xEDvel salvar o local.");
315
+ }
316
+ function stockLocationPayload(input) {
317
+ const payload = {};
318
+ if (input.name !== void 0) payload.name = input.name;
319
+ if (input.code !== void 0) payload.code = input.code;
320
+ if (input.kind !== void 0) payload.kind = input.kind;
321
+ if (input.isSellable !== void 0) payload.is_sellable = input.isSellable;
322
+ if (input.isDefault !== void 0) payload.is_default = input.isDefault;
323
+ if (input.status !== void 0) payload.status = input.status;
324
+ if (input.postalCode !== void 0) payload.postal_code = toOriginPostalCode(input.postalCode);
325
+ if (input.city !== void 0) payload.city = input.city || null;
326
+ if (input.state !== void 0) payload.state = input.state || null;
327
+ if (input.street !== void 0) payload.street = input.street || null;
328
+ if (input.number !== void 0) payload.number = input.number || null;
329
+ if (input.district !== void 0) payload.district = input.district || null;
330
+ if (input.complement !== void 0) payload.complement = input.complement || null;
331
+ if (input.document !== void 0) payload.document = String(input.document ?? "").replace(/\D/gu, "") || null;
332
+ if (input.phone !== void 0) payload.phone = String(input.phone ?? "").replace(/\D/gu, "") || null;
333
+ if (input.email !== void 0) payload.email = input.email || null;
334
+ if (input.carrierAgencies !== void 0) payload.carrier_agencies = input.carrierAgencies ?? {};
335
+ if (input.shipsOnline !== void 0) payload.ships_online = input.shipsOnline;
336
+ if (input.fulfillmentProvider !== void 0) {
337
+ payload.fulfillment_provider = input.fulfillmentProvider || null;
338
+ }
339
+ return payload;
340
+ }
122
341
  function shippingZonePayload(input) {
123
342
  const payload = {};
124
343
  if (input.name !== void 0) payload.name = input.name;
@@ -131,6 +350,34 @@ function shippingZonePayload(input) {
131
350
  if (input.etaMaxDays !== void 0) payload.eta_max_days = input.etaMaxDays ?? null;
132
351
  if (input.active !== void 0) payload.active = input.active;
133
352
  if (input.sortOrder !== void 0) payload.sort_order = input.sortOrder;
353
+ if (input.excluded !== void 0) payload.excluded = input.excluded;
354
+ if (input.profileId !== void 0) payload.profile_id = input.profileId ?? null;
355
+ return payload;
356
+ }
357
+ function rowToShippingPackage(row) {
358
+ return {
359
+ id: row.id,
360
+ tenantId: row.tenant_id,
361
+ name: row.name,
362
+ kind: row.kind ?? "box",
363
+ heightCm: Number(row.height_cm ?? 0),
364
+ widthCm: Number(row.width_cm ?? 0),
365
+ lengthCm: Number(row.length_cm ?? 0),
366
+ emptyWeightGrams: Number(row.empty_weight_grams ?? 0),
367
+ isDefault: row.is_default ?? false,
368
+ active: row.active ?? true
369
+ };
370
+ }
371
+ function shippingPackagePayload(input) {
372
+ const payload = {};
373
+ if (input.name !== void 0) payload.name = input.name;
374
+ if (input.kind !== void 0) payload.kind = input.kind;
375
+ if (input.heightCm !== void 0) payload.height_cm = input.heightCm;
376
+ if (input.widthCm !== void 0) payload.width_cm = input.widthCm;
377
+ if (input.lengthCm !== void 0) payload.length_cm = input.lengthCm;
378
+ if (input.emptyWeightGrams !== void 0) payload.empty_weight_grams = input.emptyWeightGrams;
379
+ if (input.isDefault !== void 0) payload.is_default = input.isDefault;
380
+ if (input.active !== void 0) payload.active = input.active;
134
381
  return payload;
135
382
  }
136
383
  function rowToProduct(row) {
@@ -154,10 +401,21 @@ function rowToProduct(row) {
154
401
  isPhysical: row.is_physical ?? true,
155
402
  weight: row.weight,
156
403
  weightUnit: row.weight_unit ?? "kg",
404
+ heightCm: numOrNull(row.height_cm),
405
+ widthCm: numOrNull(row.width_cm),
406
+ lengthCm: numOrNull(row.length_cm),
407
+ packageId: row.package_id ?? null,
408
+ handling: Array.isArray(row.handling) ? row.handling : [],
409
+ shippingProfileId: row.shipping_profile_id ?? null,
157
410
  createdAt: row.created_at,
158
411
  updatedAt: row.updated_at
159
412
  };
160
413
  }
414
+ function numOrNull(value) {
415
+ if (value == null || value === "") return null;
416
+ const n = Number(value);
417
+ return Number.isFinite(n) ? n : null;
418
+ }
161
419
  function rowToImage(row) {
162
420
  return {
163
421
  id: row.id,
@@ -229,6 +487,7 @@ function rowToCustomer(row) {
229
487
  lastName: row.last_name ?? "",
230
488
  email: row.email,
231
489
  phone: row.phone,
490
+ document: row.document ?? null,
232
491
  notes: row.notes,
233
492
  ordersCount: row.orders_count ?? 0,
234
493
  totalSpent: row.total_spent ?? 0,
@@ -285,22 +544,13 @@ function segmentPayload(input) {
285
544
  return p;
286
545
  }
287
546
  var GIFT_CODE_ALPHABET = "ABCDEFGHJKLMNPQRTUVWXY2346789";
288
- var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "avif", "bmp"]);
289
- var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set(["mp4", "mov", "webm", "m4v", "avi", "mkv"]);
290
547
  function generateGiftCardCode() {
291
548
  const bytes = new Uint8Array(12);
292
549
  globalThis.crypto.getRandomValues(bytes);
293
550
  const chars = Array.from(bytes, (b) => GIFT_CODE_ALPHABET[b % GIFT_CODE_ALPHABET.length]);
294
551
  return `${chars.slice(0, 4).join("")}-${chars.slice(4, 8).join("")}-${chars.slice(8, 12).join("")}`;
295
552
  }
296
- function mediaKindOf(name, mime) {
297
- if (mime?.startsWith("image/")) return "image";
298
- if (mime?.startsWith("video/")) return "video";
299
- const ext = (name.split(".").pop() ?? "").toLowerCase();
300
- if (IMAGE_EXTENSIONS.has(ext)) return "image";
301
- if (VIDEO_EXTENSIONS.has(ext)) return "video";
302
- return "document";
303
- }
553
+ var LIBRARY_FOLDER = "library";
304
554
  function nullableNumber(value) {
305
555
  if (value === null || value === void 0 || value === "") return null;
306
556
  const n = Number(value);
@@ -418,11 +668,13 @@ var SupabaseShopProvider = class {
418
668
  * RLS on both tables already restricts anon reads to active variants of
419
669
  * publicly readable products; the status filter here keeps the admin path
420
670
  * (authenticated tenant member, who CAN see archived rows) consistent. */
421
- async getProductVariants(productId) {
671
+ async getProductVariants(productId, opts) {
422
672
  const db = getDb();
673
+ let variantQuery = db.from(T.productVariants).select("*").eq("product_id", productId);
674
+ if (!opts?.includeArchived) variantQuery = variantQuery.eq("status", "active");
423
675
  const [optionsRes, variantsRes] = await Promise.all([
424
676
  db.from(T.productOptions).select("*").eq("product_id", productId).order("position", { ascending: true }),
425
- db.from(T.productVariants).select("*").eq("product_id", productId).eq("status", "active").order("position", { ascending: true })
677
+ variantQuery.order("position", { ascending: true })
426
678
  ]);
427
679
  if (optionsRes.error) throw optionsRes.error;
428
680
  if (variantsRes.error) throw variantsRes.error;
@@ -432,19 +684,102 @@ var SupabaseShopProvider = class {
432
684
  values: Array.isArray(row.values) ? row.values.map(String) : [],
433
685
  position: row.position ?? 0
434
686
  }));
435
- const variants = (variantsRes.data ?? []).map((row) => ({
687
+ const variants = (variantsRes.data ?? []).map(rowToVariant);
688
+ return { options, variants };
689
+ }
690
+ // --- The variant admin ------------------------------------------------------
691
+ async saveProductOptions(productId, input) {
692
+ const db = getDb();
693
+ const tenantId = getTenantId();
694
+ const clean = normalizeVariantAxes(input);
695
+ const refusal = validateVariantAxes(clean);
696
+ if (refusal) throw new Error(refusal);
697
+ const { data: existing, error: readError } = await db.from(T.productOptions).select("*").eq("product_id", productId);
698
+ if (readError) throw readError;
699
+ const byName = new Map((existing ?? []).map((row) => [row.name, row]));
700
+ const wanted = new Set(clean.map((axis) => axis.name));
701
+ const stale = (existing ?? []).filter((row) => !wanted.has(row.name)).map((row) => row.id);
702
+ if (stale.length > 0) {
703
+ const { error: error2 } = await db.from(T.productOptions).delete().in("id", stale);
704
+ if (error2) throw error2;
705
+ }
706
+ for (const [position, axis] of clean.entries()) {
707
+ const row = byName.get(axis.name);
708
+ if (row) {
709
+ const { error: error2 } = await db.from(T.productOptions).update({ values: axis.values, position }).eq("id", row.id);
710
+ if (error2) throw error2;
711
+ } else {
712
+ const { error: error2 } = await db.from(T.productOptions).insert({
713
+ product_id: productId,
714
+ tenant_id: tenantId,
715
+ name: axis.name,
716
+ values: axis.values,
717
+ position
718
+ });
719
+ if (error2) throw error2;
720
+ }
721
+ }
722
+ const { data, error } = await db.from(T.productOptions).select("*").eq("product_id", productId).order("position", { ascending: true });
723
+ if (error) throw error;
724
+ return (data ?? []).map((row) => ({
436
725
  id: row.id,
437
- productId: row.product_id,
438
- options: row.options && typeof row.options === "object" ? row.options : {},
439
- sku: row.sku ?? null,
440
- price: row.price == null ? null : Number(row.price),
441
- compareAtPrice: row.compare_at_price == null ? null : Number(row.compare_at_price),
442
- inventoryCount: row.inventory_count ?? 0,
443
- position: row.position ?? 0,
444
- status: row.status ?? "active",
445
- imageId: row.image_id ?? null
726
+ name: row.name,
727
+ values: Array.isArray(row.values) ? row.values.map(String) : [],
728
+ position: row.position ?? 0
446
729
  }));
447
- return { options, variants };
730
+ }
731
+ async createProductVariant(productId, input) {
732
+ const db = getDb();
733
+ const tenantId = getTenantId();
734
+ if (Object.keys(input.options).length === 0) {
735
+ throw new Error("Uma variante sem op\xE7\xF5es \xE9 a variante padr\xE3o (0037), criada pelo banco. Declare ao menos um eixo.");
736
+ }
737
+ const { data: existing, error: readError } = await db.from(T.productVariants).select("*").eq("product_id", productId);
738
+ if (readError) throw readError;
739
+ const wantedKey = variantOptionsKey(input.options);
740
+ const match = (existing ?? []).find((row) => variantOptionsKey(row.options ?? {}) === wantedKey);
741
+ if (match) return this.updateProductVariant(match.id, { ...input, status: input.status ?? "active" });
742
+ const { data, error } = await db.from(T.productVariants).insert({
743
+ product_id: productId,
744
+ tenant_id: tenantId,
745
+ options: input.options,
746
+ ...variantPatch(input),
747
+ // NOT copied from the product: 0028 defines a null price as "inherit the
748
+ // product's", and 0037's header spells out why a copy is a bug — the
749
+ // storefront would show the new product price and the server charge the
750
+ // frozen variant one.
751
+ status: input.status ?? "active"
752
+ }).select("*").single();
753
+ if (error) throw error;
754
+ return rowToVariant(data);
755
+ }
756
+ async updateProductVariant(variantId, input) {
757
+ const db = getDb();
758
+ const { data, error } = await db.from(T.productVariants).update(variantPatch(input)).eq("id", variantId).select("*").single();
759
+ if (error) throw error;
760
+ return rowToVariant(data);
761
+ }
762
+ async archiveProductVariant(variantId) {
763
+ const db = getDb();
764
+ const { error } = await db.from(T.productVariants).update({ status: "archived" }).eq("id", variantId);
765
+ if (error) throw error;
766
+ }
767
+ async setVariantStock(variantId, count, note) {
768
+ const target = Math.max(0, Math.trunc(count));
769
+ const store = resolveStockStore(getTenantId() ?? "", () => createSupabaseStockStore());
770
+ const current = await store.getAvailable(variantId);
771
+ const delta = target - current;
772
+ if (delta === 0) return current;
773
+ const priorMovements = await store.listMovements({ variantId, limit: 1 });
774
+ const opening = priorMovements.length === 0 && delta > 0;
775
+ await store.recordMovement({
776
+ variantId,
777
+ kind: opening ? "receipt" : "adjustment",
778
+ reasonCode: opening ? OPENING_BALANCE_REASON : "correction",
779
+ quantity: delta,
780
+ note: note ?? "Estoque definido na matriz de variantes."
781
+ });
782
+ return target;
448
783
  }
449
784
  async createProduct(input) {
450
785
  const db = getDb();
@@ -465,7 +800,17 @@ var SupabaseShopProvider = class {
465
800
  category_id: input.categoryId ?? null,
466
801
  is_physical: input.isPhysical ?? true,
467
802
  weight: input.weight ?? null,
468
- weight_unit: input.weightUnit ?? "kg"
803
+ weight_unit: input.weightUnit ?? "kg",
804
+ height_cm: input.heightCm ?? null,
805
+ width_cm: input.widthCm ?? null,
806
+ length_cm: input.lengthCm ?? null,
807
+ // Null = a padrão da loja (0047). É o estado normal, não um campo vazio.
808
+ package_id: input.packageId ?? null,
809
+ // NULL and not []: "nothing declared" and "declared as needing nothing"
810
+ // are the same fact about a box, and one representation of it keeps the
811
+ // `&&` overlap test in the 0048 trigger reading one way.
812
+ handling: input.handling?.length ? input.handling : null,
813
+ shipping_profile_id: input.shippingProfileId ?? null
469
814
  }).select(`*, images:${T.productImages}(*)`).single();
470
815
  if (error) throw error;
471
816
  return rowToProduct(data);
@@ -488,6 +833,13 @@ var SupabaseShopProvider = class {
488
833
  if (input.categoryId !== void 0) updates.category_id = input.categoryId;
489
834
  if (input.isPhysical !== void 0) updates.is_physical = input.isPhysical;
490
835
  if (input.weight !== void 0) updates.weight = input.weight;
836
+ if (input.weightUnit !== void 0) updates.weight_unit = input.weightUnit;
837
+ if (input.heightCm !== void 0) updates.height_cm = input.heightCm;
838
+ if (input.widthCm !== void 0) updates.width_cm = input.widthCm;
839
+ if (input.lengthCm !== void 0) updates.length_cm = input.lengthCm;
840
+ if (input.packageId !== void 0) updates.package_id = input.packageId || null;
841
+ if (input.handling !== void 0) updates.handling = input.handling.length ? input.handling : null;
842
+ if (input.shippingProfileId !== void 0) updates.shipping_profile_id = input.shippingProfileId ?? null;
491
843
  const { data, error } = await db.from(T.products).update(updates).eq("id", id).select(`*, images:${T.productImages}(*)`).single();
492
844
  if (error) throw error;
493
845
  return rowToProduct(data);
@@ -532,9 +884,43 @@ var SupabaseShopProvider = class {
532
884
  if (error) throw error;
533
885
  return rowToImage(data);
534
886
  }
887
+ async replaceProductImage(imageId, file) {
888
+ const db = getDb();
889
+ const tenantId = getTenantId();
890
+ const { data: row, error: readErr } = await db.from(T.productImages).select("id,url,product_id").eq("id", imageId).single();
891
+ if (readErr) throw readErr;
892
+ const path = `${tenantId}/products/${row.product_id}/${Date.now()}-${file.name}`;
893
+ const bucket = db.storage.from(T.mediaBucket);
894
+ const { error: uploadErr } = await bucket.upload(path, file);
895
+ if (uploadErr) throw uploadErr;
896
+ const { data: { publicUrl } } = bucket.getPublicUrl(path);
897
+ const { data, error } = await db.from(T.productImages).update({ url: publicUrl }).eq("id", imageId).select().single();
898
+ if (error) throw error;
899
+ await this.removeUnreferencedObject(row.url);
900
+ return rowToImage(data);
901
+ }
535
902
  async deleteProductImage(imageId) {
536
- const { error } = await getDb().from(T.productImages).delete().eq("id", imageId);
903
+ const db = getDb();
904
+ const { data: row } = await db.from(T.productImages).select("url").eq("id", imageId).maybeSingle();
905
+ const { error } = await db.from(T.productImages).delete().eq("id", imageId);
537
906
  if (error) throw error;
907
+ if (row?.url) await this.removeUnreferencedObject(row.url);
908
+ }
909
+ /**
910
+ * Drop a stored file once nothing points at it anymore.
911
+ *
912
+ * The row was the only thing keeping the object reachable, so leaving it
913
+ * behind is a file nobody can see and nobody can delete — and the bill for it
914
+ * arrives all the same. Two rows can share one URL (a duplicated product), and
915
+ * the survivor has to keep working, so the count decides.
916
+ */
917
+ async removeUnreferencedObject(url) {
918
+ const path = url ? storagePathOf(url) : null;
919
+ if (!path) return;
920
+ const db = getDb();
921
+ const { count } = await db.from(T.productImages).select("id", { count: "exact", head: true }).eq("url", url);
922
+ if (count) return;
923
+ await db.storage.from(T.mediaBucket).remove([path]);
538
924
  }
539
925
  // ---------------------------------------------------------------------------
540
926
  // Categories
@@ -643,10 +1029,188 @@ var SupabaseShopProvider = class {
643
1029
  if (error) throw error;
644
1030
  return rowToShippingZone(data);
645
1031
  }
1032
+ // -------------------------------------------------------------------------
1033
+ // Embalagens (0046)
1034
+ //
1035
+ // `is_default` has a partial unique index behind it, so promoting a box has
1036
+ // to demote the previous one in the same round trip — otherwise the second
1037
+ // default is refused by the database and the merchant sees a save that
1038
+ // failed for a reason the screen cannot explain.
1039
+ // -------------------------------------------------------------------------
1040
+ async listShippingPackages() {
1041
+ const tenantId = getTenantId();
1042
+ const { data, error } = await getDb().from(T.shippingPackages).select("*").eq("tenant_id", tenantId).order("is_default", { ascending: false }).order("name", { ascending: true });
1043
+ if (error) throw error;
1044
+ return (data ?? []).map(rowToShippingPackage);
1045
+ }
1046
+ async createShippingPackage(input) {
1047
+ const tenantId = getTenantId();
1048
+ if (!tenantId) throw new Error("@fayz-ai/shop: createShippingPackage requires a tenant id.");
1049
+ if (input.isDefault) await this.clearDefaultPackage(tenantId);
1050
+ const { data, error } = await getDb().from(T.shippingPackages).insert({ tenant_id: tenantId, ...shippingPackagePayload(input) }).select().single();
1051
+ if (error) throw error;
1052
+ return rowToShippingPackage(data);
1053
+ }
1054
+ async updateShippingPackage(id, input) {
1055
+ const tenantId = getTenantId();
1056
+ if (input.isDefault && tenantId) await this.clearDefaultPackage(tenantId, id);
1057
+ const { data, error } = await getDb().from(T.shippingPackages).update(shippingPackagePayload(input)).eq("id", id).select().single();
1058
+ if (error) throw error;
1059
+ return rowToShippingPackage(data);
1060
+ }
1061
+ async deleteShippingPackage(id) {
1062
+ const { error } = await getDb().from(T.shippingPackages).delete().eq("id", id);
1063
+ if (error) throw error;
1064
+ }
1065
+ async clearDefaultPackage(tenantId, exceptId) {
1066
+ let query = getDb().from(T.shippingPackages).update({ is_default: false }).eq("tenant_id", tenantId).eq("is_default", true);
1067
+ if (exceptId) query = query.neq("id", exceptId);
1068
+ const { error } = await query;
1069
+ if (error) throw error;
1070
+ }
646
1071
  async deleteShippingZone(id) {
647
1072
  const { error } = await getDb().from(T.shippingZones).delete().eq("id", id);
648
1073
  if (error) throw error;
649
1074
  }
1075
+ // -------------------------------------------------------------------------
1076
+ // Store policy + delivery fallback (0044b / 0015)
1077
+ //
1078
+ // Both are ONE ROW PER TENANT, and both are upserted rather than
1079
+ // insert-or-update: a store that never opened these screens has no row, and
1080
+ // making the first save conditional on discovering that is two round trips to
1081
+ // learn something the primary key already knows.
1082
+ // -------------------------------------------------------------------------
1083
+ async getShippingSettings() {
1084
+ const tenantId = getTenantId();
1085
+ const { data, error } = await getDb().from(T.shippingSettings).select("flat_rate, free_above, currency").eq("tenant_id", tenantId).maybeSingle();
1086
+ if (error) throw error;
1087
+ if (!data) return null;
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
+ async updateShippingSettings(input) {
1095
+ const tenantId = getTenantId();
1096
+ if (!tenantId) throw new Error("@fayz-ai/shop: updateShippingSettings requires a tenant id.");
1097
+ const payload = { tenant_id: tenantId };
1098
+ if (input.flatRate !== void 0) payload.flat_rate = input.flatRate;
1099
+ if (input.freeAbove !== void 0) payload.free_above = input.freeAbove;
1100
+ if (input.currency !== void 0) payload.currency = input.currency;
1101
+ const { data, error } = await getDb().from(T.shippingSettings).upsert(payload, { onConflict: "tenant_id" }).select("flat_rate, free_above, currency").single();
1102
+ if (error) throw error;
1103
+ return {
1104
+ flatRate: Number(data.flat_rate ?? 0),
1105
+ freeAbove: data.free_above == null ? null : Number(data.free_above),
1106
+ currency: data.currency ?? "BRL"
1107
+ };
1108
+ }
1109
+ /**
1110
+ * "Does this store deliver here?" — asked with the address, before the card.
1111
+ *
1112
+ * Degrades to "yes, and not our own zone" on any failure. A store that cannot
1113
+ * reach the check must not stop selling: the order itself is still refused by
1114
+ * the 0048 trigger if the region really is excluded, so the worst case is the
1115
+ * shopper learning it one screen later instead of never selling at all.
1116
+ */
1117
+ async listShippingProfiles() {
1118
+ const { data, error } = await getDb().from(T.shippingProfiles).select("*").eq("tenant_id", getTenantId()).order("is_default", { ascending: false }).order("name");
1119
+ if (error) throw error;
1120
+ return (data ?? []).map(rowToShippingProfile);
1121
+ }
1122
+ async createShippingProfile(input) {
1123
+ const tenantId = getTenantId();
1124
+ if (!tenantId) throw new Error("@fayz-ai/shop: createShippingProfile requires a tenant id.");
1125
+ const { data, error } = await getDb().from(T.shippingProfiles).insert({ tenant_id: tenantId, ...shippingProfilePayload(input) }).select("*").single();
1126
+ if (error) throw error;
1127
+ return rowToShippingProfile(data);
1128
+ }
1129
+ async updateShippingProfile(id, input) {
1130
+ const { data, error } = await getDb().from(T.shippingProfiles).update(shippingProfilePayload(input)).eq("id", id).select("*").single();
1131
+ if (error) throw error;
1132
+ return rowToShippingProfile(data);
1133
+ }
1134
+ async deleteShippingProfile(id) {
1135
+ const { error } = await getDb().from(T.shippingProfiles).delete().eq("id", id);
1136
+ if (error) throw error;
1137
+ }
1138
+ // Places the store ships from (0058). Default first, then by name — the same
1139
+ // order the ledger's listLocations() uses, so the two screens agree.
1140
+ async listStockLocations() {
1141
+ const { data, error } = await getDb().from(T.stockLocations).select("*").eq("tenant_id", getTenantId()).order("is_default", { ascending: false }).order("name");
1142
+ if (error) throw error;
1143
+ return (data ?? []).map(rowToStockLocation);
1144
+ }
1145
+ async createStockLocation(input) {
1146
+ const tenantId = getTenantId();
1147
+ if (!tenantId) throw new Error("@fayz-ai/shop: createStockLocation requires a tenant id.");
1148
+ if (input.isDefault) await this.clearDefaultStockLocation(tenantId);
1149
+ const payload = stockLocationPayload(input);
1150
+ if (payload.code === void 0) payload.code = toLocationCode(input.name);
1151
+ const { data, error } = await getDb().from(T.stockLocations).insert({ tenant_id: tenantId, ...payload }).select("*").single();
1152
+ if (error) throw locationWriteError(error);
1153
+ return rowToStockLocation(data);
1154
+ }
1155
+ async updateStockLocation(id, input) {
1156
+ const tenantId = getTenantId();
1157
+ if (input.isDefault && tenantId) await this.clearDefaultStockLocation(tenantId, id);
1158
+ const { data, error } = await getDb().from(T.stockLocations).update(stockLocationPayload(input)).eq("id", id).select("*").single();
1159
+ if (error) throw locationWriteError(error);
1160
+ return rowToStockLocation(data);
1161
+ }
1162
+ async deleteStockLocation(id) {
1163
+ const { error } = await getDb().from(T.stockLocations).delete().eq("id", id);
1164
+ if (error) throw locationWriteError(error);
1165
+ }
1166
+ async clearDefaultStockLocation(tenantId, exceptId) {
1167
+ let query = getDb().from(T.stockLocations).update({ is_default: false }).eq("tenant_id", tenantId).eq("is_default", true);
1168
+ if (exceptId) query = query.neq("id", exceptId);
1169
+ const { error } = await query;
1170
+ if (error) throw error;
1171
+ }
1172
+ async checkDelivery(postalCode) {
1173
+ const tenantId = getTenantId();
1174
+ const digits = String(postalCode ?? "").replace(/\D/g, "");
1175
+ if (!tenantId || digits.length !== 8) return { refused: false, ownZone: false };
1176
+ const { data, error } = await getDb().rpc("shop_delivery_check", {
1177
+ p_tenant_id: tenantId,
1178
+ p_postal_code: digits
1179
+ });
1180
+ if (error) return { refused: false, ownZone: false };
1181
+ const row = Array.isArray(data) ? data[0] : data;
1182
+ return {
1183
+ refused: Boolean(row?.refused),
1184
+ refusedZone: row?.refused_zone ?? null,
1185
+ ownZone: Boolean(row?.own_zone)
1186
+ };
1187
+ }
1188
+ async getStoreSettings() {
1189
+ const tenantId = getTenantId();
1190
+ const { data, error } = await getDb().from(T.settings).select("*").eq("tenant_id", tenantId).maybeSingle();
1191
+ if (error) throw error;
1192
+ return data ? rowToStoreSettings(data) : null;
1193
+ }
1194
+ async updateStoreSettings(input) {
1195
+ const tenantId = getTenantId();
1196
+ if (!tenantId) throw new Error("@fayz-ai/shop: updateStoreSettings requires a tenant id.");
1197
+ const { data, error } = await getDb().from(T.settings).upsert({ tenant_id: tenantId, ...storeSettingsPayload(input) }, { onConflict: "tenant_id" }).select("*").single();
1198
+ if (error) throw error;
1199
+ return rowToStoreSettings(data);
1200
+ }
1201
+ /**
1202
+ * The shopper's read. Through the RPC, not the table: the table is closed to
1203
+ * anon on purpose (0029) and shop_store_settings() is the narrow window —
1204
+ * storefront fields only, and only for a published store.
1205
+ */
1206
+ async getPublicStoreSettings() {
1207
+ const tenantId = getTenantId();
1208
+ if (!tenantId) return null;
1209
+ const { data, error } = await getDb().rpc("shop_store_settings", { p_tenant_id: tenantId });
1210
+ if (error) throw error;
1211
+ const row = Array.isArray(data) ? data[0] : data;
1212
+ return row ? rowToStoreSettings(row) : null;
1213
+ }
650
1214
  /**
651
1215
  * Settle a pending order: the merchant confirming that the money arrived.
652
1216
  *
@@ -664,6 +1228,227 @@ var SupabaseShopProvider = class {
664
1228
  if (error) throw error;
665
1229
  return this.getOrder(id);
666
1230
  }
1231
+ // ---------------------------------------------------------------------------
1232
+ // Carrier labels. Everything goes through the function: the token never
1233
+ // reaches the browser, and `buy` is the one call that spends money.
1234
+ // ---------------------------------------------------------------------------
1235
+ async labelCall(orderId, action, extra = {}) {
1236
+ const tenantId = getTenantId();
1237
+ if (!tenantId) throw new Error("@fayz-ai/shop: shipping labels require a tenant id.");
1238
+ const { ok, payload } = await callShopFunction(getDb(), "shop-shipping-label", {
1239
+ tenantId,
1240
+ orderId,
1241
+ action,
1242
+ ...extra
1243
+ });
1244
+ if (!ok) throw new Error(payload?.error ?? "A transportadora n\xE3o respondeu agora.");
1245
+ return payload;
1246
+ }
1247
+ async quoteShippingLabel(orderId, carrier) {
1248
+ const payload = await this.labelCall(orderId, "quote", carrier ? { carrier } : {});
1249
+ return {
1250
+ options: (payload?.options ?? []).map((option) => ({
1251
+ serviceId: Number(option.service_id ?? option.serviceId),
1252
+ name: String(option.name ?? "Entrega"),
1253
+ carrier: String(option.carrier ?? ""),
1254
+ price: Number(option.price ?? 0),
1255
+ etaMinDays: option.eta_min_days ?? null,
1256
+ etaMaxDays: option.eta_max_days ?? null
1257
+ })),
1258
+ label: payload?.label ? {
1259
+ labelId: String(payload.label.external_id),
1260
+ labelUrl: payload.label.url ?? null,
1261
+ status: payload.label.status ?? null,
1262
+ tracking: payload.label.payload?.tracking ?? null
1263
+ } : null
1264
+ };
1265
+ }
1266
+ /** Spends the merchant's balance at the carrier. Never call it on a whim. */
1267
+ async buyShippingLabel(orderId, serviceId, carrier) {
1268
+ const payload = await this.labelCall(orderId, "buy", { serviceId, ...carrier ? { carrier } : {} });
1269
+ return {
1270
+ labelId: String(payload?.labelId ?? ""),
1271
+ labelUrl: payload?.labelUrl ?? null,
1272
+ status: "bought",
1273
+ tracking: payload?.tracking ?? null
1274
+ };
1275
+ }
1276
+ async printShippingLabel(orderId, carrier) {
1277
+ const payload = await this.labelCall(orderId, "print", carrier ? { carrier } : {});
1278
+ return payload?.labelUrl ?? null;
1279
+ }
1280
+ /** Drop-off points near a place. Store configuration — no order involved. */
1281
+ async listShippingAgencies(query) {
1282
+ const tenantId = getTenantId();
1283
+ if (!tenantId) return [];
1284
+ const { ok, payload } = await callShopFunction(getDb(), "shop-shipping-label", {
1285
+ tenantId,
1286
+ action: "agencies",
1287
+ state: query.state ?? "",
1288
+ city: query.city ?? "",
1289
+ ...query.carrier ? { carrier: query.carrier } : {}
1290
+ });
1291
+ if (!ok) throw new Error(payload?.error ?? "A transportadora n\xE3o respondeu agora.");
1292
+ return payload?.agencies ?? [];
1293
+ }
1294
+ async trackShippingLabel(orderId, carrier) {
1295
+ const payload = await this.labelCall(orderId, "track", carrier ? { carrier } : {});
1296
+ return payload?.tracking ?? null;
1297
+ }
1298
+ /**
1299
+ * The customer's address book, for the console. `shop_place_order` files a
1300
+ * new one here on every delivered order, and until now only the storefront
1301
+ * read them back — so a repeat customer had their address retyped by hand.
1302
+ */
1303
+ async listCustomerAddresses(customerId) {
1304
+ if (!asUuid(customerId)) return [];
1305
+ 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 });
1306
+ if (error) return [];
1307
+ return (data ?? []).map((row) => ({
1308
+ id: row.id,
1309
+ label: row.label ?? void 0,
1310
+ recipient: row.recipient ?? void 0,
1311
+ phone: row.phone ?? void 0,
1312
+ postalCode: row.postal_code ?? "",
1313
+ street: row.street ?? "",
1314
+ number: row.number ?? void 0,
1315
+ complement: row.complement ?? void 0,
1316
+ district: row.district ?? void 0,
1317
+ city: row.city ?? "",
1318
+ state: row.state ?? "",
1319
+ country: row.country ?? void 0,
1320
+ isDefault: row.is_default ?? false
1321
+ }));
1322
+ }
1323
+ /**
1324
+ * What delivery costs to a postcode — the same answer the shopper gets.
1325
+ *
1326
+ * The console needs it to quote a price out loud while the customer is on the
1327
+ * line. Carrier first (when the merchant did not turn gateway quotes off),
1328
+ * zones as the fallback, exactly as the storefront resolves it. Advisory
1329
+ * either way: shop_place_order requotes and ignores whatever a client sends.
1330
+ */
1331
+ async quoteShipping(postalCode, subtotal) {
1332
+ const digits = toPostalDigits(postalCode);
1333
+ if (!digits) return [];
1334
+ const tenantId = getTenantId();
1335
+ if (!tenantId) return [];
1336
+ const settings = await this.getStoreSettings().catch(() => null);
1337
+ if (settings?.carrierQuotes !== false) {
1338
+ try {
1339
+ const { ok, payload } = await callShopFunction(getDb(), "shop-shipping-quote", {
1340
+ tenantId,
1341
+ postalCode: digits,
1342
+ subtotal
1343
+ });
1344
+ const rows = ok ? payload?.options ?? [] : [];
1345
+ if (rows.length > 0) {
1346
+ return rows.map((row) => ({
1347
+ zoneId: String(row.service_id ?? ""),
1348
+ name: String(row.name ?? "Entrega"),
1349
+ carrier: row.carrier ?? void 0,
1350
+ rate: Number(row.price ?? 0),
1351
+ baseRate: Number(row.price ?? 0),
1352
+ freeAbove: null,
1353
+ etaMinDays: row.eta_min_days ?? void 0,
1354
+ etaMaxDays: row.eta_max_days ?? void 0,
1355
+ free: Number(row.price ?? 0) === 0
1356
+ }));
1357
+ }
1358
+ } catch {
1359
+ }
1360
+ }
1361
+ const { data, error } = await getDb().rpc("shop_quote_shipping", {
1362
+ p_tenant_id: tenantId,
1363
+ p_postal_code: digits,
1364
+ p_subtotal: subtotal
1365
+ });
1366
+ if (error) return [];
1367
+ return (data ?? []).map((row) => ({
1368
+ zoneId: row.zone_id,
1369
+ name: row.name,
1370
+ carrier: row.carrier ?? void 0,
1371
+ rate: Number(row.rate ?? 0),
1372
+ baseRate: Number(row.base_rate ?? row.rate ?? 0),
1373
+ freeAbove: row.free_above == null ? null : Number(row.free_above),
1374
+ etaMinDays: row.eta_min_days ?? void 0,
1375
+ etaMaxDays: row.eta_max_days ?? void 0,
1376
+ free: Boolean(row.free)
1377
+ }));
1378
+ }
1379
+ /**
1380
+ * Charge a cart that is not an order yet — the phone sale, from the counter's
1381
+ * side. Same door the storefront uses, on purpose: `shop_price_cart` inside
1382
+ * the function reprices everything, so the console sends products and
1383
+ * quantities and never money. The order is born in `settleCheckoutSession`,
1384
+ * once the acquirer says the Pix landed, so an abandoned charge leaves no
1385
+ * ghost order holding stock.
1386
+ */
1387
+ async openCheckoutSession(input) {
1388
+ const tenantId = getTenantId();
1389
+ if (!tenantId) throw new Error("@fayz-ai/shop: openCheckoutSession requires a tenant id.");
1390
+ const provider = String(input.provider ?? "").trim() || "abacatepay";
1391
+ const gateway = checkoutGateway(provider);
1392
+ if (!gateway) {
1393
+ throw new Error("Esta loja ainda n\xE3o tem um meio de cobran\xE7a conectado.");
1394
+ }
1395
+ const { ok, status, payload } = await callShopFunction(getDb(), gateway.open, {
1396
+ tenantId,
1397
+ items: input.items.map((item) => ({
1398
+ productId: item.productId,
1399
+ optionsLabel: item.optionsLabel ?? null,
1400
+ quantity: item.quantity
1401
+ })),
1402
+ customer: input.customer,
1403
+ shippingAddress: input.shippingAddress ?? null,
1404
+ discountCode: input.discountCode ?? null
1405
+ });
1406
+ if (!ok || !payload?.sessionId) {
1407
+ 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."));
1408
+ }
1409
+ return {
1410
+ sessionId: payload.sessionId,
1411
+ total: Number(payload.total ?? 0),
1412
+ state: "open",
1413
+ provider,
1414
+ charge: payload.brCode ? {
1415
+ emv: payload.brCode,
1416
+ // AbacatePay embeds the `data:` prefix; the type promises bare
1417
+ // base64. Left in, the view prefixes it again and the QR is a
1418
+ // silently broken <img>.
1419
+ //
1420
+ // Vindi sends no base64 at all — its `qrCodeUrl` is a URL to an SVG,
1421
+ // which cannot be squeezed into this field without lying about what
1422
+ // it is. The buyer still gets the copy-and-paste code, which is what
1423
+ // actually pays; a rendered QR for Vindi waits for the payment view
1424
+ // to learn the second shape.
1425
+ qrcodeBase64: String(payload.brCodeBase64 ?? "").replace(/^data:image\/\w+;base64,/u, "") || null,
1426
+ expiresAt: payload.expiresAt ?? ""
1427
+ } : null
1428
+ };
1429
+ }
1430
+ /**
1431
+ * Ask whether the money landed. Runs on a timer, so a network blip must read
1432
+ * as "still waiting" (`unknown`) and never as a refused payment.
1433
+ *
1434
+ * `provider` names which gateway opened the session — handed back by
1435
+ * `openCheckoutSession` rather than re-read from settings, because a merchant
1436
+ * who switches acquirer mid-checkout would otherwise have the new gateway
1437
+ * asked about a transaction that lives in the old one.
1438
+ */
1439
+ async settleCheckoutSession(sessionId, provider) {
1440
+ const tenantId = getTenantId();
1441
+ if (!tenantId) return { state: "unknown" };
1442
+ const gateway = checkoutGateway(String(provider ?? "").trim() || "abacatepay");
1443
+ if (!gateway) return { state: "unknown" };
1444
+ try {
1445
+ const { ok, payload } = await callShopFunction(getDb(), gateway.settle, { tenantId, sessionId });
1446
+ if (!ok || !payload?.state) return { state: "unknown" };
1447
+ return { state: payload.state, orderId: payload.orderId ?? null, reason: payload.reason ?? null };
1448
+ } catch {
1449
+ return { state: "unknown" };
1450
+ }
1451
+ }
667
1452
  // Trusted placement — delegates all price/discount/inventory authority to the
668
1453
  // shop_place_order RPC (SECURITY DEFINER). The browser never sets a price.
669
1454
  async placeOrder(input) {
@@ -700,7 +1485,12 @@ var SupabaseShopProvider = class {
700
1485
  country: input.shippingAddress.country ?? "BR",
701
1486
  recipient: input.shippingAddress.recipient ?? null,
702
1487
  phone: input.shippingAddress.phone ?? null,
703
- label: input.shippingAddress.label ?? null
1488
+ label: input.shippingAddress.label ?? null,
1489
+ // Not an address field: it rides here because shop_place_order's
1490
+ // signature cannot grow without a DROP, and 0050's trigger lifts it to
1491
+ // plg_shop_orders.customer_document. The storefront already sent it;
1492
+ // the console did not, so a phone sale was born with no CPF.
1493
+ document: input.shippingAddress.document ?? null
704
1494
  } : null,
705
1495
  p_payment_method: input.paymentMethod ?? null,
706
1496
  p_notes: input.notes ?? null,
@@ -723,7 +1513,7 @@ var SupabaseShopProvider = class {
723
1513
  const tenantId = getTenantId();
724
1514
  let q = db.from(T.customers).select("*").order("first_name", { ascending: true });
725
1515
  if (tenantId) q = q.eq("tenant_id", tenantId);
726
- if (options?.search) q = q.or(`first_name.ilike.%${options.search}%,last_name.ilike.%${options.search}%,email.ilike.%${options.search}%`);
1516
+ for (const filter of customerSearchFilters(options?.search)) q = q.or(filter);
727
1517
  if (options?.limit) q = q.limit(options.limit);
728
1518
  const { data, error } = await q;
729
1519
  if (error) throw error;
@@ -735,7 +1525,15 @@ var SupabaseShopProvider = class {
735
1525
  return data ? rowToCustomer(data) : null;
736
1526
  }
737
1527
  async createCustomer(input) {
738
- 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();
1528
+ const { data, error } = await getDb().from(T.customers).insert({
1529
+ tenant_id: getTenantId(),
1530
+ first_name: input.firstName,
1531
+ last_name: input.lastName ?? "",
1532
+ email: input.email ?? null,
1533
+ phone: input.phone ?? null,
1534
+ document: input.document ?? null,
1535
+ notes: input.notes ?? null
1536
+ }).select().single();
739
1537
  if (error) throw error;
740
1538
  return rowToCustomer(data);
741
1539
  }
@@ -745,6 +1543,7 @@ var SupabaseShopProvider = class {
745
1543
  if (input.lastName !== void 0) updates.last_name = input.lastName;
746
1544
  if (input.email !== void 0) updates.email = input.email;
747
1545
  if (input.phone !== void 0) updates.phone = input.phone;
1546
+ if (input.document !== void 0) updates.document = input.document;
748
1547
  if (input.notes !== void 0) updates.notes = input.notes;
749
1548
  const { data, error } = await getDb().from(T.customers).update(updates).eq("id", id).select().single();
750
1549
  if (error) throw error;
@@ -920,22 +1719,6 @@ var SupabaseShopProvider = class {
920
1719
  async deleteEntityRecord(entity, id) {
921
1720
  return this.entityStore(entity).delete(id);
922
1721
  }
923
- // Sidecar fields on core records (migration 0031): one row per
924
- // (reference, reference_id), validated on write against defineSidecarFields.
925
- async getEntityFields(reference, referenceId) {
926
- const { data, error } = await getDb().from(SIDECAR_TABLE).select("data").eq("reference", reference).eq("reference_id", referenceId).maybeSingle();
927
- if (error) throw error;
928
- return data ? data.data ?? {} : null;
929
- }
930
- async setEntityFields(reference, referenceId, fields) {
931
- validateSidecarWrite(reference, fields);
932
- const { data, error } = await getDb().from(SIDECAR_TABLE).upsert(
933
- { tenant_id: getTenantId(), reference, reference_id: referenceId, data: fields },
934
- { onConflict: "tenant_id,reference,reference_id" }
935
- ).select("data").single();
936
- if (error) throw error;
937
- return data.data ?? {};
938
- }
939
1722
  // -------------------------------------------------------------------------
940
1723
  // Coleções, grupos/segmentos, vale-presente, e-mails e mídia.
941
1724
  // -------------------------------------------------------------------------
@@ -1130,10 +1913,27 @@ var SupabaseShopProvider = class {
1130
1913
  return rowToEmailTemplate(data);
1131
1914
  }
1132
1915
  async listMediaFiles() {
1133
- const db = getDb();
1134
1916
  const tenantId = getTenantId();
1135
1917
  if (!tenantId) return [];
1136
- const bucket = db.storage.from(T.mediaBucket);
1918
+ const [attached, library] = await Promise.all([
1919
+ this.listAttachedMedia(tenantId),
1920
+ this.listLibraryFiles(tenantId)
1921
+ ]);
1922
+ return mergeMediaFiles(attached, library);
1923
+ }
1924
+ /** Every product photo in the store, each one carrying its product. */
1925
+ async listAttachedMedia(tenantId) {
1926
+ const db = getDb();
1927
+ 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);
1928
+ if (error || !rows?.length) return [];
1929
+ const productIds = [...new Set(rows.map((r) => r.product_id).filter(Boolean))];
1930
+ const { data: products } = await db.from(T.products).select("id,name").in("id", productIds);
1931
+ const nameById = new Map((products ?? []).map((p) => [p.id, p.name]));
1932
+ return rows.map((row) => productImageToMediaFile(rowToImage(row), nameById.get(row.product_id)));
1933
+ }
1934
+ /** What the merchant uploaded straight to the library — owned by nothing. */
1935
+ async listLibraryFiles(tenantId) {
1936
+ const bucket = getDb().storage.from(T.mediaBucket);
1137
1937
  const listAt = async (prefix) => {
1138
1938
  const { data, error } = await bucket.list(prefix, {
1139
1939
  limit: 1e3,
@@ -1143,14 +1943,15 @@ var SupabaseShopProvider = class {
1143
1943
  return (data ?? []).map((entry) => ({ prefix, entry }));
1144
1944
  };
1145
1945
  const top = await listAt(tenantId);
1146
- const folders = top.filter(({ entry }) => !entry.id).map(({ entry }) => `${tenantId}/${entry.name}`);
1946
+ const folders = top.filter(({ entry }) => !entry.id && entry.name === LIBRARY_FOLDER).map(({ entry }) => `${tenantId}/${entry.name}`);
1147
1947
  const nested = (await Promise.all(folders.map(listAt))).flat();
1148
1948
  return [...top, ...nested].filter(({ entry }) => entry.id).map(({ prefix, entry }) => {
1149
1949
  const path = `${prefix}/${entry.name}`;
1150
1950
  const { data: { publicUrl } } = bucket.getPublicUrl(path);
1951
+ const name = displayNameOf(entry.name);
1151
1952
  const file = {
1152
1953
  id: path,
1153
- name: entry.name,
1954
+ name,
1154
1955
  url: publicUrl,
1155
1956
  kind: mediaKindOf(entry.name, entry.metadata?.mimetype),
1156
1957
  size: Number(entry.metadata?.size ?? 0),
@@ -1158,13 +1959,13 @@ var SupabaseShopProvider = class {
1158
1959
  createdAt: entry.created_at ?? entry.updated_at ?? ""
1159
1960
  };
1160
1961
  return file;
1161
- }).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
1962
+ });
1162
1963
  }
1163
1964
  async uploadMediaFile(file) {
1164
1965
  const db = getDb();
1165
1966
  const tenantId = getTenantId();
1166
1967
  if (!tenantId) throw new Error("@fayz-ai/shop: no active tenant for media upload");
1167
- const path = `${tenantId}/library/${Date.now()}-${file.name}`;
1968
+ const path = `${tenantId}/${LIBRARY_FOLDER}/${Date.now()}-${file.name}`;
1168
1969
  const bucket = db.storage.from(T.mediaBucket);
1169
1970
  const { error } = await bucket.upload(path, file);
1170
1971
  if (error) throw error;
@@ -1217,6 +2018,70 @@ function createSupabaseShopProvider() {
1217
2018
  return new SupabaseShopProvider();
1218
2019
  }
1219
2020
 
2021
+ // src/shipping/label-provider.ts
2022
+ var registry = /* @__PURE__ */ new Map();
2023
+ function registerShippingLabelProvider(provider) {
2024
+ registry.set(provider.id, provider);
2025
+ }
2026
+ function listShippingLabelProviders() {
2027
+ return [...registry.values()];
2028
+ }
2029
+ function resetShippingLabelProviders() {
2030
+ registry.clear();
2031
+ }
2032
+ function resolveShippingLabelProvider(options) {
2033
+ const { connected, preferred } = options;
2034
+ if (preferred === "") return null;
2035
+ if (preferred) {
2036
+ const chosen = registry.get(preferred);
2037
+ return chosen && connected.includes(preferred) ? chosen : null;
2038
+ }
2039
+ for (const id of connected) {
2040
+ const found = registry.get(id);
2041
+ if (found) return found;
2042
+ }
2043
+ return null;
2044
+ }
2045
+
2046
+ // src/lib/document.ts
2047
+ var documentDigits = (value) => String(value ?? "").replace(/\D/g, "");
2048
+ function checkDigit(digits, weights) {
2049
+ const sum = weights.reduce((acc, weight, index) => acc + Number(digits[index]) * weight, 0);
2050
+ const rest = sum % 11;
2051
+ return rest < 2 ? 0 : 11 - rest;
2052
+ }
2053
+ var CPF_FIRST = [10, 9, 8, 7, 6, 5, 4, 3, 2];
2054
+ var CPF_SECOND = [11, 10, 9, 8, 7, 6, 5, 4, 3, 2];
2055
+ var CNPJ_FIRST = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
2056
+ var CNPJ_SECOND = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
2057
+ function isValidCpf(value) {
2058
+ const digits = documentDigits(value);
2059
+ if (digits.length !== 11 || /^(\d)\1{10}$/.test(digits)) return false;
2060
+ return checkDigit(digits, CPF_FIRST) === Number(digits[9]) && checkDigit(digits, CPF_SECOND) === Number(digits[10]);
2061
+ }
2062
+ function isValidCnpj(value) {
2063
+ const digits = documentDigits(value);
2064
+ if (digits.length !== 14 || /^(\d)\1{13}$/.test(digits)) return false;
2065
+ return checkDigit(digits, CNPJ_FIRST) === Number(digits[12]) && checkDigit(digits, CNPJ_SECOND) === Number(digits[13]);
2066
+ }
2067
+ function documentKind(value) {
2068
+ const length = documentDigits(value).length;
2069
+ if (length === 11) return "cpf";
2070
+ if (length === 14) return "cnpj";
2071
+ return null;
2072
+ }
2073
+ function isValidDocument(value) {
2074
+ const kind = documentKind(value);
2075
+ return kind === "cpf" ? isValidCpf(value) : kind === "cnpj" ? isValidCnpj(value) : false;
2076
+ }
2077
+ function formatDocument(value) {
2078
+ const digits = documentDigits(value).slice(0, 14);
2079
+ if (digits.length <= 11) {
2080
+ 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");
2081
+ }
2082
+ 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");
2083
+ }
2084
+
1220
2085
  // src/lib/emails.ts
1221
2086
  var EMAIL_PLACEHOLDERS = [
1222
2087
  "{{store_name}}",
@@ -1406,6 +2271,6 @@ function buildShopMessageDefaults() {
1406
2271
  });
1407
2272
  }
1408
2273
 
1409
- export { EMAIL_PLACEHOLDERS, EMAIL_PREVIEW_SAMPLE, SHOP_EMAIL_EVENTS, SupabaseShopProvider, applyDiscount, buildShopDomainEvents, buildShopMessageDefaults, checkInventory, computeCartTotals, createSupabaseShopProvider, getCustomerOrders, renderEmailTemplate, resolveEmailTemplates, validateDiscount };
2274
+ export { CHARGEABLE_PROVIDERS, EMAIL_PLACEHOLDERS, EMAIL_PREVIEW_SAMPLE, SHOP_EMAIL_EVENTS, SupabaseShopProvider, applyDiscount, buildShopDomainEvents, buildShopMessageDefaults, checkInventory, checkoutGateway, computeCartTotals, createSupabaseShopProvider, documentDigits, documentKind, formatDocument, getCustomerOrders, getShopFunctionsUrl, isChargeableProvider, isValidCnpj, isValidCpf, isValidDocument, listShippingLabelProviders, registerShippingLabelProvider, renderEmailTemplate, resetShippingLabelProviders, resolveEmailTemplates, resolveShippingLabelProvider, setShopFunctionsUrl, validateDiscount };
1410
2275
  //# sourceMappingURL=index.js.map
1411
2276
  //# sourceMappingURL=index.js.map