@infuro/cms-core 1.0.14 → 1.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin.cjs +2067 -740
- package/dist/admin.cjs.map +1 -1
- package/dist/admin.d.cts +4 -0
- package/dist/admin.d.ts +4 -0
- package/dist/admin.js +2019 -677
- package/dist/admin.js.map +1 -1
- package/dist/api.cjs +967 -87
- package/dist/api.cjs.map +1 -1
- package/dist/api.d.cts +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/api.js +962 -84
- package/dist/api.js.map +1 -1
- package/dist/hooks.cjs +159 -0
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.d.cts +24 -1
- package/dist/hooks.d.ts +24 -1
- package/dist/hooks.js +165 -0
- package/dist/hooks.js.map +1 -1
- package/dist/{index-CjBf9dAb.d.ts → index-C85X7cc7.d.ts} +16 -2
- package/dist/{index-Be8NLxu-.d.cts → index-h42MoUNq.d.cts} +16 -2
- package/dist/index.cjs +5676 -4456
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -12
- package/dist/index.d.ts +104 -12
- package/dist/index.js +5670 -4467
- package/dist/index.js.map +1 -1
- package/dist/migrations/1775000000000-ProductUomTypeOrderItemSnapshots.ts +29 -0
- package/dist/migrations/1775200000000-MediaDriveFolders.ts +38 -0
- package/package.json +10 -12
package/dist/api.js
CHANGED
|
@@ -8,7 +8,25 @@ var __export = (target, all) => {
|
|
|
8
8
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
// src/plugins/erp/erp-queue.ts
|
|
12
|
+
async function queueErp(cms, payload) {
|
|
13
|
+
const queue = cms.getPlugin("queue");
|
|
14
|
+
if (!queue) return;
|
|
15
|
+
await queue.add(ERP_QUEUE_NAME, payload);
|
|
16
|
+
}
|
|
17
|
+
var ERP_QUEUE_NAME;
|
|
18
|
+
var init_erp_queue = __esm({
|
|
19
|
+
"src/plugins/erp/erp-queue.ts"() {
|
|
20
|
+
"use strict";
|
|
21
|
+
ERP_QUEUE_NAME = "erp";
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
11
25
|
// src/plugins/erp/erp-config-enabled.ts
|
|
26
|
+
var erp_config_enabled_exports = {};
|
|
27
|
+
__export(erp_config_enabled_exports, {
|
|
28
|
+
isErpIntegrationEnabled: () => isErpIntegrationEnabled
|
|
29
|
+
});
|
|
12
30
|
async function isErpIntegrationEnabled(cms, dataSource, entityMap) {
|
|
13
31
|
if (!cms.getPlugin("erp")) return false;
|
|
14
32
|
const configRepo = dataSource.getRepository(entityMap.configs);
|
|
@@ -63,14 +81,31 @@ async function queueEmail(cms, payload) {
|
|
|
63
81
|
}
|
|
64
82
|
}
|
|
65
83
|
async function queueOrderPlacedEmails(cms, payload) {
|
|
66
|
-
const {
|
|
84
|
+
const {
|
|
85
|
+
orderNumber,
|
|
86
|
+
total,
|
|
87
|
+
subtotal,
|
|
88
|
+
tax,
|
|
89
|
+
currency,
|
|
90
|
+
customerName,
|
|
91
|
+
customerEmail,
|
|
92
|
+
salesTeamEmails,
|
|
93
|
+
companyDetails,
|
|
94
|
+
lineItems,
|
|
95
|
+
billingAddress,
|
|
96
|
+
shippingAddress
|
|
97
|
+
} = payload;
|
|
67
98
|
const base = {
|
|
68
99
|
orderNumber,
|
|
69
100
|
total: total != null ? String(total) : void 0,
|
|
101
|
+
subtotal: subtotal != null ? String(subtotal) : void 0,
|
|
102
|
+
tax: tax != null ? String(tax) : void 0,
|
|
70
103
|
currency,
|
|
71
104
|
customerName,
|
|
72
105
|
companyDetails: companyDetails ?? {},
|
|
73
|
-
lineItems: lineItems ?? []
|
|
106
|
+
lineItems: lineItems ?? [],
|
|
107
|
+
billingAddress,
|
|
108
|
+
shippingAddress
|
|
74
109
|
};
|
|
75
110
|
const customerLower = customerEmail?.trim().toLowerCase() ?? "";
|
|
76
111
|
const jobs = [];
|
|
@@ -248,18 +283,124 @@ var init_erp_order_invoice = __esm({
|
|
|
248
283
|
}
|
|
249
284
|
});
|
|
250
285
|
|
|
251
|
-
// src/
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
286
|
+
// src/plugins/erp/paid-order-erp.ts
|
|
287
|
+
var paid_order_erp_exports = {};
|
|
288
|
+
__export(paid_order_erp_exports, {
|
|
289
|
+
queueErpPaidOrderForOrderId: () => queueErpPaidOrderForOrderId
|
|
290
|
+
});
|
|
291
|
+
function roundMoney(major) {
|
|
292
|
+
return Math.round(major * 100) / 100;
|
|
293
|
+
}
|
|
294
|
+
function addressToWebhookDto(a) {
|
|
295
|
+
if (!a) return {};
|
|
296
|
+
return {
|
|
297
|
+
line1: a.line1 ?? "",
|
|
298
|
+
line2: a.line2 ?? "",
|
|
299
|
+
city: a.city ?? "",
|
|
300
|
+
state: a.state ?? "",
|
|
301
|
+
postalCode: a.postalCode ?? "",
|
|
302
|
+
country: a.country ?? ""
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function orderStatusLabel(status) {
|
|
306
|
+
const s = (status || "").toLowerCase();
|
|
307
|
+
if (s === "confirmed") return "Confirmed";
|
|
308
|
+
if (s === "pending") return "Pending";
|
|
309
|
+
if (!status) return "Pending";
|
|
310
|
+
return status.charAt(0).toUpperCase() + status.slice(1);
|
|
260
311
|
}
|
|
312
|
+
function paymentRowToWebhookDto(p, amountMajorOverride) {
|
|
313
|
+
const currency = String(p.currency || "INR");
|
|
314
|
+
const amountMajor = amountMajorOverride != null && Number.isFinite(amountMajorOverride) ? amountMajorOverride : Number(p.amount);
|
|
315
|
+
const meta = { ...p.metadata || {} };
|
|
316
|
+
delete meta.amount;
|
|
317
|
+
delete meta.currency;
|
|
318
|
+
return {
|
|
319
|
+
id: String(p.externalReference || `payment_${p.id}`),
|
|
320
|
+
amount: roundMoney(amountMajor),
|
|
321
|
+
currency_code: currency,
|
|
322
|
+
captured_at: p.paidAt ? new Date(p.paidAt).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
323
|
+
provider_id: String(p.method || "unknown"),
|
|
324
|
+
data: { status: "captured", ...meta }
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId) {
|
|
328
|
+
try {
|
|
329
|
+
const configRepo = dataSource.getRepository(entityMap.configs);
|
|
330
|
+
const cfgRows = await configRepo.find({ where: { settings: "erp", deleted: false } });
|
|
331
|
+
for (const row of cfgRows) {
|
|
332
|
+
const r = row;
|
|
333
|
+
if (r.key === "enabled" && r.value === "false") return;
|
|
334
|
+
}
|
|
335
|
+
if (!cms.getPlugin("erp")) return;
|
|
336
|
+
const orderRepo = dataSource.getRepository(entityMap.orders);
|
|
337
|
+
const ord = await orderRepo.findOne({
|
|
338
|
+
where: { id: orderId },
|
|
339
|
+
relations: ["items", "items.product", "contact", "billingAddress", "shippingAddress", "payments"]
|
|
340
|
+
});
|
|
341
|
+
if (!ord) return;
|
|
342
|
+
const o = ord;
|
|
343
|
+
const okKind = o.orderKind === void 0 || o.orderKind === null || o.orderKind === "sale";
|
|
344
|
+
if (!okKind) return;
|
|
345
|
+
const rawPayments = o.payments ?? [];
|
|
346
|
+
const completedPayments = rawPayments.filter((pay) => pay.status === "completed" && pay.deleted !== true);
|
|
347
|
+
if (!completedPayments.length) return;
|
|
348
|
+
const rawItems = o.items ?? [];
|
|
349
|
+
const lines = rawItems.filter((it) => it.product).map((it) => {
|
|
350
|
+
const p = it.product;
|
|
351
|
+
const sku = p.sku || `SKU-${p.id}`;
|
|
352
|
+
const itemType = typeof it.productType === "string" && it.productType.trim() ? String(it.productType).trim() : p.type === "service" ? "service" : "product";
|
|
353
|
+
return {
|
|
354
|
+
sku,
|
|
355
|
+
quantity: Number(it.quantity) || 1,
|
|
356
|
+
unitPrice: Number(it.unitPrice),
|
|
357
|
+
title: p.name || sku,
|
|
358
|
+
discount: 0,
|
|
359
|
+
tax: Number(it.tax) || 0,
|
|
360
|
+
uom: (typeof it.uom === "string" && it.uom.trim() ? it.uom : p.uom) || void 0,
|
|
361
|
+
tax_code: typeof it.taxCode === "string" && it.taxCode.trim() ? String(it.taxCode).trim() : void 0,
|
|
362
|
+
hsn_number: (typeof it.hsn === "string" && it.hsn.trim() ? it.hsn : p.hsn) || void 0,
|
|
363
|
+
type: itemType
|
|
364
|
+
};
|
|
365
|
+
});
|
|
366
|
+
if (!lines.length) return;
|
|
367
|
+
const contact = o.contact;
|
|
368
|
+
const orderTotalMajor = Number(o.total);
|
|
369
|
+
const paymentDtos = completedPayments.length === 1 && Number.isFinite(orderTotalMajor) ? [paymentRowToWebhookDto(completedPayments[0], orderTotalMajor)] : completedPayments.map((pay) => paymentRowToWebhookDto(pay));
|
|
370
|
+
const baseMeta = o.metadata && typeof o.metadata === "object" && !Array.isArray(o.metadata) ? { ...o.metadata } : {};
|
|
371
|
+
const orderDto = {
|
|
372
|
+
platformType: "website",
|
|
373
|
+
platformOrderId: String(o.orderNumber),
|
|
374
|
+
platformOrderNumber: String(o.orderNumber),
|
|
375
|
+
order_date: o.createdAt ? new Date(o.createdAt).toISOString() : void 0,
|
|
376
|
+
status: orderStatusLabel(o.status),
|
|
377
|
+
customer: {
|
|
378
|
+
name: contact?.name || "",
|
|
379
|
+
email: contact?.email || "",
|
|
380
|
+
phone: contact?.phone || ""
|
|
381
|
+
},
|
|
382
|
+
shippingAddress: addressToWebhookDto(o.shippingAddress),
|
|
383
|
+
billingAddress: addressToWebhookDto(o.billingAddress),
|
|
384
|
+
items: lines,
|
|
385
|
+
payments: paymentDtos,
|
|
386
|
+
metadata: { ...baseMeta, source: "storefront" }
|
|
387
|
+
};
|
|
388
|
+
await queueErp(cms, { kind: "order", order: orderDto });
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
var init_paid_order_erp = __esm({
|
|
393
|
+
"src/plugins/erp/paid-order-erp.ts"() {
|
|
394
|
+
"use strict";
|
|
395
|
+
init_erp_queue();
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// src/api/crud.ts
|
|
400
|
+
import { Brackets, ILike, MoreThan } from "typeorm";
|
|
261
401
|
|
|
262
402
|
// src/plugins/erp/erp-contact-sync.ts
|
|
403
|
+
init_erp_queue();
|
|
263
404
|
function splitName(full) {
|
|
264
405
|
const t = (full || "").trim();
|
|
265
406
|
if (!t) return { firstName: "Contact", lastName: "" };
|
|
@@ -297,6 +438,7 @@ async function queueErpCreateContactIfEnabled(cms, dataSource, entityMap, input)
|
|
|
297
438
|
}
|
|
298
439
|
|
|
299
440
|
// src/plugins/erp/erp-product-sync.ts
|
|
441
|
+
init_erp_queue();
|
|
300
442
|
init_erp_config_enabled();
|
|
301
443
|
async function queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, product) {
|
|
302
444
|
try {
|
|
@@ -304,14 +446,21 @@ async function queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, produc
|
|
|
304
446
|
if (!sku) return;
|
|
305
447
|
const on = await isErpIntegrationEnabled(cms, dataSource, entityMap);
|
|
306
448
|
if (!on) return;
|
|
449
|
+
const rawMeta = product.metadata;
|
|
450
|
+
let metadata;
|
|
451
|
+
if (rawMeta && typeof rawMeta === "object" && !Array.isArray(rawMeta)) {
|
|
452
|
+
const { description: _d, ...rest } = rawMeta;
|
|
453
|
+
metadata = Object.keys(rest).length ? rest : void 0;
|
|
454
|
+
}
|
|
307
455
|
const payload = {
|
|
308
456
|
sku,
|
|
309
457
|
title: product.name || sku,
|
|
310
458
|
name: product.name,
|
|
311
|
-
description: typeof product.metadata === "object" && product.metadata && "description" in product.metadata ? String(product.metadata.description ?? "") : void 0,
|
|
312
459
|
hsn_number: product.hsn,
|
|
460
|
+
uom: product.uom != null && String(product.uom).trim() ? String(product.uom).trim() : void 0,
|
|
461
|
+
type: product.type === "service" ? "service" : "product",
|
|
313
462
|
is_active: product.status === "available",
|
|
314
|
-
metadata
|
|
463
|
+
metadata
|
|
315
464
|
};
|
|
316
465
|
await queueErp(cms, { kind: "productUpsert", product: payload });
|
|
317
466
|
} catch {
|
|
@@ -561,16 +710,58 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
561
710
|
const repo = dataSource.getRepository(entity);
|
|
562
711
|
const typeFilter = searchParams.get("type");
|
|
563
712
|
const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
|
|
713
|
+
if (resource === "media") {
|
|
714
|
+
const qb = repo.createQueryBuilder("m");
|
|
715
|
+
const parentIdParam = searchParams.get("parentId");
|
|
716
|
+
if (parentIdParam != null && parentIdParam !== "") {
|
|
717
|
+
const n = Number(parentIdParam);
|
|
718
|
+
if (!Number.isFinite(n)) return json({ error: "Invalid parentId" }, { status: 400 });
|
|
719
|
+
qb.where("m.parentId = :pid", { pid: n });
|
|
720
|
+
} else {
|
|
721
|
+
qb.where("m.parentId IS NULL");
|
|
722
|
+
}
|
|
723
|
+
if (search && typeof search === "string" && search.trim()) {
|
|
724
|
+
qb.andWhere("m.filename ILIKE :search", { search: `%${search.trim()}%` });
|
|
725
|
+
}
|
|
726
|
+
if (typeFilter) {
|
|
727
|
+
qb.andWhere(
|
|
728
|
+
new Brackets((sq) => {
|
|
729
|
+
sq.where("m.kind = :folderKind", { folderKind: "folder" }).orWhere("m.mimeType LIKE :mtp", {
|
|
730
|
+
mtp: `${typeFilter}/%`
|
|
731
|
+
});
|
|
732
|
+
})
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
const allowedSort = ["filename", "createdAt", "id"];
|
|
736
|
+
const sf = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "filename";
|
|
737
|
+
const so = sortOrder === "DESC" ? "DESC" : "ASC";
|
|
738
|
+
qb.orderBy("CASE WHEN m.kind = :fk THEN 0 ELSE 1 END", "ASC").addOrderBy(`m.${sf}`, so).setParameter("fk", "folder").skip(skip).take(limit);
|
|
739
|
+
const [data2, total2] = await qb.getManyAndCount();
|
|
740
|
+
return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
|
|
741
|
+
}
|
|
564
742
|
const sortField = columnNames.has(sortFieldRaw) ? sortFieldRaw : "createdAt";
|
|
565
743
|
let where = {};
|
|
566
|
-
if (
|
|
567
|
-
const mediaWhere = {};
|
|
568
|
-
if (search) mediaWhere.filename = ILike(`%${search}%`);
|
|
569
|
-
if (typeFilter) mediaWhere.mimeType = Like(`${typeFilter}/%`);
|
|
570
|
-
where = Object.keys(mediaWhere).length > 0 ? mediaWhere : {};
|
|
571
|
-
} else if (search) {
|
|
744
|
+
if (search) {
|
|
572
745
|
where = buildSearchWhereClause(repo, search);
|
|
573
746
|
}
|
|
747
|
+
const intFilterKeys = ["productId", "attributeId", "taxId"];
|
|
748
|
+
const extraWhere = {};
|
|
749
|
+
for (const key of intFilterKeys) {
|
|
750
|
+
const v = searchParams.get(key);
|
|
751
|
+
if (v != null && v !== "" && columnNames.has(key)) {
|
|
752
|
+
const n = Number(v);
|
|
753
|
+
if (Number.isFinite(n)) extraWhere[key] = n;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (Object.keys(extraWhere).length > 0) {
|
|
757
|
+
if (Array.isArray(where)) {
|
|
758
|
+
where = where.map((w) => ({ ...w, ...extraWhere }));
|
|
759
|
+
} else if (where && typeof where === "object" && Object.keys(where).length > 0) {
|
|
760
|
+
where = { ...where, ...extraWhere };
|
|
761
|
+
} else {
|
|
762
|
+
where = extraWhere;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
574
765
|
const [data, total] = await repo.findAndCount({
|
|
575
766
|
skip,
|
|
576
767
|
take: limit,
|
|
@@ -590,6 +781,38 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
590
781
|
if (!body || typeof body !== "object" || Object.keys(body).length === 0) {
|
|
591
782
|
return json({ error: "Invalid request payload" }, { status: 400 });
|
|
592
783
|
}
|
|
784
|
+
if (resource === "media") {
|
|
785
|
+
const b = body;
|
|
786
|
+
const kind = b.kind === "folder" ? "folder" : "file";
|
|
787
|
+
b.kind = kind;
|
|
788
|
+
const fn = String(b.filename ?? "").trim().slice(0, 255);
|
|
789
|
+
if (!fn) return json({ error: "filename required" }, { status: 400 });
|
|
790
|
+
b.filename = fn;
|
|
791
|
+
let pid = null;
|
|
792
|
+
if (b.parentId != null && b.parentId !== "") {
|
|
793
|
+
const n = Number(b.parentId);
|
|
794
|
+
if (!Number.isFinite(n)) return json({ error: "Invalid parentId" }, { status: 400 });
|
|
795
|
+
pid = n;
|
|
796
|
+
}
|
|
797
|
+
b.parentId = pid;
|
|
798
|
+
const mediaRepo = dataSource.getRepository(entityMap.media);
|
|
799
|
+
if (pid != null) {
|
|
800
|
+
const parent = await mediaRepo.findOne({ where: { id: pid } });
|
|
801
|
+
if (!parent || parent.kind !== "folder") {
|
|
802
|
+
return json({ error: "parent must be a folder" }, { status: 400 });
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (kind === "folder") {
|
|
806
|
+
b.url = null;
|
|
807
|
+
b.mimeType = "inode/directory";
|
|
808
|
+
b.size = 0;
|
|
809
|
+
} else {
|
|
810
|
+
if (!b.url || typeof b.url !== "string") return json({ error: "url required for files" }, { status: 400 });
|
|
811
|
+
if (!b.mimeType || typeof b.mimeType !== "string") {
|
|
812
|
+
b.mimeType = "application/octet-stream";
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
593
816
|
const repo = dataSource.getRepository(entity);
|
|
594
817
|
sanitizeBodyForEntity(repo, body);
|
|
595
818
|
const created = await repo.save(repo.create(body));
|
|
@@ -862,6 +1085,11 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
862
1085
|
return updated2 ? json(updated2) : json({ message: "Not found" }, { status: 404 });
|
|
863
1086
|
}
|
|
864
1087
|
const updatePayload = rawBody && typeof rawBody === "object" ? pickColumnUpdates(repo, rawBody) : {};
|
|
1088
|
+
if (resource === "media") {
|
|
1089
|
+
const u = updatePayload;
|
|
1090
|
+
delete u.parentId;
|
|
1091
|
+
delete u.kind;
|
|
1092
|
+
}
|
|
865
1093
|
if (Object.keys(updatePayload).length > 0) {
|
|
866
1094
|
sanitizeBodyForEntity(repo, updatePayload);
|
|
867
1095
|
await repo.update(numericId, updatePayload);
|
|
@@ -1037,7 +1265,8 @@ function createUserAuthApiRouter(config) {
|
|
|
1037
1265
|
|
|
1038
1266
|
// src/api/cms-handlers.ts
|
|
1039
1267
|
init_email_queue();
|
|
1040
|
-
|
|
1268
|
+
init_erp_queue();
|
|
1269
|
+
import { MoreThanOrEqual, ILike as ILike2, In } from "typeorm";
|
|
1041
1270
|
|
|
1042
1271
|
// src/plugins/captcha/assert.ts
|
|
1043
1272
|
async function assertCaptchaOk(getCms, body, req, json) {
|
|
@@ -1055,6 +1284,194 @@ async function assertCaptchaOk(getCms, body, req, json) {
|
|
|
1055
1284
|
return json({ error: result.message }, { status: result.status });
|
|
1056
1285
|
}
|
|
1057
1286
|
|
|
1287
|
+
// src/lib/media-folder-path.ts
|
|
1288
|
+
function sanitizeMediaFolderPath(input) {
|
|
1289
|
+
if (input == null) return "";
|
|
1290
|
+
if (typeof input !== "string") return "";
|
|
1291
|
+
const segments = input.replace(/\\/g, "/").split("/").map((s) => s.trim()).filter(Boolean).filter((s) => s !== ".." && s !== ".");
|
|
1292
|
+
const joined = segments.join("/");
|
|
1293
|
+
return joined.length > 512 ? joined.slice(0, 512) : joined;
|
|
1294
|
+
}
|
|
1295
|
+
function sanitizeStorageSegment(name) {
|
|
1296
|
+
const s = name.replace(/[/\\]/g, "-").trim().slice(0, 255);
|
|
1297
|
+
return s || "item";
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// src/lib/media-parent-path.ts
|
|
1301
|
+
async function relativePathFromMediaParentId(dataSource, entityMap, parentId) {
|
|
1302
|
+
if (parentId == null) return "";
|
|
1303
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1304
|
+
const segments = [];
|
|
1305
|
+
let id = parentId;
|
|
1306
|
+
for (let d = 0; d < 64 && id != null; d++) {
|
|
1307
|
+
const row = await repo.findOne({ where: { id } });
|
|
1308
|
+
if (!row) break;
|
|
1309
|
+
const m = row;
|
|
1310
|
+
if (m.kind !== "folder") break;
|
|
1311
|
+
segments.unshift(sanitizeStorageSegment(m.filename));
|
|
1312
|
+
id = m.parentId ?? null;
|
|
1313
|
+
}
|
|
1314
|
+
return segments.join("/");
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// src/lib/media-zip-extract.ts
|
|
1318
|
+
import { IsNull as IsNull2 } from "typeorm";
|
|
1319
|
+
var ZIP_MIME_TYPES = /* @__PURE__ */ new Set(["application/zip", "application/x-zip-compressed"]);
|
|
1320
|
+
var MAX_ENTRIES = 2e3;
|
|
1321
|
+
var MAX_TOTAL_UNCOMPRESSED = 80 * 1024 * 1024;
|
|
1322
|
+
function isZipMedia(mime, filename) {
|
|
1323
|
+
if (mime && ZIP_MIME_TYPES.has(mime)) return true;
|
|
1324
|
+
return filename.toLowerCase().endsWith(".zip");
|
|
1325
|
+
}
|
|
1326
|
+
async function readBufferFromPublicUrl(url) {
|
|
1327
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
1328
|
+
const r = await fetch(url);
|
|
1329
|
+
if (!r.ok) throw new Error("Failed to download file");
|
|
1330
|
+
return Buffer.from(await r.arrayBuffer());
|
|
1331
|
+
}
|
|
1332
|
+
if (url.startsWith("/")) {
|
|
1333
|
+
const { readFile } = await import("fs/promises");
|
|
1334
|
+
const { join } = await import("path");
|
|
1335
|
+
const rel = url.replace(/^\/+/, "");
|
|
1336
|
+
return readFile(join(process.cwd(), "public", rel));
|
|
1337
|
+
}
|
|
1338
|
+
throw new Error("Unsupported media URL");
|
|
1339
|
+
}
|
|
1340
|
+
function sanitizeZipPath(entryName) {
|
|
1341
|
+
const norm = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
1342
|
+
for (const seg of norm) {
|
|
1343
|
+
if (seg === ".." || seg === ".") return null;
|
|
1344
|
+
}
|
|
1345
|
+
return norm;
|
|
1346
|
+
}
|
|
1347
|
+
function shouldSkipEntry(parts) {
|
|
1348
|
+
if (parts[0] === "__MACOSX") return true;
|
|
1349
|
+
const last = parts[parts.length - 1];
|
|
1350
|
+
if (last === ".DS_Store") return true;
|
|
1351
|
+
return false;
|
|
1352
|
+
}
|
|
1353
|
+
function guessMimeType(fileName) {
|
|
1354
|
+
const lower = fileName.toLowerCase();
|
|
1355
|
+
if (lower.endsWith(".png")) return "image/png";
|
|
1356
|
+
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
1357
|
+
if (lower.endsWith(".gif")) return "image/gif";
|
|
1358
|
+
if (lower.endsWith(".webp")) return "image/webp";
|
|
1359
|
+
if (lower.endsWith(".svg")) return "image/svg+xml";
|
|
1360
|
+
if (lower.endsWith(".pdf")) return "application/pdf";
|
|
1361
|
+
if (lower.endsWith(".txt")) return "text/plain";
|
|
1362
|
+
if (lower.endsWith(".json")) return "application/json";
|
|
1363
|
+
if (lower.endsWith(".zip")) return "application/zip";
|
|
1364
|
+
return "application/octet-stream";
|
|
1365
|
+
}
|
|
1366
|
+
async function findOrCreateFolder(dataSource, entityMap, parentId, name) {
|
|
1367
|
+
const safe = sanitizeStorageSegment(name);
|
|
1368
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1369
|
+
const where = parentId == null ? { kind: "folder", filename: safe, parentId: IsNull2() } : { kind: "folder", filename: safe, parentId };
|
|
1370
|
+
const existing = await repo.findOne({ where });
|
|
1371
|
+
if (existing) return existing.id;
|
|
1372
|
+
const row = await repo.save(
|
|
1373
|
+
repo.create({
|
|
1374
|
+
kind: "folder",
|
|
1375
|
+
parentId,
|
|
1376
|
+
filename: safe,
|
|
1377
|
+
url: null,
|
|
1378
|
+
mimeType: "inode/directory",
|
|
1379
|
+
size: 0,
|
|
1380
|
+
alt: null,
|
|
1381
|
+
isPublic: false,
|
|
1382
|
+
deleted: false
|
|
1383
|
+
})
|
|
1384
|
+
);
|
|
1385
|
+
return row.id;
|
|
1386
|
+
}
|
|
1387
|
+
async function ensureFolderChain(dataSource, entityMap, rootParentId, pathSegments) {
|
|
1388
|
+
let pid = rootParentId;
|
|
1389
|
+
for (const seg of pathSegments) {
|
|
1390
|
+
if (!seg) continue;
|
|
1391
|
+
pid = await findOrCreateFolder(dataSource, entityMap, pid, seg);
|
|
1392
|
+
}
|
|
1393
|
+
return pid;
|
|
1394
|
+
}
|
|
1395
|
+
async function extractZipMediaIntoParentTree(opts) {
|
|
1396
|
+
const { dataSource, entityMap, zipMediaRow } = opts;
|
|
1397
|
+
const row = zipMediaRow;
|
|
1398
|
+
if (row.kind !== "file" || !row.url) throw new Error("Not a file");
|
|
1399
|
+
if (!isZipMedia(row.mimeType, row.filename)) throw new Error("Not a zip archive");
|
|
1400
|
+
const buffer = await readBufferFromPublicUrl(row.url);
|
|
1401
|
+
const { default: AdmZip } = await import("adm-zip");
|
|
1402
|
+
const zip = new AdmZip(buffer);
|
|
1403
|
+
const entries = zip.getEntries();
|
|
1404
|
+
if (entries.length > MAX_ENTRIES) throw new Error(`Too many zip entries (max ${MAX_ENTRIES})`);
|
|
1405
|
+
const rootParentId = row.parentId;
|
|
1406
|
+
const items = [];
|
|
1407
|
+
let totalUncompressed = 0;
|
|
1408
|
+
for (const e of entries) {
|
|
1409
|
+
const raw = e.entryName;
|
|
1410
|
+
const parts = sanitizeZipPath(raw);
|
|
1411
|
+
if (!parts || shouldSkipEntry(parts)) continue;
|
|
1412
|
+
const isDir = e.isDirectory || /\/$/.test(raw);
|
|
1413
|
+
let data = null;
|
|
1414
|
+
if (!isDir) {
|
|
1415
|
+
data = e.getData();
|
|
1416
|
+
totalUncompressed += data.length;
|
|
1417
|
+
if (totalUncompressed > MAX_TOTAL_UNCOMPRESSED) {
|
|
1418
|
+
throw new Error(`Uncompressed content exceeds limit (${MAX_TOTAL_UNCOMPRESSED} bytes)`);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
items.push({ parts, isDir, data });
|
|
1422
|
+
}
|
|
1423
|
+
items.sort((a, b) => {
|
|
1424
|
+
const da = a.parts.length;
|
|
1425
|
+
const db = b.parts.length;
|
|
1426
|
+
if (da !== db) return da - db;
|
|
1427
|
+
return a.parts.join("/").localeCompare(b.parts.join("/"));
|
|
1428
|
+
});
|
|
1429
|
+
let files = 0;
|
|
1430
|
+
let folderEntries = 0;
|
|
1431
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1432
|
+
for (const it of items) {
|
|
1433
|
+
if (it.isDir) {
|
|
1434
|
+
await ensureFolderChain(dataSource, entityMap, rootParentId, it.parts);
|
|
1435
|
+
folderEntries++;
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
const fileName = it.parts[it.parts.length - 1];
|
|
1439
|
+
const dirParts = it.parts.slice(0, -1);
|
|
1440
|
+
const parentFolderId = await ensureFolderChain(dataSource, entityMap, rootParentId, dirParts);
|
|
1441
|
+
const buf = it.data;
|
|
1442
|
+
const relBase = await relativePathFromMediaParentId(dataSource, entityMap, parentFolderId);
|
|
1443
|
+
const relativeUnderUploads = relBase ? `${relBase}/${fileName}` : fileName;
|
|
1444
|
+
const contentType = guessMimeType(fileName);
|
|
1445
|
+
let publicUrl;
|
|
1446
|
+
if (opts.storage) {
|
|
1447
|
+
publicUrl = await opts.storage.upload(buf, `uploads/${relativeUnderUploads}`, contentType);
|
|
1448
|
+
} else {
|
|
1449
|
+
const fs = await import("fs/promises");
|
|
1450
|
+
const pathMod = await import("path");
|
|
1451
|
+
const dir = pathMod.join(process.cwd(), opts.localUploadDir);
|
|
1452
|
+
const filePath = pathMod.join(dir, relativeUnderUploads);
|
|
1453
|
+
await fs.mkdir(pathMod.dirname(filePath), { recursive: true });
|
|
1454
|
+
await fs.writeFile(filePath, buf);
|
|
1455
|
+
publicUrl = `/${opts.localUploadDir.replace(/^\/+/, "").replace(/\\/g, "/")}/${relativeUnderUploads.replace(/\\/g, "/")}`;
|
|
1456
|
+
}
|
|
1457
|
+
await repo.save(
|
|
1458
|
+
repo.create({
|
|
1459
|
+
kind: "file",
|
|
1460
|
+
parentId: parentFolderId,
|
|
1461
|
+
filename: fileName,
|
|
1462
|
+
url: publicUrl,
|
|
1463
|
+
mimeType: contentType,
|
|
1464
|
+
size: buf.length,
|
|
1465
|
+
alt: null,
|
|
1466
|
+
isPublic: false,
|
|
1467
|
+
deleted: false
|
|
1468
|
+
})
|
|
1469
|
+
);
|
|
1470
|
+
files++;
|
|
1471
|
+
}
|
|
1472
|
+
return { files, folderEntries };
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1058
1475
|
// src/api/cms-handlers.ts
|
|
1059
1476
|
function createDashboardStatsHandler(config) {
|
|
1060
1477
|
const { dataSource, entityMap, json, requireAuth, requirePermission, requireEntityPermission } = config;
|
|
@@ -1072,26 +1489,209 @@ function createDashboardStatsHandler(config) {
|
|
|
1072
1489
|
try {
|
|
1073
1490
|
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3);
|
|
1074
1491
|
const repo = (name) => entityMap[name] ? dataSource.getRepository(entityMap[name]) : void 0;
|
|
1075
|
-
const [contactsCount, formsCount, formSubmissionsCount, usersCount, blogsCount, recentContacts, recentSubmissions] = await Promise.all([
|
|
1492
|
+
const [contactsCount, formsCount, formSubmissionsCount, usersCount, blogsCount, recentContacts, recentSubmissions, contactTypeRows] = await Promise.all([
|
|
1076
1493
|
repo("contacts")?.count() ?? 0,
|
|
1077
1494
|
repo("forms")?.count({ where: { deleted: false } }) ?? 0,
|
|
1078
1495
|
repo("form_submissions")?.count() ?? 0,
|
|
1079
1496
|
repo("users")?.count({ where: { deleted: false } }) ?? 0,
|
|
1080
1497
|
repo("blogs")?.count({ where: { deleted: false } }) ?? 0,
|
|
1081
1498
|
repo("contacts")?.count({ where: { createdAt: MoreThanOrEqual(sevenDaysAgo) } }) ?? 0,
|
|
1082
|
-
repo("form_submissions")?.count({ where: { createdAt: MoreThanOrEqual(sevenDaysAgo) } }) ?? 0
|
|
1499
|
+
repo("form_submissions")?.count({ where: { createdAt: MoreThanOrEqual(sevenDaysAgo) } }) ?? 0,
|
|
1500
|
+
repo("contacts")?.createQueryBuilder("c").select("COALESCE(NULLIF(TRIM(c.type), ''), 'unknown')", "type").addSelect("COUNT(*)", "count").where("c.deleted = :deleted", { deleted: false }).groupBy("COALESCE(NULLIF(TRIM(c.type), ''), 'unknown')").getRawMany() ?? []
|
|
1083
1501
|
]);
|
|
1084
1502
|
return json({
|
|
1085
1503
|
contacts: { total: contactsCount, recent: recentContacts },
|
|
1086
1504
|
forms: { total: formsCount, submissions: formSubmissionsCount, recentSubmissions },
|
|
1087
1505
|
users: usersCount,
|
|
1088
|
-
blogs: blogsCount
|
|
1506
|
+
blogs: blogsCount,
|
|
1507
|
+
contactTypes: (contactTypeRows ?? []).map((row) => ({
|
|
1508
|
+
type: row.type || "unknown",
|
|
1509
|
+
count: Number(row.count || 0)
|
|
1510
|
+
}))
|
|
1089
1511
|
});
|
|
1090
1512
|
} catch (err) {
|
|
1091
1513
|
return json({ error: "Failed to fetch dashboard stats" }, { status: 500 });
|
|
1092
1514
|
}
|
|
1093
1515
|
};
|
|
1094
1516
|
}
|
|
1517
|
+
function toNum(v) {
|
|
1518
|
+
const n = typeof v === "number" ? v : Number(v ?? 0);
|
|
1519
|
+
return Number.isFinite(n) ? n : 0;
|
|
1520
|
+
}
|
|
1521
|
+
function toIsoDate(d) {
|
|
1522
|
+
return d.toISOString().slice(0, 10);
|
|
1523
|
+
}
|
|
1524
|
+
function createEcommerceAnalyticsHandler(config) {
|
|
1525
|
+
const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
|
|
1526
|
+
return async function GET(req) {
|
|
1527
|
+
const authErr = await requireAuth(req);
|
|
1528
|
+
if (authErr) return authErr;
|
|
1529
|
+
if (requireEntityPermission) {
|
|
1530
|
+
const pe = await requireEntityPermission(req, "analytics", "read");
|
|
1531
|
+
if (pe) return pe;
|
|
1532
|
+
}
|
|
1533
|
+
if (!entityMap.orders || !entityMap.order_items || !entityMap.payments || !entityMap.products) {
|
|
1534
|
+
return json({ error: "Store analytics unavailable" }, { status: 404 });
|
|
1535
|
+
}
|
|
1536
|
+
try {
|
|
1537
|
+
const url = new URL(req.url);
|
|
1538
|
+
const rawDays = parseInt(url.searchParams.get("days") || "30", 10);
|
|
1539
|
+
const days = Number.isFinite(rawDays) ? Math.min(365, Math.max(7, rawDays)) : 30;
|
|
1540
|
+
const end = /* @__PURE__ */ new Date();
|
|
1541
|
+
const start = new Date(end.getTime() - days * 24 * 60 * 60 * 1e3);
|
|
1542
|
+
const orderRepo = dataSource.getRepository(entityMap.orders);
|
|
1543
|
+
const paymentRepo = dataSource.getRepository(entityMap.payments);
|
|
1544
|
+
const itemRepo = dataSource.getRepository(entityMap.order_items);
|
|
1545
|
+
const productRepo = dataSource.getRepository(entityMap.products);
|
|
1546
|
+
const [salesOrders, returnOrders, replacementOrders, payments, products] = await Promise.all([
|
|
1547
|
+
orderRepo.find({
|
|
1548
|
+
where: { deleted: false, createdAt: MoreThanOrEqual(start), orderKind: "sale", status: In(["confirmed", "processing", "completed"]) },
|
|
1549
|
+
select: ["id", "contactId", "createdAt", "subtotal", "discount", "tax", "total", "status"]
|
|
1550
|
+
}),
|
|
1551
|
+
orderRepo.find({
|
|
1552
|
+
where: { deleted: false, createdAt: MoreThanOrEqual(start), orderKind: "return" },
|
|
1553
|
+
select: ["id", "createdAt", "total"]
|
|
1554
|
+
}),
|
|
1555
|
+
orderRepo.find({
|
|
1556
|
+
where: { deleted: false, createdAt: MoreThanOrEqual(start), orderKind: "replacement" },
|
|
1557
|
+
select: ["id", "createdAt", "total"]
|
|
1558
|
+
}),
|
|
1559
|
+
paymentRepo.find({
|
|
1560
|
+
where: { deleted: false, createdAt: MoreThanOrEqual(start) },
|
|
1561
|
+
select: ["id", "status", "method", "amount", "createdAt"]
|
|
1562
|
+
}),
|
|
1563
|
+
productRepo.find({
|
|
1564
|
+
where: { deleted: false },
|
|
1565
|
+
select: ["id", "name", "quantity"]
|
|
1566
|
+
})
|
|
1567
|
+
]);
|
|
1568
|
+
const saleOrderIds = salesOrders.map((o) => o.id);
|
|
1569
|
+
const orderItems = saleOrderIds.length ? await itemRepo.find({
|
|
1570
|
+
where: { orderId: In(saleOrderIds) },
|
|
1571
|
+
select: ["id", "orderId", "productId", "quantity", "total"]
|
|
1572
|
+
}) : [];
|
|
1573
|
+
const grossSales = salesOrders.reduce((sum, o) => sum + toNum(o.subtotal), 0);
|
|
1574
|
+
const discounts = salesOrders.reduce((sum, o) => sum + toNum(o.discount), 0);
|
|
1575
|
+
const taxes = salesOrders.reduce((sum, o) => sum + toNum(o.tax), 0);
|
|
1576
|
+
const returnsValue = returnOrders.reduce((sum, o) => sum + toNum(o.total), 0);
|
|
1577
|
+
const replacementsValue = replacementOrders.reduce((sum, o) => sum + toNum(o.total), 0);
|
|
1578
|
+
const netSales = grossSales - discounts - returnsValue;
|
|
1579
|
+
const ordersCount = salesOrders.length;
|
|
1580
|
+
const aov = ordersCount > 0 ? netSales / ordersCount : 0;
|
|
1581
|
+
const returnRate = ordersCount > 0 ? returnOrders.length / ordersCount * 100 : 0;
|
|
1582
|
+
const salesByDate = /* @__PURE__ */ new Map();
|
|
1583
|
+
const returnsByDate = /* @__PURE__ */ new Map();
|
|
1584
|
+
for (const o of salesOrders) {
|
|
1585
|
+
const key = toIsoDate(new Date(o.createdAt));
|
|
1586
|
+
const row = salesByDate.get(key) ?? { value: 0, orders: 0 };
|
|
1587
|
+
row.value += toNum(o.total);
|
|
1588
|
+
row.orders += 1;
|
|
1589
|
+
salesByDate.set(key, row);
|
|
1590
|
+
}
|
|
1591
|
+
for (const o of returnOrders) {
|
|
1592
|
+
const key = toIsoDate(new Date(o.createdAt));
|
|
1593
|
+
const row = returnsByDate.get(key) ?? { value: 0, count: 0 };
|
|
1594
|
+
row.value += toNum(o.total);
|
|
1595
|
+
row.count += 1;
|
|
1596
|
+
returnsByDate.set(key, row);
|
|
1597
|
+
}
|
|
1598
|
+
const salesOverTime = [];
|
|
1599
|
+
const returnsTrend = [];
|
|
1600
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
1601
|
+
const d = new Date(end.getTime() - i * 24 * 60 * 60 * 1e3);
|
|
1602
|
+
const key = toIsoDate(d);
|
|
1603
|
+
const sales = salesByDate.get(key) ?? { value: 0, orders: 0 };
|
|
1604
|
+
const returns = returnsByDate.get(key) ?? { value: 0, count: 0 };
|
|
1605
|
+
salesOverTime.push({ date: key, value: Number(sales.value.toFixed(2)), orders: sales.orders });
|
|
1606
|
+
returnsTrend.push({ date: key, value: Number(returns.value.toFixed(2)), count: returns.count });
|
|
1607
|
+
}
|
|
1608
|
+
const productNameMap = /* @__PURE__ */ new Map();
|
|
1609
|
+
for (const p of products) productNameMap.set(Number(p.id), (p.name || `Product #${p.id}`).trim());
|
|
1610
|
+
const productAgg = /* @__PURE__ */ new Map();
|
|
1611
|
+
for (const item of orderItems) {
|
|
1612
|
+
const productId = Number(item.productId);
|
|
1613
|
+
const productName = productNameMap.get(productId) || `Product #${productId}`;
|
|
1614
|
+
const row = productAgg.get(productId) ?? { name: productName, units: 0, sales: 0 };
|
|
1615
|
+
row.units += toNum(item.quantity);
|
|
1616
|
+
row.sales += toNum(item.total);
|
|
1617
|
+
productAgg.set(productId, row);
|
|
1618
|
+
}
|
|
1619
|
+
const topProducts = Array.from(productAgg.values()).sort((a, b) => b.sales - a.sales).slice(0, 5).map((p) => ({ ...p, sales: Number(p.sales.toFixed(2)) }));
|
|
1620
|
+
const allSaleOrderContactIds = Array.from(new Set(salesOrders.map((o) => Number(o.contactId)).filter((n) => Number.isInteger(n) && n > 0)));
|
|
1621
|
+
const allTimeCounts = allSaleOrderContactIds.length ? await orderRepo.createQueryBuilder("o").select("o.contactId", "contactId").addSelect("COUNT(*)", "total").where("o.deleted = :deleted", { deleted: false }).andWhere("o.orderKind = :orderKind", { orderKind: "sale" }).andWhere("o.contactId IN (:...contactIds)", { contactIds: allSaleOrderContactIds }).groupBy("o.contactId").getRawMany() : [];
|
|
1622
|
+
const countMap = /* @__PURE__ */ new Map();
|
|
1623
|
+
for (const c of allTimeCounts) countMap.set(Number(c.contactId), Number(c.total));
|
|
1624
|
+
const purchasingCustomers = allSaleOrderContactIds.length;
|
|
1625
|
+
const returningCustomers = allSaleOrderContactIds.filter((id) => (countMap.get(id) ?? 0) > 1).length;
|
|
1626
|
+
const newCustomers = Math.max(0, purchasingCustomers - returningCustomers);
|
|
1627
|
+
const returningCustomerRate = purchasingCustomers > 0 ? returningCustomers / purchasingCustomers * 100 : 0;
|
|
1628
|
+
const totalPayments = payments.length;
|
|
1629
|
+
const completedPayments = payments.filter((p) => p.status === "completed").length;
|
|
1630
|
+
const failedPayments = payments.filter((p) => p.status === "failed").length;
|
|
1631
|
+
const paymentSuccessRate = totalPayments > 0 ? completedPayments / totalPayments * 100 : 0;
|
|
1632
|
+
const paymentMethodMap = /* @__PURE__ */ new Map();
|
|
1633
|
+
for (const p of payments) {
|
|
1634
|
+
const method = (p.method || "unknown").toLowerCase();
|
|
1635
|
+
const row = paymentMethodMap.get(method) ?? { method, count: 0, amount: 0 };
|
|
1636
|
+
row.count += 1;
|
|
1637
|
+
row.amount += toNum(p.amount);
|
|
1638
|
+
paymentMethodMap.set(method, row);
|
|
1639
|
+
}
|
|
1640
|
+
const paymentMethods = Array.from(paymentMethodMap.values()).sort((a, b) => b.count - a.count).map((p) => ({ ...p, amount: Number(p.amount.toFixed(2)) }));
|
|
1641
|
+
const totalInventory = products.reduce((sum, p) => sum + toNum(p.quantity), 0);
|
|
1642
|
+
const outOfStockCount = products.filter((p) => toNum(p.quantity) <= 0).length;
|
|
1643
|
+
const lowStockCount = products.filter((p) => toNum(p.quantity) > 0 && toNum(p.quantity) <= 5).length;
|
|
1644
|
+
const inventoryRisk = {
|
|
1645
|
+
outOfStockCount,
|
|
1646
|
+
lowStockCount,
|
|
1647
|
+
totalInventory
|
|
1648
|
+
};
|
|
1649
|
+
return json({
|
|
1650
|
+
rangeDays: days,
|
|
1651
|
+
kpis: {
|
|
1652
|
+
netSales: Number(netSales.toFixed(2)),
|
|
1653
|
+
grossSales: Number(grossSales.toFixed(2)),
|
|
1654
|
+
ordersPlaced: ordersCount,
|
|
1655
|
+
averageOrderValue: Number(aov.toFixed(2)),
|
|
1656
|
+
returningCustomerRate: Number(returningCustomerRate.toFixed(2)),
|
|
1657
|
+
returnRate: Number(returnRate.toFixed(2)),
|
|
1658
|
+
returnValue: Number(returnsValue.toFixed(2)),
|
|
1659
|
+
discounts: Number(discounts.toFixed(2)),
|
|
1660
|
+
taxes: Number(taxes.toFixed(2)),
|
|
1661
|
+
paymentSuccessRate: Number(paymentSuccessRate.toFixed(2))
|
|
1662
|
+
},
|
|
1663
|
+
salesOverTime,
|
|
1664
|
+
topProducts,
|
|
1665
|
+
customerMix: {
|
|
1666
|
+
newCustomers,
|
|
1667
|
+
returningCustomers,
|
|
1668
|
+
repeatPurchaseRate: Number(returningCustomerRate.toFixed(2))
|
|
1669
|
+
},
|
|
1670
|
+
returnsTrend,
|
|
1671
|
+
paymentPerformance: {
|
|
1672
|
+
successCount: completedPayments,
|
|
1673
|
+
failedCount: failedPayments,
|
|
1674
|
+
successRate: Number(paymentSuccessRate.toFixed(2)),
|
|
1675
|
+
methods: paymentMethods
|
|
1676
|
+
},
|
|
1677
|
+
conversionProxy: {
|
|
1678
|
+
sessions: 0,
|
|
1679
|
+
checkoutStarted: 0,
|
|
1680
|
+
ordersPlaced: ordersCount
|
|
1681
|
+
},
|
|
1682
|
+
salesBreakdown: {
|
|
1683
|
+
sales: { count: ordersCount, value: Number(grossSales.toFixed(2)) },
|
|
1684
|
+
returns: { count: returnOrders.length, value: Number(returnsValue.toFixed(2)) },
|
|
1685
|
+
replacements: { count: replacementOrders.length, value: Number(replacementsValue.toFixed(2)) }
|
|
1686
|
+
},
|
|
1687
|
+
geoPerformance: [],
|
|
1688
|
+
inventoryRisk
|
|
1689
|
+
});
|
|
1690
|
+
} catch {
|
|
1691
|
+
return json({ error: "Failed to fetch ecommerce analytics" }, { status: 500 });
|
|
1692
|
+
}
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1095
1695
|
function createAnalyticsHandlers(config) {
|
|
1096
1696
|
const { json, getAnalyticsData, getPropertyId, getPermissions } = config;
|
|
1097
1697
|
return {
|
|
@@ -1123,8 +1723,27 @@ function createAnalyticsHandlers(config) {
|
|
|
1123
1723
|
};
|
|
1124
1724
|
}
|
|
1125
1725
|
function createUploadHandler(config) {
|
|
1126
|
-
const {
|
|
1127
|
-
|
|
1726
|
+
const {
|
|
1727
|
+
json,
|
|
1728
|
+
requireAuth,
|
|
1729
|
+
requireEntityPermission,
|
|
1730
|
+
storage,
|
|
1731
|
+
localUploadDir = "public/uploads",
|
|
1732
|
+
allowedTypes,
|
|
1733
|
+
maxSizeBytes = 10 * 1024 * 1024,
|
|
1734
|
+
dataSource,
|
|
1735
|
+
entityMap
|
|
1736
|
+
} = config;
|
|
1737
|
+
const allowed = allowedTypes ?? [
|
|
1738
|
+
"image/jpeg",
|
|
1739
|
+
"image/png",
|
|
1740
|
+
"image/gif",
|
|
1741
|
+
"image/webp",
|
|
1742
|
+
"application/pdf",
|
|
1743
|
+
"text/plain",
|
|
1744
|
+
"application/zip",
|
|
1745
|
+
"application/x-zip-compressed"
|
|
1746
|
+
];
|
|
1128
1747
|
return async function POST(req) {
|
|
1129
1748
|
const authErr = await requireAuth(req);
|
|
1130
1749
|
if (authErr) return authErr;
|
|
@@ -1137,28 +1756,92 @@ function createUploadHandler(config) {
|
|
|
1137
1756
|
const file = formData.get("file");
|
|
1138
1757
|
if (!file) return json({ error: "No file uploaded" }, { status: 400 });
|
|
1139
1758
|
if (!allowed.includes(file.type)) return json({ error: "File type not allowed" }, { status: 400 });
|
|
1140
|
-
|
|
1759
|
+
const defaultMax = 10 * 1024 * 1024;
|
|
1760
|
+
const maxZipBytes = 80 * 1024 * 1024;
|
|
1761
|
+
const baseMax = maxSizeBytes ?? defaultMax;
|
|
1762
|
+
const effectiveMax = file.type === "application/zip" || file.type === "application/x-zip-compressed" ? Math.max(baseMax, maxZipBytes) : baseMax;
|
|
1763
|
+
if (file.size > effectiveMax) return json({ error: "File size exceeds limit" }, { status: 400 });
|
|
1764
|
+
const parentRaw = formData.get("parentId");
|
|
1765
|
+
let parentId = null;
|
|
1766
|
+
if (parentRaw != null && String(parentRaw).trim() !== "") {
|
|
1767
|
+
const n = Number(parentRaw);
|
|
1768
|
+
if (!Number.isFinite(n)) return json({ error: "Invalid parentId" }, { status: 400 });
|
|
1769
|
+
parentId = n;
|
|
1770
|
+
}
|
|
1771
|
+
let folder = "";
|
|
1772
|
+
if (parentId != null) {
|
|
1773
|
+
if (!dataSource || !entityMap?.media) {
|
|
1774
|
+
return json({ error: "Upload handler needs dataSource and entityMap for folder uploads" }, { status: 400 });
|
|
1775
|
+
}
|
|
1776
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1777
|
+
const p = await repo.findOne({ where: { id: parentId } });
|
|
1778
|
+
if (!p || p.kind !== "folder") {
|
|
1779
|
+
return json({ error: "parent must be a folder" }, { status: 400 });
|
|
1780
|
+
}
|
|
1781
|
+
folder = await relativePathFromMediaParentId(dataSource, entityMap, parentId);
|
|
1782
|
+
} else {
|
|
1783
|
+
const folderRawLegacy = formData.get("folder") ?? formData.get("folderPath");
|
|
1784
|
+
if (folderRawLegacy && typeof folderRawLegacy === "string" && folderRawLegacy.trim()) {
|
|
1785
|
+
folder = sanitizeMediaFolderPath(folderRawLegacy);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1141
1788
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
1142
1789
|
const fileName = `${Date.now()}-${file.name}`;
|
|
1143
1790
|
const contentType = file.type || "application/octet-stream";
|
|
1791
|
+
const relativeUnderUploads = folder ? `${folder}/${fileName}` : fileName;
|
|
1144
1792
|
const raw = typeof storage === "function" ? storage() : storage;
|
|
1145
1793
|
const storageService = raw instanceof Promise ? await raw : raw;
|
|
1146
1794
|
if (storageService) {
|
|
1147
|
-
const fileUrl = await storageService.upload(buffer, `uploads/${
|
|
1148
|
-
return json({ filePath: fileUrl });
|
|
1795
|
+
const fileUrl = await storageService.upload(buffer, `uploads/${relativeUnderUploads}`, contentType);
|
|
1796
|
+
return json({ filePath: fileUrl, parentId });
|
|
1149
1797
|
}
|
|
1150
1798
|
const fs = await import("fs/promises");
|
|
1151
1799
|
const path = await import("path");
|
|
1152
1800
|
const dir = path.join(process.cwd(), localUploadDir);
|
|
1153
|
-
|
|
1154
|
-
|
|
1801
|
+
const filePath = path.join(dir, relativeUnderUploads);
|
|
1802
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
1155
1803
|
await fs.writeFile(filePath, buffer);
|
|
1156
|
-
|
|
1804
|
+
const urlRel = `${localUploadDir.replace(/^\/+/, "").replace(/\\/g, "/")}/${relativeUnderUploads.replace(/\\/g, "/")}`;
|
|
1805
|
+
return json({ filePath: `/${urlRel}`, parentId });
|
|
1157
1806
|
} catch (err) {
|
|
1158
1807
|
return json({ error: "File upload failed" }, { status: 500 });
|
|
1159
1808
|
}
|
|
1160
1809
|
};
|
|
1161
1810
|
}
|
|
1811
|
+
function createMediaZipExtractHandler(config) {
|
|
1812
|
+
const { json, requireAuth, requireEntityPermission, storage, localUploadDir = "public/uploads", dataSource, entityMap } = config;
|
|
1813
|
+
return async function POST(_req, zipMediaId) {
|
|
1814
|
+
const authErr = await requireAuth(_req);
|
|
1815
|
+
if (authErr) return authErr;
|
|
1816
|
+
if (requireEntityPermission) {
|
|
1817
|
+
const pe = await requireEntityPermission(_req, "media", "create");
|
|
1818
|
+
if (pe) return pe;
|
|
1819
|
+
}
|
|
1820
|
+
if (!dataSource || !entityMap?.media) {
|
|
1821
|
+
return json({ error: "Media extract requires dataSource and entityMap" }, { status: 500 });
|
|
1822
|
+
}
|
|
1823
|
+
const id = Number(zipMediaId);
|
|
1824
|
+
if (!Number.isFinite(id)) return json({ error: "Invalid id" }, { status: 400 });
|
|
1825
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1826
|
+
const row = await repo.findOne({ where: { id } });
|
|
1827
|
+
if (!row) return json({ error: "Not found" }, { status: 404 });
|
|
1828
|
+
try {
|
|
1829
|
+
const raw = typeof storage === "function" ? storage() : storage;
|
|
1830
|
+
const storageService = raw instanceof Promise ? await raw : raw;
|
|
1831
|
+
const result = await extractZipMediaIntoParentTree({
|
|
1832
|
+
dataSource,
|
|
1833
|
+
entityMap,
|
|
1834
|
+
zipMediaRow: row,
|
|
1835
|
+
storage: storageService,
|
|
1836
|
+
localUploadDir
|
|
1837
|
+
});
|
|
1838
|
+
return json({ ok: true, ...result });
|
|
1839
|
+
} catch (e) {
|
|
1840
|
+
const msg = e instanceof Error ? e.message : "Extract failed";
|
|
1841
|
+
return json({ error: msg }, { status: 400 });
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1162
1845
|
function createBlogBySlugHandler(config) {
|
|
1163
1846
|
const { dataSource, entityMap, json } = config;
|
|
1164
1847
|
return async function GET(_req, slug) {
|
|
@@ -2300,6 +2983,7 @@ function createCmsApiHandler(config) {
|
|
|
2300
2983
|
getCms,
|
|
2301
2984
|
userAuth: userAuthConfig,
|
|
2302
2985
|
dashboard,
|
|
2986
|
+
ecommerceAnalytics,
|
|
2303
2987
|
analytics: analyticsConfig,
|
|
2304
2988
|
upload,
|
|
2305
2989
|
blogBySlug,
|
|
@@ -2366,8 +3050,28 @@ function createCmsApiHandler(config) {
|
|
|
2366
3050
|
});
|
|
2367
3051
|
const userAuthRouter = userAuth ? createUserAuthApiRouter(userAuth) : null;
|
|
2368
3052
|
const dashboardGet = dashboard ? createDashboardStatsHandler(mergePerm(dashboard) ?? dashboard) : null;
|
|
3053
|
+
const ecommerceAnalyticsResolved = mergePerm(
|
|
3054
|
+
ecommerceAnalytics ?? {
|
|
3055
|
+
dataSource,
|
|
3056
|
+
entityMap,
|
|
3057
|
+
json: config.json,
|
|
3058
|
+
requireAuth: config.requireAuth
|
|
3059
|
+
}
|
|
3060
|
+
) ?? {
|
|
3061
|
+
dataSource,
|
|
3062
|
+
entityMap,
|
|
3063
|
+
json: config.json,
|
|
3064
|
+
requireAuth: config.requireAuth
|
|
3065
|
+
};
|
|
3066
|
+
const ecommerceAnalyticsGet = createEcommerceAnalyticsHandler(ecommerceAnalyticsResolved);
|
|
2369
3067
|
const analyticsHandlers = analytics ? createAnalyticsHandlers(analytics) : null;
|
|
2370
|
-
const
|
|
3068
|
+
const uploadMerged = upload ? {
|
|
3069
|
+
...mergePerm(upload) ?? upload,
|
|
3070
|
+
dataSource: upload.dataSource ?? dataSource,
|
|
3071
|
+
entityMap: upload.entityMap ?? entityMap
|
|
3072
|
+
} : null;
|
|
3073
|
+
const uploadPost = uploadMerged ? createUploadHandler(uploadMerged) : null;
|
|
3074
|
+
const zipExtractPost = uploadMerged ? createMediaZipExtractHandler(uploadMerged) : null;
|
|
2371
3075
|
const blogBySlugGet = blogBySlug ? createBlogBySlugHandler(blogBySlug) : null;
|
|
2372
3076
|
const formBySlugGet = formBySlug ? createFormBySlugHandler(formBySlug) : null;
|
|
2373
3077
|
const formSaveHandlers = formSaveConfig ? createFormSaveHandlers(mergePerm(formSaveConfig) ?? formSaveConfig) : null;
|
|
@@ -2416,6 +3120,11 @@ function createCmsApiHandler(config) {
|
|
|
2416
3120
|
if (path[0] === "dashboard" && path[1] === "stats" && path.length === 2 && method === "GET" && dashboardGet) {
|
|
2417
3121
|
return dashboardGet(req);
|
|
2418
3122
|
}
|
|
3123
|
+
if (path[0] === "dashboard" && path[1] === "ecommerce" && path.length === 2 && method === "GET" && ecommerceAnalyticsGet) {
|
|
3124
|
+
const g = await analyticsGate();
|
|
3125
|
+
if (g) return g;
|
|
3126
|
+
return ecommerceAnalyticsGet(req);
|
|
3127
|
+
}
|
|
2419
3128
|
if (path[0] === "analytics" && analyticsHandlers) {
|
|
2420
3129
|
if (path.length === 1 && method === "GET") {
|
|
2421
3130
|
const g = await analyticsGate();
|
|
@@ -2434,6 +3143,9 @@ function createCmsApiHandler(config) {
|
|
|
2434
3143
|
}
|
|
2435
3144
|
}
|
|
2436
3145
|
if (path[0] === "upload" && path.length === 1 && method === "POST" && uploadPost) return uploadPost(req);
|
|
3146
|
+
if (path[0] === "media" && path[1] === "extract" && path.length === 3 && method === "POST" && zipExtractPost) {
|
|
3147
|
+
return zipExtractPost(req, path[2]);
|
|
3148
|
+
}
|
|
2437
3149
|
if (path[0] === "blogs" && path[1] === "slug" && path.length === 3 && method === "GET" && blogBySlugGet) {
|
|
2438
3150
|
return blogBySlugGet(req, path[2]);
|
|
2439
3151
|
}
|
|
@@ -2516,6 +3228,29 @@ function createCmsApiHandler(config) {
|
|
|
2516
3228
|
if (!Number.isFinite(oid)) return config.json({ error: "Invalid id" }, { status: 400 });
|
|
2517
3229
|
return streamOrderInvoicePdf2(cms, dataSource, entityMap, oid, {});
|
|
2518
3230
|
}
|
|
3231
|
+
if (path[0] === "orders" && path.length === 3 && path[2] === "repost-erp" && getCms) {
|
|
3232
|
+
const a = await config.requireAuth(req);
|
|
3233
|
+
if (a) return a;
|
|
3234
|
+
if (perm) {
|
|
3235
|
+
const pe = await perm(req, "orders", method === "GET" ? "read" : "update");
|
|
3236
|
+
if (pe) return pe;
|
|
3237
|
+
}
|
|
3238
|
+
const oid = Number(path[1]);
|
|
3239
|
+
if (!Number.isFinite(oid)) return config.json({ error: "Invalid id" }, { status: 400 });
|
|
3240
|
+
const cms = await getCms();
|
|
3241
|
+
const { isErpIntegrationEnabled: isErpIntegrationEnabled3 } = await Promise.resolve().then(() => (init_erp_config_enabled(), erp_config_enabled_exports));
|
|
3242
|
+
const enabled = await isErpIntegrationEnabled3(cms, dataSource, entityMap);
|
|
3243
|
+
if (method === "GET") {
|
|
3244
|
+
return config.json({ enabled });
|
|
3245
|
+
}
|
|
3246
|
+
if (method === "POST") {
|
|
3247
|
+
if (!enabled) return config.json({ error: "ERP integration is disabled" }, { status: 409 });
|
|
3248
|
+
const { queueErpPaidOrderForOrderId: queueErpPaidOrderForOrderId2 } = await Promise.resolve().then(() => (init_paid_order_erp(), paid_order_erp_exports));
|
|
3249
|
+
await queueErpPaidOrderForOrderId2(cms, dataSource, entityMap, oid);
|
|
3250
|
+
return config.json({ ok: true });
|
|
3251
|
+
}
|
|
3252
|
+
return config.json({ error: "Method not allowed" }, { status: 405 });
|
|
3253
|
+
}
|
|
2519
3254
|
if (path.length === 0) return config.json({ error: "Not found" }, { status: 404 });
|
|
2520
3255
|
const resource = resolveResource(path[0]);
|
|
2521
3256
|
if (!crudResources.includes(resource)) return config.json({ error: "Invalid resource" }, { status: 400 });
|
|
@@ -2548,7 +3283,7 @@ function createCmsApiHandler(config) {
|
|
|
2548
3283
|
}
|
|
2549
3284
|
|
|
2550
3285
|
// src/api/storefront-handlers.ts
|
|
2551
|
-
import { In, IsNull as
|
|
3286
|
+
import { In as In2, IsNull as IsNull4 } from "typeorm";
|
|
2552
3287
|
|
|
2553
3288
|
// src/lib/is-valid-signup-email.ts
|
|
2554
3289
|
var MAX_EMAIL = 254;
|
|
@@ -2808,7 +3543,7 @@ async function queueSms(cms, payload) {
|
|
|
2808
3543
|
|
|
2809
3544
|
// src/lib/otp-challenge.ts
|
|
2810
3545
|
import { createHmac, randomInt, timingSafeEqual } from "crypto";
|
|
2811
|
-
import { IsNull as
|
|
3546
|
+
import { IsNull as IsNull3, MoreThan as MoreThan2 } from "typeorm";
|
|
2812
3547
|
var OTP_TTL_MS = 10 * 60 * 1e3;
|
|
2813
3548
|
var MAX_SENDS_PER_HOUR = 5;
|
|
2814
3549
|
var MAX_VERIFY_ATTEMPTS = 8;
|
|
@@ -2856,7 +3591,7 @@ async function createOtpChallenge(dataSource, entityMap, input) {
|
|
|
2856
3591
|
await repo.delete({
|
|
2857
3592
|
purpose,
|
|
2858
3593
|
identifier,
|
|
2859
|
-
consumedAt:
|
|
3594
|
+
consumedAt: IsNull3()
|
|
2860
3595
|
});
|
|
2861
3596
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
2862
3597
|
const codeHash = hashOtpCode(code, purpose, identifier, pepper);
|
|
@@ -2877,7 +3612,7 @@ async function verifyAndConsumeOtpChallenge(dataSource, entityMap, input) {
|
|
|
2877
3612
|
const { purpose, identifier, code, pepper } = input;
|
|
2878
3613
|
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
2879
3614
|
const row = await repo.findOne({
|
|
2880
|
-
where: { purpose, identifier, consumedAt:
|
|
3615
|
+
where: { purpose, identifier, consumedAt: IsNull3() },
|
|
2881
3616
|
order: { id: "DESC" }
|
|
2882
3617
|
});
|
|
2883
3618
|
if (!row) {
|
|
@@ -2944,6 +3679,152 @@ function createStorefrontApiHandler(config) {
|
|
|
2944
3679
|
const tokenRepo = () => dataSource.getRepository(entityMap.password_reset_tokens);
|
|
2945
3680
|
const collectionRepo = () => dataSource.getRepository(entityMap.collections);
|
|
2946
3681
|
const groupRepo = () => dataSource.getRepository(entityMap.user_groups);
|
|
3682
|
+
const configRepo = () => dataSource.getRepository(entityMap.configs);
|
|
3683
|
+
const CART_CHECKOUT_RELATIONS = ["items", "items.product", "items.product.taxes", "items.product.taxes.tax"];
|
|
3684
|
+
function roundMoney2(n) {
|
|
3685
|
+
return Math.round(n * 100) / 100;
|
|
3686
|
+
}
|
|
3687
|
+
async function getStoreDefaultTaxRate() {
|
|
3688
|
+
const rows = await configRepo().find({ where: { settings: "store", deleted: false } });
|
|
3689
|
+
for (const row of rows) {
|
|
3690
|
+
const r = row;
|
|
3691
|
+
if (r.key === "defaultTaxRate") {
|
|
3692
|
+
const n = parseFloat(String(r.value ?? "").trim());
|
|
3693
|
+
return Number.isFinite(n) && n >= 0 ? n : null;
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
return null;
|
|
3697
|
+
}
|
|
3698
|
+
function computeTaxForProductLine(p, lineSubtotal, defaultRate) {
|
|
3699
|
+
const pts = p.taxes ?? [];
|
|
3700
|
+
const activePts = pts.filter((pt) => {
|
|
3701
|
+
const t = pt.tax;
|
|
3702
|
+
return t != null && t.active !== false;
|
|
3703
|
+
});
|
|
3704
|
+
if (activePts.length) {
|
|
3705
|
+
let sumRate = 0;
|
|
3706
|
+
const slugs = [];
|
|
3707
|
+
for (const pt of activePts) {
|
|
3708
|
+
const t = pt.tax;
|
|
3709
|
+
const r = Number(pt.rate != null && pt.rate !== "" ? pt.rate : t.rate ?? 0);
|
|
3710
|
+
if (Number.isFinite(r)) sumRate += r;
|
|
3711
|
+
const slug = String(t.slug ?? "").trim();
|
|
3712
|
+
if (slug) slugs.push(slug);
|
|
3713
|
+
}
|
|
3714
|
+
const tax = roundMoney2(lineSubtotal * sumRate / 100);
|
|
3715
|
+
return {
|
|
3716
|
+
tax,
|
|
3717
|
+
taxRate: sumRate > 0 ? roundMoney2(sumRate) : null,
|
|
3718
|
+
taxCode: slugs.length ? [...new Set(slugs)].sort().join(",") : null
|
|
3719
|
+
};
|
|
3720
|
+
}
|
|
3721
|
+
if (defaultRate != null && defaultRate > 0) {
|
|
3722
|
+
return {
|
|
3723
|
+
tax: roundMoney2(lineSubtotal * defaultRate / 100),
|
|
3724
|
+
taxRate: roundMoney2(defaultRate),
|
|
3725
|
+
taxCode: null
|
|
3726
|
+
};
|
|
3727
|
+
}
|
|
3728
|
+
return { tax: 0, taxRate: null, taxCode: null };
|
|
3729
|
+
}
|
|
3730
|
+
function parseInlineAddress(raw) {
|
|
3731
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
3732
|
+
const o = raw;
|
|
3733
|
+
const line1 = String(o.line1 ?? "").trim();
|
|
3734
|
+
if (!line1) return null;
|
|
3735
|
+
return {
|
|
3736
|
+
line1,
|
|
3737
|
+
line2: o.line2 != null ? String(o.line2) : "",
|
|
3738
|
+
city: o.city != null ? String(o.city) : "",
|
|
3739
|
+
state: o.state != null ? String(o.state) : "",
|
|
3740
|
+
postalCode: o.postalCode != null ? String(o.postalCode) : "",
|
|
3741
|
+
country: o.country != null ? String(o.country) : ""
|
|
3742
|
+
};
|
|
3743
|
+
}
|
|
3744
|
+
function intFromBody(v) {
|
|
3745
|
+
if (typeof v === "number" && Number.isInteger(v)) return v;
|
|
3746
|
+
if (typeof v === "string" && /^\d+$/.test(v)) return parseInt(v, 10);
|
|
3747
|
+
return void 0;
|
|
3748
|
+
}
|
|
3749
|
+
async function resolveCheckoutAddress(contactId, idVal, inlineVal) {
|
|
3750
|
+
const aid = intFromBody(idVal);
|
|
3751
|
+
if (aid != null) {
|
|
3752
|
+
const existing = await addressRepo().findOne({
|
|
3753
|
+
where: { id: aid, contactId }
|
|
3754
|
+
});
|
|
3755
|
+
if (!existing) return { id: null, error: "Address not found" };
|
|
3756
|
+
return { id: aid };
|
|
3757
|
+
}
|
|
3758
|
+
const addr = parseInlineAddress(inlineVal);
|
|
3759
|
+
if (addr) {
|
|
3760
|
+
const saved = await addressRepo().save(
|
|
3761
|
+
addressRepo().create({
|
|
3762
|
+
contactId,
|
|
3763
|
+
line1: addr.line1,
|
|
3764
|
+
line2: addr.line2?.trim() ? addr.line2 : null,
|
|
3765
|
+
city: addr.city?.trim() ? addr.city : null,
|
|
3766
|
+
state: addr.state?.trim() ? addr.state : null,
|
|
3767
|
+
postalCode: addr.postalCode?.trim() ? addr.postalCode : null,
|
|
3768
|
+
country: addr.country?.trim() ? addr.country : null
|
|
3769
|
+
})
|
|
3770
|
+
);
|
|
3771
|
+
return { id: saved.id };
|
|
3772
|
+
}
|
|
3773
|
+
return { id: null };
|
|
3774
|
+
}
|
|
3775
|
+
async function prepareCheckoutFromCart(b, cart, contactId) {
|
|
3776
|
+
const defaultRate = await getStoreDefaultTaxRate();
|
|
3777
|
+
const lines = [];
|
|
3778
|
+
let subtotal = 0;
|
|
3779
|
+
let orderTax = 0;
|
|
3780
|
+
let needsShipping = false;
|
|
3781
|
+
for (const it of cart.items || []) {
|
|
3782
|
+
const p = it.product;
|
|
3783
|
+
if (!p || p.deleted || p.status !== "available") continue;
|
|
3784
|
+
const unit = Number(p.price);
|
|
3785
|
+
const qty = it.quantity || 1;
|
|
3786
|
+
const lineSubtotal = unit * qty;
|
|
3787
|
+
const pType = p.type === "service" ? "service" : "product";
|
|
3788
|
+
if (pType === "product") needsShipping = true;
|
|
3789
|
+
const { tax, taxRate, taxCode } = computeTaxForProductLine(p, lineSubtotal, defaultRate);
|
|
3790
|
+
const lineTotal = roundMoney2(lineSubtotal + tax);
|
|
3791
|
+
subtotal = roundMoney2(subtotal + lineSubtotal);
|
|
3792
|
+
orderTax = roundMoney2(orderTax + tax);
|
|
3793
|
+
lines.push({
|
|
3794
|
+
productId: p.id,
|
|
3795
|
+
quantity: qty,
|
|
3796
|
+
unitPrice: unit,
|
|
3797
|
+
tax,
|
|
3798
|
+
total: lineTotal,
|
|
3799
|
+
hsn: p.hsn ?? null,
|
|
3800
|
+
uom: p.uom ?? null,
|
|
3801
|
+
productType: pType,
|
|
3802
|
+
taxRate,
|
|
3803
|
+
taxCode
|
|
3804
|
+
});
|
|
3805
|
+
}
|
|
3806
|
+
if (!lines.length) return { ok: false, status: 400, message: "No available items in cart" };
|
|
3807
|
+
const bill = await resolveCheckoutAddress(contactId, b.billingAddressId, b.billingAddress);
|
|
3808
|
+
if (bill.error) return { ok: false, status: 400, message: bill.error };
|
|
3809
|
+
if (bill.id == null) return { ok: false, status: 400, message: "Billing address required" };
|
|
3810
|
+
const ship = await resolveCheckoutAddress(contactId, b.shippingAddressId, b.shippingAddress);
|
|
3811
|
+
if (ship.error) return { ok: false, status: 400, message: ship.error };
|
|
3812
|
+
let shippingAddressId = ship.id;
|
|
3813
|
+
if (needsShipping && shippingAddressId == null) shippingAddressId = bill.id;
|
|
3814
|
+
if (needsShipping && shippingAddressId == null) {
|
|
3815
|
+
return { ok: false, status: 400, message: "Shipping address required" };
|
|
3816
|
+
}
|
|
3817
|
+
const orderTotal = roundMoney2(subtotal + orderTax);
|
|
3818
|
+
return {
|
|
3819
|
+
ok: true,
|
|
3820
|
+
lines,
|
|
3821
|
+
subtotal,
|
|
3822
|
+
orderTax,
|
|
3823
|
+
orderTotal,
|
|
3824
|
+
billingAddressId: bill.id,
|
|
3825
|
+
shippingAddressId
|
|
3826
|
+
};
|
|
3827
|
+
}
|
|
2947
3828
|
async function syncContactToErp(contact) {
|
|
2948
3829
|
if (!getCms) return;
|
|
2949
3830
|
try {
|
|
@@ -2965,7 +3846,7 @@ function createStorefrontApiHandler(config) {
|
|
|
2965
3846
|
const u = await userRepo().findOne({ where: { id: userId } });
|
|
2966
3847
|
if (!u) return null;
|
|
2967
3848
|
const unclaimed = await contactRepo().findOne({
|
|
2968
|
-
where: { email: u.email, userId:
|
|
3849
|
+
where: { email: u.email, userId: IsNull4(), deleted: false }
|
|
2969
3850
|
});
|
|
2970
3851
|
if (unclaimed) {
|
|
2971
3852
|
await contactRepo().update(unclaimed.id, { userId });
|
|
@@ -3069,6 +3950,7 @@ function createStorefrontApiHandler(config) {
|
|
|
3069
3950
|
slug: p.slug,
|
|
3070
3951
|
price: p.price,
|
|
3071
3952
|
sku: p.sku,
|
|
3953
|
+
type: p.type === "service" ? "service" : "product",
|
|
3072
3954
|
image: primaryProductImageUrl(p.metadata)
|
|
3073
3955
|
} : null
|
|
3074
3956
|
};
|
|
@@ -3096,6 +3978,8 @@ function createStorefrontApiHandler(config) {
|
|
|
3096
3978
|
slug: p.slug,
|
|
3097
3979
|
sku: p.sku,
|
|
3098
3980
|
hsn: p.hsn,
|
|
3981
|
+
uom: p.uom ?? null,
|
|
3982
|
+
type: p.type === "service" ? "service" : "product",
|
|
3099
3983
|
price: p.price,
|
|
3100
3984
|
compareAtPrice: p.compareAtPrice,
|
|
3101
3985
|
status: p.status,
|
|
@@ -3797,7 +4681,7 @@ function createStorefrontApiHandler(config) {
|
|
|
3797
4681
|
contactId = contact.id;
|
|
3798
4682
|
cart = await cartRepo().findOne({
|
|
3799
4683
|
where: { contactId },
|
|
3800
|
-
relations: [
|
|
4684
|
+
relations: [...CART_CHECKOUT_RELATIONS]
|
|
3801
4685
|
});
|
|
3802
4686
|
} else {
|
|
3803
4687
|
const email = String(b.email ?? "").trim();
|
|
@@ -3830,25 +4714,14 @@ function createStorefrontApiHandler(config) {
|
|
|
3830
4714
|
if (!guestToken) return json({ error: "Cart not found" }, { status: 400 });
|
|
3831
4715
|
cart = await cartRepo().findOne({
|
|
3832
4716
|
where: { guestToken },
|
|
3833
|
-
relations: [
|
|
4717
|
+
relations: [...CART_CHECKOUT_RELATIONS]
|
|
3834
4718
|
});
|
|
3835
4719
|
}
|
|
3836
4720
|
if (!cart || !(cart.items || []).length) {
|
|
3837
4721
|
return json({ error: "Cart is empty" }, { status: 400 });
|
|
3838
4722
|
}
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
for (const it of cart.items || []) {
|
|
3842
|
-
const p = it.product;
|
|
3843
|
-
if (!p || p.deleted || p.status !== "available") continue;
|
|
3844
|
-
const unit = Number(p.price);
|
|
3845
|
-
const qty = it.quantity || 1;
|
|
3846
|
-
const lineTotal = unit * qty;
|
|
3847
|
-
subtotal += lineTotal;
|
|
3848
|
-
lines.push({ productId: p.id, quantity: qty, unitPrice: unit, tax: 0, total: lineTotal });
|
|
3849
|
-
}
|
|
3850
|
-
if (!lines.length) return json({ error: "No available items in cart" }, { status: 400 });
|
|
3851
|
-
const total = subtotal;
|
|
4723
|
+
const prepOrd = await prepareCheckoutFromCart(b, cart, contactId);
|
|
4724
|
+
if (!prepOrd.ok) return json({ error: prepOrd.message }, { status: prepOrd.status });
|
|
3852
4725
|
const cartId = cart.id;
|
|
3853
4726
|
const ord = await orderRepo().save(
|
|
3854
4727
|
orderRepo().create({
|
|
@@ -3856,13 +4729,13 @@ function createStorefrontApiHandler(config) {
|
|
|
3856
4729
|
orderKind: "sale",
|
|
3857
4730
|
parentOrderId: null,
|
|
3858
4731
|
contactId,
|
|
3859
|
-
billingAddressId:
|
|
3860
|
-
shippingAddressId:
|
|
4732
|
+
billingAddressId: prepOrd.billingAddressId,
|
|
4733
|
+
shippingAddressId: prepOrd.shippingAddressId,
|
|
3861
4734
|
status: "pending",
|
|
3862
|
-
subtotal,
|
|
3863
|
-
tax:
|
|
4735
|
+
subtotal: prepOrd.subtotal,
|
|
4736
|
+
tax: prepOrd.orderTax,
|
|
3864
4737
|
discount: 0,
|
|
3865
|
-
total,
|
|
4738
|
+
total: prepOrd.orderTotal,
|
|
3866
4739
|
currency: cart.currency || "INR",
|
|
3867
4740
|
metadata: { cartId }
|
|
3868
4741
|
})
|
|
@@ -3871,7 +4744,7 @@ function createStorefrontApiHandler(config) {
|
|
|
3871
4744
|
await orderRepo().update(oid, {
|
|
3872
4745
|
orderNumber: buildCanonicalOrderNumber("sale", oid, ord.createdAt ?? /* @__PURE__ */ new Date())
|
|
3873
4746
|
});
|
|
3874
|
-
for (const line of lines) {
|
|
4747
|
+
for (const line of prepOrd.lines) {
|
|
3875
4748
|
await orderItemRepo().save(
|
|
3876
4749
|
orderItemRepo().create({
|
|
3877
4750
|
orderId: oid,
|
|
@@ -3879,14 +4752,21 @@ function createStorefrontApiHandler(config) {
|
|
|
3879
4752
|
quantity: line.quantity,
|
|
3880
4753
|
unitPrice: line.unitPrice,
|
|
3881
4754
|
tax: line.tax,
|
|
3882
|
-
total: line.total
|
|
4755
|
+
total: line.total,
|
|
4756
|
+
hsn: line.hsn,
|
|
4757
|
+
uom: line.uom,
|
|
4758
|
+
productType: line.productType,
|
|
4759
|
+
taxRate: line.taxRate,
|
|
4760
|
+
taxCode: line.taxCode
|
|
3883
4761
|
})
|
|
3884
4762
|
);
|
|
3885
4763
|
}
|
|
3886
4764
|
return json({
|
|
3887
4765
|
orderId: oid,
|
|
3888
4766
|
orderNumber: ord.orderNumber,
|
|
3889
|
-
|
|
4767
|
+
subtotal: prepOrd.subtotal,
|
|
4768
|
+
tax: prepOrd.orderTax,
|
|
4769
|
+
total: prepOrd.orderTotal,
|
|
3890
4770
|
currency: cart.currency || "INR"
|
|
3891
4771
|
});
|
|
3892
4772
|
}
|
|
@@ -3904,7 +4784,7 @@ function createStorefrontApiHandler(config) {
|
|
|
3904
4784
|
contactId = contact.id;
|
|
3905
4785
|
cart = await cartRepo().findOne({
|
|
3906
4786
|
where: { contactId },
|
|
3907
|
-
relations: [
|
|
4787
|
+
relations: [...CART_CHECKOUT_RELATIONS]
|
|
3908
4788
|
});
|
|
3909
4789
|
} else {
|
|
3910
4790
|
const email = String(b.email ?? "").trim();
|
|
@@ -3937,38 +4817,27 @@ function createStorefrontApiHandler(config) {
|
|
|
3937
4817
|
if (!guestToken) return json({ error: "Cart not found" }, { status: 400 });
|
|
3938
4818
|
cart = await cartRepo().findOne({
|
|
3939
4819
|
where: { guestToken },
|
|
3940
|
-
relations: [
|
|
4820
|
+
relations: [...CART_CHECKOUT_RELATIONS]
|
|
3941
4821
|
});
|
|
3942
4822
|
}
|
|
3943
4823
|
if (!cart || !(cart.items || []).length) {
|
|
3944
4824
|
return json({ error: "Cart is empty" }, { status: 400 });
|
|
3945
4825
|
}
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
for (const it of cart.items || []) {
|
|
3949
|
-
const p = it.product;
|
|
3950
|
-
if (!p || p.deleted || p.status !== "available") continue;
|
|
3951
|
-
const unit = Number(p.price);
|
|
3952
|
-
const qty = it.quantity || 1;
|
|
3953
|
-
const lineTotal = unit * qty;
|
|
3954
|
-
subtotal += lineTotal;
|
|
3955
|
-
lines.push({ productId: p.id, quantity: qty, unitPrice: unit, tax: 0, total: lineTotal });
|
|
3956
|
-
}
|
|
3957
|
-
if (!lines.length) return json({ error: "No available items in cart" }, { status: 400 });
|
|
3958
|
-
const total = subtotal;
|
|
4826
|
+
const prepChk = await prepareCheckoutFromCart(b, cart, contactId);
|
|
4827
|
+
if (!prepChk.ok) return json({ error: prepChk.message }, { status: prepChk.status });
|
|
3959
4828
|
const ord = await orderRepo().save(
|
|
3960
4829
|
orderRepo().create({
|
|
3961
4830
|
orderNumber: temporaryOrderNumberPlaceholder(),
|
|
3962
4831
|
orderKind: "sale",
|
|
3963
4832
|
parentOrderId: null,
|
|
3964
4833
|
contactId,
|
|
3965
|
-
billingAddressId:
|
|
3966
|
-
shippingAddressId:
|
|
4834
|
+
billingAddressId: prepChk.billingAddressId,
|
|
4835
|
+
shippingAddressId: prepChk.shippingAddressId,
|
|
3967
4836
|
status: "pending",
|
|
3968
|
-
subtotal,
|
|
3969
|
-
tax:
|
|
4837
|
+
subtotal: prepChk.subtotal,
|
|
4838
|
+
tax: prepChk.orderTax,
|
|
3970
4839
|
discount: 0,
|
|
3971
|
-
total,
|
|
4840
|
+
total: prepChk.orderTotal,
|
|
3972
4841
|
currency: cart.currency || "INR"
|
|
3973
4842
|
})
|
|
3974
4843
|
);
|
|
@@ -3976,7 +4845,7 @@ function createStorefrontApiHandler(config) {
|
|
|
3976
4845
|
await orderRepo().update(oid, {
|
|
3977
4846
|
orderNumber: buildCanonicalOrderNumber("sale", oid, ord.createdAt ?? /* @__PURE__ */ new Date())
|
|
3978
4847
|
});
|
|
3979
|
-
for (const line of lines) {
|
|
4848
|
+
for (const line of prepChk.lines) {
|
|
3980
4849
|
await orderItemRepo().save(
|
|
3981
4850
|
orderItemRepo().create({
|
|
3982
4851
|
orderId: oid,
|
|
@@ -3984,7 +4853,12 @@ function createStorefrontApiHandler(config) {
|
|
|
3984
4853
|
quantity: line.quantity,
|
|
3985
4854
|
unitPrice: line.unitPrice,
|
|
3986
4855
|
tax: line.tax,
|
|
3987
|
-
total: line.total
|
|
4856
|
+
total: line.total,
|
|
4857
|
+
hsn: line.hsn,
|
|
4858
|
+
uom: line.uom,
|
|
4859
|
+
productType: line.productType,
|
|
4860
|
+
taxRate: line.taxRate,
|
|
4861
|
+
taxCode: line.taxCode
|
|
3988
4862
|
})
|
|
3989
4863
|
);
|
|
3990
4864
|
}
|
|
@@ -3993,7 +4867,9 @@ function createStorefrontApiHandler(config) {
|
|
|
3993
4867
|
return json({
|
|
3994
4868
|
orderId: oid,
|
|
3995
4869
|
orderNumber: ord.orderNumber,
|
|
3996
|
-
|
|
4870
|
+
subtotal: prepChk.subtotal,
|
|
4871
|
+
tax: prepChk.orderTax,
|
|
4872
|
+
total: prepChk.orderTotal
|
|
3997
4873
|
});
|
|
3998
4874
|
}
|
|
3999
4875
|
if (path[0] === "orders" && path.length === 1 && method === "GET") {
|
|
@@ -4011,7 +4887,7 @@ function createStorefrontApiHandler(config) {
|
|
|
4011
4887
|
const previewByOrder = {};
|
|
4012
4888
|
if (orderIds.length) {
|
|
4013
4889
|
const oItems = await orderItemRepo().find({
|
|
4014
|
-
where: { orderId:
|
|
4890
|
+
where: { orderId: In2(orderIds) },
|
|
4015
4891
|
relations: ["product"],
|
|
4016
4892
|
order: { id: "ASC" }
|
|
4017
4893
|
});
|
|
@@ -4143,9 +5019,11 @@ export {
|
|
|
4143
5019
|
createCrudByIdHandler,
|
|
4144
5020
|
createCrudHandler,
|
|
4145
5021
|
createDashboardStatsHandler,
|
|
5022
|
+
createEcommerceAnalyticsHandler,
|
|
4146
5023
|
createForgotPasswordHandler,
|
|
4147
5024
|
createFormBySlugHandler,
|
|
4148
5025
|
createInviteAcceptHandler,
|
|
5026
|
+
createMediaZipExtractHandler,
|
|
4149
5027
|
createSetPasswordHandler,
|
|
4150
5028
|
createSettingsApiHandlers,
|
|
4151
5029
|
createStorefrontApiHandler,
|