@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.
- package/dist/{chunk-STRNMFC4.js → chunk-26YK5V6P.js} +35 -6
- package/dist/chunk-26YK5V6P.js.map +1 -0
- package/dist/{chunk-Q3N4PS6P.js → chunk-EUFSFVGS.js} +64 -5
- package/dist/chunk-EUFSFVGS.js.map +1 -0
- package/dist/{chunk-3GERSY5F.js → chunk-UVQ6UUDN.js} +5 -2
- package/dist/chunk-UVQ6UUDN.js.map +1 -0
- package/dist/{chunk-7GIWXT5D.js → chunk-WWQICATY.js} +3 -3
- package/dist/{chunk-7GIWXT5D.js.map → chunk-WWQICATY.js.map} +1 -1
- package/dist/data/checkout-gateways.d.ts +13 -0
- package/dist/data/checkout-gateways.d.ts.map +1 -1
- package/dist/data/functions.d.ts +3 -1
- package/dist/data/functions.d.ts.map +1 -1
- package/dist/data/mock-provider.d.ts +4 -0
- package/dist/data/mock-provider.d.ts.map +1 -1
- package/dist/data/provider.d.ts +90 -1
- package/dist/data/provider.d.ts.map +1 -1
- package/dist/data/supabase-provider.d.ts +26 -1
- package/dist/data/supabase-provider.d.ts.map +1 -1
- package/dist/data/tables.d.ts +51 -0
- package/dist/data/tables.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +273 -40
- package/dist/index.js.map +1 -1
- package/dist/inventory/postgrest-double.d.ts +9 -0
- package/dist/inventory/postgrest-double.d.ts.map +1 -1
- package/dist/inventory/supabase-stock-store.d.ts.map +1 -1
- package/dist/inventory/vocabulary.d.ts.map +1 -1
- package/dist/inventory.js +1 -1
- package/dist/lib/commerce.d.ts.map +1 -1
- package/dist/lib/message-events.d.ts.map +1 -1
- package/dist/lib/order-events.d.ts +5 -0
- package/dist/lib/order-events.d.ts.map +1 -0
- package/dist/mock.js +2 -2
- package/dist/rules/adapter.d.ts +3 -0
- package/dist/rules/adapter.d.ts.map +1 -1
- package/dist/rules.js +2 -2
- package/dist/runtime.js +3 -3
- package/dist/types.d.ts +58 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/dist/chunk-3GERSY5F.js.map +0 -1
- package/dist/chunk-Q3N4PS6P.js.map +0 -1
- 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-
|
|
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-
|
|
3
|
-
import { getShopProvider } from './chunk-
|
|
4
|
-
export { getShopProvider, resetShopProvider, setShopProvider } from './chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
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
|
|
425
|
-
if (input.sortOrder
|
|
426
|
-
if (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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
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(
|
|
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(
|
|
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}}
|
|
2368
|
-
order_shipped: "Oi {{customer_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() {
|