@fayz-ai/shop 0.9.0-next.1 → 0.10.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-3GERSY5F.js +526 -0
- package/dist/chunk-3GERSY5F.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-IYQHKMLB.js → chunk-IL6GA4ER.js} +4 -4
- package/dist/chunk-IL6GA4ER.js.map +1 -0
- package/dist/chunk-KLDS7IPU.js +17 -0
- package/dist/chunk-KLDS7IPU.js.map +1 -0
- package/dist/chunk-RGTIJQJL.js +93 -0
- package/dist/chunk-RGTIJQJL.js.map +1 -0
- package/dist/{chunk-SVTCJ6HP.js → chunk-U3O2I3RM.js} +395 -27
- package/dist/chunk-U3O2I3RM.js.map +1 -0
- package/dist/chunk-XERLK5LD.js +1415 -0
- package/dist/chunk-XERLK5LD.js.map +1 -0
- package/dist/chunk-XOP6SHPB.js +142 -0
- package/dist/chunk-XOP6SHPB.js.map +1 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.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 +133 -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 +142 -0
- package/dist/data/provider.d.ts.map +1 -0
- package/dist/data/supabase-provider.d.ts +100 -0
- package/dist/data/supabase-provider.d.ts.map +1 -0
- package/dist/data/tables.d.ts +35 -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 +11 -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/sidecar.d.ts +9 -0
- package/dist/entities/sidecar.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 +17 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +728 -41
- 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 +292 -0
- package/dist/inventory/stock-types.d.ts.map +1 -0
- package/dist/inventory/supabase-stock-store.d.ts +59 -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/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/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/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/types.d.ts +231 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +26 -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.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,11 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
3
|
-
import './chunk-
|
|
4
|
-
export {
|
|
5
|
-
|
|
1
|
+
import { T } from './chunk-XERLK5LD.js';
|
|
2
|
+
export { DEFAULT_STOCK_TRACKING, MockStockStore, STOCK_TRACKING_MODES, StockMovementError, SupabaseStockStore, createMockStockStore, createSupabaseStockStore, listStockMovementKinds, listStockReasonCodes, mirroredInventoryCount, registerStockMovementKind, registerStockReasonCode, registerStockStore, replayStockLedger, resolveStockStore, validateStockMovement } from './chunk-XERLK5LD.js';
|
|
3
|
+
import { getShopProvider } from './chunk-IL6GA4ER.js';
|
|
4
|
+
export { getShopProvider, resetShopProvider, setShopProvider } from './chunk-IL6GA4ER.js';
|
|
5
|
+
import './chunk-FQPGE3YN.js';
|
|
6
|
+
export { MockShopProvider, createMockShopProvider } from './chunk-U3O2I3RM.js';
|
|
7
|
+
export { MOCK_CATEGORIES, MOCK_DISCOUNTS, MOCK_PRODUCTS, buildMockCatalog, svgPlaceholder } from './chunk-IKNGCRUM.js';
|
|
8
|
+
import { createSupabaseEntityStore } from './chunk-RGTIJQJL.js';
|
|
9
|
+
import { getShopTenantId } from './chunk-KLDS7IPU.js';
|
|
10
|
+
export { getShopTenantId, setShopTenantResolver } from './chunk-KLDS7IPU.js';
|
|
11
|
+
import { resolveEntityStore, SIDECAR_TABLE, validateSidecarWrite } from './chunk-XOP6SHPB.js';
|
|
12
|
+
import './chunk-3GERSY5F.js';
|
|
6
13
|
import { getSupabaseClientOptional } from '@fayz-ai/core';
|
|
7
14
|
|
|
8
|
-
// src/commerce.ts
|
|
15
|
+
// src/lib/commerce.ts
|
|
9
16
|
function round2(n) {
|
|
10
17
|
return Math.round(n * 100) / 100;
|
|
11
18
|
}
|
|
@@ -79,38 +86,6 @@ async function computeCartTotals(input, provider = getShopProvider()) {
|
|
|
79
86
|
async function getCustomerOrders(customerId, provider = getShopProvider()) {
|
|
80
87
|
return provider.listOrders({ customerId, limit: 100 });
|
|
81
88
|
}
|
|
82
|
-
|
|
83
|
-
// src/tenant.ts
|
|
84
|
-
var _resolver = null;
|
|
85
|
-
function setShopTenantResolver(resolver) {
|
|
86
|
-
_resolver = resolver;
|
|
87
|
-
}
|
|
88
|
-
function getShopTenantId() {
|
|
89
|
-
if (!_resolver) return void 0;
|
|
90
|
-
try {
|
|
91
|
-
return _resolver();
|
|
92
|
-
} catch {
|
|
93
|
-
return void 0;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// src/tables.ts
|
|
98
|
-
var T = {
|
|
99
|
-
products: "plg_shop_products",
|
|
100
|
-
productImages: "plg_shop_product_images",
|
|
101
|
-
categories: "plg_shop_categories",
|
|
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
|
-
};
|
|
112
|
-
|
|
113
|
-
// src/supabase-provider.ts
|
|
114
89
|
function getDb() {
|
|
115
90
|
const supabase = getSupabaseClientOptional();
|
|
116
91
|
if (!supabase) throw new Error("@fayz-ai/shop: Supabase client not initialized. Call setGlobalSupabaseClient() first.");
|
|
@@ -297,6 +272,122 @@ function rowToSalesChannel(row) {
|
|
|
297
272
|
updatedAt: row.updated_at
|
|
298
273
|
};
|
|
299
274
|
}
|
|
275
|
+
function segmentPayload(input) {
|
|
276
|
+
const p = {};
|
|
277
|
+
if (input.name !== void 0) p.name = input.name;
|
|
278
|
+
if (input.description !== void 0) p.description = input.description;
|
|
279
|
+
if (input.minOrders !== void 0) p.min_orders = nullableNumber(input.minOrders);
|
|
280
|
+
if (input.maxOrders !== void 0) p.max_orders = nullableNumber(input.maxOrders);
|
|
281
|
+
if (input.minSpent !== void 0) p.min_spent = nullableNumber(input.minSpent);
|
|
282
|
+
if (input.maxSpent !== void 0) p.max_spent = nullableNumber(input.maxSpent);
|
|
283
|
+
if (input.joinedAfter !== void 0) p.joined_after = input.joinedAfter || null;
|
|
284
|
+
if (input.joinedBefore !== void 0) p.joined_before = input.joinedBefore || null;
|
|
285
|
+
return p;
|
|
286
|
+
}
|
|
287
|
+
var GIFT_CODE_ALPHABET = "ABCDEFGHJKLMNPQRTUVWXY2346789";
|
|
288
|
+
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "avif", "bmp"]);
|
|
289
|
+
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set(["mp4", "mov", "webm", "m4v", "avi", "mkv"]);
|
|
290
|
+
function generateGiftCardCode() {
|
|
291
|
+
const bytes = new Uint8Array(12);
|
|
292
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
293
|
+
const chars = Array.from(bytes, (b) => GIFT_CODE_ALPHABET[b % GIFT_CODE_ALPHABET.length]);
|
|
294
|
+
return `${chars.slice(0, 4).join("")}-${chars.slice(4, 8).join("")}-${chars.slice(8, 12).join("")}`;
|
|
295
|
+
}
|
|
296
|
+
function mediaKindOf(name, mime) {
|
|
297
|
+
if (mime?.startsWith("image/")) return "image";
|
|
298
|
+
if (mime?.startsWith("video/")) return "video";
|
|
299
|
+
const ext = (name.split(".").pop() ?? "").toLowerCase();
|
|
300
|
+
if (IMAGE_EXTENSIONS.has(ext)) return "image";
|
|
301
|
+
if (VIDEO_EXTENSIONS.has(ext)) return "video";
|
|
302
|
+
return "document";
|
|
303
|
+
}
|
|
304
|
+
function nullableNumber(value) {
|
|
305
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
306
|
+
const n = Number(value);
|
|
307
|
+
return Number.isFinite(n) ? n : null;
|
|
308
|
+
}
|
|
309
|
+
function rowToCollection(row) {
|
|
310
|
+
const members = row.members ?? [];
|
|
311
|
+
return {
|
|
312
|
+
id: row.id,
|
|
313
|
+
tenantId: row.tenant_id,
|
|
314
|
+
title: row.title,
|
|
315
|
+
handle: row.handle ?? slugify(row.title ?? ""),
|
|
316
|
+
description: row.description ?? null,
|
|
317
|
+
imageUrl: row.image_url ?? null,
|
|
318
|
+
seoTitle: row.seo_title ?? null,
|
|
319
|
+
seoDescription: row.seo_description ?? null,
|
|
320
|
+
published: row.published ?? true,
|
|
321
|
+
sortOrder: row.sort_order ?? 0,
|
|
322
|
+
productIds: [...members].sort((a, b) => (a.position ?? 0) - (b.position ?? 0)).map((m) => m.product_id),
|
|
323
|
+
createdAt: row.created_at,
|
|
324
|
+
updatedAt: row.updated_at ?? row.created_at
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function rowToCustomerGroup(row) {
|
|
328
|
+
const members = row.members ?? [];
|
|
329
|
+
return {
|
|
330
|
+
id: row.id,
|
|
331
|
+
tenantId: row.tenant_id,
|
|
332
|
+
name: row.name,
|
|
333
|
+
description: row.description ?? null,
|
|
334
|
+
color: row.color ?? null,
|
|
335
|
+
customerIds: members.map((m) => m.customer_id),
|
|
336
|
+
createdAt: row.created_at,
|
|
337
|
+
updatedAt: row.updated_at ?? row.created_at
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function rowToCustomerSegment(row) {
|
|
341
|
+
return {
|
|
342
|
+
id: row.id,
|
|
343
|
+
tenantId: row.tenant_id,
|
|
344
|
+
name: row.name,
|
|
345
|
+
description: row.description ?? null,
|
|
346
|
+
minOrders: nullableNumber(row.min_orders),
|
|
347
|
+
maxOrders: nullableNumber(row.max_orders),
|
|
348
|
+
minSpent: nullableNumber(row.min_spent),
|
|
349
|
+
maxSpent: nullableNumber(row.max_spent),
|
|
350
|
+
joinedAfter: row.joined_after ?? null,
|
|
351
|
+
joinedBefore: row.joined_before ?? null,
|
|
352
|
+
createdAt: row.created_at,
|
|
353
|
+
updatedAt: row.updated_at ?? row.created_at
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
var SHOP_EVENT_PREFIX = "shop.";
|
|
357
|
+
var EMAIL_CHANNEL = "email";
|
|
358
|
+
function rowToEmailTemplate(row) {
|
|
359
|
+
return {
|
|
360
|
+
id: row.id,
|
|
361
|
+
tenantId: row.tenant_id,
|
|
362
|
+
event: String(row.event_key ?? "").slice(SHOP_EVENT_PREFIX.length),
|
|
363
|
+
// NULL stays NULL: the catalog resolves the default (resolveEmailTemplates),
|
|
364
|
+
// not this mapping. Collapsing to `false` here would silently disable every
|
|
365
|
+
// template whose row exists only because its text was edited.
|
|
366
|
+
enabled: row.enabled ?? null,
|
|
367
|
+
// Empty string and NULL both mean "no override" — a subject the merchant
|
|
368
|
+
// cleared should fall back to the default, not send a blank subject line.
|
|
369
|
+
subject: row.subject || null,
|
|
370
|
+
body: row.body || null,
|
|
371
|
+
updatedAt: row.updated_at ?? row.created_at
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function rowToGiftCard(row) {
|
|
375
|
+
return {
|
|
376
|
+
id: row.id,
|
|
377
|
+
tenantId: row.tenant_id,
|
|
378
|
+
code: String(row.code ?? "").toUpperCase(),
|
|
379
|
+
initialBalance: Number(row.initial_balance ?? 0),
|
|
380
|
+
balance: Number(row.balance ?? 0),
|
|
381
|
+
currency: row.currency ?? "BRL",
|
|
382
|
+
status: row.status ?? "active",
|
|
383
|
+
expiresAt: row.expires_at ?? null,
|
|
384
|
+
customerId: row.customer_id ?? null,
|
|
385
|
+
customerEmail: row.customer_email ?? null,
|
|
386
|
+
note: row.note ?? null,
|
|
387
|
+
createdAt: row.created_at,
|
|
388
|
+
updatedAt: row.updated_at ?? row.created_at
|
|
389
|
+
};
|
|
390
|
+
}
|
|
300
391
|
var SupabaseShopProvider = class {
|
|
301
392
|
// ---------------------------------------------------------------------------
|
|
302
393
|
// Products
|
|
@@ -323,6 +414,38 @@ var SupabaseShopProvider = class {
|
|
|
323
414
|
if (error) throw error;
|
|
324
415
|
return data ? rowToProduct(data) : null;
|
|
325
416
|
}
|
|
417
|
+
/** Option axes + variants from the 0028 tables (spine M5). The storefront
|
|
418
|
+
* RLS on both tables already restricts anon reads to active variants of
|
|
419
|
+
* publicly readable products; the status filter here keeps the admin path
|
|
420
|
+
* (authenticated tenant member, who CAN see archived rows) consistent. */
|
|
421
|
+
async getProductVariants(productId) {
|
|
422
|
+
const db = getDb();
|
|
423
|
+
const [optionsRes, variantsRes] = await Promise.all([
|
|
424
|
+
db.from(T.productOptions).select("*").eq("product_id", productId).order("position", { ascending: true }),
|
|
425
|
+
db.from(T.productVariants).select("*").eq("product_id", productId).eq("status", "active").order("position", { ascending: true })
|
|
426
|
+
]);
|
|
427
|
+
if (optionsRes.error) throw optionsRes.error;
|
|
428
|
+
if (variantsRes.error) throw variantsRes.error;
|
|
429
|
+
const options = (optionsRes.data ?? []).map((row) => ({
|
|
430
|
+
id: row.id,
|
|
431
|
+
name: row.name,
|
|
432
|
+
values: Array.isArray(row.values) ? row.values.map(String) : [],
|
|
433
|
+
position: row.position ?? 0
|
|
434
|
+
}));
|
|
435
|
+
const variants = (variantsRes.data ?? []).map((row) => ({
|
|
436
|
+
id: row.id,
|
|
437
|
+
productId: row.product_id,
|
|
438
|
+
options: row.options && typeof row.options === "object" ? row.options : {},
|
|
439
|
+
sku: row.sku ?? null,
|
|
440
|
+
price: row.price == null ? null : Number(row.price),
|
|
441
|
+
compareAtPrice: row.compare_at_price == null ? null : Number(row.compare_at_price),
|
|
442
|
+
inventoryCount: row.inventory_count ?? 0,
|
|
443
|
+
position: row.position ?? 0,
|
|
444
|
+
status: row.status ?? "active",
|
|
445
|
+
imageId: row.image_id ?? null
|
|
446
|
+
}));
|
|
447
|
+
return { options, variants };
|
|
448
|
+
}
|
|
326
449
|
async createProduct(input) {
|
|
327
450
|
const db = getDb();
|
|
328
451
|
const tenantId = getTenantId();
|
|
@@ -549,7 +672,15 @@ var SupabaseShopProvider = class {
|
|
|
549
672
|
if (!tenantId) throw new Error("@fayz-ai/shop: placeOrder requires a tenant id (configure the shop tenant resolver).");
|
|
550
673
|
const { data, error } = await db.rpc("shop_place_order", {
|
|
551
674
|
p_tenant_id: tenantId,
|
|
552
|
-
|
|
675
|
+
// variant_id rides on each line (0029): the RPC prices and decrements
|
|
676
|
+
// the variant when present, product-level otherwise. Pre-0029 servers
|
|
677
|
+
// ignore unknown jsonb keys, so this stays compatible either way.
|
|
678
|
+
p_items: input.items.map((i) => ({
|
|
679
|
+
product_id: i.productId,
|
|
680
|
+
variant_id: asUuid(i.variantId) ?? void 0,
|
|
681
|
+
quantity: i.quantity,
|
|
682
|
+
options_label: i.optionsLabel ?? null
|
|
683
|
+
})),
|
|
553
684
|
p_customer_id: asUuid(input.customerId),
|
|
554
685
|
p_customer_name: input.customer?.name ?? null,
|
|
555
686
|
p_customer_email: input.customer?.email ?? null,
|
|
@@ -572,7 +703,11 @@ var SupabaseShopProvider = class {
|
|
|
572
703
|
label: input.shippingAddress.label ?? null
|
|
573
704
|
} : null,
|
|
574
705
|
p_payment_method: input.paymentMethod ?? null,
|
|
575
|
-
p_notes: input.notes ?? null
|
|
706
|
+
p_notes: input.notes ?? null,
|
|
707
|
+
// Only when something contributed: p_metadata exists from 0029 on, and
|
|
708
|
+
// a pre-0029 server rejects unknown NAMED arguments (unlike unknown
|
|
709
|
+
// jsonb keys). Absent metadata therefore must not send the key at all.
|
|
710
|
+
...input.metadata !== void 0 ? { p_metadata: input.metadata } : {}
|
|
576
711
|
});
|
|
577
712
|
if (error) throw error;
|
|
578
713
|
const orderId = typeof data === "string" ? data : data?.id ?? data?.order_id ?? data;
|
|
@@ -636,6 +771,50 @@ var SupabaseShopProvider = class {
|
|
|
636
771
|
return rowToCustomer(row);
|
|
637
772
|
}
|
|
638
773
|
// ---------------------------------------------------------------------------
|
|
774
|
+
// Promotions (spine M6) — evaluation happens INSIDE the database via the
|
|
775
|
+
// SECURITY DEFINER function (migrations/0033), so an anonymous storefront
|
|
776
|
+
// never reads promotion rows or codes directly: it sends the cart snapshot
|
|
777
|
+
// and receives ordered actions. The function mirrors the TS reference
|
|
778
|
+
// evaluator (src/rules/evaluator.ts), which is the tested spec.
|
|
779
|
+
// ---------------------------------------------------------------------------
|
|
780
|
+
async evaluatePromotions(input) {
|
|
781
|
+
const db = getDb();
|
|
782
|
+
const tenantId = getTenantId();
|
|
783
|
+
if (!tenantId) throw new Error("@fayz-ai/shop: evaluatePromotions requires a tenant id (configure the shop tenant resolver).");
|
|
784
|
+
const cart = input.cart;
|
|
785
|
+
const { data, error } = await db.rpc("shop_compute_promotion_actions", {
|
|
786
|
+
p_tenant_id: tenantId,
|
|
787
|
+
// The serializable RuleCartSnapshot, snake_case on the wire — the same
|
|
788
|
+
// contract 0033 documents for p_cart.
|
|
789
|
+
p_cart: {
|
|
790
|
+
items: cart.items.map((item) => ({
|
|
791
|
+
id: item.id ?? item.productId,
|
|
792
|
+
product_id: item.productId,
|
|
793
|
+
quantity: item.quantity,
|
|
794
|
+
unit_price: item.unitPrice,
|
|
795
|
+
category_id: item.categoryId ?? null,
|
|
796
|
+
collection_id: item.collectionId ?? null
|
|
797
|
+
})),
|
|
798
|
+
shipping_total: cart.shippingTotal ?? null,
|
|
799
|
+
region: cart.region ?? null,
|
|
800
|
+
customer: cart.customer ? { email: cart.customer.email ?? null, tags: cart.customer.tags ?? [] } : null,
|
|
801
|
+
codes: cart.codes ?? []
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
if (error) throw error;
|
|
805
|
+
const rows = Array.isArray(data) ? data : [];
|
|
806
|
+
return rows.map((row) => {
|
|
807
|
+
const code = row.code ?? null;
|
|
808
|
+
if (row.action === "addShippingDiscount") {
|
|
809
|
+
return { action: "addShippingDiscount", promotionId: String(row.promotion_id), code, amount: Number(row.amount) };
|
|
810
|
+
}
|
|
811
|
+
if (row.action === "addFreeItem") {
|
|
812
|
+
return { action: "addFreeItem", promotionId: String(row.promotion_id), code, productId: String(row.product_id), quantity: Number(row.quantity) };
|
|
813
|
+
}
|
|
814
|
+
return { action: "addItemDiscount", promotionId: String(row.promotion_id), code, itemId: String(row.item_id), amount: Number(row.amount) };
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
// ---------------------------------------------------------------------------
|
|
639
818
|
// Discounts
|
|
640
819
|
// ---------------------------------------------------------------------------
|
|
641
820
|
async listDiscounts(options) {
|
|
@@ -714,11 +893,519 @@ var SupabaseShopProvider = class {
|
|
|
714
893
|
const { error } = await getDb().from(T.salesChannels).delete().eq("id", id);
|
|
715
894
|
if (error) throw error;
|
|
716
895
|
}
|
|
896
|
+
// ---------------------------------------------------------------------------
|
|
897
|
+
// Custom entities (spine M4). Storage resolves through the EntityStore
|
|
898
|
+
// indirection (I5): a promoted entity's registered store wins; everything
|
|
899
|
+
// else reads/writes the generic jsonb table (migration 0030), whose row
|
|
900
|
+
// access is the RLS generated from each declaration (I6).
|
|
901
|
+
// ---------------------------------------------------------------------------
|
|
902
|
+
entityStore(entity) {
|
|
903
|
+
return resolveEntityStore(entity, createSupabaseEntityStore);
|
|
904
|
+
}
|
|
905
|
+
async listEntityRecords(entity, options) {
|
|
906
|
+
return this.entityStore(entity).list(options);
|
|
907
|
+
}
|
|
908
|
+
async getEntityRecord(entity, id) {
|
|
909
|
+
return this.entityStore(entity).get(id);
|
|
910
|
+
}
|
|
911
|
+
async getEntityRecordBySlug(entity, slug) {
|
|
912
|
+
return this.entityStore(entity).getBySlug(slug);
|
|
913
|
+
}
|
|
914
|
+
async createEntityRecord(entity, input) {
|
|
915
|
+
return this.entityStore(entity).create(input);
|
|
916
|
+
}
|
|
917
|
+
async updateEntityRecord(entity, id, input) {
|
|
918
|
+
return this.entityStore(entity).update(id, input);
|
|
919
|
+
}
|
|
920
|
+
async deleteEntityRecord(entity, id) {
|
|
921
|
+
return this.entityStore(entity).delete(id);
|
|
922
|
+
}
|
|
923
|
+
// Sidecar fields on core records (migration 0031): one row per
|
|
924
|
+
// (reference, reference_id), validated on write against defineSidecarFields.
|
|
925
|
+
async getEntityFields(reference, referenceId) {
|
|
926
|
+
const { data, error } = await getDb().from(SIDECAR_TABLE).select("data").eq("reference", reference).eq("reference_id", referenceId).maybeSingle();
|
|
927
|
+
if (error) throw error;
|
|
928
|
+
return data ? data.data ?? {} : null;
|
|
929
|
+
}
|
|
930
|
+
async setEntityFields(reference, referenceId, fields) {
|
|
931
|
+
validateSidecarWrite(reference, fields);
|
|
932
|
+
const { data, error } = await getDb().from(SIDECAR_TABLE).upsert(
|
|
933
|
+
{ tenant_id: getTenantId(), reference, reference_id: referenceId, data: fields },
|
|
934
|
+
{ onConflict: "tenant_id,reference,reference_id" }
|
|
935
|
+
).select("data").single();
|
|
936
|
+
if (error) throw error;
|
|
937
|
+
return data.data ?? {};
|
|
938
|
+
}
|
|
939
|
+
// -------------------------------------------------------------------------
|
|
940
|
+
// Coleções, grupos/segmentos, vale-presente, e-mails e mídia.
|
|
941
|
+
// -------------------------------------------------------------------------
|
|
942
|
+
async listCollections() {
|
|
943
|
+
const db = getDb();
|
|
944
|
+
const tenantId = getTenantId();
|
|
945
|
+
let q = db.from(T.collections).select(`*, members:${T.collectionProducts}(product_id, position)`).order("sort_order", { ascending: true });
|
|
946
|
+
if (tenantId) q = q.eq("tenant_id", tenantId);
|
|
947
|
+
const { data, error } = await q;
|
|
948
|
+
if (error) throw error;
|
|
949
|
+
return (data ?? []).map(rowToCollection);
|
|
950
|
+
}
|
|
951
|
+
async getCollection(id) {
|
|
952
|
+
const { data, error } = await getDb().from(T.collections).select(`*, members:${T.collectionProducts}(product_id, position)`).eq("id", id).maybeSingle();
|
|
953
|
+
if (error) throw error;
|
|
954
|
+
return data ? rowToCollection(data) : null;
|
|
955
|
+
}
|
|
956
|
+
async createCollection(input) {
|
|
957
|
+
const db = getDb();
|
|
958
|
+
const tenantId = getTenantId();
|
|
959
|
+
const { data, error } = await db.from(T.collections).insert({
|
|
960
|
+
tenant_id: tenantId,
|
|
961
|
+
title: input.title,
|
|
962
|
+
handle: input.handle?.trim() || slugify(input.title),
|
|
963
|
+
description: input.description ?? null,
|
|
964
|
+
image_url: input.imageUrl ?? null,
|
|
965
|
+
seo_title: input.seoTitle ?? null,
|
|
966
|
+
seo_description: input.seoDescription ?? null,
|
|
967
|
+
published: input.published ?? true,
|
|
968
|
+
sort_order: input.sortOrder ?? 0
|
|
969
|
+
}).select().single();
|
|
970
|
+
if (error) throw error;
|
|
971
|
+
await this.syncCollectionProducts(data.id, tenantId, input.productIds);
|
|
972
|
+
return await this.getCollection(data.id) ?? rowToCollection(data);
|
|
973
|
+
}
|
|
974
|
+
async updateCollection(id, input) {
|
|
975
|
+
const db = getDb();
|
|
976
|
+
const updates = {};
|
|
977
|
+
if (input.title !== void 0) updates.title = input.title;
|
|
978
|
+
if (input.handle !== void 0) updates.handle = input.handle?.trim() || slugify(String(input.title ?? ""));
|
|
979
|
+
if (input.description !== void 0) updates.description = input.description;
|
|
980
|
+
if (input.imageUrl !== void 0) updates.image_url = input.imageUrl;
|
|
981
|
+
if (input.seoTitle !== void 0) updates.seo_title = input.seoTitle;
|
|
982
|
+
if (input.seoDescription !== void 0) updates.seo_description = input.seoDescription;
|
|
983
|
+
if (input.published !== void 0) updates.published = input.published;
|
|
984
|
+
if (input.sortOrder !== void 0) updates.sort_order = input.sortOrder;
|
|
985
|
+
if (Object.keys(updates).length > 0) {
|
|
986
|
+
const { error } = await db.from(T.collections).update(updates).eq("id", id);
|
|
987
|
+
if (error) throw error;
|
|
988
|
+
}
|
|
989
|
+
if (input.productIds !== void 0) {
|
|
990
|
+
await this.syncCollectionProducts(id, getTenantId(), input.productIds);
|
|
991
|
+
}
|
|
992
|
+
const updated = await this.getCollection(id);
|
|
993
|
+
if (!updated) throw new Error("Collection not found");
|
|
994
|
+
return updated;
|
|
995
|
+
}
|
|
996
|
+
async deleteCollection(id) {
|
|
997
|
+
const { error } = await getDb().from(T.collections).delete().eq("id", id);
|
|
998
|
+
if (error) throw error;
|
|
999
|
+
}
|
|
1000
|
+
async listCustomerGroups() {
|
|
1001
|
+
const db = getDb();
|
|
1002
|
+
const tenantId = getTenantId();
|
|
1003
|
+
let q = db.from(T.customerGroups).select(`*, members:${T.customerGroupMembers}(customer_id)`).order("name", { ascending: true });
|
|
1004
|
+
if (tenantId) q = q.eq("tenant_id", tenantId);
|
|
1005
|
+
const { data, error } = await q;
|
|
1006
|
+
if (error) throw error;
|
|
1007
|
+
return (data ?? []).map(rowToCustomerGroup);
|
|
1008
|
+
}
|
|
1009
|
+
async createCustomerGroup(input) {
|
|
1010
|
+
const tenantId = getTenantId();
|
|
1011
|
+
const { data, error } = await getDb().from(T.customerGroups).insert({
|
|
1012
|
+
tenant_id: tenantId,
|
|
1013
|
+
name: input.name,
|
|
1014
|
+
description: input.description ?? null,
|
|
1015
|
+
color: input.color ?? null
|
|
1016
|
+
}).select().single();
|
|
1017
|
+
if (error) throw error;
|
|
1018
|
+
await this.syncGroupMembers(data.id, tenantId, input.customerIds);
|
|
1019
|
+
return { ...rowToCustomerGroup(data), customerIds: input.customerIds ?? [] };
|
|
1020
|
+
}
|
|
1021
|
+
async updateCustomerGroup(id, input) {
|
|
1022
|
+
const db = getDb();
|
|
1023
|
+
const updates = {};
|
|
1024
|
+
if (input.name !== void 0) updates.name = input.name;
|
|
1025
|
+
if (input.description !== void 0) updates.description = input.description;
|
|
1026
|
+
if (input.color !== void 0) updates.color = input.color;
|
|
1027
|
+
if (Object.keys(updates).length > 0) {
|
|
1028
|
+
const { error: error2 } = await db.from(T.customerGroups).update(updates).eq("id", id);
|
|
1029
|
+
if (error2) throw error2;
|
|
1030
|
+
}
|
|
1031
|
+
if (input.customerIds !== void 0) {
|
|
1032
|
+
await this.syncGroupMembers(id, getTenantId(), input.customerIds);
|
|
1033
|
+
}
|
|
1034
|
+
const { data, error } = await db.from(T.customerGroups).select(`*, members:${T.customerGroupMembers}(customer_id)`).eq("id", id).single();
|
|
1035
|
+
if (error) throw error;
|
|
1036
|
+
return rowToCustomerGroup(data);
|
|
1037
|
+
}
|
|
1038
|
+
async deleteCustomerGroup(id) {
|
|
1039
|
+
const { error } = await getDb().from(T.customerGroups).delete().eq("id", id);
|
|
1040
|
+
if (error) throw error;
|
|
1041
|
+
}
|
|
1042
|
+
async listCustomerSegments() {
|
|
1043
|
+
const db = getDb();
|
|
1044
|
+
const tenantId = getTenantId();
|
|
1045
|
+
let q = db.from(T.customerSegments).select("*").order("name", { ascending: true });
|
|
1046
|
+
if (tenantId) q = q.eq("tenant_id", tenantId);
|
|
1047
|
+
const { data, error } = await q;
|
|
1048
|
+
if (error) throw error;
|
|
1049
|
+
return (data ?? []).map(rowToCustomerSegment);
|
|
1050
|
+
}
|
|
1051
|
+
async createCustomerSegment(input) {
|
|
1052
|
+
const { data, error } = await getDb().from(T.customerSegments).insert({ tenant_id: getTenantId(), ...segmentPayload(input), name: input.name }).select().single();
|
|
1053
|
+
if (error) throw error;
|
|
1054
|
+
return rowToCustomerSegment(data);
|
|
1055
|
+
}
|
|
1056
|
+
async updateCustomerSegment(id, input) {
|
|
1057
|
+
const { data, error } = await getDb().from(T.customerSegments).update(segmentPayload(input)).eq("id", id).select().single();
|
|
1058
|
+
if (error) throw error;
|
|
1059
|
+
return rowToCustomerSegment(data);
|
|
1060
|
+
}
|
|
1061
|
+
async deleteCustomerSegment(id) {
|
|
1062
|
+
const { error } = await getDb().from(T.customerSegments).delete().eq("id", id);
|
|
1063
|
+
if (error) throw error;
|
|
1064
|
+
}
|
|
1065
|
+
async listGiftCards() {
|
|
1066
|
+
const db = getDb();
|
|
1067
|
+
const tenantId = getTenantId();
|
|
1068
|
+
let q = db.from(T.giftCards).select("*").order("created_at", { ascending: false });
|
|
1069
|
+
if (tenantId) q = q.eq("tenant_id", tenantId);
|
|
1070
|
+
const { data, error } = await q;
|
|
1071
|
+
if (error) throw error;
|
|
1072
|
+
return (data ?? []).map(rowToGiftCard);
|
|
1073
|
+
}
|
|
1074
|
+
async createGiftCard(input) {
|
|
1075
|
+
const amount = Number(input.initialBalance) || 0;
|
|
1076
|
+
const { data, error } = await getDb().from(T.giftCards).insert({
|
|
1077
|
+
tenant_id: getTenantId(),
|
|
1078
|
+
code: (input.code?.trim() || generateGiftCardCode()).toUpperCase(),
|
|
1079
|
+
initial_balance: amount,
|
|
1080
|
+
// A new card is worth its face value. The admin never types the
|
|
1081
|
+
// remaining balance at issue — that is what redemption is for.
|
|
1082
|
+
balance: amount,
|
|
1083
|
+
currency: input.currency ?? "BRL",
|
|
1084
|
+
status: input.status ?? "active",
|
|
1085
|
+
expires_at: input.expiresAt ?? null,
|
|
1086
|
+
customer_id: asUuid(input.customerId),
|
|
1087
|
+
customer_email: input.customerEmail ?? null,
|
|
1088
|
+
note: input.note ?? null
|
|
1089
|
+
}).select().single();
|
|
1090
|
+
if (error) throw error;
|
|
1091
|
+
return rowToGiftCard(data);
|
|
1092
|
+
}
|
|
1093
|
+
async updateGiftCard(id, input) {
|
|
1094
|
+
const updates = {};
|
|
1095
|
+
if (input.code !== void 0) updates.code = input.code.trim().toUpperCase();
|
|
1096
|
+
if (input.balance !== void 0) updates.balance = input.balance;
|
|
1097
|
+
if (input.status !== void 0) updates.status = input.status;
|
|
1098
|
+
if (input.expiresAt !== void 0) updates.expires_at = input.expiresAt;
|
|
1099
|
+
if (input.customerId !== void 0) updates.customer_id = asUuid(input.customerId);
|
|
1100
|
+
if (input.customerEmail !== void 0) updates.customer_email = input.customerEmail;
|
|
1101
|
+
if (input.note !== void 0) updates.note = input.note;
|
|
1102
|
+
const { data, error } = await getDb().from(T.giftCards).update(updates).eq("id", id).select().single();
|
|
1103
|
+
if (error) throw error;
|
|
1104
|
+
return rowToGiftCard(data);
|
|
1105
|
+
}
|
|
1106
|
+
async deleteGiftCard(id) {
|
|
1107
|
+
const { error } = await getDb().from(T.giftCards).delete().eq("id", id);
|
|
1108
|
+
if (error) throw error;
|
|
1109
|
+
}
|
|
1110
|
+
async listEmailTemplates() {
|
|
1111
|
+
const db = getDb();
|
|
1112
|
+
const tenantId = getTenantId();
|
|
1113
|
+
let q = db.from(T.messageTemplates).select("*").like("event_key", `${SHOP_EVENT_PREFIX}%`).eq("channel", EMAIL_CHANNEL);
|
|
1114
|
+
if (tenantId) q = q.eq("tenant_id", tenantId);
|
|
1115
|
+
const { data, error } = await q;
|
|
1116
|
+
if (error) throw error;
|
|
1117
|
+
return (data ?? []).map(rowToEmailTemplate);
|
|
1118
|
+
}
|
|
1119
|
+
async upsertEmailTemplate(input) {
|
|
1120
|
+
const payload = {
|
|
1121
|
+
tenant_id: getTenantId(),
|
|
1122
|
+
event_key: `${SHOP_EVENT_PREFIX}${input.event}`,
|
|
1123
|
+
channel: EMAIL_CHANNEL
|
|
1124
|
+
};
|
|
1125
|
+
if (input.enabled !== void 0) payload.enabled = input.enabled;
|
|
1126
|
+
if (input.subject !== void 0) payload.subject = input.subject;
|
|
1127
|
+
if (input.body !== void 0) payload.body = input.body;
|
|
1128
|
+
const { data, error } = await getDb().from(T.messageTemplates).upsert(payload, { onConflict: "tenant_id,event_key,channel" }).select().single();
|
|
1129
|
+
if (error) throw error;
|
|
1130
|
+
return rowToEmailTemplate(data);
|
|
1131
|
+
}
|
|
1132
|
+
async listMediaFiles() {
|
|
1133
|
+
const db = getDb();
|
|
1134
|
+
const tenantId = getTenantId();
|
|
1135
|
+
if (!tenantId) return [];
|
|
1136
|
+
const bucket = db.storage.from(T.mediaBucket);
|
|
1137
|
+
const listAt = async (prefix) => {
|
|
1138
|
+
const { data, error } = await bucket.list(prefix, {
|
|
1139
|
+
limit: 1e3,
|
|
1140
|
+
sortBy: { column: "created_at", order: "desc" }
|
|
1141
|
+
});
|
|
1142
|
+
if (error) return [];
|
|
1143
|
+
return (data ?? []).map((entry) => ({ prefix, entry }));
|
|
1144
|
+
};
|
|
1145
|
+
const top = await listAt(tenantId);
|
|
1146
|
+
const folders = top.filter(({ entry }) => !entry.id).map(({ entry }) => `${tenantId}/${entry.name}`);
|
|
1147
|
+
const nested = (await Promise.all(folders.map(listAt))).flat();
|
|
1148
|
+
return [...top, ...nested].filter(({ entry }) => entry.id).map(({ prefix, entry }) => {
|
|
1149
|
+
const path = `${prefix}/${entry.name}`;
|
|
1150
|
+
const { data: { publicUrl } } = bucket.getPublicUrl(path);
|
|
1151
|
+
const file = {
|
|
1152
|
+
id: path,
|
|
1153
|
+
name: entry.name,
|
|
1154
|
+
url: publicUrl,
|
|
1155
|
+
kind: mediaKindOf(entry.name, entry.metadata?.mimetype),
|
|
1156
|
+
size: Number(entry.metadata?.size ?? 0),
|
|
1157
|
+
extension: (entry.name.split(".").pop() ?? "").toUpperCase(),
|
|
1158
|
+
createdAt: entry.created_at ?? entry.updated_at ?? ""
|
|
1159
|
+
};
|
|
1160
|
+
return file;
|
|
1161
|
+
}).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
1162
|
+
}
|
|
1163
|
+
async uploadMediaFile(file) {
|
|
1164
|
+
const db = getDb();
|
|
1165
|
+
const tenantId = getTenantId();
|
|
1166
|
+
if (!tenantId) throw new Error("@fayz-ai/shop: no active tenant for media upload");
|
|
1167
|
+
const path = `${tenantId}/library/${Date.now()}-${file.name}`;
|
|
1168
|
+
const bucket = db.storage.from(T.mediaBucket);
|
|
1169
|
+
const { error } = await bucket.upload(path, file);
|
|
1170
|
+
if (error) throw error;
|
|
1171
|
+
const { data: { publicUrl } } = bucket.getPublicUrl(path);
|
|
1172
|
+
return {
|
|
1173
|
+
id: path,
|
|
1174
|
+
name: file.name,
|
|
1175
|
+
url: publicUrl,
|
|
1176
|
+
kind: mediaKindOf(file.name, file.type),
|
|
1177
|
+
size: file.size,
|
|
1178
|
+
extension: (file.name.split(".").pop() ?? "").toUpperCase(),
|
|
1179
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
async deleteMediaFile(id) {
|
|
1183
|
+
const { error } = await getDb().storage.from(T.mediaBucket).remove([id]);
|
|
1184
|
+
if (error) throw error;
|
|
1185
|
+
}
|
|
1186
|
+
async syncCollectionProducts(collectionId, tenantId, productIds) {
|
|
1187
|
+
if (productIds === void 0) return;
|
|
1188
|
+
const db = getDb();
|
|
1189
|
+
const { error: delErr } = await db.from(T.collectionProducts).delete().eq("collection_id", collectionId);
|
|
1190
|
+
if (delErr) throw delErr;
|
|
1191
|
+
if (productIds.length === 0) return;
|
|
1192
|
+
const rows = productIds.map((productId, position) => ({
|
|
1193
|
+
collection_id: collectionId,
|
|
1194
|
+
product_id: productId,
|
|
1195
|
+
tenant_id: tenantId,
|
|
1196
|
+
position
|
|
1197
|
+
}));
|
|
1198
|
+
const { error } = await db.from(T.collectionProducts).insert(rows);
|
|
1199
|
+
if (error) throw error;
|
|
1200
|
+
}
|
|
1201
|
+
async syncGroupMembers(groupId, tenantId, customerIds) {
|
|
1202
|
+
if (customerIds === void 0) return;
|
|
1203
|
+
const db = getDb();
|
|
1204
|
+
const { error: delErr } = await db.from(T.customerGroupMembers).delete().eq("group_id", groupId);
|
|
1205
|
+
if (delErr) throw delErr;
|
|
1206
|
+
if (customerIds.length === 0) return;
|
|
1207
|
+
const rows = customerIds.map((customerId) => ({
|
|
1208
|
+
group_id: groupId,
|
|
1209
|
+
customer_id: customerId,
|
|
1210
|
+
tenant_id: tenantId
|
|
1211
|
+
}));
|
|
1212
|
+
const { error } = await db.from(T.customerGroupMembers).insert(rows);
|
|
1213
|
+
if (error) throw error;
|
|
1214
|
+
}
|
|
717
1215
|
};
|
|
718
1216
|
function createSupabaseShopProvider() {
|
|
719
1217
|
return new SupabaseShopProvider();
|
|
720
1218
|
}
|
|
721
1219
|
|
|
722
|
-
|
|
1220
|
+
// src/lib/emails.ts
|
|
1221
|
+
var EMAIL_PLACEHOLDERS = [
|
|
1222
|
+
"{{store_name}}",
|
|
1223
|
+
"{{customer_name}}",
|
|
1224
|
+
"{{order_number}}",
|
|
1225
|
+
"{{order_total}}",
|
|
1226
|
+
"{{order_items}}",
|
|
1227
|
+
"{{tracking_code}}",
|
|
1228
|
+
"{{cart_url}}"
|
|
1229
|
+
];
|
|
1230
|
+
var SHOP_EMAIL_EVENTS = [
|
|
1231
|
+
{
|
|
1232
|
+
event: "order_confirmation",
|
|
1233
|
+
kind: "transactional",
|
|
1234
|
+
icon: "CircleCheck",
|
|
1235
|
+
title: "Confirma\xE7\xE3o de pedido",
|
|
1236
|
+
description: "Enviado quando o cliente conclui a compra",
|
|
1237
|
+
trigger: "Imediatamente ap\xF3s o checkout",
|
|
1238
|
+
defaultSubject: "Pedido #{{order_number}} confirmado",
|
|
1239
|
+
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}}!",
|
|
1240
|
+
defaultEnabled: true
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
event: "order_paid",
|
|
1244
|
+
kind: "transactional",
|
|
1245
|
+
icon: "CreditCard",
|
|
1246
|
+
title: "Pagamento aprovado",
|
|
1247
|
+
description: "Enviado quando o pagamento \xE9 confirmado",
|
|
1248
|
+
trigger: "Quando o pagamento \xE9 aprovado",
|
|
1249
|
+
defaultSubject: "Pagamento aprovado \u2014 pedido #{{order_number}}",
|
|
1250
|
+
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}}",
|
|
1251
|
+
defaultEnabled: false
|
|
1252
|
+
},
|
|
1253
|
+
{
|
|
1254
|
+
event: "order_shipped",
|
|
1255
|
+
kind: "transactional",
|
|
1256
|
+
icon: "Truck",
|
|
1257
|
+
title: "Pedido enviado",
|
|
1258
|
+
description: "Enviado quando o pedido sai para entrega, com o rastreio",
|
|
1259
|
+
trigger: "Quando a entrega \xE9 marcada como enviada",
|
|
1260
|
+
defaultSubject: "Seu pedido saiu para entrega \u2014 #{{order_number}}",
|
|
1261
|
+
defaultBody: "Ol\xE1 {{customer_name}},\n\nSeu pedido #{{order_number}} est\xE1 a caminho.\n\nRastreio: {{tracking_code}}\n\n{{store_name}}",
|
|
1262
|
+
defaultEnabled: false
|
|
1263
|
+
},
|
|
1264
|
+
{
|
|
1265
|
+
event: "order_delivered",
|
|
1266
|
+
kind: "transactional",
|
|
1267
|
+
icon: "PackageCheck",
|
|
1268
|
+
title: "Pedido entregue",
|
|
1269
|
+
description: "Enviado quando a entrega \xE9 conclu\xEDda",
|
|
1270
|
+
trigger: "Quando a entrega \xE9 marcada como conclu\xEDda",
|
|
1271
|
+
defaultSubject: "Seu pedido chegou \u2014 #{{order_number}}",
|
|
1272
|
+
defaultBody: "Ol\xE1 {{customer_name}},\n\nSeu pedido #{{order_number}} foi entregue. Esperamos que goste!\n\n{{store_name}}",
|
|
1273
|
+
defaultEnabled: false
|
|
1274
|
+
},
|
|
1275
|
+
{
|
|
1276
|
+
event: "order_cancelled",
|
|
1277
|
+
kind: "transactional",
|
|
1278
|
+
icon: "CircleX",
|
|
1279
|
+
title: "Pedido cancelado",
|
|
1280
|
+
description: "Enviado quando um pedido \xE9 cancelado",
|
|
1281
|
+
trigger: "Quando o pedido \xE9 cancelado",
|
|
1282
|
+
defaultSubject: "Pedido #{{order_number}} cancelado",
|
|
1283
|
+
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}}",
|
|
1284
|
+
defaultEnabled: false
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
event: "refund_confirmation",
|
|
1288
|
+
kind: "transactional",
|
|
1289
|
+
icon: "RotateCcw",
|
|
1290
|
+
title: "Reembolso processado",
|
|
1291
|
+
description: "Enviado quando um reembolso \xE9 processado",
|
|
1292
|
+
trigger: "Quando o pagamento \xE9 estornado",
|
|
1293
|
+
defaultSubject: "Reembolso emitido \u2014 pedido #{{order_number}}",
|
|
1294
|
+
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}}",
|
|
1295
|
+
defaultEnabled: false
|
|
1296
|
+
},
|
|
1297
|
+
{
|
|
1298
|
+
event: "customer_welcome",
|
|
1299
|
+
kind: "transactional",
|
|
1300
|
+
icon: "Mail",
|
|
1301
|
+
title: "Boas-vindas",
|
|
1302
|
+
description: "Enviado quando um cliente cria conta na loja",
|
|
1303
|
+
trigger: "Quando o cliente se cadastra",
|
|
1304
|
+
defaultSubject: "Bem-vindo(a) \xE0 {{store_name}}",
|
|
1305
|
+
defaultBody: "Ol\xE1 {{customer_name}},\n\nSua conta na {{store_name}} est\xE1 pronta. Bom proveito!\n\n{{store_name}}",
|
|
1306
|
+
defaultEnabled: false
|
|
1307
|
+
},
|
|
1308
|
+
{
|
|
1309
|
+
event: "abandoned_cart",
|
|
1310
|
+
kind: "marketing",
|
|
1311
|
+
icon: "ShoppingCart",
|
|
1312
|
+
title: "Carrinho abandonado",
|
|
1313
|
+
description: "Enviado quando o cliente deixa itens no carrinho sem finalizar",
|
|
1314
|
+
trigger: "1 hora ap\xF3s o abandono do carrinho",
|
|
1315
|
+
defaultSubject: "Voc\xEA esqueceu alguns itens no carrinho",
|
|
1316
|
+
defaultBody: "Ol\xE1 {{customer_name}},\n\nSeus itens ainda est\xE3o te esperando.\n\n{{cart_url}}\n\n{{store_name}}",
|
|
1317
|
+
defaultEnabled: false
|
|
1318
|
+
}
|
|
1319
|
+
];
|
|
1320
|
+
function renderEmailTemplate(text, values) {
|
|
1321
|
+
return text.replace(/\{\{\s*(\w+)\s*\}\}/g, (whole, key) => values[key] ?? whole);
|
|
1322
|
+
}
|
|
1323
|
+
var EMAIL_PREVIEW_SAMPLE = {
|
|
1324
|
+
store_name: "Sua Loja",
|
|
1325
|
+
customer_name: "Maria Silva",
|
|
1326
|
+
order_number: "1042",
|
|
1327
|
+
order_total: "R$ 189,90",
|
|
1328
|
+
order_items: "1 \xD7 Camiseta Cl\xE1ssica \u2014 R$ 89,90\n2 \xD7 Caneca Esmaltada \u2014 R$ 100,00",
|
|
1329
|
+
tracking_code: "BR123456789BR",
|
|
1330
|
+
cart_url: "https://sualoja.com.br/carrinho"
|
|
1331
|
+
};
|
|
1332
|
+
function resolveEmailTemplates(overrides) {
|
|
1333
|
+
const byEvent = new Map(overrides.map((o) => [o.event, o]));
|
|
1334
|
+
return SHOP_EMAIL_EVENTS.map((def) => {
|
|
1335
|
+
const override = byEvent.get(def.event) ?? null;
|
|
1336
|
+
return {
|
|
1337
|
+
...def,
|
|
1338
|
+
override,
|
|
1339
|
+
enabled: override?.enabled ?? def.defaultEnabled,
|
|
1340
|
+
subject: override?.subject ?? def.defaultSubject,
|
|
1341
|
+
body: override?.body ?? def.defaultBody,
|
|
1342
|
+
customized: Boolean(override?.subject || override?.body)
|
|
1343
|
+
};
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// src/lib/message-events.ts
|
|
1348
|
+
var ORDER_FIELDS = [
|
|
1349
|
+
{ name: "order_number", label: "N\xFAmero do pedido", type: "string", example: "1042" },
|
|
1350
|
+
{ name: "order_total", label: "Total", type: "money", example: "R$ 189,90" },
|
|
1351
|
+
{ 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" },
|
|
1352
|
+
{ name: "customer_name", label: "Cliente", type: "string", example: "Maria Silva" },
|
|
1353
|
+
{ name: "store_name", label: "Loja", type: "string", example: "Sua Loja" }
|
|
1354
|
+
];
|
|
1355
|
+
var TRACKING_FIELD = {
|
|
1356
|
+
name: "tracking_code",
|
|
1357
|
+
label: "C\xF3digo de rastreio",
|
|
1358
|
+
type: "string",
|
|
1359
|
+
example: "BR123456789BR"
|
|
1360
|
+
};
|
|
1361
|
+
var CART_FIELD = {
|
|
1362
|
+
name: "cart_url",
|
|
1363
|
+
label: "Link do carrinho",
|
|
1364
|
+
type: "url",
|
|
1365
|
+
example: "https://sualoja.com.br/carrinho"
|
|
1366
|
+
};
|
|
1367
|
+
var FIELDS_BY_EVENT = {
|
|
1368
|
+
order_shipped: [...ORDER_FIELDS, TRACKING_FIELD],
|
|
1369
|
+
order_delivered: [...ORDER_FIELDS, TRACKING_FIELD],
|
|
1370
|
+
abandoned_cart: [...ORDER_FIELDS, CART_FIELD]
|
|
1371
|
+
};
|
|
1372
|
+
var NOT_AUDITED = /* @__PURE__ */ new Set(["abandoned_cart"]);
|
|
1373
|
+
function buildShopDomainEvents() {
|
|
1374
|
+
return SHOP_EMAIL_EVENTS.map((def) => ({
|
|
1375
|
+
name: `shop.${def.event}`,
|
|
1376
|
+
title: def.title,
|
|
1377
|
+
description: def.description,
|
|
1378
|
+
trigger: def.trigger,
|
|
1379
|
+
icon: def.icon,
|
|
1380
|
+
fields: FIELDS_BY_EVENT[def.event] ?? ORDER_FIELDS,
|
|
1381
|
+
audit: !NOT_AUDITED.has(def.event)
|
|
1382
|
+
}));
|
|
1383
|
+
}
|
|
1384
|
+
var WHATSAPP_EVENTS = /* @__PURE__ */ new Set(["order_confirmation", "order_shipped", "abandoned_cart"]);
|
|
1385
|
+
var WHATSAPP_BODY = {
|
|
1386
|
+
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}}",
|
|
1387
|
+
order_shipped: "Oi {{customer_name}}! Seu pedido #{{order_number}} saiu para entrega \u{1F69A}\n\nRastreio: {{tracking_code}}\n\n\u2014 {{store_name}}",
|
|
1388
|
+
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}}"
|
|
1389
|
+
};
|
|
1390
|
+
function buildShopMessageDefaults() {
|
|
1391
|
+
return SHOP_EMAIL_EVENTS.map((def) => {
|
|
1392
|
+
const whatsapp = WHATSAPP_EVENTS.has(def.event) ? WHATSAPP_BODY[def.event] : void 0;
|
|
1393
|
+
return {
|
|
1394
|
+
eventKey: `shop.${def.event}`,
|
|
1395
|
+
kind: def.kind,
|
|
1396
|
+
channels: whatsapp ? ["email", "whatsapp"].slice() : ["email"],
|
|
1397
|
+
defaults: {
|
|
1398
|
+
email: { subject: def.defaultSubject, body: def.defaultBody },
|
|
1399
|
+
...whatsapp ? { whatsapp: { body: whatsapp } } : {}
|
|
1400
|
+
},
|
|
1401
|
+
// Only email inherits "on by default". WhatsApp bills per message and
|
|
1402
|
+
// needs an approved number — switching itself on would spend someone's
|
|
1403
|
+
// money unasked.
|
|
1404
|
+
defaultEnabled: { email: def.defaultEnabled }
|
|
1405
|
+
};
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
export { EMAIL_PLACEHOLDERS, EMAIL_PREVIEW_SAMPLE, SHOP_EMAIL_EVENTS, SupabaseShopProvider, applyDiscount, buildShopDomainEvents, buildShopMessageDefaults, checkInventory, computeCartTotals, createSupabaseShopProvider, getCustomerOrders, renderEmailTemplate, resolveEmailTemplates, validateDiscount };
|
|
723
1410
|
//# sourceMappingURL=index.js.map
|
|
724
1411
|
//# sourceMappingURL=index.js.map
|