@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
|
@@ -0,0 +1,1170 @@
|
|
|
1
|
+
import { MOCK_PRODUCTS, MOCK_CATEGORIES, MOCK_DISCOUNTS, MOCK_VARIANTS } from './chunk-IKNGCRUM.js';
|
|
2
|
+
import { resolveEntityStore, createMockEntityStore } from './chunk-NWW3WJ7Y.js';
|
|
3
|
+
import { buildRuleContext, computePromotionActions, discountToPromotion } from './chunk-3GERSY5F.js';
|
|
4
|
+
import { resolveEntityDeclaration } from '@fayz-ai/core';
|
|
5
|
+
|
|
6
|
+
// src/data/media.ts
|
|
7
|
+
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "avif", "bmp"]);
|
|
8
|
+
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set(["mp4", "mov", "webm", "m4v", "avi", "mkv"]);
|
|
9
|
+
function mediaKindOf(name, mime) {
|
|
10
|
+
if (mime?.startsWith("image/")) return "image";
|
|
11
|
+
if (mime?.startsWith("video/")) return "video";
|
|
12
|
+
const ext = (name.split(".").pop() ?? "").toLowerCase();
|
|
13
|
+
if (IMAGE_EXTENSIONS.has(ext)) return "image";
|
|
14
|
+
if (VIDEO_EXTENSIONS.has(ext)) return "video";
|
|
15
|
+
return "document";
|
|
16
|
+
}
|
|
17
|
+
function storagePathOf(url) {
|
|
18
|
+
const match = /\/object\/(?:public|sign|authenticated)\/[^/]+\/([^?]+)/.exec(url);
|
|
19
|
+
if (!match) return null;
|
|
20
|
+
try {
|
|
21
|
+
return decodeURIComponent(match[1]);
|
|
22
|
+
} catch {
|
|
23
|
+
return match[1];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function displayNameOf(path) {
|
|
27
|
+
const base = path.split("/").pop() ?? path;
|
|
28
|
+
return base.replace(/^\d{10,}-/, "");
|
|
29
|
+
}
|
|
30
|
+
function productImageToMediaFile(image, productName) {
|
|
31
|
+
const path = storagePathOf(image.url);
|
|
32
|
+
const name = displayNameOf(path ?? image.url);
|
|
33
|
+
return {
|
|
34
|
+
// Fall back to the URL as identity: an image row that points outside our
|
|
35
|
+
// bucket still belongs in the library, it just cannot be deleted by path.
|
|
36
|
+
id: path ?? image.url,
|
|
37
|
+
name,
|
|
38
|
+
url: image.url,
|
|
39
|
+
kind: mediaKindOf(name),
|
|
40
|
+
size: 0,
|
|
41
|
+
extension: (name.split(".").pop() ?? "").toUpperCase(),
|
|
42
|
+
createdAt: image.createdAt ?? "",
|
|
43
|
+
reference: {
|
|
44
|
+
type: "product",
|
|
45
|
+
id: image.productId,
|
|
46
|
+
label: productName?.trim() || "Produto sem nome",
|
|
47
|
+
attachmentId: image.id,
|
|
48
|
+
isCover: image.isPrimary
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function mergeMediaFiles(attached, library) {
|
|
53
|
+
const seen = new Set(attached.map((f) => f.id));
|
|
54
|
+
return [...attached, ...library.filter((f) => !seen.has(f.id))].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/lib/variant-matrix.ts
|
|
58
|
+
var MAX_VARIANT_AXES = 3;
|
|
59
|
+
var MAX_VARIANT_COMBINATIONS = 100;
|
|
60
|
+
function variantOptionsKey(options) {
|
|
61
|
+
return JSON.stringify(
|
|
62
|
+
Object.keys(options).sort().map((key) => [key, options[key]])
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
function normalizeVariantAxes(axes) {
|
|
66
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
67
|
+
const result = [];
|
|
68
|
+
for (const axis of axes) {
|
|
69
|
+
const name = axis.name.trim();
|
|
70
|
+
if (!name || seenNames.has(name)) continue;
|
|
71
|
+
const values = [];
|
|
72
|
+
const seenValues = /* @__PURE__ */ new Set();
|
|
73
|
+
for (const raw of axis.values) {
|
|
74
|
+
const value = raw.trim();
|
|
75
|
+
if (!value || seenValues.has(value)) continue;
|
|
76
|
+
seenValues.add(value);
|
|
77
|
+
values.push(value);
|
|
78
|
+
}
|
|
79
|
+
if (values.length === 0) continue;
|
|
80
|
+
seenNames.add(name);
|
|
81
|
+
result.push({ name, values });
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
function variantCombinations(axes) {
|
|
86
|
+
const clean = normalizeVariantAxes(axes);
|
|
87
|
+
if (clean.length === 0) return [];
|
|
88
|
+
let rows = [{}];
|
|
89
|
+
for (const axis of clean) {
|
|
90
|
+
const next = [];
|
|
91
|
+
for (const row of rows) {
|
|
92
|
+
for (const value of axis.values) next.push({ ...row, [axis.name]: value });
|
|
93
|
+
}
|
|
94
|
+
rows = next;
|
|
95
|
+
}
|
|
96
|
+
return rows;
|
|
97
|
+
}
|
|
98
|
+
function validateVariantAxes(axes) {
|
|
99
|
+
const clean = normalizeVariantAxes(axes);
|
|
100
|
+
if (clean.length > MAX_VARIANT_AXES) {
|
|
101
|
+
return `Um produto aceita no m\xE1ximo ${MAX_VARIANT_AXES} op\xE7\xF5es (ex.: Tamanho, Cor, Material).`;
|
|
102
|
+
}
|
|
103
|
+
const total = clean.reduce((acc, axis) => acc * axis.values.length, 1);
|
|
104
|
+
if (clean.length > 0 && total > MAX_VARIANT_COMBINATIONS) {
|
|
105
|
+
return `Essas op\xE7\xF5es gerariam ${total} variantes, e o limite \xE9 ${MAX_VARIANT_COMBINATIONS}. Reduza os valores ou separe em dois produtos.`;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
function planVariantMatrix(axes, existing) {
|
|
110
|
+
const wanted = variantCombinations(axes);
|
|
111
|
+
const wantedKeys = new Set(wanted.map(variantOptionsKey));
|
|
112
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
113
|
+
let defaultVariant = null;
|
|
114
|
+
for (const variant of existing) {
|
|
115
|
+
if (variant.isDefaultVariant) {
|
|
116
|
+
defaultVariant = variant;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
byKey.set(variantOptionsKey(variant.options), variant);
|
|
120
|
+
}
|
|
121
|
+
const plan = {
|
|
122
|
+
rows: [],
|
|
123
|
+
create: [],
|
|
124
|
+
reactivate: [],
|
|
125
|
+
keep: [],
|
|
126
|
+
archive: [],
|
|
127
|
+
defaultVariant
|
|
128
|
+
};
|
|
129
|
+
for (const [position, options] of wanted.entries()) {
|
|
130
|
+
const variant = byKey.get(variantOptionsKey(options)) ?? null;
|
|
131
|
+
const row = { options, position, variant };
|
|
132
|
+
plan.rows.push(row);
|
|
133
|
+
if (!variant) plan.create.push(row);
|
|
134
|
+
else if (variant.status === "archived") plan.reactivate.push(row);
|
|
135
|
+
else plan.keep.push(row);
|
|
136
|
+
}
|
|
137
|
+
for (const [key, variant] of byKey) {
|
|
138
|
+
if (!wantedKeys.has(key) && variant.status === "active") plan.archive.push(variant);
|
|
139
|
+
}
|
|
140
|
+
return plan;
|
|
141
|
+
}
|
|
142
|
+
function hasOnlyDefaultVariant(variants) {
|
|
143
|
+
const active = variants.filter((variant) => variant.status === "active");
|
|
144
|
+
return active.length === 1 && Boolean(active[0].isDefaultVariant);
|
|
145
|
+
}
|
|
146
|
+
function leavesNothingSellable(plan) {
|
|
147
|
+
const willHaveReal = plan.keep.length + plan.reactivate.length + plan.create.length;
|
|
148
|
+
return willHaveReal === 0 && plan.defaultVariant === null;
|
|
149
|
+
}
|
|
150
|
+
function axesFromOptions(options) {
|
|
151
|
+
return [...options].sort((a, b) => a.position - b.position).map((option) => ({ name: option.name, values: [...option.values] }));
|
|
152
|
+
}
|
|
153
|
+
async function applyVariantMatrix(provider, productId, axes, existing) {
|
|
154
|
+
const refusal = validateVariantAxes(axes);
|
|
155
|
+
if (refusal) throw new Error(refusal);
|
|
156
|
+
if (!provider.saveProductOptions || !provider.createProductVariant || !provider.updateProductVariant) {
|
|
157
|
+
throw new Error("Este provider n\xE3o administra variantes.");
|
|
158
|
+
}
|
|
159
|
+
const clean = normalizeVariantAxes(axes);
|
|
160
|
+
const plan = planVariantMatrix(clean, existing);
|
|
161
|
+
await provider.saveProductOptions(productId, clean);
|
|
162
|
+
for (const row of plan.create) {
|
|
163
|
+
await provider.createProductVariant(productId, { options: row.options, position: row.position });
|
|
164
|
+
}
|
|
165
|
+
for (const row of plan.reactivate) {
|
|
166
|
+
await provider.updateProductVariant(row.variant.id, { status: "active", position: row.position });
|
|
167
|
+
}
|
|
168
|
+
for (const row of plan.keep) {
|
|
169
|
+
if (row.variant.position !== row.position) {
|
|
170
|
+
await provider.updateProductVariant(row.variant.id, { position: row.position });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
for (const variant of plan.archive) {
|
|
174
|
+
await (provider.archiveProductVariant?.(variant.id) ?? provider.updateProductVariant(variant.id, { status: "archived" }));
|
|
175
|
+
}
|
|
176
|
+
const real = plan.create.length + plan.reactivate.length + plan.keep.length;
|
|
177
|
+
if (plan.defaultVariant) {
|
|
178
|
+
if (real > 0 && plan.defaultVariant.status === "active") {
|
|
179
|
+
await provider.updateProductVariant(plan.defaultVariant.id, { status: "archived" });
|
|
180
|
+
} else if (real === 0 && plan.defaultVariant.status === "archived") {
|
|
181
|
+
await provider.updateProductVariant(plan.defaultVariant.id, { status: "active" });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return plan;
|
|
185
|
+
}
|
|
186
|
+
function formatVariantCombination(options, axes) {
|
|
187
|
+
const order = axes?.map((axis) => axis.name) ?? Object.keys(options).sort();
|
|
188
|
+
return order.filter((name) => options[name] != null).map((name) => `${name}: ${options[name]}`).join(" \xB7 ");
|
|
189
|
+
}
|
|
190
|
+
function uid() {
|
|
191
|
+
return Math.random().toString(36).slice(2);
|
|
192
|
+
}
|
|
193
|
+
function now() {
|
|
194
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
195
|
+
}
|
|
196
|
+
var ORDERS_KEY = "fayz.shop.mock.orders.v1";
|
|
197
|
+
var CUSTOMERS_KEY = "fayz.shop.mock.customers.v1";
|
|
198
|
+
var ENQUIRIES_KEY = "fayz.shop.mock.enquiries.v1";
|
|
199
|
+
var CHANNELS_KEY = "fayz.shop.mock.channels.v1";
|
|
200
|
+
var SETTINGS_KEY = "fayz.shop.mock.settings.v1";
|
|
201
|
+
var SHIPPING_SETTINGS_KEY = "fayz.shop.mock.shipping-settings.v1";
|
|
202
|
+
var DEFAULT_CHANNELS = [
|
|
203
|
+
{ id: "online-store", tenantId: "mock", kind: "online_store", label: "Loja Online", url: null, enabled: true, position: 0, createdAt: now(), updatedAt: now() }
|
|
204
|
+
];
|
|
205
|
+
function loadStored(key) {
|
|
206
|
+
try {
|
|
207
|
+
if (typeof localStorage === "undefined") return [];
|
|
208
|
+
const raw = localStorage.getItem(key);
|
|
209
|
+
return raw ? JSON.parse(raw) : [];
|
|
210
|
+
} catch {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function store(key, value) {
|
|
215
|
+
try {
|
|
216
|
+
if (typeof localStorage === "undefined") return;
|
|
217
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
var COLLECTIONS_KEY = "fayz.shop.mock.collections.v1";
|
|
222
|
+
var GIFT_CARDS_KEY = "fayz.shop.mock.gift-cards.v1";
|
|
223
|
+
var EMAIL_TEMPLATES_KEY = "fayz.shop.mock.email-templates.v1";
|
|
224
|
+
var CUSTOMER_GROUPS_KEY = "fayz.shop.mock.customer-groups.v1";
|
|
225
|
+
var CUSTOMER_SEGMENTS_KEY = "fayz.shop.mock.customer-segments.v1";
|
|
226
|
+
function mockGiftCardCode() {
|
|
227
|
+
const alphabet = "ABCDEFGHJKLMNPQRTUVWXY2346789";
|
|
228
|
+
const pick = () => alphabet[Math.floor(Math.random() * alphabet.length)];
|
|
229
|
+
const block = () => Array.from({ length: 4 }, pick).join("");
|
|
230
|
+
return `${block()}-${block()}-${block()}`;
|
|
231
|
+
}
|
|
232
|
+
function mockSlugify(name) {
|
|
233
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
234
|
+
}
|
|
235
|
+
var MockShopProvider = class {
|
|
236
|
+
constructor(seed) {
|
|
237
|
+
this.collections = loadStored(COLLECTIONS_KEY);
|
|
238
|
+
this.giftCards = loadStored(GIFT_CARDS_KEY);
|
|
239
|
+
this.emailTemplates = loadStored(EMAIL_TEMPLATES_KEY);
|
|
240
|
+
this.customerGroups = loadStored(CUSTOMER_GROUPS_KEY);
|
|
241
|
+
this.customerSegments = loadStored(CUSTOMER_SEGMENTS_KEY);
|
|
242
|
+
this.orders = loadStored(ORDERS_KEY);
|
|
243
|
+
this.customers = loadStored(CUSTOMERS_KEY);
|
|
244
|
+
this.enquiries = loadStored(ENQUIRIES_KEY);
|
|
245
|
+
this.channels = (() => {
|
|
246
|
+
const stored = loadStored(CHANNELS_KEY);
|
|
247
|
+
return stored.length ? stored : DEFAULT_CHANNELS.map((c) => ({ ...c }));
|
|
248
|
+
})();
|
|
249
|
+
this.entityStores = /* @__PURE__ */ new Map();
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
// Checkout sessions
|
|
252
|
+
//
|
|
253
|
+
// Mirrors the live inversion: the session holds the cart, and the ORDER is
|
|
254
|
+
// only written when the settle says the money is in. Here the "provider" is a
|
|
255
|
+
// clock — the second poll pays — which is what lets the four-step checkout be
|
|
256
|
+
// walked end to end without an acquirer, in demo stores and in the e2e suite.
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
this.checkoutSessions = {};
|
|
259
|
+
this.products = (seed?.products ?? MOCK_PRODUCTS).map((p) => ({ ...p }));
|
|
260
|
+
this.categories = (seed?.categories ?? MOCK_CATEGORIES).map((c) => ({ ...c }));
|
|
261
|
+
this.discounts = (seed?.discounts ?? MOCK_DISCOUNTS).map((d) => ({ ...d }));
|
|
262
|
+
this.variantSets = Object.fromEntries(
|
|
263
|
+
Object.entries(seed?.variants ?? (seed?.products ? {} : MOCK_VARIANTS)).map(([productId, set]) => [
|
|
264
|
+
productId,
|
|
265
|
+
{ options: set.options.map((o) => ({ ...o, values: [...o.values] })), variants: set.variants.map((v) => ({ ...v, options: { ...v.options } })) }
|
|
266
|
+
])
|
|
267
|
+
);
|
|
268
|
+
this.promotions = (seed?.promotions ?? []).map((p) => ({ ...p }));
|
|
269
|
+
this.entitySeed = seed?.entities ?? {};
|
|
270
|
+
}
|
|
271
|
+
/** The I5 indirection: a promoted (registered) store wins; otherwise the
|
|
272
|
+
* entity gets a lazily-built in-memory store seeded from the mock seed.
|
|
273
|
+
* Lazy on purpose — the seed is validated against the declaration, which
|
|
274
|
+
* the app may register after constructing the provider. */
|
|
275
|
+
entityStore(entity) {
|
|
276
|
+
resolveEntityDeclaration(entity);
|
|
277
|
+
return resolveEntityStore(entity, (key) => {
|
|
278
|
+
let store2 = this.entityStores.get(key);
|
|
279
|
+
if (!store2) {
|
|
280
|
+
store2 = createMockEntityStore(key, { seed: this.entitySeed[key] });
|
|
281
|
+
this.entityStores.set(key, store2);
|
|
282
|
+
}
|
|
283
|
+
return store2;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
// Re-read before mutating: HMR can re-instantiate the provider mid-session
|
|
287
|
+
// and a stale in-memory copy must not clobber persisted records.
|
|
288
|
+
syncOrders() {
|
|
289
|
+
this.orders = loadStored(ORDERS_KEY);
|
|
290
|
+
}
|
|
291
|
+
syncCustomers() {
|
|
292
|
+
this.customers = loadStored(CUSTOMERS_KEY);
|
|
293
|
+
}
|
|
294
|
+
syncEnquiries() {
|
|
295
|
+
this.enquiries = loadStored(ENQUIRIES_KEY);
|
|
296
|
+
}
|
|
297
|
+
syncChannels() {
|
|
298
|
+
const stored = loadStored(CHANNELS_KEY);
|
|
299
|
+
if (stored.length) this.channels = stored;
|
|
300
|
+
}
|
|
301
|
+
persistOrders() {
|
|
302
|
+
store(ORDERS_KEY, this.orders);
|
|
303
|
+
}
|
|
304
|
+
persistCustomers() {
|
|
305
|
+
store(CUSTOMERS_KEY, this.customers);
|
|
306
|
+
}
|
|
307
|
+
persistEnquiries() {
|
|
308
|
+
store(ENQUIRIES_KEY, this.enquiries);
|
|
309
|
+
}
|
|
310
|
+
persistChannels() {
|
|
311
|
+
store(CHANNELS_KEY, this.channels);
|
|
312
|
+
}
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// Products
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
async listProducts(opts) {
|
|
317
|
+
let list = [...this.products];
|
|
318
|
+
if (opts?.status) list = list.filter((p) => p.status === opts.status);
|
|
319
|
+
if (opts?.categoryId) list = list.filter((p) => p.categoryId === opts.categoryId);
|
|
320
|
+
if (opts?.slug) list = list.filter((p) => p.slug === opts.slug);
|
|
321
|
+
if (opts?.search) {
|
|
322
|
+
const q = opts.search.toLowerCase();
|
|
323
|
+
list = list.filter(
|
|
324
|
+
(p) => p.name.toLowerCase().includes(q) || (p.description ?? "").toLowerCase().includes(q) || (p.sku ?? "").toLowerCase().includes(q)
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
const orderBy = opts?.orderBy ?? "sort_order";
|
|
328
|
+
const dir = opts?.order === "desc" ? -1 : 1;
|
|
329
|
+
if (opts?.search && !opts.orderBy) {
|
|
330
|
+
const q = opts.search.toLowerCase();
|
|
331
|
+
const rank = (p) => p.name.toLowerCase().startsWith(q) ? 0 : p.name.toLowerCase().includes(q) ? 1 : 2;
|
|
332
|
+
list.sort((a, b) => rank(a) - rank(b) || a.sortOrder - b.sortOrder);
|
|
333
|
+
} else {
|
|
334
|
+
list.sort((a, b) => {
|
|
335
|
+
switch (orderBy) {
|
|
336
|
+
case "name":
|
|
337
|
+
return a.name.localeCompare(b.name) * dir;
|
|
338
|
+
case "price":
|
|
339
|
+
return (a.price - b.price) * dir;
|
|
340
|
+
case "created_at":
|
|
341
|
+
return a.createdAt.localeCompare(b.createdAt) * dir;
|
|
342
|
+
default:
|
|
343
|
+
return (a.sortOrder - b.sortOrder) * dir;
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
const offset = opts?.offset ?? 0;
|
|
348
|
+
if (offset || opts?.limit) list = list.slice(offset, opts?.limit ? offset + opts.limit : void 0);
|
|
349
|
+
return list;
|
|
350
|
+
}
|
|
351
|
+
async getProduct(id) {
|
|
352
|
+
return this.products.find((p) => p.id === id) ?? null;
|
|
353
|
+
}
|
|
354
|
+
/** Option axes + variants (spine M5) — active variants only, like the 0028
|
|
355
|
+
* storefront RLS. Answers empty sets for a variantless product. */
|
|
356
|
+
async getProductVariants(productId, opts) {
|
|
357
|
+
const set = this.variantSets[productId];
|
|
358
|
+
if (!set) return { options: [], variants: [] };
|
|
359
|
+
return {
|
|
360
|
+
options: set.options.map((o) => ({ ...o, values: [...o.values] })).sort((a, b) => a.position - b.position),
|
|
361
|
+
variants: set.variants.filter((v) => opts?.includeArchived || v.status === "active").map((v) => ({ ...v, options: { ...v.options } })).sort((a, b) => a.position - b.position)
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
// --- The variant admin ------------------------------------------------------
|
|
365
|
+
// Modelled on the SQL rather than on convenience: 0028's UNIQUE
|
|
366
|
+
// (product_id, options), 0037's default-variant triggers and 0036's mirror all
|
|
367
|
+
// have a counterpart here, so a test that passes against this mock is
|
|
368
|
+
// asserting the real semantics and not a friendlier imitation of them.
|
|
369
|
+
variantSet(productId) {
|
|
370
|
+
var _a;
|
|
371
|
+
(_a = this.variantSets)[productId] ?? (_a[productId] = { options: [], variants: [] });
|
|
372
|
+
return this.variantSets[productId];
|
|
373
|
+
}
|
|
374
|
+
async saveProductOptions(productId, input) {
|
|
375
|
+
const set = this.variantSet(productId);
|
|
376
|
+
const existing = new Map(set.options.map((o) => [o.name, o]));
|
|
377
|
+
set.options = input.map((axis, position) => ({
|
|
378
|
+
id: existing.get(axis.name)?.id ?? uid(),
|
|
379
|
+
name: axis.name,
|
|
380
|
+
values: [...axis.values],
|
|
381
|
+
position
|
|
382
|
+
}));
|
|
383
|
+
return set.options.map((o) => ({ ...o, values: [...o.values] }));
|
|
384
|
+
}
|
|
385
|
+
async createProductVariant(productId, input) {
|
|
386
|
+
const set = this.variantSet(productId);
|
|
387
|
+
const key = variantOptionsKey(input.options);
|
|
388
|
+
const existing = set.variants.find((v) => variantOptionsKey(v.options) === key);
|
|
389
|
+
if (existing) return this.updateProductVariant(existing.id, { ...input, status: input.status ?? "active" });
|
|
390
|
+
const variant = {
|
|
391
|
+
id: uid(),
|
|
392
|
+
productId,
|
|
393
|
+
options: { ...input.options },
|
|
394
|
+
sku: input.sku ?? null,
|
|
395
|
+
price: input.price ?? null,
|
|
396
|
+
compareAtPrice: input.compareAtPrice ?? null,
|
|
397
|
+
inventoryCount: 0,
|
|
398
|
+
position: input.position ?? set.variants.length,
|
|
399
|
+
status: input.status ?? "active",
|
|
400
|
+
imageId: input.imageId ?? null,
|
|
401
|
+
isDefaultVariant: false,
|
|
402
|
+
weight: input.weight ?? null,
|
|
403
|
+
weightUnit: input.weightUnit ?? null,
|
|
404
|
+
heightCm: input.heightCm ?? null,
|
|
405
|
+
widthCm: input.widthCm ?? null,
|
|
406
|
+
lengthCm: input.lengthCm ?? null
|
|
407
|
+
};
|
|
408
|
+
set.variants.push(variant);
|
|
409
|
+
for (const other of set.variants) {
|
|
410
|
+
if (other.isDefaultVariant && other.status === "active") other.status = "archived";
|
|
411
|
+
}
|
|
412
|
+
this.syncProductFromVariants(productId);
|
|
413
|
+
return { ...variant, options: { ...variant.options } };
|
|
414
|
+
}
|
|
415
|
+
async updateProductVariant(variantId, input) {
|
|
416
|
+
for (const [productId, set] of Object.entries(this.variantSets)) {
|
|
417
|
+
const variant = set.variants.find((v) => v.id === variantId);
|
|
418
|
+
if (!variant) continue;
|
|
419
|
+
for (const [key, value] of Object.entries(input)) {
|
|
420
|
+
if (value !== void 0) variant[key] = value;
|
|
421
|
+
}
|
|
422
|
+
this.syncProductFromVariants(productId);
|
|
423
|
+
return { ...variant, options: { ...variant.options } };
|
|
424
|
+
}
|
|
425
|
+
throw new Error("Variant not found");
|
|
426
|
+
}
|
|
427
|
+
async archiveProductVariant(variantId) {
|
|
428
|
+
await this.updateProductVariant(variantId, { status: "archived" });
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* The 0034→0036 path, compressed: the merchant states a count, the difference
|
|
432
|
+
* is what gets recorded, and inventory_count is the mirror that follows. The
|
|
433
|
+
* ledger itself is MockStockStore's business — what matters here is that no
|
|
434
|
+
* caller ever assigns to inventoryCount.
|
|
435
|
+
*/
|
|
436
|
+
async setVariantStock(variantId, count) {
|
|
437
|
+
const target = Math.max(0, Math.trunc(count));
|
|
438
|
+
for (const [productId, set] of Object.entries(this.variantSets)) {
|
|
439
|
+
const variant = set.variants.find((v) => v.id === variantId);
|
|
440
|
+
if (!variant) continue;
|
|
441
|
+
variant.inventoryCount = target;
|
|
442
|
+
this.syncProductFromVariants(productId);
|
|
443
|
+
return target;
|
|
444
|
+
}
|
|
445
|
+
throw new Error("Variant not found");
|
|
446
|
+
}
|
|
447
|
+
/** 0028's sync trigger: the product row is the SUM of active variant stock. */
|
|
448
|
+
syncProductFromVariants(productId) {
|
|
449
|
+
const product = this.products.find((p) => p.id === productId);
|
|
450
|
+
const set = this.variantSets[productId];
|
|
451
|
+
if (!product || !set) return;
|
|
452
|
+
const active = set.variants.filter((v) => v.status === "active");
|
|
453
|
+
product.inventoryCount = active.reduce((sum, v) => sum + v.inventoryCount, 0);
|
|
454
|
+
}
|
|
455
|
+
async createProduct(input) {
|
|
456
|
+
const p = { id: uid(), tenantId: "mock", name: input.name, slug: input.slug ?? input.name.toLowerCase().replace(/\s+/g, "-"), description: input.description ?? null, price: input.price, compareAtPrice: input.compareAtPrice ?? null, currency: input.currency ?? "BRL", status: input.status ?? "active", inventoryCount: input.inventoryCount ?? 0, sku: input.sku ?? null, sortOrder: input.sortOrder ?? 0, metadata: input.metadata ?? {}, images: [], categoryId: input.categoryId ?? null, categoryName: null, isPhysical: input.isPhysical ?? true, weight: input.weight ?? null, weightUnit: input.weightUnit ?? "kg", handling: input.handling ?? [], createdAt: now(), updatedAt: now() };
|
|
457
|
+
this.products.push(p);
|
|
458
|
+
return p;
|
|
459
|
+
}
|
|
460
|
+
async updateProduct(id, input) {
|
|
461
|
+
const i = this.products.findIndex((p) => p.id === id);
|
|
462
|
+
if (i < 0) throw new Error("Product not found");
|
|
463
|
+
this.products[i] = { ...this.products[i], ...input, updatedAt: now() };
|
|
464
|
+
return this.products[i];
|
|
465
|
+
}
|
|
466
|
+
async deleteProduct(id) {
|
|
467
|
+
this.products = this.products.filter((p) => p.id !== id);
|
|
468
|
+
}
|
|
469
|
+
async uploadProductImage(_productId, _file) {
|
|
470
|
+
throw new Error("Image upload not available in mock mode");
|
|
471
|
+
}
|
|
472
|
+
async deleteProductImage(imageId) {
|
|
473
|
+
this.products = this.products.map((p) => p.images.some((i) => i.id === imageId) ? { ...p, images: p.images.filter((i) => i.id !== imageId) } : p);
|
|
474
|
+
}
|
|
475
|
+
/** The media library, mock-side: no bucket to walk, so the catalogue's own
|
|
476
|
+
* photos ARE the library. Uploading has nowhere to go, which is why
|
|
477
|
+
* uploadMediaFile stays unimplemented and the screen hides its button. */
|
|
478
|
+
async listMediaFiles() {
|
|
479
|
+
const attached = this.products.flatMap((p) => p.images.map((image) => productImageToMediaFile(image, p.name)));
|
|
480
|
+
return mergeMediaFiles(attached, []);
|
|
481
|
+
}
|
|
482
|
+
async createProductEnquiry(input) {
|
|
483
|
+
this.syncEnquiries();
|
|
484
|
+
const ts = now();
|
|
485
|
+
const enquiry = {
|
|
486
|
+
id: uid(),
|
|
487
|
+
tenantId: "mock",
|
|
488
|
+
productId: input.productId ?? null,
|
|
489
|
+
productName: input.productName,
|
|
490
|
+
productSlug: input.productSlug ?? null,
|
|
491
|
+
customerName: input.customerName,
|
|
492
|
+
customerEmail: input.customerEmail,
|
|
493
|
+
customerPhone: input.customerPhone ?? null,
|
|
494
|
+
subject: input.subject ?? null,
|
|
495
|
+
message: input.message,
|
|
496
|
+
sourceUrl: input.sourceUrl ?? null,
|
|
497
|
+
status: "new",
|
|
498
|
+
metadata: input.metadata ?? {},
|
|
499
|
+
createdAt: ts,
|
|
500
|
+
updatedAt: ts
|
|
501
|
+
};
|
|
502
|
+
this.enquiries.unshift(enquiry);
|
|
503
|
+
this.persistEnquiries();
|
|
504
|
+
return enquiry;
|
|
505
|
+
}
|
|
506
|
+
async listProductEnquiries(opts) {
|
|
507
|
+
this.syncEnquiries();
|
|
508
|
+
let list = [...this.enquiries];
|
|
509
|
+
if (opts?.productId) list = list.filter((e) => e.productId === opts.productId);
|
|
510
|
+
if (opts?.customerEmail) {
|
|
511
|
+
const email = opts.customerEmail.toLowerCase();
|
|
512
|
+
list = list.filter((e) => e.customerEmail.toLowerCase() === email);
|
|
513
|
+
}
|
|
514
|
+
if (opts?.status) list = list.filter((e) => e.status === opts.status);
|
|
515
|
+
list.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
516
|
+
const offset = opts?.offset ?? 0;
|
|
517
|
+
if (offset || opts?.limit) list = list.slice(offset, opts?.limit ? offset + opts.limit : void 0);
|
|
518
|
+
return list;
|
|
519
|
+
}
|
|
520
|
+
// ---------------------------------------------------------------------------
|
|
521
|
+
// Categories
|
|
522
|
+
// ---------------------------------------------------------------------------
|
|
523
|
+
async listCategories() {
|
|
524
|
+
return [...this.categories].sort((a, b) => a.sortOrder - b.sortOrder);
|
|
525
|
+
}
|
|
526
|
+
async createCategory(input) {
|
|
527
|
+
const c = { id: uid(), tenantId: "mock", name: input.name, slug: input.slug ?? input.name.toLowerCase().replace(/\s+/g, "-"), description: input.description ?? null, parentId: input.parentId ?? null, sortOrder: input.sortOrder ?? 0, createdAt: now() };
|
|
528
|
+
this.categories.push(c);
|
|
529
|
+
return c;
|
|
530
|
+
}
|
|
531
|
+
async updateCategory(id, input) {
|
|
532
|
+
const i = this.categories.findIndex((c) => c.id === id);
|
|
533
|
+
if (i < 0) throw new Error("Category not found");
|
|
534
|
+
this.categories[i] = { ...this.categories[i], ...input };
|
|
535
|
+
return this.categories[i];
|
|
536
|
+
}
|
|
537
|
+
async deleteCategory(id) {
|
|
538
|
+
this.categories = this.categories.filter((c) => c.id !== id);
|
|
539
|
+
}
|
|
540
|
+
// ---------------------------------------------------------------------------
|
|
541
|
+
// Orders
|
|
542
|
+
// ---------------------------------------------------------------------------
|
|
543
|
+
async listOrders(opts) {
|
|
544
|
+
this.syncOrders();
|
|
545
|
+
let list = [...this.orders];
|
|
546
|
+
if (opts?.status) list = list.filter((o) => o.status === opts.status);
|
|
547
|
+
if (opts?.financialStatus) list = list.filter((o) => o.financialStatus === opts.financialStatus);
|
|
548
|
+
if (opts?.fulfillmentStatus) list = list.filter((o) => o.fulfillmentStatus === opts.fulfillmentStatus);
|
|
549
|
+
if (opts?.customerId) list = list.filter((o) => o.customerId === opts.customerId);
|
|
550
|
+
if (opts?.customerEmail) {
|
|
551
|
+
const email = opts.customerEmail.toLowerCase();
|
|
552
|
+
list = list.filter((o) => (o.customerEmail ?? "").toLowerCase() === email);
|
|
553
|
+
}
|
|
554
|
+
if (opts?.search) {
|
|
555
|
+
const q = opts.search.toLowerCase();
|
|
556
|
+
list = list.filter(
|
|
557
|
+
(o) => (o.customerName ?? "").toLowerCase().includes(q) || (o.customerEmail ?? "").toLowerCase().includes(q) || String(o.orderNumber).includes(q)
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
list.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
561
|
+
if (opts?.limit) list = list.slice(0, opts.limit);
|
|
562
|
+
return list;
|
|
563
|
+
}
|
|
564
|
+
async getOrder(id) {
|
|
565
|
+
this.syncOrders();
|
|
566
|
+
return this.orders.find((o) => o.id === id) ?? null;
|
|
567
|
+
}
|
|
568
|
+
async createOrder(input) {
|
|
569
|
+
this.syncOrders();
|
|
570
|
+
const subtotal = input.items.reduce((s, i) => s + i.quantity * i.unitPrice, 0);
|
|
571
|
+
const discountTotal = input.discountTotal ?? 0;
|
|
572
|
+
const shippingTotal = input.shippingTotal ?? 0;
|
|
573
|
+
const total = Math.round((subtotal - discountTotal + shippingTotal) * 100) / 100;
|
|
574
|
+
const orderId = uid();
|
|
575
|
+
const maxNumber = this.orders.reduce((m, o2) => Math.max(m, o2.orderNumber), 1e3);
|
|
576
|
+
const o = {
|
|
577
|
+
id: orderId,
|
|
578
|
+
tenantId: "mock",
|
|
579
|
+
orderNumber: maxNumber + 1,
|
|
580
|
+
status: "open",
|
|
581
|
+
financialStatus: "pending",
|
|
582
|
+
fulfillmentStatus: "unfulfilled",
|
|
583
|
+
currency: input.currency ?? "BRL",
|
|
584
|
+
subtotal: Math.round(subtotal * 100) / 100,
|
|
585
|
+
taxTotal: 0,
|
|
586
|
+
discountTotal,
|
|
587
|
+
shippingTotal,
|
|
588
|
+
total,
|
|
589
|
+
customerId: input.customerId ?? null,
|
|
590
|
+
customerName: input.customerName ?? null,
|
|
591
|
+
customerEmail: input.customerEmail ?? null,
|
|
592
|
+
discountCode: input.discountCode ?? null,
|
|
593
|
+
notes: input.notes ?? null,
|
|
594
|
+
shippingAddress: null,
|
|
595
|
+
// CreateOrderInput has no payment method — that is a PlaceOrderInput
|
|
596
|
+
// concern; the mock records none rather than inventing one.
|
|
597
|
+
paymentMethodKind: null,
|
|
598
|
+
paidAt: null,
|
|
599
|
+
items: input.items.map((i) => ({ id: uid(), orderId, productId: i.productId ?? null, name: i.name, sku: i.sku ?? null, quantity: i.quantity, unitPrice: i.unitPrice, total: Math.round(i.quantity * i.unitPrice * 100) / 100, imageUrl: i.imageUrl ?? null })),
|
|
600
|
+
createdAt: now(),
|
|
601
|
+
updatedAt: now()
|
|
602
|
+
};
|
|
603
|
+
this.orders.push(o);
|
|
604
|
+
this.persistOrders();
|
|
605
|
+
return o;
|
|
606
|
+
}
|
|
607
|
+
async updateOrder(id, input) {
|
|
608
|
+
this.syncOrders();
|
|
609
|
+
const i = this.orders.findIndex((o) => o.id === id);
|
|
610
|
+
if (i < 0) throw new Error("Order not found");
|
|
611
|
+
this.orders[i] = { ...this.orders[i], ...input, updatedAt: now() };
|
|
612
|
+
this.persistOrders();
|
|
613
|
+
return this.orders[i];
|
|
614
|
+
}
|
|
615
|
+
// Trusted placement (mock parity with the shop_place_order RPC): re-reads
|
|
616
|
+
// prices + stock from the catalog, EVALUATES THE DECLARED PROMOTIONS through
|
|
617
|
+
// the rule engine, validates the legacy discount code, decrements inventory.
|
|
618
|
+
// The browser-supplied prices and totals are never used.
|
|
619
|
+
async placeOrder(input) {
|
|
620
|
+
this.syncOrders();
|
|
621
|
+
this.syncCustomers();
|
|
622
|
+
let customerId = input.customerId ?? null;
|
|
623
|
+
let customerName = input.customer?.name?.trim() || null;
|
|
624
|
+
const email = input.customer?.email?.trim().toLowerCase() || null;
|
|
625
|
+
if (!customerId && email) {
|
|
626
|
+
const existing = this.customers.find((c) => (c.email ?? "").toLowerCase() === email);
|
|
627
|
+
if (existing) {
|
|
628
|
+
customerId = existing.id;
|
|
629
|
+
customerName = customerName ?? `${existing.firstName} ${existing.lastName}`.trim();
|
|
630
|
+
} else {
|
|
631
|
+
const [first, ...rest] = (customerName ?? email).split(" ");
|
|
632
|
+
const created = await this.createCustomer({ firstName: first || email, lastName: rest.join(" "), email });
|
|
633
|
+
customerId = created.id;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const items = [];
|
|
637
|
+
const lineDetails = [];
|
|
638
|
+
const snapshotItems = [];
|
|
639
|
+
let subtotal = 0;
|
|
640
|
+
for (const [index, line] of input.items.entries()) {
|
|
641
|
+
const product = this.products.find((p) => p.id === line.productId);
|
|
642
|
+
if (!product || product.status !== "active") throw new Error(`Product unavailable: ${line.productId}`);
|
|
643
|
+
let variant;
|
|
644
|
+
if (line.variantId) {
|
|
645
|
+
variant = this.variantSets[line.productId]?.variants.find((v) => v.id === line.variantId && v.status === "active");
|
|
646
|
+
if (!variant) throw new Error(`Variant unavailable: ${line.variantId}`);
|
|
647
|
+
if (variant.inventoryCount < line.quantity) throw new Error(`Insufficient stock for ${product.name}`);
|
|
648
|
+
} else if (product.inventoryCount < line.quantity) {
|
|
649
|
+
throw new Error(`Insufficient stock for ${product.name}`);
|
|
650
|
+
}
|
|
651
|
+
const unitPrice = variant?.price ?? product.price;
|
|
652
|
+
const label = line.optionsLabel || (variant ? Object.entries(variant.options).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}: ${v}`).join(" \xB7 ") : "");
|
|
653
|
+
const name = label ? `${product.name} (${label})` : product.name;
|
|
654
|
+
const lineTotal = unitPrice * line.quantity;
|
|
655
|
+
subtotal += lineTotal;
|
|
656
|
+
lineDetails.push({ productId: product.id, categoryId: product.categoryId, lineTotal });
|
|
657
|
+
snapshotItems.push({
|
|
658
|
+
id: `${index}:${line.variantId ?? product.id}`,
|
|
659
|
+
productId: product.id,
|
|
660
|
+
quantity: line.quantity,
|
|
661
|
+
unitPrice,
|
|
662
|
+
categoryId: product.categoryId,
|
|
663
|
+
collectionId: typeof product.metadata?.collection === "string" ? product.metadata.collection : null
|
|
664
|
+
});
|
|
665
|
+
items.push({ productId: product.id, name, sku: variant?.sku ?? product.sku ?? void 0, quantity: line.quantity, unitPrice, imageUrl: product.images[0]?.url });
|
|
666
|
+
}
|
|
667
|
+
subtotal = Math.round(subtotal * 100) / 100;
|
|
668
|
+
let shippingTotal = Math.max(input.shippingTotal ?? 0, 0);
|
|
669
|
+
let discountTotal = 0;
|
|
670
|
+
let appliedCode = null;
|
|
671
|
+
const entered = input.discountCode ? [input.discountCode.trim()] : [];
|
|
672
|
+
const cart = {
|
|
673
|
+
items: snapshotItems,
|
|
674
|
+
shippingTotal,
|
|
675
|
+
customer: { email: input.customer?.email ?? null },
|
|
676
|
+
codes: entered,
|
|
677
|
+
currency: input.currency
|
|
678
|
+
};
|
|
679
|
+
const context = await buildRuleContext(cart);
|
|
680
|
+
const actions = computePromotionActions(cart, this.promotions, { context });
|
|
681
|
+
let promotionItemDiscount = 0;
|
|
682
|
+
let promotionShippingDiscount = 0;
|
|
683
|
+
const firedPromotionIds = /* @__PURE__ */ new Set();
|
|
684
|
+
const firedCodes = /* @__PURE__ */ new Set();
|
|
685
|
+
for (const action of actions) {
|
|
686
|
+
firedPromotionIds.add(action.promotionId);
|
|
687
|
+
if (action.code) firedCodes.add(action.code.trim().toUpperCase());
|
|
688
|
+
if (action.action === "addItemDiscount") promotionItemDiscount += action.amount;
|
|
689
|
+
else if (action.action === "addShippingDiscount") promotionShippingDiscount += action.amount;
|
|
690
|
+
else if (action.action === "addFreeItem") {
|
|
691
|
+
const granted = this.products.find((p) => p.id === action.productId);
|
|
692
|
+
if (granted) {
|
|
693
|
+
items.push({ productId: granted.id, name: granted.name, sku: granted.sku ?? void 0, quantity: action.quantity, unitPrice: 0, imageUrl: granted.images[0]?.url });
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
for (const promotion of this.promotions) {
|
|
698
|
+
if (firedPromotionIds.has(promotion.id)) promotion.timesUsed = (promotion.timesUsed ?? 0) + 1;
|
|
699
|
+
}
|
|
700
|
+
discountTotal += Math.round(promotionItemDiscount * 100) / 100;
|
|
701
|
+
shippingTotal = Math.max(0, Math.round((shippingTotal - promotionShippingDiscount) * 100) / 100);
|
|
702
|
+
if (input.discountCode && firedCodes.has(input.discountCode.trim().toUpperCase())) {
|
|
703
|
+
appliedCode = input.discountCode.trim().toUpperCase();
|
|
704
|
+
}
|
|
705
|
+
if (input.discountCode && !appliedCode) {
|
|
706
|
+
const code = input.discountCode.trim().toLowerCase();
|
|
707
|
+
const d = this.discounts.find((x) => (x.code ?? "").toLowerCase() === code);
|
|
708
|
+
const ts = now();
|
|
709
|
+
const meetsMin = !d?.minSubtotal || subtotal >= d.minSubtotal;
|
|
710
|
+
const valid = !!d && d.status === "active" && d.startsAt <= ts && (!d.endsAt || d.endsAt >= ts) && (d.usageLimit == null || d.timesUsed < d.usageLimit) && meetsMin;
|
|
711
|
+
if (valid && d) {
|
|
712
|
+
appliedCode = d.code;
|
|
713
|
+
let eligible = subtotal;
|
|
714
|
+
if (d.appliesTo === "products") {
|
|
715
|
+
eligible = lineDetails.filter((l) => d.productIds.includes(l.productId)).reduce((s, l) => s + l.lineTotal, 0);
|
|
716
|
+
} else if (d.appliesTo === "categories") {
|
|
717
|
+
eligible = lineDetails.filter((l) => l.categoryId && d.categoryIds.includes(l.categoryId)).reduce((s, l) => s + l.lineTotal, 0);
|
|
718
|
+
}
|
|
719
|
+
eligible = Math.round(eligible * 100) / 100;
|
|
720
|
+
if (d.type === "percentage") discountTotal += Math.round(eligible * d.value) / 100;
|
|
721
|
+
else if (d.type === "fixed_amount") discountTotal += Math.min(d.value, eligible);
|
|
722
|
+
else if (d.type === "free_shipping") shippingTotal = 0;
|
|
723
|
+
d.timesUsed += 1;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
discountTotal = Math.round(Math.min(Math.max(discountTotal, 0), subtotal) * 100) / 100;
|
|
727
|
+
const order = await this.createOrder({
|
|
728
|
+
customerId: customerId ?? void 0,
|
|
729
|
+
customerName: customerName ?? void 0,
|
|
730
|
+
customerEmail: input.customer?.email ?? void 0,
|
|
731
|
+
currency: input.currency,
|
|
732
|
+
notes: input.notes,
|
|
733
|
+
discountCode: appliedCode ?? void 0,
|
|
734
|
+
discountTotal,
|
|
735
|
+
shippingTotal,
|
|
736
|
+
items
|
|
737
|
+
});
|
|
738
|
+
for (const line of input.items) {
|
|
739
|
+
const product = this.products.find((p) => p.id === line.productId);
|
|
740
|
+
if (!product) continue;
|
|
741
|
+
const variant = line.variantId ? this.variantSets[line.productId]?.variants.find((v) => v.id === line.variantId) : void 0;
|
|
742
|
+
if (variant) {
|
|
743
|
+
variant.inventoryCount = Math.max(0, variant.inventoryCount - line.quantity);
|
|
744
|
+
product.inventoryCount = this.variantSets[line.productId].variants.filter((v) => v.status === "active").reduce((sum, v) => sum + v.inventoryCount, 0);
|
|
745
|
+
} else {
|
|
746
|
+
product.inventoryCount = Math.max(0, product.inventoryCount - line.quantity);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return order;
|
|
750
|
+
}
|
|
751
|
+
async openCheckoutSession(input) {
|
|
752
|
+
const sessionId = uid();
|
|
753
|
+
this.checkoutSessions[sessionId] = { input, polls: 0 };
|
|
754
|
+
let total = 0;
|
|
755
|
+
for (const line of input.items) {
|
|
756
|
+
const product = this.products.find((p) => p.id === line.productId);
|
|
757
|
+
if (!product) continue;
|
|
758
|
+
const variant = line.variantId ? this.variantSets[line.productId]?.variants.find((v) => v.id === line.variantId) : void 0;
|
|
759
|
+
total += (variant?.price ?? product.price) * line.quantity;
|
|
760
|
+
}
|
|
761
|
+
return {
|
|
762
|
+
sessionId,
|
|
763
|
+
total: Math.round(total * 100) / 100,
|
|
764
|
+
state: "open",
|
|
765
|
+
charge: {
|
|
766
|
+
// Shaped like a real EMV payload so the copy button and the layout are
|
|
767
|
+
// exercised, not a placeholder that fits in one line.
|
|
768
|
+
emv: `00020126580014BR.GOV.BCB.PIX0136${sessionId}520400005303986540${total.toFixed(2)}5802BR6304MOCK`,
|
|
769
|
+
qrcodeBase64: null,
|
|
770
|
+
expiresAt: new Date(Date.now() + 30 * 6e4).toISOString()
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
async settleCheckoutSession(sessionId) {
|
|
775
|
+
const session = this.checkoutSessions[sessionId];
|
|
776
|
+
if (!session) return { state: "expired", reason: "Sess\xE3o de checkout n\xE3o encontrada." };
|
|
777
|
+
if (session.orderId) return { state: "paid", orderId: session.orderId };
|
|
778
|
+
session.polls += 1;
|
|
779
|
+
if (session.polls < 2) return { state: "open" };
|
|
780
|
+
const order = await this.placeOrder({
|
|
781
|
+
items: session.input.items,
|
|
782
|
+
customer: session.input.customer,
|
|
783
|
+
discountCode: session.input.discountCode ?? void 0,
|
|
784
|
+
shippingAddress: session.input.shippingAddress
|
|
785
|
+
});
|
|
786
|
+
await this.updateOrder(order.id, { financialStatus: "paid" });
|
|
787
|
+
session.orderId = order.id;
|
|
788
|
+
return { state: "paid", orderId: order.id };
|
|
789
|
+
}
|
|
790
|
+
// ---------------------------------------------------------------------------
|
|
791
|
+
// Customers
|
|
792
|
+
// ---------------------------------------------------------------------------
|
|
793
|
+
async listCustomers(opts) {
|
|
794
|
+
this.syncCustomers();
|
|
795
|
+
let list = [...this.customers];
|
|
796
|
+
if (opts?.search) list = list.filter((c) => `${c.firstName} ${c.lastName} ${c.email}`.toLowerCase().includes(opts.search.toLowerCase()));
|
|
797
|
+
if (opts?.limit) list = list.slice(0, opts.limit);
|
|
798
|
+
return list;
|
|
799
|
+
}
|
|
800
|
+
async getCustomer(id) {
|
|
801
|
+
this.syncCustomers();
|
|
802
|
+
return this.customers.find((c) => c.id === id) ?? null;
|
|
803
|
+
}
|
|
804
|
+
async createCustomer(input) {
|
|
805
|
+
this.syncCustomers();
|
|
806
|
+
const c = { id: uid(), tenantId: "mock", firstName: input.firstName, lastName: input.lastName ?? "", email: input.email ?? null, phone: input.phone ?? null, document: input.document ?? null, notes: input.notes ?? null, ordersCount: 0, totalSpent: 0, createdAt: now(), updatedAt: now() };
|
|
807
|
+
this.customers.push(c);
|
|
808
|
+
this.persistCustomers();
|
|
809
|
+
return c;
|
|
810
|
+
}
|
|
811
|
+
async updateCustomer(id, input) {
|
|
812
|
+
this.syncCustomers();
|
|
813
|
+
const i = this.customers.findIndex((c) => c.id === id);
|
|
814
|
+
if (i < 0) throw new Error("Customer not found");
|
|
815
|
+
this.customers[i] = { ...this.customers[i], ...input, updatedAt: now() };
|
|
816
|
+
this.persistCustomers();
|
|
817
|
+
return this.customers[i];
|
|
818
|
+
}
|
|
819
|
+
async deleteCustomer(id) {
|
|
820
|
+
this.syncCustomers();
|
|
821
|
+
this.customers = this.customers.filter((c) => c.id !== id);
|
|
822
|
+
this.persistCustomers();
|
|
823
|
+
}
|
|
824
|
+
async resolveCustomer(input) {
|
|
825
|
+
this.syncCustomers();
|
|
826
|
+
const email = input.email.trim().toLowerCase();
|
|
827
|
+
const existing = this.customers.find((c) => (c.email ?? "").toLowerCase() === email);
|
|
828
|
+
if (existing) return existing;
|
|
829
|
+
const [first, ...rest] = (input.name?.trim() || email).split(/\s+/);
|
|
830
|
+
return this.createCustomer({ firstName: first || email, lastName: rest.join(" "), email });
|
|
831
|
+
}
|
|
832
|
+
// ---------------------------------------------------------------------------
|
|
833
|
+
// Promotions (spine M6) — the mock provider IS the reference evaluator, so
|
|
834
|
+
// tests and pilot fixtures exercise the rule engine without a database.
|
|
835
|
+
// Seeded promotions ride as-is; legacy discounts ride through the adapter
|
|
836
|
+
// (a percent code = one eligibility rule on code + percentage method).
|
|
837
|
+
// ---------------------------------------------------------------------------
|
|
838
|
+
async evaluatePromotions(input) {
|
|
839
|
+
const promotions = [
|
|
840
|
+
...this.promotions,
|
|
841
|
+
...this.discounts.map(discountToPromotion).filter((p) => p !== null)
|
|
842
|
+
];
|
|
843
|
+
const context = await buildRuleContext(input.cart);
|
|
844
|
+
return computePromotionActions(input.cart, promotions, { context });
|
|
845
|
+
}
|
|
846
|
+
// ---------------------------------------------------------------------------
|
|
847
|
+
// Discounts
|
|
848
|
+
// ---------------------------------------------------------------------------
|
|
849
|
+
async listDiscounts(opts) {
|
|
850
|
+
let list = [...this.discounts];
|
|
851
|
+
if (opts?.status) list = list.filter((d) => d.status === opts.status);
|
|
852
|
+
if (opts?.type) list = list.filter((d) => d.type === opts.type);
|
|
853
|
+
if (opts?.search) list = list.filter((d) => d.title.toLowerCase().includes(opts.search.toLowerCase()));
|
|
854
|
+
if (opts?.limit) list = list.slice(0, opts.limit);
|
|
855
|
+
return list;
|
|
856
|
+
}
|
|
857
|
+
async getDiscount(id) {
|
|
858
|
+
return this.discounts.find((d) => d.id === id) ?? null;
|
|
859
|
+
}
|
|
860
|
+
async createDiscount(input) {
|
|
861
|
+
const d = { id: uid(), tenantId: "mock", title: input.title, code: input.code ?? null, type: input.type, method: input.method ?? "code", value: input.value, usageLimit: input.usageLimit ?? null, oncePerCustomer: input.oncePerCustomer ?? false, minSubtotal: input.minSubtotal ?? null, appliesTo: input.appliesTo ?? "all", productIds: input.productIds ?? [], categoryIds: input.categoryIds ?? [], startsAt: input.startsAt ?? now(), endsAt: input.endsAt ?? null, status: input.status ?? "active", timesUsed: 0, createdAt: now(), updatedAt: now() };
|
|
862
|
+
this.discounts.push(d);
|
|
863
|
+
return d;
|
|
864
|
+
}
|
|
865
|
+
async updateDiscount(id, input) {
|
|
866
|
+
const i = this.discounts.findIndex((d) => d.id === id);
|
|
867
|
+
if (i < 0) throw new Error("Discount not found");
|
|
868
|
+
this.discounts[i] = { ...this.discounts[i], ...input, updatedAt: now() };
|
|
869
|
+
return this.discounts[i];
|
|
870
|
+
}
|
|
871
|
+
async deleteDiscount(id) {
|
|
872
|
+
this.discounts = this.discounts.filter((d) => d.id !== id);
|
|
873
|
+
}
|
|
874
|
+
// ---------------------------------------------------------------------------
|
|
875
|
+
// Sales channels
|
|
876
|
+
// ---------------------------------------------------------------------------
|
|
877
|
+
async listSalesChannels() {
|
|
878
|
+
this.syncChannels();
|
|
879
|
+
return [...this.channels].sort((a, b) => a.position - b.position);
|
|
880
|
+
}
|
|
881
|
+
async createSalesChannel(input) {
|
|
882
|
+
this.syncChannels();
|
|
883
|
+
const c = { id: uid(), tenantId: "mock", kind: input.kind, label: input.label, url: input.url ?? null, enabled: input.enabled ?? true, position: input.position ?? this.channels.length, createdAt: now(), updatedAt: now() };
|
|
884
|
+
this.channels.push(c);
|
|
885
|
+
this.persistChannels();
|
|
886
|
+
return c;
|
|
887
|
+
}
|
|
888
|
+
async updateSalesChannel(id, input) {
|
|
889
|
+
this.syncChannels();
|
|
890
|
+
const i = this.channels.findIndex((c) => c.id === id);
|
|
891
|
+
if (i < 0) throw new Error("Sales channel not found");
|
|
892
|
+
this.channels[i] = { ...this.channels[i], ...input, updatedAt: now() };
|
|
893
|
+
this.persistChannels();
|
|
894
|
+
return this.channels[i];
|
|
895
|
+
}
|
|
896
|
+
async deleteSalesChannel(id) {
|
|
897
|
+
this.syncChannels();
|
|
898
|
+
this.channels = this.channels.filter((c) => c.id !== id);
|
|
899
|
+
this.persistChannels();
|
|
900
|
+
}
|
|
901
|
+
// ---------------------------------------------------------------------------
|
|
902
|
+
// Store policy + delivery fallback
|
|
903
|
+
//
|
|
904
|
+
// A single object behind a list-shaped store helper, so the same loadStored /
|
|
905
|
+
// store pair serves it. Absent = null, NOT an object of defaults: "nothing
|
|
906
|
+
// decided" is a state the callers are written to handle, and manufacturing
|
|
907
|
+
// defaults here would hide the one case the live provider really has.
|
|
908
|
+
// ---------------------------------------------------------------------------
|
|
909
|
+
async getStoreSettings() {
|
|
910
|
+
return loadStored(SETTINGS_KEY)[0] ?? null;
|
|
911
|
+
}
|
|
912
|
+
async updateStoreSettings(input) {
|
|
913
|
+
const next = { ...await this.getStoreSettings() ?? {}, ...input };
|
|
914
|
+
store(SETTINGS_KEY, [next]);
|
|
915
|
+
return next;
|
|
916
|
+
}
|
|
917
|
+
/** In mock mode there is no publication gate to fail, so the shopper's read
|
|
918
|
+
* and the merchant's are the same row. */
|
|
919
|
+
async getPublicStoreSettings() {
|
|
920
|
+
return this.getStoreSettings();
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Mock mode has no delivery zones at all, so nothing is refused and no zone is
|
|
924
|
+
* the store's own. Answered rather than omitted so a screen developed against
|
|
925
|
+
* the mock sees the same shape it will see live — an absent method would make
|
|
926
|
+
* the caller take its "provider cannot check" branch and never exercise this.
|
|
927
|
+
*/
|
|
928
|
+
async checkDelivery() {
|
|
929
|
+
return { refused: false, ownZone: false };
|
|
930
|
+
}
|
|
931
|
+
async getShippingSettings() {
|
|
932
|
+
return loadStored(SHIPPING_SETTINGS_KEY)[0] ?? null;
|
|
933
|
+
}
|
|
934
|
+
async updateShippingSettings(input) {
|
|
935
|
+
const current = await this.getShippingSettings();
|
|
936
|
+
const next = {
|
|
937
|
+
flatRate: input.flatRate ?? current?.flatRate ?? 0,
|
|
938
|
+
freeAbove: input.freeAbove !== void 0 ? input.freeAbove : current?.freeAbove ?? null,
|
|
939
|
+
currency: input.currency ?? current?.currency ?? "BRL"
|
|
940
|
+
};
|
|
941
|
+
store(SHIPPING_SETTINGS_KEY, [next]);
|
|
942
|
+
return next;
|
|
943
|
+
}
|
|
944
|
+
// ---------------------------------------------------------------------------
|
|
945
|
+
// Custom entities (spine M4) — same EntityStore port as the live provider.
|
|
946
|
+
// ---------------------------------------------------------------------------
|
|
947
|
+
async listEntityRecords(entity, options) {
|
|
948
|
+
return this.entityStore(entity).list(options);
|
|
949
|
+
}
|
|
950
|
+
async getEntityRecord(entity, id) {
|
|
951
|
+
return this.entityStore(entity).get(id);
|
|
952
|
+
}
|
|
953
|
+
async getEntityRecordBySlug(entity, slug) {
|
|
954
|
+
return this.entityStore(entity).getBySlug(slug);
|
|
955
|
+
}
|
|
956
|
+
async createEntityRecord(entity, input) {
|
|
957
|
+
return this.entityStore(entity).create(input);
|
|
958
|
+
}
|
|
959
|
+
async updateEntityRecord(entity, id, input) {
|
|
960
|
+
return this.entityStore(entity).update(id, input);
|
|
961
|
+
}
|
|
962
|
+
async deleteEntityRecord(entity, id) {
|
|
963
|
+
return this.entityStore(entity).delete(id);
|
|
964
|
+
}
|
|
965
|
+
// Coleções, grupos/segmentos, vale-presente e e-mails — paridade com o
|
|
966
|
+
// provider Supabase. Mídia não entra: é storage, não tem mock honesto.
|
|
967
|
+
async listCollections() {
|
|
968
|
+
this.syncCollections();
|
|
969
|
+
return [...this.collections].sort((a, b) => a.sortOrder - b.sortOrder);
|
|
970
|
+
}
|
|
971
|
+
async getCollection(id) {
|
|
972
|
+
this.syncCollections();
|
|
973
|
+
return this.collections.find((c) => c.id === id) ?? null;
|
|
974
|
+
}
|
|
975
|
+
async createCollection(input) {
|
|
976
|
+
this.syncCollections();
|
|
977
|
+
const c = {
|
|
978
|
+
id: uid(),
|
|
979
|
+
tenantId: "mock",
|
|
980
|
+
title: input.title,
|
|
981
|
+
handle: input.handle?.trim() || mockSlugify(input.title),
|
|
982
|
+
description: input.description ?? null,
|
|
983
|
+
imageUrl: input.imageUrl ?? null,
|
|
984
|
+
seoTitle: input.seoTitle ?? null,
|
|
985
|
+
seoDescription: input.seoDescription ?? null,
|
|
986
|
+
published: input.published ?? true,
|
|
987
|
+
sortOrder: input.sortOrder ?? this.collections.length,
|
|
988
|
+
productIds: input.productIds ?? [],
|
|
989
|
+
createdAt: now(),
|
|
990
|
+
updatedAt: now()
|
|
991
|
+
};
|
|
992
|
+
this.collections.push(c);
|
|
993
|
+
this.persistCollections();
|
|
994
|
+
return c;
|
|
995
|
+
}
|
|
996
|
+
async updateCollection(id, input) {
|
|
997
|
+
this.syncCollections();
|
|
998
|
+
const i = this.collections.findIndex((c) => c.id === id);
|
|
999
|
+
if (i < 0) throw new Error("Collection not found");
|
|
1000
|
+
this.collections[i] = { ...this.collections[i], ...input, updatedAt: now() };
|
|
1001
|
+
this.persistCollections();
|
|
1002
|
+
return this.collections[i];
|
|
1003
|
+
}
|
|
1004
|
+
async deleteCollection(id) {
|
|
1005
|
+
this.syncCollections();
|
|
1006
|
+
this.collections = this.collections.filter((c) => c.id !== id);
|
|
1007
|
+
this.persistCollections();
|
|
1008
|
+
}
|
|
1009
|
+
async listCustomerGroups() {
|
|
1010
|
+
const stored = loadStored(CUSTOMER_GROUPS_KEY);
|
|
1011
|
+
if (stored.length) this.customerGroups = stored;
|
|
1012
|
+
return [...this.customerGroups].sort((a, b) => a.name.localeCompare(b.name, "pt-BR"));
|
|
1013
|
+
}
|
|
1014
|
+
async createCustomerGroup(input) {
|
|
1015
|
+
await this.listCustomerGroups();
|
|
1016
|
+
const g = {
|
|
1017
|
+
id: uid(),
|
|
1018
|
+
tenantId: "mock",
|
|
1019
|
+
name: input.name,
|
|
1020
|
+
description: input.description ?? null,
|
|
1021
|
+
color: input.color ?? null,
|
|
1022
|
+
customerIds: input.customerIds ?? [],
|
|
1023
|
+
createdAt: now(),
|
|
1024
|
+
updatedAt: now()
|
|
1025
|
+
};
|
|
1026
|
+
this.customerGroups.push(g);
|
|
1027
|
+
store(CUSTOMER_GROUPS_KEY, this.customerGroups);
|
|
1028
|
+
return g;
|
|
1029
|
+
}
|
|
1030
|
+
async updateCustomerGroup(id, input) {
|
|
1031
|
+
await this.listCustomerGroups();
|
|
1032
|
+
const i = this.customerGroups.findIndex((g) => g.id === id);
|
|
1033
|
+
if (i < 0) throw new Error("Customer group not found");
|
|
1034
|
+
this.customerGroups[i] = { ...this.customerGroups[i], ...input, updatedAt: now() };
|
|
1035
|
+
store(CUSTOMER_GROUPS_KEY, this.customerGroups);
|
|
1036
|
+
return this.customerGroups[i];
|
|
1037
|
+
}
|
|
1038
|
+
async deleteCustomerGroup(id) {
|
|
1039
|
+
await this.listCustomerGroups();
|
|
1040
|
+
this.customerGroups = this.customerGroups.filter((g) => g.id !== id);
|
|
1041
|
+
store(CUSTOMER_GROUPS_KEY, this.customerGroups);
|
|
1042
|
+
}
|
|
1043
|
+
async listCustomerSegments() {
|
|
1044
|
+
const stored = loadStored(CUSTOMER_SEGMENTS_KEY);
|
|
1045
|
+
if (stored.length) this.customerSegments = stored;
|
|
1046
|
+
return [...this.customerSegments].sort((a, b) => a.name.localeCompare(b.name, "pt-BR"));
|
|
1047
|
+
}
|
|
1048
|
+
async createCustomerSegment(input) {
|
|
1049
|
+
await this.listCustomerSegments();
|
|
1050
|
+
const s = {
|
|
1051
|
+
id: uid(),
|
|
1052
|
+
tenantId: "mock",
|
|
1053
|
+
name: input.name,
|
|
1054
|
+
description: input.description ?? null,
|
|
1055
|
+
minOrders: input.minOrders ?? null,
|
|
1056
|
+
maxOrders: input.maxOrders ?? null,
|
|
1057
|
+
minSpent: input.minSpent ?? null,
|
|
1058
|
+
maxSpent: input.maxSpent ?? null,
|
|
1059
|
+
joinedAfter: input.joinedAfter ?? null,
|
|
1060
|
+
joinedBefore: input.joinedBefore ?? null,
|
|
1061
|
+
createdAt: now(),
|
|
1062
|
+
updatedAt: now()
|
|
1063
|
+
};
|
|
1064
|
+
this.customerSegments.push(s);
|
|
1065
|
+
store(CUSTOMER_SEGMENTS_KEY, this.customerSegments);
|
|
1066
|
+
return s;
|
|
1067
|
+
}
|
|
1068
|
+
async updateCustomerSegment(id, input) {
|
|
1069
|
+
await this.listCustomerSegments();
|
|
1070
|
+
const i = this.customerSegments.findIndex((s) => s.id === id);
|
|
1071
|
+
if (i < 0) throw new Error("Customer segment not found");
|
|
1072
|
+
this.customerSegments[i] = { ...this.customerSegments[i], ...input, updatedAt: now() };
|
|
1073
|
+
store(CUSTOMER_SEGMENTS_KEY, this.customerSegments);
|
|
1074
|
+
return this.customerSegments[i];
|
|
1075
|
+
}
|
|
1076
|
+
async deleteCustomerSegment(id) {
|
|
1077
|
+
await this.listCustomerSegments();
|
|
1078
|
+
this.customerSegments = this.customerSegments.filter((s) => s.id !== id);
|
|
1079
|
+
store(CUSTOMER_SEGMENTS_KEY, this.customerSegments);
|
|
1080
|
+
}
|
|
1081
|
+
async listGiftCards() {
|
|
1082
|
+
this.syncGiftCards();
|
|
1083
|
+
return [...this.giftCards].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
1084
|
+
}
|
|
1085
|
+
async createGiftCard(input) {
|
|
1086
|
+
this.syncGiftCards();
|
|
1087
|
+
const amount = Number(input.initialBalance) || 0;
|
|
1088
|
+
const g = {
|
|
1089
|
+
id: uid(),
|
|
1090
|
+
tenantId: "mock",
|
|
1091
|
+
code: (input.code?.trim() || mockGiftCardCode()).toUpperCase(),
|
|
1092
|
+
// Same rule the supabase provider enforces: a new card is worth its face
|
|
1093
|
+
// value, and only redemption moves the remaining balance.
|
|
1094
|
+
initialBalance: amount,
|
|
1095
|
+
balance: amount,
|
|
1096
|
+
currency: input.currency ?? "BRL",
|
|
1097
|
+
status: input.status ?? "active",
|
|
1098
|
+
expiresAt: input.expiresAt ?? null,
|
|
1099
|
+
customerId: input.customerId ?? null,
|
|
1100
|
+
customerEmail: input.customerEmail ?? null,
|
|
1101
|
+
note: input.note ?? null,
|
|
1102
|
+
createdAt: now(),
|
|
1103
|
+
updatedAt: now()
|
|
1104
|
+
};
|
|
1105
|
+
this.giftCards.push(g);
|
|
1106
|
+
this.persistGiftCards();
|
|
1107
|
+
return g;
|
|
1108
|
+
}
|
|
1109
|
+
async updateGiftCard(id, input) {
|
|
1110
|
+
this.syncGiftCards();
|
|
1111
|
+
const i = this.giftCards.findIndex((g) => g.id === id);
|
|
1112
|
+
if (i < 0) throw new Error("Gift card not found");
|
|
1113
|
+
const code = input.code !== void 0 ? input.code.trim().toUpperCase() : this.giftCards[i].code;
|
|
1114
|
+
this.giftCards[i] = { ...this.giftCards[i], ...input, code, updatedAt: now() };
|
|
1115
|
+
this.persistGiftCards();
|
|
1116
|
+
return this.giftCards[i];
|
|
1117
|
+
}
|
|
1118
|
+
async deleteGiftCard(id) {
|
|
1119
|
+
this.syncGiftCards();
|
|
1120
|
+
this.giftCards = this.giftCards.filter((g) => g.id !== id);
|
|
1121
|
+
this.persistGiftCards();
|
|
1122
|
+
}
|
|
1123
|
+
async listEmailTemplates() {
|
|
1124
|
+
this.syncEmailTemplates();
|
|
1125
|
+
return [...this.emailTemplates];
|
|
1126
|
+
}
|
|
1127
|
+
async upsertEmailTemplate(input) {
|
|
1128
|
+
this.syncEmailTemplates();
|
|
1129
|
+
const i = this.emailTemplates.findIndex((t) => t.event === input.event);
|
|
1130
|
+
const base = i >= 0 ? this.emailTemplates[i] : {
|
|
1131
|
+
id: uid(),
|
|
1132
|
+
tenantId: "mock",
|
|
1133
|
+
event: input.event,
|
|
1134
|
+
enabled: false,
|
|
1135
|
+
subject: null,
|
|
1136
|
+
body: null,
|
|
1137
|
+
updatedAt: now()
|
|
1138
|
+
};
|
|
1139
|
+
const next = { ...base, ...input, updatedAt: now() };
|
|
1140
|
+
if (i >= 0) this.emailTemplates[i] = next;
|
|
1141
|
+
else this.emailTemplates.push(next);
|
|
1142
|
+
store(EMAIL_TEMPLATES_KEY, this.emailTemplates);
|
|
1143
|
+
return next;
|
|
1144
|
+
}
|
|
1145
|
+
syncCollections() {
|
|
1146
|
+
const stored = loadStored(COLLECTIONS_KEY);
|
|
1147
|
+
if (stored.length) this.collections = stored;
|
|
1148
|
+
}
|
|
1149
|
+
persistCollections() {
|
|
1150
|
+
store(COLLECTIONS_KEY, this.collections);
|
|
1151
|
+
}
|
|
1152
|
+
syncGiftCards() {
|
|
1153
|
+
const stored = loadStored(GIFT_CARDS_KEY);
|
|
1154
|
+
if (stored.length) this.giftCards = stored;
|
|
1155
|
+
}
|
|
1156
|
+
persistGiftCards() {
|
|
1157
|
+
store(GIFT_CARDS_KEY, this.giftCards);
|
|
1158
|
+
}
|
|
1159
|
+
syncEmailTemplates() {
|
|
1160
|
+
const stored = loadStored(EMAIL_TEMPLATES_KEY);
|
|
1161
|
+
if (stored.length) this.emailTemplates = stored;
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
function createMockShopProvider(seed) {
|
|
1165
|
+
return new MockShopProvider(seed);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
export { MAX_VARIANT_AXES, MAX_VARIANT_COMBINATIONS, MockShopProvider, applyVariantMatrix, axesFromOptions, createMockShopProvider, displayNameOf, formatVariantCombination, hasOnlyDefaultVariant, leavesNothingSellable, mediaKindOf, mergeMediaFiles, normalizeVariantAxes, planVariantMatrix, productImageToMediaFile, storagePathOf, validateVariantAxes, variantCombinations, variantOptionsKey };
|
|
1169
|
+
//# sourceMappingURL=chunk-U6HV3LVP.js.map
|
|
1170
|
+
//# sourceMappingURL=chunk-U6HV3LVP.js.map
|