@infuro/cms-core 1.0.12 → 1.0.15
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 +1326 -544
- package/dist/admin.cjs.map +1 -1
- package/dist/admin.js +1170 -388
- package/dist/admin.js.map +1 -1
- package/dist/api.cjs +1486 -98
- 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 +1485 -97
- package/dist/api.js.map +1 -1
- package/dist/auth.cjs +62 -19
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +12 -1
- package/dist/auth.d.ts +12 -1
- package/dist/auth.js +62 -19
- package/dist/auth.js.map +1 -1
- package/dist/{index-JrST6EIC.d.cts → index-BQnqJ7EO.d.cts} +30 -4
- package/dist/{index-C4Yl7js9.d.ts → index-BiagwMjV.d.ts} +30 -4
- package/dist/index.cjs +3305 -936
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +359 -54
- package/dist/index.d.ts +359 -54
- package/dist/index.js +3114 -768
- package/dist/index.js.map +1 -1
- package/dist/migrations/1774600000000-OrderKindParentOrderNumber.ts +36 -0
- package/dist/migrations/1774800000000-OtpChallengesUserPhone.ts +41 -0
- package/dist/migrations/1774900000000-MessageTemplates.ts +39 -0
- package/dist/migrations/1775000000000-ProductUomTypeOrderItemSnapshots.ts +29 -0
- package/package.json +1 -1
package/dist/api.cjs
CHANGED
|
@@ -30,6 +30,41 @@ 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
|
+
|
|
47
|
+
// src/plugins/erp/erp-config-enabled.ts
|
|
48
|
+
var erp_config_enabled_exports = {};
|
|
49
|
+
__export(erp_config_enabled_exports, {
|
|
50
|
+
isErpIntegrationEnabled: () => isErpIntegrationEnabled
|
|
51
|
+
});
|
|
52
|
+
async function isErpIntegrationEnabled(cms, dataSource, entityMap) {
|
|
53
|
+
if (!cms.getPlugin("erp")) return false;
|
|
54
|
+
const configRepo = dataSource.getRepository(entityMap.configs);
|
|
55
|
+
const cfgRows = await configRepo.find({ where: { settings: "erp", deleted: false } });
|
|
56
|
+
for (const row of cfgRows) {
|
|
57
|
+
const r = row;
|
|
58
|
+
if (r.key === "enabled" && r.value === "false") return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
var init_erp_config_enabled = __esm({
|
|
63
|
+
"src/plugins/erp/erp-config-enabled.ts"() {
|
|
64
|
+
"use strict";
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
33
68
|
// src/plugins/email/email-queue.ts
|
|
34
69
|
var email_queue_exports = {};
|
|
35
70
|
__export(email_queue_exports, {
|
|
@@ -68,14 +103,31 @@ async function queueEmail(cms, payload) {
|
|
|
68
103
|
}
|
|
69
104
|
}
|
|
70
105
|
async function queueOrderPlacedEmails(cms, payload) {
|
|
71
|
-
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;
|
|
72
120
|
const base = {
|
|
73
|
-
orderNumber
|
|
121
|
+
orderNumber,
|
|
74
122
|
total: total != null ? String(total) : void 0,
|
|
123
|
+
subtotal: subtotal != null ? String(subtotal) : void 0,
|
|
124
|
+
tax: tax != null ? String(tax) : void 0,
|
|
75
125
|
currency,
|
|
76
126
|
customerName,
|
|
77
127
|
companyDetails: companyDetails ?? {},
|
|
78
|
-
lineItems: lineItems ?? []
|
|
128
|
+
lineItems: lineItems ?? [],
|
|
129
|
+
billingAddress,
|
|
130
|
+
shippingAddress
|
|
79
131
|
};
|
|
80
132
|
const customerLower = customerEmail?.trim().toLowerCase() ?? "";
|
|
81
133
|
const jobs = [];
|
|
@@ -118,6 +170,254 @@ var init_email_queue = __esm({
|
|
|
118
170
|
}
|
|
119
171
|
});
|
|
120
172
|
|
|
173
|
+
// src/plugins/erp/erp-response-map.ts
|
|
174
|
+
function pickString(o, keys) {
|
|
175
|
+
for (const k of keys) {
|
|
176
|
+
const v = o[k];
|
|
177
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
178
|
+
}
|
|
179
|
+
return void 0;
|
|
180
|
+
}
|
|
181
|
+
function unwrapErpReadData(json) {
|
|
182
|
+
if (!json || typeof json !== "object") return null;
|
|
183
|
+
const o = json;
|
|
184
|
+
const d = o.data;
|
|
185
|
+
if (d && typeof d === "object" && !Array.isArray(d)) return d;
|
|
186
|
+
return o;
|
|
187
|
+
}
|
|
188
|
+
function firstObject(data, keys) {
|
|
189
|
+
for (const k of keys) {
|
|
190
|
+
const v = data[k];
|
|
191
|
+
if (v && typeof v === "object" && !Array.isArray(v)) return v;
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
function extractEvents(src) {
|
|
196
|
+
const timeline = src.timeline ?? src.events ?? src.history ?? src.trackingEvents;
|
|
197
|
+
if (!Array.isArray(timeline) || !timeline.length) return void 0;
|
|
198
|
+
const events = [];
|
|
199
|
+
for (const row of timeline) {
|
|
200
|
+
if (!row || typeof row !== "object") continue;
|
|
201
|
+
const r = row;
|
|
202
|
+
const at = pickString(r, ["at", "timestamp", "date", "occurredAt"]) ?? (r.at instanceof Date ? r.at.toISOString() : void 0);
|
|
203
|
+
const label = pickString(r, ["label", "status", "title", "message", "description"]);
|
|
204
|
+
const detail = pickString(r, ["detail", "notes", "description"]);
|
|
205
|
+
if (at || label || detail) events.push({ at, label, detail });
|
|
206
|
+
}
|
|
207
|
+
return events.length ? events : void 0;
|
|
208
|
+
}
|
|
209
|
+
function mapErpPayloadToFulfillment(data) {
|
|
210
|
+
const nested = firstObject(data, ["fulfillment", "packaging", "shipment", "shipping", "delivery"]);
|
|
211
|
+
const src = nested || data;
|
|
212
|
+
const status = pickString(src, ["status", "fulfillmentStatus", "state", "label", "packagingStatus"]);
|
|
213
|
+
const trackingId = pickString(src, ["trackingId", "tracking_id", "trackingNumber", "awb", "trackingUrl"]);
|
|
214
|
+
const events = extractEvents(src);
|
|
215
|
+
if (!status && !trackingId && !(events && events.length)) return void 0;
|
|
216
|
+
return { status, trackingId, events };
|
|
217
|
+
}
|
|
218
|
+
function mapErpPayloadToInvoiceNumber(data) {
|
|
219
|
+
const nested = firstObject(data, ["invoice", "latestInvoice", "postedInvoice"]);
|
|
220
|
+
const src = nested || data;
|
|
221
|
+
return pickString(src, ["invoiceNumber", "invoice_number", "number", "name", "id"]);
|
|
222
|
+
}
|
|
223
|
+
function extractChildOrderRefsFromSalePayload(data) {
|
|
224
|
+
const lists = [data.returns, data.returnOrders, data.relatedReturns, data.childOrders, data.children];
|
|
225
|
+
const seen = /* @__PURE__ */ new Set();
|
|
226
|
+
const out = [];
|
|
227
|
+
for (const list of lists) {
|
|
228
|
+
if (!Array.isArray(list)) continue;
|
|
229
|
+
for (const item of list) {
|
|
230
|
+
if (!item || typeof item !== "object") continue;
|
|
231
|
+
const o = item;
|
|
232
|
+
const ref = pickString(o, ["platformReturnId", "platform_return_id", "refId", "ref_id"]) ?? (typeof o.id === "string" ? o.id : void 0);
|
|
233
|
+
if (!ref || seen.has(ref)) continue;
|
|
234
|
+
seen.add(ref);
|
|
235
|
+
const t = (pickString(o, ["kind", "type", "orderKind"]) || "").toLowerCase();
|
|
236
|
+
const orderKind = /replac/.test(t) ? "replacement" : "return";
|
|
237
|
+
out.push({ ref, orderKind });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
var init_erp_response_map = __esm({
|
|
243
|
+
"src/plugins/erp/erp-response-map.ts"() {
|
|
244
|
+
"use strict";
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// src/plugins/erp/erp-order-invoice.ts
|
|
249
|
+
var erp_order_invoice_exports = {};
|
|
250
|
+
__export(erp_order_invoice_exports, {
|
|
251
|
+
streamOrderInvoicePdf: () => streamOrderInvoicePdf
|
|
252
|
+
});
|
|
253
|
+
function pickInvoiceId(data) {
|
|
254
|
+
const nested = data.invoice && typeof data.invoice === "object" && !Array.isArray(data.invoice) ? data.invoice : null;
|
|
255
|
+
const src = nested || data;
|
|
256
|
+
for (const k of ["invoiceId", "invoice_id", "id"]) {
|
|
257
|
+
const v = src[k];
|
|
258
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
259
|
+
}
|
|
260
|
+
return void 0;
|
|
261
|
+
}
|
|
262
|
+
async function streamOrderInvoicePdf(cms, dataSource, entityMap, orderId, options) {
|
|
263
|
+
const jsonErr = (msg, status) => new Response(JSON.stringify({ error: msg }), {
|
|
264
|
+
status,
|
|
265
|
+
headers: { "Content-Type": "application/json" }
|
|
266
|
+
});
|
|
267
|
+
const on = await isErpIntegrationEnabled(cms, dataSource, entityMap);
|
|
268
|
+
if (!on) return jsonErr("Invoice not available", 503);
|
|
269
|
+
const erp = cms.getPlugin("erp");
|
|
270
|
+
if (!erp?.submission) return jsonErr("Invoice not available", 503);
|
|
271
|
+
const orderRepo = dataSource.getRepository(entityMap.orders);
|
|
272
|
+
const order = await orderRepo.findOne({ where: { id: orderId, deleted: false } });
|
|
273
|
+
if (!order) return jsonErr("Not found", 404);
|
|
274
|
+
const kind = order.orderKind || "sale";
|
|
275
|
+
if (kind !== "sale") return jsonErr("Invoice only for sale orders", 400);
|
|
276
|
+
if (options.ownerContactId != null && order.contactId !== options.ownerContactId) {
|
|
277
|
+
return jsonErr("Not found", 404);
|
|
278
|
+
}
|
|
279
|
+
const meta = order.metadata && typeof order.metadata === "object" && !Array.isArray(order.metadata) ? order.metadata : {};
|
|
280
|
+
const inv = meta.invoice && typeof meta.invoice === "object" && !Array.isArray(meta.invoice) ? meta.invoice : {};
|
|
281
|
+
let invoiceId = typeof inv.invoiceId === "string" ? inv.invoiceId.trim() : "";
|
|
282
|
+
if (!invoiceId) {
|
|
283
|
+
const refId = String(order.orderNumber || "");
|
|
284
|
+
const r = await erp.submission.postErpReadAction("get-invoice", { platformOrderId: refId });
|
|
285
|
+
const d = r.ok ? unwrapErpReadData(r.json) : null;
|
|
286
|
+
invoiceId = d ? pickInvoiceId(d) || "" : "";
|
|
287
|
+
}
|
|
288
|
+
if (!invoiceId) return jsonErr("Invoice not ready", 404);
|
|
289
|
+
const pdf = await erp.submission.fetchInvoicePdf(invoiceId);
|
|
290
|
+
if (!pdf.ok || !pdf.buffer) return jsonErr(pdf.error || "PDF fetch failed", 502);
|
|
291
|
+
const filename = `invoice-${orderId}.pdf`;
|
|
292
|
+
return new Response(pdf.buffer, {
|
|
293
|
+
status: 200,
|
|
294
|
+
headers: {
|
|
295
|
+
"Content-Type": pdf.contentType || "application/pdf",
|
|
296
|
+
"Content-Disposition": `attachment; filename="${filename}"`
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
var init_erp_order_invoice = __esm({
|
|
301
|
+
"src/plugins/erp/erp-order-invoice.ts"() {
|
|
302
|
+
"use strict";
|
|
303
|
+
init_erp_response_map();
|
|
304
|
+
init_erp_config_enabled();
|
|
305
|
+
}
|
|
306
|
+
});
|
|
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
|
+
|
|
121
421
|
// src/api/index.ts
|
|
122
422
|
var api_exports = {};
|
|
123
423
|
__export(api_exports, {
|
|
@@ -145,6 +445,76 @@ module.exports = __toCommonJS(api_exports);
|
|
|
145
445
|
|
|
146
446
|
// src/api/crud.ts
|
|
147
447
|
var import_typeorm = require("typeorm");
|
|
448
|
+
|
|
449
|
+
// src/plugins/erp/erp-contact-sync.ts
|
|
450
|
+
init_erp_queue();
|
|
451
|
+
function splitName(full) {
|
|
452
|
+
const t = (full || "").trim();
|
|
453
|
+
if (!t) return { firstName: "Contact", lastName: "" };
|
|
454
|
+
const parts = t.split(/\s+/);
|
|
455
|
+
if (parts.length === 1) return { firstName: parts[0], lastName: "" };
|
|
456
|
+
return { firstName: parts[0], lastName: parts.slice(1).join(" ") };
|
|
457
|
+
}
|
|
458
|
+
async function queueErpCreateContactIfEnabled(cms, dataSource, entityMap, input) {
|
|
459
|
+
try {
|
|
460
|
+
const configRepo = dataSource.getRepository(entityMap.configs);
|
|
461
|
+
const cfgRows = await configRepo.find({ where: { settings: "erp", deleted: false } });
|
|
462
|
+
for (const row of cfgRows) {
|
|
463
|
+
const r = row;
|
|
464
|
+
if (r.key === "enabled" && r.value === "false") return;
|
|
465
|
+
}
|
|
466
|
+
if (!cms.getPlugin("erp")) return;
|
|
467
|
+
const email = (input.email ?? "").trim();
|
|
468
|
+
if (!email) return;
|
|
469
|
+
const { firstName, lastName } = splitName(input.name);
|
|
470
|
+
await queueErp(cms, {
|
|
471
|
+
kind: "createContact",
|
|
472
|
+
contact: {
|
|
473
|
+
email,
|
|
474
|
+
firstName,
|
|
475
|
+
lastName,
|
|
476
|
+
phone: input.phone?.trim() || void 0,
|
|
477
|
+
companyName: input.company?.trim() || void 0,
|
|
478
|
+
type: input.type?.trim() || void 0,
|
|
479
|
+
notes: input.notes?.trim() || void 0,
|
|
480
|
+
tags: input.tags?.length ? [...input.tags] : void 0
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
} catch {
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/plugins/erp/erp-product-sync.ts
|
|
488
|
+
init_erp_queue();
|
|
489
|
+
init_erp_config_enabled();
|
|
490
|
+
async function queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, product) {
|
|
491
|
+
try {
|
|
492
|
+
const sku = typeof product.sku === "string" ? product.sku.trim() : "";
|
|
493
|
+
if (!sku) return;
|
|
494
|
+
const on = await isErpIntegrationEnabled(cms, dataSource, entityMap);
|
|
495
|
+
if (!on) return;
|
|
496
|
+
const rawMeta = product.metadata;
|
|
497
|
+
let metadata;
|
|
498
|
+
if (rawMeta && typeof rawMeta === "object" && !Array.isArray(rawMeta)) {
|
|
499
|
+
const { description: _d, ...rest } = rawMeta;
|
|
500
|
+
metadata = Object.keys(rest).length ? rest : void 0;
|
|
501
|
+
}
|
|
502
|
+
const payload = {
|
|
503
|
+
sku,
|
|
504
|
+
title: product.name || sku,
|
|
505
|
+
name: product.name,
|
|
506
|
+
hsn_number: product.hsn,
|
|
507
|
+
uom: product.uom != null && String(product.uom).trim() ? String(product.uom).trim() : void 0,
|
|
508
|
+
type: product.type === "service" ? "service" : "product",
|
|
509
|
+
is_active: product.status === "available",
|
|
510
|
+
metadata
|
|
511
|
+
};
|
|
512
|
+
await queueErp(cms, { kind: "productUpsert", product: payload });
|
|
513
|
+
} catch {
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/api/crud.ts
|
|
148
518
|
var DATE_COLUMN_TYPES = /* @__PURE__ */ new Set([
|
|
149
519
|
"date",
|
|
150
520
|
"datetime",
|
|
@@ -197,8 +567,27 @@ function buildSearchWhereClause(repo, search) {
|
|
|
197
567
|
if (ors.length === 0) return {};
|
|
198
568
|
return ors.length === 1 ? ors[0] : ors;
|
|
199
569
|
}
|
|
570
|
+
function makeContactErpSync(dataSource, entityMap, getCms) {
|
|
571
|
+
return async function syncContactRowToErp(row) {
|
|
572
|
+
if (!getCms) return;
|
|
573
|
+
try {
|
|
574
|
+
const cms = await getCms();
|
|
575
|
+
const c = row;
|
|
576
|
+
await queueErpCreateContactIfEnabled(cms, dataSource, entityMap, {
|
|
577
|
+
name: c.name,
|
|
578
|
+
email: c.email,
|
|
579
|
+
phone: c.phone,
|
|
580
|
+
type: c.type,
|
|
581
|
+
company: c.company,
|
|
582
|
+
notes: c.notes
|
|
583
|
+
});
|
|
584
|
+
} catch {
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
}
|
|
200
588
|
function createCrudHandler(dataSource, entityMap, options) {
|
|
201
|
-
const { requireAuth, json, requireEntityPermission: reqPerm } = options;
|
|
589
|
+
const { requireAuth, json, requireEntityPermission: reqPerm, getCms } = options;
|
|
590
|
+
const syncContactRowToErp = makeContactErpSync(dataSource, entityMap, getCms);
|
|
202
591
|
async function authz(req, resource, action) {
|
|
203
592
|
const authError = await requireAuth(req);
|
|
204
593
|
if (authError) return authError;
|
|
@@ -378,6 +767,24 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
378
767
|
} else if (search) {
|
|
379
768
|
where = buildSearchWhereClause(repo, search);
|
|
380
769
|
}
|
|
770
|
+
const intFilterKeys = ["productId", "attributeId", "taxId"];
|
|
771
|
+
const extraWhere = {};
|
|
772
|
+
for (const key of intFilterKeys) {
|
|
773
|
+
const v = searchParams.get(key);
|
|
774
|
+
if (v != null && v !== "" && columnNames.has(key)) {
|
|
775
|
+
const n = Number(v);
|
|
776
|
+
if (Number.isFinite(n)) extraWhere[key] = n;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (Object.keys(extraWhere).length > 0) {
|
|
780
|
+
if (Array.isArray(where)) {
|
|
781
|
+
where = where.map((w) => ({ ...w, ...extraWhere }));
|
|
782
|
+
} else if (where && typeof where === "object" && Object.keys(where).length > 0) {
|
|
783
|
+
where = { ...where, ...extraWhere };
|
|
784
|
+
} else {
|
|
785
|
+
where = extraWhere;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
381
788
|
const [data, total] = await repo.findAndCount({
|
|
382
789
|
skip,
|
|
383
790
|
take: limit,
|
|
@@ -400,6 +807,13 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
400
807
|
const repo = dataSource.getRepository(entity);
|
|
401
808
|
sanitizeBodyForEntity(repo, body);
|
|
402
809
|
const created = await repo.save(repo.create(body));
|
|
810
|
+
if (resource === "contacts") {
|
|
811
|
+
await syncContactRowToErp(created);
|
|
812
|
+
}
|
|
813
|
+
if (resource === "products" && getCms) {
|
|
814
|
+
const cms = await getCms();
|
|
815
|
+
await queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, created);
|
|
816
|
+
}
|
|
403
817
|
return json(created, { status: 201 });
|
|
404
818
|
},
|
|
405
819
|
async GET_METADATA(req, resource) {
|
|
@@ -506,7 +920,8 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
506
920
|
};
|
|
507
921
|
}
|
|
508
922
|
function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
509
|
-
const { requireAuth, json, requireEntityPermission: reqPerm } = options;
|
|
923
|
+
const { requireAuth, json, requireEntityPermission: reqPerm, getCms } = options;
|
|
924
|
+
const syncContactRowToErp = makeContactErpSync(dataSource, entityMap, getCms);
|
|
510
925
|
async function authz(req, resource, action) {
|
|
511
926
|
const authError = await requireAuth(req);
|
|
512
927
|
if (authError) return authError;
|
|
@@ -529,7 +944,11 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
529
944
|
relations: ["contact", "billingAddress", "shippingAddress", "items", "items.product", "items.product.collection", "payments"]
|
|
530
945
|
});
|
|
531
946
|
if (!order) return json({ message: "Not found" }, { status: 404 });
|
|
532
|
-
|
|
947
|
+
const relatedOrders = await repo.find({
|
|
948
|
+
where: { parentOrderId: Number(id), deleted: false },
|
|
949
|
+
order: { id: "ASC" }
|
|
950
|
+
});
|
|
951
|
+
return json({ ...order, relatedOrders });
|
|
533
952
|
}
|
|
534
953
|
if (resource === "contacts") {
|
|
535
954
|
const contact = await repo.findOne({
|
|
@@ -662,6 +1081,13 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
662
1081
|
await repo.update(numericId, updatePayload);
|
|
663
1082
|
}
|
|
664
1083
|
const updated = await repo.findOne({ where: { id: numericId } });
|
|
1084
|
+
if (resource === "contacts" && updated) {
|
|
1085
|
+
await syncContactRowToErp(updated);
|
|
1086
|
+
}
|
|
1087
|
+
if (resource === "products" && updated && getCms) {
|
|
1088
|
+
const cms = await getCms();
|
|
1089
|
+
await queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, updated);
|
|
1090
|
+
}
|
|
665
1091
|
return updated ? json(updated) : json({ message: "Not found" }, { status: 404 });
|
|
666
1092
|
},
|
|
667
1093
|
async DELETE(req, resource, id) {
|
|
@@ -826,6 +1252,25 @@ function createUserAuthApiRouter(config) {
|
|
|
826
1252
|
// src/api/cms-handlers.ts
|
|
827
1253
|
var import_typeorm3 = require("typeorm");
|
|
828
1254
|
init_email_queue();
|
|
1255
|
+
init_erp_queue();
|
|
1256
|
+
|
|
1257
|
+
// src/plugins/captcha/assert.ts
|
|
1258
|
+
async function assertCaptchaOk(getCms, body, req, json) {
|
|
1259
|
+
if (!getCms) return null;
|
|
1260
|
+
let cms;
|
|
1261
|
+
try {
|
|
1262
|
+
cms = await getCms();
|
|
1263
|
+
} catch {
|
|
1264
|
+
return null;
|
|
1265
|
+
}
|
|
1266
|
+
const svc = cms.getPlugin("captcha");
|
|
1267
|
+
if (!svc || typeof svc.verify !== "function") return null;
|
|
1268
|
+
const result = await svc.verify(body, req);
|
|
1269
|
+
if (result.ok) return null;
|
|
1270
|
+
return json({ error: result.message }, { status: result.status });
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// src/api/cms-handlers.ts
|
|
829
1274
|
function createDashboardStatsHandler(config) {
|
|
830
1275
|
const { dataSource, entityMap, json, requireAuth, requirePermission, requireEntityPermission } = config;
|
|
831
1276
|
return async function GET(req) {
|
|
@@ -1067,6 +1512,32 @@ function createFormSaveHandlers(config) {
|
|
|
1067
1512
|
}
|
|
1068
1513
|
};
|
|
1069
1514
|
}
|
|
1515
|
+
async function isErpIntegrationEnabled2(dataSource, entityMap) {
|
|
1516
|
+
const repo = dataSource.getRepository(entityMap.configs);
|
|
1517
|
+
const rows = await repo.find({ where: { settings: "erp", deleted: false } });
|
|
1518
|
+
for (const row of rows) {
|
|
1519
|
+
const r = row;
|
|
1520
|
+
if (r.key === "enabled") return r.value !== "false";
|
|
1521
|
+
}
|
|
1522
|
+
return true;
|
|
1523
|
+
}
|
|
1524
|
+
async function getErpOpportunityFormIds(dataSource, entityMap) {
|
|
1525
|
+
const repo = dataSource.getRepository(entityMap.configs);
|
|
1526
|
+
const row = await repo.findOne({
|
|
1527
|
+
where: { settings: "erp", key: "opportunityFormIds", deleted: false }
|
|
1528
|
+
});
|
|
1529
|
+
if (!row) return null;
|
|
1530
|
+
const raw = (row.value ?? "").trim();
|
|
1531
|
+
if (!raw) return [];
|
|
1532
|
+
try {
|
|
1533
|
+
const parsed = JSON.parse(raw);
|
|
1534
|
+
if (!Array.isArray(parsed)) return [];
|
|
1535
|
+
const ids = parsed.map((x) => typeof x === "number" ? x : Number(x)).filter((n) => Number.isInteger(n) && n > 0);
|
|
1536
|
+
return [...new Set(ids)];
|
|
1537
|
+
} catch {
|
|
1538
|
+
return [];
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1070
1541
|
function createFormSubmissionGetByIdHandler(config) {
|
|
1071
1542
|
const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
|
|
1072
1543
|
return async function GET(req, id) {
|
|
@@ -1155,13 +1626,15 @@ function pickContactFromSubmission(fields, data) {
|
|
|
1155
1626
|
return { name: name || email, email, phone: phone || null };
|
|
1156
1627
|
}
|
|
1157
1628
|
function createFormSubmissionHandler(config) {
|
|
1158
|
-
const { dataSource, entityMap, json } = config;
|
|
1629
|
+
const { dataSource, entityMap, json, getCms } = config;
|
|
1159
1630
|
return async function POST(req) {
|
|
1160
1631
|
try {
|
|
1161
1632
|
const body = await req.json();
|
|
1162
1633
|
if (!body || typeof body !== "object") {
|
|
1163
1634
|
return json({ error: "Invalid request payload" }, { status: 400 });
|
|
1164
1635
|
}
|
|
1636
|
+
const captchaErr = await assertCaptchaOk(getCms, body, req, json);
|
|
1637
|
+
if (captchaErr) return captchaErr;
|
|
1165
1638
|
const formId = typeof body.formId === "number" ? body.formId : Number(body.formId);
|
|
1166
1639
|
if (!Number.isInteger(formId) || formId <= 0) {
|
|
1167
1640
|
return json({ error: "formId is required and must be a positive integer" }, { status: 400 });
|
|
@@ -1228,28 +1701,44 @@ function createFormSubmissionHandler(config) {
|
|
|
1228
1701
|
contactEmail = contactData.email;
|
|
1229
1702
|
}
|
|
1230
1703
|
}
|
|
1231
|
-
if (config.getCms
|
|
1704
|
+
if (config.getCms) {
|
|
1232
1705
|
try {
|
|
1233
1706
|
const cms = await config.getCms();
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1707
|
+
if (config.getCompanyDetails && config.getRecipientForChannel) {
|
|
1708
|
+
const to = await config.getRecipientForChannel("crm");
|
|
1709
|
+
if (to) {
|
|
1710
|
+
const companyDetails = await config.getCompanyDetails();
|
|
1711
|
+
const formFieldRows = activeFields.map((f) => ({
|
|
1712
|
+
label: f.label && String(f.label).trim() || `Field ${f.id}`,
|
|
1713
|
+
value: formatSubmissionFieldValue(data[String(f.id)])
|
|
1714
|
+
}));
|
|
1715
|
+
await queueEmail(cms, {
|
|
1716
|
+
to,
|
|
1717
|
+
templateName: "formSubmission",
|
|
1718
|
+
ctx: {
|
|
1719
|
+
formName,
|
|
1720
|
+
contactName,
|
|
1721
|
+
contactEmail,
|
|
1722
|
+
formData: data,
|
|
1723
|
+
formFieldRows,
|
|
1724
|
+
companyDetails: companyDetails ?? {}
|
|
1725
|
+
}
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
if (await isErpIntegrationEnabled2(dataSource, entityMap)) {
|
|
1730
|
+
const erp = cms.getPlugin("erp");
|
|
1731
|
+
if (erp) {
|
|
1732
|
+
const contact = erp.submission.extractContactData(data, activeFields);
|
|
1733
|
+
if (contact?.email?.trim()) {
|
|
1734
|
+
const opportunityFormIds = await getErpOpportunityFormIds(dataSource, entityMap);
|
|
1735
|
+
const asOpportunity = opportunityFormIds != null && opportunityFormIds.length > 0 && opportunityFormIds.includes(formId);
|
|
1736
|
+
await queueErp(
|
|
1737
|
+
cms,
|
|
1738
|
+
asOpportunity ? { kind: "formOpportunity", contact } : { kind: "lead", contact }
|
|
1739
|
+
);
|
|
1251
1740
|
}
|
|
1252
|
-
}
|
|
1741
|
+
}
|
|
1253
1742
|
}
|
|
1254
1743
|
} catch {
|
|
1255
1744
|
}
|
|
@@ -1695,6 +2184,125 @@ ${contextParts.join("\n\n")}` : "You are a helpful assistant for the company. If
|
|
|
1695
2184
|
};
|
|
1696
2185
|
}
|
|
1697
2186
|
|
|
2187
|
+
// src/message-templates/sms-defaults.ts
|
|
2188
|
+
var SMS_MESSAGE_TEMPLATE_DEFAULTS = [
|
|
2189
|
+
{
|
|
2190
|
+
templateKey: "auth.otp_login",
|
|
2191
|
+
name: "Sign-in OTP (SMS)",
|
|
2192
|
+
body: "Your sign-in code is {{code}}. Valid 10 minutes.",
|
|
2193
|
+
providerMeta: { otpVarKey: "var1" }
|
|
2194
|
+
},
|
|
2195
|
+
{
|
|
2196
|
+
templateKey: "auth.otp_verify_phone",
|
|
2197
|
+
name: "Verify phone OTP (SMS)",
|
|
2198
|
+
body: "Your verification code is {{code}}. Valid 10 minutes.",
|
|
2199
|
+
providerMeta: { otpVarKey: "var1" }
|
|
2200
|
+
}
|
|
2201
|
+
];
|
|
2202
|
+
function getSmsTemplateDefault(templateKey) {
|
|
2203
|
+
return SMS_MESSAGE_TEMPLATE_DEFAULTS.find((d) => d.templateKey === templateKey);
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
// src/api/message-template-admin-handlers.ts
|
|
2207
|
+
function createSmsMessageTemplateHandlers(config) {
|
|
2208
|
+
const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
|
|
2209
|
+
const repo = () => dataSource.getRepository(entityMap.message_templates);
|
|
2210
|
+
async function requireSettingsRead(req) {
|
|
2211
|
+
const a = await requireAuth(req);
|
|
2212
|
+
if (a) return a;
|
|
2213
|
+
if (requireEntityPermission) {
|
|
2214
|
+
const pe = await requireEntityPermission(req, "settings", "read");
|
|
2215
|
+
if (pe) return pe;
|
|
2216
|
+
}
|
|
2217
|
+
return null;
|
|
2218
|
+
}
|
|
2219
|
+
async function requireSettingsUpdate(req) {
|
|
2220
|
+
const a = await requireAuth(req);
|
|
2221
|
+
if (a) return a;
|
|
2222
|
+
if (requireEntityPermission) {
|
|
2223
|
+
const pe = await requireEntityPermission(req, "settings", "update");
|
|
2224
|
+
if (pe) return pe;
|
|
2225
|
+
}
|
|
2226
|
+
return null;
|
|
2227
|
+
}
|
|
2228
|
+
return {
|
|
2229
|
+
async GET(req) {
|
|
2230
|
+
const err = await requireSettingsRead(req);
|
|
2231
|
+
if (err) return err;
|
|
2232
|
+
try {
|
|
2233
|
+
const rows = await repo().find({ where: { channel: "sms", deleted: false } });
|
|
2234
|
+
const byKey = new Map(rows.map((r) => [r.templateKey, r]));
|
|
2235
|
+
const items = SMS_MESSAGE_TEMPLATE_DEFAULTS.map((def) => {
|
|
2236
|
+
const row = byKey.get(def.templateKey);
|
|
2237
|
+
return {
|
|
2238
|
+
templateKey: def.templateKey,
|
|
2239
|
+
name: def.name,
|
|
2240
|
+
defaultBody: def.body,
|
|
2241
|
+
body: row?.body?.trim() ? row.body : def.body,
|
|
2242
|
+
externalTemplateRef: row?.externalTemplateRef?.trim() ?? "",
|
|
2243
|
+
otpVarKey: row?.providerMeta && typeof row.providerMeta.otpVarKey === "string" ? String(row.providerMeta.otpVarKey) : def.providerMeta?.otpVarKey ?? "var1",
|
|
2244
|
+
enabled: row ? row.enabled : false,
|
|
2245
|
+
dbId: row?.id ?? null
|
|
2246
|
+
};
|
|
2247
|
+
});
|
|
2248
|
+
return json({ items });
|
|
2249
|
+
} catch {
|
|
2250
|
+
return json({ error: "Failed to load templates" }, { status: 500 });
|
|
2251
|
+
}
|
|
2252
|
+
},
|
|
2253
|
+
async PUT(req) {
|
|
2254
|
+
const err = await requireSettingsUpdate(req);
|
|
2255
|
+
if (err) return err;
|
|
2256
|
+
try {
|
|
2257
|
+
const raw = await req.json().catch(() => null);
|
|
2258
|
+
if (!raw?.items || !Array.isArray(raw.items)) {
|
|
2259
|
+
return json({ error: "Invalid payload" }, { status: 400 });
|
|
2260
|
+
}
|
|
2261
|
+
for (const item of raw.items) {
|
|
2262
|
+
const templateKey = typeof item.templateKey === "string" ? item.templateKey.trim() : "";
|
|
2263
|
+
if (!getSmsTemplateDefault(templateKey)) continue;
|
|
2264
|
+
const body = typeof item.body === "string" ? item.body : "";
|
|
2265
|
+
const externalTemplateRef = typeof item.externalTemplateRef === "string" ? item.externalTemplateRef.trim() : "";
|
|
2266
|
+
const otpVarKey = typeof item.otpVarKey === "string" && item.otpVarKey.trim() ? item.otpVarKey.trim() : "var1";
|
|
2267
|
+
const enabled = item.enabled !== false;
|
|
2268
|
+
const existing = await repo().findOne({
|
|
2269
|
+
where: { channel: "sms", templateKey, deleted: false }
|
|
2270
|
+
});
|
|
2271
|
+
const def = getSmsTemplateDefault(templateKey);
|
|
2272
|
+
const providerMeta = { otpVarKey };
|
|
2273
|
+
if (existing) {
|
|
2274
|
+
await repo().update(existing.id, {
|
|
2275
|
+
name: def.name,
|
|
2276
|
+
body,
|
|
2277
|
+
externalTemplateRef: externalTemplateRef || null,
|
|
2278
|
+
providerMeta,
|
|
2279
|
+
enabled,
|
|
2280
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2281
|
+
});
|
|
2282
|
+
} else {
|
|
2283
|
+
await repo().save(
|
|
2284
|
+
repo().create({
|
|
2285
|
+
channel: "sms",
|
|
2286
|
+
templateKey,
|
|
2287
|
+
name: def.name,
|
|
2288
|
+
subject: null,
|
|
2289
|
+
body,
|
|
2290
|
+
externalTemplateRef: externalTemplateRef || null,
|
|
2291
|
+
providerMeta,
|
|
2292
|
+
enabled,
|
|
2293
|
+
deleted: false
|
|
2294
|
+
})
|
|
2295
|
+
);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
return json({ ok: true });
|
|
2299
|
+
} catch {
|
|
2300
|
+
return json({ error: "Failed to save templates" }, { status: 500 });
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
|
|
1698
2306
|
// src/auth/permission-entities.ts
|
|
1699
2307
|
var PERMISSION_ENTITY_INTERNAL_EXCLUDE = /* @__PURE__ */ new Set([
|
|
1700
2308
|
"users",
|
|
@@ -1708,7 +2316,8 @@ var PERMISSION_ENTITY_INTERNAL_EXCLUDE = /* @__PURE__ */ new Set([
|
|
|
1708
2316
|
"carts",
|
|
1709
2317
|
"cart_items",
|
|
1710
2318
|
"wishlists",
|
|
1711
|
-
"wishlist_items"
|
|
2319
|
+
"wishlist_items",
|
|
2320
|
+
"message_templates"
|
|
1712
2321
|
]);
|
|
1713
2322
|
var PERMISSION_LOGICAL_ENTITIES = [
|
|
1714
2323
|
"users",
|
|
@@ -1894,7 +2503,8 @@ var DEFAULT_EXCLUDE = /* @__PURE__ */ new Set([
|
|
|
1894
2503
|
"carts",
|
|
1895
2504
|
"cart_items",
|
|
1896
2505
|
"wishlists",
|
|
1897
|
-
"wishlist_items"
|
|
2506
|
+
"wishlist_items",
|
|
2507
|
+
"message_templates"
|
|
1898
2508
|
]);
|
|
1899
2509
|
function createCmsApiHandler(config) {
|
|
1900
2510
|
const {
|
|
@@ -1957,7 +2567,8 @@ function createCmsApiHandler(config) {
|
|
|
1957
2567
|
const crudOpts = {
|
|
1958
2568
|
requireAuth: config.requireAuth,
|
|
1959
2569
|
json: config.json,
|
|
1960
|
-
requireEntityPermission: reqEntityPerm
|
|
2570
|
+
requireEntityPermission: reqEntityPerm,
|
|
2571
|
+
getCms
|
|
1961
2572
|
};
|
|
1962
2573
|
const crud = createCrudHandler(dataSource, entityMap, crudOpts);
|
|
1963
2574
|
const crudById = createCrudByIdHandler(dataSource, entityMap, crudOpts);
|
|
@@ -1987,6 +2598,13 @@ function createCmsApiHandler(config) {
|
|
|
1987
2598
|
const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
|
|
1988
2599
|
const profilePut = userProfile ? createUserProfileHandler(userProfile) : null;
|
|
1989
2600
|
const settingsHandlers = settingsConfig ? createSettingsApiHandlers(settingsConfig) : null;
|
|
2601
|
+
const smsMessageTemplateHandlers = createSmsMessageTemplateHandlers({
|
|
2602
|
+
dataSource,
|
|
2603
|
+
entityMap,
|
|
2604
|
+
json: config.json,
|
|
2605
|
+
requireAuth: config.requireAuth,
|
|
2606
|
+
requireEntityPermission: reqEntityPerm
|
|
2607
|
+
});
|
|
1990
2608
|
const chatHandlers = chatConfig ? createChatHandlers(chatConfig) : null;
|
|
1991
2609
|
function resolveResource(segment) {
|
|
1992
2610
|
const model = pathToModel(segment);
|
|
@@ -2091,11 +2709,51 @@ function createCmsApiHandler(config) {
|
|
|
2091
2709
|
return settingsHandlers.PUT(req, group);
|
|
2092
2710
|
}
|
|
2093
2711
|
}
|
|
2712
|
+
if (path[0] === "message-templates" && path[1] === "sms" && path.length === 2) {
|
|
2713
|
+
if (method === "GET") return smsMessageTemplateHandlers.GET(req);
|
|
2714
|
+
if (method === "PUT") return smsMessageTemplateHandlers.PUT(req);
|
|
2715
|
+
}
|
|
2094
2716
|
if (path[0] === "chat" && chatHandlers) {
|
|
2095
2717
|
if (path.length === 2 && path[1] === "identify" && method === "POST") return chatHandlers.identify(req);
|
|
2096
2718
|
if (path.length === 4 && path[1] === "conversations" && path[3] === "messages" && method === "GET") return chatHandlers.getMessages(req, path[2]);
|
|
2097
2719
|
if (path.length === 2 && path[1] === "messages" && method === "POST") return chatHandlers.postMessage(req);
|
|
2098
2720
|
}
|
|
2721
|
+
if (path[0] === "orders" && path.length === 3 && path[2] === "invoice" && method === "GET" && getCms) {
|
|
2722
|
+
const a = await config.requireAuth(req);
|
|
2723
|
+
if (a) return a;
|
|
2724
|
+
if (perm) {
|
|
2725
|
+
const pe = await perm(req, "orders", "read");
|
|
2726
|
+
if (pe) return pe;
|
|
2727
|
+
}
|
|
2728
|
+
const cms = await getCms();
|
|
2729
|
+
const { streamOrderInvoicePdf: streamOrderInvoicePdf2 } = await Promise.resolve().then(() => (init_erp_order_invoice(), erp_order_invoice_exports));
|
|
2730
|
+
const oid = Number(path[1]);
|
|
2731
|
+
if (!Number.isFinite(oid)) return config.json({ error: "Invalid id" }, { status: 400 });
|
|
2732
|
+
return streamOrderInvoicePdf2(cms, dataSource, entityMap, oid, {});
|
|
2733
|
+
}
|
|
2734
|
+
if (path[0] === "orders" && path.length === 3 && path[2] === "repost-erp" && getCms) {
|
|
2735
|
+
const a = await config.requireAuth(req);
|
|
2736
|
+
if (a) return a;
|
|
2737
|
+
if (perm) {
|
|
2738
|
+
const pe = await perm(req, "orders", method === "GET" ? "read" : "update");
|
|
2739
|
+
if (pe) return pe;
|
|
2740
|
+
}
|
|
2741
|
+
const oid = Number(path[1]);
|
|
2742
|
+
if (!Number.isFinite(oid)) return config.json({ error: "Invalid id" }, { status: 400 });
|
|
2743
|
+
const cms = await getCms();
|
|
2744
|
+
const { isErpIntegrationEnabled: isErpIntegrationEnabled3 } = await Promise.resolve().then(() => (init_erp_config_enabled(), erp_config_enabled_exports));
|
|
2745
|
+
const enabled = await isErpIntegrationEnabled3(cms, dataSource, entityMap);
|
|
2746
|
+
if (method === "GET") {
|
|
2747
|
+
return config.json({ enabled });
|
|
2748
|
+
}
|
|
2749
|
+
if (method === "POST") {
|
|
2750
|
+
if (!enabled) return config.json({ error: "ERP integration is disabled" }, { status: 409 });
|
|
2751
|
+
const { queueErpPaidOrderForOrderId: queueErpPaidOrderForOrderId2 } = await Promise.resolve().then(() => (init_paid_order_erp(), paid_order_erp_exports));
|
|
2752
|
+
await queueErpPaidOrderForOrderId2(cms, dataSource, entityMap, oid);
|
|
2753
|
+
return config.json({ ok: true });
|
|
2754
|
+
}
|
|
2755
|
+
return config.json({ error: "Method not allowed" }, { status: 405 });
|
|
2756
|
+
}
|
|
2099
2757
|
if (path.length === 0) return config.json({ error: "Not found" }, { status: 404 });
|
|
2100
2758
|
const resource = resolveResource(path[0]);
|
|
2101
2759
|
if (!crudResources.includes(resource)) return config.json({ error: "Invalid resource" }, { status: 400 });
|
|
@@ -2128,7 +2786,7 @@ function createCmsApiHandler(config) {
|
|
|
2128
2786
|
}
|
|
2129
2787
|
|
|
2130
2788
|
// src/api/storefront-handlers.ts
|
|
2131
|
-
var
|
|
2789
|
+
var import_typeorm5 = require("typeorm");
|
|
2132
2790
|
|
|
2133
2791
|
// src/lib/is-valid-signup-email.ts
|
|
2134
2792
|
var MAX_EMAIL = 254;
|
|
@@ -2150,6 +2808,339 @@ function isValidSignupEmail(email) {
|
|
|
2150
2808
|
|
|
2151
2809
|
// src/api/storefront-handlers.ts
|
|
2152
2810
|
init_email_queue();
|
|
2811
|
+
|
|
2812
|
+
// src/lib/order-number.ts
|
|
2813
|
+
var KIND_PREFIX = {
|
|
2814
|
+
sale: "OSL",
|
|
2815
|
+
return: "ORT",
|
|
2816
|
+
replacement: "ORP"
|
|
2817
|
+
};
|
|
2818
|
+
function orderNumberYymmUtc(at) {
|
|
2819
|
+
const yy = String(at.getUTCFullYear()).slice(-2);
|
|
2820
|
+
const mm = String(at.getUTCMonth() + 1).padStart(2, "0");
|
|
2821
|
+
return yy + mm;
|
|
2822
|
+
}
|
|
2823
|
+
function maskOrderIdSegment(id) {
|
|
2824
|
+
let x = id >>> 0 ^ 2779096485;
|
|
2825
|
+
x = Math.imul(x, 2654435761) >>> 0;
|
|
2826
|
+
return x.toString(36).toUpperCase().padStart(8, "0").slice(-8);
|
|
2827
|
+
}
|
|
2828
|
+
function buildCanonicalOrderNumber(kind, id, at) {
|
|
2829
|
+
return KIND_PREFIX[kind] + orderNumberYymmUtc(at) + maskOrderIdSegment(id);
|
|
2830
|
+
}
|
|
2831
|
+
function temporaryOrderNumberPlaceholder() {
|
|
2832
|
+
return `TMP${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`.toUpperCase();
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
// src/lib/order-storefront-metadata.ts
|
|
2836
|
+
function mergeOrderMetadataPatch(existing, patch) {
|
|
2837
|
+
const base = existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing } : {};
|
|
2838
|
+
if (patch.fulfillment !== void 0) {
|
|
2839
|
+
if (patch.fulfillment === null) delete base.fulfillment;
|
|
2840
|
+
else base.fulfillment = patch.fulfillment;
|
|
2841
|
+
}
|
|
2842
|
+
if (patch.invoice !== void 0) {
|
|
2843
|
+
if (patch.invoice === null) delete base.invoice;
|
|
2844
|
+
else base.invoice = patch.invoice;
|
|
2845
|
+
}
|
|
2846
|
+
return base;
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
// src/plugins/erp/erp-order-status-map.ts
|
|
2850
|
+
function mapErpSaleStatusToOrderStatus(erpLabel) {
|
|
2851
|
+
if (!erpLabel || typeof erpLabel !== "string") return void 0;
|
|
2852
|
+
const k = erpLabel.trim().toLowerCase().replace(/\s+/g, "_");
|
|
2853
|
+
const map = {
|
|
2854
|
+
draft: "pending",
|
|
2855
|
+
pending: "pending",
|
|
2856
|
+
open: "pending",
|
|
2857
|
+
new: "pending",
|
|
2858
|
+
unconfirmed: "pending",
|
|
2859
|
+
confirmed: "confirmed",
|
|
2860
|
+
processing: "processing",
|
|
2861
|
+
packed: "processing",
|
|
2862
|
+
shipped: "processing",
|
|
2863
|
+
in_transit: "processing",
|
|
2864
|
+
out_for_delivery: "processing",
|
|
2865
|
+
delivered: "completed",
|
|
2866
|
+
completed: "completed",
|
|
2867
|
+
closed: "completed",
|
|
2868
|
+
fulfilled: "completed",
|
|
2869
|
+
cancelled: "cancelled",
|
|
2870
|
+
canceled: "cancelled",
|
|
2871
|
+
void: "cancelled"
|
|
2872
|
+
};
|
|
2873
|
+
return map[k];
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
// src/plugins/erp/erp-order-sync.ts
|
|
2877
|
+
init_erp_response_map();
|
|
2878
|
+
init_erp_config_enabled();
|
|
2879
|
+
function pickInvoiceId2(data) {
|
|
2880
|
+
const nested = data.invoice && typeof data.invoice === "object" && !Array.isArray(data.invoice) ? data.invoice : null;
|
|
2881
|
+
const src = nested || data;
|
|
2882
|
+
for (const k of ["invoiceId", "invoice_id", "id"]) {
|
|
2883
|
+
const v = src[k];
|
|
2884
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
2885
|
+
}
|
|
2886
|
+
return void 0;
|
|
2887
|
+
}
|
|
2888
|
+
async function ensureChildOrdersFromRefs(orderRepo, parent, refs, contactId, currency) {
|
|
2889
|
+
for (const { ref, orderKind } of refs) {
|
|
2890
|
+
const existing = await orderRepo.createQueryBuilder("o").where("o.parentOrderId = :pid", { pid: parent.id }).andWhere("o.deleted = :d", { d: false }).andWhere("o.metadata->>'platformRef' = :ref", { ref }).getOne();
|
|
2891
|
+
if (existing) continue;
|
|
2892
|
+
const tmp = temporaryOrderNumberPlaceholder();
|
|
2893
|
+
const row = await orderRepo.save(
|
|
2894
|
+
orderRepo.create({
|
|
2895
|
+
orderNumber: tmp,
|
|
2896
|
+
orderKind,
|
|
2897
|
+
parentOrderId: parent.id,
|
|
2898
|
+
contactId,
|
|
2899
|
+
billingAddressId: null,
|
|
2900
|
+
shippingAddressId: null,
|
|
2901
|
+
status: "pending",
|
|
2902
|
+
subtotal: 0,
|
|
2903
|
+
tax: 0,
|
|
2904
|
+
discount: 0,
|
|
2905
|
+
total: 0,
|
|
2906
|
+
currency,
|
|
2907
|
+
metadata: { platformRef: ref },
|
|
2908
|
+
deleted: false
|
|
2909
|
+
})
|
|
2910
|
+
);
|
|
2911
|
+
const r = row;
|
|
2912
|
+
await orderRepo.update(r.id, {
|
|
2913
|
+
orderNumber: buildCanonicalOrderNumber(orderKind, r.id, r.createdAt ?? /* @__PURE__ */ new Date())
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
function deepMergeFulfillment(a, b) {
|
|
2918
|
+
if (!a) return b;
|
|
2919
|
+
if (!b) return a;
|
|
2920
|
+
return {
|
|
2921
|
+
...a,
|
|
2922
|
+
...b,
|
|
2923
|
+
events: b.events?.length ? b.events : a.events
|
|
2924
|
+
};
|
|
2925
|
+
}
|
|
2926
|
+
async function refreshOrderFromErp(cms, dataSource, entityMap, submission, order) {
|
|
2927
|
+
const orderRepo = dataSource.getRepository(entityMap.orders);
|
|
2928
|
+
const kind = order.orderKind || "sale";
|
|
2929
|
+
const meta = order.metadata && typeof order.metadata === "object" && !Array.isArray(order.metadata) ? { ...order.metadata } : {};
|
|
2930
|
+
if (kind === "sale") {
|
|
2931
|
+
const refId = String(order.orderNumber || "");
|
|
2932
|
+
let fulfillment;
|
|
2933
|
+
let invoiceNumber;
|
|
2934
|
+
let invoiceId;
|
|
2935
|
+
let newStatus;
|
|
2936
|
+
const r1 = await submission.postErpReadAction("get-order-status", { platformOrderId: refId });
|
|
2937
|
+
const d1 = r1.ok ? unwrapErpReadData(r1.json) : null;
|
|
2938
|
+
if (d1) {
|
|
2939
|
+
const mapped = mapErpSaleStatusToOrderStatus(
|
|
2940
|
+
typeof d1.status === "string" ? d1.status : typeof d1.orderStatus === "string" ? d1.orderStatus : typeof d1.state === "string" ? d1.state : void 0
|
|
2941
|
+
);
|
|
2942
|
+
if (mapped) newStatus = mapped;
|
|
2943
|
+
fulfillment = mapErpPayloadToFulfillment(d1);
|
|
2944
|
+
const refs = extractChildOrderRefsFromSalePayload(d1);
|
|
2945
|
+
if (refs.length) {
|
|
2946
|
+
await ensureChildOrdersFromRefs(
|
|
2947
|
+
orderRepo,
|
|
2948
|
+
order,
|
|
2949
|
+
refs,
|
|
2950
|
+
order.contactId,
|
|
2951
|
+
String(order.currency || "INR")
|
|
2952
|
+
);
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
const r2 = await submission.postErpReadAction("get-fulfillment-status", { platformOrderId: refId });
|
|
2956
|
+
const d2 = r2.ok ? unwrapErpReadData(r2.json) : null;
|
|
2957
|
+
if (d2) {
|
|
2958
|
+
fulfillment = deepMergeFulfillment(fulfillment, mapErpPayloadToFulfillment(d2));
|
|
2959
|
+
}
|
|
2960
|
+
const r3 = await submission.postErpReadAction("get-invoice", { platformOrderId: refId });
|
|
2961
|
+
const d3 = r3.ok ? unwrapErpReadData(r3.json) : null;
|
|
2962
|
+
if (d3) {
|
|
2963
|
+
invoiceNumber = mapErpPayloadToInvoiceNumber(d3);
|
|
2964
|
+
invoiceId = pickInvoiceId2(d3);
|
|
2965
|
+
}
|
|
2966
|
+
const oid = order.id;
|
|
2967
|
+
const prevInv = meta.invoice && typeof meta.invoice === "object" && !Array.isArray(meta.invoice) ? { ...meta.invoice } : {};
|
|
2968
|
+
const nextInvoice = {
|
|
2969
|
+
...prevInv,
|
|
2970
|
+
link: `/api/storefront/orders/${oid}/invoice`,
|
|
2971
|
+
...invoiceNumber ? { invoiceNumber } : {},
|
|
2972
|
+
...invoiceId ? { invoiceId } : {}
|
|
2973
|
+
};
|
|
2974
|
+
const patch = { invoice: nextInvoice };
|
|
2975
|
+
if (fulfillment !== void 0) patch.fulfillment = fulfillment;
|
|
2976
|
+
const nextMeta = mergeOrderMetadataPatch(meta, patch);
|
|
2977
|
+
await orderRepo.update(oid, {
|
|
2978
|
+
...newStatus ? { status: newStatus } : {},
|
|
2979
|
+
metadata: nextMeta,
|
|
2980
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2981
|
+
});
|
|
2982
|
+
return;
|
|
2983
|
+
}
|
|
2984
|
+
if (kind === "return" || kind === "replacement") {
|
|
2985
|
+
const platformReturnId = String(order.orderNumber || "");
|
|
2986
|
+
const r = await submission.postErpReadAction("get-return-status", { platformReturnId });
|
|
2987
|
+
const d = r.ok ? unwrapErpReadData(r.json) : null;
|
|
2988
|
+
if (!d) return;
|
|
2989
|
+
const mapped = mapErpSaleStatusToOrderStatus(
|
|
2990
|
+
typeof d.status === "string" ? d.status : typeof d.returnStatus === "string" ? d.returnStatus : void 0
|
|
2991
|
+
);
|
|
2992
|
+
const fulfillment = mapErpPayloadToFulfillment(d);
|
|
2993
|
+
const patch = {};
|
|
2994
|
+
if (fulfillment !== void 0) patch.fulfillment = fulfillment;
|
|
2995
|
+
const nextMeta = Object.keys(patch).length ? mergeOrderMetadataPatch(meta, patch) : meta;
|
|
2996
|
+
await orderRepo.update(order.id, {
|
|
2997
|
+
...mapped ? { status: mapped } : {},
|
|
2998
|
+
metadata: nextMeta,
|
|
2999
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3000
|
+
});
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
async function tryRefreshOrderFromErpForStorefront(cms, dataSource, entityMap, order) {
|
|
3004
|
+
try {
|
|
3005
|
+
const on = await isErpIntegrationEnabled(cms, dataSource, entityMap);
|
|
3006
|
+
if (!on) return;
|
|
3007
|
+
const erp = cms.getPlugin("erp");
|
|
3008
|
+
if (!erp?.submission) return;
|
|
3009
|
+
await refreshOrderFromErp(cms, dataSource, entityMap, erp.submission, order);
|
|
3010
|
+
} catch {
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
// src/api/storefront-handlers.ts
|
|
3015
|
+
init_erp_order_invoice();
|
|
3016
|
+
|
|
3017
|
+
// src/plugins/sms/sms-queue.ts
|
|
3018
|
+
var SMS_QUEUE_NAME = "sms";
|
|
3019
|
+
async function queueSms(cms, payload) {
|
|
3020
|
+
const queue = cms.getPlugin("queue");
|
|
3021
|
+
const sms = cms.getPlugin("sms");
|
|
3022
|
+
if (queue) {
|
|
3023
|
+
await queue.add(SMS_QUEUE_NAME, payload);
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
3026
|
+
if (sms && typeof sms.send === "function") {
|
|
3027
|
+
if (payload.templateKey?.trim()) {
|
|
3028
|
+
await sms.send({
|
|
3029
|
+
to: payload.to,
|
|
3030
|
+
templateKey: payload.templateKey.trim(),
|
|
3031
|
+
variables: payload.variables,
|
|
3032
|
+
otpCode: payload.otpCode
|
|
3033
|
+
});
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
if (payload.body?.trim()) {
|
|
3037
|
+
await sms.send({
|
|
3038
|
+
to: payload.to,
|
|
3039
|
+
body: payload.body,
|
|
3040
|
+
otpCode: payload.otpCode,
|
|
3041
|
+
variables: payload.variables
|
|
3042
|
+
});
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
// src/lib/otp-challenge.ts
|
|
3048
|
+
var import_crypto = require("crypto");
|
|
3049
|
+
var import_typeorm4 = require("typeorm");
|
|
3050
|
+
var OTP_TTL_MS = 10 * 60 * 1e3;
|
|
3051
|
+
var MAX_SENDS_PER_HOUR = 5;
|
|
3052
|
+
var MAX_VERIFY_ATTEMPTS = 8;
|
|
3053
|
+
function getPepper(explicit) {
|
|
3054
|
+
return (explicit || process.env.OTP_PEPPER || process.env.NEXTAUTH_SECRET || "dev-otp-pepper").trim();
|
|
3055
|
+
}
|
|
3056
|
+
function hashOtpCode(code, purpose, identifier, pepper) {
|
|
3057
|
+
return (0, import_crypto.createHmac)("sha256", getPepper(pepper)).update(`${purpose}|${identifier}|${code}`).digest("hex");
|
|
3058
|
+
}
|
|
3059
|
+
function verifyOtpCodeHash(code, storedHash, purpose, identifier, pepper) {
|
|
3060
|
+
const h = hashOtpCode(code, purpose, identifier, pepper);
|
|
3061
|
+
try {
|
|
3062
|
+
return (0, import_crypto.timingSafeEqual)(Buffer.from(h, "utf8"), Buffer.from(storedHash, "utf8"));
|
|
3063
|
+
} catch {
|
|
3064
|
+
return false;
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
function generateNumericOtp(length = 6) {
|
|
3068
|
+
const max = 10 ** length;
|
|
3069
|
+
return (0, import_crypto.randomInt)(0, max).toString().padStart(length, "0");
|
|
3070
|
+
}
|
|
3071
|
+
function normalizePhoneE164(raw, defaultCountryCode) {
|
|
3072
|
+
const t = raw.trim();
|
|
3073
|
+
const digitsOnly = t.replace(/\D/g, "");
|
|
3074
|
+
if (digitsOnly.length < 10) return null;
|
|
3075
|
+
if (t.startsWith("+")) return `+${digitsOnly}`;
|
|
3076
|
+
const cc = (defaultCountryCode || process.env.DEFAULT_PHONE_COUNTRY_CODE || "91").replace(/\D/g, "");
|
|
3077
|
+
if (digitsOnly.length > 10) return `+${digitsOnly}`;
|
|
3078
|
+
return `+${cc}${digitsOnly}`;
|
|
3079
|
+
}
|
|
3080
|
+
async function countRecentOtpSends(dataSource, entityMap, purpose, identifier, since) {
|
|
3081
|
+
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
3082
|
+
return repo.count({
|
|
3083
|
+
where: { purpose, identifier, createdAt: (0, import_typeorm4.MoreThan)(since) }
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
3086
|
+
async function createOtpChallenge(dataSource, entityMap, input) {
|
|
3087
|
+
const { purpose, channel, identifier, code, pepper } = input;
|
|
3088
|
+
const since = new Date(Date.now() - 60 * 60 * 1e3);
|
|
3089
|
+
const recent = await countRecentOtpSends(dataSource, entityMap, purpose, identifier, since);
|
|
3090
|
+
if (recent >= MAX_SENDS_PER_HOUR) {
|
|
3091
|
+
return { ok: false, error: "Too many codes sent. Try again later.", status: 429 };
|
|
3092
|
+
}
|
|
3093
|
+
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
3094
|
+
await repo.delete({
|
|
3095
|
+
purpose,
|
|
3096
|
+
identifier,
|
|
3097
|
+
consumedAt: (0, import_typeorm4.IsNull)()
|
|
3098
|
+
});
|
|
3099
|
+
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
3100
|
+
const codeHash = hashOtpCode(code, purpose, identifier, pepper);
|
|
3101
|
+
await repo.save(
|
|
3102
|
+
repo.create({
|
|
3103
|
+
purpose,
|
|
3104
|
+
channel,
|
|
3105
|
+
identifier,
|
|
3106
|
+
codeHash,
|
|
3107
|
+
expiresAt,
|
|
3108
|
+
attempts: 0,
|
|
3109
|
+
consumedAt: null
|
|
3110
|
+
})
|
|
3111
|
+
);
|
|
3112
|
+
return { ok: true };
|
|
3113
|
+
}
|
|
3114
|
+
async function verifyAndConsumeOtpChallenge(dataSource, entityMap, input) {
|
|
3115
|
+
const { purpose, identifier, code, pepper } = input;
|
|
3116
|
+
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
3117
|
+
const row = await repo.findOne({
|
|
3118
|
+
where: { purpose, identifier, consumedAt: (0, import_typeorm4.IsNull)() },
|
|
3119
|
+
order: { id: "DESC" }
|
|
3120
|
+
});
|
|
3121
|
+
if (!row) {
|
|
3122
|
+
return { ok: false, error: "Invalid or expired code", status: 400 };
|
|
3123
|
+
}
|
|
3124
|
+
const r = row;
|
|
3125
|
+
if (new Date(r.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
3126
|
+
await repo.delete(row.id);
|
|
3127
|
+
return { ok: false, error: "Invalid or expired code", status: 400 };
|
|
3128
|
+
}
|
|
3129
|
+
const attempts = r.attempts || 0;
|
|
3130
|
+
if (attempts >= MAX_VERIFY_ATTEMPTS) {
|
|
3131
|
+
await repo.delete(row.id);
|
|
3132
|
+
return { ok: false, error: "Too many attempts", status: 400 };
|
|
3133
|
+
}
|
|
3134
|
+
const valid = verifyOtpCodeHash(code, r.codeHash, purpose, identifier, pepper);
|
|
3135
|
+
if (!valid) {
|
|
3136
|
+
await repo.update(row.id, { attempts: attempts + 1 });
|
|
3137
|
+
return { ok: false, error: "Invalid or expired code", status: 400 };
|
|
3138
|
+
}
|
|
3139
|
+
await repo.update(row.id, { consumedAt: /* @__PURE__ */ new Date(), attempts: attempts + 1 });
|
|
3140
|
+
return { ok: true };
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
// src/api/storefront-handlers.ts
|
|
2153
3144
|
var GUEST_COOKIE = "guest_id";
|
|
2154
3145
|
var ONE_YEAR = 60 * 60 * 24 * 365;
|
|
2155
3146
|
function parseCookies(header) {
|
|
@@ -2167,13 +3158,17 @@ function parseCookies(header) {
|
|
|
2167
3158
|
function guestCookieHeader(name, token) {
|
|
2168
3159
|
return `${name}=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${ONE_YEAR}`;
|
|
2169
3160
|
}
|
|
2170
|
-
function orderNumber() {
|
|
2171
|
-
return `ORD-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
2172
|
-
}
|
|
2173
3161
|
var SIGNUP_VERIFY_EXPIRY_HOURS = 72;
|
|
2174
3162
|
function createStorefrontApiHandler(config) {
|
|
2175
3163
|
const { dataSource, entityMap, json, getSessionUser, getCms, getCompanyDetails, publicSiteUrl } = config;
|
|
2176
3164
|
const cookieName = config.guestCookieName ?? GUEST_COOKIE;
|
|
3165
|
+
const otpFlags = config.otpFlags;
|
|
3166
|
+
const otpPepper = config.otpPepper;
|
|
3167
|
+
const defaultPhoneCc = config.defaultPhoneCountryCode;
|
|
3168
|
+
const otpAllowPhoneLogin = config.otpAllowPhoneLogin !== false;
|
|
3169
|
+
function otpOff(key) {
|
|
3170
|
+
return !otpFlags || otpFlags[key] !== true;
|
|
3171
|
+
}
|
|
2177
3172
|
const cartRepo = () => dataSource.getRepository(entityMap.carts);
|
|
2178
3173
|
const cartItemRepo = () => dataSource.getRepository(entityMap.cart_items);
|
|
2179
3174
|
const productRepo = () => dataSource.getRepository(entityMap.products);
|
|
@@ -2187,13 +3182,174 @@ function createStorefrontApiHandler(config) {
|
|
|
2187
3182
|
const tokenRepo = () => dataSource.getRepository(entityMap.password_reset_tokens);
|
|
2188
3183
|
const collectionRepo = () => dataSource.getRepository(entityMap.collections);
|
|
2189
3184
|
const groupRepo = () => dataSource.getRepository(entityMap.user_groups);
|
|
3185
|
+
const configRepo = () => dataSource.getRepository(entityMap.configs);
|
|
3186
|
+
const CART_CHECKOUT_RELATIONS = ["items", "items.product", "items.product.taxes", "items.product.taxes.tax"];
|
|
3187
|
+
function roundMoney2(n) {
|
|
3188
|
+
return Math.round(n * 100) / 100;
|
|
3189
|
+
}
|
|
3190
|
+
async function getStoreDefaultTaxRate() {
|
|
3191
|
+
const rows = await configRepo().find({ where: { settings: "store", deleted: false } });
|
|
3192
|
+
for (const row of rows) {
|
|
3193
|
+
const r = row;
|
|
3194
|
+
if (r.key === "defaultTaxRate") {
|
|
3195
|
+
const n = parseFloat(String(r.value ?? "").trim());
|
|
3196
|
+
return Number.isFinite(n) && n >= 0 ? n : null;
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
return null;
|
|
3200
|
+
}
|
|
3201
|
+
function computeTaxForProductLine(p, lineSubtotal, defaultRate) {
|
|
3202
|
+
const pts = p.taxes ?? [];
|
|
3203
|
+
const activePts = pts.filter((pt) => {
|
|
3204
|
+
const t = pt.tax;
|
|
3205
|
+
return t != null && t.active !== false;
|
|
3206
|
+
});
|
|
3207
|
+
if (activePts.length) {
|
|
3208
|
+
let sumRate = 0;
|
|
3209
|
+
const slugs = [];
|
|
3210
|
+
for (const pt of activePts) {
|
|
3211
|
+
const t = pt.tax;
|
|
3212
|
+
const r = Number(pt.rate != null && pt.rate !== "" ? pt.rate : t.rate ?? 0);
|
|
3213
|
+
if (Number.isFinite(r)) sumRate += r;
|
|
3214
|
+
const slug = String(t.slug ?? "").trim();
|
|
3215
|
+
if (slug) slugs.push(slug);
|
|
3216
|
+
}
|
|
3217
|
+
const tax = roundMoney2(lineSubtotal * sumRate / 100);
|
|
3218
|
+
return {
|
|
3219
|
+
tax,
|
|
3220
|
+
taxRate: sumRate > 0 ? roundMoney2(sumRate) : null,
|
|
3221
|
+
taxCode: slugs.length ? [...new Set(slugs)].sort().join(",") : null
|
|
3222
|
+
};
|
|
3223
|
+
}
|
|
3224
|
+
if (defaultRate != null && defaultRate > 0) {
|
|
3225
|
+
return {
|
|
3226
|
+
tax: roundMoney2(lineSubtotal * defaultRate / 100),
|
|
3227
|
+
taxRate: roundMoney2(defaultRate),
|
|
3228
|
+
taxCode: null
|
|
3229
|
+
};
|
|
3230
|
+
}
|
|
3231
|
+
return { tax: 0, taxRate: null, taxCode: null };
|
|
3232
|
+
}
|
|
3233
|
+
function parseInlineAddress(raw) {
|
|
3234
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
3235
|
+
const o = raw;
|
|
3236
|
+
const line1 = String(o.line1 ?? "").trim();
|
|
3237
|
+
if (!line1) return null;
|
|
3238
|
+
return {
|
|
3239
|
+
line1,
|
|
3240
|
+
line2: o.line2 != null ? String(o.line2) : "",
|
|
3241
|
+
city: o.city != null ? String(o.city) : "",
|
|
3242
|
+
state: o.state != null ? String(o.state) : "",
|
|
3243
|
+
postalCode: o.postalCode != null ? String(o.postalCode) : "",
|
|
3244
|
+
country: o.country != null ? String(o.country) : ""
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3247
|
+
function intFromBody(v) {
|
|
3248
|
+
if (typeof v === "number" && Number.isInteger(v)) return v;
|
|
3249
|
+
if (typeof v === "string" && /^\d+$/.test(v)) return parseInt(v, 10);
|
|
3250
|
+
return void 0;
|
|
3251
|
+
}
|
|
3252
|
+
async function resolveCheckoutAddress(contactId, idVal, inlineVal) {
|
|
3253
|
+
const aid = intFromBody(idVal);
|
|
3254
|
+
if (aid != null) {
|
|
3255
|
+
const existing = await addressRepo().findOne({
|
|
3256
|
+
where: { id: aid, contactId }
|
|
3257
|
+
});
|
|
3258
|
+
if (!existing) return { id: null, error: "Address not found" };
|
|
3259
|
+
return { id: aid };
|
|
3260
|
+
}
|
|
3261
|
+
const addr = parseInlineAddress(inlineVal);
|
|
3262
|
+
if (addr) {
|
|
3263
|
+
const saved = await addressRepo().save(
|
|
3264
|
+
addressRepo().create({
|
|
3265
|
+
contactId,
|
|
3266
|
+
line1: addr.line1,
|
|
3267
|
+
line2: addr.line2?.trim() ? addr.line2 : null,
|
|
3268
|
+
city: addr.city?.trim() ? addr.city : null,
|
|
3269
|
+
state: addr.state?.trim() ? addr.state : null,
|
|
3270
|
+
postalCode: addr.postalCode?.trim() ? addr.postalCode : null,
|
|
3271
|
+
country: addr.country?.trim() ? addr.country : null
|
|
3272
|
+
})
|
|
3273
|
+
);
|
|
3274
|
+
return { id: saved.id };
|
|
3275
|
+
}
|
|
3276
|
+
return { id: null };
|
|
3277
|
+
}
|
|
3278
|
+
async function prepareCheckoutFromCart(b, cart, contactId) {
|
|
3279
|
+
const defaultRate = await getStoreDefaultTaxRate();
|
|
3280
|
+
const lines = [];
|
|
3281
|
+
let subtotal = 0;
|
|
3282
|
+
let orderTax = 0;
|
|
3283
|
+
let needsShipping = false;
|
|
3284
|
+
for (const it of cart.items || []) {
|
|
3285
|
+
const p = it.product;
|
|
3286
|
+
if (!p || p.deleted || p.status !== "available") continue;
|
|
3287
|
+
const unit = Number(p.price);
|
|
3288
|
+
const qty = it.quantity || 1;
|
|
3289
|
+
const lineSubtotal = unit * qty;
|
|
3290
|
+
const pType = p.type === "service" ? "service" : "product";
|
|
3291
|
+
if (pType === "product") needsShipping = true;
|
|
3292
|
+
const { tax, taxRate, taxCode } = computeTaxForProductLine(p, lineSubtotal, defaultRate);
|
|
3293
|
+
const lineTotal = roundMoney2(lineSubtotal + tax);
|
|
3294
|
+
subtotal = roundMoney2(subtotal + lineSubtotal);
|
|
3295
|
+
orderTax = roundMoney2(orderTax + tax);
|
|
3296
|
+
lines.push({
|
|
3297
|
+
productId: p.id,
|
|
3298
|
+
quantity: qty,
|
|
3299
|
+
unitPrice: unit,
|
|
3300
|
+
tax,
|
|
3301
|
+
total: lineTotal,
|
|
3302
|
+
hsn: p.hsn ?? null,
|
|
3303
|
+
uom: p.uom ?? null,
|
|
3304
|
+
productType: pType,
|
|
3305
|
+
taxRate,
|
|
3306
|
+
taxCode
|
|
3307
|
+
});
|
|
3308
|
+
}
|
|
3309
|
+
if (!lines.length) return { ok: false, status: 400, message: "No available items in cart" };
|
|
3310
|
+
const bill = await resolveCheckoutAddress(contactId, b.billingAddressId, b.billingAddress);
|
|
3311
|
+
if (bill.error) return { ok: false, status: 400, message: bill.error };
|
|
3312
|
+
if (bill.id == null) return { ok: false, status: 400, message: "Billing address required" };
|
|
3313
|
+
const ship = await resolveCheckoutAddress(contactId, b.shippingAddressId, b.shippingAddress);
|
|
3314
|
+
if (ship.error) return { ok: false, status: 400, message: ship.error };
|
|
3315
|
+
let shippingAddressId = ship.id;
|
|
3316
|
+
if (needsShipping && shippingAddressId == null) shippingAddressId = bill.id;
|
|
3317
|
+
if (needsShipping && shippingAddressId == null) {
|
|
3318
|
+
return { ok: false, status: 400, message: "Shipping address required" };
|
|
3319
|
+
}
|
|
3320
|
+
const orderTotal = roundMoney2(subtotal + orderTax);
|
|
3321
|
+
return {
|
|
3322
|
+
ok: true,
|
|
3323
|
+
lines,
|
|
3324
|
+
subtotal,
|
|
3325
|
+
orderTax,
|
|
3326
|
+
orderTotal,
|
|
3327
|
+
billingAddressId: bill.id,
|
|
3328
|
+
shippingAddressId
|
|
3329
|
+
};
|
|
3330
|
+
}
|
|
3331
|
+
async function syncContactToErp(contact) {
|
|
3332
|
+
if (!getCms) return;
|
|
3333
|
+
try {
|
|
3334
|
+
const cms = await getCms();
|
|
3335
|
+
await queueErpCreateContactIfEnabled(cms, dataSource, entityMap, {
|
|
3336
|
+
name: String(contact.name ?? ""),
|
|
3337
|
+
email: String(contact.email ?? "").trim(),
|
|
3338
|
+
phone: contact.phone,
|
|
3339
|
+
type: contact.type,
|
|
3340
|
+
company: contact.company,
|
|
3341
|
+
notes: contact.notes
|
|
3342
|
+
});
|
|
3343
|
+
} catch {
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
2190
3346
|
async function ensureContactForUser(userId) {
|
|
2191
3347
|
let c = await contactRepo().findOne({ where: { userId, deleted: false } });
|
|
2192
3348
|
if (c) return c;
|
|
2193
3349
|
const u = await userRepo().findOne({ where: { id: userId } });
|
|
2194
3350
|
if (!u) return null;
|
|
2195
3351
|
const unclaimed = await contactRepo().findOne({
|
|
2196
|
-
where: { email: u.email, userId: (0,
|
|
3352
|
+
where: { email: u.email, userId: (0, import_typeorm5.IsNull)(), deleted: false }
|
|
2197
3353
|
});
|
|
2198
3354
|
if (unclaimed) {
|
|
2199
3355
|
await contactRepo().update(unclaimed.id, { userId });
|
|
@@ -2208,6 +3364,7 @@ function createStorefrontApiHandler(config) {
|
|
|
2208
3364
|
deleted: false
|
|
2209
3365
|
})
|
|
2210
3366
|
);
|
|
3367
|
+
await syncContactToErp(created);
|
|
2211
3368
|
return { id: created.id };
|
|
2212
3369
|
}
|
|
2213
3370
|
async function getOrCreateCart(req) {
|
|
@@ -2296,24 +3453,42 @@ function createStorefrontApiHandler(config) {
|
|
|
2296
3453
|
slug: p.slug,
|
|
2297
3454
|
price: p.price,
|
|
2298
3455
|
sku: p.sku,
|
|
3456
|
+
type: p.type === "service" ? "service" : "product",
|
|
2299
3457
|
image: primaryProductImageUrl(p.metadata)
|
|
2300
3458
|
} : null
|
|
2301
3459
|
};
|
|
2302
3460
|
})
|
|
2303
3461
|
};
|
|
2304
3462
|
}
|
|
3463
|
+
function serializeSeo(seo) {
|
|
3464
|
+
if (!seo || typeof seo !== "object") return void 0;
|
|
3465
|
+
const s = seo;
|
|
3466
|
+
return {
|
|
3467
|
+
title: s.title ?? null,
|
|
3468
|
+
description: s.description ?? null,
|
|
3469
|
+
keywords: s.keywords ?? null,
|
|
3470
|
+
ogTitle: s.ogTitle ?? null,
|
|
3471
|
+
ogDescription: s.ogDescription ?? null,
|
|
3472
|
+
ogImage: s.ogImage ?? null,
|
|
3473
|
+
slug: s.slug ?? null
|
|
3474
|
+
};
|
|
3475
|
+
}
|
|
2305
3476
|
function serializeProduct(p) {
|
|
3477
|
+
const seo = serializeSeo(p.seo);
|
|
2306
3478
|
return {
|
|
2307
3479
|
id: p.id,
|
|
2308
3480
|
name: p.name,
|
|
2309
3481
|
slug: p.slug,
|
|
2310
3482
|
sku: p.sku,
|
|
2311
3483
|
hsn: p.hsn,
|
|
3484
|
+
uom: p.uom ?? null,
|
|
3485
|
+
type: p.type === "service" ? "service" : "product",
|
|
2312
3486
|
price: p.price,
|
|
2313
3487
|
compareAtPrice: p.compareAtPrice,
|
|
2314
3488
|
status: p.status,
|
|
2315
3489
|
collectionId: p.collectionId,
|
|
2316
|
-
metadata: p.metadata
|
|
3490
|
+
metadata: p.metadata,
|
|
3491
|
+
...seo ? { seo } : {}
|
|
2317
3492
|
};
|
|
2318
3493
|
}
|
|
2319
3494
|
return {
|
|
@@ -2380,7 +3555,7 @@ function createStorefrontApiHandler(config) {
|
|
|
2380
3555
|
const byId = /^\d+$/.test(idOrSlug);
|
|
2381
3556
|
const product = await productRepo().findOne({
|
|
2382
3557
|
where: byId ? { id: parseInt(idOrSlug, 10), status: "available", deleted: false } : { slug: idOrSlug, status: "available", deleted: false },
|
|
2383
|
-
relations: ["attributes", "attributes.attribute"]
|
|
3558
|
+
relations: ["attributes", "attributes.attribute", "seo"]
|
|
2384
3559
|
});
|
|
2385
3560
|
if (!product) return json({ error: "Not found" }, { status: 404 });
|
|
2386
3561
|
const p = product;
|
|
@@ -2424,7 +3599,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2424
3599
|
const idOrSlug = path[1];
|
|
2425
3600
|
const byId = /^\d+$/.test(idOrSlug);
|
|
2426
3601
|
const collection = await collectionRepo().findOne({
|
|
2427
|
-
where: byId ? { id: parseInt(idOrSlug, 10), active: true, deleted: false } : { slug: idOrSlug, active: true, deleted: false }
|
|
3602
|
+
where: byId ? { id: parseInt(idOrSlug, 10), active: true, deleted: false } : { slug: idOrSlug, active: true, deleted: false },
|
|
3603
|
+
relations: ["seo"]
|
|
2428
3604
|
});
|
|
2429
3605
|
if (!collection) return json({ error: "Not found" }, { status: 404 });
|
|
2430
3606
|
const col = collection;
|
|
@@ -2432,12 +3608,14 @@ function createStorefrontApiHandler(config) {
|
|
|
2432
3608
|
where: { collectionId: col.id, status: "available", deleted: false },
|
|
2433
3609
|
order: { id: "ASC" }
|
|
2434
3610
|
});
|
|
3611
|
+
const colSeo = serializeSeo(col.seo);
|
|
2435
3612
|
return json({
|
|
2436
3613
|
id: col.id,
|
|
2437
3614
|
name: col.name,
|
|
2438
3615
|
slug: col.slug,
|
|
2439
3616
|
description: col.description,
|
|
2440
3617
|
image: col.image,
|
|
3618
|
+
...colSeo ? { seo: colSeo } : {},
|
|
2441
3619
|
products: products.map((p) => serializeProduct(p))
|
|
2442
3620
|
});
|
|
2443
3621
|
}
|
|
@@ -2475,6 +3653,7 @@ function createStorefrontApiHandler(config) {
|
|
|
2475
3653
|
await userRepo().update(uid, { name: b.name.trim() });
|
|
2476
3654
|
}
|
|
2477
3655
|
const updatedContact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
3656
|
+
if (updatedContact) await syncContactToErp(updatedContact);
|
|
2478
3657
|
const updatedUser = await userRepo().findOne({ where: { id: uid }, select: ["id", "name", "email"] });
|
|
2479
3658
|
return json({
|
|
2480
3659
|
user: updatedUser ? { id: updatedUser.id, name: updatedUser.name, email: updatedUser.email } : null,
|
|
@@ -2562,13 +3741,155 @@ function createStorefrontApiHandler(config) {
|
|
|
2562
3741
|
const email = record.email;
|
|
2563
3742
|
const user = await userRepo().findOne({ where: { email }, select: ["id", "blocked"] });
|
|
2564
3743
|
if (!user) return json({ error: "User not found" }, { status: 400 });
|
|
2565
|
-
await userRepo().update(user.id, {
|
|
3744
|
+
await userRepo().update(user.id, {
|
|
3745
|
+
blocked: false,
|
|
3746
|
+
emailVerifiedAt: /* @__PURE__ */ new Date(),
|
|
3747
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3748
|
+
});
|
|
2566
3749
|
await tokenRepo().delete({ email });
|
|
2567
3750
|
return json({ success: true, message: "Email verified. You can sign in." });
|
|
2568
3751
|
}
|
|
3752
|
+
if (path[0] === "auth" && path[1] === "otp" && path[2] === "send" && path.length === 3 && method === "POST") {
|
|
3753
|
+
const b = await req.json().catch(() => ({}));
|
|
3754
|
+
const purposeRaw = typeof b.purpose === "string" ? b.purpose.trim() : "";
|
|
3755
|
+
const purpose = purposeRaw === "login" || purposeRaw === "verify_email" || purposeRaw === "verify_phone" ? purposeRaw : "";
|
|
3756
|
+
if (!purpose) return json({ error: "purpose must be login, verify_email, or verify_phone" }, { status: 400 });
|
|
3757
|
+
if (purpose === "login" && otpOff("login")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
3758
|
+
if (purpose === "verify_email" && otpOff("verifyEmail")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
3759
|
+
if (purpose === "verify_phone" && otpOff("verifyPhone")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
3760
|
+
const capOtp = await assertCaptchaOk(getCms, b, req, json);
|
|
3761
|
+
if (capOtp) return capOtp;
|
|
3762
|
+
const emailIn = typeof b.email === "string" ? b.email.trim().toLowerCase() : "";
|
|
3763
|
+
const phoneIn = typeof b.phone === "string" ? b.phone.trim() : "";
|
|
3764
|
+
let identifier;
|
|
3765
|
+
let channel;
|
|
3766
|
+
if (purpose === "login") {
|
|
3767
|
+
if (emailIn) {
|
|
3768
|
+
identifier = emailIn;
|
|
3769
|
+
channel = "email";
|
|
3770
|
+
} else if (phoneIn) {
|
|
3771
|
+
if (!otpAllowPhoneLogin) {
|
|
3772
|
+
return json({ error: "Phone sign-in is not enabled" }, { status: 403 });
|
|
3773
|
+
}
|
|
3774
|
+
const p = normalizePhoneE164(phoneIn, defaultPhoneCc);
|
|
3775
|
+
if (!p) return json({ error: "Invalid phone" }, { status: 400 });
|
|
3776
|
+
identifier = p;
|
|
3777
|
+
channel = "sms";
|
|
3778
|
+
} else {
|
|
3779
|
+
return json({ error: "email or phone required" }, { status: 400 });
|
|
3780
|
+
}
|
|
3781
|
+
const user = channel === "email" ? await userRepo().findOne({ where: { email: identifier } }) : await userRepo().findOne({ where: { phone: identifier } });
|
|
3782
|
+
if (!user || user.deleted || user.blocked) {
|
|
3783
|
+
return json({ ok: true });
|
|
3784
|
+
}
|
|
3785
|
+
} else if (purpose === "verify_email") {
|
|
3786
|
+
if (!emailIn || !isValidSignupEmail(emailIn)) return json({ error: "Valid email required" }, { status: 400 });
|
|
3787
|
+
identifier = emailIn;
|
|
3788
|
+
channel = "email";
|
|
3789
|
+
const user = await userRepo().findOne({ where: { email: identifier } });
|
|
3790
|
+
if (!user || user.deleted) return json({ ok: true });
|
|
3791
|
+
} else {
|
|
3792
|
+
const su = await getSessionUser();
|
|
3793
|
+
const uid = su?.id ? parseInt(String(su.id), 10) : NaN;
|
|
3794
|
+
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
3795
|
+
const p = normalizePhoneE164(phoneIn, defaultPhoneCc);
|
|
3796
|
+
if (!p) return json({ error: "Valid phone required" }, { status: 400 });
|
|
3797
|
+
identifier = p;
|
|
3798
|
+
channel = "sms";
|
|
3799
|
+
const taken = await userRepo().findOne({
|
|
3800
|
+
where: { phone: identifier },
|
|
3801
|
+
select: ["id"]
|
|
3802
|
+
});
|
|
3803
|
+
if (taken && taken.id !== uid) {
|
|
3804
|
+
return json({ error: "Phone already in use" }, { status: 400 });
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
const code = generateNumericOtp(6);
|
|
3808
|
+
const created = await createOtpChallenge(dataSource, entityMap, {
|
|
3809
|
+
purpose,
|
|
3810
|
+
channel,
|
|
3811
|
+
identifier,
|
|
3812
|
+
code,
|
|
3813
|
+
pepper: otpPepper
|
|
3814
|
+
});
|
|
3815
|
+
if (!created.ok) return json({ error: created.error }, { status: created.status });
|
|
3816
|
+
if (!getCms) return json({ error: "OTP delivery not configured" }, { status: 503 });
|
|
3817
|
+
try {
|
|
3818
|
+
const cms = await getCms();
|
|
3819
|
+
if (channel === "email") {
|
|
3820
|
+
if (!cms.getPlugin("email")) return json({ error: "Email not configured" }, { status: 503 });
|
|
3821
|
+
const companyDetails = getCompanyDetails ? await getCompanyDetails() : {};
|
|
3822
|
+
await queueEmail(cms, {
|
|
3823
|
+
to: identifier,
|
|
3824
|
+
templateName: "otp",
|
|
3825
|
+
ctx: { code, companyDetails: companyDetails ?? {} }
|
|
3826
|
+
});
|
|
3827
|
+
} else {
|
|
3828
|
+
if (!cms.getPlugin("sms")) return json({ error: "SMS not configured" }, { status: 503 });
|
|
3829
|
+
const templateKey = purpose === "verify_phone" ? "auth.otp_verify_phone" : "auth.otp_login";
|
|
3830
|
+
await queueSms(cms, { to: identifier, templateKey, variables: { code } });
|
|
3831
|
+
}
|
|
3832
|
+
} catch {
|
|
3833
|
+
return json({ error: "Failed to send code" }, { status: 500 });
|
|
3834
|
+
}
|
|
3835
|
+
return json({ ok: true });
|
|
3836
|
+
}
|
|
3837
|
+
if (path[0] === "auth" && path[1] === "otp" && path[2] === "verify-email" && path.length === 3 && method === "POST") {
|
|
3838
|
+
if (otpOff("verifyEmail")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
3839
|
+
const b = await req.json().catch(() => ({}));
|
|
3840
|
+
const email = typeof b.email === "string" ? b.email.trim().toLowerCase() : "";
|
|
3841
|
+
const code = typeof b.code === "string" ? b.code.trim() : "";
|
|
3842
|
+
if (!email || !code) return json({ error: "email and code required" }, { status: 400 });
|
|
3843
|
+
const v = await verifyAndConsumeOtpChallenge(dataSource, entityMap, {
|
|
3844
|
+
purpose: "verify_email",
|
|
3845
|
+
identifier: email,
|
|
3846
|
+
code,
|
|
3847
|
+
pepper: otpPepper
|
|
3848
|
+
});
|
|
3849
|
+
if (!v.ok) return json({ error: v.error }, { status: v.status });
|
|
3850
|
+
const user = await userRepo().findOne({ where: { email } });
|
|
3851
|
+
if (!user) return json({ error: "User not found" }, { status: 400 });
|
|
3852
|
+
await userRepo().update(user.id, {
|
|
3853
|
+
blocked: false,
|
|
3854
|
+
emailVerifiedAt: /* @__PURE__ */ new Date(),
|
|
3855
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3856
|
+
});
|
|
3857
|
+
await tokenRepo().delete({ email });
|
|
3858
|
+
return json({ success: true, message: "Email verified. You can sign in." });
|
|
3859
|
+
}
|
|
3860
|
+
if (path[0] === "auth" && path[1] === "otp" && path[2] === "verify-phone" && path.length === 3 && method === "POST") {
|
|
3861
|
+
if (otpOff("verifyPhone")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
3862
|
+
const su = await getSessionUser();
|
|
3863
|
+
const uid = su?.id ? parseInt(String(su.id), 10) : NaN;
|
|
3864
|
+
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
3865
|
+
const b = await req.json().catch(() => ({}));
|
|
3866
|
+
const phoneRaw = typeof b.phone === "string" ? b.phone.trim() : "";
|
|
3867
|
+
const code = typeof b.code === "string" ? b.code.trim() : "";
|
|
3868
|
+
const phone = normalizePhoneE164(phoneRaw, defaultPhoneCc);
|
|
3869
|
+
if (!phone || !code) return json({ error: "phone and code required" }, { status: 400 });
|
|
3870
|
+
const v = await verifyAndConsumeOtpChallenge(dataSource, entityMap, {
|
|
3871
|
+
purpose: "verify_phone",
|
|
3872
|
+
identifier: phone,
|
|
3873
|
+
code,
|
|
3874
|
+
pepper: otpPepper
|
|
3875
|
+
});
|
|
3876
|
+
if (!v.ok) return json({ error: v.error }, { status: v.status });
|
|
3877
|
+
const taken = await userRepo().findOne({ where: { phone }, select: ["id"] });
|
|
3878
|
+
if (taken && taken.id !== uid) {
|
|
3879
|
+
return json({ error: "Phone already in use" }, { status: 400 });
|
|
3880
|
+
}
|
|
3881
|
+
await userRepo().update(uid, { phone, phoneVerifiedAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() });
|
|
3882
|
+
const contact = await ensureContactForUser(uid);
|
|
3883
|
+
if (contact) {
|
|
3884
|
+
await contactRepo().update(contact.id, { phone });
|
|
3885
|
+
}
|
|
3886
|
+
return json({ success: true });
|
|
3887
|
+
}
|
|
2569
3888
|
if (path[0] === "register" && path.length === 1 && method === "POST") {
|
|
2570
3889
|
if (!config.hashPassword) return json({ error: "Registration not configured" }, { status: 501 });
|
|
2571
3890
|
const b = await req.json().catch(() => ({}));
|
|
3891
|
+
const capReg = await assertCaptchaOk(getCms, b, req, json);
|
|
3892
|
+
if (capReg) return capReg;
|
|
2572
3893
|
const name = typeof b.name === "string" ? b.name.trim() : "";
|
|
2573
3894
|
const email = typeof b.email === "string" ? b.email.trim().toLowerCase() : "";
|
|
2574
3895
|
const password = typeof b.password === "string" ? b.password : "";
|
|
@@ -2630,6 +3951,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2630
3951
|
}
|
|
2631
3952
|
if (path[0] === "cart" && path[1] === "items" && path.length === 2 && method === "POST") {
|
|
2632
3953
|
const body = await req.json().catch(() => ({}));
|
|
3954
|
+
const capCart = await assertCaptchaOk(getCms, body, req, json);
|
|
3955
|
+
if (capCart) return capCart;
|
|
2633
3956
|
const productId = Number(body.productId);
|
|
2634
3957
|
const quantity = Math.max(1, Number(body.quantity) || 1);
|
|
2635
3958
|
if (!Number.isFinite(productId)) return json({ error: "productId required" }, { status: 400 });
|
|
@@ -2829,6 +4152,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2829
4152
|
const { wishlist, setCookie, err } = await getOrCreateWishlist(req);
|
|
2830
4153
|
if (err) return err;
|
|
2831
4154
|
const b = await req.json().catch(() => ({}));
|
|
4155
|
+
const capWl = await assertCaptchaOk(getCms, b, req, json);
|
|
4156
|
+
if (capWl) return capWl;
|
|
2832
4157
|
const productId = Number(b.productId);
|
|
2833
4158
|
if (!Number.isFinite(productId)) return json({ error: "productId required" }, { status: 400 });
|
|
2834
4159
|
const wid = wishlist.id;
|
|
@@ -2847,6 +4172,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2847
4172
|
}
|
|
2848
4173
|
if (path[0] === "checkout" && path[1] === "order" && path.length === 2 && method === "POST") {
|
|
2849
4174
|
const b = await req.json().catch(() => ({}));
|
|
4175
|
+
const capOrd = await assertCaptchaOk(getCms, b, req, json);
|
|
4176
|
+
if (capOrd) return capOrd;
|
|
2850
4177
|
const u = await getSessionUser();
|
|
2851
4178
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
2852
4179
|
let contactId;
|
|
@@ -2857,11 +4184,11 @@ function createStorefrontApiHandler(config) {
|
|
|
2857
4184
|
contactId = contact.id;
|
|
2858
4185
|
cart = await cartRepo().findOne({
|
|
2859
4186
|
where: { contactId },
|
|
2860
|
-
relations: [
|
|
4187
|
+
relations: [...CART_CHECKOUT_RELATIONS]
|
|
2861
4188
|
});
|
|
2862
4189
|
} else {
|
|
2863
|
-
const email = (b.email
|
|
2864
|
-
const name = (b.name
|
|
4190
|
+
const email = String(b.email ?? "").trim();
|
|
4191
|
+
const name = String(b.name ?? "").trim();
|
|
2865
4192
|
if (!email || !name) return json({ error: "name and email required for guest checkout" }, { status: 400 });
|
|
2866
4193
|
let contact = await contactRepo().findOne({ where: { email, deleted: false } });
|
|
2867
4194
|
if (contact && contact.userId != null) {
|
|
@@ -2872,55 +4199,55 @@ function createStorefrontApiHandler(config) {
|
|
|
2872
4199
|
contactRepo().create({
|
|
2873
4200
|
name,
|
|
2874
4201
|
email,
|
|
2875
|
-
phone: b.phone
|
|
4202
|
+
phone: b.phone != null && b.phone !== "" ? String(b.phone) : null,
|
|
2876
4203
|
userId: null,
|
|
2877
4204
|
deleted: false
|
|
2878
4205
|
})
|
|
2879
4206
|
);
|
|
2880
|
-
} else if (name)
|
|
4207
|
+
} else if (name)
|
|
4208
|
+
await contactRepo().update(contact.id, {
|
|
4209
|
+
name,
|
|
4210
|
+
phone: b.phone != null && b.phone !== "" ? String(b.phone) : contact.phone
|
|
4211
|
+
});
|
|
2881
4212
|
contactId = contact.id;
|
|
4213
|
+
const guestForErp = await contactRepo().findOne({ where: { id: contactId } });
|
|
4214
|
+
if (guestForErp) await syncContactToErp(guestForErp);
|
|
2882
4215
|
const cookies = parseCookies(req.headers.get("cookie"));
|
|
2883
4216
|
const guestToken = cookies[cookieName];
|
|
2884
4217
|
if (!guestToken) return json({ error: "Cart not found" }, { status: 400 });
|
|
2885
4218
|
cart = await cartRepo().findOne({
|
|
2886
4219
|
where: { guestToken },
|
|
2887
|
-
relations: [
|
|
4220
|
+
relations: [...CART_CHECKOUT_RELATIONS]
|
|
2888
4221
|
});
|
|
2889
4222
|
}
|
|
2890
4223
|
if (!cart || !(cart.items || []).length) {
|
|
2891
4224
|
return json({ error: "Cart is empty" }, { status: 400 });
|
|
2892
4225
|
}
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
for (const it of cart.items || []) {
|
|
2896
|
-
const p = it.product;
|
|
2897
|
-
if (!p || p.deleted || p.status !== "available") continue;
|
|
2898
|
-
const unit = Number(p.price);
|
|
2899
|
-
const qty = it.quantity || 1;
|
|
2900
|
-
const lineTotal = unit * qty;
|
|
2901
|
-
subtotal += lineTotal;
|
|
2902
|
-
lines.push({ productId: p.id, quantity: qty, unitPrice: unit, tax: 0, total: lineTotal });
|
|
2903
|
-
}
|
|
2904
|
-
if (!lines.length) return json({ error: "No available items in cart" }, { status: 400 });
|
|
2905
|
-
const total = subtotal;
|
|
4226
|
+
const prepOrd = await prepareCheckoutFromCart(b, cart, contactId);
|
|
4227
|
+
if (!prepOrd.ok) return json({ error: prepOrd.message }, { status: prepOrd.status });
|
|
2906
4228
|
const cartId = cart.id;
|
|
2907
4229
|
const ord = await orderRepo().save(
|
|
2908
4230
|
orderRepo().create({
|
|
2909
|
-
orderNumber:
|
|
4231
|
+
orderNumber: temporaryOrderNumberPlaceholder(),
|
|
4232
|
+
orderKind: "sale",
|
|
4233
|
+
parentOrderId: null,
|
|
2910
4234
|
contactId,
|
|
2911
|
-
billingAddressId:
|
|
2912
|
-
shippingAddressId:
|
|
4235
|
+
billingAddressId: prepOrd.billingAddressId,
|
|
4236
|
+
shippingAddressId: prepOrd.shippingAddressId,
|
|
2913
4237
|
status: "pending",
|
|
2914
|
-
subtotal,
|
|
2915
|
-
tax:
|
|
4238
|
+
subtotal: prepOrd.subtotal,
|
|
4239
|
+
tax: prepOrd.orderTax,
|
|
2916
4240
|
discount: 0,
|
|
2917
|
-
total,
|
|
4241
|
+
total: prepOrd.orderTotal,
|
|
2918
4242
|
currency: cart.currency || "INR",
|
|
2919
4243
|
metadata: { cartId }
|
|
2920
4244
|
})
|
|
2921
4245
|
);
|
|
2922
4246
|
const oid = ord.id;
|
|
2923
|
-
|
|
4247
|
+
await orderRepo().update(oid, {
|
|
4248
|
+
orderNumber: buildCanonicalOrderNumber("sale", oid, ord.createdAt ?? /* @__PURE__ */ new Date())
|
|
4249
|
+
});
|
|
4250
|
+
for (const line of prepOrd.lines) {
|
|
2924
4251
|
await orderItemRepo().save(
|
|
2925
4252
|
orderItemRepo().create({
|
|
2926
4253
|
orderId: oid,
|
|
@@ -2928,19 +4255,28 @@ function createStorefrontApiHandler(config) {
|
|
|
2928
4255
|
quantity: line.quantity,
|
|
2929
4256
|
unitPrice: line.unitPrice,
|
|
2930
4257
|
tax: line.tax,
|
|
2931
|
-
total: line.total
|
|
4258
|
+
total: line.total,
|
|
4259
|
+
hsn: line.hsn,
|
|
4260
|
+
uom: line.uom,
|
|
4261
|
+
productType: line.productType,
|
|
4262
|
+
taxRate: line.taxRate,
|
|
4263
|
+
taxCode: line.taxCode
|
|
2932
4264
|
})
|
|
2933
4265
|
);
|
|
2934
4266
|
}
|
|
2935
4267
|
return json({
|
|
2936
4268
|
orderId: oid,
|
|
2937
4269
|
orderNumber: ord.orderNumber,
|
|
2938
|
-
|
|
4270
|
+
subtotal: prepOrd.subtotal,
|
|
4271
|
+
tax: prepOrd.orderTax,
|
|
4272
|
+
total: prepOrd.orderTotal,
|
|
2939
4273
|
currency: cart.currency || "INR"
|
|
2940
4274
|
});
|
|
2941
4275
|
}
|
|
2942
4276
|
if (path[0] === "checkout" && path.length === 1 && method === "POST") {
|
|
2943
4277
|
const b = await req.json().catch(() => ({}));
|
|
4278
|
+
const capChk = await assertCaptchaOk(getCms, b, req, json);
|
|
4279
|
+
if (capChk) return capChk;
|
|
2944
4280
|
const u = await getSessionUser();
|
|
2945
4281
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
2946
4282
|
let contactId;
|
|
@@ -2951,11 +4287,11 @@ function createStorefrontApiHandler(config) {
|
|
|
2951
4287
|
contactId = contact.id;
|
|
2952
4288
|
cart = await cartRepo().findOne({
|
|
2953
4289
|
where: { contactId },
|
|
2954
|
-
relations: [
|
|
4290
|
+
relations: [...CART_CHECKOUT_RELATIONS]
|
|
2955
4291
|
});
|
|
2956
4292
|
} else {
|
|
2957
|
-
const email = (b.email
|
|
2958
|
-
const name = (b.name
|
|
4293
|
+
const email = String(b.email ?? "").trim();
|
|
4294
|
+
const name = String(b.name ?? "").trim();
|
|
2959
4295
|
if (!email || !name) return json({ error: "name and email required for guest checkout" }, { status: 400 });
|
|
2960
4296
|
let contact = await contactRepo().findOne({ where: { email, deleted: false } });
|
|
2961
4297
|
if (contact && contact.userId != null) {
|
|
@@ -2966,53 +4302,53 @@ function createStorefrontApiHandler(config) {
|
|
|
2966
4302
|
contactRepo().create({
|
|
2967
4303
|
name,
|
|
2968
4304
|
email,
|
|
2969
|
-
phone: b.phone
|
|
4305
|
+
phone: b.phone != null && b.phone !== "" ? String(b.phone) : null,
|
|
2970
4306
|
userId: null,
|
|
2971
4307
|
deleted: false
|
|
2972
4308
|
})
|
|
2973
4309
|
);
|
|
2974
|
-
} else if (name)
|
|
4310
|
+
} else if (name)
|
|
4311
|
+
await contactRepo().update(contact.id, {
|
|
4312
|
+
name,
|
|
4313
|
+
phone: b.phone != null && b.phone !== "" ? String(b.phone) : contact.phone
|
|
4314
|
+
});
|
|
2975
4315
|
contactId = contact.id;
|
|
4316
|
+
const guestForErp2 = await contactRepo().findOne({ where: { id: contactId } });
|
|
4317
|
+
if (guestForErp2) await syncContactToErp(guestForErp2);
|
|
2976
4318
|
const cookies = parseCookies(req.headers.get("cookie"));
|
|
2977
4319
|
const guestToken = cookies[cookieName];
|
|
2978
4320
|
if (!guestToken) return json({ error: "Cart not found" }, { status: 400 });
|
|
2979
4321
|
cart = await cartRepo().findOne({
|
|
2980
4322
|
where: { guestToken },
|
|
2981
|
-
relations: [
|
|
4323
|
+
relations: [...CART_CHECKOUT_RELATIONS]
|
|
2982
4324
|
});
|
|
2983
4325
|
}
|
|
2984
4326
|
if (!cart || !(cart.items || []).length) {
|
|
2985
4327
|
return json({ error: "Cart is empty" }, { status: 400 });
|
|
2986
4328
|
}
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
for (const it of cart.items || []) {
|
|
2990
|
-
const p = it.product;
|
|
2991
|
-
if (!p || p.deleted || p.status !== "available") continue;
|
|
2992
|
-
const unit = Number(p.price);
|
|
2993
|
-
const qty = it.quantity || 1;
|
|
2994
|
-
const lineTotal = unit * qty;
|
|
2995
|
-
subtotal += lineTotal;
|
|
2996
|
-
lines.push({ productId: p.id, quantity: qty, unitPrice: unit, tax: 0, total: lineTotal });
|
|
2997
|
-
}
|
|
2998
|
-
if (!lines.length) return json({ error: "No available items in cart" }, { status: 400 });
|
|
2999
|
-
const total = subtotal;
|
|
4329
|
+
const prepChk = await prepareCheckoutFromCart(b, cart, contactId);
|
|
4330
|
+
if (!prepChk.ok) return json({ error: prepChk.message }, { status: prepChk.status });
|
|
3000
4331
|
const ord = await orderRepo().save(
|
|
3001
4332
|
orderRepo().create({
|
|
3002
|
-
orderNumber:
|
|
4333
|
+
orderNumber: temporaryOrderNumberPlaceholder(),
|
|
4334
|
+
orderKind: "sale",
|
|
4335
|
+
parentOrderId: null,
|
|
3003
4336
|
contactId,
|
|
3004
|
-
billingAddressId:
|
|
3005
|
-
shippingAddressId:
|
|
4337
|
+
billingAddressId: prepChk.billingAddressId,
|
|
4338
|
+
shippingAddressId: prepChk.shippingAddressId,
|
|
3006
4339
|
status: "pending",
|
|
3007
|
-
subtotal,
|
|
3008
|
-
tax:
|
|
4340
|
+
subtotal: prepChk.subtotal,
|
|
4341
|
+
tax: prepChk.orderTax,
|
|
3009
4342
|
discount: 0,
|
|
3010
|
-
total,
|
|
4343
|
+
total: prepChk.orderTotal,
|
|
3011
4344
|
currency: cart.currency || "INR"
|
|
3012
4345
|
})
|
|
3013
4346
|
);
|
|
3014
4347
|
const oid = ord.id;
|
|
3015
|
-
|
|
4348
|
+
await orderRepo().update(oid, {
|
|
4349
|
+
orderNumber: buildCanonicalOrderNumber("sale", oid, ord.createdAt ?? /* @__PURE__ */ new Date())
|
|
4350
|
+
});
|
|
4351
|
+
for (const line of prepChk.lines) {
|
|
3016
4352
|
await orderItemRepo().save(
|
|
3017
4353
|
orderItemRepo().create({
|
|
3018
4354
|
orderId: oid,
|
|
@@ -3020,7 +4356,12 @@ function createStorefrontApiHandler(config) {
|
|
|
3020
4356
|
quantity: line.quantity,
|
|
3021
4357
|
unitPrice: line.unitPrice,
|
|
3022
4358
|
tax: line.tax,
|
|
3023
|
-
total: line.total
|
|
4359
|
+
total: line.total,
|
|
4360
|
+
hsn: line.hsn,
|
|
4361
|
+
uom: line.uom,
|
|
4362
|
+
productType: line.productType,
|
|
4363
|
+
taxRate: line.taxRate,
|
|
4364
|
+
taxCode: line.taxCode
|
|
3024
4365
|
})
|
|
3025
4366
|
);
|
|
3026
4367
|
}
|
|
@@ -3029,7 +4370,9 @@ function createStorefrontApiHandler(config) {
|
|
|
3029
4370
|
return json({
|
|
3030
4371
|
orderId: oid,
|
|
3031
4372
|
orderNumber: ord.orderNumber,
|
|
3032
|
-
|
|
4373
|
+
subtotal: prepChk.subtotal,
|
|
4374
|
+
tax: prepChk.orderTax,
|
|
4375
|
+
total: prepChk.orderTotal
|
|
3033
4376
|
});
|
|
3034
4377
|
}
|
|
3035
4378
|
if (path[0] === "orders" && path.length === 1 && method === "GET") {
|
|
@@ -3039,7 +4382,7 @@ function createStorefrontApiHandler(config) {
|
|
|
3039
4382
|
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
3040
4383
|
if (!contact) return json({ orders: [] });
|
|
3041
4384
|
const orders = await orderRepo().find({
|
|
3042
|
-
where: { contactId: contact.id, deleted: false },
|
|
4385
|
+
where: { contactId: contact.id, deleted: false, orderKind: "sale" },
|
|
3043
4386
|
order: { createdAt: "DESC" },
|
|
3044
4387
|
take: 50
|
|
3045
4388
|
});
|
|
@@ -3047,7 +4390,7 @@ function createStorefrontApiHandler(config) {
|
|
|
3047
4390
|
const previewByOrder = {};
|
|
3048
4391
|
if (orderIds.length) {
|
|
3049
4392
|
const oItems = await orderItemRepo().find({
|
|
3050
|
-
where: { orderId: (0,
|
|
4393
|
+
where: { orderId: (0, import_typeorm5.In)(orderIds) },
|
|
3051
4394
|
relations: ["product"],
|
|
3052
4395
|
order: { id: "ASC" }
|
|
3053
4396
|
});
|
|
@@ -3074,6 +4417,20 @@ function createStorefrontApiHandler(config) {
|
|
|
3074
4417
|
})
|
|
3075
4418
|
});
|
|
3076
4419
|
}
|
|
4420
|
+
if (path[0] === "orders" && path.length === 3 && path[2] === "invoice" && method === "GET") {
|
|
4421
|
+
const u = await getSessionUser();
|
|
4422
|
+
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
4423
|
+
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
4424
|
+
if (!getCms) return json({ error: "Not found" }, { status: 404 });
|
|
4425
|
+
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
4426
|
+
if (!contact) return json({ error: "Not found" }, { status: 404 });
|
|
4427
|
+
const orderId = parseInt(path[1], 10);
|
|
4428
|
+
if (!Number.isFinite(orderId)) return json({ error: "Invalid id" }, { status: 400 });
|
|
4429
|
+
const cms = await getCms();
|
|
4430
|
+
return streamOrderInvoicePdf(cms, dataSource, entityMap, orderId, {
|
|
4431
|
+
ownerContactId: contact.id
|
|
4432
|
+
});
|
|
4433
|
+
}
|
|
3077
4434
|
if (path[0] === "orders" && path.length === 2 && method === "GET") {
|
|
3078
4435
|
const u = await getSessionUser();
|
|
3079
4436
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
@@ -3081,11 +4438,20 @@ function createStorefrontApiHandler(config) {
|
|
|
3081
4438
|
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
3082
4439
|
if (!contact) return json({ error: "Not found" }, { status: 404 });
|
|
3083
4440
|
const orderId = parseInt(path[1], 10);
|
|
3084
|
-
|
|
4441
|
+
let order = await orderRepo().findOne({
|
|
3085
4442
|
where: { id: orderId, contactId: contact.id, deleted: false },
|
|
3086
4443
|
relations: ["items", "items.product"]
|
|
3087
4444
|
});
|
|
3088
4445
|
if (!order) return json({ error: "Not found" }, { status: 404 });
|
|
4446
|
+
if (getCms) {
|
|
4447
|
+
const cms = await getCms();
|
|
4448
|
+
await tryRefreshOrderFromErpForStorefront(cms, dataSource, entityMap, order);
|
|
4449
|
+
order = await orderRepo().findOne({
|
|
4450
|
+
where: { id: orderId, contactId: contact.id, deleted: false },
|
|
4451
|
+
relations: ["items", "items.product"]
|
|
4452
|
+
});
|
|
4453
|
+
}
|
|
4454
|
+
if (!order) return json({ error: "Not found" }, { status: 404 });
|
|
3089
4455
|
const o = order;
|
|
3090
4456
|
const lines = (o.items || []).map((line) => {
|
|
3091
4457
|
const p = line.product;
|
|
@@ -3104,10 +4470,22 @@ function createStorefrontApiHandler(config) {
|
|
|
3104
4470
|
} : null
|
|
3105
4471
|
};
|
|
3106
4472
|
});
|
|
4473
|
+
const kind = o.orderKind || "sale";
|
|
4474
|
+
let relatedOrders = [];
|
|
4475
|
+
if (kind === "sale") {
|
|
4476
|
+
relatedOrders = await orderRepo().find({
|
|
4477
|
+
where: { parentOrderId: orderId, deleted: false },
|
|
4478
|
+
order: { id: "ASC" }
|
|
4479
|
+
});
|
|
4480
|
+
}
|
|
4481
|
+
const meta = o.metadata;
|
|
4482
|
+
const fulfillmentPreview = meta && typeof meta.fulfillment === "object" && meta.fulfillment && "status" in meta.fulfillment ? String(meta.fulfillment.status ?? "") : "";
|
|
3107
4483
|
return json({
|
|
3108
4484
|
order: {
|
|
3109
4485
|
id: o.id,
|
|
3110
4486
|
orderNumber: o.orderNumber,
|
|
4487
|
+
orderKind: kind,
|
|
4488
|
+
parentOrderId: o.parentOrderId ?? null,
|
|
3111
4489
|
status: o.status,
|
|
3112
4490
|
subtotal: o.subtotal,
|
|
3113
4491
|
tax: o.tax,
|
|
@@ -3115,8 +4493,18 @@ function createStorefrontApiHandler(config) {
|
|
|
3115
4493
|
total: o.total,
|
|
3116
4494
|
currency: o.currency,
|
|
3117
4495
|
createdAt: o.createdAt,
|
|
4496
|
+
metadata: o.metadata ?? null,
|
|
3118
4497
|
items: lines
|
|
3119
|
-
}
|
|
4498
|
+
},
|
|
4499
|
+
relatedOrders: relatedOrders.map((r) => ({
|
|
4500
|
+
id: r.id,
|
|
4501
|
+
orderNumber: r.orderNumber,
|
|
4502
|
+
orderKind: r.orderKind ?? "return",
|
|
4503
|
+
status: r.status,
|
|
4504
|
+
createdAt: r.createdAt,
|
|
4505
|
+
fulfillmentStatus: r.metadata && typeof r.metadata === "object" && r.metadata.fulfillment?.status
|
|
4506
|
+
})),
|
|
4507
|
+
fulfillmentPreview: fulfillmentPreview || void 0
|
|
3120
4508
|
});
|
|
3121
4509
|
}
|
|
3122
4510
|
return json({ error: "Not found" }, { status: 404 });
|