@fayz-ai/shop 0.14.2 → 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-U6HV3LVP.js → chunk-EUFSFVGS.js} +67 -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-2WACO2TG.js → chunk-WWQICATY.js} +3 -3
  8. package/dist/{chunk-2WACO2TG.js.map → chunk-WWQICATY.js.map} +1 -1
  9. package/dist/data/checkout-gateways.d.ts +83 -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 +106 -2
  16. package/dist/data/provider.d.ts.map +1 -1
  17. package/dist/data/supabase-provider.d.ts +39 -2
  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 +2 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +329 -47
  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 +96 -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-STRNMFC4.js.map +0 -1
  44. package/dist/chunk-U6HV3LVP.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-2WACO2TG.js';
4
- export { getShopProvider, resetShopProvider, setShopProvider } from './chunk-2WACO2TG.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-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';
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;
@@ -195,18 +229,37 @@ async function callShopFunction(db, slug, body) {
195
229
 
196
230
  // src/data/checkout-gateways.ts
197
231
  var GATEWAYS = Object.freeze({
198
- abacatepay: Object.freeze({ open: "shop-checkout-open", settle: "shop-checkout-settle" }),
199
- vindi: Object.freeze({ open: "vindi-checkout-open", settle: "vindi-checkout-settle" })
232
+ abacatepay: Object.freeze({ open: "shop-checkout-open", settle: "shop-checkout-settle", link: true, card: false, label: "AbacatePay" }),
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) {
203
241
  return checkoutGateway(provider) !== null;
204
242
  }
243
+ function supportsPaymentLink(provider) {
244
+ return checkoutGateway(provider)?.link === true;
245
+ }
246
+ function supportsCard(provider) {
247
+ return checkoutGateway(provider)?.card === true;
248
+ }
249
+ function gatewayLabel(provider) {
250
+ return checkoutGateway(provider)?.label ?? null;
251
+ }
205
252
  function checkoutGateway(provider) {
206
253
  const slug = String(provider ?? "").trim();
207
254
  if (!slug) return null;
208
255
  return GATEWAYS[slug] ?? null;
209
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
+ }
210
263
 
211
264
  // src/data/customer-search.ts
212
265
  var COLUMNS = ["first_name", "last_name", "email", "phone"];
@@ -408,13 +461,13 @@ function shippingZonePayload(input) {
408
461
  if (input.carrier !== void 0) payload.carrier = input.carrier || null;
409
462
  if (input.postalFrom !== void 0) payload.postal_from = toPostalDigits(input.postalFrom);
410
463
  if (input.postalTo !== void 0) payload.postal_to = toPostalDigits(input.postalTo);
411
- if (input.rate !== void 0) payload.rate = input.rate;
464
+ if (input.rate != null) payload.rate = input.rate;
412
465
  if (input.freeAbove !== void 0) payload.free_above = input.freeAbove ?? null;
413
466
  if (input.etaMinDays !== void 0) payload.eta_min_days = input.etaMinDays ?? null;
414
467
  if (input.etaMaxDays !== void 0) payload.eta_max_days = input.etaMaxDays ?? null;
415
- if (input.active !== void 0) payload.active = input.active;
416
- if (input.sortOrder !== void 0) payload.sort_order = input.sortOrder;
417
- 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;
418
471
  if (input.profileId !== void 0) payload.profile_id = input.profileId ?? null;
419
472
  return payload;
420
473
  }
@@ -508,6 +561,7 @@ function rowToOrderItem(row) {
508
561
  id: row.id,
509
562
  orderId: row.order_id,
510
563
  productId: row.product_id ?? null,
564
+ variantId: row.variant_id ?? null,
511
565
  name: row.name,
512
566
  sku: row.sku ?? null,
513
567
  quantity: row.quantity ?? 0,
@@ -703,6 +757,13 @@ function rowToGiftCard(row) {
703
757
  updatedAt: row.updated_at ?? row.created_at
704
758
  };
705
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
+ }
706
767
  var SupabaseShopProvider = class {
707
768
  // ---------------------------------------------------------------------------
708
769
  // Products
@@ -735,7 +796,7 @@ var SupabaseShopProvider = class {
735
796
  * (authenticated tenant member, who CAN see archived rows) consistent. */
736
797
  async getProductVariants(productId, opts) {
737
798
  const db = getDb();
738
- let variantQuery = db.from(T.productVariants).select("*").eq("product_id", productId);
799
+ let variantQuery = db.from(V.productVariants).select("*").eq("shop_product_id", productId);
739
800
  if (!opts?.includeArchived) variantQuery = variantQuery.eq("status", "active");
740
801
  const [optionsRes, variantsRes] = await Promise.all([
741
802
  db.from(T.productOptions).select("*").eq("product_id", productId).order("position", { ascending: true }),
@@ -799,7 +860,7 @@ var SupabaseShopProvider = class {
799
860
  if (Object.keys(input.options).length === 0) {
800
861
  throw new Error("Uma variante sem op\xE7\xF5es \xE9 a variante padr\xE3o (0037), criada pelo banco. Declare ao menos um eixo.");
801
862
  }
802
- 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);
803
864
  if (readError) throw readError;
804
865
  const wantedKey = variantOptionsKey(input.options);
805
866
  const match = (existing ?? []).find((row) => variantOptionsKey(row.options ?? {}) === wantedKey);
@@ -846,6 +907,36 @@ var SupabaseShopProvider = class {
846
907
  });
847
908
  return target;
848
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
+ }
849
940
  async createProduct(input) {
850
941
  const db = getDb();
851
942
  const tenantId = getTenantId();
@@ -1028,7 +1119,7 @@ var SupabaseShopProvider = class {
1028
1119
  async listOrders(options) {
1029
1120
  const db = getDb();
1030
1121
  const tenantId = getTenantId();
1031
- let q = db.from(T.orders).select(`*, items:${T.orderItems}(*)`);
1122
+ let q = db.from(V.orders).select(`*, items:${V.orderItems}(*)`);
1032
1123
  if (tenantId) q = q.eq("tenant_id", tenantId);
1033
1124
  if (options?.status) q = q.eq("status", options.status);
1034
1125
  if (options?.financialStatus) q = q.eq("financial_status", options.financialStatus);
@@ -1043,9 +1134,14 @@ var SupabaseShopProvider = class {
1043
1134
  return (data ?? []).map(rowToOrder);
1044
1135
  }
1045
1136
  async getOrder(id) {
1046
- 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();
1047
1139
  if (error) throw error;
1048
- 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;
1049
1145
  }
1050
1146
  async createOrder(input) {
1051
1147
  const db = getDb();
@@ -1453,11 +1549,18 @@ var SupabaseShopProvider = class {
1453
1549
  async openCheckoutSession(input) {
1454
1550
  const tenantId = getTenantId();
1455
1551
  if (!tenantId) throw new Error("@fayz-ai/shop: openCheckoutSession requires a tenant id.");
1456
- 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
+ );
1457
1557
  const gateway = checkoutGateway(provider);
1458
1558
  if (!gateway) {
1459
1559
  throw new Error("Esta loja ainda n\xE3o tem um meio de cobran\xE7a conectado.");
1460
1560
  }
1561
+ if (input.asLink && !gateway.link) {
1562
+ throw new Error("O meio de cobran\xE7a desta loja ainda n\xE3o gera link de pagamento.");
1563
+ }
1461
1564
  const { ok, status, payload } = await callShopFunction(getDb(), gateway.open, {
1462
1565
  tenantId,
1463
1566
  items: input.items.map((item) => ({
@@ -1467,16 +1570,42 @@ var SupabaseShopProvider = class {
1467
1570
  })),
1468
1571
  customer: input.customer,
1469
1572
  shippingAddress: input.shippingAddress ?? null,
1470
- discountCode: input.discountCode ?? null
1573
+ discountCode: input.discountCode ?? null,
1574
+ // What the buyer chose, in the store's words. The gateway translates it to
1575
+ // whatever its own API calls that method, and refuses if the merchant
1576
+ // never enabled it — which is a different answer from "no acquirer".
1577
+ method: input.paymentMethod ?? null,
1578
+ returnUrl: input.returnUrl ?? null,
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
1471
1591
  });
1472
1592
  if (!ok || !payload?.sessionId) {
1473
- 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."));
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."));
1594
+ failure.status = status;
1595
+ if (payload?.code) failure.code = String(payload.code);
1596
+ throw failure;
1474
1597
  }
1475
1598
  return {
1476
1599
  sessionId: payload.sessionId,
1477
1600
  total: Number(payload.total ?? 0),
1478
1601
  state: "open",
1479
1602
  provider,
1603
+ method: payload.method ?? input.paymentMethod ?? null,
1604
+ // One field for two situations that are the same thing from here: a link
1605
+ // the merchant sends, and the page a card buyer is redirected to. Both are
1606
+ // "this payment happens on the acquirer's page", and the caller decides
1607
+ // whether to copy it or to go there.
1608
+ paymentUrl: payload.redirectUrl ?? payload.paymentUrl ?? null,
1480
1609
  charge: payload.brCode ? {
1481
1610
  emv: payload.brCode,
1482
1611
  // AbacatePay embeds the `data:` prefix; the type promises bare
@@ -1485,10 +1614,10 @@ var SupabaseShopProvider = class {
1485
1614
  //
1486
1615
  // Vindi sends no base64 at all — its `qrCodeUrl` is a URL to an SVG,
1487
1616
  // which cannot be squeezed into this field without lying about what
1488
- // it is. The buyer still gets the copy-and-paste code, which is what
1489
- // actually pays; a rendered QR for Vindi waits for the payment view
1490
- // to learn the second shape.
1617
+ // it is. It travels beside it instead, and the view draws whichever
1618
+ // arrived.
1491
1619
  qrcodeBase64: String(payload.brCodeBase64 ?? "").replace(/^data:image\/\w+;base64,/u, "") || null,
1620
+ qrcodeUrl: String(payload.qrCodeUrl ?? "").trim() || null,
1492
1621
  expiresAt: payload.expiresAt ?? ""
1493
1622
  } : null
1494
1623
  };
@@ -1502,27 +1631,60 @@ var SupabaseShopProvider = class {
1502
1631
  * who switches acquirer mid-checkout would otherwise have the new gateway
1503
1632
  * asked about a transaction that lives in the old one.
1504
1633
  */
1505
- async settleCheckoutSession(sessionId, provider) {
1634
+ async settleCheckoutSession(sessionId, provider, chargeToken) {
1506
1635
  const tenantId = getTenantId();
1507
1636
  if (!tenantId) return { state: "unknown" };
1508
1637
  const gateway = checkoutGateway(String(provider ?? "").trim() || "abacatepay");
1509
1638
  if (!gateway) return { state: "unknown" };
1510
1639
  try {
1511
- const { ok, payload } = await callShopFunction(getDb(), gateway.settle, { tenantId, sessionId });
1640
+ const { ok, payload } = await callShopFunction(getDb(), gateway.settle, {
1641
+ tenantId,
1642
+ sessionId,
1643
+ ...chargeToken ? { chargeToken } : {}
1644
+ }, { timeoutMs: 12e3 });
1512
1645
  if (!ok || !payload?.state) return { state: "unknown" };
1513
1646
  return { state: payload.state, orderId: payload.orderId ?? null, reason: payload.reason ?? null };
1514
1647
  } catch {
1515
1648
  return { state: "unknown" };
1516
1649
  }
1517
1650
  }
1651
+ /**
1652
+ * Remember the contact details of whoever is signed in.
1653
+ *
1654
+ * Called when the buyer leaves the contact step, not when an order completes:
1655
+ * an abandoned checkout teaches the store nothing, and the person who most
1656
+ * needs the field remembered is the one who did not finish. The function
1657
+ * decides what may be written and to whose row — this only carries the values.
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
+ }
1669
+ async saveCustomerContact(input) {
1670
+ const tenantId = getTenantId();
1671
+ if (!tenantId) return;
1672
+ const { error } = await getDb().rpc("shop_save_customer_contact", {
1673
+ p_tenant_id: tenantId,
1674
+ p_phone: input.phone ?? null,
1675
+ p_document: input.document ?? null
1676
+ });
1677
+ if (error) console.warn("[shop] could not save customer contact:", error.message);
1678
+ }
1518
1679
  // Trusted placement — delegates all price/discount/inventory authority to the
1519
1680
  // shop_place_order RPC (SECURITY DEFINER). The browser never sets a price.
1520
- async placeOrder(input) {
1521
- const db = getDb();
1522
- const tenantId = input.tenantId ?? getTenantId();
1523
- if (!tenantId) throw new Error("@fayz-ai/shop: placeOrder requires a tenant id (configure the shop tenant resolver).");
1524
- const { data, error } = await db.rpc("shop_place_order", {
1525
- 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 {
1526
1688
  // variant_id rides on each line (0029): the RPC prices and decrements
1527
1689
  // the variant when present, product-level otherwise. Pre-0029 servers
1528
1690
  // ignore unknown jsonb keys, so this stays compatible either way.
@@ -1564,11 +1726,125 @@ var SupabaseShopProvider = class {
1564
1726
  // a pre-0029 server rejects unknown NAMED arguments (unlike unknown
1565
1727
  // jsonb keys). Absent metadata therefore must not send the key at all.
1566
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)
1567
1738
  });
1568
1739
  if (error) throw error;
1569
1740
  const orderId = typeof data === "string" ? data : data?.id ?? data?.order_id ?? data;
1570
1741
  const order = await this.getOrder(orderId);
1571
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.");
1572
1848
  return order;
1573
1849
  }
1574
1850
  // ---------------------------------------------------------------------------
@@ -1577,7 +1853,7 @@ var SupabaseShopProvider = class {
1577
1853
  async listCustomers(options) {
1578
1854
  const db = getDb();
1579
1855
  const tenantId = getTenantId();
1580
- 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 });
1581
1857
  if (tenantId) q = q.eq("tenant_id", tenantId);
1582
1858
  for (const filter of customerSearchFilters(options?.search)) q = q.or(filter);
1583
1859
  if (options?.limit) q = q.limit(options.limit);
@@ -1586,7 +1862,7 @@ var SupabaseShopProvider = class {
1586
1862
  return (data ?? []).map(rowToCustomer);
1587
1863
  }
1588
1864
  async getCustomer(id) {
1589
- 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();
1590
1866
  if (error) throw error;
1591
1867
  return data ? rowToCustomer(data) : null;
1592
1868
  }
@@ -2282,6 +2558,12 @@ var ORDER_FIELDS = [
2282
2558
  { name: "order_total", label: "Total", type: "money", example: "R$ 189,90" },
2283
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" },
2284
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.
2285
2567
  { name: "store_name", label: "Loja", type: "string", example: "Sua Loja" }
2286
2568
  ];
2287
2569
  var TRACKING_FIELD = {
@@ -2315,8 +2597,8 @@ function buildShopDomainEvents() {
2315
2597
  }
2316
2598
  var WHATSAPP_EVENTS = /* @__PURE__ */ new Set(["order_confirmation", "order_shipped", "abandoned_cart"]);
2317
2599
  var WHATSAPP_BODY = {
2318
- 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}}",
2319
- 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!",
2320
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}}"
2321
2603
  };
2322
2604
  function buildShopMessageDefaults() {
@@ -2365,6 +2647,6 @@ function clampQuality(q) {
2365
2647
  return Math.min(100, Math.max(20, Math.round(q)));
2366
2648
  }
2367
2649
 
2368
- export { CHARGEABLE_PROVIDERS, EMAIL_PLACEHOLDERS, EMAIL_PREVIEW_SAMPLE, IMAGE_SRCSET_WIDTHS, SHOP_EMAIL_EVENTS, SupabaseShopProvider, applyDiscount, buildShopDomainEvents, buildShopMessageDefaults, checkInventory, checkoutGateway, computeCartTotals, createSupabaseShopProvider, documentDigits, documentKind, formatDocument, getCustomerOrders, getShopFunctionsUrl, imageSrcSet, imageVariantUrl, isChargeableProvider, isStorageImageUrl, isValidCnpj, isValidCpf, isValidDocument, listShippingLabelProviders, optimizeImageForUpload, planUpload, registerShippingLabelProvider, renderEmailTemplate, resetShippingLabelProviders, resolveEmailTemplates, resolveShippingLabelProvider, setShopFunctionsUrl, validateDiscount };
2650
+ export { CHARGEABLE_PROVIDERS, EMAIL_PLACEHOLDERS, EMAIL_PREVIEW_SAMPLE, IMAGE_SRCSET_WIDTHS, SHOP_EMAIL_EVENTS, SupabaseShopProvider, applyDiscount, buildShopDomainEvents, buildShopMessageDefaults, checkInventory, checkoutGateway, computeCartTotals, createSupabaseShopProvider, documentDigits, documentKind, formatDocument, gatewayLabel, getCustomerOrders, getShopFunctionsUrl, imageSrcSet, imageVariantUrl, isChargeableProvider, isStorageImageUrl, isValidCnpj, isValidCpf, isValidDocument, listShippingLabelProviders, optimizeImageForUpload, planUpload, registerShippingLabelProvider, renderEmailTemplate, resetShippingLabelProviders, resolveEmailTemplates, resolveShippingLabelProvider, setShopFunctionsUrl, supportsCard, supportsPaymentLink, validateDiscount };
2369
2651
  //# sourceMappingURL=index.js.map
2370
2652
  //# sourceMappingURL=index.js.map