@fayz-ai/shop 0.10.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 (63) 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/customer-search.d.ts +3 -0
  12. package/dist/data/customer-search.d.ts.map +1 -0
  13. package/dist/data/functions.d.ts +9 -0
  14. package/dist/data/functions.d.ts.map +1 -0
  15. package/dist/data/media.d.ts +37 -0
  16. package/dist/data/media.d.ts.map +1 -0
  17. package/dist/data/mock-provider.d.ts +40 -8
  18. package/dist/data/mock-provider.d.ts.map +1 -1
  19. package/dist/data/provider.d.ts +200 -7
  20. package/dist/data/provider.d.ts.map +1 -1
  21. package/dist/data/supabase-provider.d.ts +113 -5
  22. package/dist/data/supabase-provider.d.ts.map +1 -1
  23. package/dist/data/tables.d.ts +3 -0
  24. package/dist/data/tables.d.ts.map +1 -1
  25. package/dist/entities/index.d.ts +0 -1
  26. package/dist/entities/index.d.ts.map +1 -1
  27. package/dist/entities.js +2 -2
  28. package/dist/index.d.ts +14 -5
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +891 -60
  31. package/dist/index.js.map +1 -1
  32. package/dist/inventory/index.d.ts +2 -2
  33. package/dist/inventory/index.d.ts.map +1 -1
  34. package/dist/inventory/stock-types.d.ts +60 -1
  35. package/dist/inventory/stock-types.d.ts.map +1 -1
  36. package/dist/inventory/supabase-stock-store.d.ts +1 -0
  37. package/dist/inventory/supabase-stock-store.d.ts.map +1 -1
  38. package/dist/inventory.js +1 -1
  39. package/dist/lib/commerce.d.ts.map +1 -1
  40. package/dist/lib/document.d.ts +13 -0
  41. package/dist/lib/document.d.ts.map +1 -0
  42. package/dist/lib/variant-matrix.d.ts +120 -0
  43. package/dist/lib/variant-matrix.d.ts.map +1 -0
  44. package/dist/mock.js +2 -2
  45. package/dist/providers/checkout.d.ts +129 -0
  46. package/dist/providers/checkout.d.ts.map +1 -0
  47. package/dist/providers/index.d.ts +5 -0
  48. package/dist/providers/index.d.ts.map +1 -0
  49. package/dist/providers/shipping.d.ts +43 -0
  50. package/dist/providers/shipping.d.ts.map +1 -0
  51. package/dist/providers.js +70 -0
  52. package/dist/providers.js.map +1 -0
  53. package/dist/runtime.js +3 -3
  54. package/dist/shipping/label-provider.d.ts +43 -0
  55. package/dist/shipping/label-provider.d.ts.map +1 -0
  56. package/dist/types.d.ts +457 -0
  57. package/dist/types.d.ts.map +1 -1
  58. package/package.json +8 -4
  59. package/dist/chunk-U3O2I3RM.js.map +0 -1
  60. package/dist/chunk-XERLK5LD.js.map +0 -1
  61. package/dist/chunk-XOP6SHPB.js.map +0 -1
  62. package/dist/entities/sidecar.d.ts +0 -9
  63. 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,52 @@ 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/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
+ }
137
+
138
+ // src/data/supabase-provider.ts
89
139
  function getDb() {
90
140
  const supabase = getSupabaseClientOptional();
91
141
  if (!supabase) throw new Error("@fayz-ai/shop: Supabase client not initialized. Call setGlobalSupabaseClient() first.");
@@ -103,6 +153,95 @@ function toPostalDigits(value) {
103
153
  const digits = String(value ?? "").replace(/\D/g, "");
104
154
  return digits ? digits.padStart(8, "0").slice(0, 8) : void 0;
105
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
+ }
106
245
  function rowToShippingZone(row) {
107
246
  return {
108
247
  id: row.id,
@@ -116,9 +255,74 @@ function rowToShippingZone(row) {
116
255
  etaMinDays: row.eta_min_days ?? null,
117
256
  etaMaxDays: row.eta_max_days ?? null,
118
257
  active: row.active ?? true,
119
- 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
120
273
  };
121
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
+ }
122
326
  function shippingZonePayload(input) {
123
327
  const payload = {};
124
328
  if (input.name !== void 0) payload.name = input.name;
@@ -131,6 +335,34 @@ function shippingZonePayload(input) {
131
335
  if (input.etaMaxDays !== void 0) payload.eta_max_days = input.etaMaxDays ?? null;
132
336
  if (input.active !== void 0) payload.active = input.active;
133
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;
134
366
  return payload;
135
367
  }
136
368
  function rowToProduct(row) {
@@ -154,10 +386,21 @@ function rowToProduct(row) {
154
386
  isPhysical: row.is_physical ?? true,
155
387
  weight: row.weight,
156
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,
157
395
  createdAt: row.created_at,
158
396
  updatedAt: row.updated_at
159
397
  };
160
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
+ }
161
404
  function rowToImage(row) {
162
405
  return {
163
406
  id: row.id,
@@ -229,6 +472,7 @@ function rowToCustomer(row) {
229
472
  lastName: row.last_name ?? "",
230
473
  email: row.email,
231
474
  phone: row.phone,
475
+ document: row.document ?? null,
232
476
  notes: row.notes,
233
477
  ordersCount: row.orders_count ?? 0,
234
478
  totalSpent: row.total_spent ?? 0,
@@ -285,22 +529,13 @@ function segmentPayload(input) {
285
529
  return p;
286
530
  }
287
531
  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
532
  function generateGiftCardCode() {
291
533
  const bytes = new Uint8Array(12);
292
534
  globalThis.crypto.getRandomValues(bytes);
293
535
  const chars = Array.from(bytes, (b) => GIFT_CODE_ALPHABET[b % GIFT_CODE_ALPHABET.length]);
294
536
  return `${chars.slice(0, 4).join("")}-${chars.slice(4, 8).join("")}-${chars.slice(8, 12).join("")}`;
295
537
  }
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
- }
538
+ var LIBRARY_FOLDER = "library";
304
539
  function nullableNumber(value) {
305
540
  if (value === null || value === void 0 || value === "") return null;
306
541
  const n = Number(value);
@@ -418,11 +653,13 @@ var SupabaseShopProvider = class {
418
653
  * RLS on both tables already restricts anon reads to active variants of
419
654
  * publicly readable products; the status filter here keeps the admin path
420
655
  * (authenticated tenant member, who CAN see archived rows) consistent. */
421
- async getProductVariants(productId) {
656
+ async getProductVariants(productId, opts) {
422
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");
423
660
  const [optionsRes, variantsRes] = await Promise.all([
424
661
  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 })
662
+ variantQuery.order("position", { ascending: true })
426
663
  ]);
427
664
  if (optionsRes.error) throw optionsRes.error;
428
665
  if (variantsRes.error) throw variantsRes.error;
@@ -432,19 +669,102 @@ var SupabaseShopProvider = class {
432
669
  values: Array.isArray(row.values) ? row.values.map(String) : [],
433
670
  position: row.position ?? 0
434
671
  }));
435
- const variants = (variantsRes.data ?? []).map((row) => ({
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) => ({
436
710
  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
711
+ name: row.name,
712
+ values: Array.isArray(row.values) ? row.values.map(String) : [],
713
+ position: row.position ?? 0
446
714
  }));
447
- return { options, variants };
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;
448
768
  }
449
769
  async createProduct(input) {
450
770
  const db = getDb();
@@ -465,7 +785,17 @@ var SupabaseShopProvider = class {
465
785
  category_id: input.categoryId ?? null,
466
786
  is_physical: input.isPhysical ?? true,
467
787
  weight: input.weight ?? null,
468
- 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
469
799
  }).select(`*, images:${T.productImages}(*)`).single();
470
800
  if (error) throw error;
471
801
  return rowToProduct(data);
@@ -488,6 +818,13 @@ var SupabaseShopProvider = class {
488
818
  if (input.categoryId !== void 0) updates.category_id = input.categoryId;
489
819
  if (input.isPhysical !== void 0) updates.is_physical = input.isPhysical;
490
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;
491
828
  const { data, error } = await db.from(T.products).update(updates).eq("id", id).select(`*, images:${T.productImages}(*)`).single();
492
829
  if (error) throw error;
493
830
  return rowToProduct(data);
@@ -532,9 +869,43 @@ var SupabaseShopProvider = class {
532
869
  if (error) throw error;
533
870
  return rowToImage(data);
534
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
+ }
535
887
  async deleteProductImage(imageId) {
536
- 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);
537
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]);
538
909
  }
539
910
  // ---------------------------------------------------------------------------
540
911
  // Categories
@@ -643,10 +1014,188 @@ var SupabaseShopProvider = class {
643
1014
  if (error) throw error;
644
1015
  return rowToShippingZone(data);
645
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
+ }
646
1056
  async deleteShippingZone(id) {
647
1057
  const { error } = await getDb().from(T.shippingZones).delete().eq("id", id);
648
1058
  if (error) throw error;
649
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
+ }
650
1199
  /**
651
1200
  * Settle a pending order: the merchant confirming that the money arrived.
652
1201
  *
@@ -664,6 +1213,208 @@ var SupabaseShopProvider = class {
664
1213
  if (error) throw error;
665
1214
  return this.getOrder(id);
666
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
+ }
667
1418
  // Trusted placement — delegates all price/discount/inventory authority to the
668
1419
  // shop_place_order RPC (SECURITY DEFINER). The browser never sets a price.
669
1420
  async placeOrder(input) {
@@ -700,7 +1451,12 @@ var SupabaseShopProvider = class {
700
1451
  country: input.shippingAddress.country ?? "BR",
701
1452
  recipient: input.shippingAddress.recipient ?? null,
702
1453
  phone: input.shippingAddress.phone ?? null,
703
- 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
704
1460
  } : null,
705
1461
  p_payment_method: input.paymentMethod ?? null,
706
1462
  p_notes: input.notes ?? null,
@@ -723,7 +1479,7 @@ var SupabaseShopProvider = class {
723
1479
  const tenantId = getTenantId();
724
1480
  let q = db.from(T.customers).select("*").order("first_name", { ascending: true });
725
1481
  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}%`);
1482
+ for (const filter of customerSearchFilters(options?.search)) q = q.or(filter);
727
1483
  if (options?.limit) q = q.limit(options.limit);
728
1484
  const { data, error } = await q;
729
1485
  if (error) throw error;
@@ -735,7 +1491,15 @@ var SupabaseShopProvider = class {
735
1491
  return data ? rowToCustomer(data) : null;
736
1492
  }
737
1493
  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();
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();
739
1503
  if (error) throw error;
740
1504
  return rowToCustomer(data);
741
1505
  }
@@ -745,6 +1509,7 @@ var SupabaseShopProvider = class {
745
1509
  if (input.lastName !== void 0) updates.last_name = input.lastName;
746
1510
  if (input.email !== void 0) updates.email = input.email;
747
1511
  if (input.phone !== void 0) updates.phone = input.phone;
1512
+ if (input.document !== void 0) updates.document = input.document;
748
1513
  if (input.notes !== void 0) updates.notes = input.notes;
749
1514
  const { data, error } = await getDb().from(T.customers).update(updates).eq("id", id).select().single();
750
1515
  if (error) throw error;
@@ -920,22 +1685,6 @@ var SupabaseShopProvider = class {
920
1685
  async deleteEntityRecord(entity, id) {
921
1686
  return this.entityStore(entity).delete(id);
922
1687
  }
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
1688
  // -------------------------------------------------------------------------
940
1689
  // Coleções, grupos/segmentos, vale-presente, e-mails e mídia.
941
1690
  // -------------------------------------------------------------------------
@@ -1130,10 +1879,27 @@ var SupabaseShopProvider = class {
1130
1879
  return rowToEmailTemplate(data);
1131
1880
  }
1132
1881
  async listMediaFiles() {
1133
- const db = getDb();
1134
1882
  const tenantId = getTenantId();
1135
1883
  if (!tenantId) return [];
1136
- const bucket = db.storage.from(T.mediaBucket);
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);
1137
1903
  const listAt = async (prefix) => {
1138
1904
  const { data, error } = await bucket.list(prefix, {
1139
1905
  limit: 1e3,
@@ -1143,14 +1909,15 @@ var SupabaseShopProvider = class {
1143
1909
  return (data ?? []).map((entry) => ({ prefix, entry }));
1144
1910
  };
1145
1911
  const top = await listAt(tenantId);
1146
- const folders = top.filter(({ entry }) => !entry.id).map(({ entry }) => `${tenantId}/${entry.name}`);
1912
+ const folders = top.filter(({ entry }) => !entry.id && entry.name === LIBRARY_FOLDER).map(({ entry }) => `${tenantId}/${entry.name}`);
1147
1913
  const nested = (await Promise.all(folders.map(listAt))).flat();
1148
1914
  return [...top, ...nested].filter(({ entry }) => entry.id).map(({ prefix, entry }) => {
1149
1915
  const path = `${prefix}/${entry.name}`;
1150
1916
  const { data: { publicUrl } } = bucket.getPublicUrl(path);
1917
+ const name = displayNameOf(entry.name);
1151
1918
  const file = {
1152
1919
  id: path,
1153
- name: entry.name,
1920
+ name,
1154
1921
  url: publicUrl,
1155
1922
  kind: mediaKindOf(entry.name, entry.metadata?.mimetype),
1156
1923
  size: Number(entry.metadata?.size ?? 0),
@@ -1158,13 +1925,13 @@ var SupabaseShopProvider = class {
1158
1925
  createdAt: entry.created_at ?? entry.updated_at ?? ""
1159
1926
  };
1160
1927
  return file;
1161
- }).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
1928
+ });
1162
1929
  }
1163
1930
  async uploadMediaFile(file) {
1164
1931
  const db = getDb();
1165
1932
  const tenantId = getTenantId();
1166
1933
  if (!tenantId) throw new Error("@fayz-ai/shop: no active tenant for media upload");
1167
- const path = `${tenantId}/library/${Date.now()}-${file.name}`;
1934
+ const path = `${tenantId}/${LIBRARY_FOLDER}/${Date.now()}-${file.name}`;
1168
1935
  const bucket = db.storage.from(T.mediaBucket);
1169
1936
  const { error } = await bucket.upload(path, file);
1170
1937
  if (error) throw error;
@@ -1217,6 +1984,70 @@ function createSupabaseShopProvider() {
1217
1984
  return new SupabaseShopProvider();
1218
1985
  }
1219
1986
 
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
+
1220
2051
  // src/lib/emails.ts
1221
2052
  var EMAIL_PLACEHOLDERS = [
1222
2053
  "{{store_name}}",
@@ -1406,6 +2237,6 @@ function buildShopMessageDefaults() {
1406
2237
  });
1407
2238
  }
1408
2239
 
1409
- export { EMAIL_PLACEHOLDERS, EMAIL_PREVIEW_SAMPLE, SHOP_EMAIL_EVENTS, SupabaseShopProvider, applyDiscount, buildShopDomainEvents, buildShopMessageDefaults, checkInventory, computeCartTotals, createSupabaseShopProvider, getCustomerOrders, renderEmailTemplate, resolveEmailTemplates, validateDiscount };
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 };
1410
2241
  //# sourceMappingURL=index.js.map
1411
2242
  //# sourceMappingURL=index.js.map