@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.
- package/dist/{chunk-STRNMFC4.js → chunk-26YK5V6P.js} +35 -6
- package/dist/chunk-26YK5V6P.js.map +1 -0
- package/dist/{chunk-U6HV3LVP.js → chunk-EUFSFVGS.js} +67 -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-2WACO2TG.js → chunk-WWQICATY.js} +3 -3
- package/dist/{chunk-2WACO2TG.js.map → chunk-WWQICATY.js.map} +1 -1
- package/dist/data/checkout-gateways.d.ts +83 -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 +106 -2
- package/dist/data/provider.d.ts.map +1 -1
- package/dist/data/supabase-provider.d.ts +39 -2
- 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 +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +329 -47
- 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 +96 -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-STRNMFC4.js.map +0 -1
- 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-
|
|
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;
|
|
@@ -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
|
|
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
|
|
416
|
-
if (input.sortOrder
|
|
417
|
-
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;
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
1489
|
-
//
|
|
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, {
|
|
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
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
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(
|
|
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(
|
|
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}}
|
|
2319
|
-
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!",
|
|
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
|