@fayz-ai/shop 0.10.0 → 0.12.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-IL6GA4ER.js → chunk-2WACO2TG.js} +3 -3
- package/dist/{chunk-IL6GA4ER.js.map → chunk-2WACO2TG.js.map} +1 -1
- package/dist/{chunk-RGTIJQJL.js → chunk-6BO2TMZ5.js} +3 -3
- package/dist/{chunk-RGTIJQJL.js.map → chunk-6BO2TMZ5.js.map} +1 -1
- package/dist/{chunk-XOP6SHPB.js → chunk-NWW3WJ7Y.js} +4 -10
- package/dist/chunk-NWW3WJ7Y.js.map +1 -0
- package/dist/{chunk-XERLK5LD.js → chunk-STRNMFC4.js} +28 -5
- package/dist/chunk-STRNMFC4.js.map +1 -0
- package/dist/{chunk-U3O2I3RM.js → chunk-U6HV3LVP.js} +385 -23
- package/dist/chunk-U6HV3LVP.js.map +1 -0
- package/dist/data/checkout-gateways.d.ts +28 -0
- package/dist/data/checkout-gateways.d.ts.map +1 -0
- package/dist/data/customer-search.d.ts +3 -0
- package/dist/data/customer-search.d.ts.map +1 -0
- package/dist/data/functions.d.ts +9 -0
- package/dist/data/functions.d.ts.map +1 -0
- package/dist/data/media.d.ts +37 -0
- package/dist/data/media.d.ts.map +1 -0
- package/dist/data/mock-provider.d.ts +40 -8
- package/dist/data/mock-provider.d.ts.map +1 -1
- package/dist/data/provider.d.ts +205 -7
- package/dist/data/provider.d.ts.map +1 -1
- package/dist/data/supabase-provider.d.ts +118 -5
- package/dist/data/supabase-provider.d.ts.map +1 -1
- package/dist/data/tables.d.ts +3 -0
- package/dist/data/tables.d.ts.map +1 -1
- package/dist/entities/index.d.ts +0 -1
- package/dist/entities/index.d.ts.map +1 -1
- package/dist/entities.js +2 -2
- package/dist/index.d.ts +16 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +925 -60
- package/dist/index.js.map +1 -1
- package/dist/inventory/index.d.ts +2 -2
- package/dist/inventory/index.d.ts.map +1 -1
- package/dist/inventory/stock-types.d.ts +60 -1
- package/dist/inventory/stock-types.d.ts.map +1 -1
- package/dist/inventory/supabase-stock-store.d.ts +1 -0
- package/dist/inventory/supabase-stock-store.d.ts.map +1 -1
- package/dist/inventory.js +1 -1
- package/dist/lib/commerce.d.ts.map +1 -1
- package/dist/lib/document.d.ts +13 -0
- package/dist/lib/document.d.ts.map +1 -0
- package/dist/lib/variant-matrix.d.ts +120 -0
- package/dist/lib/variant-matrix.d.ts.map +1 -0
- package/dist/mock.js +2 -2
- package/dist/providers/checkout.d.ts +129 -0
- package/dist/providers/checkout.d.ts.map +1 -0
- package/dist/providers/index.d.ts +5 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/shipping.d.ts +43 -0
- package/dist/providers/shipping.d.ts.map +1 -0
- package/dist/providers.js +70 -0
- package/dist/providers.js.map +1 -0
- package/dist/runtime.js +3 -3
- package/dist/shipping/label-provider.d.ts +43 -0
- package/dist/shipping/label-provider.d.ts.map +1 -0
- package/dist/types.d.ts +475 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -4
- package/dist/chunk-U3O2I3RM.js.map +0 -1
- package/dist/chunk-XERLK5LD.js.map +0 -1
- package/dist/chunk-XOP6SHPB.js.map +0 -1
- package/dist/entities/sidecar.d.ts +0 -9
- package/dist/entities/sidecar.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { T } from './chunk-
|
|
2
|
-
export { DEFAULT_STOCK_TRACKING, MockStockStore, 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, resolveStockStore, createSupabaseStockStore, OPENING_BALANCE_REASON, rowToStockLocation } from './chunk-STRNMFC4.js';
|
|
2
|
+
export { DEFAULT_STOCK_TRACKING, MockStockStore, OWN_FULFILLMENT, STOCK_TRACKING_MODES, StockMovementError, SupabaseStockStore, createMockStockStore, createSupabaseStockStore, listStockMovementKinds, listStockReasonCodes, mirroredInventoryCount, registerStockMovementKind, registerStockReasonCode, registerStockStore, replayStockLedger, resolveStockStore, validateStockMovement } from './chunk-STRNMFC4.js';
|
|
3
|
+
import { getShopProvider } from './chunk-2WACO2TG.js';
|
|
4
|
+
export { getShopProvider, resetShopProvider, setShopProvider } from './chunk-2WACO2TG.js';
|
|
5
5
|
import './chunk-FQPGE3YN.js';
|
|
6
|
-
|
|
6
|
+
import { normalizeVariantAxes, validateVariantAxes, variantOptionsKey, storagePathOf, mergeMediaFiles, productImageToMediaFile, displayNameOf, mediaKindOf } from './chunk-U6HV3LVP.js';
|
|
7
|
+
export { MAX_VARIANT_AXES, MAX_VARIANT_COMBINATIONS, MockShopProvider, applyVariantMatrix, axesFromOptions, createMockShopProvider, displayNameOf, formatVariantCombination, hasOnlyDefaultVariant, leavesNothingSellable, mediaKindOf, mergeMediaFiles, normalizeVariantAxes, planVariantMatrix, productImageToMediaFile, storagePathOf, validateVariantAxes, variantCombinations, variantOptionsKey } from './chunk-U6HV3LVP.js';
|
|
7
8
|
export { MOCK_CATEGORIES, MOCK_DISCOUNTS, MOCK_PRODUCTS, buildMockCatalog, svgPlaceholder } from './chunk-IKNGCRUM.js';
|
|
8
|
-
import { createSupabaseEntityStore } from './chunk-
|
|
9
|
+
import { createSupabaseEntityStore } from './chunk-6BO2TMZ5.js';
|
|
9
10
|
import { getShopTenantId } from './chunk-KLDS7IPU.js';
|
|
10
11
|
export { getShopTenantId, setShopTenantResolver } from './chunk-KLDS7IPU.js';
|
|
11
|
-
import { resolveEntityStore
|
|
12
|
+
import { resolveEntityStore } from './chunk-NWW3WJ7Y.js';
|
|
12
13
|
import './chunk-3GERSY5F.js';
|
|
13
14
|
import { getSupabaseClientOptional } from '@fayz-ai/core';
|
|
14
15
|
|
|
@@ -19,6 +20,9 @@ function round2(n) {
|
|
|
19
20
|
async function validateDiscount(input, provider = getShopProvider()) {
|
|
20
21
|
const code = input.code.trim();
|
|
21
22
|
if (!code) return { valid: false, code: null, type: null, value: 0, reason: "empty" };
|
|
23
|
+
if (typeof provider.validateDiscountCode === "function") {
|
|
24
|
+
return await provider.validateDiscountCode(code, input.subtotal);
|
|
25
|
+
}
|
|
22
26
|
const list = await provider.listDiscounts({ status: "active" });
|
|
23
27
|
const discount = list.find((d) => (d.code ?? "").toUpperCase() === code.toUpperCase());
|
|
24
28
|
if (!discount) return { valid: false, code: null, type: null, value: 0, reason: "not_found" };
|
|
@@ -72,7 +76,7 @@ async function computeCartTotals(input, provider = getShopProvider()) {
|
|
|
72
76
|
let discountCode = null;
|
|
73
77
|
if (input.discountCode) {
|
|
74
78
|
const applied = applyDiscount(
|
|
75
|
-
await validateDiscount({ code: input.discountCode, customerId: input.customerId }, provider),
|
|
79
|
+
await validateDiscount({ code: input.discountCode, subtotal, customerId: input.customerId }, provider),
|
|
76
80
|
subtotal
|
|
77
81
|
);
|
|
78
82
|
discountTotal = applied.discountTotal;
|
|
@@ -86,6 +90,67 @@ async function computeCartTotals(input, provider = getShopProvider()) {
|
|
|
86
90
|
async function getCustomerOrders(customerId, provider = getShopProvider()) {
|
|
87
91
|
return provider.listOrders({ customerId, limit: 100 });
|
|
88
92
|
}
|
|
93
|
+
|
|
94
|
+
// src/data/functions.ts
|
|
95
|
+
var _functionsUrl = null;
|
|
96
|
+
function setShopFunctionsUrl(url) {
|
|
97
|
+
_functionsUrl = url ? url.replace(/\/+$/u, "") : null;
|
|
98
|
+
}
|
|
99
|
+
function getShopFunctionsUrl() {
|
|
100
|
+
return _functionsUrl;
|
|
101
|
+
}
|
|
102
|
+
async function callShopFunction(db, slug, body) {
|
|
103
|
+
const override = getShopFunctionsUrl();
|
|
104
|
+
if (override) {
|
|
105
|
+
let token = null;
|
|
106
|
+
try {
|
|
107
|
+
token = (await db?.auth?.getSession?.())?.data?.session?.access_token ?? null;
|
|
108
|
+
} catch {
|
|
109
|
+
}
|
|
110
|
+
const response = await fetch(`${override}/${slug}`, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: {
|
|
113
|
+
"Content-Type": "application/json",
|
|
114
|
+
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
115
|
+
},
|
|
116
|
+
body: JSON.stringify(body)
|
|
117
|
+
});
|
|
118
|
+
const payload2 = await response.json().catch(() => null);
|
|
119
|
+
return { ok: response.ok, status: response.status, payload: payload2 };
|
|
120
|
+
}
|
|
121
|
+
const { data, error } = await db.functions.invoke(slug, { body });
|
|
122
|
+
if (!error) return { ok: true, status: 200, payload: data };
|
|
123
|
+
const status = Number(error?.context?.status ?? 502);
|
|
124
|
+
let payload = null;
|
|
125
|
+
try {
|
|
126
|
+
payload = await error?.context?.json?.();
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
return { ok: false, status, payload: payload ?? { error: error.message } };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/data/checkout-gateways.ts
|
|
133
|
+
var GATEWAYS = Object.freeze({
|
|
134
|
+
abacatepay: Object.freeze({ open: "shop-checkout-open", settle: "shop-checkout-settle" }),
|
|
135
|
+
vindi: Object.freeze({ open: "vindi-checkout-open", settle: "vindi-checkout-settle" })
|
|
136
|
+
});
|
|
137
|
+
var CHARGEABLE_PROVIDERS = Object.freeze(Object.keys(GATEWAYS));
|
|
138
|
+
function isChargeableProvider(provider) {
|
|
139
|
+
return checkoutGateway(provider) !== null;
|
|
140
|
+
}
|
|
141
|
+
function checkoutGateway(provider) {
|
|
142
|
+
const slug = String(provider ?? "").trim();
|
|
143
|
+
if (!slug) return null;
|
|
144
|
+
return GATEWAYS[slug] ?? null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/data/customer-search.ts
|
|
148
|
+
var COLUMNS = ["first_name", "last_name", "email", "phone"];
|
|
149
|
+
function customerSearchFilters(term) {
|
|
150
|
+
return String(term ?? "").trim().split(/\s+/u).map((word) => word.replace(/[%,()]/gu, "")).filter(Boolean).map((word) => COLUMNS.map((column) => `${column}.ilike.%${word}%`).join(","));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/data/supabase-provider.ts
|
|
89
154
|
function getDb() {
|
|
90
155
|
const supabase = getSupabaseClientOptional();
|
|
91
156
|
if (!supabase) throw new Error("@fayz-ai/shop: Supabase client not initialized. Call setGlobalSupabaseClient() first.");
|
|
@@ -103,6 +168,95 @@ function toPostalDigits(value) {
|
|
|
103
168
|
const digits = String(value ?? "").replace(/\D/g, "");
|
|
104
169
|
return digits ? digits.padStart(8, "0").slice(0, 8) : void 0;
|
|
105
170
|
}
|
|
171
|
+
function rowToVariant(row) {
|
|
172
|
+
return {
|
|
173
|
+
id: row.id,
|
|
174
|
+
productId: row.product_id,
|
|
175
|
+
options: row.options && typeof row.options === "object" ? row.options : {},
|
|
176
|
+
sku: row.sku ?? null,
|
|
177
|
+
// numeric(12,2) arrives as a string on the wire; Number() once, here.
|
|
178
|
+
price: row.price == null ? null : Number(row.price),
|
|
179
|
+
compareAtPrice: row.compare_at_price == null ? null : Number(row.compare_at_price),
|
|
180
|
+
inventoryCount: row.inventory_count ?? 0,
|
|
181
|
+
position: row.position ?? 0,
|
|
182
|
+
status: row.status ?? "active",
|
|
183
|
+
imageId: row.image_id ?? null,
|
|
184
|
+
isDefaultVariant: row.is_default_variant ?? false,
|
|
185
|
+
// Null inherits the product's, the way `price` has since 0028.
|
|
186
|
+
weight: numOrNull(row.weight),
|
|
187
|
+
weightUnit: row.weight_unit ?? null,
|
|
188
|
+
heightCm: numOrNull(row.height_cm),
|
|
189
|
+
widthCm: numOrNull(row.width_cm),
|
|
190
|
+
lengthCm: numOrNull(row.length_cm)
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function variantPatch(input) {
|
|
194
|
+
const patch = {};
|
|
195
|
+
const map = {
|
|
196
|
+
sku: "sku",
|
|
197
|
+
price: "price",
|
|
198
|
+
compareAtPrice: "compare_at_price",
|
|
199
|
+
position: "position",
|
|
200
|
+
status: "status",
|
|
201
|
+
imageId: "image_id",
|
|
202
|
+
weight: "weight",
|
|
203
|
+
weightUnit: "weight_unit",
|
|
204
|
+
heightCm: "height_cm",
|
|
205
|
+
widthCm: "width_cm",
|
|
206
|
+
lengthCm: "length_cm"
|
|
207
|
+
};
|
|
208
|
+
for (const [key, column] of Object.entries(map)) {
|
|
209
|
+
const value = input[key];
|
|
210
|
+
if (value === void 0) continue;
|
|
211
|
+
patch[column] = key === "sku" && typeof value === "string" && value.trim() === "" ? null : value;
|
|
212
|
+
}
|
|
213
|
+
return patch;
|
|
214
|
+
}
|
|
215
|
+
function rowToStoreSettings(row) {
|
|
216
|
+
const bool = (v) => v == null ? void 0 : Boolean(v);
|
|
217
|
+
const methods = Array.isArray(row.payment_methods) && row.payment_methods.length > 0 ? row.payment_methods : void 0;
|
|
218
|
+
const fields = row.checkout_fields && typeof row.checkout_fields === "object" && Object.keys(row.checkout_fields).length > 0 ? row.checkout_fields : void 0;
|
|
219
|
+
return {
|
|
220
|
+
currency: row.currency ?? void 0,
|
|
221
|
+
locale: row.locale ?? void 0,
|
|
222
|
+
showOutOfStock: bool(row.show_out_of_stock),
|
|
223
|
+
requireSku: bool(row.require_sku),
|
|
224
|
+
stockOwnership: row.stock_ownership ?? void 0,
|
|
225
|
+
paymentMethods: methods,
|
|
226
|
+
paymentProvider: row.payment_provider ?? void 0,
|
|
227
|
+
guestCheckout: bool(row.guest_checkout),
|
|
228
|
+
requireConfirmedEmail: bool(row.require_confirmed_email),
|
|
229
|
+
marketingOptIn: bool(row.marketing_opt_in),
|
|
230
|
+
discountsEnabled: bool(row.discounts_enabled),
|
|
231
|
+
checkoutFields: fields,
|
|
232
|
+
autoFulfill: row.auto_fulfill ?? void 0,
|
|
233
|
+
notifyNewOrder: bool(row.notify_new_order),
|
|
234
|
+
notifyLowStock: bool(row.notify_low_stock),
|
|
235
|
+
handlingNote: row.handling_note ?? void 0,
|
|
236
|
+
carrierQuotes: bool(row.carrier_quotes)
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function storeSettingsPayload(input) {
|
|
240
|
+
const payload = {};
|
|
241
|
+
if (input.currency !== void 0) payload.currency = input.currency;
|
|
242
|
+
if (input.locale !== void 0) payload.locale = input.locale;
|
|
243
|
+
if (input.showOutOfStock !== void 0) payload.show_out_of_stock = input.showOutOfStock;
|
|
244
|
+
if (input.requireSku !== void 0) payload.require_sku = input.requireSku;
|
|
245
|
+
if (input.stockOwnership !== void 0) payload.stock_ownership = input.stockOwnership;
|
|
246
|
+
if (input.paymentMethods !== void 0) payload.payment_methods = input.paymentMethods;
|
|
247
|
+
if (input.paymentProvider !== void 0) payload.payment_provider = input.paymentProvider ?? null;
|
|
248
|
+
if (input.guestCheckout !== void 0) payload.guest_checkout = input.guestCheckout;
|
|
249
|
+
if (input.requireConfirmedEmail !== void 0) payload.require_confirmed_email = input.requireConfirmedEmail;
|
|
250
|
+
if (input.marketingOptIn !== void 0) payload.marketing_opt_in = input.marketingOptIn;
|
|
251
|
+
if (input.discountsEnabled !== void 0) payload.discounts_enabled = input.discountsEnabled;
|
|
252
|
+
if (input.checkoutFields !== void 0) payload.checkout_fields = input.checkoutFields ?? {};
|
|
253
|
+
if (input.autoFulfill !== void 0) payload.auto_fulfill = input.autoFulfill;
|
|
254
|
+
if (input.notifyNewOrder !== void 0) payload.notify_new_order = input.notifyNewOrder;
|
|
255
|
+
if (input.notifyLowStock !== void 0) payload.notify_low_stock = input.notifyLowStock;
|
|
256
|
+
if (input.handlingNote !== void 0) payload.handling_note = input.handlingNote || null;
|
|
257
|
+
if (input.carrierQuotes !== void 0) payload.carrier_quotes = input.carrierQuotes;
|
|
258
|
+
return payload;
|
|
259
|
+
}
|
|
106
260
|
function rowToShippingZone(row) {
|
|
107
261
|
return {
|
|
108
262
|
id: row.id,
|
|
@@ -116,9 +270,74 @@ function rowToShippingZone(row) {
|
|
|
116
270
|
etaMinDays: row.eta_min_days ?? null,
|
|
117
271
|
etaMaxDays: row.eta_max_days ?? null,
|
|
118
272
|
active: row.active ?? true,
|
|
119
|
-
sortOrder: row.sort_order ?? 0
|
|
273
|
+
sortOrder: row.sort_order ?? 0,
|
|
274
|
+
excluded: row.excluded ?? false,
|
|
275
|
+
profileId: row.profile_id ?? null
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function rowToShippingProfile(row) {
|
|
279
|
+
return {
|
|
280
|
+
id: row.id,
|
|
281
|
+
tenantId: row.tenant_id,
|
|
282
|
+
name: row.name,
|
|
283
|
+
// undefined, never false: NULL means "inherit the store's decision", and a
|
|
284
|
+
// mapper that coalesced would turn every new profile into one that refuses
|
|
285
|
+
// the gateway.
|
|
286
|
+
carrierQuotes: row.carrier_quotes == null ? void 0 : Boolean(row.carrier_quotes),
|
|
287
|
+
isDefault: row.is_default ?? false
|
|
120
288
|
};
|
|
121
289
|
}
|
|
290
|
+
function shippingProfilePayload(input) {
|
|
291
|
+
const payload = {};
|
|
292
|
+
if (input.name !== void 0) payload.name = input.name;
|
|
293
|
+
if (input.carrierQuotes !== void 0) payload.carrier_quotes = input.carrierQuotes ?? null;
|
|
294
|
+
if (input.isDefault !== void 0) payload.is_default = input.isDefault;
|
|
295
|
+
return payload;
|
|
296
|
+
}
|
|
297
|
+
function toOriginPostalCode(raw) {
|
|
298
|
+
const digits = String(raw ?? "").replace(/\D/g, "");
|
|
299
|
+
if (!digits) return null;
|
|
300
|
+
if (digits.length !== 8) throw new Error("O CEP do local precisa ter 8 d\xEDgitos.");
|
|
301
|
+
return digits;
|
|
302
|
+
}
|
|
303
|
+
function toLocationCode(name) {
|
|
304
|
+
const slug = name.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
305
|
+
return slug || "local";
|
|
306
|
+
}
|
|
307
|
+
function locationWriteError(error) {
|
|
308
|
+
if (error?.code === "23505") return new Error("J\xE1 existe um local com esse nome.");
|
|
309
|
+
if (error?.code === "23503") {
|
|
310
|
+
return new Error(
|
|
311
|
+
"Este local j\xE1 tem movimenta\xE7\xF5es de estoque. Arquive-o em vez de excluir \u2014 o hist\xF3rico aponta para ele."
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
return new Error(error?.message ?? "N\xE3o foi poss\xEDvel salvar o local.");
|
|
315
|
+
}
|
|
316
|
+
function stockLocationPayload(input) {
|
|
317
|
+
const payload = {};
|
|
318
|
+
if (input.name !== void 0) payload.name = input.name;
|
|
319
|
+
if (input.code !== void 0) payload.code = input.code;
|
|
320
|
+
if (input.kind !== void 0) payload.kind = input.kind;
|
|
321
|
+
if (input.isSellable !== void 0) payload.is_sellable = input.isSellable;
|
|
322
|
+
if (input.isDefault !== void 0) payload.is_default = input.isDefault;
|
|
323
|
+
if (input.status !== void 0) payload.status = input.status;
|
|
324
|
+
if (input.postalCode !== void 0) payload.postal_code = toOriginPostalCode(input.postalCode);
|
|
325
|
+
if (input.city !== void 0) payload.city = input.city || null;
|
|
326
|
+
if (input.state !== void 0) payload.state = input.state || null;
|
|
327
|
+
if (input.street !== void 0) payload.street = input.street || null;
|
|
328
|
+
if (input.number !== void 0) payload.number = input.number || null;
|
|
329
|
+
if (input.district !== void 0) payload.district = input.district || null;
|
|
330
|
+
if (input.complement !== void 0) payload.complement = input.complement || null;
|
|
331
|
+
if (input.document !== void 0) payload.document = String(input.document ?? "").replace(/\D/gu, "") || null;
|
|
332
|
+
if (input.phone !== void 0) payload.phone = String(input.phone ?? "").replace(/\D/gu, "") || null;
|
|
333
|
+
if (input.email !== void 0) payload.email = input.email || null;
|
|
334
|
+
if (input.carrierAgencies !== void 0) payload.carrier_agencies = input.carrierAgencies ?? {};
|
|
335
|
+
if (input.shipsOnline !== void 0) payload.ships_online = input.shipsOnline;
|
|
336
|
+
if (input.fulfillmentProvider !== void 0) {
|
|
337
|
+
payload.fulfillment_provider = input.fulfillmentProvider || null;
|
|
338
|
+
}
|
|
339
|
+
return payload;
|
|
340
|
+
}
|
|
122
341
|
function shippingZonePayload(input) {
|
|
123
342
|
const payload = {};
|
|
124
343
|
if (input.name !== void 0) payload.name = input.name;
|
|
@@ -131,6 +350,34 @@ function shippingZonePayload(input) {
|
|
|
131
350
|
if (input.etaMaxDays !== void 0) payload.eta_max_days = input.etaMaxDays ?? null;
|
|
132
351
|
if (input.active !== void 0) payload.active = input.active;
|
|
133
352
|
if (input.sortOrder !== void 0) payload.sort_order = input.sortOrder;
|
|
353
|
+
if (input.excluded !== void 0) payload.excluded = input.excluded;
|
|
354
|
+
if (input.profileId !== void 0) payload.profile_id = input.profileId ?? null;
|
|
355
|
+
return payload;
|
|
356
|
+
}
|
|
357
|
+
function rowToShippingPackage(row) {
|
|
358
|
+
return {
|
|
359
|
+
id: row.id,
|
|
360
|
+
tenantId: row.tenant_id,
|
|
361
|
+
name: row.name,
|
|
362
|
+
kind: row.kind ?? "box",
|
|
363
|
+
heightCm: Number(row.height_cm ?? 0),
|
|
364
|
+
widthCm: Number(row.width_cm ?? 0),
|
|
365
|
+
lengthCm: Number(row.length_cm ?? 0),
|
|
366
|
+
emptyWeightGrams: Number(row.empty_weight_grams ?? 0),
|
|
367
|
+
isDefault: row.is_default ?? false,
|
|
368
|
+
active: row.active ?? true
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function shippingPackagePayload(input) {
|
|
372
|
+
const payload = {};
|
|
373
|
+
if (input.name !== void 0) payload.name = input.name;
|
|
374
|
+
if (input.kind !== void 0) payload.kind = input.kind;
|
|
375
|
+
if (input.heightCm !== void 0) payload.height_cm = input.heightCm;
|
|
376
|
+
if (input.widthCm !== void 0) payload.width_cm = input.widthCm;
|
|
377
|
+
if (input.lengthCm !== void 0) payload.length_cm = input.lengthCm;
|
|
378
|
+
if (input.emptyWeightGrams !== void 0) payload.empty_weight_grams = input.emptyWeightGrams;
|
|
379
|
+
if (input.isDefault !== void 0) payload.is_default = input.isDefault;
|
|
380
|
+
if (input.active !== void 0) payload.active = input.active;
|
|
134
381
|
return payload;
|
|
135
382
|
}
|
|
136
383
|
function rowToProduct(row) {
|
|
@@ -154,10 +401,21 @@ function rowToProduct(row) {
|
|
|
154
401
|
isPhysical: row.is_physical ?? true,
|
|
155
402
|
weight: row.weight,
|
|
156
403
|
weightUnit: row.weight_unit ?? "kg",
|
|
404
|
+
heightCm: numOrNull(row.height_cm),
|
|
405
|
+
widthCm: numOrNull(row.width_cm),
|
|
406
|
+
lengthCm: numOrNull(row.length_cm),
|
|
407
|
+
packageId: row.package_id ?? null,
|
|
408
|
+
handling: Array.isArray(row.handling) ? row.handling : [],
|
|
409
|
+
shippingProfileId: row.shipping_profile_id ?? null,
|
|
157
410
|
createdAt: row.created_at,
|
|
158
411
|
updatedAt: row.updated_at
|
|
159
412
|
};
|
|
160
413
|
}
|
|
414
|
+
function numOrNull(value) {
|
|
415
|
+
if (value == null || value === "") return null;
|
|
416
|
+
const n = Number(value);
|
|
417
|
+
return Number.isFinite(n) ? n : null;
|
|
418
|
+
}
|
|
161
419
|
function rowToImage(row) {
|
|
162
420
|
return {
|
|
163
421
|
id: row.id,
|
|
@@ -229,6 +487,7 @@ function rowToCustomer(row) {
|
|
|
229
487
|
lastName: row.last_name ?? "",
|
|
230
488
|
email: row.email,
|
|
231
489
|
phone: row.phone,
|
|
490
|
+
document: row.document ?? null,
|
|
232
491
|
notes: row.notes,
|
|
233
492
|
ordersCount: row.orders_count ?? 0,
|
|
234
493
|
totalSpent: row.total_spent ?? 0,
|
|
@@ -285,22 +544,13 @@ function segmentPayload(input) {
|
|
|
285
544
|
return p;
|
|
286
545
|
}
|
|
287
546
|
var GIFT_CODE_ALPHABET = "ABCDEFGHJKLMNPQRTUVWXY2346789";
|
|
288
|
-
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "avif", "bmp"]);
|
|
289
|
-
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set(["mp4", "mov", "webm", "m4v", "avi", "mkv"]);
|
|
290
547
|
function generateGiftCardCode() {
|
|
291
548
|
const bytes = new Uint8Array(12);
|
|
292
549
|
globalThis.crypto.getRandomValues(bytes);
|
|
293
550
|
const chars = Array.from(bytes, (b) => GIFT_CODE_ALPHABET[b % GIFT_CODE_ALPHABET.length]);
|
|
294
551
|
return `${chars.slice(0, 4).join("")}-${chars.slice(4, 8).join("")}-${chars.slice(8, 12).join("")}`;
|
|
295
552
|
}
|
|
296
|
-
|
|
297
|
-
if (mime?.startsWith("image/")) return "image";
|
|
298
|
-
if (mime?.startsWith("video/")) return "video";
|
|
299
|
-
const ext = (name.split(".").pop() ?? "").toLowerCase();
|
|
300
|
-
if (IMAGE_EXTENSIONS.has(ext)) return "image";
|
|
301
|
-
if (VIDEO_EXTENSIONS.has(ext)) return "video";
|
|
302
|
-
return "document";
|
|
303
|
-
}
|
|
553
|
+
var LIBRARY_FOLDER = "library";
|
|
304
554
|
function nullableNumber(value) {
|
|
305
555
|
if (value === null || value === void 0 || value === "") return null;
|
|
306
556
|
const n = Number(value);
|
|
@@ -418,11 +668,13 @@ var SupabaseShopProvider = class {
|
|
|
418
668
|
* RLS on both tables already restricts anon reads to active variants of
|
|
419
669
|
* publicly readable products; the status filter here keeps the admin path
|
|
420
670
|
* (authenticated tenant member, who CAN see archived rows) consistent. */
|
|
421
|
-
async getProductVariants(productId) {
|
|
671
|
+
async getProductVariants(productId, opts) {
|
|
422
672
|
const db = getDb();
|
|
673
|
+
let variantQuery = db.from(T.productVariants).select("*").eq("product_id", productId);
|
|
674
|
+
if (!opts?.includeArchived) variantQuery = variantQuery.eq("status", "active");
|
|
423
675
|
const [optionsRes, variantsRes] = await Promise.all([
|
|
424
676
|
db.from(T.productOptions).select("*").eq("product_id", productId).order("position", { ascending: true }),
|
|
425
|
-
|
|
677
|
+
variantQuery.order("position", { ascending: true })
|
|
426
678
|
]);
|
|
427
679
|
if (optionsRes.error) throw optionsRes.error;
|
|
428
680
|
if (variantsRes.error) throw variantsRes.error;
|
|
@@ -432,19 +684,102 @@ var SupabaseShopProvider = class {
|
|
|
432
684
|
values: Array.isArray(row.values) ? row.values.map(String) : [],
|
|
433
685
|
position: row.position ?? 0
|
|
434
686
|
}));
|
|
435
|
-
const variants = (variantsRes.data ?? []).map(
|
|
687
|
+
const variants = (variantsRes.data ?? []).map(rowToVariant);
|
|
688
|
+
return { options, variants };
|
|
689
|
+
}
|
|
690
|
+
// --- The variant admin ------------------------------------------------------
|
|
691
|
+
async saveProductOptions(productId, input) {
|
|
692
|
+
const db = getDb();
|
|
693
|
+
const tenantId = getTenantId();
|
|
694
|
+
const clean = normalizeVariantAxes(input);
|
|
695
|
+
const refusal = validateVariantAxes(clean);
|
|
696
|
+
if (refusal) throw new Error(refusal);
|
|
697
|
+
const { data: existing, error: readError } = await db.from(T.productOptions).select("*").eq("product_id", productId);
|
|
698
|
+
if (readError) throw readError;
|
|
699
|
+
const byName = new Map((existing ?? []).map((row) => [row.name, row]));
|
|
700
|
+
const wanted = new Set(clean.map((axis) => axis.name));
|
|
701
|
+
const stale = (existing ?? []).filter((row) => !wanted.has(row.name)).map((row) => row.id);
|
|
702
|
+
if (stale.length > 0) {
|
|
703
|
+
const { error: error2 } = await db.from(T.productOptions).delete().in("id", stale);
|
|
704
|
+
if (error2) throw error2;
|
|
705
|
+
}
|
|
706
|
+
for (const [position, axis] of clean.entries()) {
|
|
707
|
+
const row = byName.get(axis.name);
|
|
708
|
+
if (row) {
|
|
709
|
+
const { error: error2 } = await db.from(T.productOptions).update({ values: axis.values, position }).eq("id", row.id);
|
|
710
|
+
if (error2) throw error2;
|
|
711
|
+
} else {
|
|
712
|
+
const { error: error2 } = await db.from(T.productOptions).insert({
|
|
713
|
+
product_id: productId,
|
|
714
|
+
tenant_id: tenantId,
|
|
715
|
+
name: axis.name,
|
|
716
|
+
values: axis.values,
|
|
717
|
+
position
|
|
718
|
+
});
|
|
719
|
+
if (error2) throw error2;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const { data, error } = await db.from(T.productOptions).select("*").eq("product_id", productId).order("position", { ascending: true });
|
|
723
|
+
if (error) throw error;
|
|
724
|
+
return (data ?? []).map((row) => ({
|
|
436
725
|
id: row.id,
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
price: row.price == null ? null : Number(row.price),
|
|
441
|
-
compareAtPrice: row.compare_at_price == null ? null : Number(row.compare_at_price),
|
|
442
|
-
inventoryCount: row.inventory_count ?? 0,
|
|
443
|
-
position: row.position ?? 0,
|
|
444
|
-
status: row.status ?? "active",
|
|
445
|
-
imageId: row.image_id ?? null
|
|
726
|
+
name: row.name,
|
|
727
|
+
values: Array.isArray(row.values) ? row.values.map(String) : [],
|
|
728
|
+
position: row.position ?? 0
|
|
446
729
|
}));
|
|
447
|
-
|
|
730
|
+
}
|
|
731
|
+
async createProductVariant(productId, input) {
|
|
732
|
+
const db = getDb();
|
|
733
|
+
const tenantId = getTenantId();
|
|
734
|
+
if (Object.keys(input.options).length === 0) {
|
|
735
|
+
throw new Error("Uma variante sem op\xE7\xF5es \xE9 a variante padr\xE3o (0037), criada pelo banco. Declare ao menos um eixo.");
|
|
736
|
+
}
|
|
737
|
+
const { data: existing, error: readError } = await db.from(T.productVariants).select("*").eq("product_id", productId);
|
|
738
|
+
if (readError) throw readError;
|
|
739
|
+
const wantedKey = variantOptionsKey(input.options);
|
|
740
|
+
const match = (existing ?? []).find((row) => variantOptionsKey(row.options ?? {}) === wantedKey);
|
|
741
|
+
if (match) return this.updateProductVariant(match.id, { ...input, status: input.status ?? "active" });
|
|
742
|
+
const { data, error } = await db.from(T.productVariants).insert({
|
|
743
|
+
product_id: productId,
|
|
744
|
+
tenant_id: tenantId,
|
|
745
|
+
options: input.options,
|
|
746
|
+
...variantPatch(input),
|
|
747
|
+
// NOT copied from the product: 0028 defines a null price as "inherit the
|
|
748
|
+
// product's", and 0037's header spells out why a copy is a bug — the
|
|
749
|
+
// storefront would show the new product price and the server charge the
|
|
750
|
+
// frozen variant one.
|
|
751
|
+
status: input.status ?? "active"
|
|
752
|
+
}).select("*").single();
|
|
753
|
+
if (error) throw error;
|
|
754
|
+
return rowToVariant(data);
|
|
755
|
+
}
|
|
756
|
+
async updateProductVariant(variantId, input) {
|
|
757
|
+
const db = getDb();
|
|
758
|
+
const { data, error } = await db.from(T.productVariants).update(variantPatch(input)).eq("id", variantId).select("*").single();
|
|
759
|
+
if (error) throw error;
|
|
760
|
+
return rowToVariant(data);
|
|
761
|
+
}
|
|
762
|
+
async archiveProductVariant(variantId) {
|
|
763
|
+
const db = getDb();
|
|
764
|
+
const { error } = await db.from(T.productVariants).update({ status: "archived" }).eq("id", variantId);
|
|
765
|
+
if (error) throw error;
|
|
766
|
+
}
|
|
767
|
+
async setVariantStock(variantId, count, note) {
|
|
768
|
+
const target = Math.max(0, Math.trunc(count));
|
|
769
|
+
const store = resolveStockStore(getTenantId() ?? "", () => createSupabaseStockStore());
|
|
770
|
+
const current = await store.getAvailable(variantId);
|
|
771
|
+
const delta = target - current;
|
|
772
|
+
if (delta === 0) return current;
|
|
773
|
+
const priorMovements = await store.listMovements({ variantId, limit: 1 });
|
|
774
|
+
const opening = priorMovements.length === 0 && delta > 0;
|
|
775
|
+
await store.recordMovement({
|
|
776
|
+
variantId,
|
|
777
|
+
kind: opening ? "receipt" : "adjustment",
|
|
778
|
+
reasonCode: opening ? OPENING_BALANCE_REASON : "correction",
|
|
779
|
+
quantity: delta,
|
|
780
|
+
note: note ?? "Estoque definido na matriz de variantes."
|
|
781
|
+
});
|
|
782
|
+
return target;
|
|
448
783
|
}
|
|
449
784
|
async createProduct(input) {
|
|
450
785
|
const db = getDb();
|
|
@@ -465,7 +800,17 @@ var SupabaseShopProvider = class {
|
|
|
465
800
|
category_id: input.categoryId ?? null,
|
|
466
801
|
is_physical: input.isPhysical ?? true,
|
|
467
802
|
weight: input.weight ?? null,
|
|
468
|
-
weight_unit: input.weightUnit ?? "kg"
|
|
803
|
+
weight_unit: input.weightUnit ?? "kg",
|
|
804
|
+
height_cm: input.heightCm ?? null,
|
|
805
|
+
width_cm: input.widthCm ?? null,
|
|
806
|
+
length_cm: input.lengthCm ?? null,
|
|
807
|
+
// Null = a padrão da loja (0047). É o estado normal, não um campo vazio.
|
|
808
|
+
package_id: input.packageId ?? null,
|
|
809
|
+
// NULL and not []: "nothing declared" and "declared as needing nothing"
|
|
810
|
+
// are the same fact about a box, and one representation of it keeps the
|
|
811
|
+
// `&&` overlap test in the 0048 trigger reading one way.
|
|
812
|
+
handling: input.handling?.length ? input.handling : null,
|
|
813
|
+
shipping_profile_id: input.shippingProfileId ?? null
|
|
469
814
|
}).select(`*, images:${T.productImages}(*)`).single();
|
|
470
815
|
if (error) throw error;
|
|
471
816
|
return rowToProduct(data);
|
|
@@ -488,6 +833,13 @@ var SupabaseShopProvider = class {
|
|
|
488
833
|
if (input.categoryId !== void 0) updates.category_id = input.categoryId;
|
|
489
834
|
if (input.isPhysical !== void 0) updates.is_physical = input.isPhysical;
|
|
490
835
|
if (input.weight !== void 0) updates.weight = input.weight;
|
|
836
|
+
if (input.weightUnit !== void 0) updates.weight_unit = input.weightUnit;
|
|
837
|
+
if (input.heightCm !== void 0) updates.height_cm = input.heightCm;
|
|
838
|
+
if (input.widthCm !== void 0) updates.width_cm = input.widthCm;
|
|
839
|
+
if (input.lengthCm !== void 0) updates.length_cm = input.lengthCm;
|
|
840
|
+
if (input.packageId !== void 0) updates.package_id = input.packageId || null;
|
|
841
|
+
if (input.handling !== void 0) updates.handling = input.handling.length ? input.handling : null;
|
|
842
|
+
if (input.shippingProfileId !== void 0) updates.shipping_profile_id = input.shippingProfileId ?? null;
|
|
491
843
|
const { data, error } = await db.from(T.products).update(updates).eq("id", id).select(`*, images:${T.productImages}(*)`).single();
|
|
492
844
|
if (error) throw error;
|
|
493
845
|
return rowToProduct(data);
|
|
@@ -532,9 +884,43 @@ var SupabaseShopProvider = class {
|
|
|
532
884
|
if (error) throw error;
|
|
533
885
|
return rowToImage(data);
|
|
534
886
|
}
|
|
887
|
+
async replaceProductImage(imageId, file) {
|
|
888
|
+
const db = getDb();
|
|
889
|
+
const tenantId = getTenantId();
|
|
890
|
+
const { data: row, error: readErr } = await db.from(T.productImages).select("id,url,product_id").eq("id", imageId).single();
|
|
891
|
+
if (readErr) throw readErr;
|
|
892
|
+
const path = `${tenantId}/products/${row.product_id}/${Date.now()}-${file.name}`;
|
|
893
|
+
const bucket = db.storage.from(T.mediaBucket);
|
|
894
|
+
const { error: uploadErr } = await bucket.upload(path, file);
|
|
895
|
+
if (uploadErr) throw uploadErr;
|
|
896
|
+
const { data: { publicUrl } } = bucket.getPublicUrl(path);
|
|
897
|
+
const { data, error } = await db.from(T.productImages).update({ url: publicUrl }).eq("id", imageId).select().single();
|
|
898
|
+
if (error) throw error;
|
|
899
|
+
await this.removeUnreferencedObject(row.url);
|
|
900
|
+
return rowToImage(data);
|
|
901
|
+
}
|
|
535
902
|
async deleteProductImage(imageId) {
|
|
536
|
-
const
|
|
903
|
+
const db = getDb();
|
|
904
|
+
const { data: row } = await db.from(T.productImages).select("url").eq("id", imageId).maybeSingle();
|
|
905
|
+
const { error } = await db.from(T.productImages).delete().eq("id", imageId);
|
|
537
906
|
if (error) throw error;
|
|
907
|
+
if (row?.url) await this.removeUnreferencedObject(row.url);
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Drop a stored file once nothing points at it anymore.
|
|
911
|
+
*
|
|
912
|
+
* The row was the only thing keeping the object reachable, so leaving it
|
|
913
|
+
* behind is a file nobody can see and nobody can delete — and the bill for it
|
|
914
|
+
* arrives all the same. Two rows can share one URL (a duplicated product), and
|
|
915
|
+
* the survivor has to keep working, so the count decides.
|
|
916
|
+
*/
|
|
917
|
+
async removeUnreferencedObject(url) {
|
|
918
|
+
const path = url ? storagePathOf(url) : null;
|
|
919
|
+
if (!path) return;
|
|
920
|
+
const db = getDb();
|
|
921
|
+
const { count } = await db.from(T.productImages).select("id", { count: "exact", head: true }).eq("url", url);
|
|
922
|
+
if (count) return;
|
|
923
|
+
await db.storage.from(T.mediaBucket).remove([path]);
|
|
538
924
|
}
|
|
539
925
|
// ---------------------------------------------------------------------------
|
|
540
926
|
// Categories
|
|
@@ -643,10 +1029,188 @@ var SupabaseShopProvider = class {
|
|
|
643
1029
|
if (error) throw error;
|
|
644
1030
|
return rowToShippingZone(data);
|
|
645
1031
|
}
|
|
1032
|
+
// -------------------------------------------------------------------------
|
|
1033
|
+
// Embalagens (0046)
|
|
1034
|
+
//
|
|
1035
|
+
// `is_default` has a partial unique index behind it, so promoting a box has
|
|
1036
|
+
// to demote the previous one in the same round trip — otherwise the second
|
|
1037
|
+
// default is refused by the database and the merchant sees a save that
|
|
1038
|
+
// failed for a reason the screen cannot explain.
|
|
1039
|
+
// -------------------------------------------------------------------------
|
|
1040
|
+
async listShippingPackages() {
|
|
1041
|
+
const tenantId = getTenantId();
|
|
1042
|
+
const { data, error } = await getDb().from(T.shippingPackages).select("*").eq("tenant_id", tenantId).order("is_default", { ascending: false }).order("name", { ascending: true });
|
|
1043
|
+
if (error) throw error;
|
|
1044
|
+
return (data ?? []).map(rowToShippingPackage);
|
|
1045
|
+
}
|
|
1046
|
+
async createShippingPackage(input) {
|
|
1047
|
+
const tenantId = getTenantId();
|
|
1048
|
+
if (!tenantId) throw new Error("@fayz-ai/shop: createShippingPackage requires a tenant id.");
|
|
1049
|
+
if (input.isDefault) await this.clearDefaultPackage(tenantId);
|
|
1050
|
+
const { data, error } = await getDb().from(T.shippingPackages).insert({ tenant_id: tenantId, ...shippingPackagePayload(input) }).select().single();
|
|
1051
|
+
if (error) throw error;
|
|
1052
|
+
return rowToShippingPackage(data);
|
|
1053
|
+
}
|
|
1054
|
+
async updateShippingPackage(id, input) {
|
|
1055
|
+
const tenantId = getTenantId();
|
|
1056
|
+
if (input.isDefault && tenantId) await this.clearDefaultPackage(tenantId, id);
|
|
1057
|
+
const { data, error } = await getDb().from(T.shippingPackages).update(shippingPackagePayload(input)).eq("id", id).select().single();
|
|
1058
|
+
if (error) throw error;
|
|
1059
|
+
return rowToShippingPackage(data);
|
|
1060
|
+
}
|
|
1061
|
+
async deleteShippingPackage(id) {
|
|
1062
|
+
const { error } = await getDb().from(T.shippingPackages).delete().eq("id", id);
|
|
1063
|
+
if (error) throw error;
|
|
1064
|
+
}
|
|
1065
|
+
async clearDefaultPackage(tenantId, exceptId) {
|
|
1066
|
+
let query = getDb().from(T.shippingPackages).update({ is_default: false }).eq("tenant_id", tenantId).eq("is_default", true);
|
|
1067
|
+
if (exceptId) query = query.neq("id", exceptId);
|
|
1068
|
+
const { error } = await query;
|
|
1069
|
+
if (error) throw error;
|
|
1070
|
+
}
|
|
646
1071
|
async deleteShippingZone(id) {
|
|
647
1072
|
const { error } = await getDb().from(T.shippingZones).delete().eq("id", id);
|
|
648
1073
|
if (error) throw error;
|
|
649
1074
|
}
|
|
1075
|
+
// -------------------------------------------------------------------------
|
|
1076
|
+
// Store policy + delivery fallback (0044b / 0015)
|
|
1077
|
+
//
|
|
1078
|
+
// Both are ONE ROW PER TENANT, and both are upserted rather than
|
|
1079
|
+
// insert-or-update: a store that never opened these screens has no row, and
|
|
1080
|
+
// making the first save conditional on discovering that is two round trips to
|
|
1081
|
+
// learn something the primary key already knows.
|
|
1082
|
+
// -------------------------------------------------------------------------
|
|
1083
|
+
async getShippingSettings() {
|
|
1084
|
+
const tenantId = getTenantId();
|
|
1085
|
+
const { data, error } = await getDb().from(T.shippingSettings).select("flat_rate, free_above, currency").eq("tenant_id", tenantId).maybeSingle();
|
|
1086
|
+
if (error) throw error;
|
|
1087
|
+
if (!data) return null;
|
|
1088
|
+
return {
|
|
1089
|
+
flatRate: Number(data.flat_rate ?? 0),
|
|
1090
|
+
freeAbove: data.free_above == null ? null : Number(data.free_above),
|
|
1091
|
+
currency: data.currency ?? "BRL"
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
async updateShippingSettings(input) {
|
|
1095
|
+
const tenantId = getTenantId();
|
|
1096
|
+
if (!tenantId) throw new Error("@fayz-ai/shop: updateShippingSettings requires a tenant id.");
|
|
1097
|
+
const payload = { tenant_id: tenantId };
|
|
1098
|
+
if (input.flatRate !== void 0) payload.flat_rate = input.flatRate;
|
|
1099
|
+
if (input.freeAbove !== void 0) payload.free_above = input.freeAbove;
|
|
1100
|
+
if (input.currency !== void 0) payload.currency = input.currency;
|
|
1101
|
+
const { data, error } = await getDb().from(T.shippingSettings).upsert(payload, { onConflict: "tenant_id" }).select("flat_rate, free_above, currency").single();
|
|
1102
|
+
if (error) throw error;
|
|
1103
|
+
return {
|
|
1104
|
+
flatRate: Number(data.flat_rate ?? 0),
|
|
1105
|
+
freeAbove: data.free_above == null ? null : Number(data.free_above),
|
|
1106
|
+
currency: data.currency ?? "BRL"
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* "Does this store deliver here?" — asked with the address, before the card.
|
|
1111
|
+
*
|
|
1112
|
+
* Degrades to "yes, and not our own zone" on any failure. A store that cannot
|
|
1113
|
+
* reach the check must not stop selling: the order itself is still refused by
|
|
1114
|
+
* the 0048 trigger if the region really is excluded, so the worst case is the
|
|
1115
|
+
* shopper learning it one screen later instead of never selling at all.
|
|
1116
|
+
*/
|
|
1117
|
+
async listShippingProfiles() {
|
|
1118
|
+
const { data, error } = await getDb().from(T.shippingProfiles).select("*").eq("tenant_id", getTenantId()).order("is_default", { ascending: false }).order("name");
|
|
1119
|
+
if (error) throw error;
|
|
1120
|
+
return (data ?? []).map(rowToShippingProfile);
|
|
1121
|
+
}
|
|
1122
|
+
async createShippingProfile(input) {
|
|
1123
|
+
const tenantId = getTenantId();
|
|
1124
|
+
if (!tenantId) throw new Error("@fayz-ai/shop: createShippingProfile requires a tenant id.");
|
|
1125
|
+
const { data, error } = await getDb().from(T.shippingProfiles).insert({ tenant_id: tenantId, ...shippingProfilePayload(input) }).select("*").single();
|
|
1126
|
+
if (error) throw error;
|
|
1127
|
+
return rowToShippingProfile(data);
|
|
1128
|
+
}
|
|
1129
|
+
async updateShippingProfile(id, input) {
|
|
1130
|
+
const { data, error } = await getDb().from(T.shippingProfiles).update(shippingProfilePayload(input)).eq("id", id).select("*").single();
|
|
1131
|
+
if (error) throw error;
|
|
1132
|
+
return rowToShippingProfile(data);
|
|
1133
|
+
}
|
|
1134
|
+
async deleteShippingProfile(id) {
|
|
1135
|
+
const { error } = await getDb().from(T.shippingProfiles).delete().eq("id", id);
|
|
1136
|
+
if (error) throw error;
|
|
1137
|
+
}
|
|
1138
|
+
// Places the store ships from (0058). Default first, then by name — the same
|
|
1139
|
+
// order the ledger's listLocations() uses, so the two screens agree.
|
|
1140
|
+
async listStockLocations() {
|
|
1141
|
+
const { data, error } = await getDb().from(T.stockLocations).select("*").eq("tenant_id", getTenantId()).order("is_default", { ascending: false }).order("name");
|
|
1142
|
+
if (error) throw error;
|
|
1143
|
+
return (data ?? []).map(rowToStockLocation);
|
|
1144
|
+
}
|
|
1145
|
+
async createStockLocation(input) {
|
|
1146
|
+
const tenantId = getTenantId();
|
|
1147
|
+
if (!tenantId) throw new Error("@fayz-ai/shop: createStockLocation requires a tenant id.");
|
|
1148
|
+
if (input.isDefault) await this.clearDefaultStockLocation(tenantId);
|
|
1149
|
+
const payload = stockLocationPayload(input);
|
|
1150
|
+
if (payload.code === void 0) payload.code = toLocationCode(input.name);
|
|
1151
|
+
const { data, error } = await getDb().from(T.stockLocations).insert({ tenant_id: tenantId, ...payload }).select("*").single();
|
|
1152
|
+
if (error) throw locationWriteError(error);
|
|
1153
|
+
return rowToStockLocation(data);
|
|
1154
|
+
}
|
|
1155
|
+
async updateStockLocation(id, input) {
|
|
1156
|
+
const tenantId = getTenantId();
|
|
1157
|
+
if (input.isDefault && tenantId) await this.clearDefaultStockLocation(tenantId, id);
|
|
1158
|
+
const { data, error } = await getDb().from(T.stockLocations).update(stockLocationPayload(input)).eq("id", id).select("*").single();
|
|
1159
|
+
if (error) throw locationWriteError(error);
|
|
1160
|
+
return rowToStockLocation(data);
|
|
1161
|
+
}
|
|
1162
|
+
async deleteStockLocation(id) {
|
|
1163
|
+
const { error } = await getDb().from(T.stockLocations).delete().eq("id", id);
|
|
1164
|
+
if (error) throw locationWriteError(error);
|
|
1165
|
+
}
|
|
1166
|
+
async clearDefaultStockLocation(tenantId, exceptId) {
|
|
1167
|
+
let query = getDb().from(T.stockLocations).update({ is_default: false }).eq("tenant_id", tenantId).eq("is_default", true);
|
|
1168
|
+
if (exceptId) query = query.neq("id", exceptId);
|
|
1169
|
+
const { error } = await query;
|
|
1170
|
+
if (error) throw error;
|
|
1171
|
+
}
|
|
1172
|
+
async checkDelivery(postalCode) {
|
|
1173
|
+
const tenantId = getTenantId();
|
|
1174
|
+
const digits = String(postalCode ?? "").replace(/\D/g, "");
|
|
1175
|
+
if (!tenantId || digits.length !== 8) return { refused: false, ownZone: false };
|
|
1176
|
+
const { data, error } = await getDb().rpc("shop_delivery_check", {
|
|
1177
|
+
p_tenant_id: tenantId,
|
|
1178
|
+
p_postal_code: digits
|
|
1179
|
+
});
|
|
1180
|
+
if (error) return { refused: false, ownZone: false };
|
|
1181
|
+
const row = Array.isArray(data) ? data[0] : data;
|
|
1182
|
+
return {
|
|
1183
|
+
refused: Boolean(row?.refused),
|
|
1184
|
+
refusedZone: row?.refused_zone ?? null,
|
|
1185
|
+
ownZone: Boolean(row?.own_zone)
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
async getStoreSettings() {
|
|
1189
|
+
const tenantId = getTenantId();
|
|
1190
|
+
const { data, error } = await getDb().from(T.settings).select("*").eq("tenant_id", tenantId).maybeSingle();
|
|
1191
|
+
if (error) throw error;
|
|
1192
|
+
return data ? rowToStoreSettings(data) : null;
|
|
1193
|
+
}
|
|
1194
|
+
async updateStoreSettings(input) {
|
|
1195
|
+
const tenantId = getTenantId();
|
|
1196
|
+
if (!tenantId) throw new Error("@fayz-ai/shop: updateStoreSettings requires a tenant id.");
|
|
1197
|
+
const { data, error } = await getDb().from(T.settings).upsert({ tenant_id: tenantId, ...storeSettingsPayload(input) }, { onConflict: "tenant_id" }).select("*").single();
|
|
1198
|
+
if (error) throw error;
|
|
1199
|
+
return rowToStoreSettings(data);
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* The shopper's read. Through the RPC, not the table: the table is closed to
|
|
1203
|
+
* anon on purpose (0029) and shop_store_settings() is the narrow window —
|
|
1204
|
+
* storefront fields only, and only for a published store.
|
|
1205
|
+
*/
|
|
1206
|
+
async getPublicStoreSettings() {
|
|
1207
|
+
const tenantId = getTenantId();
|
|
1208
|
+
if (!tenantId) return null;
|
|
1209
|
+
const { data, error } = await getDb().rpc("shop_store_settings", { p_tenant_id: tenantId });
|
|
1210
|
+
if (error) throw error;
|
|
1211
|
+
const row = Array.isArray(data) ? data[0] : data;
|
|
1212
|
+
return row ? rowToStoreSettings(row) : null;
|
|
1213
|
+
}
|
|
650
1214
|
/**
|
|
651
1215
|
* Settle a pending order: the merchant confirming that the money arrived.
|
|
652
1216
|
*
|
|
@@ -664,6 +1228,227 @@ var SupabaseShopProvider = class {
|
|
|
664
1228
|
if (error) throw error;
|
|
665
1229
|
return this.getOrder(id);
|
|
666
1230
|
}
|
|
1231
|
+
// ---------------------------------------------------------------------------
|
|
1232
|
+
// Carrier labels. Everything goes through the function: the token never
|
|
1233
|
+
// reaches the browser, and `buy` is the one call that spends money.
|
|
1234
|
+
// ---------------------------------------------------------------------------
|
|
1235
|
+
async labelCall(orderId, action, extra = {}) {
|
|
1236
|
+
const tenantId = getTenantId();
|
|
1237
|
+
if (!tenantId) throw new Error("@fayz-ai/shop: shipping labels require a tenant id.");
|
|
1238
|
+
const { ok, payload } = await callShopFunction(getDb(), "shop-shipping-label", {
|
|
1239
|
+
tenantId,
|
|
1240
|
+
orderId,
|
|
1241
|
+
action,
|
|
1242
|
+
...extra
|
|
1243
|
+
});
|
|
1244
|
+
if (!ok) throw new Error(payload?.error ?? "A transportadora n\xE3o respondeu agora.");
|
|
1245
|
+
return payload;
|
|
1246
|
+
}
|
|
1247
|
+
async quoteShippingLabel(orderId, carrier) {
|
|
1248
|
+
const payload = await this.labelCall(orderId, "quote", carrier ? { carrier } : {});
|
|
1249
|
+
return {
|
|
1250
|
+
options: (payload?.options ?? []).map((option) => ({
|
|
1251
|
+
serviceId: Number(option.service_id ?? option.serviceId),
|
|
1252
|
+
name: String(option.name ?? "Entrega"),
|
|
1253
|
+
carrier: String(option.carrier ?? ""),
|
|
1254
|
+
price: Number(option.price ?? 0),
|
|
1255
|
+
etaMinDays: option.eta_min_days ?? null,
|
|
1256
|
+
etaMaxDays: option.eta_max_days ?? null
|
|
1257
|
+
})),
|
|
1258
|
+
label: payload?.label ? {
|
|
1259
|
+
labelId: String(payload.label.external_id),
|
|
1260
|
+
labelUrl: payload.label.url ?? null,
|
|
1261
|
+
status: payload.label.status ?? null,
|
|
1262
|
+
tracking: payload.label.payload?.tracking ?? null
|
|
1263
|
+
} : null
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
/** Spends the merchant's balance at the carrier. Never call it on a whim. */
|
|
1267
|
+
async buyShippingLabel(orderId, serviceId, carrier) {
|
|
1268
|
+
const payload = await this.labelCall(orderId, "buy", { serviceId, ...carrier ? { carrier } : {} });
|
|
1269
|
+
return {
|
|
1270
|
+
labelId: String(payload?.labelId ?? ""),
|
|
1271
|
+
labelUrl: payload?.labelUrl ?? null,
|
|
1272
|
+
status: "bought",
|
|
1273
|
+
tracking: payload?.tracking ?? null
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
async printShippingLabel(orderId, carrier) {
|
|
1277
|
+
const payload = await this.labelCall(orderId, "print", carrier ? { carrier } : {});
|
|
1278
|
+
return payload?.labelUrl ?? null;
|
|
1279
|
+
}
|
|
1280
|
+
/** Drop-off points near a place. Store configuration — no order involved. */
|
|
1281
|
+
async listShippingAgencies(query) {
|
|
1282
|
+
const tenantId = getTenantId();
|
|
1283
|
+
if (!tenantId) return [];
|
|
1284
|
+
const { ok, payload } = await callShopFunction(getDb(), "shop-shipping-label", {
|
|
1285
|
+
tenantId,
|
|
1286
|
+
action: "agencies",
|
|
1287
|
+
state: query.state ?? "",
|
|
1288
|
+
city: query.city ?? "",
|
|
1289
|
+
...query.carrier ? { carrier: query.carrier } : {}
|
|
1290
|
+
});
|
|
1291
|
+
if (!ok) throw new Error(payload?.error ?? "A transportadora n\xE3o respondeu agora.");
|
|
1292
|
+
return payload?.agencies ?? [];
|
|
1293
|
+
}
|
|
1294
|
+
async trackShippingLabel(orderId, carrier) {
|
|
1295
|
+
const payload = await this.labelCall(orderId, "track", carrier ? { carrier } : {});
|
|
1296
|
+
return payload?.tracking ?? null;
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* The customer's address book, for the console. `shop_place_order` files a
|
|
1300
|
+
* new one here on every delivered order, and until now only the storefront
|
|
1301
|
+
* read them back — so a repeat customer had their address retyped by hand.
|
|
1302
|
+
*/
|
|
1303
|
+
async listCustomerAddresses(customerId) {
|
|
1304
|
+
if (!asUuid(customerId)) return [];
|
|
1305
|
+
const { data, error } = await getDb().from("addresses").select("id,label,recipient,phone,postal_code,street,number,complement,district,city,state,country,is_default").eq("owner_type", "shop_customer").eq("owner_id", customerId).order("is_default", { ascending: false }).order("created_at", { ascending: false });
|
|
1306
|
+
if (error) return [];
|
|
1307
|
+
return (data ?? []).map((row) => ({
|
|
1308
|
+
id: row.id,
|
|
1309
|
+
label: row.label ?? void 0,
|
|
1310
|
+
recipient: row.recipient ?? void 0,
|
|
1311
|
+
phone: row.phone ?? void 0,
|
|
1312
|
+
postalCode: row.postal_code ?? "",
|
|
1313
|
+
street: row.street ?? "",
|
|
1314
|
+
number: row.number ?? void 0,
|
|
1315
|
+
complement: row.complement ?? void 0,
|
|
1316
|
+
district: row.district ?? void 0,
|
|
1317
|
+
city: row.city ?? "",
|
|
1318
|
+
state: row.state ?? "",
|
|
1319
|
+
country: row.country ?? void 0,
|
|
1320
|
+
isDefault: row.is_default ?? false
|
|
1321
|
+
}));
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* What delivery costs to a postcode — the same answer the shopper gets.
|
|
1325
|
+
*
|
|
1326
|
+
* The console needs it to quote a price out loud while the customer is on the
|
|
1327
|
+
* line. Carrier first (when the merchant did not turn gateway quotes off),
|
|
1328
|
+
* zones as the fallback, exactly as the storefront resolves it. Advisory
|
|
1329
|
+
* either way: shop_place_order requotes and ignores whatever a client sends.
|
|
1330
|
+
*/
|
|
1331
|
+
async quoteShipping(postalCode, subtotal) {
|
|
1332
|
+
const digits = toPostalDigits(postalCode);
|
|
1333
|
+
if (!digits) return [];
|
|
1334
|
+
const tenantId = getTenantId();
|
|
1335
|
+
if (!tenantId) return [];
|
|
1336
|
+
const settings = await this.getStoreSettings().catch(() => null);
|
|
1337
|
+
if (settings?.carrierQuotes !== false) {
|
|
1338
|
+
try {
|
|
1339
|
+
const { ok, payload } = await callShopFunction(getDb(), "shop-shipping-quote", {
|
|
1340
|
+
tenantId,
|
|
1341
|
+
postalCode: digits,
|
|
1342
|
+
subtotal
|
|
1343
|
+
});
|
|
1344
|
+
const rows = ok ? payload?.options ?? [] : [];
|
|
1345
|
+
if (rows.length > 0) {
|
|
1346
|
+
return rows.map((row) => ({
|
|
1347
|
+
zoneId: String(row.service_id ?? ""),
|
|
1348
|
+
name: String(row.name ?? "Entrega"),
|
|
1349
|
+
carrier: row.carrier ?? void 0,
|
|
1350
|
+
rate: Number(row.price ?? 0),
|
|
1351
|
+
baseRate: Number(row.price ?? 0),
|
|
1352
|
+
freeAbove: null,
|
|
1353
|
+
etaMinDays: row.eta_min_days ?? void 0,
|
|
1354
|
+
etaMaxDays: row.eta_max_days ?? void 0,
|
|
1355
|
+
free: Number(row.price ?? 0) === 0
|
|
1356
|
+
}));
|
|
1357
|
+
}
|
|
1358
|
+
} catch {
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
const { data, error } = await getDb().rpc("shop_quote_shipping", {
|
|
1362
|
+
p_tenant_id: tenantId,
|
|
1363
|
+
p_postal_code: digits,
|
|
1364
|
+
p_subtotal: subtotal
|
|
1365
|
+
});
|
|
1366
|
+
if (error) return [];
|
|
1367
|
+
return (data ?? []).map((row) => ({
|
|
1368
|
+
zoneId: row.zone_id,
|
|
1369
|
+
name: row.name,
|
|
1370
|
+
carrier: row.carrier ?? void 0,
|
|
1371
|
+
rate: Number(row.rate ?? 0),
|
|
1372
|
+
baseRate: Number(row.base_rate ?? row.rate ?? 0),
|
|
1373
|
+
freeAbove: row.free_above == null ? null : Number(row.free_above),
|
|
1374
|
+
etaMinDays: row.eta_min_days ?? void 0,
|
|
1375
|
+
etaMaxDays: row.eta_max_days ?? void 0,
|
|
1376
|
+
free: Boolean(row.free)
|
|
1377
|
+
}));
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Charge a cart that is not an order yet — the phone sale, from the counter's
|
|
1381
|
+
* side. Same door the storefront uses, on purpose: `shop_price_cart` inside
|
|
1382
|
+
* the function reprices everything, so the console sends products and
|
|
1383
|
+
* quantities and never money. The order is born in `settleCheckoutSession`,
|
|
1384
|
+
* once the acquirer says the Pix landed, so an abandoned charge leaves no
|
|
1385
|
+
* ghost order holding stock.
|
|
1386
|
+
*/
|
|
1387
|
+
async openCheckoutSession(input) {
|
|
1388
|
+
const tenantId = getTenantId();
|
|
1389
|
+
if (!tenantId) throw new Error("@fayz-ai/shop: openCheckoutSession requires a tenant id.");
|
|
1390
|
+
const provider = String(input.provider ?? "").trim() || "abacatepay";
|
|
1391
|
+
const gateway = checkoutGateway(provider);
|
|
1392
|
+
if (!gateway) {
|
|
1393
|
+
throw new Error("Esta loja ainda n\xE3o tem um meio de cobran\xE7a conectado.");
|
|
1394
|
+
}
|
|
1395
|
+
const { ok, status, payload } = await callShopFunction(getDb(), gateway.open, {
|
|
1396
|
+
tenantId,
|
|
1397
|
+
items: input.items.map((item) => ({
|
|
1398
|
+
productId: item.productId,
|
|
1399
|
+
optionsLabel: item.optionsLabel ?? null,
|
|
1400
|
+
quantity: item.quantity
|
|
1401
|
+
})),
|
|
1402
|
+
customer: input.customer,
|
|
1403
|
+
shippingAddress: input.shippingAddress ?? null,
|
|
1404
|
+
discountCode: input.discountCode ?? null
|
|
1405
|
+
});
|
|
1406
|
+
if (!ok || !payload?.sessionId) {
|
|
1407
|
+
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."));
|
|
1408
|
+
}
|
|
1409
|
+
return {
|
|
1410
|
+
sessionId: payload.sessionId,
|
|
1411
|
+
total: Number(payload.total ?? 0),
|
|
1412
|
+
state: "open",
|
|
1413
|
+
provider,
|
|
1414
|
+
charge: payload.brCode ? {
|
|
1415
|
+
emv: payload.brCode,
|
|
1416
|
+
// AbacatePay embeds the `data:` prefix; the type promises bare
|
|
1417
|
+
// base64. Left in, the view prefixes it again and the QR is a
|
|
1418
|
+
// silently broken <img>.
|
|
1419
|
+
//
|
|
1420
|
+
// Vindi sends no base64 at all — its `qrCodeUrl` is a URL to an SVG,
|
|
1421
|
+
// which cannot be squeezed into this field without lying about what
|
|
1422
|
+
// it is. The buyer still gets the copy-and-paste code, which is what
|
|
1423
|
+
// actually pays; a rendered QR for Vindi waits for the payment view
|
|
1424
|
+
// to learn the second shape.
|
|
1425
|
+
qrcodeBase64: String(payload.brCodeBase64 ?? "").replace(/^data:image\/\w+;base64,/u, "") || null,
|
|
1426
|
+
expiresAt: payload.expiresAt ?? ""
|
|
1427
|
+
} : null
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Ask whether the money landed. Runs on a timer, so a network blip must read
|
|
1432
|
+
* as "still waiting" (`unknown`) and never as a refused payment.
|
|
1433
|
+
*
|
|
1434
|
+
* `provider` names which gateway opened the session — handed back by
|
|
1435
|
+
* `openCheckoutSession` rather than re-read from settings, because a merchant
|
|
1436
|
+
* who switches acquirer mid-checkout would otherwise have the new gateway
|
|
1437
|
+
* asked about a transaction that lives in the old one.
|
|
1438
|
+
*/
|
|
1439
|
+
async settleCheckoutSession(sessionId, provider) {
|
|
1440
|
+
const tenantId = getTenantId();
|
|
1441
|
+
if (!tenantId) return { state: "unknown" };
|
|
1442
|
+
const gateway = checkoutGateway(String(provider ?? "").trim() || "abacatepay");
|
|
1443
|
+
if (!gateway) return { state: "unknown" };
|
|
1444
|
+
try {
|
|
1445
|
+
const { ok, payload } = await callShopFunction(getDb(), gateway.settle, { tenantId, sessionId });
|
|
1446
|
+
if (!ok || !payload?.state) return { state: "unknown" };
|
|
1447
|
+
return { state: payload.state, orderId: payload.orderId ?? null, reason: payload.reason ?? null };
|
|
1448
|
+
} catch {
|
|
1449
|
+
return { state: "unknown" };
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
667
1452
|
// Trusted placement — delegates all price/discount/inventory authority to the
|
|
668
1453
|
// shop_place_order RPC (SECURITY DEFINER). The browser never sets a price.
|
|
669
1454
|
async placeOrder(input) {
|
|
@@ -700,7 +1485,12 @@ var SupabaseShopProvider = class {
|
|
|
700
1485
|
country: input.shippingAddress.country ?? "BR",
|
|
701
1486
|
recipient: input.shippingAddress.recipient ?? null,
|
|
702
1487
|
phone: input.shippingAddress.phone ?? null,
|
|
703
|
-
label: input.shippingAddress.label ?? null
|
|
1488
|
+
label: input.shippingAddress.label ?? null,
|
|
1489
|
+
// Not an address field: it rides here because shop_place_order's
|
|
1490
|
+
// signature cannot grow without a DROP, and 0050's trigger lifts it to
|
|
1491
|
+
// plg_shop_orders.customer_document. The storefront already sent it;
|
|
1492
|
+
// the console did not, so a phone sale was born with no CPF.
|
|
1493
|
+
document: input.shippingAddress.document ?? null
|
|
704
1494
|
} : null,
|
|
705
1495
|
p_payment_method: input.paymentMethod ?? null,
|
|
706
1496
|
p_notes: input.notes ?? null,
|
|
@@ -723,7 +1513,7 @@ var SupabaseShopProvider = class {
|
|
|
723
1513
|
const tenantId = getTenantId();
|
|
724
1514
|
let q = db.from(T.customers).select("*").order("first_name", { ascending: true });
|
|
725
1515
|
if (tenantId) q = q.eq("tenant_id", tenantId);
|
|
726
|
-
|
|
1516
|
+
for (const filter of customerSearchFilters(options?.search)) q = q.or(filter);
|
|
727
1517
|
if (options?.limit) q = q.limit(options.limit);
|
|
728
1518
|
const { data, error } = await q;
|
|
729
1519
|
if (error) throw error;
|
|
@@ -735,7 +1525,15 @@ var SupabaseShopProvider = class {
|
|
|
735
1525
|
return data ? rowToCustomer(data) : null;
|
|
736
1526
|
}
|
|
737
1527
|
async createCustomer(input) {
|
|
738
|
-
const { data, error } = await getDb().from(T.customers).insert({
|
|
1528
|
+
const { data, error } = await getDb().from(T.customers).insert({
|
|
1529
|
+
tenant_id: getTenantId(),
|
|
1530
|
+
first_name: input.firstName,
|
|
1531
|
+
last_name: input.lastName ?? "",
|
|
1532
|
+
email: input.email ?? null,
|
|
1533
|
+
phone: input.phone ?? null,
|
|
1534
|
+
document: input.document ?? null,
|
|
1535
|
+
notes: input.notes ?? null
|
|
1536
|
+
}).select().single();
|
|
739
1537
|
if (error) throw error;
|
|
740
1538
|
return rowToCustomer(data);
|
|
741
1539
|
}
|
|
@@ -745,6 +1543,7 @@ var SupabaseShopProvider = class {
|
|
|
745
1543
|
if (input.lastName !== void 0) updates.last_name = input.lastName;
|
|
746
1544
|
if (input.email !== void 0) updates.email = input.email;
|
|
747
1545
|
if (input.phone !== void 0) updates.phone = input.phone;
|
|
1546
|
+
if (input.document !== void 0) updates.document = input.document;
|
|
748
1547
|
if (input.notes !== void 0) updates.notes = input.notes;
|
|
749
1548
|
const { data, error } = await getDb().from(T.customers).update(updates).eq("id", id).select().single();
|
|
750
1549
|
if (error) throw error;
|
|
@@ -920,22 +1719,6 @@ var SupabaseShopProvider = class {
|
|
|
920
1719
|
async deleteEntityRecord(entity, id) {
|
|
921
1720
|
return this.entityStore(entity).delete(id);
|
|
922
1721
|
}
|
|
923
|
-
// Sidecar fields on core records (migration 0031): one row per
|
|
924
|
-
// (reference, reference_id), validated on write against defineSidecarFields.
|
|
925
|
-
async getEntityFields(reference, referenceId) {
|
|
926
|
-
const { data, error } = await getDb().from(SIDECAR_TABLE).select("data").eq("reference", reference).eq("reference_id", referenceId).maybeSingle();
|
|
927
|
-
if (error) throw error;
|
|
928
|
-
return data ? data.data ?? {} : null;
|
|
929
|
-
}
|
|
930
|
-
async setEntityFields(reference, referenceId, fields) {
|
|
931
|
-
validateSidecarWrite(reference, fields);
|
|
932
|
-
const { data, error } = await getDb().from(SIDECAR_TABLE).upsert(
|
|
933
|
-
{ tenant_id: getTenantId(), reference, reference_id: referenceId, data: fields },
|
|
934
|
-
{ onConflict: "tenant_id,reference,reference_id" }
|
|
935
|
-
).select("data").single();
|
|
936
|
-
if (error) throw error;
|
|
937
|
-
return data.data ?? {};
|
|
938
|
-
}
|
|
939
1722
|
// -------------------------------------------------------------------------
|
|
940
1723
|
// Coleções, grupos/segmentos, vale-presente, e-mails e mídia.
|
|
941
1724
|
// -------------------------------------------------------------------------
|
|
@@ -1130,10 +1913,27 @@ var SupabaseShopProvider = class {
|
|
|
1130
1913
|
return rowToEmailTemplate(data);
|
|
1131
1914
|
}
|
|
1132
1915
|
async listMediaFiles() {
|
|
1133
|
-
const db = getDb();
|
|
1134
1916
|
const tenantId = getTenantId();
|
|
1135
1917
|
if (!tenantId) return [];
|
|
1136
|
-
const
|
|
1918
|
+
const [attached, library] = await Promise.all([
|
|
1919
|
+
this.listAttachedMedia(tenantId),
|
|
1920
|
+
this.listLibraryFiles(tenantId)
|
|
1921
|
+
]);
|
|
1922
|
+
return mergeMediaFiles(attached, library);
|
|
1923
|
+
}
|
|
1924
|
+
/** Every product photo in the store, each one carrying its product. */
|
|
1925
|
+
async listAttachedMedia(tenantId) {
|
|
1926
|
+
const db = getDb();
|
|
1927
|
+
const { data: rows, error } = await db.from(T.productImages).select("id,product_id,url,alt_text,sort_order,is_primary,created_at").eq("tenant_id", tenantId).order("created_at", { ascending: false }).limit(1e3);
|
|
1928
|
+
if (error || !rows?.length) return [];
|
|
1929
|
+
const productIds = [...new Set(rows.map((r) => r.product_id).filter(Boolean))];
|
|
1930
|
+
const { data: products } = await db.from(T.products).select("id,name").in("id", productIds);
|
|
1931
|
+
const nameById = new Map((products ?? []).map((p) => [p.id, p.name]));
|
|
1932
|
+
return rows.map((row) => productImageToMediaFile(rowToImage(row), nameById.get(row.product_id)));
|
|
1933
|
+
}
|
|
1934
|
+
/** What the merchant uploaded straight to the library — owned by nothing. */
|
|
1935
|
+
async listLibraryFiles(tenantId) {
|
|
1936
|
+
const bucket = getDb().storage.from(T.mediaBucket);
|
|
1137
1937
|
const listAt = async (prefix) => {
|
|
1138
1938
|
const { data, error } = await bucket.list(prefix, {
|
|
1139
1939
|
limit: 1e3,
|
|
@@ -1143,14 +1943,15 @@ var SupabaseShopProvider = class {
|
|
|
1143
1943
|
return (data ?? []).map((entry) => ({ prefix, entry }));
|
|
1144
1944
|
};
|
|
1145
1945
|
const top = await listAt(tenantId);
|
|
1146
|
-
const folders = top.filter(({ entry }) => !entry.id).map(({ entry }) => `${tenantId}/${entry.name}`);
|
|
1946
|
+
const folders = top.filter(({ entry }) => !entry.id && entry.name === LIBRARY_FOLDER).map(({ entry }) => `${tenantId}/${entry.name}`);
|
|
1147
1947
|
const nested = (await Promise.all(folders.map(listAt))).flat();
|
|
1148
1948
|
return [...top, ...nested].filter(({ entry }) => entry.id).map(({ prefix, entry }) => {
|
|
1149
1949
|
const path = `${prefix}/${entry.name}`;
|
|
1150
1950
|
const { data: { publicUrl } } = bucket.getPublicUrl(path);
|
|
1951
|
+
const name = displayNameOf(entry.name);
|
|
1151
1952
|
const file = {
|
|
1152
1953
|
id: path,
|
|
1153
|
-
name
|
|
1954
|
+
name,
|
|
1154
1955
|
url: publicUrl,
|
|
1155
1956
|
kind: mediaKindOf(entry.name, entry.metadata?.mimetype),
|
|
1156
1957
|
size: Number(entry.metadata?.size ?? 0),
|
|
@@ -1158,13 +1959,13 @@ var SupabaseShopProvider = class {
|
|
|
1158
1959
|
createdAt: entry.created_at ?? entry.updated_at ?? ""
|
|
1159
1960
|
};
|
|
1160
1961
|
return file;
|
|
1161
|
-
})
|
|
1962
|
+
});
|
|
1162
1963
|
}
|
|
1163
1964
|
async uploadMediaFile(file) {
|
|
1164
1965
|
const db = getDb();
|
|
1165
1966
|
const tenantId = getTenantId();
|
|
1166
1967
|
if (!tenantId) throw new Error("@fayz-ai/shop: no active tenant for media upload");
|
|
1167
|
-
const path = `${tenantId}
|
|
1968
|
+
const path = `${tenantId}/${LIBRARY_FOLDER}/${Date.now()}-${file.name}`;
|
|
1168
1969
|
const bucket = db.storage.from(T.mediaBucket);
|
|
1169
1970
|
const { error } = await bucket.upload(path, file);
|
|
1170
1971
|
if (error) throw error;
|
|
@@ -1217,6 +2018,70 @@ function createSupabaseShopProvider() {
|
|
|
1217
2018
|
return new SupabaseShopProvider();
|
|
1218
2019
|
}
|
|
1219
2020
|
|
|
2021
|
+
// src/shipping/label-provider.ts
|
|
2022
|
+
var registry = /* @__PURE__ */ new Map();
|
|
2023
|
+
function registerShippingLabelProvider(provider) {
|
|
2024
|
+
registry.set(provider.id, provider);
|
|
2025
|
+
}
|
|
2026
|
+
function listShippingLabelProviders() {
|
|
2027
|
+
return [...registry.values()];
|
|
2028
|
+
}
|
|
2029
|
+
function resetShippingLabelProviders() {
|
|
2030
|
+
registry.clear();
|
|
2031
|
+
}
|
|
2032
|
+
function resolveShippingLabelProvider(options) {
|
|
2033
|
+
const { connected, preferred } = options;
|
|
2034
|
+
if (preferred === "") return null;
|
|
2035
|
+
if (preferred) {
|
|
2036
|
+
const chosen = registry.get(preferred);
|
|
2037
|
+
return chosen && connected.includes(preferred) ? chosen : null;
|
|
2038
|
+
}
|
|
2039
|
+
for (const id of connected) {
|
|
2040
|
+
const found = registry.get(id);
|
|
2041
|
+
if (found) return found;
|
|
2042
|
+
}
|
|
2043
|
+
return null;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
// src/lib/document.ts
|
|
2047
|
+
var documentDigits = (value) => String(value ?? "").replace(/\D/g, "");
|
|
2048
|
+
function checkDigit(digits, weights) {
|
|
2049
|
+
const sum = weights.reduce((acc, weight, index) => acc + Number(digits[index]) * weight, 0);
|
|
2050
|
+
const rest = sum % 11;
|
|
2051
|
+
return rest < 2 ? 0 : 11 - rest;
|
|
2052
|
+
}
|
|
2053
|
+
var CPF_FIRST = [10, 9, 8, 7, 6, 5, 4, 3, 2];
|
|
2054
|
+
var CPF_SECOND = [11, 10, 9, 8, 7, 6, 5, 4, 3, 2];
|
|
2055
|
+
var CNPJ_FIRST = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
|
|
2056
|
+
var CNPJ_SECOND = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
|
|
2057
|
+
function isValidCpf(value) {
|
|
2058
|
+
const digits = documentDigits(value);
|
|
2059
|
+
if (digits.length !== 11 || /^(\d)\1{10}$/.test(digits)) return false;
|
|
2060
|
+
return checkDigit(digits, CPF_FIRST) === Number(digits[9]) && checkDigit(digits, CPF_SECOND) === Number(digits[10]);
|
|
2061
|
+
}
|
|
2062
|
+
function isValidCnpj(value) {
|
|
2063
|
+
const digits = documentDigits(value);
|
|
2064
|
+
if (digits.length !== 14 || /^(\d)\1{13}$/.test(digits)) return false;
|
|
2065
|
+
return checkDigit(digits, CNPJ_FIRST) === Number(digits[12]) && checkDigit(digits, CNPJ_SECOND) === Number(digits[13]);
|
|
2066
|
+
}
|
|
2067
|
+
function documentKind(value) {
|
|
2068
|
+
const length = documentDigits(value).length;
|
|
2069
|
+
if (length === 11) return "cpf";
|
|
2070
|
+
if (length === 14) return "cnpj";
|
|
2071
|
+
return null;
|
|
2072
|
+
}
|
|
2073
|
+
function isValidDocument(value) {
|
|
2074
|
+
const kind = documentKind(value);
|
|
2075
|
+
return kind === "cpf" ? isValidCpf(value) : kind === "cnpj" ? isValidCnpj(value) : false;
|
|
2076
|
+
}
|
|
2077
|
+
function formatDocument(value) {
|
|
2078
|
+
const digits = documentDigits(value).slice(0, 14);
|
|
2079
|
+
if (digits.length <= 11) {
|
|
2080
|
+
return digits.replace(/^(\d{3})(\d)/, "$1.$2").replace(/^(\d{3})\.(\d{3})(\d)/, "$1.$2.$3").replace(/\.(\d{3})(\d{1,2})$/, ".$1-$2");
|
|
2081
|
+
}
|
|
2082
|
+
return digits.replace(/^(\d{2})(\d)/, "$1.$2").replace(/^(\d{2})\.(\d{3})(\d)/, "$1.$2.$3").replace(/\.(\d{3})(\d)/, ".$1/$2").replace(/(\d{4})(\d{1,2})$/, "$1-$2");
|
|
2083
|
+
}
|
|
2084
|
+
|
|
1220
2085
|
// src/lib/emails.ts
|
|
1221
2086
|
var EMAIL_PLACEHOLDERS = [
|
|
1222
2087
|
"{{store_name}}",
|
|
@@ -1406,6 +2271,6 @@ function buildShopMessageDefaults() {
|
|
|
1406
2271
|
});
|
|
1407
2272
|
}
|
|
1408
2273
|
|
|
1409
|
-
export { EMAIL_PLACEHOLDERS, EMAIL_PREVIEW_SAMPLE, SHOP_EMAIL_EVENTS, SupabaseShopProvider, applyDiscount, buildShopDomainEvents, buildShopMessageDefaults, checkInventory, computeCartTotals, createSupabaseShopProvider, getCustomerOrders, renderEmailTemplate, resolveEmailTemplates, validateDiscount };
|
|
2274
|
+
export { CHARGEABLE_PROVIDERS, EMAIL_PLACEHOLDERS, EMAIL_PREVIEW_SAMPLE, SHOP_EMAIL_EVENTS, SupabaseShopProvider, applyDiscount, buildShopDomainEvents, buildShopMessageDefaults, checkInventory, checkoutGateway, computeCartTotals, createSupabaseShopProvider, documentDigits, documentKind, formatDocument, getCustomerOrders, getShopFunctionsUrl, isChargeableProvider, isValidCnpj, isValidCpf, isValidDocument, listShippingLabelProviders, registerShippingLabelProvider, renderEmailTemplate, resetShippingLabelProviders, resolveEmailTemplates, resolveShippingLabelProvider, setShopFunctionsUrl, validateDiscount };
|
|
1410
2275
|
//# sourceMappingURL=index.js.map
|
|
1411
2276
|
//# sourceMappingURL=index.js.map
|