@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.cjs
CHANGED
|
@@ -30,7 +30,25 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
));
|
|
31
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
32
|
|
|
33
|
+
// src/plugins/erp/erp-queue.ts
|
|
34
|
+
async function queueErp(cms, payload) {
|
|
35
|
+
const queue = cms.getPlugin("queue");
|
|
36
|
+
if (!queue) return;
|
|
37
|
+
await queue.add(ERP_QUEUE_NAME, payload);
|
|
38
|
+
}
|
|
39
|
+
var ERP_QUEUE_NAME;
|
|
40
|
+
var init_erp_queue = __esm({
|
|
41
|
+
"src/plugins/erp/erp-queue.ts"() {
|
|
42
|
+
"use strict";
|
|
43
|
+
ERP_QUEUE_NAME = "erp";
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
33
47
|
// src/plugins/erp/erp-config-enabled.ts
|
|
48
|
+
var erp_config_enabled_exports = {};
|
|
49
|
+
__export(erp_config_enabled_exports, {
|
|
50
|
+
isErpIntegrationEnabled: () => isErpIntegrationEnabled
|
|
51
|
+
});
|
|
34
52
|
async function isErpIntegrationEnabled(cms, dataSource, entityMap) {
|
|
35
53
|
if (!cms.getPlugin("erp")) return false;
|
|
36
54
|
const configRepo = dataSource.getRepository(entityMap.configs);
|
|
@@ -85,14 +103,31 @@ async function queueEmail(cms, payload) {
|
|
|
85
103
|
}
|
|
86
104
|
}
|
|
87
105
|
async function queueOrderPlacedEmails(cms, payload) {
|
|
88
|
-
const {
|
|
106
|
+
const {
|
|
107
|
+
orderNumber,
|
|
108
|
+
total,
|
|
109
|
+
subtotal,
|
|
110
|
+
tax,
|
|
111
|
+
currency,
|
|
112
|
+
customerName,
|
|
113
|
+
customerEmail,
|
|
114
|
+
salesTeamEmails,
|
|
115
|
+
companyDetails,
|
|
116
|
+
lineItems,
|
|
117
|
+
billingAddress,
|
|
118
|
+
shippingAddress
|
|
119
|
+
} = payload;
|
|
89
120
|
const base = {
|
|
90
121
|
orderNumber,
|
|
91
122
|
total: total != null ? String(total) : void 0,
|
|
123
|
+
subtotal: subtotal != null ? String(subtotal) : void 0,
|
|
124
|
+
tax: tax != null ? String(tax) : void 0,
|
|
92
125
|
currency,
|
|
93
126
|
customerName,
|
|
94
127
|
companyDetails: companyDetails ?? {},
|
|
95
|
-
lineItems: lineItems ?? []
|
|
128
|
+
lineItems: lineItems ?? [],
|
|
129
|
+
billingAddress,
|
|
130
|
+
shippingAddress
|
|
96
131
|
};
|
|
97
132
|
const customerLower = customerEmail?.trim().toLowerCase() ?? "";
|
|
98
133
|
const jobs = [];
|
|
@@ -270,6 +305,119 @@ var init_erp_order_invoice = __esm({
|
|
|
270
305
|
}
|
|
271
306
|
});
|
|
272
307
|
|
|
308
|
+
// src/plugins/erp/paid-order-erp.ts
|
|
309
|
+
var paid_order_erp_exports = {};
|
|
310
|
+
__export(paid_order_erp_exports, {
|
|
311
|
+
queueErpPaidOrderForOrderId: () => queueErpPaidOrderForOrderId
|
|
312
|
+
});
|
|
313
|
+
function roundMoney(major) {
|
|
314
|
+
return Math.round(major * 100) / 100;
|
|
315
|
+
}
|
|
316
|
+
function addressToWebhookDto(a) {
|
|
317
|
+
if (!a) return {};
|
|
318
|
+
return {
|
|
319
|
+
line1: a.line1 ?? "",
|
|
320
|
+
line2: a.line2 ?? "",
|
|
321
|
+
city: a.city ?? "",
|
|
322
|
+
state: a.state ?? "",
|
|
323
|
+
postalCode: a.postalCode ?? "",
|
|
324
|
+
country: a.country ?? ""
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function orderStatusLabel(status) {
|
|
328
|
+
const s = (status || "").toLowerCase();
|
|
329
|
+
if (s === "confirmed") return "Confirmed";
|
|
330
|
+
if (s === "pending") return "Pending";
|
|
331
|
+
if (!status) return "Pending";
|
|
332
|
+
return status.charAt(0).toUpperCase() + status.slice(1);
|
|
333
|
+
}
|
|
334
|
+
function paymentRowToWebhookDto(p, amountMajorOverride) {
|
|
335
|
+
const currency = String(p.currency || "INR");
|
|
336
|
+
const amountMajor = amountMajorOverride != null && Number.isFinite(amountMajorOverride) ? amountMajorOverride : Number(p.amount);
|
|
337
|
+
const meta = { ...p.metadata || {} };
|
|
338
|
+
delete meta.amount;
|
|
339
|
+
delete meta.currency;
|
|
340
|
+
return {
|
|
341
|
+
id: String(p.externalReference || `payment_${p.id}`),
|
|
342
|
+
amount: roundMoney(amountMajor),
|
|
343
|
+
currency_code: currency,
|
|
344
|
+
captured_at: p.paidAt ? new Date(p.paidAt).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
345
|
+
provider_id: String(p.method || "unknown"),
|
|
346
|
+
data: { status: "captured", ...meta }
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId) {
|
|
350
|
+
try {
|
|
351
|
+
const configRepo = dataSource.getRepository(entityMap.configs);
|
|
352
|
+
const cfgRows = await configRepo.find({ where: { settings: "erp", deleted: false } });
|
|
353
|
+
for (const row of cfgRows) {
|
|
354
|
+
const r = row;
|
|
355
|
+
if (r.key === "enabled" && r.value === "false") return;
|
|
356
|
+
}
|
|
357
|
+
if (!cms.getPlugin("erp")) return;
|
|
358
|
+
const orderRepo = dataSource.getRepository(entityMap.orders);
|
|
359
|
+
const ord = await orderRepo.findOne({
|
|
360
|
+
where: { id: orderId },
|
|
361
|
+
relations: ["items", "items.product", "contact", "billingAddress", "shippingAddress", "payments"]
|
|
362
|
+
});
|
|
363
|
+
if (!ord) return;
|
|
364
|
+
const o = ord;
|
|
365
|
+
const okKind = o.orderKind === void 0 || o.orderKind === null || o.orderKind === "sale";
|
|
366
|
+
if (!okKind) return;
|
|
367
|
+
const rawPayments = o.payments ?? [];
|
|
368
|
+
const completedPayments = rawPayments.filter((pay) => pay.status === "completed" && pay.deleted !== true);
|
|
369
|
+
if (!completedPayments.length) return;
|
|
370
|
+
const rawItems = o.items ?? [];
|
|
371
|
+
const lines = rawItems.filter((it) => it.product).map((it) => {
|
|
372
|
+
const p = it.product;
|
|
373
|
+
const sku = p.sku || `SKU-${p.id}`;
|
|
374
|
+
const itemType = typeof it.productType === "string" && it.productType.trim() ? String(it.productType).trim() : p.type === "service" ? "service" : "product";
|
|
375
|
+
return {
|
|
376
|
+
sku,
|
|
377
|
+
quantity: Number(it.quantity) || 1,
|
|
378
|
+
unitPrice: Number(it.unitPrice),
|
|
379
|
+
title: p.name || sku,
|
|
380
|
+
discount: 0,
|
|
381
|
+
tax: Number(it.tax) || 0,
|
|
382
|
+
uom: (typeof it.uom === "string" && it.uom.trim() ? it.uom : p.uom) || void 0,
|
|
383
|
+
tax_code: typeof it.taxCode === "string" && it.taxCode.trim() ? String(it.taxCode).trim() : void 0,
|
|
384
|
+
hsn_number: (typeof it.hsn === "string" && it.hsn.trim() ? it.hsn : p.hsn) || void 0,
|
|
385
|
+
type: itemType
|
|
386
|
+
};
|
|
387
|
+
});
|
|
388
|
+
if (!lines.length) return;
|
|
389
|
+
const contact = o.contact;
|
|
390
|
+
const orderTotalMajor = Number(o.total);
|
|
391
|
+
const paymentDtos = completedPayments.length === 1 && Number.isFinite(orderTotalMajor) ? [paymentRowToWebhookDto(completedPayments[0], orderTotalMajor)] : completedPayments.map((pay) => paymentRowToWebhookDto(pay));
|
|
392
|
+
const baseMeta = o.metadata && typeof o.metadata === "object" && !Array.isArray(o.metadata) ? { ...o.metadata } : {};
|
|
393
|
+
const orderDto = {
|
|
394
|
+
platformType: "website",
|
|
395
|
+
platformOrderId: String(o.orderNumber),
|
|
396
|
+
platformOrderNumber: String(o.orderNumber),
|
|
397
|
+
order_date: o.createdAt ? new Date(o.createdAt).toISOString() : void 0,
|
|
398
|
+
status: orderStatusLabel(o.status),
|
|
399
|
+
customer: {
|
|
400
|
+
name: contact?.name || "",
|
|
401
|
+
email: contact?.email || "",
|
|
402
|
+
phone: contact?.phone || ""
|
|
403
|
+
},
|
|
404
|
+
shippingAddress: addressToWebhookDto(o.shippingAddress),
|
|
405
|
+
billingAddress: addressToWebhookDto(o.billingAddress),
|
|
406
|
+
items: lines,
|
|
407
|
+
payments: paymentDtos,
|
|
408
|
+
metadata: { ...baseMeta, source: "storefront" }
|
|
409
|
+
};
|
|
410
|
+
await queueErp(cms, { kind: "order", order: orderDto });
|
|
411
|
+
} catch {
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
var init_paid_order_erp = __esm({
|
|
415
|
+
"src/plugins/erp/paid-order-erp.ts"() {
|
|
416
|
+
"use strict";
|
|
417
|
+
init_erp_queue();
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
273
421
|
// src/api/index.ts
|
|
274
422
|
var api_exports = {};
|
|
275
423
|
__export(api_exports, {
|
|
@@ -280,9 +428,11 @@ __export(api_exports, {
|
|
|
280
428
|
createCrudByIdHandler: () => createCrudByIdHandler,
|
|
281
429
|
createCrudHandler: () => createCrudHandler,
|
|
282
430
|
createDashboardStatsHandler: () => createDashboardStatsHandler,
|
|
431
|
+
createEcommerceAnalyticsHandler: () => createEcommerceAnalyticsHandler,
|
|
283
432
|
createForgotPasswordHandler: () => createForgotPasswordHandler,
|
|
284
433
|
createFormBySlugHandler: () => createFormBySlugHandler,
|
|
285
434
|
createInviteAcceptHandler: () => createInviteAcceptHandler,
|
|
435
|
+
createMediaZipExtractHandler: () => createMediaZipExtractHandler,
|
|
286
436
|
createSetPasswordHandler: () => createSetPasswordHandler,
|
|
287
437
|
createSettingsApiHandlers: () => createSettingsApiHandlers,
|
|
288
438
|
createStorefrontApiHandler: () => createStorefrontApiHandler,
|
|
@@ -298,15 +448,8 @@ module.exports = __toCommonJS(api_exports);
|
|
|
298
448
|
// src/api/crud.ts
|
|
299
449
|
var import_typeorm = require("typeorm");
|
|
300
450
|
|
|
301
|
-
// src/plugins/erp/erp-queue.ts
|
|
302
|
-
var ERP_QUEUE_NAME = "erp";
|
|
303
|
-
async function queueErp(cms, payload) {
|
|
304
|
-
const queue = cms.getPlugin("queue");
|
|
305
|
-
if (!queue) return;
|
|
306
|
-
await queue.add(ERP_QUEUE_NAME, payload);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
451
|
// src/plugins/erp/erp-contact-sync.ts
|
|
452
|
+
init_erp_queue();
|
|
310
453
|
function splitName(full) {
|
|
311
454
|
const t = (full || "").trim();
|
|
312
455
|
if (!t) return { firstName: "Contact", lastName: "" };
|
|
@@ -344,6 +487,7 @@ async function queueErpCreateContactIfEnabled(cms, dataSource, entityMap, input)
|
|
|
344
487
|
}
|
|
345
488
|
|
|
346
489
|
// src/plugins/erp/erp-product-sync.ts
|
|
490
|
+
init_erp_queue();
|
|
347
491
|
init_erp_config_enabled();
|
|
348
492
|
async function queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, product) {
|
|
349
493
|
try {
|
|
@@ -351,14 +495,21 @@ async function queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, produc
|
|
|
351
495
|
if (!sku) return;
|
|
352
496
|
const on = await isErpIntegrationEnabled(cms, dataSource, entityMap);
|
|
353
497
|
if (!on) return;
|
|
498
|
+
const rawMeta = product.metadata;
|
|
499
|
+
let metadata;
|
|
500
|
+
if (rawMeta && typeof rawMeta === "object" && !Array.isArray(rawMeta)) {
|
|
501
|
+
const { description: _d, ...rest } = rawMeta;
|
|
502
|
+
metadata = Object.keys(rest).length ? rest : void 0;
|
|
503
|
+
}
|
|
354
504
|
const payload = {
|
|
355
505
|
sku,
|
|
356
506
|
title: product.name || sku,
|
|
357
507
|
name: product.name,
|
|
358
|
-
description: typeof product.metadata === "object" && product.metadata && "description" in product.metadata ? String(product.metadata.description ?? "") : void 0,
|
|
359
508
|
hsn_number: product.hsn,
|
|
509
|
+
uom: product.uom != null && String(product.uom).trim() ? String(product.uom).trim() : void 0,
|
|
510
|
+
type: product.type === "service" ? "service" : "product",
|
|
360
511
|
is_active: product.status === "available",
|
|
361
|
-
metadata
|
|
512
|
+
metadata
|
|
362
513
|
};
|
|
363
514
|
await queueErp(cms, { kind: "productUpsert", product: payload });
|
|
364
515
|
} catch {
|
|
@@ -608,16 +759,58 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
608
759
|
const repo = dataSource.getRepository(entity);
|
|
609
760
|
const typeFilter = searchParams.get("type");
|
|
610
761
|
const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
|
|
762
|
+
if (resource === "media") {
|
|
763
|
+
const qb = repo.createQueryBuilder("m");
|
|
764
|
+
const parentIdParam = searchParams.get("parentId");
|
|
765
|
+
if (parentIdParam != null && parentIdParam !== "") {
|
|
766
|
+
const n = Number(parentIdParam);
|
|
767
|
+
if (!Number.isFinite(n)) return json({ error: "Invalid parentId" }, { status: 400 });
|
|
768
|
+
qb.where("m.parentId = :pid", { pid: n });
|
|
769
|
+
} else {
|
|
770
|
+
qb.where("m.parentId IS NULL");
|
|
771
|
+
}
|
|
772
|
+
if (search && typeof search === "string" && search.trim()) {
|
|
773
|
+
qb.andWhere("m.filename ILIKE :search", { search: `%${search.trim()}%` });
|
|
774
|
+
}
|
|
775
|
+
if (typeFilter) {
|
|
776
|
+
qb.andWhere(
|
|
777
|
+
new import_typeorm.Brackets((sq) => {
|
|
778
|
+
sq.where("m.kind = :folderKind", { folderKind: "folder" }).orWhere("m.mimeType LIKE :mtp", {
|
|
779
|
+
mtp: `${typeFilter}/%`
|
|
780
|
+
});
|
|
781
|
+
})
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
const allowedSort = ["filename", "createdAt", "id"];
|
|
785
|
+
const sf = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "filename";
|
|
786
|
+
const so = sortOrder === "DESC" ? "DESC" : "ASC";
|
|
787
|
+
qb.orderBy("CASE WHEN m.kind = :fk THEN 0 ELSE 1 END", "ASC").addOrderBy(`m.${sf}`, so).setParameter("fk", "folder").skip(skip).take(limit);
|
|
788
|
+
const [data2, total2] = await qb.getManyAndCount();
|
|
789
|
+
return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
|
|
790
|
+
}
|
|
611
791
|
const sortField = columnNames.has(sortFieldRaw) ? sortFieldRaw : "createdAt";
|
|
612
792
|
let where = {};
|
|
613
|
-
if (
|
|
614
|
-
const mediaWhere = {};
|
|
615
|
-
if (search) mediaWhere.filename = (0, import_typeorm.ILike)(`%${search}%`);
|
|
616
|
-
if (typeFilter) mediaWhere.mimeType = (0, import_typeorm.Like)(`${typeFilter}/%`);
|
|
617
|
-
where = Object.keys(mediaWhere).length > 0 ? mediaWhere : {};
|
|
618
|
-
} else if (search) {
|
|
793
|
+
if (search) {
|
|
619
794
|
where = buildSearchWhereClause(repo, search);
|
|
620
795
|
}
|
|
796
|
+
const intFilterKeys = ["productId", "attributeId", "taxId"];
|
|
797
|
+
const extraWhere = {};
|
|
798
|
+
for (const key of intFilterKeys) {
|
|
799
|
+
const v = searchParams.get(key);
|
|
800
|
+
if (v != null && v !== "" && columnNames.has(key)) {
|
|
801
|
+
const n = Number(v);
|
|
802
|
+
if (Number.isFinite(n)) extraWhere[key] = n;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (Object.keys(extraWhere).length > 0) {
|
|
806
|
+
if (Array.isArray(where)) {
|
|
807
|
+
where = where.map((w) => ({ ...w, ...extraWhere }));
|
|
808
|
+
} else if (where && typeof where === "object" && Object.keys(where).length > 0) {
|
|
809
|
+
where = { ...where, ...extraWhere };
|
|
810
|
+
} else {
|
|
811
|
+
where = extraWhere;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
621
814
|
const [data, total] = await repo.findAndCount({
|
|
622
815
|
skip,
|
|
623
816
|
take: limit,
|
|
@@ -637,6 +830,38 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
637
830
|
if (!body || typeof body !== "object" || Object.keys(body).length === 0) {
|
|
638
831
|
return json({ error: "Invalid request payload" }, { status: 400 });
|
|
639
832
|
}
|
|
833
|
+
if (resource === "media") {
|
|
834
|
+
const b = body;
|
|
835
|
+
const kind = b.kind === "folder" ? "folder" : "file";
|
|
836
|
+
b.kind = kind;
|
|
837
|
+
const fn = String(b.filename ?? "").trim().slice(0, 255);
|
|
838
|
+
if (!fn) return json({ error: "filename required" }, { status: 400 });
|
|
839
|
+
b.filename = fn;
|
|
840
|
+
let pid = null;
|
|
841
|
+
if (b.parentId != null && b.parentId !== "") {
|
|
842
|
+
const n = Number(b.parentId);
|
|
843
|
+
if (!Number.isFinite(n)) return json({ error: "Invalid parentId" }, { status: 400 });
|
|
844
|
+
pid = n;
|
|
845
|
+
}
|
|
846
|
+
b.parentId = pid;
|
|
847
|
+
const mediaRepo = dataSource.getRepository(entityMap.media);
|
|
848
|
+
if (pid != null) {
|
|
849
|
+
const parent = await mediaRepo.findOne({ where: { id: pid } });
|
|
850
|
+
if (!parent || parent.kind !== "folder") {
|
|
851
|
+
return json({ error: "parent must be a folder" }, { status: 400 });
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (kind === "folder") {
|
|
855
|
+
b.url = null;
|
|
856
|
+
b.mimeType = "inode/directory";
|
|
857
|
+
b.size = 0;
|
|
858
|
+
} else {
|
|
859
|
+
if (!b.url || typeof b.url !== "string") return json({ error: "url required for files" }, { status: 400 });
|
|
860
|
+
if (!b.mimeType || typeof b.mimeType !== "string") {
|
|
861
|
+
b.mimeType = "application/octet-stream";
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
640
865
|
const repo = dataSource.getRepository(entity);
|
|
641
866
|
sanitizeBodyForEntity(repo, body);
|
|
642
867
|
const created = await repo.save(repo.create(body));
|
|
@@ -909,6 +1134,11 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
909
1134
|
return updated2 ? json(updated2) : json({ message: "Not found" }, { status: 404 });
|
|
910
1135
|
}
|
|
911
1136
|
const updatePayload = rawBody && typeof rawBody === "object" ? pickColumnUpdates(repo, rawBody) : {};
|
|
1137
|
+
if (resource === "media") {
|
|
1138
|
+
const u = updatePayload;
|
|
1139
|
+
delete u.parentId;
|
|
1140
|
+
delete u.kind;
|
|
1141
|
+
}
|
|
912
1142
|
if (Object.keys(updatePayload).length > 0) {
|
|
913
1143
|
sanitizeBodyForEntity(repo, updatePayload);
|
|
914
1144
|
await repo.update(numericId, updatePayload);
|
|
@@ -1083,8 +1313,9 @@ function createUserAuthApiRouter(config) {
|
|
|
1083
1313
|
}
|
|
1084
1314
|
|
|
1085
1315
|
// src/api/cms-handlers.ts
|
|
1086
|
-
var
|
|
1316
|
+
var import_typeorm4 = require("typeorm");
|
|
1087
1317
|
init_email_queue();
|
|
1318
|
+
init_erp_queue();
|
|
1088
1319
|
|
|
1089
1320
|
// src/plugins/captcha/assert.ts
|
|
1090
1321
|
async function assertCaptchaOk(getCms, body, req, json) {
|
|
@@ -1102,6 +1333,194 @@ async function assertCaptchaOk(getCms, body, req, json) {
|
|
|
1102
1333
|
return json({ error: result.message }, { status: result.status });
|
|
1103
1334
|
}
|
|
1104
1335
|
|
|
1336
|
+
// src/lib/media-folder-path.ts
|
|
1337
|
+
function sanitizeMediaFolderPath(input) {
|
|
1338
|
+
if (input == null) return "";
|
|
1339
|
+
if (typeof input !== "string") return "";
|
|
1340
|
+
const segments = input.replace(/\\/g, "/").split("/").map((s) => s.trim()).filter(Boolean).filter((s) => s !== ".." && s !== ".");
|
|
1341
|
+
const joined = segments.join("/");
|
|
1342
|
+
return joined.length > 512 ? joined.slice(0, 512) : joined;
|
|
1343
|
+
}
|
|
1344
|
+
function sanitizeStorageSegment(name) {
|
|
1345
|
+
const s = name.replace(/[/\\]/g, "-").trim().slice(0, 255);
|
|
1346
|
+
return s || "item";
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/lib/media-parent-path.ts
|
|
1350
|
+
async function relativePathFromMediaParentId(dataSource, entityMap, parentId) {
|
|
1351
|
+
if (parentId == null) return "";
|
|
1352
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1353
|
+
const segments = [];
|
|
1354
|
+
let id = parentId;
|
|
1355
|
+
for (let d = 0; d < 64 && id != null; d++) {
|
|
1356
|
+
const row = await repo.findOne({ where: { id } });
|
|
1357
|
+
if (!row) break;
|
|
1358
|
+
const m = row;
|
|
1359
|
+
if (m.kind !== "folder") break;
|
|
1360
|
+
segments.unshift(sanitizeStorageSegment(m.filename));
|
|
1361
|
+
id = m.parentId ?? null;
|
|
1362
|
+
}
|
|
1363
|
+
return segments.join("/");
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// src/lib/media-zip-extract.ts
|
|
1367
|
+
var import_typeorm3 = require("typeorm");
|
|
1368
|
+
var ZIP_MIME_TYPES = /* @__PURE__ */ new Set(["application/zip", "application/x-zip-compressed"]);
|
|
1369
|
+
var MAX_ENTRIES = 2e3;
|
|
1370
|
+
var MAX_TOTAL_UNCOMPRESSED = 80 * 1024 * 1024;
|
|
1371
|
+
function isZipMedia(mime, filename) {
|
|
1372
|
+
if (mime && ZIP_MIME_TYPES.has(mime)) return true;
|
|
1373
|
+
return filename.toLowerCase().endsWith(".zip");
|
|
1374
|
+
}
|
|
1375
|
+
async function readBufferFromPublicUrl(url) {
|
|
1376
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
1377
|
+
const r = await fetch(url);
|
|
1378
|
+
if (!r.ok) throw new Error("Failed to download file");
|
|
1379
|
+
return Buffer.from(await r.arrayBuffer());
|
|
1380
|
+
}
|
|
1381
|
+
if (url.startsWith("/")) {
|
|
1382
|
+
const { readFile } = await import("fs/promises");
|
|
1383
|
+
const { join } = await import("path");
|
|
1384
|
+
const rel = url.replace(/^\/+/, "");
|
|
1385
|
+
return readFile(join(process.cwd(), "public", rel));
|
|
1386
|
+
}
|
|
1387
|
+
throw new Error("Unsupported media URL");
|
|
1388
|
+
}
|
|
1389
|
+
function sanitizeZipPath(entryName) {
|
|
1390
|
+
const norm = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
1391
|
+
for (const seg of norm) {
|
|
1392
|
+
if (seg === ".." || seg === ".") return null;
|
|
1393
|
+
}
|
|
1394
|
+
return norm;
|
|
1395
|
+
}
|
|
1396
|
+
function shouldSkipEntry(parts) {
|
|
1397
|
+
if (parts[0] === "__MACOSX") return true;
|
|
1398
|
+
const last = parts[parts.length - 1];
|
|
1399
|
+
if (last === ".DS_Store") return true;
|
|
1400
|
+
return false;
|
|
1401
|
+
}
|
|
1402
|
+
function guessMimeType(fileName) {
|
|
1403
|
+
const lower = fileName.toLowerCase();
|
|
1404
|
+
if (lower.endsWith(".png")) return "image/png";
|
|
1405
|
+
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
1406
|
+
if (lower.endsWith(".gif")) return "image/gif";
|
|
1407
|
+
if (lower.endsWith(".webp")) return "image/webp";
|
|
1408
|
+
if (lower.endsWith(".svg")) return "image/svg+xml";
|
|
1409
|
+
if (lower.endsWith(".pdf")) return "application/pdf";
|
|
1410
|
+
if (lower.endsWith(".txt")) return "text/plain";
|
|
1411
|
+
if (lower.endsWith(".json")) return "application/json";
|
|
1412
|
+
if (lower.endsWith(".zip")) return "application/zip";
|
|
1413
|
+
return "application/octet-stream";
|
|
1414
|
+
}
|
|
1415
|
+
async function findOrCreateFolder(dataSource, entityMap, parentId, name) {
|
|
1416
|
+
const safe = sanitizeStorageSegment(name);
|
|
1417
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1418
|
+
const where = parentId == null ? { kind: "folder", filename: safe, parentId: (0, import_typeorm3.IsNull)() } : { kind: "folder", filename: safe, parentId };
|
|
1419
|
+
const existing = await repo.findOne({ where });
|
|
1420
|
+
if (existing) return existing.id;
|
|
1421
|
+
const row = await repo.save(
|
|
1422
|
+
repo.create({
|
|
1423
|
+
kind: "folder",
|
|
1424
|
+
parentId,
|
|
1425
|
+
filename: safe,
|
|
1426
|
+
url: null,
|
|
1427
|
+
mimeType: "inode/directory",
|
|
1428
|
+
size: 0,
|
|
1429
|
+
alt: null,
|
|
1430
|
+
isPublic: false,
|
|
1431
|
+
deleted: false
|
|
1432
|
+
})
|
|
1433
|
+
);
|
|
1434
|
+
return row.id;
|
|
1435
|
+
}
|
|
1436
|
+
async function ensureFolderChain(dataSource, entityMap, rootParentId, pathSegments) {
|
|
1437
|
+
let pid = rootParentId;
|
|
1438
|
+
for (const seg of pathSegments) {
|
|
1439
|
+
if (!seg) continue;
|
|
1440
|
+
pid = await findOrCreateFolder(dataSource, entityMap, pid, seg);
|
|
1441
|
+
}
|
|
1442
|
+
return pid;
|
|
1443
|
+
}
|
|
1444
|
+
async function extractZipMediaIntoParentTree(opts) {
|
|
1445
|
+
const { dataSource, entityMap, zipMediaRow } = opts;
|
|
1446
|
+
const row = zipMediaRow;
|
|
1447
|
+
if (row.kind !== "file" || !row.url) throw new Error("Not a file");
|
|
1448
|
+
if (!isZipMedia(row.mimeType, row.filename)) throw new Error("Not a zip archive");
|
|
1449
|
+
const buffer = await readBufferFromPublicUrl(row.url);
|
|
1450
|
+
const { default: AdmZip } = await import("adm-zip");
|
|
1451
|
+
const zip = new AdmZip(buffer);
|
|
1452
|
+
const entries = zip.getEntries();
|
|
1453
|
+
if (entries.length > MAX_ENTRIES) throw new Error(`Too many zip entries (max ${MAX_ENTRIES})`);
|
|
1454
|
+
const rootParentId = row.parentId;
|
|
1455
|
+
const items = [];
|
|
1456
|
+
let totalUncompressed = 0;
|
|
1457
|
+
for (const e of entries) {
|
|
1458
|
+
const raw = e.entryName;
|
|
1459
|
+
const parts = sanitizeZipPath(raw);
|
|
1460
|
+
if (!parts || shouldSkipEntry(parts)) continue;
|
|
1461
|
+
const isDir = e.isDirectory || /\/$/.test(raw);
|
|
1462
|
+
let data = null;
|
|
1463
|
+
if (!isDir) {
|
|
1464
|
+
data = e.getData();
|
|
1465
|
+
totalUncompressed += data.length;
|
|
1466
|
+
if (totalUncompressed > MAX_TOTAL_UNCOMPRESSED) {
|
|
1467
|
+
throw new Error(`Uncompressed content exceeds limit (${MAX_TOTAL_UNCOMPRESSED} bytes)`);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
items.push({ parts, isDir, data });
|
|
1471
|
+
}
|
|
1472
|
+
items.sort((a, b) => {
|
|
1473
|
+
const da = a.parts.length;
|
|
1474
|
+
const db = b.parts.length;
|
|
1475
|
+
if (da !== db) return da - db;
|
|
1476
|
+
return a.parts.join("/").localeCompare(b.parts.join("/"));
|
|
1477
|
+
});
|
|
1478
|
+
let files = 0;
|
|
1479
|
+
let folderEntries = 0;
|
|
1480
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1481
|
+
for (const it of items) {
|
|
1482
|
+
if (it.isDir) {
|
|
1483
|
+
await ensureFolderChain(dataSource, entityMap, rootParentId, it.parts);
|
|
1484
|
+
folderEntries++;
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
const fileName = it.parts[it.parts.length - 1];
|
|
1488
|
+
const dirParts = it.parts.slice(0, -1);
|
|
1489
|
+
const parentFolderId = await ensureFolderChain(dataSource, entityMap, rootParentId, dirParts);
|
|
1490
|
+
const buf = it.data;
|
|
1491
|
+
const relBase = await relativePathFromMediaParentId(dataSource, entityMap, parentFolderId);
|
|
1492
|
+
const relativeUnderUploads = relBase ? `${relBase}/${fileName}` : fileName;
|
|
1493
|
+
const contentType = guessMimeType(fileName);
|
|
1494
|
+
let publicUrl;
|
|
1495
|
+
if (opts.storage) {
|
|
1496
|
+
publicUrl = await opts.storage.upload(buf, `uploads/${relativeUnderUploads}`, contentType);
|
|
1497
|
+
} else {
|
|
1498
|
+
const fs = await import("fs/promises");
|
|
1499
|
+
const pathMod = await import("path");
|
|
1500
|
+
const dir = pathMod.join(process.cwd(), opts.localUploadDir);
|
|
1501
|
+
const filePath = pathMod.join(dir, relativeUnderUploads);
|
|
1502
|
+
await fs.mkdir(pathMod.dirname(filePath), { recursive: true });
|
|
1503
|
+
await fs.writeFile(filePath, buf);
|
|
1504
|
+
publicUrl = `/${opts.localUploadDir.replace(/^\/+/, "").replace(/\\/g, "/")}/${relativeUnderUploads.replace(/\\/g, "/")}`;
|
|
1505
|
+
}
|
|
1506
|
+
await repo.save(
|
|
1507
|
+
repo.create({
|
|
1508
|
+
kind: "file",
|
|
1509
|
+
parentId: parentFolderId,
|
|
1510
|
+
filename: fileName,
|
|
1511
|
+
url: publicUrl,
|
|
1512
|
+
mimeType: contentType,
|
|
1513
|
+
size: buf.length,
|
|
1514
|
+
alt: null,
|
|
1515
|
+
isPublic: false,
|
|
1516
|
+
deleted: false
|
|
1517
|
+
})
|
|
1518
|
+
);
|
|
1519
|
+
files++;
|
|
1520
|
+
}
|
|
1521
|
+
return { files, folderEntries };
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1105
1524
|
// src/api/cms-handlers.ts
|
|
1106
1525
|
function createDashboardStatsHandler(config) {
|
|
1107
1526
|
const { dataSource, entityMap, json, requireAuth, requirePermission, requireEntityPermission } = config;
|
|
@@ -1119,26 +1538,209 @@ function createDashboardStatsHandler(config) {
|
|
|
1119
1538
|
try {
|
|
1120
1539
|
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3);
|
|
1121
1540
|
const repo = (name) => entityMap[name] ? dataSource.getRepository(entityMap[name]) : void 0;
|
|
1122
|
-
const [contactsCount, formsCount, formSubmissionsCount, usersCount, blogsCount, recentContacts, recentSubmissions] = await Promise.all([
|
|
1541
|
+
const [contactsCount, formsCount, formSubmissionsCount, usersCount, blogsCount, recentContacts, recentSubmissions, contactTypeRows] = await Promise.all([
|
|
1123
1542
|
repo("contacts")?.count() ?? 0,
|
|
1124
1543
|
repo("forms")?.count({ where: { deleted: false } }) ?? 0,
|
|
1125
1544
|
repo("form_submissions")?.count() ?? 0,
|
|
1126
1545
|
repo("users")?.count({ where: { deleted: false } }) ?? 0,
|
|
1127
1546
|
repo("blogs")?.count({ where: { deleted: false } }) ?? 0,
|
|
1128
|
-
repo("contacts")?.count({ where: { createdAt: (0,
|
|
1129
|
-
repo("form_submissions")?.count({ where: { createdAt: (0,
|
|
1547
|
+
repo("contacts")?.count({ where: { createdAt: (0, import_typeorm4.MoreThanOrEqual)(sevenDaysAgo) } }) ?? 0,
|
|
1548
|
+
repo("form_submissions")?.count({ where: { createdAt: (0, import_typeorm4.MoreThanOrEqual)(sevenDaysAgo) } }) ?? 0,
|
|
1549
|
+
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() ?? []
|
|
1130
1550
|
]);
|
|
1131
1551
|
return json({
|
|
1132
1552
|
contacts: { total: contactsCount, recent: recentContacts },
|
|
1133
1553
|
forms: { total: formsCount, submissions: formSubmissionsCount, recentSubmissions },
|
|
1134
1554
|
users: usersCount,
|
|
1135
|
-
blogs: blogsCount
|
|
1555
|
+
blogs: blogsCount,
|
|
1556
|
+
contactTypes: (contactTypeRows ?? []).map((row) => ({
|
|
1557
|
+
type: row.type || "unknown",
|
|
1558
|
+
count: Number(row.count || 0)
|
|
1559
|
+
}))
|
|
1136
1560
|
});
|
|
1137
1561
|
} catch (err) {
|
|
1138
1562
|
return json({ error: "Failed to fetch dashboard stats" }, { status: 500 });
|
|
1139
1563
|
}
|
|
1140
1564
|
};
|
|
1141
1565
|
}
|
|
1566
|
+
function toNum(v) {
|
|
1567
|
+
const n = typeof v === "number" ? v : Number(v ?? 0);
|
|
1568
|
+
return Number.isFinite(n) ? n : 0;
|
|
1569
|
+
}
|
|
1570
|
+
function toIsoDate(d) {
|
|
1571
|
+
return d.toISOString().slice(0, 10);
|
|
1572
|
+
}
|
|
1573
|
+
function createEcommerceAnalyticsHandler(config) {
|
|
1574
|
+
const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
|
|
1575
|
+
return async function GET(req) {
|
|
1576
|
+
const authErr = await requireAuth(req);
|
|
1577
|
+
if (authErr) return authErr;
|
|
1578
|
+
if (requireEntityPermission) {
|
|
1579
|
+
const pe = await requireEntityPermission(req, "analytics", "read");
|
|
1580
|
+
if (pe) return pe;
|
|
1581
|
+
}
|
|
1582
|
+
if (!entityMap.orders || !entityMap.order_items || !entityMap.payments || !entityMap.products) {
|
|
1583
|
+
return json({ error: "Store analytics unavailable" }, { status: 404 });
|
|
1584
|
+
}
|
|
1585
|
+
try {
|
|
1586
|
+
const url = new URL(req.url);
|
|
1587
|
+
const rawDays = parseInt(url.searchParams.get("days") || "30", 10);
|
|
1588
|
+
const days = Number.isFinite(rawDays) ? Math.min(365, Math.max(7, rawDays)) : 30;
|
|
1589
|
+
const end = /* @__PURE__ */ new Date();
|
|
1590
|
+
const start = new Date(end.getTime() - days * 24 * 60 * 60 * 1e3);
|
|
1591
|
+
const orderRepo = dataSource.getRepository(entityMap.orders);
|
|
1592
|
+
const paymentRepo = dataSource.getRepository(entityMap.payments);
|
|
1593
|
+
const itemRepo = dataSource.getRepository(entityMap.order_items);
|
|
1594
|
+
const productRepo = dataSource.getRepository(entityMap.products);
|
|
1595
|
+
const [salesOrders, returnOrders, replacementOrders, payments, products] = await Promise.all([
|
|
1596
|
+
orderRepo.find({
|
|
1597
|
+
where: { deleted: false, createdAt: (0, import_typeorm4.MoreThanOrEqual)(start), orderKind: "sale", status: (0, import_typeorm4.In)(["confirmed", "processing", "completed"]) },
|
|
1598
|
+
select: ["id", "contactId", "createdAt", "subtotal", "discount", "tax", "total", "status"]
|
|
1599
|
+
}),
|
|
1600
|
+
orderRepo.find({
|
|
1601
|
+
where: { deleted: false, createdAt: (0, import_typeorm4.MoreThanOrEqual)(start), orderKind: "return" },
|
|
1602
|
+
select: ["id", "createdAt", "total"]
|
|
1603
|
+
}),
|
|
1604
|
+
orderRepo.find({
|
|
1605
|
+
where: { deleted: false, createdAt: (0, import_typeorm4.MoreThanOrEqual)(start), orderKind: "replacement" },
|
|
1606
|
+
select: ["id", "createdAt", "total"]
|
|
1607
|
+
}),
|
|
1608
|
+
paymentRepo.find({
|
|
1609
|
+
where: { deleted: false, createdAt: (0, import_typeorm4.MoreThanOrEqual)(start) },
|
|
1610
|
+
select: ["id", "status", "method", "amount", "createdAt"]
|
|
1611
|
+
}),
|
|
1612
|
+
productRepo.find({
|
|
1613
|
+
where: { deleted: false },
|
|
1614
|
+
select: ["id", "name", "quantity"]
|
|
1615
|
+
})
|
|
1616
|
+
]);
|
|
1617
|
+
const saleOrderIds = salesOrders.map((o) => o.id);
|
|
1618
|
+
const orderItems = saleOrderIds.length ? await itemRepo.find({
|
|
1619
|
+
where: { orderId: (0, import_typeorm4.In)(saleOrderIds) },
|
|
1620
|
+
select: ["id", "orderId", "productId", "quantity", "total"]
|
|
1621
|
+
}) : [];
|
|
1622
|
+
const grossSales = salesOrders.reduce((sum, o) => sum + toNum(o.subtotal), 0);
|
|
1623
|
+
const discounts = salesOrders.reduce((sum, o) => sum + toNum(o.discount), 0);
|
|
1624
|
+
const taxes = salesOrders.reduce((sum, o) => sum + toNum(o.tax), 0);
|
|
1625
|
+
const returnsValue = returnOrders.reduce((sum, o) => sum + toNum(o.total), 0);
|
|
1626
|
+
const replacementsValue = replacementOrders.reduce((sum, o) => sum + toNum(o.total), 0);
|
|
1627
|
+
const netSales = grossSales - discounts - returnsValue;
|
|
1628
|
+
const ordersCount = salesOrders.length;
|
|
1629
|
+
const aov = ordersCount > 0 ? netSales / ordersCount : 0;
|
|
1630
|
+
const returnRate = ordersCount > 0 ? returnOrders.length / ordersCount * 100 : 0;
|
|
1631
|
+
const salesByDate = /* @__PURE__ */ new Map();
|
|
1632
|
+
const returnsByDate = /* @__PURE__ */ new Map();
|
|
1633
|
+
for (const o of salesOrders) {
|
|
1634
|
+
const key = toIsoDate(new Date(o.createdAt));
|
|
1635
|
+
const row = salesByDate.get(key) ?? { value: 0, orders: 0 };
|
|
1636
|
+
row.value += toNum(o.total);
|
|
1637
|
+
row.orders += 1;
|
|
1638
|
+
salesByDate.set(key, row);
|
|
1639
|
+
}
|
|
1640
|
+
for (const o of returnOrders) {
|
|
1641
|
+
const key = toIsoDate(new Date(o.createdAt));
|
|
1642
|
+
const row = returnsByDate.get(key) ?? { value: 0, count: 0 };
|
|
1643
|
+
row.value += toNum(o.total);
|
|
1644
|
+
row.count += 1;
|
|
1645
|
+
returnsByDate.set(key, row);
|
|
1646
|
+
}
|
|
1647
|
+
const salesOverTime = [];
|
|
1648
|
+
const returnsTrend = [];
|
|
1649
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
1650
|
+
const d = new Date(end.getTime() - i * 24 * 60 * 60 * 1e3);
|
|
1651
|
+
const key = toIsoDate(d);
|
|
1652
|
+
const sales = salesByDate.get(key) ?? { value: 0, orders: 0 };
|
|
1653
|
+
const returns = returnsByDate.get(key) ?? { value: 0, count: 0 };
|
|
1654
|
+
salesOverTime.push({ date: key, value: Number(sales.value.toFixed(2)), orders: sales.orders });
|
|
1655
|
+
returnsTrend.push({ date: key, value: Number(returns.value.toFixed(2)), count: returns.count });
|
|
1656
|
+
}
|
|
1657
|
+
const productNameMap = /* @__PURE__ */ new Map();
|
|
1658
|
+
for (const p of products) productNameMap.set(Number(p.id), (p.name || `Product #${p.id}`).trim());
|
|
1659
|
+
const productAgg = /* @__PURE__ */ new Map();
|
|
1660
|
+
for (const item of orderItems) {
|
|
1661
|
+
const productId = Number(item.productId);
|
|
1662
|
+
const productName = productNameMap.get(productId) || `Product #${productId}`;
|
|
1663
|
+
const row = productAgg.get(productId) ?? { name: productName, units: 0, sales: 0 };
|
|
1664
|
+
row.units += toNum(item.quantity);
|
|
1665
|
+
row.sales += toNum(item.total);
|
|
1666
|
+
productAgg.set(productId, row);
|
|
1667
|
+
}
|
|
1668
|
+
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)) }));
|
|
1669
|
+
const allSaleOrderContactIds = Array.from(new Set(salesOrders.map((o) => Number(o.contactId)).filter((n) => Number.isInteger(n) && n > 0)));
|
|
1670
|
+
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() : [];
|
|
1671
|
+
const countMap = /* @__PURE__ */ new Map();
|
|
1672
|
+
for (const c of allTimeCounts) countMap.set(Number(c.contactId), Number(c.total));
|
|
1673
|
+
const purchasingCustomers = allSaleOrderContactIds.length;
|
|
1674
|
+
const returningCustomers = allSaleOrderContactIds.filter((id) => (countMap.get(id) ?? 0) > 1).length;
|
|
1675
|
+
const newCustomers = Math.max(0, purchasingCustomers - returningCustomers);
|
|
1676
|
+
const returningCustomerRate = purchasingCustomers > 0 ? returningCustomers / purchasingCustomers * 100 : 0;
|
|
1677
|
+
const totalPayments = payments.length;
|
|
1678
|
+
const completedPayments = payments.filter((p) => p.status === "completed").length;
|
|
1679
|
+
const failedPayments = payments.filter((p) => p.status === "failed").length;
|
|
1680
|
+
const paymentSuccessRate = totalPayments > 0 ? completedPayments / totalPayments * 100 : 0;
|
|
1681
|
+
const paymentMethodMap = /* @__PURE__ */ new Map();
|
|
1682
|
+
for (const p of payments) {
|
|
1683
|
+
const method = (p.method || "unknown").toLowerCase();
|
|
1684
|
+
const row = paymentMethodMap.get(method) ?? { method, count: 0, amount: 0 };
|
|
1685
|
+
row.count += 1;
|
|
1686
|
+
row.amount += toNum(p.amount);
|
|
1687
|
+
paymentMethodMap.set(method, row);
|
|
1688
|
+
}
|
|
1689
|
+
const paymentMethods = Array.from(paymentMethodMap.values()).sort((a, b) => b.count - a.count).map((p) => ({ ...p, amount: Number(p.amount.toFixed(2)) }));
|
|
1690
|
+
const totalInventory = products.reduce((sum, p) => sum + toNum(p.quantity), 0);
|
|
1691
|
+
const outOfStockCount = products.filter((p) => toNum(p.quantity) <= 0).length;
|
|
1692
|
+
const lowStockCount = products.filter((p) => toNum(p.quantity) > 0 && toNum(p.quantity) <= 5).length;
|
|
1693
|
+
const inventoryRisk = {
|
|
1694
|
+
outOfStockCount,
|
|
1695
|
+
lowStockCount,
|
|
1696
|
+
totalInventory
|
|
1697
|
+
};
|
|
1698
|
+
return json({
|
|
1699
|
+
rangeDays: days,
|
|
1700
|
+
kpis: {
|
|
1701
|
+
netSales: Number(netSales.toFixed(2)),
|
|
1702
|
+
grossSales: Number(grossSales.toFixed(2)),
|
|
1703
|
+
ordersPlaced: ordersCount,
|
|
1704
|
+
averageOrderValue: Number(aov.toFixed(2)),
|
|
1705
|
+
returningCustomerRate: Number(returningCustomerRate.toFixed(2)),
|
|
1706
|
+
returnRate: Number(returnRate.toFixed(2)),
|
|
1707
|
+
returnValue: Number(returnsValue.toFixed(2)),
|
|
1708
|
+
discounts: Number(discounts.toFixed(2)),
|
|
1709
|
+
taxes: Number(taxes.toFixed(2)),
|
|
1710
|
+
paymentSuccessRate: Number(paymentSuccessRate.toFixed(2))
|
|
1711
|
+
},
|
|
1712
|
+
salesOverTime,
|
|
1713
|
+
topProducts,
|
|
1714
|
+
customerMix: {
|
|
1715
|
+
newCustomers,
|
|
1716
|
+
returningCustomers,
|
|
1717
|
+
repeatPurchaseRate: Number(returningCustomerRate.toFixed(2))
|
|
1718
|
+
},
|
|
1719
|
+
returnsTrend,
|
|
1720
|
+
paymentPerformance: {
|
|
1721
|
+
successCount: completedPayments,
|
|
1722
|
+
failedCount: failedPayments,
|
|
1723
|
+
successRate: Number(paymentSuccessRate.toFixed(2)),
|
|
1724
|
+
methods: paymentMethods
|
|
1725
|
+
},
|
|
1726
|
+
conversionProxy: {
|
|
1727
|
+
sessions: 0,
|
|
1728
|
+
checkoutStarted: 0,
|
|
1729
|
+
ordersPlaced: ordersCount
|
|
1730
|
+
},
|
|
1731
|
+
salesBreakdown: {
|
|
1732
|
+
sales: { count: ordersCount, value: Number(grossSales.toFixed(2)) },
|
|
1733
|
+
returns: { count: returnOrders.length, value: Number(returnsValue.toFixed(2)) },
|
|
1734
|
+
replacements: { count: replacementOrders.length, value: Number(replacementsValue.toFixed(2)) }
|
|
1735
|
+
},
|
|
1736
|
+
geoPerformance: [],
|
|
1737
|
+
inventoryRisk
|
|
1738
|
+
});
|
|
1739
|
+
} catch {
|
|
1740
|
+
return json({ error: "Failed to fetch ecommerce analytics" }, { status: 500 });
|
|
1741
|
+
}
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1142
1744
|
function createAnalyticsHandlers(config) {
|
|
1143
1745
|
const { json, getAnalyticsData, getPropertyId, getPermissions } = config;
|
|
1144
1746
|
return {
|
|
@@ -1170,8 +1772,27 @@ function createAnalyticsHandlers(config) {
|
|
|
1170
1772
|
};
|
|
1171
1773
|
}
|
|
1172
1774
|
function createUploadHandler(config) {
|
|
1173
|
-
const {
|
|
1174
|
-
|
|
1775
|
+
const {
|
|
1776
|
+
json,
|
|
1777
|
+
requireAuth,
|
|
1778
|
+
requireEntityPermission,
|
|
1779
|
+
storage,
|
|
1780
|
+
localUploadDir = "public/uploads",
|
|
1781
|
+
allowedTypes,
|
|
1782
|
+
maxSizeBytes = 10 * 1024 * 1024,
|
|
1783
|
+
dataSource,
|
|
1784
|
+
entityMap
|
|
1785
|
+
} = config;
|
|
1786
|
+
const allowed = allowedTypes ?? [
|
|
1787
|
+
"image/jpeg",
|
|
1788
|
+
"image/png",
|
|
1789
|
+
"image/gif",
|
|
1790
|
+
"image/webp",
|
|
1791
|
+
"application/pdf",
|
|
1792
|
+
"text/plain",
|
|
1793
|
+
"application/zip",
|
|
1794
|
+
"application/x-zip-compressed"
|
|
1795
|
+
];
|
|
1175
1796
|
return async function POST(req) {
|
|
1176
1797
|
const authErr = await requireAuth(req);
|
|
1177
1798
|
if (authErr) return authErr;
|
|
@@ -1184,28 +1805,92 @@ function createUploadHandler(config) {
|
|
|
1184
1805
|
const file = formData.get("file");
|
|
1185
1806
|
if (!file) return json({ error: "No file uploaded" }, { status: 400 });
|
|
1186
1807
|
if (!allowed.includes(file.type)) return json({ error: "File type not allowed" }, { status: 400 });
|
|
1187
|
-
|
|
1808
|
+
const defaultMax = 10 * 1024 * 1024;
|
|
1809
|
+
const maxZipBytes = 80 * 1024 * 1024;
|
|
1810
|
+
const baseMax = maxSizeBytes ?? defaultMax;
|
|
1811
|
+
const effectiveMax = file.type === "application/zip" || file.type === "application/x-zip-compressed" ? Math.max(baseMax, maxZipBytes) : baseMax;
|
|
1812
|
+
if (file.size > effectiveMax) return json({ error: "File size exceeds limit" }, { status: 400 });
|
|
1813
|
+
const parentRaw = formData.get("parentId");
|
|
1814
|
+
let parentId = null;
|
|
1815
|
+
if (parentRaw != null && String(parentRaw).trim() !== "") {
|
|
1816
|
+
const n = Number(parentRaw);
|
|
1817
|
+
if (!Number.isFinite(n)) return json({ error: "Invalid parentId" }, { status: 400 });
|
|
1818
|
+
parentId = n;
|
|
1819
|
+
}
|
|
1820
|
+
let folder = "";
|
|
1821
|
+
if (parentId != null) {
|
|
1822
|
+
if (!dataSource || !entityMap?.media) {
|
|
1823
|
+
return json({ error: "Upload handler needs dataSource and entityMap for folder uploads" }, { status: 400 });
|
|
1824
|
+
}
|
|
1825
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1826
|
+
const p = await repo.findOne({ where: { id: parentId } });
|
|
1827
|
+
if (!p || p.kind !== "folder") {
|
|
1828
|
+
return json({ error: "parent must be a folder" }, { status: 400 });
|
|
1829
|
+
}
|
|
1830
|
+
folder = await relativePathFromMediaParentId(dataSource, entityMap, parentId);
|
|
1831
|
+
} else {
|
|
1832
|
+
const folderRawLegacy = formData.get("folder") ?? formData.get("folderPath");
|
|
1833
|
+
if (folderRawLegacy && typeof folderRawLegacy === "string" && folderRawLegacy.trim()) {
|
|
1834
|
+
folder = sanitizeMediaFolderPath(folderRawLegacy);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1188
1837
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
1189
1838
|
const fileName = `${Date.now()}-${file.name}`;
|
|
1190
1839
|
const contentType = file.type || "application/octet-stream";
|
|
1840
|
+
const relativeUnderUploads = folder ? `${folder}/${fileName}` : fileName;
|
|
1191
1841
|
const raw = typeof storage === "function" ? storage() : storage;
|
|
1192
1842
|
const storageService = raw instanceof Promise ? await raw : raw;
|
|
1193
1843
|
if (storageService) {
|
|
1194
|
-
const fileUrl = await storageService.upload(buffer, `uploads/${
|
|
1195
|
-
return json({ filePath: fileUrl });
|
|
1844
|
+
const fileUrl = await storageService.upload(buffer, `uploads/${relativeUnderUploads}`, contentType);
|
|
1845
|
+
return json({ filePath: fileUrl, parentId });
|
|
1196
1846
|
}
|
|
1197
1847
|
const fs = await import("fs/promises");
|
|
1198
1848
|
const path = await import("path");
|
|
1199
1849
|
const dir = path.join(process.cwd(), localUploadDir);
|
|
1200
|
-
|
|
1201
|
-
|
|
1850
|
+
const filePath = path.join(dir, relativeUnderUploads);
|
|
1851
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
1202
1852
|
await fs.writeFile(filePath, buffer);
|
|
1203
|
-
|
|
1853
|
+
const urlRel = `${localUploadDir.replace(/^\/+/, "").replace(/\\/g, "/")}/${relativeUnderUploads.replace(/\\/g, "/")}`;
|
|
1854
|
+
return json({ filePath: `/${urlRel}`, parentId });
|
|
1204
1855
|
} catch (err) {
|
|
1205
1856
|
return json({ error: "File upload failed" }, { status: 500 });
|
|
1206
1857
|
}
|
|
1207
1858
|
};
|
|
1208
1859
|
}
|
|
1860
|
+
function createMediaZipExtractHandler(config) {
|
|
1861
|
+
const { json, requireAuth, requireEntityPermission, storage, localUploadDir = "public/uploads", dataSource, entityMap } = config;
|
|
1862
|
+
return async function POST(_req, zipMediaId) {
|
|
1863
|
+
const authErr = await requireAuth(_req);
|
|
1864
|
+
if (authErr) return authErr;
|
|
1865
|
+
if (requireEntityPermission) {
|
|
1866
|
+
const pe = await requireEntityPermission(_req, "media", "create");
|
|
1867
|
+
if (pe) return pe;
|
|
1868
|
+
}
|
|
1869
|
+
if (!dataSource || !entityMap?.media) {
|
|
1870
|
+
return json({ error: "Media extract requires dataSource and entityMap" }, { status: 500 });
|
|
1871
|
+
}
|
|
1872
|
+
const id = Number(zipMediaId);
|
|
1873
|
+
if (!Number.isFinite(id)) return json({ error: "Invalid id" }, { status: 400 });
|
|
1874
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1875
|
+
const row = await repo.findOne({ where: { id } });
|
|
1876
|
+
if (!row) return json({ error: "Not found" }, { status: 404 });
|
|
1877
|
+
try {
|
|
1878
|
+
const raw = typeof storage === "function" ? storage() : storage;
|
|
1879
|
+
const storageService = raw instanceof Promise ? await raw : raw;
|
|
1880
|
+
const result = await extractZipMediaIntoParentTree({
|
|
1881
|
+
dataSource,
|
|
1882
|
+
entityMap,
|
|
1883
|
+
zipMediaRow: row,
|
|
1884
|
+
storage: storageService,
|
|
1885
|
+
localUploadDir
|
|
1886
|
+
});
|
|
1887
|
+
return json({ ok: true, ...result });
|
|
1888
|
+
} catch (e) {
|
|
1889
|
+
const msg = e instanceof Error ? e.message : "Extract failed";
|
|
1890
|
+
return json({ error: msg }, { status: 400 });
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1209
1894
|
function createBlogBySlugHandler(config) {
|
|
1210
1895
|
const { dataSource, entityMap, json } = config;
|
|
1211
1896
|
return async function GET(_req, slug) {
|
|
@@ -1618,7 +2303,7 @@ function createUsersApiHandlers(config) {
|
|
|
1618
2303
|
const sortField = url.searchParams.get("sortField") || "createdAt";
|
|
1619
2304
|
const sortOrder = url.searchParams.get("sortOrder") === "desc" ? "DESC" : "ASC";
|
|
1620
2305
|
const search = url.searchParams.get("search");
|
|
1621
|
-
const where = search ? [{ name: (0,
|
|
2306
|
+
const where = search ? [{ name: (0, import_typeorm4.ILike)(`%${search}%`) }, { email: (0, import_typeorm4.ILike)(`%${search}%`) }] : {};
|
|
1622
2307
|
const [data, total] = await userRepo().findAndCount({
|
|
1623
2308
|
skip,
|
|
1624
2309
|
take: limit,
|
|
@@ -1978,7 +2663,7 @@ function createChatHandlers(config) {
|
|
|
1978
2663
|
if (contextParts.length === 0) {
|
|
1979
2664
|
const terms = getQueryTerms(message);
|
|
1980
2665
|
if (terms.length > 0) {
|
|
1981
|
-
const conditions = terms.map((t) => ({ content: (0,
|
|
2666
|
+
const conditions = terms.map((t) => ({ content: (0, import_typeorm4.ILike)(`%${t}%`) }));
|
|
1982
2667
|
const chunks = await chunkRepo().find({
|
|
1983
2668
|
where: conditions,
|
|
1984
2669
|
take: KB_CHUNK_LIMIT,
|
|
@@ -2347,6 +3032,7 @@ function createCmsApiHandler(config) {
|
|
|
2347
3032
|
getCms,
|
|
2348
3033
|
userAuth: userAuthConfig,
|
|
2349
3034
|
dashboard,
|
|
3035
|
+
ecommerceAnalytics,
|
|
2350
3036
|
analytics: analyticsConfig,
|
|
2351
3037
|
upload,
|
|
2352
3038
|
blogBySlug,
|
|
@@ -2413,8 +3099,28 @@ function createCmsApiHandler(config) {
|
|
|
2413
3099
|
});
|
|
2414
3100
|
const userAuthRouter = userAuth ? createUserAuthApiRouter(userAuth) : null;
|
|
2415
3101
|
const dashboardGet = dashboard ? createDashboardStatsHandler(mergePerm(dashboard) ?? dashboard) : null;
|
|
3102
|
+
const ecommerceAnalyticsResolved = mergePerm(
|
|
3103
|
+
ecommerceAnalytics ?? {
|
|
3104
|
+
dataSource,
|
|
3105
|
+
entityMap,
|
|
3106
|
+
json: config.json,
|
|
3107
|
+
requireAuth: config.requireAuth
|
|
3108
|
+
}
|
|
3109
|
+
) ?? {
|
|
3110
|
+
dataSource,
|
|
3111
|
+
entityMap,
|
|
3112
|
+
json: config.json,
|
|
3113
|
+
requireAuth: config.requireAuth
|
|
3114
|
+
};
|
|
3115
|
+
const ecommerceAnalyticsGet = createEcommerceAnalyticsHandler(ecommerceAnalyticsResolved);
|
|
2416
3116
|
const analyticsHandlers = analytics ? createAnalyticsHandlers(analytics) : null;
|
|
2417
|
-
const
|
|
3117
|
+
const uploadMerged = upload ? {
|
|
3118
|
+
...mergePerm(upload) ?? upload,
|
|
3119
|
+
dataSource: upload.dataSource ?? dataSource,
|
|
3120
|
+
entityMap: upload.entityMap ?? entityMap
|
|
3121
|
+
} : null;
|
|
3122
|
+
const uploadPost = uploadMerged ? createUploadHandler(uploadMerged) : null;
|
|
3123
|
+
const zipExtractPost = uploadMerged ? createMediaZipExtractHandler(uploadMerged) : null;
|
|
2418
3124
|
const blogBySlugGet = blogBySlug ? createBlogBySlugHandler(blogBySlug) : null;
|
|
2419
3125
|
const formBySlugGet = formBySlug ? createFormBySlugHandler(formBySlug) : null;
|
|
2420
3126
|
const formSaveHandlers = formSaveConfig ? createFormSaveHandlers(mergePerm(formSaveConfig) ?? formSaveConfig) : null;
|
|
@@ -2463,6 +3169,11 @@ function createCmsApiHandler(config) {
|
|
|
2463
3169
|
if (path[0] === "dashboard" && path[1] === "stats" && path.length === 2 && method === "GET" && dashboardGet) {
|
|
2464
3170
|
return dashboardGet(req);
|
|
2465
3171
|
}
|
|
3172
|
+
if (path[0] === "dashboard" && path[1] === "ecommerce" && path.length === 2 && method === "GET" && ecommerceAnalyticsGet) {
|
|
3173
|
+
const g = await analyticsGate();
|
|
3174
|
+
if (g) return g;
|
|
3175
|
+
return ecommerceAnalyticsGet(req);
|
|
3176
|
+
}
|
|
2466
3177
|
if (path[0] === "analytics" && analyticsHandlers) {
|
|
2467
3178
|
if (path.length === 1 && method === "GET") {
|
|
2468
3179
|
const g = await analyticsGate();
|
|
@@ -2481,6 +3192,9 @@ function createCmsApiHandler(config) {
|
|
|
2481
3192
|
}
|
|
2482
3193
|
}
|
|
2483
3194
|
if (path[0] === "upload" && path.length === 1 && method === "POST" && uploadPost) return uploadPost(req);
|
|
3195
|
+
if (path[0] === "media" && path[1] === "extract" && path.length === 3 && method === "POST" && zipExtractPost) {
|
|
3196
|
+
return zipExtractPost(req, path[2]);
|
|
3197
|
+
}
|
|
2484
3198
|
if (path[0] === "blogs" && path[1] === "slug" && path.length === 3 && method === "GET" && blogBySlugGet) {
|
|
2485
3199
|
return blogBySlugGet(req, path[2]);
|
|
2486
3200
|
}
|
|
@@ -2563,6 +3277,29 @@ function createCmsApiHandler(config) {
|
|
|
2563
3277
|
if (!Number.isFinite(oid)) return config.json({ error: "Invalid id" }, { status: 400 });
|
|
2564
3278
|
return streamOrderInvoicePdf2(cms, dataSource, entityMap, oid, {});
|
|
2565
3279
|
}
|
|
3280
|
+
if (path[0] === "orders" && path.length === 3 && path[2] === "repost-erp" && getCms) {
|
|
3281
|
+
const a = await config.requireAuth(req);
|
|
3282
|
+
if (a) return a;
|
|
3283
|
+
if (perm) {
|
|
3284
|
+
const pe = await perm(req, "orders", method === "GET" ? "read" : "update");
|
|
3285
|
+
if (pe) return pe;
|
|
3286
|
+
}
|
|
3287
|
+
const oid = Number(path[1]);
|
|
3288
|
+
if (!Number.isFinite(oid)) return config.json({ error: "Invalid id" }, { status: 400 });
|
|
3289
|
+
const cms = await getCms();
|
|
3290
|
+
const { isErpIntegrationEnabled: isErpIntegrationEnabled3 } = await Promise.resolve().then(() => (init_erp_config_enabled(), erp_config_enabled_exports));
|
|
3291
|
+
const enabled = await isErpIntegrationEnabled3(cms, dataSource, entityMap);
|
|
3292
|
+
if (method === "GET") {
|
|
3293
|
+
return config.json({ enabled });
|
|
3294
|
+
}
|
|
3295
|
+
if (method === "POST") {
|
|
3296
|
+
if (!enabled) return config.json({ error: "ERP integration is disabled" }, { status: 409 });
|
|
3297
|
+
const { queueErpPaidOrderForOrderId: queueErpPaidOrderForOrderId2 } = await Promise.resolve().then(() => (init_paid_order_erp(), paid_order_erp_exports));
|
|
3298
|
+
await queueErpPaidOrderForOrderId2(cms, dataSource, entityMap, oid);
|
|
3299
|
+
return config.json({ ok: true });
|
|
3300
|
+
}
|
|
3301
|
+
return config.json({ error: "Method not allowed" }, { status: 405 });
|
|
3302
|
+
}
|
|
2566
3303
|
if (path.length === 0) return config.json({ error: "Not found" }, { status: 404 });
|
|
2567
3304
|
const resource = resolveResource(path[0]);
|
|
2568
3305
|
if (!crudResources.includes(resource)) return config.json({ error: "Invalid resource" }, { status: 400 });
|
|
@@ -2595,7 +3332,7 @@ function createCmsApiHandler(config) {
|
|
|
2595
3332
|
}
|
|
2596
3333
|
|
|
2597
3334
|
// src/api/storefront-handlers.ts
|
|
2598
|
-
var
|
|
3335
|
+
var import_typeorm6 = require("typeorm");
|
|
2599
3336
|
|
|
2600
3337
|
// src/lib/is-valid-signup-email.ts
|
|
2601
3338
|
var MAX_EMAIL = 254;
|
|
@@ -2855,7 +3592,7 @@ async function queueSms(cms, payload) {
|
|
|
2855
3592
|
|
|
2856
3593
|
// src/lib/otp-challenge.ts
|
|
2857
3594
|
var import_crypto = require("crypto");
|
|
2858
|
-
var
|
|
3595
|
+
var import_typeorm5 = require("typeorm");
|
|
2859
3596
|
var OTP_TTL_MS = 10 * 60 * 1e3;
|
|
2860
3597
|
var MAX_SENDS_PER_HOUR = 5;
|
|
2861
3598
|
var MAX_VERIFY_ATTEMPTS = 8;
|
|
@@ -2889,7 +3626,7 @@ function normalizePhoneE164(raw, defaultCountryCode) {
|
|
|
2889
3626
|
async function countRecentOtpSends(dataSource, entityMap, purpose, identifier, since) {
|
|
2890
3627
|
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
2891
3628
|
return repo.count({
|
|
2892
|
-
where: { purpose, identifier, createdAt: (0,
|
|
3629
|
+
where: { purpose, identifier, createdAt: (0, import_typeorm5.MoreThan)(since) }
|
|
2893
3630
|
});
|
|
2894
3631
|
}
|
|
2895
3632
|
async function createOtpChallenge(dataSource, entityMap, input) {
|
|
@@ -2903,7 +3640,7 @@ async function createOtpChallenge(dataSource, entityMap, input) {
|
|
|
2903
3640
|
await repo.delete({
|
|
2904
3641
|
purpose,
|
|
2905
3642
|
identifier,
|
|
2906
|
-
consumedAt: (0,
|
|
3643
|
+
consumedAt: (0, import_typeorm5.IsNull)()
|
|
2907
3644
|
});
|
|
2908
3645
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
2909
3646
|
const codeHash = hashOtpCode(code, purpose, identifier, pepper);
|
|
@@ -2924,7 +3661,7 @@ async function verifyAndConsumeOtpChallenge(dataSource, entityMap, input) {
|
|
|
2924
3661
|
const { purpose, identifier, code, pepper } = input;
|
|
2925
3662
|
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
2926
3663
|
const row = await repo.findOne({
|
|
2927
|
-
where: { purpose, identifier, consumedAt: (0,
|
|
3664
|
+
where: { purpose, identifier, consumedAt: (0, import_typeorm5.IsNull)() },
|
|
2928
3665
|
order: { id: "DESC" }
|
|
2929
3666
|
});
|
|
2930
3667
|
if (!row) {
|
|
@@ -2991,6 +3728,152 @@ function createStorefrontApiHandler(config) {
|
|
|
2991
3728
|
const tokenRepo = () => dataSource.getRepository(entityMap.password_reset_tokens);
|
|
2992
3729
|
const collectionRepo = () => dataSource.getRepository(entityMap.collections);
|
|
2993
3730
|
const groupRepo = () => dataSource.getRepository(entityMap.user_groups);
|
|
3731
|
+
const configRepo = () => dataSource.getRepository(entityMap.configs);
|
|
3732
|
+
const CART_CHECKOUT_RELATIONS = ["items", "items.product", "items.product.taxes", "items.product.taxes.tax"];
|
|
3733
|
+
function roundMoney2(n) {
|
|
3734
|
+
return Math.round(n * 100) / 100;
|
|
3735
|
+
}
|
|
3736
|
+
async function getStoreDefaultTaxRate() {
|
|
3737
|
+
const rows = await configRepo().find({ where: { settings: "store", deleted: false } });
|
|
3738
|
+
for (const row of rows) {
|
|
3739
|
+
const r = row;
|
|
3740
|
+
if (r.key === "defaultTaxRate") {
|
|
3741
|
+
const n = parseFloat(String(r.value ?? "").trim());
|
|
3742
|
+
return Number.isFinite(n) && n >= 0 ? n : null;
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
return null;
|
|
3746
|
+
}
|
|
3747
|
+
function computeTaxForProductLine(p, lineSubtotal, defaultRate) {
|
|
3748
|
+
const pts = p.taxes ?? [];
|
|
3749
|
+
const activePts = pts.filter((pt) => {
|
|
3750
|
+
const t = pt.tax;
|
|
3751
|
+
return t != null && t.active !== false;
|
|
3752
|
+
});
|
|
3753
|
+
if (activePts.length) {
|
|
3754
|
+
let sumRate = 0;
|
|
3755
|
+
const slugs = [];
|
|
3756
|
+
for (const pt of activePts) {
|
|
3757
|
+
const t = pt.tax;
|
|
3758
|
+
const r = Number(pt.rate != null && pt.rate !== "" ? pt.rate : t.rate ?? 0);
|
|
3759
|
+
if (Number.isFinite(r)) sumRate += r;
|
|
3760
|
+
const slug = String(t.slug ?? "").trim();
|
|
3761
|
+
if (slug) slugs.push(slug);
|
|
3762
|
+
}
|
|
3763
|
+
const tax = roundMoney2(lineSubtotal * sumRate / 100);
|
|
3764
|
+
return {
|
|
3765
|
+
tax,
|
|
3766
|
+
taxRate: sumRate > 0 ? roundMoney2(sumRate) : null,
|
|
3767
|
+
taxCode: slugs.length ? [...new Set(slugs)].sort().join(",") : null
|
|
3768
|
+
};
|
|
3769
|
+
}
|
|
3770
|
+
if (defaultRate != null && defaultRate > 0) {
|
|
3771
|
+
return {
|
|
3772
|
+
tax: roundMoney2(lineSubtotal * defaultRate / 100),
|
|
3773
|
+
taxRate: roundMoney2(defaultRate),
|
|
3774
|
+
taxCode: null
|
|
3775
|
+
};
|
|
3776
|
+
}
|
|
3777
|
+
return { tax: 0, taxRate: null, taxCode: null };
|
|
3778
|
+
}
|
|
3779
|
+
function parseInlineAddress(raw) {
|
|
3780
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
3781
|
+
const o = raw;
|
|
3782
|
+
const line1 = String(o.line1 ?? "").trim();
|
|
3783
|
+
if (!line1) return null;
|
|
3784
|
+
return {
|
|
3785
|
+
line1,
|
|
3786
|
+
line2: o.line2 != null ? String(o.line2) : "",
|
|
3787
|
+
city: o.city != null ? String(o.city) : "",
|
|
3788
|
+
state: o.state != null ? String(o.state) : "",
|
|
3789
|
+
postalCode: o.postalCode != null ? String(o.postalCode) : "",
|
|
3790
|
+
country: o.country != null ? String(o.country) : ""
|
|
3791
|
+
};
|
|
3792
|
+
}
|
|
3793
|
+
function intFromBody(v) {
|
|
3794
|
+
if (typeof v === "number" && Number.isInteger(v)) return v;
|
|
3795
|
+
if (typeof v === "string" && /^\d+$/.test(v)) return parseInt(v, 10);
|
|
3796
|
+
return void 0;
|
|
3797
|
+
}
|
|
3798
|
+
async function resolveCheckoutAddress(contactId, idVal, inlineVal) {
|
|
3799
|
+
const aid = intFromBody(idVal);
|
|
3800
|
+
if (aid != null) {
|
|
3801
|
+
const existing = await addressRepo().findOne({
|
|
3802
|
+
where: { id: aid, contactId }
|
|
3803
|
+
});
|
|
3804
|
+
if (!existing) return { id: null, error: "Address not found" };
|
|
3805
|
+
return { id: aid };
|
|
3806
|
+
}
|
|
3807
|
+
const addr = parseInlineAddress(inlineVal);
|
|
3808
|
+
if (addr) {
|
|
3809
|
+
const saved = await addressRepo().save(
|
|
3810
|
+
addressRepo().create({
|
|
3811
|
+
contactId,
|
|
3812
|
+
line1: addr.line1,
|
|
3813
|
+
line2: addr.line2?.trim() ? addr.line2 : null,
|
|
3814
|
+
city: addr.city?.trim() ? addr.city : null,
|
|
3815
|
+
state: addr.state?.trim() ? addr.state : null,
|
|
3816
|
+
postalCode: addr.postalCode?.trim() ? addr.postalCode : null,
|
|
3817
|
+
country: addr.country?.trim() ? addr.country : null
|
|
3818
|
+
})
|
|
3819
|
+
);
|
|
3820
|
+
return { id: saved.id };
|
|
3821
|
+
}
|
|
3822
|
+
return { id: null };
|
|
3823
|
+
}
|
|
3824
|
+
async function prepareCheckoutFromCart(b, cart, contactId) {
|
|
3825
|
+
const defaultRate = await getStoreDefaultTaxRate();
|
|
3826
|
+
const lines = [];
|
|
3827
|
+
let subtotal = 0;
|
|
3828
|
+
let orderTax = 0;
|
|
3829
|
+
let needsShipping = false;
|
|
3830
|
+
for (const it of cart.items || []) {
|
|
3831
|
+
const p = it.product;
|
|
3832
|
+
if (!p || p.deleted || p.status !== "available") continue;
|
|
3833
|
+
const unit = Number(p.price);
|
|
3834
|
+
const qty = it.quantity || 1;
|
|
3835
|
+
const lineSubtotal = unit * qty;
|
|
3836
|
+
const pType = p.type === "service" ? "service" : "product";
|
|
3837
|
+
if (pType === "product") needsShipping = true;
|
|
3838
|
+
const { tax, taxRate, taxCode } = computeTaxForProductLine(p, lineSubtotal, defaultRate);
|
|
3839
|
+
const lineTotal = roundMoney2(lineSubtotal + tax);
|
|
3840
|
+
subtotal = roundMoney2(subtotal + lineSubtotal);
|
|
3841
|
+
orderTax = roundMoney2(orderTax + tax);
|
|
3842
|
+
lines.push({
|
|
3843
|
+
productId: p.id,
|
|
3844
|
+
quantity: qty,
|
|
3845
|
+
unitPrice: unit,
|
|
3846
|
+
tax,
|
|
3847
|
+
total: lineTotal,
|
|
3848
|
+
hsn: p.hsn ?? null,
|
|
3849
|
+
uom: p.uom ?? null,
|
|
3850
|
+
productType: pType,
|
|
3851
|
+
taxRate,
|
|
3852
|
+
taxCode
|
|
3853
|
+
});
|
|
3854
|
+
}
|
|
3855
|
+
if (!lines.length) return { ok: false, status: 400, message: "No available items in cart" };
|
|
3856
|
+
const bill = await resolveCheckoutAddress(contactId, b.billingAddressId, b.billingAddress);
|
|
3857
|
+
if (bill.error) return { ok: false, status: 400, message: bill.error };
|
|
3858
|
+
if (bill.id == null) return { ok: false, status: 400, message: "Billing address required" };
|
|
3859
|
+
const ship = await resolveCheckoutAddress(contactId, b.shippingAddressId, b.shippingAddress);
|
|
3860
|
+
if (ship.error) return { ok: false, status: 400, message: ship.error };
|
|
3861
|
+
let shippingAddressId = ship.id;
|
|
3862
|
+
if (needsShipping && shippingAddressId == null) shippingAddressId = bill.id;
|
|
3863
|
+
if (needsShipping && shippingAddressId == null) {
|
|
3864
|
+
return { ok: false, status: 400, message: "Shipping address required" };
|
|
3865
|
+
}
|
|
3866
|
+
const orderTotal = roundMoney2(subtotal + orderTax);
|
|
3867
|
+
return {
|
|
3868
|
+
ok: true,
|
|
3869
|
+
lines,
|
|
3870
|
+
subtotal,
|
|
3871
|
+
orderTax,
|
|
3872
|
+
orderTotal,
|
|
3873
|
+
billingAddressId: bill.id,
|
|
3874
|
+
shippingAddressId
|
|
3875
|
+
};
|
|
3876
|
+
}
|
|
2994
3877
|
async function syncContactToErp(contact) {
|
|
2995
3878
|
if (!getCms) return;
|
|
2996
3879
|
try {
|
|
@@ -3012,7 +3895,7 @@ function createStorefrontApiHandler(config) {
|
|
|
3012
3895
|
const u = await userRepo().findOne({ where: { id: userId } });
|
|
3013
3896
|
if (!u) return null;
|
|
3014
3897
|
const unclaimed = await contactRepo().findOne({
|
|
3015
|
-
where: { email: u.email, userId: (0,
|
|
3898
|
+
where: { email: u.email, userId: (0, import_typeorm6.IsNull)(), deleted: false }
|
|
3016
3899
|
});
|
|
3017
3900
|
if (unclaimed) {
|
|
3018
3901
|
await contactRepo().update(unclaimed.id, { userId });
|
|
@@ -3116,6 +3999,7 @@ function createStorefrontApiHandler(config) {
|
|
|
3116
3999
|
slug: p.slug,
|
|
3117
4000
|
price: p.price,
|
|
3118
4001
|
sku: p.sku,
|
|
4002
|
+
type: p.type === "service" ? "service" : "product",
|
|
3119
4003
|
image: primaryProductImageUrl(p.metadata)
|
|
3120
4004
|
} : null
|
|
3121
4005
|
};
|
|
@@ -3143,6 +4027,8 @@ function createStorefrontApiHandler(config) {
|
|
|
3143
4027
|
slug: p.slug,
|
|
3144
4028
|
sku: p.sku,
|
|
3145
4029
|
hsn: p.hsn,
|
|
4030
|
+
uom: p.uom ?? null,
|
|
4031
|
+
type: p.type === "service" ? "service" : "product",
|
|
3146
4032
|
price: p.price,
|
|
3147
4033
|
compareAtPrice: p.compareAtPrice,
|
|
3148
4034
|
status: p.status,
|
|
@@ -3844,7 +4730,7 @@ function createStorefrontApiHandler(config) {
|
|
|
3844
4730
|
contactId = contact.id;
|
|
3845
4731
|
cart = await cartRepo().findOne({
|
|
3846
4732
|
where: { contactId },
|
|
3847
|
-
relations: [
|
|
4733
|
+
relations: [...CART_CHECKOUT_RELATIONS]
|
|
3848
4734
|
});
|
|
3849
4735
|
} else {
|
|
3850
4736
|
const email = String(b.email ?? "").trim();
|
|
@@ -3877,25 +4763,14 @@ function createStorefrontApiHandler(config) {
|
|
|
3877
4763
|
if (!guestToken) return json({ error: "Cart not found" }, { status: 400 });
|
|
3878
4764
|
cart = await cartRepo().findOne({
|
|
3879
4765
|
where: { guestToken },
|
|
3880
|
-
relations: [
|
|
4766
|
+
relations: [...CART_CHECKOUT_RELATIONS]
|
|
3881
4767
|
});
|
|
3882
4768
|
}
|
|
3883
4769
|
if (!cart || !(cart.items || []).length) {
|
|
3884
4770
|
return json({ error: "Cart is empty" }, { status: 400 });
|
|
3885
4771
|
}
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
for (const it of cart.items || []) {
|
|
3889
|
-
const p = it.product;
|
|
3890
|
-
if (!p || p.deleted || p.status !== "available") continue;
|
|
3891
|
-
const unit = Number(p.price);
|
|
3892
|
-
const qty = it.quantity || 1;
|
|
3893
|
-
const lineTotal = unit * qty;
|
|
3894
|
-
subtotal += lineTotal;
|
|
3895
|
-
lines.push({ productId: p.id, quantity: qty, unitPrice: unit, tax: 0, total: lineTotal });
|
|
3896
|
-
}
|
|
3897
|
-
if (!lines.length) return json({ error: "No available items in cart" }, { status: 400 });
|
|
3898
|
-
const total = subtotal;
|
|
4772
|
+
const prepOrd = await prepareCheckoutFromCart(b, cart, contactId);
|
|
4773
|
+
if (!prepOrd.ok) return json({ error: prepOrd.message }, { status: prepOrd.status });
|
|
3899
4774
|
const cartId = cart.id;
|
|
3900
4775
|
const ord = await orderRepo().save(
|
|
3901
4776
|
orderRepo().create({
|
|
@@ -3903,13 +4778,13 @@ function createStorefrontApiHandler(config) {
|
|
|
3903
4778
|
orderKind: "sale",
|
|
3904
4779
|
parentOrderId: null,
|
|
3905
4780
|
contactId,
|
|
3906
|
-
billingAddressId:
|
|
3907
|
-
shippingAddressId:
|
|
4781
|
+
billingAddressId: prepOrd.billingAddressId,
|
|
4782
|
+
shippingAddressId: prepOrd.shippingAddressId,
|
|
3908
4783
|
status: "pending",
|
|
3909
|
-
subtotal,
|
|
3910
|
-
tax:
|
|
4784
|
+
subtotal: prepOrd.subtotal,
|
|
4785
|
+
tax: prepOrd.orderTax,
|
|
3911
4786
|
discount: 0,
|
|
3912
|
-
total,
|
|
4787
|
+
total: prepOrd.orderTotal,
|
|
3913
4788
|
currency: cart.currency || "INR",
|
|
3914
4789
|
metadata: { cartId }
|
|
3915
4790
|
})
|
|
@@ -3918,7 +4793,7 @@ function createStorefrontApiHandler(config) {
|
|
|
3918
4793
|
await orderRepo().update(oid, {
|
|
3919
4794
|
orderNumber: buildCanonicalOrderNumber("sale", oid, ord.createdAt ?? /* @__PURE__ */ new Date())
|
|
3920
4795
|
});
|
|
3921
|
-
for (const line of lines) {
|
|
4796
|
+
for (const line of prepOrd.lines) {
|
|
3922
4797
|
await orderItemRepo().save(
|
|
3923
4798
|
orderItemRepo().create({
|
|
3924
4799
|
orderId: oid,
|
|
@@ -3926,14 +4801,21 @@ function createStorefrontApiHandler(config) {
|
|
|
3926
4801
|
quantity: line.quantity,
|
|
3927
4802
|
unitPrice: line.unitPrice,
|
|
3928
4803
|
tax: line.tax,
|
|
3929
|
-
total: line.total
|
|
4804
|
+
total: line.total,
|
|
4805
|
+
hsn: line.hsn,
|
|
4806
|
+
uom: line.uom,
|
|
4807
|
+
productType: line.productType,
|
|
4808
|
+
taxRate: line.taxRate,
|
|
4809
|
+
taxCode: line.taxCode
|
|
3930
4810
|
})
|
|
3931
4811
|
);
|
|
3932
4812
|
}
|
|
3933
4813
|
return json({
|
|
3934
4814
|
orderId: oid,
|
|
3935
4815
|
orderNumber: ord.orderNumber,
|
|
3936
|
-
|
|
4816
|
+
subtotal: prepOrd.subtotal,
|
|
4817
|
+
tax: prepOrd.orderTax,
|
|
4818
|
+
total: prepOrd.orderTotal,
|
|
3937
4819
|
currency: cart.currency || "INR"
|
|
3938
4820
|
});
|
|
3939
4821
|
}
|
|
@@ -3951,7 +4833,7 @@ function createStorefrontApiHandler(config) {
|
|
|
3951
4833
|
contactId = contact.id;
|
|
3952
4834
|
cart = await cartRepo().findOne({
|
|
3953
4835
|
where: { contactId },
|
|
3954
|
-
relations: [
|
|
4836
|
+
relations: [...CART_CHECKOUT_RELATIONS]
|
|
3955
4837
|
});
|
|
3956
4838
|
} else {
|
|
3957
4839
|
const email = String(b.email ?? "").trim();
|
|
@@ -3984,38 +4866,27 @@ function createStorefrontApiHandler(config) {
|
|
|
3984
4866
|
if (!guestToken) return json({ error: "Cart not found" }, { status: 400 });
|
|
3985
4867
|
cart = await cartRepo().findOne({
|
|
3986
4868
|
where: { guestToken },
|
|
3987
|
-
relations: [
|
|
4869
|
+
relations: [...CART_CHECKOUT_RELATIONS]
|
|
3988
4870
|
});
|
|
3989
4871
|
}
|
|
3990
4872
|
if (!cart || !(cart.items || []).length) {
|
|
3991
4873
|
return json({ error: "Cart is empty" }, { status: 400 });
|
|
3992
4874
|
}
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
for (const it of cart.items || []) {
|
|
3996
|
-
const p = it.product;
|
|
3997
|
-
if (!p || p.deleted || p.status !== "available") continue;
|
|
3998
|
-
const unit = Number(p.price);
|
|
3999
|
-
const qty = it.quantity || 1;
|
|
4000
|
-
const lineTotal = unit * qty;
|
|
4001
|
-
subtotal += lineTotal;
|
|
4002
|
-
lines.push({ productId: p.id, quantity: qty, unitPrice: unit, tax: 0, total: lineTotal });
|
|
4003
|
-
}
|
|
4004
|
-
if (!lines.length) return json({ error: "No available items in cart" }, { status: 400 });
|
|
4005
|
-
const total = subtotal;
|
|
4875
|
+
const prepChk = await prepareCheckoutFromCart(b, cart, contactId);
|
|
4876
|
+
if (!prepChk.ok) return json({ error: prepChk.message }, { status: prepChk.status });
|
|
4006
4877
|
const ord = await orderRepo().save(
|
|
4007
4878
|
orderRepo().create({
|
|
4008
4879
|
orderNumber: temporaryOrderNumberPlaceholder(),
|
|
4009
4880
|
orderKind: "sale",
|
|
4010
4881
|
parentOrderId: null,
|
|
4011
4882
|
contactId,
|
|
4012
|
-
billingAddressId:
|
|
4013
|
-
shippingAddressId:
|
|
4883
|
+
billingAddressId: prepChk.billingAddressId,
|
|
4884
|
+
shippingAddressId: prepChk.shippingAddressId,
|
|
4014
4885
|
status: "pending",
|
|
4015
|
-
subtotal,
|
|
4016
|
-
tax:
|
|
4886
|
+
subtotal: prepChk.subtotal,
|
|
4887
|
+
tax: prepChk.orderTax,
|
|
4017
4888
|
discount: 0,
|
|
4018
|
-
total,
|
|
4889
|
+
total: prepChk.orderTotal,
|
|
4019
4890
|
currency: cart.currency || "INR"
|
|
4020
4891
|
})
|
|
4021
4892
|
);
|
|
@@ -4023,7 +4894,7 @@ function createStorefrontApiHandler(config) {
|
|
|
4023
4894
|
await orderRepo().update(oid, {
|
|
4024
4895
|
orderNumber: buildCanonicalOrderNumber("sale", oid, ord.createdAt ?? /* @__PURE__ */ new Date())
|
|
4025
4896
|
});
|
|
4026
|
-
for (const line of lines) {
|
|
4897
|
+
for (const line of prepChk.lines) {
|
|
4027
4898
|
await orderItemRepo().save(
|
|
4028
4899
|
orderItemRepo().create({
|
|
4029
4900
|
orderId: oid,
|
|
@@ -4031,7 +4902,12 @@ function createStorefrontApiHandler(config) {
|
|
|
4031
4902
|
quantity: line.quantity,
|
|
4032
4903
|
unitPrice: line.unitPrice,
|
|
4033
4904
|
tax: line.tax,
|
|
4034
|
-
total: line.total
|
|
4905
|
+
total: line.total,
|
|
4906
|
+
hsn: line.hsn,
|
|
4907
|
+
uom: line.uom,
|
|
4908
|
+
productType: line.productType,
|
|
4909
|
+
taxRate: line.taxRate,
|
|
4910
|
+
taxCode: line.taxCode
|
|
4035
4911
|
})
|
|
4036
4912
|
);
|
|
4037
4913
|
}
|
|
@@ -4040,7 +4916,9 @@ function createStorefrontApiHandler(config) {
|
|
|
4040
4916
|
return json({
|
|
4041
4917
|
orderId: oid,
|
|
4042
4918
|
orderNumber: ord.orderNumber,
|
|
4043
|
-
|
|
4919
|
+
subtotal: prepChk.subtotal,
|
|
4920
|
+
tax: prepChk.orderTax,
|
|
4921
|
+
total: prepChk.orderTotal
|
|
4044
4922
|
});
|
|
4045
4923
|
}
|
|
4046
4924
|
if (path[0] === "orders" && path.length === 1 && method === "GET") {
|
|
@@ -4058,7 +4936,7 @@ function createStorefrontApiHandler(config) {
|
|
|
4058
4936
|
const previewByOrder = {};
|
|
4059
4937
|
if (orderIds.length) {
|
|
4060
4938
|
const oItems = await orderItemRepo().find({
|
|
4061
|
-
where: { orderId: (0,
|
|
4939
|
+
where: { orderId: (0, import_typeorm6.In)(orderIds) },
|
|
4062
4940
|
relations: ["product"],
|
|
4063
4941
|
order: { id: "ASC" }
|
|
4064
4942
|
});
|
|
@@ -4191,9 +5069,11 @@ function createStorefrontApiHandler(config) {
|
|
|
4191
5069
|
createCrudByIdHandler,
|
|
4192
5070
|
createCrudHandler,
|
|
4193
5071
|
createDashboardStatsHandler,
|
|
5072
|
+
createEcommerceAnalyticsHandler,
|
|
4194
5073
|
createForgotPasswordHandler,
|
|
4195
5074
|
createFormBySlugHandler,
|
|
4196
5075
|
createInviteAcceptHandler,
|
|
5076
|
+
createMediaZipExtractHandler,
|
|
4197
5077
|
createSetPasswordHandler,
|
|
4198
5078
|
createSettingsApiHandlers,
|
|
4199
5079
|
createStorefrontApiHandler,
|