@fayz-ai/shop 0.15.0 → 0.16.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 (44) hide show
  1. package/dist/{chunk-STRNMFC4.js → chunk-26YK5V6P.js} +35 -6
  2. package/dist/chunk-26YK5V6P.js.map +1 -0
  3. package/dist/{chunk-Q3N4PS6P.js → chunk-EUFSFVGS.js} +64 -5
  4. package/dist/chunk-EUFSFVGS.js.map +1 -0
  5. package/dist/{chunk-3GERSY5F.js → chunk-UVQ6UUDN.js} +5 -2
  6. package/dist/chunk-UVQ6UUDN.js.map +1 -0
  7. package/dist/{chunk-7GIWXT5D.js → chunk-WWQICATY.js} +3 -3
  8. package/dist/{chunk-7GIWXT5D.js.map → chunk-WWQICATY.js.map} +1 -1
  9. package/dist/data/checkout-gateways.d.ts +13 -0
  10. package/dist/data/checkout-gateways.d.ts.map +1 -1
  11. package/dist/data/functions.d.ts +3 -1
  12. package/dist/data/functions.d.ts.map +1 -1
  13. package/dist/data/mock-provider.d.ts +4 -0
  14. package/dist/data/mock-provider.d.ts.map +1 -1
  15. package/dist/data/provider.d.ts +90 -1
  16. package/dist/data/provider.d.ts.map +1 -1
  17. package/dist/data/supabase-provider.d.ts +26 -1
  18. package/dist/data/supabase-provider.d.ts.map +1 -1
  19. package/dist/data/tables.d.ts +51 -0
  20. package/dist/data/tables.d.ts.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +273 -40
  24. package/dist/index.js.map +1 -1
  25. package/dist/inventory/postgrest-double.d.ts +9 -0
  26. package/dist/inventory/postgrest-double.d.ts.map +1 -1
  27. package/dist/inventory/supabase-stock-store.d.ts.map +1 -1
  28. package/dist/inventory/vocabulary.d.ts.map +1 -1
  29. package/dist/inventory.js +1 -1
  30. package/dist/lib/commerce.d.ts.map +1 -1
  31. package/dist/lib/message-events.d.ts.map +1 -1
  32. package/dist/lib/order-events.d.ts +5 -0
  33. package/dist/lib/order-events.d.ts.map +1 -0
  34. package/dist/mock.js +2 -2
  35. package/dist/rules/adapter.d.ts +3 -0
  36. package/dist/rules/adapter.d.ts.map +1 -1
  37. package/dist/rules.js +2 -2
  38. package/dist/runtime.js +3 -3
  39. package/dist/types.d.ts +58 -1
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +4 -4
  42. package/dist/chunk-3GERSY5F.js.map +0 -1
  43. package/dist/chunk-Q3N4PS6P.js.map +0 -1
  44. package/dist/chunk-STRNMFC4.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,16 +1,16 @@
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-7GIWXT5D.js';
4
- export { getShopProvider, resetShopProvider, setShopProvider } from './chunk-7GIWXT5D.js';
1
+ import { T, V, resolveStockStore, createSupabaseStockStore, OPENING_BALANCE_REASON, rowToStockLocation } from './chunk-26YK5V6P.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-26YK5V6P.js';
3
+ import { getShopProvider } from './chunk-WWQICATY.js';
4
+ export { getShopProvider, resetShopProvider, setShopProvider } from './chunk-WWQICATY.js';
5
5
  import './chunk-FQPGE3YN.js';
6
- import { normalizeVariantAxes, validateVariantAxes, variantOptionsKey, storagePathOf, mergeMediaFiles, productImageToMediaFile, displayNameOf, mediaKindOf } from './chunk-Q3N4PS6P.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-Q3N4PS6P.js';
6
+ import { normalizeVariantAxes, validateVariantAxes, variantOptionsKey, storagePathOf, emitOrderEvent, mergeMediaFiles, productImageToMediaFile, displayNameOf, mediaKindOf } from './chunk-EUFSFVGS.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-EUFSFVGS.js';
8
8
  export { MOCK_CATEGORIES, MOCK_DISCOUNTS, MOCK_PRODUCTS, buildMockCatalog, svgPlaceholder } from './chunk-IKNGCRUM.js';
9
9
  import { createSupabaseEntityStore } from './chunk-6BO2TMZ5.js';
10
10
  import { getShopTenantId } from './chunk-KLDS7IPU.js';
11
11
  export { getShopTenantId, setShopTenantResolver } from './chunk-KLDS7IPU.js';
12
12
  import { resolveEntityStore } from './chunk-NWW3WJ7Y.js';
13
- import './chunk-3GERSY5F.js';
13
+ import './chunk-UVQ6UUDN.js';
14
14
  import { getSupabaseClientOptional } from '@fayz-ai/core';
15
15
 
16
16
  // src/lib/commerce.ts
@@ -32,6 +32,19 @@ async function validateDiscount(input, provider = getShopProvider()) {
32
32
  if (discount.usageLimit != null && discount.timesUsed >= discount.usageLimit) {
33
33
  return { valid: false, code: discount.code, type: discount.type, value: 0, reason: "usage_limit" };
34
34
  }
35
+ if (discount.minSubtotal != null && (input.subtotal ?? 0) < discount.minSubtotal) {
36
+ return {
37
+ valid: false,
38
+ code: discount.code,
39
+ type: discount.type,
40
+ value: 0,
41
+ reason: "min_subtotal",
42
+ minSubtotal: discount.minSubtotal
43
+ };
44
+ }
45
+ if (discount.type === "free_shipping") {
46
+ return { valid: true, code: discount.code, type: discount.type, value: 0 };
47
+ }
35
48
  if (discount.type !== "percentage" && discount.type !== "fixed_amount") {
36
49
  return { valid: false, code: discount.code, type: discount.type, value: 0, reason: "unsupported" };
37
50
  }
@@ -163,7 +176,21 @@ function setShopFunctionsUrl(url) {
163
176
  function getShopFunctionsUrl() {
164
177
  return _functionsUrl;
165
178
  }
166
- async function callShopFunction(db, slug, body) {
179
+ var DEFAULT_TIMEOUT_MS = 25e3;
180
+ async function callShopFunction(db, slug, body, options) {
181
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
182
+ let timer;
183
+ const bounded = (work) => Promise.race([
184
+ work,
185
+ new Promise((_, reject) => {
186
+ timer = setTimeout(
187
+ () => reject(new Error(`A fun\xE7\xE3o ${slug} n\xE3o respondeu em ${Math.round(timeoutMs / 1e3)}s.`)),
188
+ timeoutMs
189
+ );
190
+ })
191
+ ]).finally(() => {
192
+ if (timer) clearTimeout(timer);
193
+ });
167
194
  const override = getShopFunctionsUrl();
168
195
  if (override) {
169
196
  let token = null;
@@ -171,18 +198,25 @@ async function callShopFunction(db, slug, body) {
171
198
  token = (await db?.auth?.getSession?.())?.data?.session?.access_token ?? null;
172
199
  } catch {
173
200
  }
174
- const response = await fetch(`${override}/${slug}`, {
175
- method: "POST",
176
- headers: {
177
- "Content-Type": "application/json",
178
- ...token ? { Authorization: `Bearer ${token}` } : {}
179
- },
180
- body: JSON.stringify(body)
181
- });
201
+ let response;
202
+ try {
203
+ response = await bounded(fetch(`${override}/${slug}`, {
204
+ method: "POST",
205
+ headers: {
206
+ "Content-Type": "application/json",
207
+ ...token ? { Authorization: `Bearer ${token}` } : {}
208
+ },
209
+ body: JSON.stringify(body)
210
+ }));
211
+ } catch (cause) {
212
+ throw new Error(
213
+ `A bancada de fun\xE7\xF5es em ${override} n\xE3o respondeu (${slug}). Suba-a, ou tire PUBLIC_SUPABASE_FUNCTIONS_URL do .env para usar as fun\xE7\xF5es publicadas.`
214
+ );
215
+ }
182
216
  const payload2 = await response.json().catch(() => null);
183
217
  return { ok: response.ok, status: response.status, payload: payload2 };
184
218
  }
185
- const { data, error } = await db.functions.invoke(slug, { body });
219
+ const { data, error } = await bounded(db.functions.invoke(slug, { body }));
186
220
  if (!error) return { ok: true, status: 200, payload: data };
187
221
  const status = Number(error?.context?.status ?? 502);
188
222
  let payload = null;
@@ -196,7 +230,11 @@ async function callShopFunction(db, slug, body) {
196
230
  // src/data/checkout-gateways.ts
197
231
  var GATEWAYS = Object.freeze({
198
232
  abacatepay: Object.freeze({ open: "shop-checkout-open", settle: "shop-checkout-settle", link: true, card: false, label: "AbacatePay" }),
199
- vindi: Object.freeze({ open: "vindi-checkout-open", settle: "vindi-checkout-settle", link: true, card: true, label: "Vindi" })
233
+ vindi: Object.freeze({ open: "vindi-checkout-open", settle: "vindi-checkout-settle", link: true, card: true, label: "Vindi" }),
234
+ // The first acquirer here that charges a card IN THE STORE: Mercado Pago
235
+ // tokenises in the browser, so no PAN crosses the pool and card needs no
236
+ // hosted page. `link: false` because nothing mints a shareable URL yet.
237
+ mercadopago: Object.freeze({ open: "mercadopago-checkout-open", settle: "mercadopago-checkout-settle", link: false, card: true, label: "Mercado Pago" })
200
238
  });
201
239
  var CHARGEABLE_PROVIDERS = Object.freeze(Object.keys(GATEWAYS));
202
240
  function isChargeableProvider(provider) {
@@ -216,6 +254,12 @@ function checkoutGateway(provider) {
216
254
  if (!slug) return null;
217
255
  return GATEWAYS[slug] ?? null;
218
256
  }
257
+ function resolveCheckoutProvider(explicit, configured) {
258
+ const asked = String(explicit ?? "").trim();
259
+ if (asked) return asked;
260
+ const store = String(configured ?? "").trim();
261
+ return store || "abacatepay";
262
+ }
219
263
 
220
264
  // src/data/customer-search.ts
221
265
  var COLUMNS = ["first_name", "last_name", "email", "phone"];
@@ -417,13 +461,13 @@ function shippingZonePayload(input) {
417
461
  if (input.carrier !== void 0) payload.carrier = input.carrier || null;
418
462
  if (input.postalFrom !== void 0) payload.postal_from = toPostalDigits(input.postalFrom);
419
463
  if (input.postalTo !== void 0) payload.postal_to = toPostalDigits(input.postalTo);
420
- if (input.rate !== void 0) payload.rate = input.rate;
464
+ if (input.rate != null) payload.rate = input.rate;
421
465
  if (input.freeAbove !== void 0) payload.free_above = input.freeAbove ?? null;
422
466
  if (input.etaMinDays !== void 0) payload.eta_min_days = input.etaMinDays ?? null;
423
467
  if (input.etaMaxDays !== void 0) payload.eta_max_days = input.etaMaxDays ?? null;
424
- if (input.active !== void 0) payload.active = input.active;
425
- if (input.sortOrder !== void 0) payload.sort_order = input.sortOrder;
426
- if (input.excluded !== void 0) payload.excluded = input.excluded;
468
+ if (input.active != null) payload.active = input.active;
469
+ if (input.sortOrder != null) payload.sort_order = input.sortOrder;
470
+ if (input.excluded != null) payload.excluded = input.excluded;
427
471
  if (input.profileId !== void 0) payload.profile_id = input.profileId ?? null;
428
472
  return payload;
429
473
  }
@@ -517,6 +561,7 @@ function rowToOrderItem(row) {
517
561
  id: row.id,
518
562
  orderId: row.order_id,
519
563
  productId: row.product_id ?? null,
564
+ variantId: row.variant_id ?? null,
520
565
  name: row.name,
521
566
  sku: row.sku ?? null,
522
567
  quantity: row.quantity ?? 0,
@@ -712,6 +757,13 @@ function rowToGiftCard(row) {
712
757
  updatedAt: row.updated_at ?? row.created_at
713
758
  };
714
759
  }
760
+ function cartPaymentUrl(url, token) {
761
+ const base = String(url ?? "").trim();
762
+ const handle = String(token ?? "").trim();
763
+ if (!base) return null;
764
+ if (!handle || base.includes(handle)) return base;
765
+ return `${base.replace(/\/+$/u, "")}/${handle}`;
766
+ }
715
767
  var SupabaseShopProvider = class {
716
768
  // ---------------------------------------------------------------------------
717
769
  // Products
@@ -744,7 +796,7 @@ var SupabaseShopProvider = class {
744
796
  * (authenticated tenant member, who CAN see archived rows) consistent. */
745
797
  async getProductVariants(productId, opts) {
746
798
  const db = getDb();
747
- let variantQuery = db.from(T.productVariants).select("*").eq("product_id", productId);
799
+ let variantQuery = db.from(V.productVariants).select("*").eq("shop_product_id", productId);
748
800
  if (!opts?.includeArchived) variantQuery = variantQuery.eq("status", "active");
749
801
  const [optionsRes, variantsRes] = await Promise.all([
750
802
  db.from(T.productOptions).select("*").eq("product_id", productId).order("position", { ascending: true }),
@@ -808,7 +860,7 @@ var SupabaseShopProvider = class {
808
860
  if (Object.keys(input.options).length === 0) {
809
861
  throw new Error("Uma variante sem op\xE7\xF5es \xE9 a variante padr\xE3o (0037), criada pelo banco. Declare ao menos um eixo.");
810
862
  }
811
- const { data: existing, error: readError } = await db.from(T.productVariants).select("*").eq("product_id", productId);
863
+ const { data: existing, error: readError } = await db.from(V.productVariants).select("*").eq("shop_product_id", productId);
812
864
  if (readError) throw readError;
813
865
  const wantedKey = variantOptionsKey(input.options);
814
866
  const match = (existing ?? []).find((row) => variantOptionsKey(row.options ?? {}) === wantedKey);
@@ -855,6 +907,36 @@ var SupabaseShopProvider = class {
855
907
  });
856
908
  return target;
857
909
  }
910
+ async receiveVariantStock(variantId, quantity, options) {
911
+ const amount = Math.trunc(quantity);
912
+ if (amount <= 0) throw new Error("A quantidade recebida precisa ser maior que zero.");
913
+ const store = resolveStockStore(getTenantId() ?? "", () => createSupabaseStockStore());
914
+ await store.recordMovement({
915
+ variantId,
916
+ kind: "receipt",
917
+ reasonCode: options?.reasonCode ?? "purchase",
918
+ quantity: amount,
919
+ note: options?.note,
920
+ occurredAt: options?.occurredAt
921
+ });
922
+ return store.getAvailable(variantId);
923
+ }
924
+ async receiveProductStock(productId, quantity, options) {
925
+ const amount = Math.trunc(quantity);
926
+ if (amount <= 0) throw new Error("A quantidade recebida precisa ser maior que zero.");
927
+ if (options?.variantId) {
928
+ return this.receiveVariantStock(options.variantId, amount, options);
929
+ }
930
+ const { variants } = await this.getProductVariants(productId);
931
+ if (variants.length === 1) return this.receiveVariantStock(variants[0].id, amount, options);
932
+ if (variants.length > 1) {
933
+ throw new Error("Este produto tem mais de uma varia\xE7\xE3o \u2014 escolha qual recebeu a entrada.");
934
+ }
935
+ const product = await this.getProduct(productId);
936
+ const now = Math.max(0, product?.inventoryCount ?? 0) + amount;
937
+ await this.updateProduct(productId, { inventoryCount: now });
938
+ return now;
939
+ }
858
940
  async createProduct(input) {
859
941
  const db = getDb();
860
942
  const tenantId = getTenantId();
@@ -1037,7 +1119,7 @@ var SupabaseShopProvider = class {
1037
1119
  async listOrders(options) {
1038
1120
  const db = getDb();
1039
1121
  const tenantId = getTenantId();
1040
- let q = db.from(T.orders).select(`*, items:${T.orderItems}(*)`);
1122
+ let q = db.from(V.orders).select(`*, items:${V.orderItems}(*)`);
1041
1123
  if (tenantId) q = q.eq("tenant_id", tenantId);
1042
1124
  if (options?.status) q = q.eq("status", options.status);
1043
1125
  if (options?.financialStatus) q = q.eq("financial_status", options.financialStatus);
@@ -1052,9 +1134,14 @@ var SupabaseShopProvider = class {
1052
1134
  return (data ?? []).map(rowToOrder);
1053
1135
  }
1054
1136
  async getOrder(id) {
1055
- const { data, error } = await getDb().from(T.orders).select(`*, items:${T.orderItems}(*)`).eq("id", id).maybeSingle();
1137
+ const db = getDb();
1138
+ const { data, error } = await db.from(V.orders).select(`*, items:${V.orderItems}(*)`).eq("id", id).maybeSingle();
1056
1139
  if (error) throw error;
1057
- return data ? rowToOrder(data) : null;
1140
+ if (!data) return null;
1141
+ const order = rowToOrder(data);
1142
+ const { data: session } = await db.from(T.checkoutSessions).select("id").eq("order_id", id).limit(1);
1143
+ order.origin = (session?.length ?? 0) > 0 ? "storefront" : "admin";
1144
+ return order;
1058
1145
  }
1059
1146
  async createOrder(input) {
1060
1147
  const db = getDb();
@@ -1462,7 +1549,11 @@ var SupabaseShopProvider = class {
1462
1549
  async openCheckoutSession(input) {
1463
1550
  const tenantId = getTenantId();
1464
1551
  if (!tenantId) throw new Error("@fayz-ai/shop: openCheckoutSession requires a tenant id.");
1465
- const provider = String(input.provider ?? "").trim() || "abacatepay";
1552
+ const provider = resolveCheckoutProvider(
1553
+ input.provider,
1554
+ // Só vai ao banco quando ninguém disse: a vitrine já sabe o seu.
1555
+ input.provider == null ? (await this.getStoreSettings().catch(() => null))?.paymentProvider : null
1556
+ );
1466
1557
  const gateway = checkoutGateway(provider);
1467
1558
  if (!gateway) {
1468
1559
  throw new Error("Esta loja ainda n\xE3o tem um meio de cobran\xE7a conectado.");
@@ -1485,7 +1576,18 @@ var SupabaseShopProvider = class {
1485
1576
  // never enabled it — which is a different answer from "no acquirer".
1486
1577
  method: input.paymentMethod ?? null,
1487
1578
  returnUrl: input.returnUrl ?? null,
1488
- asLink: input.asLink === true
1579
+ asLink: input.asLink === true,
1580
+ // The card, when it is typed INSIDE the store — a single-use token from
1581
+ // the acquirer's own browser SDK, never a card number. Forwarded here for
1582
+ // the same reason it is forwarded in @fayz-ai/sdk's provider: this body is
1583
+ // assembled field by field, so anything the list omits is dropped in
1584
+ // silence, and the buyer is told "we did not receive the card details"
1585
+ // after filling in a perfectly good card.
1586
+ cardToken: input.cardToken ?? null,
1587
+ installments: input.installments ?? null,
1588
+ paymentMethodId: input.paymentMethodId ?? null,
1589
+ issuerId: input.issuerId ?? null,
1590
+ deviceId: input.deviceId ?? null
1489
1591
  });
1490
1592
  if (!ok || !payload?.sessionId) {
1491
1593
  const failure = 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."));
@@ -1539,7 +1641,7 @@ var SupabaseShopProvider = class {
1539
1641
  tenantId,
1540
1642
  sessionId,
1541
1643
  ...chargeToken ? { chargeToken } : {}
1542
- });
1644
+ }, { timeoutMs: 12e3 });
1543
1645
  if (!ok || !payload?.state) return { state: "unknown" };
1544
1646
  return { state: payload.state, orderId: payload.orderId ?? null, reason: payload.reason ?? null };
1545
1647
  } catch {
@@ -1554,6 +1656,16 @@ var SupabaseShopProvider = class {
1554
1656
  * needs the field remembered is the one who did not finish. The function
1555
1657
  * decides what may be written and to whose row — this only carries the values.
1556
1658
  */
1659
+ async paymentPublicKey(provider) {
1660
+ const tenantId = getTenantId();
1661
+ const gateway = checkoutGateway(provider);
1662
+ if (!tenantId || !gateway) return null;
1663
+ const { ok, payload } = await callShopFunction(getDb(), gateway.open, {
1664
+ tenantId,
1665
+ action: "public_key"
1666
+ });
1667
+ return ok ? payload?.publicKey ?? null : null;
1668
+ }
1557
1669
  async saveCustomerContact(input) {
1558
1670
  const tenantId = getTenantId();
1559
1671
  if (!tenantId) return;
@@ -1566,12 +1678,13 @@ var SupabaseShopProvider = class {
1566
1678
  }
1567
1679
  // Trusted placement — delegates all price/discount/inventory authority to the
1568
1680
  // shop_place_order RPC (SECURITY DEFINER). The browser never sets a price.
1569
- async placeOrder(input) {
1570
- const db = getDb();
1571
- const tenantId = input.tenantId ?? getTenantId();
1572
- if (!tenantId) throw new Error("@fayz-ai/shop: placeOrder requires a tenant id (configure the shop tenant resolver).");
1573
- const { data, error } = await db.rpc("shop_place_order", {
1574
- p_tenant_id: tenantId,
1681
+ /**
1682
+ * The argument mapping placing and revising share. Two copies of it would
1683
+ * drift on the first field either screen learns to send — and the one that
1684
+ * forgot would silently drop it from the order.
1685
+ */
1686
+ orderRpcArgs(input) {
1687
+ return {
1575
1688
  // variant_id rides on each line (0029): the RPC prices and decrements
1576
1689
  // the variant when present, product-level otherwise. Pre-0029 servers
1577
1690
  // ignore unknown jsonb keys, so this stays compatible either way.
@@ -1613,11 +1726,125 @@ var SupabaseShopProvider = class {
1613
1726
  // a pre-0029 server rejects unknown NAMED arguments (unlike unknown
1614
1727
  // jsonb keys). Absent metadata therefore must not send the key at all.
1615
1728
  ...input.metadata !== void 0 ? { p_metadata: input.metadata } : {}
1729
+ };
1730
+ }
1731
+ async placeOrder(input) {
1732
+ const db = getDb();
1733
+ const tenantId = input.tenantId ?? getTenantId();
1734
+ if (!tenantId) throw new Error("@fayz-ai/shop: placeOrder requires a tenant id (configure the shop tenant resolver).");
1735
+ const { data, error } = await db.rpc("shop_place_order", {
1736
+ p_tenant_id: tenantId,
1737
+ ...this.orderRpcArgs(input)
1616
1738
  });
1617
1739
  if (error) throw error;
1618
1740
  const orderId = typeof data === "string" ? data : data?.id ?? data?.order_id ?? data;
1619
1741
  const order = await this.getOrder(orderId);
1620
1742
  if (!order) throw new Error("@fayz-ai/shop: placeOrder did not return a persisted order.");
1743
+ emitOrderEvent("order_confirmation", order);
1744
+ return order;
1745
+ }
1746
+ async findOpenOrderCharge(orderId) {
1747
+ const { data, error } = await getDb().from(T.checkoutSessions).select("id,total,currency,provider,payment_method,charge_payload,expires_at,status").eq("order_id", orderId).eq("status", "open").gt("expires_at", (/* @__PURE__ */ new Date()).toISOString()).order("created_at", { ascending: false }).limit(1);
1748
+ if (error || !data?.length) return null;
1749
+ const row = data[0];
1750
+ const payload = row.charge_payload ?? {};
1751
+ const link = payload.redirectUrl ?? payload.paymentUrl ?? payload.url ?? cartPaymentUrl(payload.url_car, payload.cart_token);
1752
+ if (!payload.brCode && !link) return null;
1753
+ return {
1754
+ sessionId: row.id,
1755
+ total: Number(row.total ?? 0),
1756
+ state: "open",
1757
+ provider: row.provider ?? null,
1758
+ method: row.payment_method ?? null,
1759
+ // Cada adquirente guarda o endereço com o nome dele. A AbacatePay grava
1760
+ // `url`; a Vindi grava o carrinho da Yapay em `url_car` + `cart_token`, e
1761
+ // a URL de pagamento é a junção dos dois — foi por não conhecer esse par
1762
+ // que "Ver cobrança" abria uma janela sem link nenhum, com um link que
1763
+ // estava gravado ali do lado.
1764
+ paymentUrl: link ?? null,
1765
+ // O MESMO mapeamento de openCheckoutSession, e pelas mesmas razões: a
1766
+ // AbacatePay devolve bytes base64 (com prefixo `data:`, que a tela
1767
+ // prefixaria de novo) e a Vindi devolve a URL de um SVG. Desenhar um como
1768
+ // o outro é um <img> quebrado na hora exata em que alguém vai pagar.
1769
+ charge: payload.brCode ? {
1770
+ emv: String(payload.brCode),
1771
+ qrcodeBase64: String(payload.brCodeBase64 ?? "").replace(/^data:image\/\w+;base64,/u, "") || null,
1772
+ qrcodeUrl: String(payload.qrCodeUrl ?? "").trim() || null,
1773
+ expiresAt: row.expires_at ?? null
1774
+ } : null
1775
+ };
1776
+ }
1777
+ async chargeExistingOrder(orderId, options) {
1778
+ const order = await this.getOrder(orderId);
1779
+ if (!order) throw new Error("@fayz-ai/shop: chargeExistingOrder requires an existing order.");
1780
+ if (order.status !== "open" || order.financialStatus !== "pending") {
1781
+ throw new Error("Este pedido n\xE3o est\xE1 aberto e sem pagamento.");
1782
+ }
1783
+ const email = order.customerEmail?.trim();
1784
+ if (!email) {
1785
+ throw new Error("Para cobrar este pedido o cadastro do cliente precisa de e-mail.");
1786
+ }
1787
+ const standing = await this.findOpenOrderCharge(orderId);
1788
+ if (standing) return standing;
1789
+ const address = order.shippingAddress;
1790
+ const session = await this.openCheckoutSession({
1791
+ // Os itens do pedido, sem preço: a sessão reprecifica pelo catálogo, e é
1792
+ // a amarração que confere se o resultado bate com o que o pedido pede.
1793
+ items: order.items.filter((item) => item.productId).map((item) => ({ productId: item.productId, quantity: item.quantity })),
1794
+ customer: { name: order.customerName, email },
1795
+ shippingAddress: address ? {
1796
+ postalCode: address.postal_code ?? "",
1797
+ street: address.street ?? "",
1798
+ number: address.number,
1799
+ complement: address.complement,
1800
+ district: address.district,
1801
+ city: address.city ?? "",
1802
+ state: address.state ?? "",
1803
+ country: address.country,
1804
+ recipient: address.recipient,
1805
+ phone: address.phone
1806
+ } : void 0,
1807
+ discountCode: order.discountCode ?? void 0,
1808
+ paymentMethod: options?.paymentMethod,
1809
+ returnUrl: options?.returnUrl,
1810
+ asLink: options?.asLink
1811
+ });
1812
+ const { error } = await getDb().rpc("shop_bind_session_to_order", {
1813
+ p_session_id: session.sessionId,
1814
+ p_order_id: orderId
1815
+ });
1816
+ if (error) {
1817
+ await getDb().from(T.checkoutSessions).update({ status: "cancelled", failure_reason: "bind_failed" }).eq("id", session.sessionId).then(void 0, () => void 0);
1818
+ const text = String(error.message ?? "");
1819
+ if (/shop_bind_session_to_order/u.test(text) && /find the function|does not exist/iu.test(text)) {
1820
+ throw new Error("Cobrar um pedido salvo ainda n\xE3o foi aplicado nesta loja (migration 0073).");
1821
+ }
1822
+ throw error;
1823
+ }
1824
+ return session;
1825
+ }
1826
+ async reviseOrder(orderId, input) {
1827
+ const args = this.orderRpcArgs(input);
1828
+ const { error } = await getDb().rpc("shop_update_order", {
1829
+ p_order_id: orderId,
1830
+ p_items: args.p_items,
1831
+ p_customer_id: args.p_customer_id,
1832
+ p_customer_name: args.p_customer_name,
1833
+ p_customer_email: args.p_customer_email,
1834
+ p_discount_code: args.p_discount_code,
1835
+ p_notes: args.p_notes,
1836
+ p_shipping_address: args.p_shipping_address,
1837
+ p_payment_method: args.p_payment_method
1838
+ });
1839
+ if (error) {
1840
+ const text = String(error.message ?? "");
1841
+ if (/shop_update_order/u.test(text) && /find the function|does not exist/iu.test(text)) {
1842
+ throw new Error("A edi\xE7\xE3o de pedidos ainda n\xE3o foi aplicada nesta loja (migration 0072).");
1843
+ }
1844
+ throw error;
1845
+ }
1846
+ const order = await this.getOrder(orderId);
1847
+ if (!order) throw new Error("@fayz-ai/shop: reviseOrder did not return a persisted order.");
1621
1848
  return order;
1622
1849
  }
1623
1850
  // ---------------------------------------------------------------------------
@@ -1626,7 +1853,7 @@ var SupabaseShopProvider = class {
1626
1853
  async listCustomers(options) {
1627
1854
  const db = getDb();
1628
1855
  const tenantId = getTenantId();
1629
- let q = db.from(T.customers).select("*").order("first_name", { ascending: true });
1856
+ let q = db.from(V.customers).select("*").order("first_name", { ascending: true });
1630
1857
  if (tenantId) q = q.eq("tenant_id", tenantId);
1631
1858
  for (const filter of customerSearchFilters(options?.search)) q = q.or(filter);
1632
1859
  if (options?.limit) q = q.limit(options.limit);
@@ -1635,7 +1862,7 @@ var SupabaseShopProvider = class {
1635
1862
  return (data ?? []).map(rowToCustomer);
1636
1863
  }
1637
1864
  async getCustomer(id) {
1638
- const { data, error } = await getDb().from(T.customers).select("*").eq("id", id).maybeSingle();
1865
+ const { data, error } = await getDb().from(V.customers).select("*").eq("id", id).maybeSingle();
1639
1866
  if (error) throw error;
1640
1867
  return data ? rowToCustomer(data) : null;
1641
1868
  }
@@ -2331,6 +2558,12 @@ var ORDER_FIELDS = [
2331
2558
  { name: "order_total", label: "Total", type: "money", example: "R$ 189,90" },
2332
2559
  { name: "order_items", label: "Itens", type: "string", example: "1 \xD7 Camiseta Cl\xE1ssica \u2014 R$ 89,90\n2 \xD7 Caneca Esmaltada \u2014 R$ 100,00" },
2333
2560
  { name: "customer_name", label: "Cliente", type: "string", example: "Maria Silva" },
2561
+ // Where the notification GOES — the automation reads its recipient from
2562
+ // these (recipientField), same contract as agenda's customer_phone.
2563
+ { name: "customer_email", label: "E-mail do cliente", type: "string", example: "maria@exemplo.com" },
2564
+ { name: "customer_phone", label: "Telefone do cliente", type: "string", example: "(21) 99889-9889" },
2565
+ // Not in the emitter's payload on purpose: the browser has no reliable
2566
+ // source for the store's public name — the SENDER fills this slot.
2334
2567
  { name: "store_name", label: "Loja", type: "string", example: "Sua Loja" }
2335
2568
  ];
2336
2569
  var TRACKING_FIELD = {
@@ -2364,8 +2597,8 @@ function buildShopDomainEvents() {
2364
2597
  }
2365
2598
  var WHATSAPP_EVENTS = /* @__PURE__ */ new Set(["order_confirmation", "order_shipped", "abandoned_cart"]);
2366
2599
  var WHATSAPP_BODY = {
2367
- order_confirmation: "Oi {{customer_name}}! Recebemos seu pedido #{{order_number}} \u2705\n\nTotal: {{order_total}}\n\nQualquer d\xFAvida \xE9 s\xF3 chamar aqui. \u2014 {{store_name}}",
2368
- order_shipped: "Oi {{customer_name}}! Seu pedido #{{order_number}} saiu para entrega \u{1F69A}\n\nRastreio: {{tracking_code}}\n\n\u2014 {{store_name}}",
2600
+ order_confirmation: "Oi {{customer_name}}! Recebemos seu pedido e ele j\xE1 est\xE1 confirmado por aqui \u2705\n\nPedido: #{{order_number}}\nTotal: {{order_total}}\n\nVamos te avisando por aqui a cada passo \u2014 e qualquer d\xFAvida, \xE9 s\xF3 responder esta mensagem.\n\nEquipe {{store_name}} agradece a prefer\xEAncia!",
2601
+ order_shipped: "Oi {{customer_name}}! Boa not\xEDcia: seu pedido saiu para entrega \u{1F69A}\n\nPedido: #{{order_number}}\nC\xF3digo de rastreio: {{tracking_code}}\n\nQualquer coisa no caminho, \xE9 s\xF3 responder esta mensagem que a gente resolve.\n\nEquipe {{store_name}} agradece a prefer\xEAncia!",
2369
2602
  abandoned_cart: "Oi {{customer_name}}! Vi que ficaram itens no seu carrinho \u{1F6D2}\n\nQuer que eu segure para voc\xEA?\n{{cart_url}}\n\n\u2014 {{store_name}}"
2370
2603
  };
2371
2604
  function buildShopMessageDefaults() {