@fayz-ai/shop 0.10.0 → 0.11.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/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 +200 -7
- package/dist/data/provider.d.ts.map +1 -1
- package/dist/data/supabase-provider.d.ts +113 -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 +14 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +891 -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 +457 -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,52 @@ 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/customer-search.ts
|
|
133
|
+
var COLUMNS = ["first_name", "last_name", "email", "phone"];
|
|
134
|
+
function customerSearchFilters(term) {
|
|
135
|
+
return String(term ?? "").trim().split(/\s+/u).map((word) => word.replace(/[%,()]/gu, "")).filter(Boolean).map((word) => COLUMNS.map((column) => `${column}.ilike.%${word}%`).join(","));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/data/supabase-provider.ts
|
|
89
139
|
function getDb() {
|
|
90
140
|
const supabase = getSupabaseClientOptional();
|
|
91
141
|
if (!supabase) throw new Error("@fayz-ai/shop: Supabase client not initialized. Call setGlobalSupabaseClient() first.");
|
|
@@ -103,6 +153,95 @@ function toPostalDigits(value) {
|
|
|
103
153
|
const digits = String(value ?? "").replace(/\D/g, "");
|
|
104
154
|
return digits ? digits.padStart(8, "0").slice(0, 8) : void 0;
|
|
105
155
|
}
|
|
156
|
+
function rowToVariant(row) {
|
|
157
|
+
return {
|
|
158
|
+
id: row.id,
|
|
159
|
+
productId: row.product_id,
|
|
160
|
+
options: row.options && typeof row.options === "object" ? row.options : {},
|
|
161
|
+
sku: row.sku ?? null,
|
|
162
|
+
// numeric(12,2) arrives as a string on the wire; Number() once, here.
|
|
163
|
+
price: row.price == null ? null : Number(row.price),
|
|
164
|
+
compareAtPrice: row.compare_at_price == null ? null : Number(row.compare_at_price),
|
|
165
|
+
inventoryCount: row.inventory_count ?? 0,
|
|
166
|
+
position: row.position ?? 0,
|
|
167
|
+
status: row.status ?? "active",
|
|
168
|
+
imageId: row.image_id ?? null,
|
|
169
|
+
isDefaultVariant: row.is_default_variant ?? false,
|
|
170
|
+
// Null inherits the product's, the way `price` has since 0028.
|
|
171
|
+
weight: numOrNull(row.weight),
|
|
172
|
+
weightUnit: row.weight_unit ?? null,
|
|
173
|
+
heightCm: numOrNull(row.height_cm),
|
|
174
|
+
widthCm: numOrNull(row.width_cm),
|
|
175
|
+
lengthCm: numOrNull(row.length_cm)
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function variantPatch(input) {
|
|
179
|
+
const patch = {};
|
|
180
|
+
const map = {
|
|
181
|
+
sku: "sku",
|
|
182
|
+
price: "price",
|
|
183
|
+
compareAtPrice: "compare_at_price",
|
|
184
|
+
position: "position",
|
|
185
|
+
status: "status",
|
|
186
|
+
imageId: "image_id",
|
|
187
|
+
weight: "weight",
|
|
188
|
+
weightUnit: "weight_unit",
|
|
189
|
+
heightCm: "height_cm",
|
|
190
|
+
widthCm: "width_cm",
|
|
191
|
+
lengthCm: "length_cm"
|
|
192
|
+
};
|
|
193
|
+
for (const [key, column] of Object.entries(map)) {
|
|
194
|
+
const value = input[key];
|
|
195
|
+
if (value === void 0) continue;
|
|
196
|
+
patch[column] = key === "sku" && typeof value === "string" && value.trim() === "" ? null : value;
|
|
197
|
+
}
|
|
198
|
+
return patch;
|
|
199
|
+
}
|
|
200
|
+
function rowToStoreSettings(row) {
|
|
201
|
+
const bool = (v) => v == null ? void 0 : Boolean(v);
|
|
202
|
+
const methods = Array.isArray(row.payment_methods) && row.payment_methods.length > 0 ? row.payment_methods : void 0;
|
|
203
|
+
const fields = row.checkout_fields && typeof row.checkout_fields === "object" && Object.keys(row.checkout_fields).length > 0 ? row.checkout_fields : void 0;
|
|
204
|
+
return {
|
|
205
|
+
currency: row.currency ?? void 0,
|
|
206
|
+
locale: row.locale ?? void 0,
|
|
207
|
+
showOutOfStock: bool(row.show_out_of_stock),
|
|
208
|
+
requireSku: bool(row.require_sku),
|
|
209
|
+
stockOwnership: row.stock_ownership ?? void 0,
|
|
210
|
+
paymentMethods: methods,
|
|
211
|
+
paymentProvider: row.payment_provider ?? void 0,
|
|
212
|
+
guestCheckout: bool(row.guest_checkout),
|
|
213
|
+
requireConfirmedEmail: bool(row.require_confirmed_email),
|
|
214
|
+
marketingOptIn: bool(row.marketing_opt_in),
|
|
215
|
+
discountsEnabled: bool(row.discounts_enabled),
|
|
216
|
+
checkoutFields: fields,
|
|
217
|
+
autoFulfill: row.auto_fulfill ?? void 0,
|
|
218
|
+
notifyNewOrder: bool(row.notify_new_order),
|
|
219
|
+
notifyLowStock: bool(row.notify_low_stock),
|
|
220
|
+
handlingNote: row.handling_note ?? void 0,
|
|
221
|
+
carrierQuotes: bool(row.carrier_quotes)
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function storeSettingsPayload(input) {
|
|
225
|
+
const payload = {};
|
|
226
|
+
if (input.currency !== void 0) payload.currency = input.currency;
|
|
227
|
+
if (input.locale !== void 0) payload.locale = input.locale;
|
|
228
|
+
if (input.showOutOfStock !== void 0) payload.show_out_of_stock = input.showOutOfStock;
|
|
229
|
+
if (input.requireSku !== void 0) payload.require_sku = input.requireSku;
|
|
230
|
+
if (input.stockOwnership !== void 0) payload.stock_ownership = input.stockOwnership;
|
|
231
|
+
if (input.paymentMethods !== void 0) payload.payment_methods = input.paymentMethods;
|
|
232
|
+
if (input.paymentProvider !== void 0) payload.payment_provider = input.paymentProvider ?? null;
|
|
233
|
+
if (input.guestCheckout !== void 0) payload.guest_checkout = input.guestCheckout;
|
|
234
|
+
if (input.requireConfirmedEmail !== void 0) payload.require_confirmed_email = input.requireConfirmedEmail;
|
|
235
|
+
if (input.marketingOptIn !== void 0) payload.marketing_opt_in = input.marketingOptIn;
|
|
236
|
+
if (input.discountsEnabled !== void 0) payload.discounts_enabled = input.discountsEnabled;
|
|
237
|
+
if (input.checkoutFields !== void 0) payload.checkout_fields = input.checkoutFields ?? {};
|
|
238
|
+
if (input.autoFulfill !== void 0) payload.auto_fulfill = input.autoFulfill;
|
|
239
|
+
if (input.notifyNewOrder !== void 0) payload.notify_new_order = input.notifyNewOrder;
|
|
240
|
+
if (input.notifyLowStock !== void 0) payload.notify_low_stock = input.notifyLowStock;
|
|
241
|
+
if (input.handlingNote !== void 0) payload.handling_note = input.handlingNote || null;
|
|
242
|
+
if (input.carrierQuotes !== void 0) payload.carrier_quotes = input.carrierQuotes;
|
|
243
|
+
return payload;
|
|
244
|
+
}
|
|
106
245
|
function rowToShippingZone(row) {
|
|
107
246
|
return {
|
|
108
247
|
id: row.id,
|
|
@@ -116,9 +255,74 @@ function rowToShippingZone(row) {
|
|
|
116
255
|
etaMinDays: row.eta_min_days ?? null,
|
|
117
256
|
etaMaxDays: row.eta_max_days ?? null,
|
|
118
257
|
active: row.active ?? true,
|
|
119
|
-
sortOrder: row.sort_order ?? 0
|
|
258
|
+
sortOrder: row.sort_order ?? 0,
|
|
259
|
+
excluded: row.excluded ?? false,
|
|
260
|
+
profileId: row.profile_id ?? null
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function rowToShippingProfile(row) {
|
|
264
|
+
return {
|
|
265
|
+
id: row.id,
|
|
266
|
+
tenantId: row.tenant_id,
|
|
267
|
+
name: row.name,
|
|
268
|
+
// undefined, never false: NULL means "inherit the store's decision", and a
|
|
269
|
+
// mapper that coalesced would turn every new profile into one that refuses
|
|
270
|
+
// the gateway.
|
|
271
|
+
carrierQuotes: row.carrier_quotes == null ? void 0 : Boolean(row.carrier_quotes),
|
|
272
|
+
isDefault: row.is_default ?? false
|
|
120
273
|
};
|
|
121
274
|
}
|
|
275
|
+
function shippingProfilePayload(input) {
|
|
276
|
+
const payload = {};
|
|
277
|
+
if (input.name !== void 0) payload.name = input.name;
|
|
278
|
+
if (input.carrierQuotes !== void 0) payload.carrier_quotes = input.carrierQuotes ?? null;
|
|
279
|
+
if (input.isDefault !== void 0) payload.is_default = input.isDefault;
|
|
280
|
+
return payload;
|
|
281
|
+
}
|
|
282
|
+
function toOriginPostalCode(raw) {
|
|
283
|
+
const digits = String(raw ?? "").replace(/\D/g, "");
|
|
284
|
+
if (!digits) return null;
|
|
285
|
+
if (digits.length !== 8) throw new Error("O CEP do local precisa ter 8 d\xEDgitos.");
|
|
286
|
+
return digits;
|
|
287
|
+
}
|
|
288
|
+
function toLocationCode(name) {
|
|
289
|
+
const slug = name.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
290
|
+
return slug || "local";
|
|
291
|
+
}
|
|
292
|
+
function locationWriteError(error) {
|
|
293
|
+
if (error?.code === "23505") return new Error("J\xE1 existe um local com esse nome.");
|
|
294
|
+
if (error?.code === "23503") {
|
|
295
|
+
return new Error(
|
|
296
|
+
"Este local j\xE1 tem movimenta\xE7\xF5es de estoque. Arquive-o em vez de excluir \u2014 o hist\xF3rico aponta para ele."
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
return new Error(error?.message ?? "N\xE3o foi poss\xEDvel salvar o local.");
|
|
300
|
+
}
|
|
301
|
+
function stockLocationPayload(input) {
|
|
302
|
+
const payload = {};
|
|
303
|
+
if (input.name !== void 0) payload.name = input.name;
|
|
304
|
+
if (input.code !== void 0) payload.code = input.code;
|
|
305
|
+
if (input.kind !== void 0) payload.kind = input.kind;
|
|
306
|
+
if (input.isSellable !== void 0) payload.is_sellable = input.isSellable;
|
|
307
|
+
if (input.isDefault !== void 0) payload.is_default = input.isDefault;
|
|
308
|
+
if (input.status !== void 0) payload.status = input.status;
|
|
309
|
+
if (input.postalCode !== void 0) payload.postal_code = toOriginPostalCode(input.postalCode);
|
|
310
|
+
if (input.city !== void 0) payload.city = input.city || null;
|
|
311
|
+
if (input.state !== void 0) payload.state = input.state || null;
|
|
312
|
+
if (input.street !== void 0) payload.street = input.street || null;
|
|
313
|
+
if (input.number !== void 0) payload.number = input.number || null;
|
|
314
|
+
if (input.district !== void 0) payload.district = input.district || null;
|
|
315
|
+
if (input.complement !== void 0) payload.complement = input.complement || null;
|
|
316
|
+
if (input.document !== void 0) payload.document = String(input.document ?? "").replace(/\D/gu, "") || null;
|
|
317
|
+
if (input.phone !== void 0) payload.phone = String(input.phone ?? "").replace(/\D/gu, "") || null;
|
|
318
|
+
if (input.email !== void 0) payload.email = input.email || null;
|
|
319
|
+
if (input.carrierAgencies !== void 0) payload.carrier_agencies = input.carrierAgencies ?? {};
|
|
320
|
+
if (input.shipsOnline !== void 0) payload.ships_online = input.shipsOnline;
|
|
321
|
+
if (input.fulfillmentProvider !== void 0) {
|
|
322
|
+
payload.fulfillment_provider = input.fulfillmentProvider || null;
|
|
323
|
+
}
|
|
324
|
+
return payload;
|
|
325
|
+
}
|
|
122
326
|
function shippingZonePayload(input) {
|
|
123
327
|
const payload = {};
|
|
124
328
|
if (input.name !== void 0) payload.name = input.name;
|
|
@@ -131,6 +335,34 @@ function shippingZonePayload(input) {
|
|
|
131
335
|
if (input.etaMaxDays !== void 0) payload.eta_max_days = input.etaMaxDays ?? null;
|
|
132
336
|
if (input.active !== void 0) payload.active = input.active;
|
|
133
337
|
if (input.sortOrder !== void 0) payload.sort_order = input.sortOrder;
|
|
338
|
+
if (input.excluded !== void 0) payload.excluded = input.excluded;
|
|
339
|
+
if (input.profileId !== void 0) payload.profile_id = input.profileId ?? null;
|
|
340
|
+
return payload;
|
|
341
|
+
}
|
|
342
|
+
function rowToShippingPackage(row) {
|
|
343
|
+
return {
|
|
344
|
+
id: row.id,
|
|
345
|
+
tenantId: row.tenant_id,
|
|
346
|
+
name: row.name,
|
|
347
|
+
kind: row.kind ?? "box",
|
|
348
|
+
heightCm: Number(row.height_cm ?? 0),
|
|
349
|
+
widthCm: Number(row.width_cm ?? 0),
|
|
350
|
+
lengthCm: Number(row.length_cm ?? 0),
|
|
351
|
+
emptyWeightGrams: Number(row.empty_weight_grams ?? 0),
|
|
352
|
+
isDefault: row.is_default ?? false,
|
|
353
|
+
active: row.active ?? true
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function shippingPackagePayload(input) {
|
|
357
|
+
const payload = {};
|
|
358
|
+
if (input.name !== void 0) payload.name = input.name;
|
|
359
|
+
if (input.kind !== void 0) payload.kind = input.kind;
|
|
360
|
+
if (input.heightCm !== void 0) payload.height_cm = input.heightCm;
|
|
361
|
+
if (input.widthCm !== void 0) payload.width_cm = input.widthCm;
|
|
362
|
+
if (input.lengthCm !== void 0) payload.length_cm = input.lengthCm;
|
|
363
|
+
if (input.emptyWeightGrams !== void 0) payload.empty_weight_grams = input.emptyWeightGrams;
|
|
364
|
+
if (input.isDefault !== void 0) payload.is_default = input.isDefault;
|
|
365
|
+
if (input.active !== void 0) payload.active = input.active;
|
|
134
366
|
return payload;
|
|
135
367
|
}
|
|
136
368
|
function rowToProduct(row) {
|
|
@@ -154,10 +386,21 @@ function rowToProduct(row) {
|
|
|
154
386
|
isPhysical: row.is_physical ?? true,
|
|
155
387
|
weight: row.weight,
|
|
156
388
|
weightUnit: row.weight_unit ?? "kg",
|
|
389
|
+
heightCm: numOrNull(row.height_cm),
|
|
390
|
+
widthCm: numOrNull(row.width_cm),
|
|
391
|
+
lengthCm: numOrNull(row.length_cm),
|
|
392
|
+
packageId: row.package_id ?? null,
|
|
393
|
+
handling: Array.isArray(row.handling) ? row.handling : [],
|
|
394
|
+
shippingProfileId: row.shipping_profile_id ?? null,
|
|
157
395
|
createdAt: row.created_at,
|
|
158
396
|
updatedAt: row.updated_at
|
|
159
397
|
};
|
|
160
398
|
}
|
|
399
|
+
function numOrNull(value) {
|
|
400
|
+
if (value == null || value === "") return null;
|
|
401
|
+
const n = Number(value);
|
|
402
|
+
return Number.isFinite(n) ? n : null;
|
|
403
|
+
}
|
|
161
404
|
function rowToImage(row) {
|
|
162
405
|
return {
|
|
163
406
|
id: row.id,
|
|
@@ -229,6 +472,7 @@ function rowToCustomer(row) {
|
|
|
229
472
|
lastName: row.last_name ?? "",
|
|
230
473
|
email: row.email,
|
|
231
474
|
phone: row.phone,
|
|
475
|
+
document: row.document ?? null,
|
|
232
476
|
notes: row.notes,
|
|
233
477
|
ordersCount: row.orders_count ?? 0,
|
|
234
478
|
totalSpent: row.total_spent ?? 0,
|
|
@@ -285,22 +529,13 @@ function segmentPayload(input) {
|
|
|
285
529
|
return p;
|
|
286
530
|
}
|
|
287
531
|
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
532
|
function generateGiftCardCode() {
|
|
291
533
|
const bytes = new Uint8Array(12);
|
|
292
534
|
globalThis.crypto.getRandomValues(bytes);
|
|
293
535
|
const chars = Array.from(bytes, (b) => GIFT_CODE_ALPHABET[b % GIFT_CODE_ALPHABET.length]);
|
|
294
536
|
return `${chars.slice(0, 4).join("")}-${chars.slice(4, 8).join("")}-${chars.slice(8, 12).join("")}`;
|
|
295
537
|
}
|
|
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
|
-
}
|
|
538
|
+
var LIBRARY_FOLDER = "library";
|
|
304
539
|
function nullableNumber(value) {
|
|
305
540
|
if (value === null || value === void 0 || value === "") return null;
|
|
306
541
|
const n = Number(value);
|
|
@@ -418,11 +653,13 @@ var SupabaseShopProvider = class {
|
|
|
418
653
|
* RLS on both tables already restricts anon reads to active variants of
|
|
419
654
|
* publicly readable products; the status filter here keeps the admin path
|
|
420
655
|
* (authenticated tenant member, who CAN see archived rows) consistent. */
|
|
421
|
-
async getProductVariants(productId) {
|
|
656
|
+
async getProductVariants(productId, opts) {
|
|
422
657
|
const db = getDb();
|
|
658
|
+
let variantQuery = db.from(T.productVariants).select("*").eq("product_id", productId);
|
|
659
|
+
if (!opts?.includeArchived) variantQuery = variantQuery.eq("status", "active");
|
|
423
660
|
const [optionsRes, variantsRes] = await Promise.all([
|
|
424
661
|
db.from(T.productOptions).select("*").eq("product_id", productId).order("position", { ascending: true }),
|
|
425
|
-
|
|
662
|
+
variantQuery.order("position", { ascending: true })
|
|
426
663
|
]);
|
|
427
664
|
if (optionsRes.error) throw optionsRes.error;
|
|
428
665
|
if (variantsRes.error) throw variantsRes.error;
|
|
@@ -432,19 +669,102 @@ var SupabaseShopProvider = class {
|
|
|
432
669
|
values: Array.isArray(row.values) ? row.values.map(String) : [],
|
|
433
670
|
position: row.position ?? 0
|
|
434
671
|
}));
|
|
435
|
-
const variants = (variantsRes.data ?? []).map(
|
|
672
|
+
const variants = (variantsRes.data ?? []).map(rowToVariant);
|
|
673
|
+
return { options, variants };
|
|
674
|
+
}
|
|
675
|
+
// --- The variant admin ------------------------------------------------------
|
|
676
|
+
async saveProductOptions(productId, input) {
|
|
677
|
+
const db = getDb();
|
|
678
|
+
const tenantId = getTenantId();
|
|
679
|
+
const clean = normalizeVariantAxes(input);
|
|
680
|
+
const refusal = validateVariantAxes(clean);
|
|
681
|
+
if (refusal) throw new Error(refusal);
|
|
682
|
+
const { data: existing, error: readError } = await db.from(T.productOptions).select("*").eq("product_id", productId);
|
|
683
|
+
if (readError) throw readError;
|
|
684
|
+
const byName = new Map((existing ?? []).map((row) => [row.name, row]));
|
|
685
|
+
const wanted = new Set(clean.map((axis) => axis.name));
|
|
686
|
+
const stale = (existing ?? []).filter((row) => !wanted.has(row.name)).map((row) => row.id);
|
|
687
|
+
if (stale.length > 0) {
|
|
688
|
+
const { error: error2 } = await db.from(T.productOptions).delete().in("id", stale);
|
|
689
|
+
if (error2) throw error2;
|
|
690
|
+
}
|
|
691
|
+
for (const [position, axis] of clean.entries()) {
|
|
692
|
+
const row = byName.get(axis.name);
|
|
693
|
+
if (row) {
|
|
694
|
+
const { error: error2 } = await db.from(T.productOptions).update({ values: axis.values, position }).eq("id", row.id);
|
|
695
|
+
if (error2) throw error2;
|
|
696
|
+
} else {
|
|
697
|
+
const { error: error2 } = await db.from(T.productOptions).insert({
|
|
698
|
+
product_id: productId,
|
|
699
|
+
tenant_id: tenantId,
|
|
700
|
+
name: axis.name,
|
|
701
|
+
values: axis.values,
|
|
702
|
+
position
|
|
703
|
+
});
|
|
704
|
+
if (error2) throw error2;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
const { data, error } = await db.from(T.productOptions).select("*").eq("product_id", productId).order("position", { ascending: true });
|
|
708
|
+
if (error) throw error;
|
|
709
|
+
return (data ?? []).map((row) => ({
|
|
436
710
|
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
|
|
711
|
+
name: row.name,
|
|
712
|
+
values: Array.isArray(row.values) ? row.values.map(String) : [],
|
|
713
|
+
position: row.position ?? 0
|
|
446
714
|
}));
|
|
447
|
-
|
|
715
|
+
}
|
|
716
|
+
async createProductVariant(productId, input) {
|
|
717
|
+
const db = getDb();
|
|
718
|
+
const tenantId = getTenantId();
|
|
719
|
+
if (Object.keys(input.options).length === 0) {
|
|
720
|
+
throw new Error("Uma variante sem op\xE7\xF5es \xE9 a variante padr\xE3o (0037), criada pelo banco. Declare ao menos um eixo.");
|
|
721
|
+
}
|
|
722
|
+
const { data: existing, error: readError } = await db.from(T.productVariants).select("*").eq("product_id", productId);
|
|
723
|
+
if (readError) throw readError;
|
|
724
|
+
const wantedKey = variantOptionsKey(input.options);
|
|
725
|
+
const match = (existing ?? []).find((row) => variantOptionsKey(row.options ?? {}) === wantedKey);
|
|
726
|
+
if (match) return this.updateProductVariant(match.id, { ...input, status: input.status ?? "active" });
|
|
727
|
+
const { data, error } = await db.from(T.productVariants).insert({
|
|
728
|
+
product_id: productId,
|
|
729
|
+
tenant_id: tenantId,
|
|
730
|
+
options: input.options,
|
|
731
|
+
...variantPatch(input),
|
|
732
|
+
// NOT copied from the product: 0028 defines a null price as "inherit the
|
|
733
|
+
// product's", and 0037's header spells out why a copy is a bug — the
|
|
734
|
+
// storefront would show the new product price and the server charge the
|
|
735
|
+
// frozen variant one.
|
|
736
|
+
status: input.status ?? "active"
|
|
737
|
+
}).select("*").single();
|
|
738
|
+
if (error) throw error;
|
|
739
|
+
return rowToVariant(data);
|
|
740
|
+
}
|
|
741
|
+
async updateProductVariant(variantId, input) {
|
|
742
|
+
const db = getDb();
|
|
743
|
+
const { data, error } = await db.from(T.productVariants).update(variantPatch(input)).eq("id", variantId).select("*").single();
|
|
744
|
+
if (error) throw error;
|
|
745
|
+
return rowToVariant(data);
|
|
746
|
+
}
|
|
747
|
+
async archiveProductVariant(variantId) {
|
|
748
|
+
const db = getDb();
|
|
749
|
+
const { error } = await db.from(T.productVariants).update({ status: "archived" }).eq("id", variantId);
|
|
750
|
+
if (error) throw error;
|
|
751
|
+
}
|
|
752
|
+
async setVariantStock(variantId, count, note) {
|
|
753
|
+
const target = Math.max(0, Math.trunc(count));
|
|
754
|
+
const store = resolveStockStore(getTenantId() ?? "", () => createSupabaseStockStore());
|
|
755
|
+
const current = await store.getAvailable(variantId);
|
|
756
|
+
const delta = target - current;
|
|
757
|
+
if (delta === 0) return current;
|
|
758
|
+
const priorMovements = await store.listMovements({ variantId, limit: 1 });
|
|
759
|
+
const opening = priorMovements.length === 0 && delta > 0;
|
|
760
|
+
await store.recordMovement({
|
|
761
|
+
variantId,
|
|
762
|
+
kind: opening ? "receipt" : "adjustment",
|
|
763
|
+
reasonCode: opening ? OPENING_BALANCE_REASON : "correction",
|
|
764
|
+
quantity: delta,
|
|
765
|
+
note: note ?? "Estoque definido na matriz de variantes."
|
|
766
|
+
});
|
|
767
|
+
return target;
|
|
448
768
|
}
|
|
449
769
|
async createProduct(input) {
|
|
450
770
|
const db = getDb();
|
|
@@ -465,7 +785,17 @@ var SupabaseShopProvider = class {
|
|
|
465
785
|
category_id: input.categoryId ?? null,
|
|
466
786
|
is_physical: input.isPhysical ?? true,
|
|
467
787
|
weight: input.weight ?? null,
|
|
468
|
-
weight_unit: input.weightUnit ?? "kg"
|
|
788
|
+
weight_unit: input.weightUnit ?? "kg",
|
|
789
|
+
height_cm: input.heightCm ?? null,
|
|
790
|
+
width_cm: input.widthCm ?? null,
|
|
791
|
+
length_cm: input.lengthCm ?? null,
|
|
792
|
+
// Null = a padrão da loja (0047). É o estado normal, não um campo vazio.
|
|
793
|
+
package_id: input.packageId ?? null,
|
|
794
|
+
// NULL and not []: "nothing declared" and "declared as needing nothing"
|
|
795
|
+
// are the same fact about a box, and one representation of it keeps the
|
|
796
|
+
// `&&` overlap test in the 0048 trigger reading one way.
|
|
797
|
+
handling: input.handling?.length ? input.handling : null,
|
|
798
|
+
shipping_profile_id: input.shippingProfileId ?? null
|
|
469
799
|
}).select(`*, images:${T.productImages}(*)`).single();
|
|
470
800
|
if (error) throw error;
|
|
471
801
|
return rowToProduct(data);
|
|
@@ -488,6 +818,13 @@ var SupabaseShopProvider = class {
|
|
|
488
818
|
if (input.categoryId !== void 0) updates.category_id = input.categoryId;
|
|
489
819
|
if (input.isPhysical !== void 0) updates.is_physical = input.isPhysical;
|
|
490
820
|
if (input.weight !== void 0) updates.weight = input.weight;
|
|
821
|
+
if (input.weightUnit !== void 0) updates.weight_unit = input.weightUnit;
|
|
822
|
+
if (input.heightCm !== void 0) updates.height_cm = input.heightCm;
|
|
823
|
+
if (input.widthCm !== void 0) updates.width_cm = input.widthCm;
|
|
824
|
+
if (input.lengthCm !== void 0) updates.length_cm = input.lengthCm;
|
|
825
|
+
if (input.packageId !== void 0) updates.package_id = input.packageId || null;
|
|
826
|
+
if (input.handling !== void 0) updates.handling = input.handling.length ? input.handling : null;
|
|
827
|
+
if (input.shippingProfileId !== void 0) updates.shipping_profile_id = input.shippingProfileId ?? null;
|
|
491
828
|
const { data, error } = await db.from(T.products).update(updates).eq("id", id).select(`*, images:${T.productImages}(*)`).single();
|
|
492
829
|
if (error) throw error;
|
|
493
830
|
return rowToProduct(data);
|
|
@@ -532,9 +869,43 @@ var SupabaseShopProvider = class {
|
|
|
532
869
|
if (error) throw error;
|
|
533
870
|
return rowToImage(data);
|
|
534
871
|
}
|
|
872
|
+
async replaceProductImage(imageId, file) {
|
|
873
|
+
const db = getDb();
|
|
874
|
+
const tenantId = getTenantId();
|
|
875
|
+
const { data: row, error: readErr } = await db.from(T.productImages).select("id,url,product_id").eq("id", imageId).single();
|
|
876
|
+
if (readErr) throw readErr;
|
|
877
|
+
const path = `${tenantId}/products/${row.product_id}/${Date.now()}-${file.name}`;
|
|
878
|
+
const bucket = db.storage.from(T.mediaBucket);
|
|
879
|
+
const { error: uploadErr } = await bucket.upload(path, file);
|
|
880
|
+
if (uploadErr) throw uploadErr;
|
|
881
|
+
const { data: { publicUrl } } = bucket.getPublicUrl(path);
|
|
882
|
+
const { data, error } = await db.from(T.productImages).update({ url: publicUrl }).eq("id", imageId).select().single();
|
|
883
|
+
if (error) throw error;
|
|
884
|
+
await this.removeUnreferencedObject(row.url);
|
|
885
|
+
return rowToImage(data);
|
|
886
|
+
}
|
|
535
887
|
async deleteProductImage(imageId) {
|
|
536
|
-
const
|
|
888
|
+
const db = getDb();
|
|
889
|
+
const { data: row } = await db.from(T.productImages).select("url").eq("id", imageId).maybeSingle();
|
|
890
|
+
const { error } = await db.from(T.productImages).delete().eq("id", imageId);
|
|
537
891
|
if (error) throw error;
|
|
892
|
+
if (row?.url) await this.removeUnreferencedObject(row.url);
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Drop a stored file once nothing points at it anymore.
|
|
896
|
+
*
|
|
897
|
+
* The row was the only thing keeping the object reachable, so leaving it
|
|
898
|
+
* behind is a file nobody can see and nobody can delete — and the bill for it
|
|
899
|
+
* arrives all the same. Two rows can share one URL (a duplicated product), and
|
|
900
|
+
* the survivor has to keep working, so the count decides.
|
|
901
|
+
*/
|
|
902
|
+
async removeUnreferencedObject(url) {
|
|
903
|
+
const path = url ? storagePathOf(url) : null;
|
|
904
|
+
if (!path) return;
|
|
905
|
+
const db = getDb();
|
|
906
|
+
const { count } = await db.from(T.productImages).select("id", { count: "exact", head: true }).eq("url", url);
|
|
907
|
+
if (count) return;
|
|
908
|
+
await db.storage.from(T.mediaBucket).remove([path]);
|
|
538
909
|
}
|
|
539
910
|
// ---------------------------------------------------------------------------
|
|
540
911
|
// Categories
|
|
@@ -643,10 +1014,188 @@ var SupabaseShopProvider = class {
|
|
|
643
1014
|
if (error) throw error;
|
|
644
1015
|
return rowToShippingZone(data);
|
|
645
1016
|
}
|
|
1017
|
+
// -------------------------------------------------------------------------
|
|
1018
|
+
// Embalagens (0046)
|
|
1019
|
+
//
|
|
1020
|
+
// `is_default` has a partial unique index behind it, so promoting a box has
|
|
1021
|
+
// to demote the previous one in the same round trip — otherwise the second
|
|
1022
|
+
// default is refused by the database and the merchant sees a save that
|
|
1023
|
+
// failed for a reason the screen cannot explain.
|
|
1024
|
+
// -------------------------------------------------------------------------
|
|
1025
|
+
async listShippingPackages() {
|
|
1026
|
+
const tenantId = getTenantId();
|
|
1027
|
+
const { data, error } = await getDb().from(T.shippingPackages).select("*").eq("tenant_id", tenantId).order("is_default", { ascending: false }).order("name", { ascending: true });
|
|
1028
|
+
if (error) throw error;
|
|
1029
|
+
return (data ?? []).map(rowToShippingPackage);
|
|
1030
|
+
}
|
|
1031
|
+
async createShippingPackage(input) {
|
|
1032
|
+
const tenantId = getTenantId();
|
|
1033
|
+
if (!tenantId) throw new Error("@fayz-ai/shop: createShippingPackage requires a tenant id.");
|
|
1034
|
+
if (input.isDefault) await this.clearDefaultPackage(tenantId);
|
|
1035
|
+
const { data, error } = await getDb().from(T.shippingPackages).insert({ tenant_id: tenantId, ...shippingPackagePayload(input) }).select().single();
|
|
1036
|
+
if (error) throw error;
|
|
1037
|
+
return rowToShippingPackage(data);
|
|
1038
|
+
}
|
|
1039
|
+
async updateShippingPackage(id, input) {
|
|
1040
|
+
const tenantId = getTenantId();
|
|
1041
|
+
if (input.isDefault && tenantId) await this.clearDefaultPackage(tenantId, id);
|
|
1042
|
+
const { data, error } = await getDb().from(T.shippingPackages).update(shippingPackagePayload(input)).eq("id", id).select().single();
|
|
1043
|
+
if (error) throw error;
|
|
1044
|
+
return rowToShippingPackage(data);
|
|
1045
|
+
}
|
|
1046
|
+
async deleteShippingPackage(id) {
|
|
1047
|
+
const { error } = await getDb().from(T.shippingPackages).delete().eq("id", id);
|
|
1048
|
+
if (error) throw error;
|
|
1049
|
+
}
|
|
1050
|
+
async clearDefaultPackage(tenantId, exceptId) {
|
|
1051
|
+
let query = getDb().from(T.shippingPackages).update({ is_default: false }).eq("tenant_id", tenantId).eq("is_default", true);
|
|
1052
|
+
if (exceptId) query = query.neq("id", exceptId);
|
|
1053
|
+
const { error } = await query;
|
|
1054
|
+
if (error) throw error;
|
|
1055
|
+
}
|
|
646
1056
|
async deleteShippingZone(id) {
|
|
647
1057
|
const { error } = await getDb().from(T.shippingZones).delete().eq("id", id);
|
|
648
1058
|
if (error) throw error;
|
|
649
1059
|
}
|
|
1060
|
+
// -------------------------------------------------------------------------
|
|
1061
|
+
// Store policy + delivery fallback (0044b / 0015)
|
|
1062
|
+
//
|
|
1063
|
+
// Both are ONE ROW PER TENANT, and both are upserted rather than
|
|
1064
|
+
// insert-or-update: a store that never opened these screens has no row, and
|
|
1065
|
+
// making the first save conditional on discovering that is two round trips to
|
|
1066
|
+
// learn something the primary key already knows.
|
|
1067
|
+
// -------------------------------------------------------------------------
|
|
1068
|
+
async getShippingSettings() {
|
|
1069
|
+
const tenantId = getTenantId();
|
|
1070
|
+
const { data, error } = await getDb().from(T.shippingSettings).select("flat_rate, free_above, currency").eq("tenant_id", tenantId).maybeSingle();
|
|
1071
|
+
if (error) throw error;
|
|
1072
|
+
if (!data) return null;
|
|
1073
|
+
return {
|
|
1074
|
+
flatRate: Number(data.flat_rate ?? 0),
|
|
1075
|
+
freeAbove: data.free_above == null ? null : Number(data.free_above),
|
|
1076
|
+
currency: data.currency ?? "BRL"
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
async updateShippingSettings(input) {
|
|
1080
|
+
const tenantId = getTenantId();
|
|
1081
|
+
if (!tenantId) throw new Error("@fayz-ai/shop: updateShippingSettings requires a tenant id.");
|
|
1082
|
+
const payload = { tenant_id: tenantId };
|
|
1083
|
+
if (input.flatRate !== void 0) payload.flat_rate = input.flatRate;
|
|
1084
|
+
if (input.freeAbove !== void 0) payload.free_above = input.freeAbove;
|
|
1085
|
+
if (input.currency !== void 0) payload.currency = input.currency;
|
|
1086
|
+
const { data, error } = await getDb().from(T.shippingSettings).upsert(payload, { onConflict: "tenant_id" }).select("flat_rate, free_above, currency").single();
|
|
1087
|
+
if (error) throw error;
|
|
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
|
+
/**
|
|
1095
|
+
* "Does this store deliver here?" — asked with the address, before the card.
|
|
1096
|
+
*
|
|
1097
|
+
* Degrades to "yes, and not our own zone" on any failure. A store that cannot
|
|
1098
|
+
* reach the check must not stop selling: the order itself is still refused by
|
|
1099
|
+
* the 0048 trigger if the region really is excluded, so the worst case is the
|
|
1100
|
+
* shopper learning it one screen later instead of never selling at all.
|
|
1101
|
+
*/
|
|
1102
|
+
async listShippingProfiles() {
|
|
1103
|
+
const { data, error } = await getDb().from(T.shippingProfiles).select("*").eq("tenant_id", getTenantId()).order("is_default", { ascending: false }).order("name");
|
|
1104
|
+
if (error) throw error;
|
|
1105
|
+
return (data ?? []).map(rowToShippingProfile);
|
|
1106
|
+
}
|
|
1107
|
+
async createShippingProfile(input) {
|
|
1108
|
+
const tenantId = getTenantId();
|
|
1109
|
+
if (!tenantId) throw new Error("@fayz-ai/shop: createShippingProfile requires a tenant id.");
|
|
1110
|
+
const { data, error } = await getDb().from(T.shippingProfiles).insert({ tenant_id: tenantId, ...shippingProfilePayload(input) }).select("*").single();
|
|
1111
|
+
if (error) throw error;
|
|
1112
|
+
return rowToShippingProfile(data);
|
|
1113
|
+
}
|
|
1114
|
+
async updateShippingProfile(id, input) {
|
|
1115
|
+
const { data, error } = await getDb().from(T.shippingProfiles).update(shippingProfilePayload(input)).eq("id", id).select("*").single();
|
|
1116
|
+
if (error) throw error;
|
|
1117
|
+
return rowToShippingProfile(data);
|
|
1118
|
+
}
|
|
1119
|
+
async deleteShippingProfile(id) {
|
|
1120
|
+
const { error } = await getDb().from(T.shippingProfiles).delete().eq("id", id);
|
|
1121
|
+
if (error) throw error;
|
|
1122
|
+
}
|
|
1123
|
+
// Places the store ships from (0058). Default first, then by name — the same
|
|
1124
|
+
// order the ledger's listLocations() uses, so the two screens agree.
|
|
1125
|
+
async listStockLocations() {
|
|
1126
|
+
const { data, error } = await getDb().from(T.stockLocations).select("*").eq("tenant_id", getTenantId()).order("is_default", { ascending: false }).order("name");
|
|
1127
|
+
if (error) throw error;
|
|
1128
|
+
return (data ?? []).map(rowToStockLocation);
|
|
1129
|
+
}
|
|
1130
|
+
async createStockLocation(input) {
|
|
1131
|
+
const tenantId = getTenantId();
|
|
1132
|
+
if (!tenantId) throw new Error("@fayz-ai/shop: createStockLocation requires a tenant id.");
|
|
1133
|
+
if (input.isDefault) await this.clearDefaultStockLocation(tenantId);
|
|
1134
|
+
const payload = stockLocationPayload(input);
|
|
1135
|
+
if (payload.code === void 0) payload.code = toLocationCode(input.name);
|
|
1136
|
+
const { data, error } = await getDb().from(T.stockLocations).insert({ tenant_id: tenantId, ...payload }).select("*").single();
|
|
1137
|
+
if (error) throw locationWriteError(error);
|
|
1138
|
+
return rowToStockLocation(data);
|
|
1139
|
+
}
|
|
1140
|
+
async updateStockLocation(id, input) {
|
|
1141
|
+
const tenantId = getTenantId();
|
|
1142
|
+
if (input.isDefault && tenantId) await this.clearDefaultStockLocation(tenantId, id);
|
|
1143
|
+
const { data, error } = await getDb().from(T.stockLocations).update(stockLocationPayload(input)).eq("id", id).select("*").single();
|
|
1144
|
+
if (error) throw locationWriteError(error);
|
|
1145
|
+
return rowToStockLocation(data);
|
|
1146
|
+
}
|
|
1147
|
+
async deleteStockLocation(id) {
|
|
1148
|
+
const { error } = await getDb().from(T.stockLocations).delete().eq("id", id);
|
|
1149
|
+
if (error) throw locationWriteError(error);
|
|
1150
|
+
}
|
|
1151
|
+
async clearDefaultStockLocation(tenantId, exceptId) {
|
|
1152
|
+
let query = getDb().from(T.stockLocations).update({ is_default: false }).eq("tenant_id", tenantId).eq("is_default", true);
|
|
1153
|
+
if (exceptId) query = query.neq("id", exceptId);
|
|
1154
|
+
const { error } = await query;
|
|
1155
|
+
if (error) throw error;
|
|
1156
|
+
}
|
|
1157
|
+
async checkDelivery(postalCode) {
|
|
1158
|
+
const tenantId = getTenantId();
|
|
1159
|
+
const digits = String(postalCode ?? "").replace(/\D/g, "");
|
|
1160
|
+
if (!tenantId || digits.length !== 8) return { refused: false, ownZone: false };
|
|
1161
|
+
const { data, error } = await getDb().rpc("shop_delivery_check", {
|
|
1162
|
+
p_tenant_id: tenantId,
|
|
1163
|
+
p_postal_code: digits
|
|
1164
|
+
});
|
|
1165
|
+
if (error) return { refused: false, ownZone: false };
|
|
1166
|
+
const row = Array.isArray(data) ? data[0] : data;
|
|
1167
|
+
return {
|
|
1168
|
+
refused: Boolean(row?.refused),
|
|
1169
|
+
refusedZone: row?.refused_zone ?? null,
|
|
1170
|
+
ownZone: Boolean(row?.own_zone)
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
async getStoreSettings() {
|
|
1174
|
+
const tenantId = getTenantId();
|
|
1175
|
+
const { data, error } = await getDb().from(T.settings).select("*").eq("tenant_id", tenantId).maybeSingle();
|
|
1176
|
+
if (error) throw error;
|
|
1177
|
+
return data ? rowToStoreSettings(data) : null;
|
|
1178
|
+
}
|
|
1179
|
+
async updateStoreSettings(input) {
|
|
1180
|
+
const tenantId = getTenantId();
|
|
1181
|
+
if (!tenantId) throw new Error("@fayz-ai/shop: updateStoreSettings requires a tenant id.");
|
|
1182
|
+
const { data, error } = await getDb().from(T.settings).upsert({ tenant_id: tenantId, ...storeSettingsPayload(input) }, { onConflict: "tenant_id" }).select("*").single();
|
|
1183
|
+
if (error) throw error;
|
|
1184
|
+
return rowToStoreSettings(data);
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* The shopper's read. Through the RPC, not the table: the table is closed to
|
|
1188
|
+
* anon on purpose (0029) and shop_store_settings() is the narrow window —
|
|
1189
|
+
* storefront fields only, and only for a published store.
|
|
1190
|
+
*/
|
|
1191
|
+
async getPublicStoreSettings() {
|
|
1192
|
+
const tenantId = getTenantId();
|
|
1193
|
+
if (!tenantId) return null;
|
|
1194
|
+
const { data, error } = await getDb().rpc("shop_store_settings", { p_tenant_id: tenantId });
|
|
1195
|
+
if (error) throw error;
|
|
1196
|
+
const row = Array.isArray(data) ? data[0] : data;
|
|
1197
|
+
return row ? rowToStoreSettings(row) : null;
|
|
1198
|
+
}
|
|
650
1199
|
/**
|
|
651
1200
|
* Settle a pending order: the merchant confirming that the money arrived.
|
|
652
1201
|
*
|
|
@@ -664,6 +1213,208 @@ var SupabaseShopProvider = class {
|
|
|
664
1213
|
if (error) throw error;
|
|
665
1214
|
return this.getOrder(id);
|
|
666
1215
|
}
|
|
1216
|
+
// ---------------------------------------------------------------------------
|
|
1217
|
+
// Carrier labels. Everything goes through the function: the token never
|
|
1218
|
+
// reaches the browser, and `buy` is the one call that spends money.
|
|
1219
|
+
// ---------------------------------------------------------------------------
|
|
1220
|
+
async labelCall(orderId, action, extra = {}) {
|
|
1221
|
+
const tenantId = getTenantId();
|
|
1222
|
+
if (!tenantId) throw new Error("@fayz-ai/shop: shipping labels require a tenant id.");
|
|
1223
|
+
const { ok, payload } = await callShopFunction(getDb(), "shop-shipping-label", {
|
|
1224
|
+
tenantId,
|
|
1225
|
+
orderId,
|
|
1226
|
+
action,
|
|
1227
|
+
...extra
|
|
1228
|
+
});
|
|
1229
|
+
if (!ok) throw new Error(payload?.error ?? "A transportadora n\xE3o respondeu agora.");
|
|
1230
|
+
return payload;
|
|
1231
|
+
}
|
|
1232
|
+
async quoteShippingLabel(orderId, carrier) {
|
|
1233
|
+
const payload = await this.labelCall(orderId, "quote", carrier ? { carrier } : {});
|
|
1234
|
+
return {
|
|
1235
|
+
options: (payload?.options ?? []).map((option) => ({
|
|
1236
|
+
serviceId: Number(option.service_id ?? option.serviceId),
|
|
1237
|
+
name: String(option.name ?? "Entrega"),
|
|
1238
|
+
carrier: String(option.carrier ?? ""),
|
|
1239
|
+
price: Number(option.price ?? 0),
|
|
1240
|
+
etaMinDays: option.eta_min_days ?? null,
|
|
1241
|
+
etaMaxDays: option.eta_max_days ?? null
|
|
1242
|
+
})),
|
|
1243
|
+
label: payload?.label ? {
|
|
1244
|
+
labelId: String(payload.label.external_id),
|
|
1245
|
+
labelUrl: payload.label.url ?? null,
|
|
1246
|
+
status: payload.label.status ?? null,
|
|
1247
|
+
tracking: payload.label.payload?.tracking ?? null
|
|
1248
|
+
} : null
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
/** Spends the merchant's balance at the carrier. Never call it on a whim. */
|
|
1252
|
+
async buyShippingLabel(orderId, serviceId, carrier) {
|
|
1253
|
+
const payload = await this.labelCall(orderId, "buy", { serviceId, ...carrier ? { carrier } : {} });
|
|
1254
|
+
return {
|
|
1255
|
+
labelId: String(payload?.labelId ?? ""),
|
|
1256
|
+
labelUrl: payload?.labelUrl ?? null,
|
|
1257
|
+
status: "bought",
|
|
1258
|
+
tracking: payload?.tracking ?? null
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
async printShippingLabel(orderId, carrier) {
|
|
1262
|
+
const payload = await this.labelCall(orderId, "print", carrier ? { carrier } : {});
|
|
1263
|
+
return payload?.labelUrl ?? null;
|
|
1264
|
+
}
|
|
1265
|
+
/** Drop-off points near a place. Store configuration — no order involved. */
|
|
1266
|
+
async listShippingAgencies(query) {
|
|
1267
|
+
const tenantId = getTenantId();
|
|
1268
|
+
if (!tenantId) return [];
|
|
1269
|
+
const { ok, payload } = await callShopFunction(getDb(), "shop-shipping-label", {
|
|
1270
|
+
tenantId,
|
|
1271
|
+
action: "agencies",
|
|
1272
|
+
state: query.state ?? "",
|
|
1273
|
+
city: query.city ?? "",
|
|
1274
|
+
...query.carrier ? { carrier: query.carrier } : {}
|
|
1275
|
+
});
|
|
1276
|
+
if (!ok) throw new Error(payload?.error ?? "A transportadora n\xE3o respondeu agora.");
|
|
1277
|
+
return payload?.agencies ?? [];
|
|
1278
|
+
}
|
|
1279
|
+
async trackShippingLabel(orderId, carrier) {
|
|
1280
|
+
const payload = await this.labelCall(orderId, "track", carrier ? { carrier } : {});
|
|
1281
|
+
return payload?.tracking ?? null;
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* The customer's address book, for the console. `shop_place_order` files a
|
|
1285
|
+
* new one here on every delivered order, and until now only the storefront
|
|
1286
|
+
* read them back — so a repeat customer had their address retyped by hand.
|
|
1287
|
+
*/
|
|
1288
|
+
async listCustomerAddresses(customerId) {
|
|
1289
|
+
if (!asUuid(customerId)) return [];
|
|
1290
|
+
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 });
|
|
1291
|
+
if (error) return [];
|
|
1292
|
+
return (data ?? []).map((row) => ({
|
|
1293
|
+
id: row.id,
|
|
1294
|
+
label: row.label ?? void 0,
|
|
1295
|
+
recipient: row.recipient ?? void 0,
|
|
1296
|
+
phone: row.phone ?? void 0,
|
|
1297
|
+
postalCode: row.postal_code ?? "",
|
|
1298
|
+
street: row.street ?? "",
|
|
1299
|
+
number: row.number ?? void 0,
|
|
1300
|
+
complement: row.complement ?? void 0,
|
|
1301
|
+
district: row.district ?? void 0,
|
|
1302
|
+
city: row.city ?? "",
|
|
1303
|
+
state: row.state ?? "",
|
|
1304
|
+
country: row.country ?? void 0,
|
|
1305
|
+
isDefault: row.is_default ?? false
|
|
1306
|
+
}));
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* What delivery costs to a postcode — the same answer the shopper gets.
|
|
1310
|
+
*
|
|
1311
|
+
* The console needs it to quote a price out loud while the customer is on the
|
|
1312
|
+
* line. Carrier first (when the merchant did not turn gateway quotes off),
|
|
1313
|
+
* zones as the fallback, exactly as the storefront resolves it. Advisory
|
|
1314
|
+
* either way: shop_place_order requotes and ignores whatever a client sends.
|
|
1315
|
+
*/
|
|
1316
|
+
async quoteShipping(postalCode, subtotal) {
|
|
1317
|
+
const digits = toPostalDigits(postalCode);
|
|
1318
|
+
if (!digits) return [];
|
|
1319
|
+
const tenantId = getTenantId();
|
|
1320
|
+
if (!tenantId) return [];
|
|
1321
|
+
const settings = await this.getStoreSettings().catch(() => null);
|
|
1322
|
+
if (settings?.carrierQuotes !== false) {
|
|
1323
|
+
try {
|
|
1324
|
+
const { ok, payload } = await callShopFunction(getDb(), "shop-shipping-quote", {
|
|
1325
|
+
tenantId,
|
|
1326
|
+
postalCode: digits,
|
|
1327
|
+
subtotal
|
|
1328
|
+
});
|
|
1329
|
+
const rows = ok ? payload?.options ?? [] : [];
|
|
1330
|
+
if (rows.length > 0) {
|
|
1331
|
+
return rows.map((row) => ({
|
|
1332
|
+
zoneId: String(row.service_id ?? ""),
|
|
1333
|
+
name: String(row.name ?? "Entrega"),
|
|
1334
|
+
carrier: row.carrier ?? void 0,
|
|
1335
|
+
rate: Number(row.price ?? 0),
|
|
1336
|
+
baseRate: Number(row.price ?? 0),
|
|
1337
|
+
freeAbove: null,
|
|
1338
|
+
etaMinDays: row.eta_min_days ?? void 0,
|
|
1339
|
+
etaMaxDays: row.eta_max_days ?? void 0,
|
|
1340
|
+
free: Number(row.price ?? 0) === 0
|
|
1341
|
+
}));
|
|
1342
|
+
}
|
|
1343
|
+
} catch {
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
const { data, error } = await getDb().rpc("shop_quote_shipping", {
|
|
1347
|
+
p_tenant_id: tenantId,
|
|
1348
|
+
p_postal_code: digits,
|
|
1349
|
+
p_subtotal: subtotal
|
|
1350
|
+
});
|
|
1351
|
+
if (error) return [];
|
|
1352
|
+
return (data ?? []).map((row) => ({
|
|
1353
|
+
zoneId: row.zone_id,
|
|
1354
|
+
name: row.name,
|
|
1355
|
+
carrier: row.carrier ?? void 0,
|
|
1356
|
+
rate: Number(row.rate ?? 0),
|
|
1357
|
+
baseRate: Number(row.base_rate ?? row.rate ?? 0),
|
|
1358
|
+
freeAbove: row.free_above == null ? null : Number(row.free_above),
|
|
1359
|
+
etaMinDays: row.eta_min_days ?? void 0,
|
|
1360
|
+
etaMaxDays: row.eta_max_days ?? void 0,
|
|
1361
|
+
free: Boolean(row.free)
|
|
1362
|
+
}));
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Charge a cart that is not an order yet — the phone sale, from the counter's
|
|
1366
|
+
* side. Same door the storefront uses, on purpose: `shop_price_cart` inside
|
|
1367
|
+
* the function reprices everything, so the console sends products and
|
|
1368
|
+
* quantities and never money. The order is born in `settleCheckoutSession`,
|
|
1369
|
+
* once the acquirer says the Pix landed, so an abandoned charge leaves no
|
|
1370
|
+
* ghost order holding stock.
|
|
1371
|
+
*/
|
|
1372
|
+
async openCheckoutSession(input) {
|
|
1373
|
+
const tenantId = getTenantId();
|
|
1374
|
+
if (!tenantId) throw new Error("@fayz-ai/shop: openCheckoutSession requires a tenant id.");
|
|
1375
|
+
const { ok, status, payload } = await callShopFunction(getDb(), "shop-checkout-open", {
|
|
1376
|
+
tenantId,
|
|
1377
|
+
items: input.items.map((item) => ({
|
|
1378
|
+
productId: item.productId,
|
|
1379
|
+
optionsLabel: item.optionsLabel ?? null,
|
|
1380
|
+
quantity: item.quantity
|
|
1381
|
+
})),
|
|
1382
|
+
customer: input.customer,
|
|
1383
|
+
shippingAddress: input.shippingAddress ?? null,
|
|
1384
|
+
discountCode: input.discountCode ?? null
|
|
1385
|
+
});
|
|
1386
|
+
if (!ok || !payload?.sessionId) {
|
|
1387
|
+
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."));
|
|
1388
|
+
}
|
|
1389
|
+
return {
|
|
1390
|
+
sessionId: payload.sessionId,
|
|
1391
|
+
total: Number(payload.total ?? 0),
|
|
1392
|
+
state: "open",
|
|
1393
|
+
charge: payload.brCode ? {
|
|
1394
|
+
emv: payload.brCode,
|
|
1395
|
+
// AbacatePay embeds the `data:` prefix; the type promises bare
|
|
1396
|
+
// base64. Left in, the view prefixes it again and the QR is a
|
|
1397
|
+
// silently broken <img>.
|
|
1398
|
+
qrcodeBase64: String(payload.brCodeBase64 ?? "").replace(/^data:image\/\w+;base64,/u, "") || null,
|
|
1399
|
+
expiresAt: payload.expiresAt ?? ""
|
|
1400
|
+
} : null
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Ask whether the money landed. Runs on a timer, so a network blip must read
|
|
1405
|
+
* as "still waiting" (`unknown`) and never as a refused payment.
|
|
1406
|
+
*/
|
|
1407
|
+
async settleCheckoutSession(sessionId) {
|
|
1408
|
+
const tenantId = getTenantId();
|
|
1409
|
+
if (!tenantId) return { state: "unknown" };
|
|
1410
|
+
try {
|
|
1411
|
+
const { ok, payload } = await callShopFunction(getDb(), "shop-checkout-settle", { tenantId, sessionId });
|
|
1412
|
+
if (!ok || !payload?.state) return { state: "unknown" };
|
|
1413
|
+
return { state: payload.state, orderId: payload.orderId ?? null, reason: payload.reason ?? null };
|
|
1414
|
+
} catch {
|
|
1415
|
+
return { state: "unknown" };
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
667
1418
|
// Trusted placement — delegates all price/discount/inventory authority to the
|
|
668
1419
|
// shop_place_order RPC (SECURITY DEFINER). The browser never sets a price.
|
|
669
1420
|
async placeOrder(input) {
|
|
@@ -700,7 +1451,12 @@ var SupabaseShopProvider = class {
|
|
|
700
1451
|
country: input.shippingAddress.country ?? "BR",
|
|
701
1452
|
recipient: input.shippingAddress.recipient ?? null,
|
|
702
1453
|
phone: input.shippingAddress.phone ?? null,
|
|
703
|
-
label: input.shippingAddress.label ?? null
|
|
1454
|
+
label: input.shippingAddress.label ?? null,
|
|
1455
|
+
// Not an address field: it rides here because shop_place_order's
|
|
1456
|
+
// signature cannot grow without a DROP, and 0050's trigger lifts it to
|
|
1457
|
+
// plg_shop_orders.customer_document. The storefront already sent it;
|
|
1458
|
+
// the console did not, so a phone sale was born with no CPF.
|
|
1459
|
+
document: input.shippingAddress.document ?? null
|
|
704
1460
|
} : null,
|
|
705
1461
|
p_payment_method: input.paymentMethod ?? null,
|
|
706
1462
|
p_notes: input.notes ?? null,
|
|
@@ -723,7 +1479,7 @@ var SupabaseShopProvider = class {
|
|
|
723
1479
|
const tenantId = getTenantId();
|
|
724
1480
|
let q = db.from(T.customers).select("*").order("first_name", { ascending: true });
|
|
725
1481
|
if (tenantId) q = q.eq("tenant_id", tenantId);
|
|
726
|
-
|
|
1482
|
+
for (const filter of customerSearchFilters(options?.search)) q = q.or(filter);
|
|
727
1483
|
if (options?.limit) q = q.limit(options.limit);
|
|
728
1484
|
const { data, error } = await q;
|
|
729
1485
|
if (error) throw error;
|
|
@@ -735,7 +1491,15 @@ var SupabaseShopProvider = class {
|
|
|
735
1491
|
return data ? rowToCustomer(data) : null;
|
|
736
1492
|
}
|
|
737
1493
|
async createCustomer(input) {
|
|
738
|
-
const { data, error } = await getDb().from(T.customers).insert({
|
|
1494
|
+
const { data, error } = await getDb().from(T.customers).insert({
|
|
1495
|
+
tenant_id: getTenantId(),
|
|
1496
|
+
first_name: input.firstName,
|
|
1497
|
+
last_name: input.lastName ?? "",
|
|
1498
|
+
email: input.email ?? null,
|
|
1499
|
+
phone: input.phone ?? null,
|
|
1500
|
+
document: input.document ?? null,
|
|
1501
|
+
notes: input.notes ?? null
|
|
1502
|
+
}).select().single();
|
|
739
1503
|
if (error) throw error;
|
|
740
1504
|
return rowToCustomer(data);
|
|
741
1505
|
}
|
|
@@ -745,6 +1509,7 @@ var SupabaseShopProvider = class {
|
|
|
745
1509
|
if (input.lastName !== void 0) updates.last_name = input.lastName;
|
|
746
1510
|
if (input.email !== void 0) updates.email = input.email;
|
|
747
1511
|
if (input.phone !== void 0) updates.phone = input.phone;
|
|
1512
|
+
if (input.document !== void 0) updates.document = input.document;
|
|
748
1513
|
if (input.notes !== void 0) updates.notes = input.notes;
|
|
749
1514
|
const { data, error } = await getDb().from(T.customers).update(updates).eq("id", id).select().single();
|
|
750
1515
|
if (error) throw error;
|
|
@@ -920,22 +1685,6 @@ var SupabaseShopProvider = class {
|
|
|
920
1685
|
async deleteEntityRecord(entity, id) {
|
|
921
1686
|
return this.entityStore(entity).delete(id);
|
|
922
1687
|
}
|
|
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
1688
|
// -------------------------------------------------------------------------
|
|
940
1689
|
// Coleções, grupos/segmentos, vale-presente, e-mails e mídia.
|
|
941
1690
|
// -------------------------------------------------------------------------
|
|
@@ -1130,10 +1879,27 @@ var SupabaseShopProvider = class {
|
|
|
1130
1879
|
return rowToEmailTemplate(data);
|
|
1131
1880
|
}
|
|
1132
1881
|
async listMediaFiles() {
|
|
1133
|
-
const db = getDb();
|
|
1134
1882
|
const tenantId = getTenantId();
|
|
1135
1883
|
if (!tenantId) return [];
|
|
1136
|
-
const
|
|
1884
|
+
const [attached, library] = await Promise.all([
|
|
1885
|
+
this.listAttachedMedia(tenantId),
|
|
1886
|
+
this.listLibraryFiles(tenantId)
|
|
1887
|
+
]);
|
|
1888
|
+
return mergeMediaFiles(attached, library);
|
|
1889
|
+
}
|
|
1890
|
+
/** Every product photo in the store, each one carrying its product. */
|
|
1891
|
+
async listAttachedMedia(tenantId) {
|
|
1892
|
+
const db = getDb();
|
|
1893
|
+
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);
|
|
1894
|
+
if (error || !rows?.length) return [];
|
|
1895
|
+
const productIds = [...new Set(rows.map((r) => r.product_id).filter(Boolean))];
|
|
1896
|
+
const { data: products } = await db.from(T.products).select("id,name").in("id", productIds);
|
|
1897
|
+
const nameById = new Map((products ?? []).map((p) => [p.id, p.name]));
|
|
1898
|
+
return rows.map((row) => productImageToMediaFile(rowToImage(row), nameById.get(row.product_id)));
|
|
1899
|
+
}
|
|
1900
|
+
/** What the merchant uploaded straight to the library — owned by nothing. */
|
|
1901
|
+
async listLibraryFiles(tenantId) {
|
|
1902
|
+
const bucket = getDb().storage.from(T.mediaBucket);
|
|
1137
1903
|
const listAt = async (prefix) => {
|
|
1138
1904
|
const { data, error } = await bucket.list(prefix, {
|
|
1139
1905
|
limit: 1e3,
|
|
@@ -1143,14 +1909,15 @@ var SupabaseShopProvider = class {
|
|
|
1143
1909
|
return (data ?? []).map((entry) => ({ prefix, entry }));
|
|
1144
1910
|
};
|
|
1145
1911
|
const top = await listAt(tenantId);
|
|
1146
|
-
const folders = top.filter(({ entry }) => !entry.id).map(({ entry }) => `${tenantId}/${entry.name}`);
|
|
1912
|
+
const folders = top.filter(({ entry }) => !entry.id && entry.name === LIBRARY_FOLDER).map(({ entry }) => `${tenantId}/${entry.name}`);
|
|
1147
1913
|
const nested = (await Promise.all(folders.map(listAt))).flat();
|
|
1148
1914
|
return [...top, ...nested].filter(({ entry }) => entry.id).map(({ prefix, entry }) => {
|
|
1149
1915
|
const path = `${prefix}/${entry.name}`;
|
|
1150
1916
|
const { data: { publicUrl } } = bucket.getPublicUrl(path);
|
|
1917
|
+
const name = displayNameOf(entry.name);
|
|
1151
1918
|
const file = {
|
|
1152
1919
|
id: path,
|
|
1153
|
-
name
|
|
1920
|
+
name,
|
|
1154
1921
|
url: publicUrl,
|
|
1155
1922
|
kind: mediaKindOf(entry.name, entry.metadata?.mimetype),
|
|
1156
1923
|
size: Number(entry.metadata?.size ?? 0),
|
|
@@ -1158,13 +1925,13 @@ var SupabaseShopProvider = class {
|
|
|
1158
1925
|
createdAt: entry.created_at ?? entry.updated_at ?? ""
|
|
1159
1926
|
};
|
|
1160
1927
|
return file;
|
|
1161
|
-
})
|
|
1928
|
+
});
|
|
1162
1929
|
}
|
|
1163
1930
|
async uploadMediaFile(file) {
|
|
1164
1931
|
const db = getDb();
|
|
1165
1932
|
const tenantId = getTenantId();
|
|
1166
1933
|
if (!tenantId) throw new Error("@fayz-ai/shop: no active tenant for media upload");
|
|
1167
|
-
const path = `${tenantId}
|
|
1934
|
+
const path = `${tenantId}/${LIBRARY_FOLDER}/${Date.now()}-${file.name}`;
|
|
1168
1935
|
const bucket = db.storage.from(T.mediaBucket);
|
|
1169
1936
|
const { error } = await bucket.upload(path, file);
|
|
1170
1937
|
if (error) throw error;
|
|
@@ -1217,6 +1984,70 @@ function createSupabaseShopProvider() {
|
|
|
1217
1984
|
return new SupabaseShopProvider();
|
|
1218
1985
|
}
|
|
1219
1986
|
|
|
1987
|
+
// src/shipping/label-provider.ts
|
|
1988
|
+
var registry = /* @__PURE__ */ new Map();
|
|
1989
|
+
function registerShippingLabelProvider(provider) {
|
|
1990
|
+
registry.set(provider.id, provider);
|
|
1991
|
+
}
|
|
1992
|
+
function listShippingLabelProviders() {
|
|
1993
|
+
return [...registry.values()];
|
|
1994
|
+
}
|
|
1995
|
+
function resetShippingLabelProviders() {
|
|
1996
|
+
registry.clear();
|
|
1997
|
+
}
|
|
1998
|
+
function resolveShippingLabelProvider(options) {
|
|
1999
|
+
const { connected, preferred } = options;
|
|
2000
|
+
if (preferred === "") return null;
|
|
2001
|
+
if (preferred) {
|
|
2002
|
+
const chosen = registry.get(preferred);
|
|
2003
|
+
return chosen && connected.includes(preferred) ? chosen : null;
|
|
2004
|
+
}
|
|
2005
|
+
for (const id of connected) {
|
|
2006
|
+
const found = registry.get(id);
|
|
2007
|
+
if (found) return found;
|
|
2008
|
+
}
|
|
2009
|
+
return null;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
// src/lib/document.ts
|
|
2013
|
+
var documentDigits = (value) => String(value ?? "").replace(/\D/g, "");
|
|
2014
|
+
function checkDigit(digits, weights) {
|
|
2015
|
+
const sum = weights.reduce((acc, weight, index) => acc + Number(digits[index]) * weight, 0);
|
|
2016
|
+
const rest = sum % 11;
|
|
2017
|
+
return rest < 2 ? 0 : 11 - rest;
|
|
2018
|
+
}
|
|
2019
|
+
var CPF_FIRST = [10, 9, 8, 7, 6, 5, 4, 3, 2];
|
|
2020
|
+
var CPF_SECOND = [11, 10, 9, 8, 7, 6, 5, 4, 3, 2];
|
|
2021
|
+
var CNPJ_FIRST = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
|
|
2022
|
+
var CNPJ_SECOND = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
|
|
2023
|
+
function isValidCpf(value) {
|
|
2024
|
+
const digits = documentDigits(value);
|
|
2025
|
+
if (digits.length !== 11 || /^(\d)\1{10}$/.test(digits)) return false;
|
|
2026
|
+
return checkDigit(digits, CPF_FIRST) === Number(digits[9]) && checkDigit(digits, CPF_SECOND) === Number(digits[10]);
|
|
2027
|
+
}
|
|
2028
|
+
function isValidCnpj(value) {
|
|
2029
|
+
const digits = documentDigits(value);
|
|
2030
|
+
if (digits.length !== 14 || /^(\d)\1{13}$/.test(digits)) return false;
|
|
2031
|
+
return checkDigit(digits, CNPJ_FIRST) === Number(digits[12]) && checkDigit(digits, CNPJ_SECOND) === Number(digits[13]);
|
|
2032
|
+
}
|
|
2033
|
+
function documentKind(value) {
|
|
2034
|
+
const length = documentDigits(value).length;
|
|
2035
|
+
if (length === 11) return "cpf";
|
|
2036
|
+
if (length === 14) return "cnpj";
|
|
2037
|
+
return null;
|
|
2038
|
+
}
|
|
2039
|
+
function isValidDocument(value) {
|
|
2040
|
+
const kind = documentKind(value);
|
|
2041
|
+
return kind === "cpf" ? isValidCpf(value) : kind === "cnpj" ? isValidCnpj(value) : false;
|
|
2042
|
+
}
|
|
2043
|
+
function formatDocument(value) {
|
|
2044
|
+
const digits = documentDigits(value).slice(0, 14);
|
|
2045
|
+
if (digits.length <= 11) {
|
|
2046
|
+
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");
|
|
2047
|
+
}
|
|
2048
|
+
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");
|
|
2049
|
+
}
|
|
2050
|
+
|
|
1220
2051
|
// src/lib/emails.ts
|
|
1221
2052
|
var EMAIL_PLACEHOLDERS = [
|
|
1222
2053
|
"{{store_name}}",
|
|
@@ -1406,6 +2237,6 @@ function buildShopMessageDefaults() {
|
|
|
1406
2237
|
});
|
|
1407
2238
|
}
|
|
1408
2239
|
|
|
1409
|
-
export { EMAIL_PLACEHOLDERS, EMAIL_PREVIEW_SAMPLE, SHOP_EMAIL_EVENTS, SupabaseShopProvider, applyDiscount, buildShopDomainEvents, buildShopMessageDefaults, checkInventory, computeCartTotals, createSupabaseShopProvider, getCustomerOrders, renderEmailTemplate, resolveEmailTemplates, validateDiscount };
|
|
2240
|
+
export { EMAIL_PLACEHOLDERS, EMAIL_PREVIEW_SAMPLE, SHOP_EMAIL_EVENTS, SupabaseShopProvider, applyDiscount, buildShopDomainEvents, buildShopMessageDefaults, checkInventory, computeCartTotals, createSupabaseShopProvider, documentDigits, documentKind, formatDocument, getCustomerOrders, getShopFunctionsUrl, isValidCnpj, isValidCpf, isValidDocument, listShippingLabelProviders, registerShippingLabelProvider, renderEmailTemplate, resetShippingLabelProviders, resolveEmailTemplates, resolveShippingLabelProvider, setShopFunctionsUrl, validateDiscount };
|
|
1410
2241
|
//# sourceMappingURL=index.js.map
|
|
1411
2242
|
//# sourceMappingURL=index.js.map
|