@fayz-ai/shop 0.9.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/README.md +18 -1
- package/dist/admin.d.ts +2 -0
- package/dist/admin.d.ts.map +1 -0
- package/dist/catalog.d.ts +1 -3
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +2 -2
- package/dist/{chunk-IYQHKMLB.js → chunk-2WACO2TG.js} +4 -4
- package/dist/chunk-2WACO2TG.js.map +1 -0
- package/dist/chunk-3GERSY5F.js +526 -0
- package/dist/chunk-3GERSY5F.js.map +1 -0
- package/dist/chunk-6BO2TMZ5.js +93 -0
- package/dist/chunk-6BO2TMZ5.js.map +1 -0
- package/dist/chunk-FQPGE3YN.js +3 -0
- package/dist/{chunk-JR25TISO.js.map → chunk-FQPGE3YN.js.map} +1 -1
- package/dist/{chunk-CTKZ6O2J.js → chunk-IKNGCRUM.js} +41 -15
- package/dist/chunk-IKNGCRUM.js.map +1 -0
- package/dist/chunk-KLDS7IPU.js +17 -0
- package/dist/chunk-KLDS7IPU.js.map +1 -0
- package/dist/chunk-NWW3WJ7Y.js +136 -0
- package/dist/chunk-NWW3WJ7Y.js.map +1 -0
- package/dist/chunk-STRNMFC4.js +1438 -0
- package/dist/chunk-STRNMFC4.js.map +1 -0
- package/dist/chunk-U6HV3LVP.js +1170 -0
- package/dist/chunk-U6HV3LVP.js.map +1 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.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/{mock-catalog.d.ts → data/mock-catalog.d.ts} +21 -1
- package/dist/data/mock-catalog.d.ts.map +1 -0
- package/dist/data/mock-provider.d.ts +165 -0
- package/dist/data/mock-provider.d.ts.map +1 -0
- package/dist/data/mock.d.ts +3 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/provider.d.ts +335 -0
- package/dist/data/provider.d.ts.map +1 -0
- package/dist/data/supabase-provider.d.ts +208 -0
- package/dist/data/supabase-provider.d.ts.map +1 -0
- package/dist/data/tables.d.ts +38 -0
- package/dist/data/tables.d.ts.map +1 -0
- package/dist/data/tenant.d.ts.map +1 -0
- package/dist/entities/entity-reconcile.d.ts +32 -0
- package/dist/entities/entity-reconcile.d.ts.map +1 -0
- package/dist/entities/entity-sql.d.ts +50 -0
- package/dist/entities/entity-sql.d.ts.map +1 -0
- package/dist/entities/entity-store.d.ts +58 -0
- package/dist/entities/entity-store.d.ts.map +1 -0
- package/dist/entities/index.d.ts +10 -0
- package/dist/entities/index.d.ts.map +1 -0
- package/dist/entities/mock-entity-store.d.ts +17 -0
- package/dist/entities/mock-entity-store.d.ts.map +1 -0
- package/dist/entities/supabase-entity-store.d.ts +8 -0
- package/dist/entities/supabase-entity-store.d.ts.map +1 -0
- package/dist/entities.js +159 -0
- package/dist/entities.js.map +1 -0
- package/dist/index.d.ts +28 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1558 -40
- package/dist/index.js.map +1 -1
- package/dist/inventory/allocation.d.ts +98 -0
- package/dist/inventory/allocation.d.ts.map +1 -0
- package/dist/inventory/index.d.ts +13 -0
- package/dist/inventory/index.d.ts.map +1 -0
- package/dist/inventory/mock-stock-store.d.ts +89 -0
- package/dist/inventory/mock-stock-store.d.ts.map +1 -0
- package/dist/inventory/postgrest-double.d.ts +65 -0
- package/dist/inventory/postgrest-double.d.ts.map +1 -0
- package/dist/inventory/shopify-mapping.d.ts +67 -0
- package/dist/inventory/shopify-mapping.d.ts.map +1 -0
- package/dist/inventory/stock-contract.d.ts +46 -0
- package/dist/inventory/stock-contract.d.ts.map +1 -0
- package/dist/inventory/stock-store.d.ts +96 -0
- package/dist/inventory/stock-store.d.ts.map +1 -0
- package/dist/inventory/stock-types.d.ts +351 -0
- package/dist/inventory/stock-types.d.ts.map +1 -0
- package/dist/inventory/supabase-stock-store.d.ts +60 -0
- package/dist/inventory/supabase-stock-store.d.ts.map +1 -0
- package/dist/inventory/vocabulary.d.ts +86 -0
- package/dist/inventory/vocabulary.d.ts.map +1 -0
- package/dist/inventory.js +4 -0
- package/dist/inventory.js.map +1 -0
- package/dist/lib/catalog.d.ts +4 -0
- package/dist/lib/catalog.d.ts.map +1 -0
- package/dist/{commerce.d.ts → lib/commerce.d.ts} +2 -2
- package/dist/lib/commerce.d.ts.map +1 -0
- package/dist/lib/document.d.ts +13 -0
- package/dist/lib/document.d.ts.map +1 -0
- package/dist/lib/emails.d.ts +50 -0
- package/dist/lib/emails.d.ts.map +1 -0
- package/dist/lib/message-events.d.ts +4 -0
- package/dist/lib/message-events.d.ts.map +1 -0
- package/dist/lib/runtime.d.ts +5 -0
- package/dist/lib/runtime.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.d.ts +1 -2
- package/dist/mock.d.ts.map +1 -1
- package/dist/mock.js +4 -2
- package/dist/provider.d.ts +1 -88
- package/dist/provider.d.ts.map +1 -1
- 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/rules/adapter.d.ts +36 -0
- package/dist/rules/adapter.d.ts.map +1 -0
- package/dist/rules/attributes.d.ts +64 -0
- package/dist/rules/attributes.d.ts.map +1 -0
- package/dist/rules/context.d.ts +43 -0
- package/dist/rules/context.d.ts.map +1 -0
- package/dist/rules/evaluator.d.ts +37 -0
- package/dist/rules/evaluator.d.ts.map +1 -0
- package/dist/rules/index.d.ts +13 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/prices.d.ts +25 -0
- package/dist/rules/prices.d.ts.map +1 -0
- package/dist/rules/rule-types.d.ts +160 -0
- package/dist/rules/rule-types.d.ts.map +1 -0
- package/dist/rules.js +22 -0
- package/dist/rules.js.map +1 -0
- package/dist/runtime.d.ts +1 -4
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +5 -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 +688 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +30 -20
- package/dist/chunk-CTKZ6O2J.js.map +0 -1
- package/dist/chunk-IYQHKMLB.js.map +0 -1
- package/dist/chunk-JR25TISO.js +0 -3
- package/dist/chunk-SVTCJ6HP.js +0 -440
- package/dist/chunk-SVTCJ6HP.js.map +0 -1
- package/dist/commerce.d.ts.map +0 -1
- package/dist/mock-catalog.d.ts.map +0 -1
- package/dist/mock-provider.d.ts +0 -60
- package/dist/mock-provider.d.ts.map +0 -1
- package/dist/supabase-provider.d.ts +0 -56
- package/dist/supabase-provider.d.ts.map +0 -1
- package/dist/tables.d.ts +0 -12
- package/dist/tables.d.ts.map +0 -1
- package/dist/tenant.d.ts.map +0 -1
- package/src/catalog.ts +0 -3
- package/src/commerce.ts +0 -120
- package/src/index.ts +0 -40
- package/src/mock-catalog.ts +0 -266
- package/src/mock-provider.ts +0 -480
- package/src/mock.ts +0 -2
- package/src/provider.ts +0 -117
- package/src/runtime.ts +0 -20
- package/src/supabase-provider.ts +0 -754
- package/src/tables.ts +0 -17
- package/src/tenant.ts +0 -21
- package/src/types.ts +0 -623
- /package/dist/{tenant.d.ts → data/tenant.d.ts} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
3
|
-
import './chunk-
|
|
4
|
-
export {
|
|
5
|
-
|
|
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
|
+
import './chunk-FQPGE3YN.js';
|
|
6
|
+
import { normalizeVariantAxes, validateVariantAxes, variantOptionsKey, storagePathOf, mergeMediaFiles, productImageToMediaFile, displayNameOf, mediaKindOf } from './chunk-U6HV3LVP.js';
|
|
7
|
+
export { MAX_VARIANT_AXES, MAX_VARIANT_COMBINATIONS, MockShopProvider, applyVariantMatrix, axesFromOptions, createMockShopProvider, displayNameOf, formatVariantCombination, hasOnlyDefaultVariant, leavesNothingSellable, mediaKindOf, mergeMediaFiles, normalizeVariantAxes, planVariantMatrix, productImageToMediaFile, storagePathOf, validateVariantAxes, variantCombinations, variantOptionsKey } from './chunk-U6HV3LVP.js';
|
|
8
|
+
export { MOCK_CATEGORIES, MOCK_DISCOUNTS, MOCK_PRODUCTS, buildMockCatalog, svgPlaceholder } from './chunk-IKNGCRUM.js';
|
|
9
|
+
import { createSupabaseEntityStore } from './chunk-6BO2TMZ5.js';
|
|
10
|
+
import { getShopTenantId } from './chunk-KLDS7IPU.js';
|
|
11
|
+
export { getShopTenantId, setShopTenantResolver } from './chunk-KLDS7IPU.js';
|
|
12
|
+
import { resolveEntityStore } from './chunk-NWW3WJ7Y.js';
|
|
13
|
+
import './chunk-3GERSY5F.js';
|
|
6
14
|
import { getSupabaseClientOptional } from '@fayz-ai/core';
|
|
7
15
|
|
|
8
|
-
// src/commerce.ts
|
|
16
|
+
// src/lib/commerce.ts
|
|
9
17
|
function round2(n) {
|
|
10
18
|
return Math.round(n * 100) / 100;
|
|
11
19
|
}
|
|
12
20
|
async function validateDiscount(input, provider = getShopProvider()) {
|
|
13
21
|
const code = input.code.trim();
|
|
14
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
|
+
}
|
|
15
26
|
const list = await provider.listDiscounts({ status: "active" });
|
|
16
27
|
const discount = list.find((d) => (d.code ?? "").toUpperCase() === code.toUpperCase());
|
|
17
28
|
if (!discount) return { valid: false, code: null, type: null, value: 0, reason: "not_found" };
|
|
@@ -65,7 +76,7 @@ async function computeCartTotals(input, provider = getShopProvider()) {
|
|
|
65
76
|
let discountCode = null;
|
|
66
77
|
if (input.discountCode) {
|
|
67
78
|
const applied = applyDiscount(
|
|
68
|
-
await validateDiscount({ code: input.discountCode, customerId: input.customerId }, provider),
|
|
79
|
+
await validateDiscount({ code: input.discountCode, subtotal, customerId: input.customerId }, provider),
|
|
69
80
|
subtotal
|
|
70
81
|
);
|
|
71
82
|
discountTotal = applied.discountTotal;
|
|
@@ -80,37 +91,51 @@ async function getCustomerOrders(customerId, provider = getShopProvider()) {
|
|
|
80
91
|
return provider.listOrders({ customerId, limit: 100 });
|
|
81
92
|
}
|
|
82
93
|
|
|
83
|
-
// src/
|
|
84
|
-
var
|
|
85
|
-
function
|
|
86
|
-
|
|
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;
|
|
87
101
|
}
|
|
88
|
-
function
|
|
89
|
-
|
|
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;
|
|
90
125
|
try {
|
|
91
|
-
|
|
126
|
+
payload = await error?.context?.json?.();
|
|
92
127
|
} catch {
|
|
93
|
-
return void 0;
|
|
94
128
|
}
|
|
129
|
+
return { ok: false, status, payload: payload ?? { error: error.message } };
|
|
95
130
|
}
|
|
96
131
|
|
|
97
|
-
// src/
|
|
98
|
-
var
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
orders: "plg_shop_orders",
|
|
103
|
-
orderItems: "plg_shop_order_items",
|
|
104
|
-
customers: "plg_shop_customers",
|
|
105
|
-
discounts: "plg_shop_discounts",
|
|
106
|
-
salesChannels: "plg_shop_channels",
|
|
107
|
-
// Core, not plg_shop_*: a delivery zone is a fact about the business, and
|
|
108
|
-
// fulfilment and financial read it too. Same reasoning as addresses and
|
|
109
|
-
// orders — see migration 0021.
|
|
110
|
-
shippingZones: "shipping_zones"
|
|
111
|
-
};
|
|
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
|
+
}
|
|
112
137
|
|
|
113
|
-
// src/supabase-provider.ts
|
|
138
|
+
// src/data/supabase-provider.ts
|
|
114
139
|
function getDb() {
|
|
115
140
|
const supabase = getSupabaseClientOptional();
|
|
116
141
|
if (!supabase) throw new Error("@fayz-ai/shop: Supabase client not initialized. Call setGlobalSupabaseClient() first.");
|
|
@@ -128,6 +153,95 @@ function toPostalDigits(value) {
|
|
|
128
153
|
const digits = String(value ?? "").replace(/\D/g, "");
|
|
129
154
|
return digits ? digits.padStart(8, "0").slice(0, 8) : void 0;
|
|
130
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
|
+
}
|
|
131
245
|
function rowToShippingZone(row) {
|
|
132
246
|
return {
|
|
133
247
|
id: row.id,
|
|
@@ -141,9 +255,74 @@ function rowToShippingZone(row) {
|
|
|
141
255
|
etaMinDays: row.eta_min_days ?? null,
|
|
142
256
|
etaMaxDays: row.eta_max_days ?? null,
|
|
143
257
|
active: row.active ?? true,
|
|
144
|
-
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
|
|
145
273
|
};
|
|
146
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
|
+
}
|
|
147
326
|
function shippingZonePayload(input) {
|
|
148
327
|
const payload = {};
|
|
149
328
|
if (input.name !== void 0) payload.name = input.name;
|
|
@@ -156,6 +335,34 @@ function shippingZonePayload(input) {
|
|
|
156
335
|
if (input.etaMaxDays !== void 0) payload.eta_max_days = input.etaMaxDays ?? null;
|
|
157
336
|
if (input.active !== void 0) payload.active = input.active;
|
|
158
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;
|
|
159
366
|
return payload;
|
|
160
367
|
}
|
|
161
368
|
function rowToProduct(row) {
|
|
@@ -179,10 +386,21 @@ function rowToProduct(row) {
|
|
|
179
386
|
isPhysical: row.is_physical ?? true,
|
|
180
387
|
weight: row.weight,
|
|
181
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,
|
|
182
395
|
createdAt: row.created_at,
|
|
183
396
|
updatedAt: row.updated_at
|
|
184
397
|
};
|
|
185
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
|
+
}
|
|
186
404
|
function rowToImage(row) {
|
|
187
405
|
return {
|
|
188
406
|
id: row.id,
|
|
@@ -254,6 +472,7 @@ function rowToCustomer(row) {
|
|
|
254
472
|
lastName: row.last_name ?? "",
|
|
255
473
|
email: row.email,
|
|
256
474
|
phone: row.phone,
|
|
475
|
+
document: row.document ?? null,
|
|
257
476
|
notes: row.notes,
|
|
258
477
|
ordersCount: row.orders_count ?? 0,
|
|
259
478
|
totalSpent: row.total_spent ?? 0,
|
|
@@ -297,6 +516,113 @@ function rowToSalesChannel(row) {
|
|
|
297
516
|
updatedAt: row.updated_at
|
|
298
517
|
};
|
|
299
518
|
}
|
|
519
|
+
function segmentPayload(input) {
|
|
520
|
+
const p = {};
|
|
521
|
+
if (input.name !== void 0) p.name = input.name;
|
|
522
|
+
if (input.description !== void 0) p.description = input.description;
|
|
523
|
+
if (input.minOrders !== void 0) p.min_orders = nullableNumber(input.minOrders);
|
|
524
|
+
if (input.maxOrders !== void 0) p.max_orders = nullableNumber(input.maxOrders);
|
|
525
|
+
if (input.minSpent !== void 0) p.min_spent = nullableNumber(input.minSpent);
|
|
526
|
+
if (input.maxSpent !== void 0) p.max_spent = nullableNumber(input.maxSpent);
|
|
527
|
+
if (input.joinedAfter !== void 0) p.joined_after = input.joinedAfter || null;
|
|
528
|
+
if (input.joinedBefore !== void 0) p.joined_before = input.joinedBefore || null;
|
|
529
|
+
return p;
|
|
530
|
+
}
|
|
531
|
+
var GIFT_CODE_ALPHABET = "ABCDEFGHJKLMNPQRTUVWXY2346789";
|
|
532
|
+
function generateGiftCardCode() {
|
|
533
|
+
const bytes = new Uint8Array(12);
|
|
534
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
535
|
+
const chars = Array.from(bytes, (b) => GIFT_CODE_ALPHABET[b % GIFT_CODE_ALPHABET.length]);
|
|
536
|
+
return `${chars.slice(0, 4).join("")}-${chars.slice(4, 8).join("")}-${chars.slice(8, 12).join("")}`;
|
|
537
|
+
}
|
|
538
|
+
var LIBRARY_FOLDER = "library";
|
|
539
|
+
function nullableNumber(value) {
|
|
540
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
541
|
+
const n = Number(value);
|
|
542
|
+
return Number.isFinite(n) ? n : null;
|
|
543
|
+
}
|
|
544
|
+
function rowToCollection(row) {
|
|
545
|
+
const members = row.members ?? [];
|
|
546
|
+
return {
|
|
547
|
+
id: row.id,
|
|
548
|
+
tenantId: row.tenant_id,
|
|
549
|
+
title: row.title,
|
|
550
|
+
handle: row.handle ?? slugify(row.title ?? ""),
|
|
551
|
+
description: row.description ?? null,
|
|
552
|
+
imageUrl: row.image_url ?? null,
|
|
553
|
+
seoTitle: row.seo_title ?? null,
|
|
554
|
+
seoDescription: row.seo_description ?? null,
|
|
555
|
+
published: row.published ?? true,
|
|
556
|
+
sortOrder: row.sort_order ?? 0,
|
|
557
|
+
productIds: [...members].sort((a, b) => (a.position ?? 0) - (b.position ?? 0)).map((m) => m.product_id),
|
|
558
|
+
createdAt: row.created_at,
|
|
559
|
+
updatedAt: row.updated_at ?? row.created_at
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function rowToCustomerGroup(row) {
|
|
563
|
+
const members = row.members ?? [];
|
|
564
|
+
return {
|
|
565
|
+
id: row.id,
|
|
566
|
+
tenantId: row.tenant_id,
|
|
567
|
+
name: row.name,
|
|
568
|
+
description: row.description ?? null,
|
|
569
|
+
color: row.color ?? null,
|
|
570
|
+
customerIds: members.map((m) => m.customer_id),
|
|
571
|
+
createdAt: row.created_at,
|
|
572
|
+
updatedAt: row.updated_at ?? row.created_at
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
function rowToCustomerSegment(row) {
|
|
576
|
+
return {
|
|
577
|
+
id: row.id,
|
|
578
|
+
tenantId: row.tenant_id,
|
|
579
|
+
name: row.name,
|
|
580
|
+
description: row.description ?? null,
|
|
581
|
+
minOrders: nullableNumber(row.min_orders),
|
|
582
|
+
maxOrders: nullableNumber(row.max_orders),
|
|
583
|
+
minSpent: nullableNumber(row.min_spent),
|
|
584
|
+
maxSpent: nullableNumber(row.max_spent),
|
|
585
|
+
joinedAfter: row.joined_after ?? null,
|
|
586
|
+
joinedBefore: row.joined_before ?? null,
|
|
587
|
+
createdAt: row.created_at,
|
|
588
|
+
updatedAt: row.updated_at ?? row.created_at
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
var SHOP_EVENT_PREFIX = "shop.";
|
|
592
|
+
var EMAIL_CHANNEL = "email";
|
|
593
|
+
function rowToEmailTemplate(row) {
|
|
594
|
+
return {
|
|
595
|
+
id: row.id,
|
|
596
|
+
tenantId: row.tenant_id,
|
|
597
|
+
event: String(row.event_key ?? "").slice(SHOP_EVENT_PREFIX.length),
|
|
598
|
+
// NULL stays NULL: the catalog resolves the default (resolveEmailTemplates),
|
|
599
|
+
// not this mapping. Collapsing to `false` here would silently disable every
|
|
600
|
+
// template whose row exists only because its text was edited.
|
|
601
|
+
enabled: row.enabled ?? null,
|
|
602
|
+
// Empty string and NULL both mean "no override" — a subject the merchant
|
|
603
|
+
// cleared should fall back to the default, not send a blank subject line.
|
|
604
|
+
subject: row.subject || null,
|
|
605
|
+
body: row.body || null,
|
|
606
|
+
updatedAt: row.updated_at ?? row.created_at
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
function rowToGiftCard(row) {
|
|
610
|
+
return {
|
|
611
|
+
id: row.id,
|
|
612
|
+
tenantId: row.tenant_id,
|
|
613
|
+
code: String(row.code ?? "").toUpperCase(),
|
|
614
|
+
initialBalance: Number(row.initial_balance ?? 0),
|
|
615
|
+
balance: Number(row.balance ?? 0),
|
|
616
|
+
currency: row.currency ?? "BRL",
|
|
617
|
+
status: row.status ?? "active",
|
|
618
|
+
expiresAt: row.expires_at ?? null,
|
|
619
|
+
customerId: row.customer_id ?? null,
|
|
620
|
+
customerEmail: row.customer_email ?? null,
|
|
621
|
+
note: row.note ?? null,
|
|
622
|
+
createdAt: row.created_at,
|
|
623
|
+
updatedAt: row.updated_at ?? row.created_at
|
|
624
|
+
};
|
|
625
|
+
}
|
|
300
626
|
var SupabaseShopProvider = class {
|
|
301
627
|
// ---------------------------------------------------------------------------
|
|
302
628
|
// Products
|
|
@@ -323,6 +649,123 @@ var SupabaseShopProvider = class {
|
|
|
323
649
|
if (error) throw error;
|
|
324
650
|
return data ? rowToProduct(data) : null;
|
|
325
651
|
}
|
|
652
|
+
/** Option axes + variants from the 0028 tables (spine M5). The storefront
|
|
653
|
+
* RLS on both tables already restricts anon reads to active variants of
|
|
654
|
+
* publicly readable products; the status filter here keeps the admin path
|
|
655
|
+
* (authenticated tenant member, who CAN see archived rows) consistent. */
|
|
656
|
+
async getProductVariants(productId, opts) {
|
|
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");
|
|
660
|
+
const [optionsRes, variantsRes] = await Promise.all([
|
|
661
|
+
db.from(T.productOptions).select("*").eq("product_id", productId).order("position", { ascending: true }),
|
|
662
|
+
variantQuery.order("position", { ascending: true })
|
|
663
|
+
]);
|
|
664
|
+
if (optionsRes.error) throw optionsRes.error;
|
|
665
|
+
if (variantsRes.error) throw variantsRes.error;
|
|
666
|
+
const options = (optionsRes.data ?? []).map((row) => ({
|
|
667
|
+
id: row.id,
|
|
668
|
+
name: row.name,
|
|
669
|
+
values: Array.isArray(row.values) ? row.values.map(String) : [],
|
|
670
|
+
position: row.position ?? 0
|
|
671
|
+
}));
|
|
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) => ({
|
|
710
|
+
id: row.id,
|
|
711
|
+
name: row.name,
|
|
712
|
+
values: Array.isArray(row.values) ? row.values.map(String) : [],
|
|
713
|
+
position: row.position ?? 0
|
|
714
|
+
}));
|
|
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;
|
|
768
|
+
}
|
|
326
769
|
async createProduct(input) {
|
|
327
770
|
const db = getDb();
|
|
328
771
|
const tenantId = getTenantId();
|
|
@@ -342,7 +785,17 @@ var SupabaseShopProvider = class {
|
|
|
342
785
|
category_id: input.categoryId ?? null,
|
|
343
786
|
is_physical: input.isPhysical ?? true,
|
|
344
787
|
weight: input.weight ?? null,
|
|
345
|
-
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
|
|
346
799
|
}).select(`*, images:${T.productImages}(*)`).single();
|
|
347
800
|
if (error) throw error;
|
|
348
801
|
return rowToProduct(data);
|
|
@@ -365,6 +818,13 @@ var SupabaseShopProvider = class {
|
|
|
365
818
|
if (input.categoryId !== void 0) updates.category_id = input.categoryId;
|
|
366
819
|
if (input.isPhysical !== void 0) updates.is_physical = input.isPhysical;
|
|
367
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;
|
|
368
828
|
const { data, error } = await db.from(T.products).update(updates).eq("id", id).select(`*, images:${T.productImages}(*)`).single();
|
|
369
829
|
if (error) throw error;
|
|
370
830
|
return rowToProduct(data);
|
|
@@ -409,9 +869,43 @@ var SupabaseShopProvider = class {
|
|
|
409
869
|
if (error) throw error;
|
|
410
870
|
return rowToImage(data);
|
|
411
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
|
+
}
|
|
412
887
|
async deleteProductImage(imageId) {
|
|
413
|
-
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);
|
|
414
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]);
|
|
415
909
|
}
|
|
416
910
|
// ---------------------------------------------------------------------------
|
|
417
911
|
// Categories
|
|
@@ -520,10 +1014,188 @@ var SupabaseShopProvider = class {
|
|
|
520
1014
|
if (error) throw error;
|
|
521
1015
|
return rowToShippingZone(data);
|
|
522
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
|
+
}
|
|
523
1056
|
async deleteShippingZone(id) {
|
|
524
1057
|
const { error } = await getDb().from(T.shippingZones).delete().eq("id", id);
|
|
525
1058
|
if (error) throw error;
|
|
526
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
|
+
}
|
|
527
1199
|
/**
|
|
528
1200
|
* Settle a pending order: the merchant confirming that the money arrived.
|
|
529
1201
|
*
|
|
@@ -541,6 +1213,208 @@ var SupabaseShopProvider = class {
|
|
|
541
1213
|
if (error) throw error;
|
|
542
1214
|
return this.getOrder(id);
|
|
543
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
|
+
}
|
|
544
1418
|
// Trusted placement — delegates all price/discount/inventory authority to the
|
|
545
1419
|
// shop_place_order RPC (SECURITY DEFINER). The browser never sets a price.
|
|
546
1420
|
async placeOrder(input) {
|
|
@@ -549,7 +1423,15 @@ var SupabaseShopProvider = class {
|
|
|
549
1423
|
if (!tenantId) throw new Error("@fayz-ai/shop: placeOrder requires a tenant id (configure the shop tenant resolver).");
|
|
550
1424
|
const { data, error } = await db.rpc("shop_place_order", {
|
|
551
1425
|
p_tenant_id: tenantId,
|
|
552
|
-
|
|
1426
|
+
// variant_id rides on each line (0029): the RPC prices and decrements
|
|
1427
|
+
// the variant when present, product-level otherwise. Pre-0029 servers
|
|
1428
|
+
// ignore unknown jsonb keys, so this stays compatible either way.
|
|
1429
|
+
p_items: input.items.map((i) => ({
|
|
1430
|
+
product_id: i.productId,
|
|
1431
|
+
variant_id: asUuid(i.variantId) ?? void 0,
|
|
1432
|
+
quantity: i.quantity,
|
|
1433
|
+
options_label: i.optionsLabel ?? null
|
|
1434
|
+
})),
|
|
553
1435
|
p_customer_id: asUuid(input.customerId),
|
|
554
1436
|
p_customer_name: input.customer?.name ?? null,
|
|
555
1437
|
p_customer_email: input.customer?.email ?? null,
|
|
@@ -569,10 +1451,19 @@ var SupabaseShopProvider = class {
|
|
|
569
1451
|
country: input.shippingAddress.country ?? "BR",
|
|
570
1452
|
recipient: input.shippingAddress.recipient ?? null,
|
|
571
1453
|
phone: input.shippingAddress.phone ?? null,
|
|
572
|
-
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
|
|
573
1460
|
} : null,
|
|
574
1461
|
p_payment_method: input.paymentMethod ?? null,
|
|
575
|
-
p_notes: input.notes ?? null
|
|
1462
|
+
p_notes: input.notes ?? null,
|
|
1463
|
+
// Only when something contributed: p_metadata exists from 0029 on, and
|
|
1464
|
+
// a pre-0029 server rejects unknown NAMED arguments (unlike unknown
|
|
1465
|
+
// jsonb keys). Absent metadata therefore must not send the key at all.
|
|
1466
|
+
...input.metadata !== void 0 ? { p_metadata: input.metadata } : {}
|
|
576
1467
|
});
|
|
577
1468
|
if (error) throw error;
|
|
578
1469
|
const orderId = typeof data === "string" ? data : data?.id ?? data?.order_id ?? data;
|
|
@@ -588,7 +1479,7 @@ var SupabaseShopProvider = class {
|
|
|
588
1479
|
const tenantId = getTenantId();
|
|
589
1480
|
let q = db.from(T.customers).select("*").order("first_name", { ascending: true });
|
|
590
1481
|
if (tenantId) q = q.eq("tenant_id", tenantId);
|
|
591
|
-
|
|
1482
|
+
for (const filter of customerSearchFilters(options?.search)) q = q.or(filter);
|
|
592
1483
|
if (options?.limit) q = q.limit(options.limit);
|
|
593
1484
|
const { data, error } = await q;
|
|
594
1485
|
if (error) throw error;
|
|
@@ -600,7 +1491,15 @@ var SupabaseShopProvider = class {
|
|
|
600
1491
|
return data ? rowToCustomer(data) : null;
|
|
601
1492
|
}
|
|
602
1493
|
async createCustomer(input) {
|
|
603
|
-
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();
|
|
604
1503
|
if (error) throw error;
|
|
605
1504
|
return rowToCustomer(data);
|
|
606
1505
|
}
|
|
@@ -610,6 +1509,7 @@ var SupabaseShopProvider = class {
|
|
|
610
1509
|
if (input.lastName !== void 0) updates.last_name = input.lastName;
|
|
611
1510
|
if (input.email !== void 0) updates.email = input.email;
|
|
612
1511
|
if (input.phone !== void 0) updates.phone = input.phone;
|
|
1512
|
+
if (input.document !== void 0) updates.document = input.document;
|
|
613
1513
|
if (input.notes !== void 0) updates.notes = input.notes;
|
|
614
1514
|
const { data, error } = await getDb().from(T.customers).update(updates).eq("id", id).select().single();
|
|
615
1515
|
if (error) throw error;
|
|
@@ -636,6 +1536,50 @@ var SupabaseShopProvider = class {
|
|
|
636
1536
|
return rowToCustomer(row);
|
|
637
1537
|
}
|
|
638
1538
|
// ---------------------------------------------------------------------------
|
|
1539
|
+
// Promotions (spine M6) — evaluation happens INSIDE the database via the
|
|
1540
|
+
// SECURITY DEFINER function (migrations/0033), so an anonymous storefront
|
|
1541
|
+
// never reads promotion rows or codes directly: it sends the cart snapshot
|
|
1542
|
+
// and receives ordered actions. The function mirrors the TS reference
|
|
1543
|
+
// evaluator (src/rules/evaluator.ts), which is the tested spec.
|
|
1544
|
+
// ---------------------------------------------------------------------------
|
|
1545
|
+
async evaluatePromotions(input) {
|
|
1546
|
+
const db = getDb();
|
|
1547
|
+
const tenantId = getTenantId();
|
|
1548
|
+
if (!tenantId) throw new Error("@fayz-ai/shop: evaluatePromotions requires a tenant id (configure the shop tenant resolver).");
|
|
1549
|
+
const cart = input.cart;
|
|
1550
|
+
const { data, error } = await db.rpc("shop_compute_promotion_actions", {
|
|
1551
|
+
p_tenant_id: tenantId,
|
|
1552
|
+
// The serializable RuleCartSnapshot, snake_case on the wire — the same
|
|
1553
|
+
// contract 0033 documents for p_cart.
|
|
1554
|
+
p_cart: {
|
|
1555
|
+
items: cart.items.map((item) => ({
|
|
1556
|
+
id: item.id ?? item.productId,
|
|
1557
|
+
product_id: item.productId,
|
|
1558
|
+
quantity: item.quantity,
|
|
1559
|
+
unit_price: item.unitPrice,
|
|
1560
|
+
category_id: item.categoryId ?? null,
|
|
1561
|
+
collection_id: item.collectionId ?? null
|
|
1562
|
+
})),
|
|
1563
|
+
shipping_total: cart.shippingTotal ?? null,
|
|
1564
|
+
region: cart.region ?? null,
|
|
1565
|
+
customer: cart.customer ? { email: cart.customer.email ?? null, tags: cart.customer.tags ?? [] } : null,
|
|
1566
|
+
codes: cart.codes ?? []
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
if (error) throw error;
|
|
1570
|
+
const rows = Array.isArray(data) ? data : [];
|
|
1571
|
+
return rows.map((row) => {
|
|
1572
|
+
const code = row.code ?? null;
|
|
1573
|
+
if (row.action === "addShippingDiscount") {
|
|
1574
|
+
return { action: "addShippingDiscount", promotionId: String(row.promotion_id), code, amount: Number(row.amount) };
|
|
1575
|
+
}
|
|
1576
|
+
if (row.action === "addFreeItem") {
|
|
1577
|
+
return { action: "addFreeItem", promotionId: String(row.promotion_id), code, productId: String(row.product_id), quantity: Number(row.quantity) };
|
|
1578
|
+
}
|
|
1579
|
+
return { action: "addItemDiscount", promotionId: String(row.promotion_id), code, itemId: String(row.item_id), amount: Number(row.amount) };
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
// ---------------------------------------------------------------------------
|
|
639
1583
|
// Discounts
|
|
640
1584
|
// ---------------------------------------------------------------------------
|
|
641
1585
|
async listDiscounts(options) {
|
|
@@ -714,11 +1658,585 @@ var SupabaseShopProvider = class {
|
|
|
714
1658
|
const { error } = await getDb().from(T.salesChannels).delete().eq("id", id);
|
|
715
1659
|
if (error) throw error;
|
|
716
1660
|
}
|
|
1661
|
+
// ---------------------------------------------------------------------------
|
|
1662
|
+
// Custom entities (spine M4). Storage resolves through the EntityStore
|
|
1663
|
+
// indirection (I5): a promoted entity's registered store wins; everything
|
|
1664
|
+
// else reads/writes the generic jsonb table (migration 0030), whose row
|
|
1665
|
+
// access is the RLS generated from each declaration (I6).
|
|
1666
|
+
// ---------------------------------------------------------------------------
|
|
1667
|
+
entityStore(entity) {
|
|
1668
|
+
return resolveEntityStore(entity, createSupabaseEntityStore);
|
|
1669
|
+
}
|
|
1670
|
+
async listEntityRecords(entity, options) {
|
|
1671
|
+
return this.entityStore(entity).list(options);
|
|
1672
|
+
}
|
|
1673
|
+
async getEntityRecord(entity, id) {
|
|
1674
|
+
return this.entityStore(entity).get(id);
|
|
1675
|
+
}
|
|
1676
|
+
async getEntityRecordBySlug(entity, slug) {
|
|
1677
|
+
return this.entityStore(entity).getBySlug(slug);
|
|
1678
|
+
}
|
|
1679
|
+
async createEntityRecord(entity, input) {
|
|
1680
|
+
return this.entityStore(entity).create(input);
|
|
1681
|
+
}
|
|
1682
|
+
async updateEntityRecord(entity, id, input) {
|
|
1683
|
+
return this.entityStore(entity).update(id, input);
|
|
1684
|
+
}
|
|
1685
|
+
async deleteEntityRecord(entity, id) {
|
|
1686
|
+
return this.entityStore(entity).delete(id);
|
|
1687
|
+
}
|
|
1688
|
+
// -------------------------------------------------------------------------
|
|
1689
|
+
// Coleções, grupos/segmentos, vale-presente, e-mails e mídia.
|
|
1690
|
+
// -------------------------------------------------------------------------
|
|
1691
|
+
async listCollections() {
|
|
1692
|
+
const db = getDb();
|
|
1693
|
+
const tenantId = getTenantId();
|
|
1694
|
+
let q = db.from(T.collections).select(`*, members:${T.collectionProducts}(product_id, position)`).order("sort_order", { ascending: true });
|
|
1695
|
+
if (tenantId) q = q.eq("tenant_id", tenantId);
|
|
1696
|
+
const { data, error } = await q;
|
|
1697
|
+
if (error) throw error;
|
|
1698
|
+
return (data ?? []).map(rowToCollection);
|
|
1699
|
+
}
|
|
1700
|
+
async getCollection(id) {
|
|
1701
|
+
const { data, error } = await getDb().from(T.collections).select(`*, members:${T.collectionProducts}(product_id, position)`).eq("id", id).maybeSingle();
|
|
1702
|
+
if (error) throw error;
|
|
1703
|
+
return data ? rowToCollection(data) : null;
|
|
1704
|
+
}
|
|
1705
|
+
async createCollection(input) {
|
|
1706
|
+
const db = getDb();
|
|
1707
|
+
const tenantId = getTenantId();
|
|
1708
|
+
const { data, error } = await db.from(T.collections).insert({
|
|
1709
|
+
tenant_id: tenantId,
|
|
1710
|
+
title: input.title,
|
|
1711
|
+
handle: input.handle?.trim() || slugify(input.title),
|
|
1712
|
+
description: input.description ?? null,
|
|
1713
|
+
image_url: input.imageUrl ?? null,
|
|
1714
|
+
seo_title: input.seoTitle ?? null,
|
|
1715
|
+
seo_description: input.seoDescription ?? null,
|
|
1716
|
+
published: input.published ?? true,
|
|
1717
|
+
sort_order: input.sortOrder ?? 0
|
|
1718
|
+
}).select().single();
|
|
1719
|
+
if (error) throw error;
|
|
1720
|
+
await this.syncCollectionProducts(data.id, tenantId, input.productIds);
|
|
1721
|
+
return await this.getCollection(data.id) ?? rowToCollection(data);
|
|
1722
|
+
}
|
|
1723
|
+
async updateCollection(id, input) {
|
|
1724
|
+
const db = getDb();
|
|
1725
|
+
const updates = {};
|
|
1726
|
+
if (input.title !== void 0) updates.title = input.title;
|
|
1727
|
+
if (input.handle !== void 0) updates.handle = input.handle?.trim() || slugify(String(input.title ?? ""));
|
|
1728
|
+
if (input.description !== void 0) updates.description = input.description;
|
|
1729
|
+
if (input.imageUrl !== void 0) updates.image_url = input.imageUrl;
|
|
1730
|
+
if (input.seoTitle !== void 0) updates.seo_title = input.seoTitle;
|
|
1731
|
+
if (input.seoDescription !== void 0) updates.seo_description = input.seoDescription;
|
|
1732
|
+
if (input.published !== void 0) updates.published = input.published;
|
|
1733
|
+
if (input.sortOrder !== void 0) updates.sort_order = input.sortOrder;
|
|
1734
|
+
if (Object.keys(updates).length > 0) {
|
|
1735
|
+
const { error } = await db.from(T.collections).update(updates).eq("id", id);
|
|
1736
|
+
if (error) throw error;
|
|
1737
|
+
}
|
|
1738
|
+
if (input.productIds !== void 0) {
|
|
1739
|
+
await this.syncCollectionProducts(id, getTenantId(), input.productIds);
|
|
1740
|
+
}
|
|
1741
|
+
const updated = await this.getCollection(id);
|
|
1742
|
+
if (!updated) throw new Error("Collection not found");
|
|
1743
|
+
return updated;
|
|
1744
|
+
}
|
|
1745
|
+
async deleteCollection(id) {
|
|
1746
|
+
const { error } = await getDb().from(T.collections).delete().eq("id", id);
|
|
1747
|
+
if (error) throw error;
|
|
1748
|
+
}
|
|
1749
|
+
async listCustomerGroups() {
|
|
1750
|
+
const db = getDb();
|
|
1751
|
+
const tenantId = getTenantId();
|
|
1752
|
+
let q = db.from(T.customerGroups).select(`*, members:${T.customerGroupMembers}(customer_id)`).order("name", { ascending: true });
|
|
1753
|
+
if (tenantId) q = q.eq("tenant_id", tenantId);
|
|
1754
|
+
const { data, error } = await q;
|
|
1755
|
+
if (error) throw error;
|
|
1756
|
+
return (data ?? []).map(rowToCustomerGroup);
|
|
1757
|
+
}
|
|
1758
|
+
async createCustomerGroup(input) {
|
|
1759
|
+
const tenantId = getTenantId();
|
|
1760
|
+
const { data, error } = await getDb().from(T.customerGroups).insert({
|
|
1761
|
+
tenant_id: tenantId,
|
|
1762
|
+
name: input.name,
|
|
1763
|
+
description: input.description ?? null,
|
|
1764
|
+
color: input.color ?? null
|
|
1765
|
+
}).select().single();
|
|
1766
|
+
if (error) throw error;
|
|
1767
|
+
await this.syncGroupMembers(data.id, tenantId, input.customerIds);
|
|
1768
|
+
return { ...rowToCustomerGroup(data), customerIds: input.customerIds ?? [] };
|
|
1769
|
+
}
|
|
1770
|
+
async updateCustomerGroup(id, input) {
|
|
1771
|
+
const db = getDb();
|
|
1772
|
+
const updates = {};
|
|
1773
|
+
if (input.name !== void 0) updates.name = input.name;
|
|
1774
|
+
if (input.description !== void 0) updates.description = input.description;
|
|
1775
|
+
if (input.color !== void 0) updates.color = input.color;
|
|
1776
|
+
if (Object.keys(updates).length > 0) {
|
|
1777
|
+
const { error: error2 } = await db.from(T.customerGroups).update(updates).eq("id", id);
|
|
1778
|
+
if (error2) throw error2;
|
|
1779
|
+
}
|
|
1780
|
+
if (input.customerIds !== void 0) {
|
|
1781
|
+
await this.syncGroupMembers(id, getTenantId(), input.customerIds);
|
|
1782
|
+
}
|
|
1783
|
+
const { data, error } = await db.from(T.customerGroups).select(`*, members:${T.customerGroupMembers}(customer_id)`).eq("id", id).single();
|
|
1784
|
+
if (error) throw error;
|
|
1785
|
+
return rowToCustomerGroup(data);
|
|
1786
|
+
}
|
|
1787
|
+
async deleteCustomerGroup(id) {
|
|
1788
|
+
const { error } = await getDb().from(T.customerGroups).delete().eq("id", id);
|
|
1789
|
+
if (error) throw error;
|
|
1790
|
+
}
|
|
1791
|
+
async listCustomerSegments() {
|
|
1792
|
+
const db = getDb();
|
|
1793
|
+
const tenantId = getTenantId();
|
|
1794
|
+
let q = db.from(T.customerSegments).select("*").order("name", { ascending: true });
|
|
1795
|
+
if (tenantId) q = q.eq("tenant_id", tenantId);
|
|
1796
|
+
const { data, error } = await q;
|
|
1797
|
+
if (error) throw error;
|
|
1798
|
+
return (data ?? []).map(rowToCustomerSegment);
|
|
1799
|
+
}
|
|
1800
|
+
async createCustomerSegment(input) {
|
|
1801
|
+
const { data, error } = await getDb().from(T.customerSegments).insert({ tenant_id: getTenantId(), ...segmentPayload(input), name: input.name }).select().single();
|
|
1802
|
+
if (error) throw error;
|
|
1803
|
+
return rowToCustomerSegment(data);
|
|
1804
|
+
}
|
|
1805
|
+
async updateCustomerSegment(id, input) {
|
|
1806
|
+
const { data, error } = await getDb().from(T.customerSegments).update(segmentPayload(input)).eq("id", id).select().single();
|
|
1807
|
+
if (error) throw error;
|
|
1808
|
+
return rowToCustomerSegment(data);
|
|
1809
|
+
}
|
|
1810
|
+
async deleteCustomerSegment(id) {
|
|
1811
|
+
const { error } = await getDb().from(T.customerSegments).delete().eq("id", id);
|
|
1812
|
+
if (error) throw error;
|
|
1813
|
+
}
|
|
1814
|
+
async listGiftCards() {
|
|
1815
|
+
const db = getDb();
|
|
1816
|
+
const tenantId = getTenantId();
|
|
1817
|
+
let q = db.from(T.giftCards).select("*").order("created_at", { ascending: false });
|
|
1818
|
+
if (tenantId) q = q.eq("tenant_id", tenantId);
|
|
1819
|
+
const { data, error } = await q;
|
|
1820
|
+
if (error) throw error;
|
|
1821
|
+
return (data ?? []).map(rowToGiftCard);
|
|
1822
|
+
}
|
|
1823
|
+
async createGiftCard(input) {
|
|
1824
|
+
const amount = Number(input.initialBalance) || 0;
|
|
1825
|
+
const { data, error } = await getDb().from(T.giftCards).insert({
|
|
1826
|
+
tenant_id: getTenantId(),
|
|
1827
|
+
code: (input.code?.trim() || generateGiftCardCode()).toUpperCase(),
|
|
1828
|
+
initial_balance: amount,
|
|
1829
|
+
// A new card is worth its face value. The admin never types the
|
|
1830
|
+
// remaining balance at issue — that is what redemption is for.
|
|
1831
|
+
balance: amount,
|
|
1832
|
+
currency: input.currency ?? "BRL",
|
|
1833
|
+
status: input.status ?? "active",
|
|
1834
|
+
expires_at: input.expiresAt ?? null,
|
|
1835
|
+
customer_id: asUuid(input.customerId),
|
|
1836
|
+
customer_email: input.customerEmail ?? null,
|
|
1837
|
+
note: input.note ?? null
|
|
1838
|
+
}).select().single();
|
|
1839
|
+
if (error) throw error;
|
|
1840
|
+
return rowToGiftCard(data);
|
|
1841
|
+
}
|
|
1842
|
+
async updateGiftCard(id, input) {
|
|
1843
|
+
const updates = {};
|
|
1844
|
+
if (input.code !== void 0) updates.code = input.code.trim().toUpperCase();
|
|
1845
|
+
if (input.balance !== void 0) updates.balance = input.balance;
|
|
1846
|
+
if (input.status !== void 0) updates.status = input.status;
|
|
1847
|
+
if (input.expiresAt !== void 0) updates.expires_at = input.expiresAt;
|
|
1848
|
+
if (input.customerId !== void 0) updates.customer_id = asUuid(input.customerId);
|
|
1849
|
+
if (input.customerEmail !== void 0) updates.customer_email = input.customerEmail;
|
|
1850
|
+
if (input.note !== void 0) updates.note = input.note;
|
|
1851
|
+
const { data, error } = await getDb().from(T.giftCards).update(updates).eq("id", id).select().single();
|
|
1852
|
+
if (error) throw error;
|
|
1853
|
+
return rowToGiftCard(data);
|
|
1854
|
+
}
|
|
1855
|
+
async deleteGiftCard(id) {
|
|
1856
|
+
const { error } = await getDb().from(T.giftCards).delete().eq("id", id);
|
|
1857
|
+
if (error) throw error;
|
|
1858
|
+
}
|
|
1859
|
+
async listEmailTemplates() {
|
|
1860
|
+
const db = getDb();
|
|
1861
|
+
const tenantId = getTenantId();
|
|
1862
|
+
let q = db.from(T.messageTemplates).select("*").like("event_key", `${SHOP_EVENT_PREFIX}%`).eq("channel", EMAIL_CHANNEL);
|
|
1863
|
+
if (tenantId) q = q.eq("tenant_id", tenantId);
|
|
1864
|
+
const { data, error } = await q;
|
|
1865
|
+
if (error) throw error;
|
|
1866
|
+
return (data ?? []).map(rowToEmailTemplate);
|
|
1867
|
+
}
|
|
1868
|
+
async upsertEmailTemplate(input) {
|
|
1869
|
+
const payload = {
|
|
1870
|
+
tenant_id: getTenantId(),
|
|
1871
|
+
event_key: `${SHOP_EVENT_PREFIX}${input.event}`,
|
|
1872
|
+
channel: EMAIL_CHANNEL
|
|
1873
|
+
};
|
|
1874
|
+
if (input.enabled !== void 0) payload.enabled = input.enabled;
|
|
1875
|
+
if (input.subject !== void 0) payload.subject = input.subject;
|
|
1876
|
+
if (input.body !== void 0) payload.body = input.body;
|
|
1877
|
+
const { data, error } = await getDb().from(T.messageTemplates).upsert(payload, { onConflict: "tenant_id,event_key,channel" }).select().single();
|
|
1878
|
+
if (error) throw error;
|
|
1879
|
+
return rowToEmailTemplate(data);
|
|
1880
|
+
}
|
|
1881
|
+
async listMediaFiles() {
|
|
1882
|
+
const tenantId = getTenantId();
|
|
1883
|
+
if (!tenantId) return [];
|
|
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);
|
|
1903
|
+
const listAt = async (prefix) => {
|
|
1904
|
+
const { data, error } = await bucket.list(prefix, {
|
|
1905
|
+
limit: 1e3,
|
|
1906
|
+
sortBy: { column: "created_at", order: "desc" }
|
|
1907
|
+
});
|
|
1908
|
+
if (error) return [];
|
|
1909
|
+
return (data ?? []).map((entry) => ({ prefix, entry }));
|
|
1910
|
+
};
|
|
1911
|
+
const top = await listAt(tenantId);
|
|
1912
|
+
const folders = top.filter(({ entry }) => !entry.id && entry.name === LIBRARY_FOLDER).map(({ entry }) => `${tenantId}/${entry.name}`);
|
|
1913
|
+
const nested = (await Promise.all(folders.map(listAt))).flat();
|
|
1914
|
+
return [...top, ...nested].filter(({ entry }) => entry.id).map(({ prefix, entry }) => {
|
|
1915
|
+
const path = `${prefix}/${entry.name}`;
|
|
1916
|
+
const { data: { publicUrl } } = bucket.getPublicUrl(path);
|
|
1917
|
+
const name = displayNameOf(entry.name);
|
|
1918
|
+
const file = {
|
|
1919
|
+
id: path,
|
|
1920
|
+
name,
|
|
1921
|
+
url: publicUrl,
|
|
1922
|
+
kind: mediaKindOf(entry.name, entry.metadata?.mimetype),
|
|
1923
|
+
size: Number(entry.metadata?.size ?? 0),
|
|
1924
|
+
extension: (entry.name.split(".").pop() ?? "").toUpperCase(),
|
|
1925
|
+
createdAt: entry.created_at ?? entry.updated_at ?? ""
|
|
1926
|
+
};
|
|
1927
|
+
return file;
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
async uploadMediaFile(file) {
|
|
1931
|
+
const db = getDb();
|
|
1932
|
+
const tenantId = getTenantId();
|
|
1933
|
+
if (!tenantId) throw new Error("@fayz-ai/shop: no active tenant for media upload");
|
|
1934
|
+
const path = `${tenantId}/${LIBRARY_FOLDER}/${Date.now()}-${file.name}`;
|
|
1935
|
+
const bucket = db.storage.from(T.mediaBucket);
|
|
1936
|
+
const { error } = await bucket.upload(path, file);
|
|
1937
|
+
if (error) throw error;
|
|
1938
|
+
const { data: { publicUrl } } = bucket.getPublicUrl(path);
|
|
1939
|
+
return {
|
|
1940
|
+
id: path,
|
|
1941
|
+
name: file.name,
|
|
1942
|
+
url: publicUrl,
|
|
1943
|
+
kind: mediaKindOf(file.name, file.type),
|
|
1944
|
+
size: file.size,
|
|
1945
|
+
extension: (file.name.split(".").pop() ?? "").toUpperCase(),
|
|
1946
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
async deleteMediaFile(id) {
|
|
1950
|
+
const { error } = await getDb().storage.from(T.mediaBucket).remove([id]);
|
|
1951
|
+
if (error) throw error;
|
|
1952
|
+
}
|
|
1953
|
+
async syncCollectionProducts(collectionId, tenantId, productIds) {
|
|
1954
|
+
if (productIds === void 0) return;
|
|
1955
|
+
const db = getDb();
|
|
1956
|
+
const { error: delErr } = await db.from(T.collectionProducts).delete().eq("collection_id", collectionId);
|
|
1957
|
+
if (delErr) throw delErr;
|
|
1958
|
+
if (productIds.length === 0) return;
|
|
1959
|
+
const rows = productIds.map((productId, position) => ({
|
|
1960
|
+
collection_id: collectionId,
|
|
1961
|
+
product_id: productId,
|
|
1962
|
+
tenant_id: tenantId,
|
|
1963
|
+
position
|
|
1964
|
+
}));
|
|
1965
|
+
const { error } = await db.from(T.collectionProducts).insert(rows);
|
|
1966
|
+
if (error) throw error;
|
|
1967
|
+
}
|
|
1968
|
+
async syncGroupMembers(groupId, tenantId, customerIds) {
|
|
1969
|
+
if (customerIds === void 0) return;
|
|
1970
|
+
const db = getDb();
|
|
1971
|
+
const { error: delErr } = await db.from(T.customerGroupMembers).delete().eq("group_id", groupId);
|
|
1972
|
+
if (delErr) throw delErr;
|
|
1973
|
+
if (customerIds.length === 0) return;
|
|
1974
|
+
const rows = customerIds.map((customerId) => ({
|
|
1975
|
+
group_id: groupId,
|
|
1976
|
+
customer_id: customerId,
|
|
1977
|
+
tenant_id: tenantId
|
|
1978
|
+
}));
|
|
1979
|
+
const { error } = await db.from(T.customerGroupMembers).insert(rows);
|
|
1980
|
+
if (error) throw error;
|
|
1981
|
+
}
|
|
717
1982
|
};
|
|
718
1983
|
function createSupabaseShopProvider() {
|
|
719
1984
|
return new SupabaseShopProvider();
|
|
720
1985
|
}
|
|
721
1986
|
|
|
722
|
-
|
|
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
|
+
|
|
2051
|
+
// src/lib/emails.ts
|
|
2052
|
+
var EMAIL_PLACEHOLDERS = [
|
|
2053
|
+
"{{store_name}}",
|
|
2054
|
+
"{{customer_name}}",
|
|
2055
|
+
"{{order_number}}",
|
|
2056
|
+
"{{order_total}}",
|
|
2057
|
+
"{{order_items}}",
|
|
2058
|
+
"{{tracking_code}}",
|
|
2059
|
+
"{{cart_url}}"
|
|
2060
|
+
];
|
|
2061
|
+
var SHOP_EMAIL_EVENTS = [
|
|
2062
|
+
{
|
|
2063
|
+
event: "order_confirmation",
|
|
2064
|
+
kind: "transactional",
|
|
2065
|
+
icon: "CircleCheck",
|
|
2066
|
+
title: "Confirma\xE7\xE3o de pedido",
|
|
2067
|
+
description: "Enviado quando o cliente conclui a compra",
|
|
2068
|
+
trigger: "Imediatamente ap\xF3s o checkout",
|
|
2069
|
+
defaultSubject: "Pedido #{{order_number}} confirmado",
|
|
2070
|
+
defaultBody: "Ol\xE1 {{customer_name}},\n\nRecebemos seu pedido #{{order_number}}.\n\n{{order_items}}\n\nTotal: {{order_total}}\n\nObrigado por comprar na {{store_name}}!",
|
|
2071
|
+
defaultEnabled: true
|
|
2072
|
+
},
|
|
2073
|
+
{
|
|
2074
|
+
event: "order_paid",
|
|
2075
|
+
kind: "transactional",
|
|
2076
|
+
icon: "CreditCard",
|
|
2077
|
+
title: "Pagamento aprovado",
|
|
2078
|
+
description: "Enviado quando o pagamento \xE9 confirmado",
|
|
2079
|
+
trigger: "Quando o pagamento \xE9 aprovado",
|
|
2080
|
+
defaultSubject: "Pagamento aprovado \u2014 pedido #{{order_number}}",
|
|
2081
|
+
defaultBody: "Ol\xE1 {{customer_name}},\n\nSeu pagamento de {{order_total}} foi aprovado. J\xE1 estamos preparando o pedido #{{order_number}}.\n\n{{store_name}}",
|
|
2082
|
+
defaultEnabled: false
|
|
2083
|
+
},
|
|
2084
|
+
{
|
|
2085
|
+
event: "order_shipped",
|
|
2086
|
+
kind: "transactional",
|
|
2087
|
+
icon: "Truck",
|
|
2088
|
+
title: "Pedido enviado",
|
|
2089
|
+
description: "Enviado quando o pedido sai para entrega, com o rastreio",
|
|
2090
|
+
trigger: "Quando a entrega \xE9 marcada como enviada",
|
|
2091
|
+
defaultSubject: "Seu pedido saiu para entrega \u2014 #{{order_number}}",
|
|
2092
|
+
defaultBody: "Ol\xE1 {{customer_name}},\n\nSeu pedido #{{order_number}} est\xE1 a caminho.\n\nRastreio: {{tracking_code}}\n\n{{store_name}}",
|
|
2093
|
+
defaultEnabled: false
|
|
2094
|
+
},
|
|
2095
|
+
{
|
|
2096
|
+
event: "order_delivered",
|
|
2097
|
+
kind: "transactional",
|
|
2098
|
+
icon: "PackageCheck",
|
|
2099
|
+
title: "Pedido entregue",
|
|
2100
|
+
description: "Enviado quando a entrega \xE9 conclu\xEDda",
|
|
2101
|
+
trigger: "Quando a entrega \xE9 marcada como conclu\xEDda",
|
|
2102
|
+
defaultSubject: "Seu pedido chegou \u2014 #{{order_number}}",
|
|
2103
|
+
defaultBody: "Ol\xE1 {{customer_name}},\n\nSeu pedido #{{order_number}} foi entregue. Esperamos que goste!\n\n{{store_name}}",
|
|
2104
|
+
defaultEnabled: false
|
|
2105
|
+
},
|
|
2106
|
+
{
|
|
2107
|
+
event: "order_cancelled",
|
|
2108
|
+
kind: "transactional",
|
|
2109
|
+
icon: "CircleX",
|
|
2110
|
+
title: "Pedido cancelado",
|
|
2111
|
+
description: "Enviado quando um pedido \xE9 cancelado",
|
|
2112
|
+
trigger: "Quando o pedido \xE9 cancelado",
|
|
2113
|
+
defaultSubject: "Pedido #{{order_number}} cancelado",
|
|
2114
|
+
defaultBody: "Ol\xE1 {{customer_name}},\n\nSeu pedido #{{order_number}} foi cancelado. Se voc\xEA n\xE3o pediu isso, responda este e-mail.\n\n{{store_name}}",
|
|
2115
|
+
defaultEnabled: false
|
|
2116
|
+
},
|
|
2117
|
+
{
|
|
2118
|
+
event: "refund_confirmation",
|
|
2119
|
+
kind: "transactional",
|
|
2120
|
+
icon: "RotateCcw",
|
|
2121
|
+
title: "Reembolso processado",
|
|
2122
|
+
description: "Enviado quando um reembolso \xE9 processado",
|
|
2123
|
+
trigger: "Quando o pagamento \xE9 estornado",
|
|
2124
|
+
defaultSubject: "Reembolso emitido \u2014 pedido #{{order_number}}",
|
|
2125
|
+
defaultBody: "Ol\xE1 {{customer_name}},\n\nO reembolso de {{order_total}} do pedido #{{order_number}} foi processado. O prazo de cr\xE9dito depende do seu banco.\n\n{{store_name}}",
|
|
2126
|
+
defaultEnabled: false
|
|
2127
|
+
},
|
|
2128
|
+
{
|
|
2129
|
+
event: "customer_welcome",
|
|
2130
|
+
kind: "transactional",
|
|
2131
|
+
icon: "Mail",
|
|
2132
|
+
title: "Boas-vindas",
|
|
2133
|
+
description: "Enviado quando um cliente cria conta na loja",
|
|
2134
|
+
trigger: "Quando o cliente se cadastra",
|
|
2135
|
+
defaultSubject: "Bem-vindo(a) \xE0 {{store_name}}",
|
|
2136
|
+
defaultBody: "Ol\xE1 {{customer_name}},\n\nSua conta na {{store_name}} est\xE1 pronta. Bom proveito!\n\n{{store_name}}",
|
|
2137
|
+
defaultEnabled: false
|
|
2138
|
+
},
|
|
2139
|
+
{
|
|
2140
|
+
event: "abandoned_cart",
|
|
2141
|
+
kind: "marketing",
|
|
2142
|
+
icon: "ShoppingCart",
|
|
2143
|
+
title: "Carrinho abandonado",
|
|
2144
|
+
description: "Enviado quando o cliente deixa itens no carrinho sem finalizar",
|
|
2145
|
+
trigger: "1 hora ap\xF3s o abandono do carrinho",
|
|
2146
|
+
defaultSubject: "Voc\xEA esqueceu alguns itens no carrinho",
|
|
2147
|
+
defaultBody: "Ol\xE1 {{customer_name}},\n\nSeus itens ainda est\xE3o te esperando.\n\n{{cart_url}}\n\n{{store_name}}",
|
|
2148
|
+
defaultEnabled: false
|
|
2149
|
+
}
|
|
2150
|
+
];
|
|
2151
|
+
function renderEmailTemplate(text, values) {
|
|
2152
|
+
return text.replace(/\{\{\s*(\w+)\s*\}\}/g, (whole, key) => values[key] ?? whole);
|
|
2153
|
+
}
|
|
2154
|
+
var EMAIL_PREVIEW_SAMPLE = {
|
|
2155
|
+
store_name: "Sua Loja",
|
|
2156
|
+
customer_name: "Maria Silva",
|
|
2157
|
+
order_number: "1042",
|
|
2158
|
+
order_total: "R$ 189,90",
|
|
2159
|
+
order_items: "1 \xD7 Camiseta Cl\xE1ssica \u2014 R$ 89,90\n2 \xD7 Caneca Esmaltada \u2014 R$ 100,00",
|
|
2160
|
+
tracking_code: "BR123456789BR",
|
|
2161
|
+
cart_url: "https://sualoja.com.br/carrinho"
|
|
2162
|
+
};
|
|
2163
|
+
function resolveEmailTemplates(overrides) {
|
|
2164
|
+
const byEvent = new Map(overrides.map((o) => [o.event, o]));
|
|
2165
|
+
return SHOP_EMAIL_EVENTS.map((def) => {
|
|
2166
|
+
const override = byEvent.get(def.event) ?? null;
|
|
2167
|
+
return {
|
|
2168
|
+
...def,
|
|
2169
|
+
override,
|
|
2170
|
+
enabled: override?.enabled ?? def.defaultEnabled,
|
|
2171
|
+
subject: override?.subject ?? def.defaultSubject,
|
|
2172
|
+
body: override?.body ?? def.defaultBody,
|
|
2173
|
+
customized: Boolean(override?.subject || override?.body)
|
|
2174
|
+
};
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// src/lib/message-events.ts
|
|
2179
|
+
var ORDER_FIELDS = [
|
|
2180
|
+
{ name: "order_number", label: "N\xFAmero do pedido", type: "string", example: "1042" },
|
|
2181
|
+
{ name: "order_total", label: "Total", type: "money", example: "R$ 189,90" },
|
|
2182
|
+
{ name: "order_items", label: "Itens", type: "string", example: "1 \xD7 Camiseta Cl\xE1ssica \u2014 R$ 89,90\n2 \xD7 Caneca Esmaltada \u2014 R$ 100,00" },
|
|
2183
|
+
{ name: "customer_name", label: "Cliente", type: "string", example: "Maria Silva" },
|
|
2184
|
+
{ name: "store_name", label: "Loja", type: "string", example: "Sua Loja" }
|
|
2185
|
+
];
|
|
2186
|
+
var TRACKING_FIELD = {
|
|
2187
|
+
name: "tracking_code",
|
|
2188
|
+
label: "C\xF3digo de rastreio",
|
|
2189
|
+
type: "string",
|
|
2190
|
+
example: "BR123456789BR"
|
|
2191
|
+
};
|
|
2192
|
+
var CART_FIELD = {
|
|
2193
|
+
name: "cart_url",
|
|
2194
|
+
label: "Link do carrinho",
|
|
2195
|
+
type: "url",
|
|
2196
|
+
example: "https://sualoja.com.br/carrinho"
|
|
2197
|
+
};
|
|
2198
|
+
var FIELDS_BY_EVENT = {
|
|
2199
|
+
order_shipped: [...ORDER_FIELDS, TRACKING_FIELD],
|
|
2200
|
+
order_delivered: [...ORDER_FIELDS, TRACKING_FIELD],
|
|
2201
|
+
abandoned_cart: [...ORDER_FIELDS, CART_FIELD]
|
|
2202
|
+
};
|
|
2203
|
+
var NOT_AUDITED = /* @__PURE__ */ new Set(["abandoned_cart"]);
|
|
2204
|
+
function buildShopDomainEvents() {
|
|
2205
|
+
return SHOP_EMAIL_EVENTS.map((def) => ({
|
|
2206
|
+
name: `shop.${def.event}`,
|
|
2207
|
+
title: def.title,
|
|
2208
|
+
description: def.description,
|
|
2209
|
+
trigger: def.trigger,
|
|
2210
|
+
icon: def.icon,
|
|
2211
|
+
fields: FIELDS_BY_EVENT[def.event] ?? ORDER_FIELDS,
|
|
2212
|
+
audit: !NOT_AUDITED.has(def.event)
|
|
2213
|
+
}));
|
|
2214
|
+
}
|
|
2215
|
+
var WHATSAPP_EVENTS = /* @__PURE__ */ new Set(["order_confirmation", "order_shipped", "abandoned_cart"]);
|
|
2216
|
+
var WHATSAPP_BODY = {
|
|
2217
|
+
order_confirmation: "Oi {{customer_name}}! Recebemos seu pedido #{{order_number}} \u2705\n\nTotal: {{order_total}}\n\nQualquer d\xFAvida \xE9 s\xF3 chamar aqui. \u2014 {{store_name}}",
|
|
2218
|
+
order_shipped: "Oi {{customer_name}}! Seu pedido #{{order_number}} saiu para entrega \u{1F69A}\n\nRastreio: {{tracking_code}}\n\n\u2014 {{store_name}}",
|
|
2219
|
+
abandoned_cart: "Oi {{customer_name}}! Vi que ficaram itens no seu carrinho \u{1F6D2}\n\nQuer que eu segure para voc\xEA?\n{{cart_url}}\n\n\u2014 {{store_name}}"
|
|
2220
|
+
};
|
|
2221
|
+
function buildShopMessageDefaults() {
|
|
2222
|
+
return SHOP_EMAIL_EVENTS.map((def) => {
|
|
2223
|
+
const whatsapp = WHATSAPP_EVENTS.has(def.event) ? WHATSAPP_BODY[def.event] : void 0;
|
|
2224
|
+
return {
|
|
2225
|
+
eventKey: `shop.${def.event}`,
|
|
2226
|
+
kind: def.kind,
|
|
2227
|
+
channels: whatsapp ? ["email", "whatsapp"].slice() : ["email"],
|
|
2228
|
+
defaults: {
|
|
2229
|
+
email: { subject: def.defaultSubject, body: def.defaultBody },
|
|
2230
|
+
...whatsapp ? { whatsapp: { body: whatsapp } } : {}
|
|
2231
|
+
},
|
|
2232
|
+
// Only email inherits "on by default". WhatsApp bills per message and
|
|
2233
|
+
// needs an approved number — switching itself on would spend someone's
|
|
2234
|
+
// money unasked.
|
|
2235
|
+
defaultEnabled: { email: def.defaultEnabled }
|
|
2236
|
+
};
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
|
|
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 };
|
|
723
2241
|
//# sourceMappingURL=index.js.map
|
|
724
2242
|
//# sourceMappingURL=index.js.map
|