@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.js
CHANGED
|
@@ -8,6 +8,41 @@ var __export = (target, all) => {
|
|
|
8
8
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
// src/plugins/erp/erp-queue.ts
|
|
12
|
+
async function queueErp(cms, payload) {
|
|
13
|
+
const queue = cms.getPlugin("queue");
|
|
14
|
+
if (!queue) return;
|
|
15
|
+
await queue.add(ERP_QUEUE_NAME, payload);
|
|
16
|
+
}
|
|
17
|
+
var ERP_QUEUE_NAME;
|
|
18
|
+
var init_erp_queue = __esm({
|
|
19
|
+
"src/plugins/erp/erp-queue.ts"() {
|
|
20
|
+
"use strict";
|
|
21
|
+
ERP_QUEUE_NAME = "erp";
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// src/plugins/erp/erp-config-enabled.ts
|
|
26
|
+
var erp_config_enabled_exports = {};
|
|
27
|
+
__export(erp_config_enabled_exports, {
|
|
28
|
+
isErpIntegrationEnabled: () => isErpIntegrationEnabled
|
|
29
|
+
});
|
|
30
|
+
async function isErpIntegrationEnabled(cms, dataSource, entityMap) {
|
|
31
|
+
if (!cms.getPlugin("erp")) return false;
|
|
32
|
+
const configRepo = dataSource.getRepository(entityMap.configs);
|
|
33
|
+
const cfgRows = await configRepo.find({ where: { settings: "erp", deleted: false } });
|
|
34
|
+
for (const row of cfgRows) {
|
|
35
|
+
const r = row;
|
|
36
|
+
if (r.key === "enabled" && r.value === "false") return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
var init_erp_config_enabled = __esm({
|
|
41
|
+
"src/plugins/erp/erp-config-enabled.ts"() {
|
|
42
|
+
"use strict";
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
11
46
|
// src/plugins/email/email-queue.ts
|
|
12
47
|
var email_queue_exports = {};
|
|
13
48
|
__export(email_queue_exports, {
|
|
@@ -46,14 +81,31 @@ async function queueEmail(cms, payload) {
|
|
|
46
81
|
}
|
|
47
82
|
}
|
|
48
83
|
async function queueOrderPlacedEmails(cms, payload) {
|
|
49
|
-
const {
|
|
84
|
+
const {
|
|
85
|
+
orderNumber,
|
|
86
|
+
total,
|
|
87
|
+
subtotal,
|
|
88
|
+
tax,
|
|
89
|
+
currency,
|
|
90
|
+
customerName,
|
|
91
|
+
customerEmail,
|
|
92
|
+
salesTeamEmails,
|
|
93
|
+
companyDetails,
|
|
94
|
+
lineItems,
|
|
95
|
+
billingAddress,
|
|
96
|
+
shippingAddress
|
|
97
|
+
} = payload;
|
|
50
98
|
const base = {
|
|
51
|
-
orderNumber
|
|
99
|
+
orderNumber,
|
|
52
100
|
total: total != null ? String(total) : void 0,
|
|
101
|
+
subtotal: subtotal != null ? String(subtotal) : void 0,
|
|
102
|
+
tax: tax != null ? String(tax) : void 0,
|
|
53
103
|
currency,
|
|
54
104
|
customerName,
|
|
55
105
|
companyDetails: companyDetails ?? {},
|
|
56
|
-
lineItems: lineItems ?? []
|
|
106
|
+
lineItems: lineItems ?? [],
|
|
107
|
+
billingAddress,
|
|
108
|
+
shippingAddress
|
|
57
109
|
};
|
|
58
110
|
const customerLower = customerEmail?.trim().toLowerCase() ?? "";
|
|
59
111
|
const jobs = [];
|
|
@@ -96,8 +148,326 @@ var init_email_queue = __esm({
|
|
|
96
148
|
}
|
|
97
149
|
});
|
|
98
150
|
|
|
151
|
+
// src/plugins/erp/erp-response-map.ts
|
|
152
|
+
function pickString(o, keys) {
|
|
153
|
+
for (const k of keys) {
|
|
154
|
+
const v = o[k];
|
|
155
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
156
|
+
}
|
|
157
|
+
return void 0;
|
|
158
|
+
}
|
|
159
|
+
function unwrapErpReadData(json) {
|
|
160
|
+
if (!json || typeof json !== "object") return null;
|
|
161
|
+
const o = json;
|
|
162
|
+
const d = o.data;
|
|
163
|
+
if (d && typeof d === "object" && !Array.isArray(d)) return d;
|
|
164
|
+
return o;
|
|
165
|
+
}
|
|
166
|
+
function firstObject(data, keys) {
|
|
167
|
+
for (const k of keys) {
|
|
168
|
+
const v = data[k];
|
|
169
|
+
if (v && typeof v === "object" && !Array.isArray(v)) return v;
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
function extractEvents(src) {
|
|
174
|
+
const timeline = src.timeline ?? src.events ?? src.history ?? src.trackingEvents;
|
|
175
|
+
if (!Array.isArray(timeline) || !timeline.length) return void 0;
|
|
176
|
+
const events = [];
|
|
177
|
+
for (const row of timeline) {
|
|
178
|
+
if (!row || typeof row !== "object") continue;
|
|
179
|
+
const r = row;
|
|
180
|
+
const at = pickString(r, ["at", "timestamp", "date", "occurredAt"]) ?? (r.at instanceof Date ? r.at.toISOString() : void 0);
|
|
181
|
+
const label = pickString(r, ["label", "status", "title", "message", "description"]);
|
|
182
|
+
const detail = pickString(r, ["detail", "notes", "description"]);
|
|
183
|
+
if (at || label || detail) events.push({ at, label, detail });
|
|
184
|
+
}
|
|
185
|
+
return events.length ? events : void 0;
|
|
186
|
+
}
|
|
187
|
+
function mapErpPayloadToFulfillment(data) {
|
|
188
|
+
const nested = firstObject(data, ["fulfillment", "packaging", "shipment", "shipping", "delivery"]);
|
|
189
|
+
const src = nested || data;
|
|
190
|
+
const status = pickString(src, ["status", "fulfillmentStatus", "state", "label", "packagingStatus"]);
|
|
191
|
+
const trackingId = pickString(src, ["trackingId", "tracking_id", "trackingNumber", "awb", "trackingUrl"]);
|
|
192
|
+
const events = extractEvents(src);
|
|
193
|
+
if (!status && !trackingId && !(events && events.length)) return void 0;
|
|
194
|
+
return { status, trackingId, events };
|
|
195
|
+
}
|
|
196
|
+
function mapErpPayloadToInvoiceNumber(data) {
|
|
197
|
+
const nested = firstObject(data, ["invoice", "latestInvoice", "postedInvoice"]);
|
|
198
|
+
const src = nested || data;
|
|
199
|
+
return pickString(src, ["invoiceNumber", "invoice_number", "number", "name", "id"]);
|
|
200
|
+
}
|
|
201
|
+
function extractChildOrderRefsFromSalePayload(data) {
|
|
202
|
+
const lists = [data.returns, data.returnOrders, data.relatedReturns, data.childOrders, data.children];
|
|
203
|
+
const seen = /* @__PURE__ */ new Set();
|
|
204
|
+
const out = [];
|
|
205
|
+
for (const list of lists) {
|
|
206
|
+
if (!Array.isArray(list)) continue;
|
|
207
|
+
for (const item of list) {
|
|
208
|
+
if (!item || typeof item !== "object") continue;
|
|
209
|
+
const o = item;
|
|
210
|
+
const ref = pickString(o, ["platformReturnId", "platform_return_id", "refId", "ref_id"]) ?? (typeof o.id === "string" ? o.id : void 0);
|
|
211
|
+
if (!ref || seen.has(ref)) continue;
|
|
212
|
+
seen.add(ref);
|
|
213
|
+
const t = (pickString(o, ["kind", "type", "orderKind"]) || "").toLowerCase();
|
|
214
|
+
const orderKind = /replac/.test(t) ? "replacement" : "return";
|
|
215
|
+
out.push({ ref, orderKind });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
var init_erp_response_map = __esm({
|
|
221
|
+
"src/plugins/erp/erp-response-map.ts"() {
|
|
222
|
+
"use strict";
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// src/plugins/erp/erp-order-invoice.ts
|
|
227
|
+
var erp_order_invoice_exports = {};
|
|
228
|
+
__export(erp_order_invoice_exports, {
|
|
229
|
+
streamOrderInvoicePdf: () => streamOrderInvoicePdf
|
|
230
|
+
});
|
|
231
|
+
function pickInvoiceId(data) {
|
|
232
|
+
const nested = data.invoice && typeof data.invoice === "object" && !Array.isArray(data.invoice) ? data.invoice : null;
|
|
233
|
+
const src = nested || data;
|
|
234
|
+
for (const k of ["invoiceId", "invoice_id", "id"]) {
|
|
235
|
+
const v = src[k];
|
|
236
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
237
|
+
}
|
|
238
|
+
return void 0;
|
|
239
|
+
}
|
|
240
|
+
async function streamOrderInvoicePdf(cms, dataSource, entityMap, orderId, options) {
|
|
241
|
+
const jsonErr = (msg, status) => new Response(JSON.stringify({ error: msg }), {
|
|
242
|
+
status,
|
|
243
|
+
headers: { "Content-Type": "application/json" }
|
|
244
|
+
});
|
|
245
|
+
const on = await isErpIntegrationEnabled(cms, dataSource, entityMap);
|
|
246
|
+
if (!on) return jsonErr("Invoice not available", 503);
|
|
247
|
+
const erp = cms.getPlugin("erp");
|
|
248
|
+
if (!erp?.submission) return jsonErr("Invoice not available", 503);
|
|
249
|
+
const orderRepo = dataSource.getRepository(entityMap.orders);
|
|
250
|
+
const order = await orderRepo.findOne({ where: { id: orderId, deleted: false } });
|
|
251
|
+
if (!order) return jsonErr("Not found", 404);
|
|
252
|
+
const kind = order.orderKind || "sale";
|
|
253
|
+
if (kind !== "sale") return jsonErr("Invoice only for sale orders", 400);
|
|
254
|
+
if (options.ownerContactId != null && order.contactId !== options.ownerContactId) {
|
|
255
|
+
return jsonErr("Not found", 404);
|
|
256
|
+
}
|
|
257
|
+
const meta = order.metadata && typeof order.metadata === "object" && !Array.isArray(order.metadata) ? order.metadata : {};
|
|
258
|
+
const inv = meta.invoice && typeof meta.invoice === "object" && !Array.isArray(meta.invoice) ? meta.invoice : {};
|
|
259
|
+
let invoiceId = typeof inv.invoiceId === "string" ? inv.invoiceId.trim() : "";
|
|
260
|
+
if (!invoiceId) {
|
|
261
|
+
const refId = String(order.orderNumber || "");
|
|
262
|
+
const r = await erp.submission.postErpReadAction("get-invoice", { platformOrderId: refId });
|
|
263
|
+
const d = r.ok ? unwrapErpReadData(r.json) : null;
|
|
264
|
+
invoiceId = d ? pickInvoiceId(d) || "" : "";
|
|
265
|
+
}
|
|
266
|
+
if (!invoiceId) return jsonErr("Invoice not ready", 404);
|
|
267
|
+
const pdf = await erp.submission.fetchInvoicePdf(invoiceId);
|
|
268
|
+
if (!pdf.ok || !pdf.buffer) return jsonErr(pdf.error || "PDF fetch failed", 502);
|
|
269
|
+
const filename = `invoice-${orderId}.pdf`;
|
|
270
|
+
return new Response(pdf.buffer, {
|
|
271
|
+
status: 200,
|
|
272
|
+
headers: {
|
|
273
|
+
"Content-Type": pdf.contentType || "application/pdf",
|
|
274
|
+
"Content-Disposition": `attachment; filename="${filename}"`
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
var init_erp_order_invoice = __esm({
|
|
279
|
+
"src/plugins/erp/erp-order-invoice.ts"() {
|
|
280
|
+
"use strict";
|
|
281
|
+
init_erp_response_map();
|
|
282
|
+
init_erp_config_enabled();
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// src/plugins/erp/paid-order-erp.ts
|
|
287
|
+
var paid_order_erp_exports = {};
|
|
288
|
+
__export(paid_order_erp_exports, {
|
|
289
|
+
queueErpPaidOrderForOrderId: () => queueErpPaidOrderForOrderId
|
|
290
|
+
});
|
|
291
|
+
function roundMoney(major) {
|
|
292
|
+
return Math.round(major * 100) / 100;
|
|
293
|
+
}
|
|
294
|
+
function addressToWebhookDto(a) {
|
|
295
|
+
if (!a) return {};
|
|
296
|
+
return {
|
|
297
|
+
line1: a.line1 ?? "",
|
|
298
|
+
line2: a.line2 ?? "",
|
|
299
|
+
city: a.city ?? "",
|
|
300
|
+
state: a.state ?? "",
|
|
301
|
+
postalCode: a.postalCode ?? "",
|
|
302
|
+
country: a.country ?? ""
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function orderStatusLabel(status) {
|
|
306
|
+
const s = (status || "").toLowerCase();
|
|
307
|
+
if (s === "confirmed") return "Confirmed";
|
|
308
|
+
if (s === "pending") return "Pending";
|
|
309
|
+
if (!status) return "Pending";
|
|
310
|
+
return status.charAt(0).toUpperCase() + status.slice(1);
|
|
311
|
+
}
|
|
312
|
+
function paymentRowToWebhookDto(p, amountMajorOverride) {
|
|
313
|
+
const currency = String(p.currency || "INR");
|
|
314
|
+
const amountMajor = amountMajorOverride != null && Number.isFinite(amountMajorOverride) ? amountMajorOverride : Number(p.amount);
|
|
315
|
+
const meta = { ...p.metadata || {} };
|
|
316
|
+
delete meta.amount;
|
|
317
|
+
delete meta.currency;
|
|
318
|
+
return {
|
|
319
|
+
id: String(p.externalReference || `payment_${p.id}`),
|
|
320
|
+
amount: roundMoney(amountMajor),
|
|
321
|
+
currency_code: currency,
|
|
322
|
+
captured_at: p.paidAt ? new Date(p.paidAt).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
323
|
+
provider_id: String(p.method || "unknown"),
|
|
324
|
+
data: { status: "captured", ...meta }
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId) {
|
|
328
|
+
try {
|
|
329
|
+
const configRepo = dataSource.getRepository(entityMap.configs);
|
|
330
|
+
const cfgRows = await configRepo.find({ where: { settings: "erp", deleted: false } });
|
|
331
|
+
for (const row of cfgRows) {
|
|
332
|
+
const r = row;
|
|
333
|
+
if (r.key === "enabled" && r.value === "false") return;
|
|
334
|
+
}
|
|
335
|
+
if (!cms.getPlugin("erp")) return;
|
|
336
|
+
const orderRepo = dataSource.getRepository(entityMap.orders);
|
|
337
|
+
const ord = await orderRepo.findOne({
|
|
338
|
+
where: { id: orderId },
|
|
339
|
+
relations: ["items", "items.product", "contact", "billingAddress", "shippingAddress", "payments"]
|
|
340
|
+
});
|
|
341
|
+
if (!ord) return;
|
|
342
|
+
const o = ord;
|
|
343
|
+
const okKind = o.orderKind === void 0 || o.orderKind === null || o.orderKind === "sale";
|
|
344
|
+
if (!okKind) return;
|
|
345
|
+
const rawPayments = o.payments ?? [];
|
|
346
|
+
const completedPayments = rawPayments.filter((pay) => pay.status === "completed" && pay.deleted !== true);
|
|
347
|
+
if (!completedPayments.length) return;
|
|
348
|
+
const rawItems = o.items ?? [];
|
|
349
|
+
const lines = rawItems.filter((it) => it.product).map((it) => {
|
|
350
|
+
const p = it.product;
|
|
351
|
+
const sku = p.sku || `SKU-${p.id}`;
|
|
352
|
+
const itemType = typeof it.productType === "string" && it.productType.trim() ? String(it.productType).trim() : p.type === "service" ? "service" : "product";
|
|
353
|
+
return {
|
|
354
|
+
sku,
|
|
355
|
+
quantity: Number(it.quantity) || 1,
|
|
356
|
+
unitPrice: Number(it.unitPrice),
|
|
357
|
+
title: p.name || sku,
|
|
358
|
+
discount: 0,
|
|
359
|
+
tax: Number(it.tax) || 0,
|
|
360
|
+
uom: (typeof it.uom === "string" && it.uom.trim() ? it.uom : p.uom) || void 0,
|
|
361
|
+
tax_code: typeof it.taxCode === "string" && it.taxCode.trim() ? String(it.taxCode).trim() : void 0,
|
|
362
|
+
hsn_number: (typeof it.hsn === "string" && it.hsn.trim() ? it.hsn : p.hsn) || void 0,
|
|
363
|
+
type: itemType
|
|
364
|
+
};
|
|
365
|
+
});
|
|
366
|
+
if (!lines.length) return;
|
|
367
|
+
const contact = o.contact;
|
|
368
|
+
const orderTotalMajor = Number(o.total);
|
|
369
|
+
const paymentDtos = completedPayments.length === 1 && Number.isFinite(orderTotalMajor) ? [paymentRowToWebhookDto(completedPayments[0], orderTotalMajor)] : completedPayments.map((pay) => paymentRowToWebhookDto(pay));
|
|
370
|
+
const baseMeta = o.metadata && typeof o.metadata === "object" && !Array.isArray(o.metadata) ? { ...o.metadata } : {};
|
|
371
|
+
const orderDto = {
|
|
372
|
+
platformType: "website",
|
|
373
|
+
platformOrderId: String(o.orderNumber),
|
|
374
|
+
platformOrderNumber: String(o.orderNumber),
|
|
375
|
+
order_date: o.createdAt ? new Date(o.createdAt).toISOString() : void 0,
|
|
376
|
+
status: orderStatusLabel(o.status),
|
|
377
|
+
customer: {
|
|
378
|
+
name: contact?.name || "",
|
|
379
|
+
email: contact?.email || "",
|
|
380
|
+
phone: contact?.phone || ""
|
|
381
|
+
},
|
|
382
|
+
shippingAddress: addressToWebhookDto(o.shippingAddress),
|
|
383
|
+
billingAddress: addressToWebhookDto(o.billingAddress),
|
|
384
|
+
items: lines,
|
|
385
|
+
payments: paymentDtos,
|
|
386
|
+
metadata: { ...baseMeta, source: "storefront" }
|
|
387
|
+
};
|
|
388
|
+
await queueErp(cms, { kind: "order", order: orderDto });
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
var init_paid_order_erp = __esm({
|
|
393
|
+
"src/plugins/erp/paid-order-erp.ts"() {
|
|
394
|
+
"use strict";
|
|
395
|
+
init_erp_queue();
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
99
399
|
// src/api/crud.ts
|
|
100
400
|
import { ILike, Like, MoreThan } from "typeorm";
|
|
401
|
+
|
|
402
|
+
// src/plugins/erp/erp-contact-sync.ts
|
|
403
|
+
init_erp_queue();
|
|
404
|
+
function splitName(full) {
|
|
405
|
+
const t = (full || "").trim();
|
|
406
|
+
if (!t) return { firstName: "Contact", lastName: "" };
|
|
407
|
+
const parts = t.split(/\s+/);
|
|
408
|
+
if (parts.length === 1) return { firstName: parts[0], lastName: "" };
|
|
409
|
+
return { firstName: parts[0], lastName: parts.slice(1).join(" ") };
|
|
410
|
+
}
|
|
411
|
+
async function queueErpCreateContactIfEnabled(cms, dataSource, entityMap, input) {
|
|
412
|
+
try {
|
|
413
|
+
const configRepo = dataSource.getRepository(entityMap.configs);
|
|
414
|
+
const cfgRows = await configRepo.find({ where: { settings: "erp", deleted: false } });
|
|
415
|
+
for (const row of cfgRows) {
|
|
416
|
+
const r = row;
|
|
417
|
+
if (r.key === "enabled" && r.value === "false") return;
|
|
418
|
+
}
|
|
419
|
+
if (!cms.getPlugin("erp")) return;
|
|
420
|
+
const email = (input.email ?? "").trim();
|
|
421
|
+
if (!email) return;
|
|
422
|
+
const { firstName, lastName } = splitName(input.name);
|
|
423
|
+
await queueErp(cms, {
|
|
424
|
+
kind: "createContact",
|
|
425
|
+
contact: {
|
|
426
|
+
email,
|
|
427
|
+
firstName,
|
|
428
|
+
lastName,
|
|
429
|
+
phone: input.phone?.trim() || void 0,
|
|
430
|
+
companyName: input.company?.trim() || void 0,
|
|
431
|
+
type: input.type?.trim() || void 0,
|
|
432
|
+
notes: input.notes?.trim() || void 0,
|
|
433
|
+
tags: input.tags?.length ? [...input.tags] : void 0
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
} catch {
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// src/plugins/erp/erp-product-sync.ts
|
|
441
|
+
init_erp_queue();
|
|
442
|
+
init_erp_config_enabled();
|
|
443
|
+
async function queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, product) {
|
|
444
|
+
try {
|
|
445
|
+
const sku = typeof product.sku === "string" ? product.sku.trim() : "";
|
|
446
|
+
if (!sku) return;
|
|
447
|
+
const on = await isErpIntegrationEnabled(cms, dataSource, entityMap);
|
|
448
|
+
if (!on) return;
|
|
449
|
+
const rawMeta = product.metadata;
|
|
450
|
+
let metadata;
|
|
451
|
+
if (rawMeta && typeof rawMeta === "object" && !Array.isArray(rawMeta)) {
|
|
452
|
+
const { description: _d, ...rest } = rawMeta;
|
|
453
|
+
metadata = Object.keys(rest).length ? rest : void 0;
|
|
454
|
+
}
|
|
455
|
+
const payload = {
|
|
456
|
+
sku,
|
|
457
|
+
title: product.name || sku,
|
|
458
|
+
name: product.name,
|
|
459
|
+
hsn_number: product.hsn,
|
|
460
|
+
uom: product.uom != null && String(product.uom).trim() ? String(product.uom).trim() : void 0,
|
|
461
|
+
type: product.type === "service" ? "service" : "product",
|
|
462
|
+
is_active: product.status === "available",
|
|
463
|
+
metadata
|
|
464
|
+
};
|
|
465
|
+
await queueErp(cms, { kind: "productUpsert", product: payload });
|
|
466
|
+
} catch {
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// src/api/crud.ts
|
|
101
471
|
var DATE_COLUMN_TYPES = /* @__PURE__ */ new Set([
|
|
102
472
|
"date",
|
|
103
473
|
"datetime",
|
|
@@ -150,8 +520,27 @@ function buildSearchWhereClause(repo, search) {
|
|
|
150
520
|
if (ors.length === 0) return {};
|
|
151
521
|
return ors.length === 1 ? ors[0] : ors;
|
|
152
522
|
}
|
|
523
|
+
function makeContactErpSync(dataSource, entityMap, getCms) {
|
|
524
|
+
return async function syncContactRowToErp(row) {
|
|
525
|
+
if (!getCms) return;
|
|
526
|
+
try {
|
|
527
|
+
const cms = await getCms();
|
|
528
|
+
const c = row;
|
|
529
|
+
await queueErpCreateContactIfEnabled(cms, dataSource, entityMap, {
|
|
530
|
+
name: c.name,
|
|
531
|
+
email: c.email,
|
|
532
|
+
phone: c.phone,
|
|
533
|
+
type: c.type,
|
|
534
|
+
company: c.company,
|
|
535
|
+
notes: c.notes
|
|
536
|
+
});
|
|
537
|
+
} catch {
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
}
|
|
153
541
|
function createCrudHandler(dataSource, entityMap, options) {
|
|
154
|
-
const { requireAuth, json, requireEntityPermission: reqPerm } = options;
|
|
542
|
+
const { requireAuth, json, requireEntityPermission: reqPerm, getCms } = options;
|
|
543
|
+
const syncContactRowToErp = makeContactErpSync(dataSource, entityMap, getCms);
|
|
155
544
|
async function authz(req, resource, action) {
|
|
156
545
|
const authError = await requireAuth(req);
|
|
157
546
|
if (authError) return authError;
|
|
@@ -331,6 +720,24 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
331
720
|
} else if (search) {
|
|
332
721
|
where = buildSearchWhereClause(repo, search);
|
|
333
722
|
}
|
|
723
|
+
const intFilterKeys = ["productId", "attributeId", "taxId"];
|
|
724
|
+
const extraWhere = {};
|
|
725
|
+
for (const key of intFilterKeys) {
|
|
726
|
+
const v = searchParams.get(key);
|
|
727
|
+
if (v != null && v !== "" && columnNames.has(key)) {
|
|
728
|
+
const n = Number(v);
|
|
729
|
+
if (Number.isFinite(n)) extraWhere[key] = n;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (Object.keys(extraWhere).length > 0) {
|
|
733
|
+
if (Array.isArray(where)) {
|
|
734
|
+
where = where.map((w) => ({ ...w, ...extraWhere }));
|
|
735
|
+
} else if (where && typeof where === "object" && Object.keys(where).length > 0) {
|
|
736
|
+
where = { ...where, ...extraWhere };
|
|
737
|
+
} else {
|
|
738
|
+
where = extraWhere;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
334
741
|
const [data, total] = await repo.findAndCount({
|
|
335
742
|
skip,
|
|
336
743
|
take: limit,
|
|
@@ -353,6 +760,13 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
353
760
|
const repo = dataSource.getRepository(entity);
|
|
354
761
|
sanitizeBodyForEntity(repo, body);
|
|
355
762
|
const created = await repo.save(repo.create(body));
|
|
763
|
+
if (resource === "contacts") {
|
|
764
|
+
await syncContactRowToErp(created);
|
|
765
|
+
}
|
|
766
|
+
if (resource === "products" && getCms) {
|
|
767
|
+
const cms = await getCms();
|
|
768
|
+
await queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, created);
|
|
769
|
+
}
|
|
356
770
|
return json(created, { status: 201 });
|
|
357
771
|
},
|
|
358
772
|
async GET_METADATA(req, resource) {
|
|
@@ -459,7 +873,8 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
459
873
|
};
|
|
460
874
|
}
|
|
461
875
|
function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
462
|
-
const { requireAuth, json, requireEntityPermission: reqPerm } = options;
|
|
876
|
+
const { requireAuth, json, requireEntityPermission: reqPerm, getCms } = options;
|
|
877
|
+
const syncContactRowToErp = makeContactErpSync(dataSource, entityMap, getCms);
|
|
463
878
|
async function authz(req, resource, action) {
|
|
464
879
|
const authError = await requireAuth(req);
|
|
465
880
|
if (authError) return authError;
|
|
@@ -482,7 +897,11 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
482
897
|
relations: ["contact", "billingAddress", "shippingAddress", "items", "items.product", "items.product.collection", "payments"]
|
|
483
898
|
});
|
|
484
899
|
if (!order) return json({ message: "Not found" }, { status: 404 });
|
|
485
|
-
|
|
900
|
+
const relatedOrders = await repo.find({
|
|
901
|
+
where: { parentOrderId: Number(id), deleted: false },
|
|
902
|
+
order: { id: "ASC" }
|
|
903
|
+
});
|
|
904
|
+
return json({ ...order, relatedOrders });
|
|
486
905
|
}
|
|
487
906
|
if (resource === "contacts") {
|
|
488
907
|
const contact = await repo.findOne({
|
|
@@ -615,6 +1034,13 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
615
1034
|
await repo.update(numericId, updatePayload);
|
|
616
1035
|
}
|
|
617
1036
|
const updated = await repo.findOne({ where: { id: numericId } });
|
|
1037
|
+
if (resource === "contacts" && updated) {
|
|
1038
|
+
await syncContactRowToErp(updated);
|
|
1039
|
+
}
|
|
1040
|
+
if (resource === "products" && updated && getCms) {
|
|
1041
|
+
const cms = await getCms();
|
|
1042
|
+
await queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, updated);
|
|
1043
|
+
}
|
|
618
1044
|
return updated ? json(updated) : json({ message: "Not found" }, { status: 404 });
|
|
619
1045
|
},
|
|
620
1046
|
async DELETE(req, resource, id) {
|
|
@@ -778,7 +1204,26 @@ function createUserAuthApiRouter(config) {
|
|
|
778
1204
|
|
|
779
1205
|
// src/api/cms-handlers.ts
|
|
780
1206
|
init_email_queue();
|
|
1207
|
+
init_erp_queue();
|
|
781
1208
|
import { MoreThanOrEqual, ILike as ILike2 } from "typeorm";
|
|
1209
|
+
|
|
1210
|
+
// src/plugins/captcha/assert.ts
|
|
1211
|
+
async function assertCaptchaOk(getCms, body, req, json) {
|
|
1212
|
+
if (!getCms) return null;
|
|
1213
|
+
let cms;
|
|
1214
|
+
try {
|
|
1215
|
+
cms = await getCms();
|
|
1216
|
+
} catch {
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1219
|
+
const svc = cms.getPlugin("captcha");
|
|
1220
|
+
if (!svc || typeof svc.verify !== "function") return null;
|
|
1221
|
+
const result = await svc.verify(body, req);
|
|
1222
|
+
if (result.ok) return null;
|
|
1223
|
+
return json({ error: result.message }, { status: result.status });
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// src/api/cms-handlers.ts
|
|
782
1227
|
function createDashboardStatsHandler(config) {
|
|
783
1228
|
const { dataSource, entityMap, json, requireAuth, requirePermission, requireEntityPermission } = config;
|
|
784
1229
|
return async function GET(req) {
|
|
@@ -1020,6 +1465,32 @@ function createFormSaveHandlers(config) {
|
|
|
1020
1465
|
}
|
|
1021
1466
|
};
|
|
1022
1467
|
}
|
|
1468
|
+
async function isErpIntegrationEnabled2(dataSource, entityMap) {
|
|
1469
|
+
const repo = dataSource.getRepository(entityMap.configs);
|
|
1470
|
+
const rows = await repo.find({ where: { settings: "erp", deleted: false } });
|
|
1471
|
+
for (const row of rows) {
|
|
1472
|
+
const r = row;
|
|
1473
|
+
if (r.key === "enabled") return r.value !== "false";
|
|
1474
|
+
}
|
|
1475
|
+
return true;
|
|
1476
|
+
}
|
|
1477
|
+
async function getErpOpportunityFormIds(dataSource, entityMap) {
|
|
1478
|
+
const repo = dataSource.getRepository(entityMap.configs);
|
|
1479
|
+
const row = await repo.findOne({
|
|
1480
|
+
where: { settings: "erp", key: "opportunityFormIds", deleted: false }
|
|
1481
|
+
});
|
|
1482
|
+
if (!row) return null;
|
|
1483
|
+
const raw = (row.value ?? "").trim();
|
|
1484
|
+
if (!raw) return [];
|
|
1485
|
+
try {
|
|
1486
|
+
const parsed = JSON.parse(raw);
|
|
1487
|
+
if (!Array.isArray(parsed)) return [];
|
|
1488
|
+
const ids = parsed.map((x) => typeof x === "number" ? x : Number(x)).filter((n) => Number.isInteger(n) && n > 0);
|
|
1489
|
+
return [...new Set(ids)];
|
|
1490
|
+
} catch {
|
|
1491
|
+
return [];
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1023
1494
|
function createFormSubmissionGetByIdHandler(config) {
|
|
1024
1495
|
const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
|
|
1025
1496
|
return async function GET(req, id) {
|
|
@@ -1108,13 +1579,15 @@ function pickContactFromSubmission(fields, data) {
|
|
|
1108
1579
|
return { name: name || email, email, phone: phone || null };
|
|
1109
1580
|
}
|
|
1110
1581
|
function createFormSubmissionHandler(config) {
|
|
1111
|
-
const { dataSource, entityMap, json } = config;
|
|
1582
|
+
const { dataSource, entityMap, json, getCms } = config;
|
|
1112
1583
|
return async function POST(req) {
|
|
1113
1584
|
try {
|
|
1114
1585
|
const body = await req.json();
|
|
1115
1586
|
if (!body || typeof body !== "object") {
|
|
1116
1587
|
return json({ error: "Invalid request payload" }, { status: 400 });
|
|
1117
1588
|
}
|
|
1589
|
+
const captchaErr = await assertCaptchaOk(getCms, body, req, json);
|
|
1590
|
+
if (captchaErr) return captchaErr;
|
|
1118
1591
|
const formId = typeof body.formId === "number" ? body.formId : Number(body.formId);
|
|
1119
1592
|
if (!Number.isInteger(formId) || formId <= 0) {
|
|
1120
1593
|
return json({ error: "formId is required and must be a positive integer" }, { status: 400 });
|
|
@@ -1181,28 +1654,44 @@ function createFormSubmissionHandler(config) {
|
|
|
1181
1654
|
contactEmail = contactData.email;
|
|
1182
1655
|
}
|
|
1183
1656
|
}
|
|
1184
|
-
if (config.getCms
|
|
1657
|
+
if (config.getCms) {
|
|
1185
1658
|
try {
|
|
1186
1659
|
const cms = await config.getCms();
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1660
|
+
if (config.getCompanyDetails && config.getRecipientForChannel) {
|
|
1661
|
+
const to = await config.getRecipientForChannel("crm");
|
|
1662
|
+
if (to) {
|
|
1663
|
+
const companyDetails = await config.getCompanyDetails();
|
|
1664
|
+
const formFieldRows = activeFields.map((f) => ({
|
|
1665
|
+
label: f.label && String(f.label).trim() || `Field ${f.id}`,
|
|
1666
|
+
value: formatSubmissionFieldValue(data[String(f.id)])
|
|
1667
|
+
}));
|
|
1668
|
+
await queueEmail(cms, {
|
|
1669
|
+
to,
|
|
1670
|
+
templateName: "formSubmission",
|
|
1671
|
+
ctx: {
|
|
1672
|
+
formName,
|
|
1673
|
+
contactName,
|
|
1674
|
+
contactEmail,
|
|
1675
|
+
formData: data,
|
|
1676
|
+
formFieldRows,
|
|
1677
|
+
companyDetails: companyDetails ?? {}
|
|
1678
|
+
}
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
if (await isErpIntegrationEnabled2(dataSource, entityMap)) {
|
|
1683
|
+
const erp = cms.getPlugin("erp");
|
|
1684
|
+
if (erp) {
|
|
1685
|
+
const contact = erp.submission.extractContactData(data, activeFields);
|
|
1686
|
+
if (contact?.email?.trim()) {
|
|
1687
|
+
const opportunityFormIds = await getErpOpportunityFormIds(dataSource, entityMap);
|
|
1688
|
+
const asOpportunity = opportunityFormIds != null && opportunityFormIds.length > 0 && opportunityFormIds.includes(formId);
|
|
1689
|
+
await queueErp(
|
|
1690
|
+
cms,
|
|
1691
|
+
asOpportunity ? { kind: "formOpportunity", contact } : { kind: "lead", contact }
|
|
1692
|
+
);
|
|
1204
1693
|
}
|
|
1205
|
-
}
|
|
1694
|
+
}
|
|
1206
1695
|
}
|
|
1207
1696
|
} catch {
|
|
1208
1697
|
}
|
|
@@ -1648,6 +2137,125 @@ ${contextParts.join("\n\n")}` : "You are a helpful assistant for the company. If
|
|
|
1648
2137
|
};
|
|
1649
2138
|
}
|
|
1650
2139
|
|
|
2140
|
+
// src/message-templates/sms-defaults.ts
|
|
2141
|
+
var SMS_MESSAGE_TEMPLATE_DEFAULTS = [
|
|
2142
|
+
{
|
|
2143
|
+
templateKey: "auth.otp_login",
|
|
2144
|
+
name: "Sign-in OTP (SMS)",
|
|
2145
|
+
body: "Your sign-in code is {{code}}. Valid 10 minutes.",
|
|
2146
|
+
providerMeta: { otpVarKey: "var1" }
|
|
2147
|
+
},
|
|
2148
|
+
{
|
|
2149
|
+
templateKey: "auth.otp_verify_phone",
|
|
2150
|
+
name: "Verify phone OTP (SMS)",
|
|
2151
|
+
body: "Your verification code is {{code}}. Valid 10 minutes.",
|
|
2152
|
+
providerMeta: { otpVarKey: "var1" }
|
|
2153
|
+
}
|
|
2154
|
+
];
|
|
2155
|
+
function getSmsTemplateDefault(templateKey) {
|
|
2156
|
+
return SMS_MESSAGE_TEMPLATE_DEFAULTS.find((d) => d.templateKey === templateKey);
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
// src/api/message-template-admin-handlers.ts
|
|
2160
|
+
function createSmsMessageTemplateHandlers(config) {
|
|
2161
|
+
const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
|
|
2162
|
+
const repo = () => dataSource.getRepository(entityMap.message_templates);
|
|
2163
|
+
async function requireSettingsRead(req) {
|
|
2164
|
+
const a = await requireAuth(req);
|
|
2165
|
+
if (a) return a;
|
|
2166
|
+
if (requireEntityPermission) {
|
|
2167
|
+
const pe = await requireEntityPermission(req, "settings", "read");
|
|
2168
|
+
if (pe) return pe;
|
|
2169
|
+
}
|
|
2170
|
+
return null;
|
|
2171
|
+
}
|
|
2172
|
+
async function requireSettingsUpdate(req) {
|
|
2173
|
+
const a = await requireAuth(req);
|
|
2174
|
+
if (a) return a;
|
|
2175
|
+
if (requireEntityPermission) {
|
|
2176
|
+
const pe = await requireEntityPermission(req, "settings", "update");
|
|
2177
|
+
if (pe) return pe;
|
|
2178
|
+
}
|
|
2179
|
+
return null;
|
|
2180
|
+
}
|
|
2181
|
+
return {
|
|
2182
|
+
async GET(req) {
|
|
2183
|
+
const err = await requireSettingsRead(req);
|
|
2184
|
+
if (err) return err;
|
|
2185
|
+
try {
|
|
2186
|
+
const rows = await repo().find({ where: { channel: "sms", deleted: false } });
|
|
2187
|
+
const byKey = new Map(rows.map((r) => [r.templateKey, r]));
|
|
2188
|
+
const items = SMS_MESSAGE_TEMPLATE_DEFAULTS.map((def) => {
|
|
2189
|
+
const row = byKey.get(def.templateKey);
|
|
2190
|
+
return {
|
|
2191
|
+
templateKey: def.templateKey,
|
|
2192
|
+
name: def.name,
|
|
2193
|
+
defaultBody: def.body,
|
|
2194
|
+
body: row?.body?.trim() ? row.body : def.body,
|
|
2195
|
+
externalTemplateRef: row?.externalTemplateRef?.trim() ?? "",
|
|
2196
|
+
otpVarKey: row?.providerMeta && typeof row.providerMeta.otpVarKey === "string" ? String(row.providerMeta.otpVarKey) : def.providerMeta?.otpVarKey ?? "var1",
|
|
2197
|
+
enabled: row ? row.enabled : false,
|
|
2198
|
+
dbId: row?.id ?? null
|
|
2199
|
+
};
|
|
2200
|
+
});
|
|
2201
|
+
return json({ items });
|
|
2202
|
+
} catch {
|
|
2203
|
+
return json({ error: "Failed to load templates" }, { status: 500 });
|
|
2204
|
+
}
|
|
2205
|
+
},
|
|
2206
|
+
async PUT(req) {
|
|
2207
|
+
const err = await requireSettingsUpdate(req);
|
|
2208
|
+
if (err) return err;
|
|
2209
|
+
try {
|
|
2210
|
+
const raw = await req.json().catch(() => null);
|
|
2211
|
+
if (!raw?.items || !Array.isArray(raw.items)) {
|
|
2212
|
+
return json({ error: "Invalid payload" }, { status: 400 });
|
|
2213
|
+
}
|
|
2214
|
+
for (const item of raw.items) {
|
|
2215
|
+
const templateKey = typeof item.templateKey === "string" ? item.templateKey.trim() : "";
|
|
2216
|
+
if (!getSmsTemplateDefault(templateKey)) continue;
|
|
2217
|
+
const body = typeof item.body === "string" ? item.body : "";
|
|
2218
|
+
const externalTemplateRef = typeof item.externalTemplateRef === "string" ? item.externalTemplateRef.trim() : "";
|
|
2219
|
+
const otpVarKey = typeof item.otpVarKey === "string" && item.otpVarKey.trim() ? item.otpVarKey.trim() : "var1";
|
|
2220
|
+
const enabled = item.enabled !== false;
|
|
2221
|
+
const existing = await repo().findOne({
|
|
2222
|
+
where: { channel: "sms", templateKey, deleted: false }
|
|
2223
|
+
});
|
|
2224
|
+
const def = getSmsTemplateDefault(templateKey);
|
|
2225
|
+
const providerMeta = { otpVarKey };
|
|
2226
|
+
if (existing) {
|
|
2227
|
+
await repo().update(existing.id, {
|
|
2228
|
+
name: def.name,
|
|
2229
|
+
body,
|
|
2230
|
+
externalTemplateRef: externalTemplateRef || null,
|
|
2231
|
+
providerMeta,
|
|
2232
|
+
enabled,
|
|
2233
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2234
|
+
});
|
|
2235
|
+
} else {
|
|
2236
|
+
await repo().save(
|
|
2237
|
+
repo().create({
|
|
2238
|
+
channel: "sms",
|
|
2239
|
+
templateKey,
|
|
2240
|
+
name: def.name,
|
|
2241
|
+
subject: null,
|
|
2242
|
+
body,
|
|
2243
|
+
externalTemplateRef: externalTemplateRef || null,
|
|
2244
|
+
providerMeta,
|
|
2245
|
+
enabled,
|
|
2246
|
+
deleted: false
|
|
2247
|
+
})
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
return json({ ok: true });
|
|
2252
|
+
} catch {
|
|
2253
|
+
return json({ error: "Failed to save templates" }, { status: 500 });
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
|
|
1651
2259
|
// src/auth/permission-entities.ts
|
|
1652
2260
|
var PERMISSION_ENTITY_INTERNAL_EXCLUDE = /* @__PURE__ */ new Set([
|
|
1653
2261
|
"users",
|
|
@@ -1661,7 +2269,8 @@ var PERMISSION_ENTITY_INTERNAL_EXCLUDE = /* @__PURE__ */ new Set([
|
|
|
1661
2269
|
"carts",
|
|
1662
2270
|
"cart_items",
|
|
1663
2271
|
"wishlists",
|
|
1664
|
-
"wishlist_items"
|
|
2272
|
+
"wishlist_items",
|
|
2273
|
+
"message_templates"
|
|
1665
2274
|
]);
|
|
1666
2275
|
var PERMISSION_LOGICAL_ENTITIES = [
|
|
1667
2276
|
"users",
|
|
@@ -1847,7 +2456,8 @@ var DEFAULT_EXCLUDE = /* @__PURE__ */ new Set([
|
|
|
1847
2456
|
"carts",
|
|
1848
2457
|
"cart_items",
|
|
1849
2458
|
"wishlists",
|
|
1850
|
-
"wishlist_items"
|
|
2459
|
+
"wishlist_items",
|
|
2460
|
+
"message_templates"
|
|
1851
2461
|
]);
|
|
1852
2462
|
function createCmsApiHandler(config) {
|
|
1853
2463
|
const {
|
|
@@ -1910,7 +2520,8 @@ function createCmsApiHandler(config) {
|
|
|
1910
2520
|
const crudOpts = {
|
|
1911
2521
|
requireAuth: config.requireAuth,
|
|
1912
2522
|
json: config.json,
|
|
1913
|
-
requireEntityPermission: reqEntityPerm
|
|
2523
|
+
requireEntityPermission: reqEntityPerm,
|
|
2524
|
+
getCms
|
|
1914
2525
|
};
|
|
1915
2526
|
const crud = createCrudHandler(dataSource, entityMap, crudOpts);
|
|
1916
2527
|
const crudById = createCrudByIdHandler(dataSource, entityMap, crudOpts);
|
|
@@ -1940,6 +2551,13 @@ function createCmsApiHandler(config) {
|
|
|
1940
2551
|
const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
|
|
1941
2552
|
const profilePut = userProfile ? createUserProfileHandler(userProfile) : null;
|
|
1942
2553
|
const settingsHandlers = settingsConfig ? createSettingsApiHandlers(settingsConfig) : null;
|
|
2554
|
+
const smsMessageTemplateHandlers = createSmsMessageTemplateHandlers({
|
|
2555
|
+
dataSource,
|
|
2556
|
+
entityMap,
|
|
2557
|
+
json: config.json,
|
|
2558
|
+
requireAuth: config.requireAuth,
|
|
2559
|
+
requireEntityPermission: reqEntityPerm
|
|
2560
|
+
});
|
|
1943
2561
|
const chatHandlers = chatConfig ? createChatHandlers(chatConfig) : null;
|
|
1944
2562
|
function resolveResource(segment) {
|
|
1945
2563
|
const model = pathToModel(segment);
|
|
@@ -2044,11 +2662,51 @@ function createCmsApiHandler(config) {
|
|
|
2044
2662
|
return settingsHandlers.PUT(req, group);
|
|
2045
2663
|
}
|
|
2046
2664
|
}
|
|
2665
|
+
if (path[0] === "message-templates" && path[1] === "sms" && path.length === 2) {
|
|
2666
|
+
if (method === "GET") return smsMessageTemplateHandlers.GET(req);
|
|
2667
|
+
if (method === "PUT") return smsMessageTemplateHandlers.PUT(req);
|
|
2668
|
+
}
|
|
2047
2669
|
if (path[0] === "chat" && chatHandlers) {
|
|
2048
2670
|
if (path.length === 2 && path[1] === "identify" && method === "POST") return chatHandlers.identify(req);
|
|
2049
2671
|
if (path.length === 4 && path[1] === "conversations" && path[3] === "messages" && method === "GET") return chatHandlers.getMessages(req, path[2]);
|
|
2050
2672
|
if (path.length === 2 && path[1] === "messages" && method === "POST") return chatHandlers.postMessage(req);
|
|
2051
2673
|
}
|
|
2674
|
+
if (path[0] === "orders" && path.length === 3 && path[2] === "invoice" && method === "GET" && getCms) {
|
|
2675
|
+
const a = await config.requireAuth(req);
|
|
2676
|
+
if (a) return a;
|
|
2677
|
+
if (perm) {
|
|
2678
|
+
const pe = await perm(req, "orders", "read");
|
|
2679
|
+
if (pe) return pe;
|
|
2680
|
+
}
|
|
2681
|
+
const cms = await getCms();
|
|
2682
|
+
const { streamOrderInvoicePdf: streamOrderInvoicePdf2 } = await Promise.resolve().then(() => (init_erp_order_invoice(), erp_order_invoice_exports));
|
|
2683
|
+
const oid = Number(path[1]);
|
|
2684
|
+
if (!Number.isFinite(oid)) return config.json({ error: "Invalid id" }, { status: 400 });
|
|
2685
|
+
return streamOrderInvoicePdf2(cms, dataSource, entityMap, oid, {});
|
|
2686
|
+
}
|
|
2687
|
+
if (path[0] === "orders" && path.length === 3 && path[2] === "repost-erp" && getCms) {
|
|
2688
|
+
const a = await config.requireAuth(req);
|
|
2689
|
+
if (a) return a;
|
|
2690
|
+
if (perm) {
|
|
2691
|
+
const pe = await perm(req, "orders", method === "GET" ? "read" : "update");
|
|
2692
|
+
if (pe) return pe;
|
|
2693
|
+
}
|
|
2694
|
+
const oid = Number(path[1]);
|
|
2695
|
+
if (!Number.isFinite(oid)) return config.json({ error: "Invalid id" }, { status: 400 });
|
|
2696
|
+
const cms = await getCms();
|
|
2697
|
+
const { isErpIntegrationEnabled: isErpIntegrationEnabled3 } = await Promise.resolve().then(() => (init_erp_config_enabled(), erp_config_enabled_exports));
|
|
2698
|
+
const enabled = await isErpIntegrationEnabled3(cms, dataSource, entityMap);
|
|
2699
|
+
if (method === "GET") {
|
|
2700
|
+
return config.json({ enabled });
|
|
2701
|
+
}
|
|
2702
|
+
if (method === "POST") {
|
|
2703
|
+
if (!enabled) return config.json({ error: "ERP integration is disabled" }, { status: 409 });
|
|
2704
|
+
const { queueErpPaidOrderForOrderId: queueErpPaidOrderForOrderId2 } = await Promise.resolve().then(() => (init_paid_order_erp(), paid_order_erp_exports));
|
|
2705
|
+
await queueErpPaidOrderForOrderId2(cms, dataSource, entityMap, oid);
|
|
2706
|
+
return config.json({ ok: true });
|
|
2707
|
+
}
|
|
2708
|
+
return config.json({ error: "Method not allowed" }, { status: 405 });
|
|
2709
|
+
}
|
|
2052
2710
|
if (path.length === 0) return config.json({ error: "Not found" }, { status: 404 });
|
|
2053
2711
|
const resource = resolveResource(path[0]);
|
|
2054
2712
|
if (!crudResources.includes(resource)) return config.json({ error: "Invalid resource" }, { status: 400 });
|
|
@@ -2081,7 +2739,7 @@ function createCmsApiHandler(config) {
|
|
|
2081
2739
|
}
|
|
2082
2740
|
|
|
2083
2741
|
// src/api/storefront-handlers.ts
|
|
2084
|
-
import { In, IsNull as
|
|
2742
|
+
import { In, IsNull as IsNull3 } from "typeorm";
|
|
2085
2743
|
|
|
2086
2744
|
// src/lib/is-valid-signup-email.ts
|
|
2087
2745
|
var MAX_EMAIL = 254;
|
|
@@ -2103,6 +2761,339 @@ function isValidSignupEmail(email) {
|
|
|
2103
2761
|
|
|
2104
2762
|
// src/api/storefront-handlers.ts
|
|
2105
2763
|
init_email_queue();
|
|
2764
|
+
|
|
2765
|
+
// src/lib/order-number.ts
|
|
2766
|
+
var KIND_PREFIX = {
|
|
2767
|
+
sale: "OSL",
|
|
2768
|
+
return: "ORT",
|
|
2769
|
+
replacement: "ORP"
|
|
2770
|
+
};
|
|
2771
|
+
function orderNumberYymmUtc(at) {
|
|
2772
|
+
const yy = String(at.getUTCFullYear()).slice(-2);
|
|
2773
|
+
const mm = String(at.getUTCMonth() + 1).padStart(2, "0");
|
|
2774
|
+
return yy + mm;
|
|
2775
|
+
}
|
|
2776
|
+
function maskOrderIdSegment(id) {
|
|
2777
|
+
let x = id >>> 0 ^ 2779096485;
|
|
2778
|
+
x = Math.imul(x, 2654435761) >>> 0;
|
|
2779
|
+
return x.toString(36).toUpperCase().padStart(8, "0").slice(-8);
|
|
2780
|
+
}
|
|
2781
|
+
function buildCanonicalOrderNumber(kind, id, at) {
|
|
2782
|
+
return KIND_PREFIX[kind] + orderNumberYymmUtc(at) + maskOrderIdSegment(id);
|
|
2783
|
+
}
|
|
2784
|
+
function temporaryOrderNumberPlaceholder() {
|
|
2785
|
+
return `TMP${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`.toUpperCase();
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
// src/lib/order-storefront-metadata.ts
|
|
2789
|
+
function mergeOrderMetadataPatch(existing, patch) {
|
|
2790
|
+
const base = existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing } : {};
|
|
2791
|
+
if (patch.fulfillment !== void 0) {
|
|
2792
|
+
if (patch.fulfillment === null) delete base.fulfillment;
|
|
2793
|
+
else base.fulfillment = patch.fulfillment;
|
|
2794
|
+
}
|
|
2795
|
+
if (patch.invoice !== void 0) {
|
|
2796
|
+
if (patch.invoice === null) delete base.invoice;
|
|
2797
|
+
else base.invoice = patch.invoice;
|
|
2798
|
+
}
|
|
2799
|
+
return base;
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
// src/plugins/erp/erp-order-status-map.ts
|
|
2803
|
+
function mapErpSaleStatusToOrderStatus(erpLabel) {
|
|
2804
|
+
if (!erpLabel || typeof erpLabel !== "string") return void 0;
|
|
2805
|
+
const k = erpLabel.trim().toLowerCase().replace(/\s+/g, "_");
|
|
2806
|
+
const map = {
|
|
2807
|
+
draft: "pending",
|
|
2808
|
+
pending: "pending",
|
|
2809
|
+
open: "pending",
|
|
2810
|
+
new: "pending",
|
|
2811
|
+
unconfirmed: "pending",
|
|
2812
|
+
confirmed: "confirmed",
|
|
2813
|
+
processing: "processing",
|
|
2814
|
+
packed: "processing",
|
|
2815
|
+
shipped: "processing",
|
|
2816
|
+
in_transit: "processing",
|
|
2817
|
+
out_for_delivery: "processing",
|
|
2818
|
+
delivered: "completed",
|
|
2819
|
+
completed: "completed",
|
|
2820
|
+
closed: "completed",
|
|
2821
|
+
fulfilled: "completed",
|
|
2822
|
+
cancelled: "cancelled",
|
|
2823
|
+
canceled: "cancelled",
|
|
2824
|
+
void: "cancelled"
|
|
2825
|
+
};
|
|
2826
|
+
return map[k];
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
// src/plugins/erp/erp-order-sync.ts
|
|
2830
|
+
init_erp_response_map();
|
|
2831
|
+
init_erp_config_enabled();
|
|
2832
|
+
function pickInvoiceId2(data) {
|
|
2833
|
+
const nested = data.invoice && typeof data.invoice === "object" && !Array.isArray(data.invoice) ? data.invoice : null;
|
|
2834
|
+
const src = nested || data;
|
|
2835
|
+
for (const k of ["invoiceId", "invoice_id", "id"]) {
|
|
2836
|
+
const v = src[k];
|
|
2837
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
2838
|
+
}
|
|
2839
|
+
return void 0;
|
|
2840
|
+
}
|
|
2841
|
+
async function ensureChildOrdersFromRefs(orderRepo, parent, refs, contactId, currency) {
|
|
2842
|
+
for (const { ref, orderKind } of refs) {
|
|
2843
|
+
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();
|
|
2844
|
+
if (existing) continue;
|
|
2845
|
+
const tmp = temporaryOrderNumberPlaceholder();
|
|
2846
|
+
const row = await orderRepo.save(
|
|
2847
|
+
orderRepo.create({
|
|
2848
|
+
orderNumber: tmp,
|
|
2849
|
+
orderKind,
|
|
2850
|
+
parentOrderId: parent.id,
|
|
2851
|
+
contactId,
|
|
2852
|
+
billingAddressId: null,
|
|
2853
|
+
shippingAddressId: null,
|
|
2854
|
+
status: "pending",
|
|
2855
|
+
subtotal: 0,
|
|
2856
|
+
tax: 0,
|
|
2857
|
+
discount: 0,
|
|
2858
|
+
total: 0,
|
|
2859
|
+
currency,
|
|
2860
|
+
metadata: { platformRef: ref },
|
|
2861
|
+
deleted: false
|
|
2862
|
+
})
|
|
2863
|
+
);
|
|
2864
|
+
const r = row;
|
|
2865
|
+
await orderRepo.update(r.id, {
|
|
2866
|
+
orderNumber: buildCanonicalOrderNumber(orderKind, r.id, r.createdAt ?? /* @__PURE__ */ new Date())
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
function deepMergeFulfillment(a, b) {
|
|
2871
|
+
if (!a) return b;
|
|
2872
|
+
if (!b) return a;
|
|
2873
|
+
return {
|
|
2874
|
+
...a,
|
|
2875
|
+
...b,
|
|
2876
|
+
events: b.events?.length ? b.events : a.events
|
|
2877
|
+
};
|
|
2878
|
+
}
|
|
2879
|
+
async function refreshOrderFromErp(cms, dataSource, entityMap, submission, order) {
|
|
2880
|
+
const orderRepo = dataSource.getRepository(entityMap.orders);
|
|
2881
|
+
const kind = order.orderKind || "sale";
|
|
2882
|
+
const meta = order.metadata && typeof order.metadata === "object" && !Array.isArray(order.metadata) ? { ...order.metadata } : {};
|
|
2883
|
+
if (kind === "sale") {
|
|
2884
|
+
const refId = String(order.orderNumber || "");
|
|
2885
|
+
let fulfillment;
|
|
2886
|
+
let invoiceNumber;
|
|
2887
|
+
let invoiceId;
|
|
2888
|
+
let newStatus;
|
|
2889
|
+
const r1 = await submission.postErpReadAction("get-order-status", { platformOrderId: refId });
|
|
2890
|
+
const d1 = r1.ok ? unwrapErpReadData(r1.json) : null;
|
|
2891
|
+
if (d1) {
|
|
2892
|
+
const mapped = mapErpSaleStatusToOrderStatus(
|
|
2893
|
+
typeof d1.status === "string" ? d1.status : typeof d1.orderStatus === "string" ? d1.orderStatus : typeof d1.state === "string" ? d1.state : void 0
|
|
2894
|
+
);
|
|
2895
|
+
if (mapped) newStatus = mapped;
|
|
2896
|
+
fulfillment = mapErpPayloadToFulfillment(d1);
|
|
2897
|
+
const refs = extractChildOrderRefsFromSalePayload(d1);
|
|
2898
|
+
if (refs.length) {
|
|
2899
|
+
await ensureChildOrdersFromRefs(
|
|
2900
|
+
orderRepo,
|
|
2901
|
+
order,
|
|
2902
|
+
refs,
|
|
2903
|
+
order.contactId,
|
|
2904
|
+
String(order.currency || "INR")
|
|
2905
|
+
);
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
const r2 = await submission.postErpReadAction("get-fulfillment-status", { platformOrderId: refId });
|
|
2909
|
+
const d2 = r2.ok ? unwrapErpReadData(r2.json) : null;
|
|
2910
|
+
if (d2) {
|
|
2911
|
+
fulfillment = deepMergeFulfillment(fulfillment, mapErpPayloadToFulfillment(d2));
|
|
2912
|
+
}
|
|
2913
|
+
const r3 = await submission.postErpReadAction("get-invoice", { platformOrderId: refId });
|
|
2914
|
+
const d3 = r3.ok ? unwrapErpReadData(r3.json) : null;
|
|
2915
|
+
if (d3) {
|
|
2916
|
+
invoiceNumber = mapErpPayloadToInvoiceNumber(d3);
|
|
2917
|
+
invoiceId = pickInvoiceId2(d3);
|
|
2918
|
+
}
|
|
2919
|
+
const oid = order.id;
|
|
2920
|
+
const prevInv = meta.invoice && typeof meta.invoice === "object" && !Array.isArray(meta.invoice) ? { ...meta.invoice } : {};
|
|
2921
|
+
const nextInvoice = {
|
|
2922
|
+
...prevInv,
|
|
2923
|
+
link: `/api/storefront/orders/${oid}/invoice`,
|
|
2924
|
+
...invoiceNumber ? { invoiceNumber } : {},
|
|
2925
|
+
...invoiceId ? { invoiceId } : {}
|
|
2926
|
+
};
|
|
2927
|
+
const patch = { invoice: nextInvoice };
|
|
2928
|
+
if (fulfillment !== void 0) patch.fulfillment = fulfillment;
|
|
2929
|
+
const nextMeta = mergeOrderMetadataPatch(meta, patch);
|
|
2930
|
+
await orderRepo.update(oid, {
|
|
2931
|
+
...newStatus ? { status: newStatus } : {},
|
|
2932
|
+
metadata: nextMeta,
|
|
2933
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2934
|
+
});
|
|
2935
|
+
return;
|
|
2936
|
+
}
|
|
2937
|
+
if (kind === "return" || kind === "replacement") {
|
|
2938
|
+
const platformReturnId = String(order.orderNumber || "");
|
|
2939
|
+
const r = await submission.postErpReadAction("get-return-status", { platformReturnId });
|
|
2940
|
+
const d = r.ok ? unwrapErpReadData(r.json) : null;
|
|
2941
|
+
if (!d) return;
|
|
2942
|
+
const mapped = mapErpSaleStatusToOrderStatus(
|
|
2943
|
+
typeof d.status === "string" ? d.status : typeof d.returnStatus === "string" ? d.returnStatus : void 0
|
|
2944
|
+
);
|
|
2945
|
+
const fulfillment = mapErpPayloadToFulfillment(d);
|
|
2946
|
+
const patch = {};
|
|
2947
|
+
if (fulfillment !== void 0) patch.fulfillment = fulfillment;
|
|
2948
|
+
const nextMeta = Object.keys(patch).length ? mergeOrderMetadataPatch(meta, patch) : meta;
|
|
2949
|
+
await orderRepo.update(order.id, {
|
|
2950
|
+
...mapped ? { status: mapped } : {},
|
|
2951
|
+
metadata: nextMeta,
|
|
2952
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2953
|
+
});
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
async function tryRefreshOrderFromErpForStorefront(cms, dataSource, entityMap, order) {
|
|
2957
|
+
try {
|
|
2958
|
+
const on = await isErpIntegrationEnabled(cms, dataSource, entityMap);
|
|
2959
|
+
if (!on) return;
|
|
2960
|
+
const erp = cms.getPlugin("erp");
|
|
2961
|
+
if (!erp?.submission) return;
|
|
2962
|
+
await refreshOrderFromErp(cms, dataSource, entityMap, erp.submission, order);
|
|
2963
|
+
} catch {
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
// src/api/storefront-handlers.ts
|
|
2968
|
+
init_erp_order_invoice();
|
|
2969
|
+
|
|
2970
|
+
// src/plugins/sms/sms-queue.ts
|
|
2971
|
+
var SMS_QUEUE_NAME = "sms";
|
|
2972
|
+
async function queueSms(cms, payload) {
|
|
2973
|
+
const queue = cms.getPlugin("queue");
|
|
2974
|
+
const sms = cms.getPlugin("sms");
|
|
2975
|
+
if (queue) {
|
|
2976
|
+
await queue.add(SMS_QUEUE_NAME, payload);
|
|
2977
|
+
return;
|
|
2978
|
+
}
|
|
2979
|
+
if (sms && typeof sms.send === "function") {
|
|
2980
|
+
if (payload.templateKey?.trim()) {
|
|
2981
|
+
await sms.send({
|
|
2982
|
+
to: payload.to,
|
|
2983
|
+
templateKey: payload.templateKey.trim(),
|
|
2984
|
+
variables: payload.variables,
|
|
2985
|
+
otpCode: payload.otpCode
|
|
2986
|
+
});
|
|
2987
|
+
return;
|
|
2988
|
+
}
|
|
2989
|
+
if (payload.body?.trim()) {
|
|
2990
|
+
await sms.send({
|
|
2991
|
+
to: payload.to,
|
|
2992
|
+
body: payload.body,
|
|
2993
|
+
otpCode: payload.otpCode,
|
|
2994
|
+
variables: payload.variables
|
|
2995
|
+
});
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
// src/lib/otp-challenge.ts
|
|
3001
|
+
import { createHmac, randomInt, timingSafeEqual } from "crypto";
|
|
3002
|
+
import { IsNull as IsNull2, MoreThan as MoreThan2 } from "typeorm";
|
|
3003
|
+
var OTP_TTL_MS = 10 * 60 * 1e3;
|
|
3004
|
+
var MAX_SENDS_PER_HOUR = 5;
|
|
3005
|
+
var MAX_VERIFY_ATTEMPTS = 8;
|
|
3006
|
+
function getPepper(explicit) {
|
|
3007
|
+
return (explicit || process.env.OTP_PEPPER || process.env.NEXTAUTH_SECRET || "dev-otp-pepper").trim();
|
|
3008
|
+
}
|
|
3009
|
+
function hashOtpCode(code, purpose, identifier, pepper) {
|
|
3010
|
+
return createHmac("sha256", getPepper(pepper)).update(`${purpose}|${identifier}|${code}`).digest("hex");
|
|
3011
|
+
}
|
|
3012
|
+
function verifyOtpCodeHash(code, storedHash, purpose, identifier, pepper) {
|
|
3013
|
+
const h = hashOtpCode(code, purpose, identifier, pepper);
|
|
3014
|
+
try {
|
|
3015
|
+
return timingSafeEqual(Buffer.from(h, "utf8"), Buffer.from(storedHash, "utf8"));
|
|
3016
|
+
} catch {
|
|
3017
|
+
return false;
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
function generateNumericOtp(length = 6) {
|
|
3021
|
+
const max = 10 ** length;
|
|
3022
|
+
return randomInt(0, max).toString().padStart(length, "0");
|
|
3023
|
+
}
|
|
3024
|
+
function normalizePhoneE164(raw, defaultCountryCode) {
|
|
3025
|
+
const t = raw.trim();
|
|
3026
|
+
const digitsOnly = t.replace(/\D/g, "");
|
|
3027
|
+
if (digitsOnly.length < 10) return null;
|
|
3028
|
+
if (t.startsWith("+")) return `+${digitsOnly}`;
|
|
3029
|
+
const cc = (defaultCountryCode || process.env.DEFAULT_PHONE_COUNTRY_CODE || "91").replace(/\D/g, "");
|
|
3030
|
+
if (digitsOnly.length > 10) return `+${digitsOnly}`;
|
|
3031
|
+
return `+${cc}${digitsOnly}`;
|
|
3032
|
+
}
|
|
3033
|
+
async function countRecentOtpSends(dataSource, entityMap, purpose, identifier, since) {
|
|
3034
|
+
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
3035
|
+
return repo.count({
|
|
3036
|
+
where: { purpose, identifier, createdAt: MoreThan2(since) }
|
|
3037
|
+
});
|
|
3038
|
+
}
|
|
3039
|
+
async function createOtpChallenge(dataSource, entityMap, input) {
|
|
3040
|
+
const { purpose, channel, identifier, code, pepper } = input;
|
|
3041
|
+
const since = new Date(Date.now() - 60 * 60 * 1e3);
|
|
3042
|
+
const recent = await countRecentOtpSends(dataSource, entityMap, purpose, identifier, since);
|
|
3043
|
+
if (recent >= MAX_SENDS_PER_HOUR) {
|
|
3044
|
+
return { ok: false, error: "Too many codes sent. Try again later.", status: 429 };
|
|
3045
|
+
}
|
|
3046
|
+
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
3047
|
+
await repo.delete({
|
|
3048
|
+
purpose,
|
|
3049
|
+
identifier,
|
|
3050
|
+
consumedAt: IsNull2()
|
|
3051
|
+
});
|
|
3052
|
+
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
3053
|
+
const codeHash = hashOtpCode(code, purpose, identifier, pepper);
|
|
3054
|
+
await repo.save(
|
|
3055
|
+
repo.create({
|
|
3056
|
+
purpose,
|
|
3057
|
+
channel,
|
|
3058
|
+
identifier,
|
|
3059
|
+
codeHash,
|
|
3060
|
+
expiresAt,
|
|
3061
|
+
attempts: 0,
|
|
3062
|
+
consumedAt: null
|
|
3063
|
+
})
|
|
3064
|
+
);
|
|
3065
|
+
return { ok: true };
|
|
3066
|
+
}
|
|
3067
|
+
async function verifyAndConsumeOtpChallenge(dataSource, entityMap, input) {
|
|
3068
|
+
const { purpose, identifier, code, pepper } = input;
|
|
3069
|
+
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
3070
|
+
const row = await repo.findOne({
|
|
3071
|
+
where: { purpose, identifier, consumedAt: IsNull2() },
|
|
3072
|
+
order: { id: "DESC" }
|
|
3073
|
+
});
|
|
3074
|
+
if (!row) {
|
|
3075
|
+
return { ok: false, error: "Invalid or expired code", status: 400 };
|
|
3076
|
+
}
|
|
3077
|
+
const r = row;
|
|
3078
|
+
if (new Date(r.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
3079
|
+
await repo.delete(row.id);
|
|
3080
|
+
return { ok: false, error: "Invalid or expired code", status: 400 };
|
|
3081
|
+
}
|
|
3082
|
+
const attempts = r.attempts || 0;
|
|
3083
|
+
if (attempts >= MAX_VERIFY_ATTEMPTS) {
|
|
3084
|
+
await repo.delete(row.id);
|
|
3085
|
+
return { ok: false, error: "Too many attempts", status: 400 };
|
|
3086
|
+
}
|
|
3087
|
+
const valid = verifyOtpCodeHash(code, r.codeHash, purpose, identifier, pepper);
|
|
3088
|
+
if (!valid) {
|
|
3089
|
+
await repo.update(row.id, { attempts: attempts + 1 });
|
|
3090
|
+
return { ok: false, error: "Invalid or expired code", status: 400 };
|
|
3091
|
+
}
|
|
3092
|
+
await repo.update(row.id, { consumedAt: /* @__PURE__ */ new Date(), attempts: attempts + 1 });
|
|
3093
|
+
return { ok: true };
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
// src/api/storefront-handlers.ts
|
|
2106
3097
|
var GUEST_COOKIE = "guest_id";
|
|
2107
3098
|
var ONE_YEAR = 60 * 60 * 24 * 365;
|
|
2108
3099
|
function parseCookies(header) {
|
|
@@ -2120,13 +3111,17 @@ function parseCookies(header) {
|
|
|
2120
3111
|
function guestCookieHeader(name, token) {
|
|
2121
3112
|
return `${name}=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${ONE_YEAR}`;
|
|
2122
3113
|
}
|
|
2123
|
-
function orderNumber() {
|
|
2124
|
-
return `ORD-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
2125
|
-
}
|
|
2126
3114
|
var SIGNUP_VERIFY_EXPIRY_HOURS = 72;
|
|
2127
3115
|
function createStorefrontApiHandler(config) {
|
|
2128
3116
|
const { dataSource, entityMap, json, getSessionUser, getCms, getCompanyDetails, publicSiteUrl } = config;
|
|
2129
3117
|
const cookieName = config.guestCookieName ?? GUEST_COOKIE;
|
|
3118
|
+
const otpFlags = config.otpFlags;
|
|
3119
|
+
const otpPepper = config.otpPepper;
|
|
3120
|
+
const defaultPhoneCc = config.defaultPhoneCountryCode;
|
|
3121
|
+
const otpAllowPhoneLogin = config.otpAllowPhoneLogin !== false;
|
|
3122
|
+
function otpOff(key) {
|
|
3123
|
+
return !otpFlags || otpFlags[key] !== true;
|
|
3124
|
+
}
|
|
2130
3125
|
const cartRepo = () => dataSource.getRepository(entityMap.carts);
|
|
2131
3126
|
const cartItemRepo = () => dataSource.getRepository(entityMap.cart_items);
|
|
2132
3127
|
const productRepo = () => dataSource.getRepository(entityMap.products);
|
|
@@ -2140,13 +3135,174 @@ function createStorefrontApiHandler(config) {
|
|
|
2140
3135
|
const tokenRepo = () => dataSource.getRepository(entityMap.password_reset_tokens);
|
|
2141
3136
|
const collectionRepo = () => dataSource.getRepository(entityMap.collections);
|
|
2142
3137
|
const groupRepo = () => dataSource.getRepository(entityMap.user_groups);
|
|
3138
|
+
const configRepo = () => dataSource.getRepository(entityMap.configs);
|
|
3139
|
+
const CART_CHECKOUT_RELATIONS = ["items", "items.product", "items.product.taxes", "items.product.taxes.tax"];
|
|
3140
|
+
function roundMoney2(n) {
|
|
3141
|
+
return Math.round(n * 100) / 100;
|
|
3142
|
+
}
|
|
3143
|
+
async function getStoreDefaultTaxRate() {
|
|
3144
|
+
const rows = await configRepo().find({ where: { settings: "store", deleted: false } });
|
|
3145
|
+
for (const row of rows) {
|
|
3146
|
+
const r = row;
|
|
3147
|
+
if (r.key === "defaultTaxRate") {
|
|
3148
|
+
const n = parseFloat(String(r.value ?? "").trim());
|
|
3149
|
+
return Number.isFinite(n) && n >= 0 ? n : null;
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
return null;
|
|
3153
|
+
}
|
|
3154
|
+
function computeTaxForProductLine(p, lineSubtotal, defaultRate) {
|
|
3155
|
+
const pts = p.taxes ?? [];
|
|
3156
|
+
const activePts = pts.filter((pt) => {
|
|
3157
|
+
const t = pt.tax;
|
|
3158
|
+
return t != null && t.active !== false;
|
|
3159
|
+
});
|
|
3160
|
+
if (activePts.length) {
|
|
3161
|
+
let sumRate = 0;
|
|
3162
|
+
const slugs = [];
|
|
3163
|
+
for (const pt of activePts) {
|
|
3164
|
+
const t = pt.tax;
|
|
3165
|
+
const r = Number(pt.rate != null && pt.rate !== "" ? pt.rate : t.rate ?? 0);
|
|
3166
|
+
if (Number.isFinite(r)) sumRate += r;
|
|
3167
|
+
const slug = String(t.slug ?? "").trim();
|
|
3168
|
+
if (slug) slugs.push(slug);
|
|
3169
|
+
}
|
|
3170
|
+
const tax = roundMoney2(lineSubtotal * sumRate / 100);
|
|
3171
|
+
return {
|
|
3172
|
+
tax,
|
|
3173
|
+
taxRate: sumRate > 0 ? roundMoney2(sumRate) : null,
|
|
3174
|
+
taxCode: slugs.length ? [...new Set(slugs)].sort().join(",") : null
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
if (defaultRate != null && defaultRate > 0) {
|
|
3178
|
+
return {
|
|
3179
|
+
tax: roundMoney2(lineSubtotal * defaultRate / 100),
|
|
3180
|
+
taxRate: roundMoney2(defaultRate),
|
|
3181
|
+
taxCode: null
|
|
3182
|
+
};
|
|
3183
|
+
}
|
|
3184
|
+
return { tax: 0, taxRate: null, taxCode: null };
|
|
3185
|
+
}
|
|
3186
|
+
function parseInlineAddress(raw) {
|
|
3187
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
3188
|
+
const o = raw;
|
|
3189
|
+
const line1 = String(o.line1 ?? "").trim();
|
|
3190
|
+
if (!line1) return null;
|
|
3191
|
+
return {
|
|
3192
|
+
line1,
|
|
3193
|
+
line2: o.line2 != null ? String(o.line2) : "",
|
|
3194
|
+
city: o.city != null ? String(o.city) : "",
|
|
3195
|
+
state: o.state != null ? String(o.state) : "",
|
|
3196
|
+
postalCode: o.postalCode != null ? String(o.postalCode) : "",
|
|
3197
|
+
country: o.country != null ? String(o.country) : ""
|
|
3198
|
+
};
|
|
3199
|
+
}
|
|
3200
|
+
function intFromBody(v) {
|
|
3201
|
+
if (typeof v === "number" && Number.isInteger(v)) return v;
|
|
3202
|
+
if (typeof v === "string" && /^\d+$/.test(v)) return parseInt(v, 10);
|
|
3203
|
+
return void 0;
|
|
3204
|
+
}
|
|
3205
|
+
async function resolveCheckoutAddress(contactId, idVal, inlineVal) {
|
|
3206
|
+
const aid = intFromBody(idVal);
|
|
3207
|
+
if (aid != null) {
|
|
3208
|
+
const existing = await addressRepo().findOne({
|
|
3209
|
+
where: { id: aid, contactId }
|
|
3210
|
+
});
|
|
3211
|
+
if (!existing) return { id: null, error: "Address not found" };
|
|
3212
|
+
return { id: aid };
|
|
3213
|
+
}
|
|
3214
|
+
const addr = parseInlineAddress(inlineVal);
|
|
3215
|
+
if (addr) {
|
|
3216
|
+
const saved = await addressRepo().save(
|
|
3217
|
+
addressRepo().create({
|
|
3218
|
+
contactId,
|
|
3219
|
+
line1: addr.line1,
|
|
3220
|
+
line2: addr.line2?.trim() ? addr.line2 : null,
|
|
3221
|
+
city: addr.city?.trim() ? addr.city : null,
|
|
3222
|
+
state: addr.state?.trim() ? addr.state : null,
|
|
3223
|
+
postalCode: addr.postalCode?.trim() ? addr.postalCode : null,
|
|
3224
|
+
country: addr.country?.trim() ? addr.country : null
|
|
3225
|
+
})
|
|
3226
|
+
);
|
|
3227
|
+
return { id: saved.id };
|
|
3228
|
+
}
|
|
3229
|
+
return { id: null };
|
|
3230
|
+
}
|
|
3231
|
+
async function prepareCheckoutFromCart(b, cart, contactId) {
|
|
3232
|
+
const defaultRate = await getStoreDefaultTaxRate();
|
|
3233
|
+
const lines = [];
|
|
3234
|
+
let subtotal = 0;
|
|
3235
|
+
let orderTax = 0;
|
|
3236
|
+
let needsShipping = false;
|
|
3237
|
+
for (const it of cart.items || []) {
|
|
3238
|
+
const p = it.product;
|
|
3239
|
+
if (!p || p.deleted || p.status !== "available") continue;
|
|
3240
|
+
const unit = Number(p.price);
|
|
3241
|
+
const qty = it.quantity || 1;
|
|
3242
|
+
const lineSubtotal = unit * qty;
|
|
3243
|
+
const pType = p.type === "service" ? "service" : "product";
|
|
3244
|
+
if (pType === "product") needsShipping = true;
|
|
3245
|
+
const { tax, taxRate, taxCode } = computeTaxForProductLine(p, lineSubtotal, defaultRate);
|
|
3246
|
+
const lineTotal = roundMoney2(lineSubtotal + tax);
|
|
3247
|
+
subtotal = roundMoney2(subtotal + lineSubtotal);
|
|
3248
|
+
orderTax = roundMoney2(orderTax + tax);
|
|
3249
|
+
lines.push({
|
|
3250
|
+
productId: p.id,
|
|
3251
|
+
quantity: qty,
|
|
3252
|
+
unitPrice: unit,
|
|
3253
|
+
tax,
|
|
3254
|
+
total: lineTotal,
|
|
3255
|
+
hsn: p.hsn ?? null,
|
|
3256
|
+
uom: p.uom ?? null,
|
|
3257
|
+
productType: pType,
|
|
3258
|
+
taxRate,
|
|
3259
|
+
taxCode
|
|
3260
|
+
});
|
|
3261
|
+
}
|
|
3262
|
+
if (!lines.length) return { ok: false, status: 400, message: "No available items in cart" };
|
|
3263
|
+
const bill = await resolveCheckoutAddress(contactId, b.billingAddressId, b.billingAddress);
|
|
3264
|
+
if (bill.error) return { ok: false, status: 400, message: bill.error };
|
|
3265
|
+
if (bill.id == null) return { ok: false, status: 400, message: "Billing address required" };
|
|
3266
|
+
const ship = await resolveCheckoutAddress(contactId, b.shippingAddressId, b.shippingAddress);
|
|
3267
|
+
if (ship.error) return { ok: false, status: 400, message: ship.error };
|
|
3268
|
+
let shippingAddressId = ship.id;
|
|
3269
|
+
if (needsShipping && shippingAddressId == null) shippingAddressId = bill.id;
|
|
3270
|
+
if (needsShipping && shippingAddressId == null) {
|
|
3271
|
+
return { ok: false, status: 400, message: "Shipping address required" };
|
|
3272
|
+
}
|
|
3273
|
+
const orderTotal = roundMoney2(subtotal + orderTax);
|
|
3274
|
+
return {
|
|
3275
|
+
ok: true,
|
|
3276
|
+
lines,
|
|
3277
|
+
subtotal,
|
|
3278
|
+
orderTax,
|
|
3279
|
+
orderTotal,
|
|
3280
|
+
billingAddressId: bill.id,
|
|
3281
|
+
shippingAddressId
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
async function syncContactToErp(contact) {
|
|
3285
|
+
if (!getCms) return;
|
|
3286
|
+
try {
|
|
3287
|
+
const cms = await getCms();
|
|
3288
|
+
await queueErpCreateContactIfEnabled(cms, dataSource, entityMap, {
|
|
3289
|
+
name: String(contact.name ?? ""),
|
|
3290
|
+
email: String(contact.email ?? "").trim(),
|
|
3291
|
+
phone: contact.phone,
|
|
3292
|
+
type: contact.type,
|
|
3293
|
+
company: contact.company,
|
|
3294
|
+
notes: contact.notes
|
|
3295
|
+
});
|
|
3296
|
+
} catch {
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
2143
3299
|
async function ensureContactForUser(userId) {
|
|
2144
3300
|
let c = await contactRepo().findOne({ where: { userId, deleted: false } });
|
|
2145
3301
|
if (c) return c;
|
|
2146
3302
|
const u = await userRepo().findOne({ where: { id: userId } });
|
|
2147
3303
|
if (!u) return null;
|
|
2148
3304
|
const unclaimed = await contactRepo().findOne({
|
|
2149
|
-
where: { email: u.email, userId:
|
|
3305
|
+
where: { email: u.email, userId: IsNull3(), deleted: false }
|
|
2150
3306
|
});
|
|
2151
3307
|
if (unclaimed) {
|
|
2152
3308
|
await contactRepo().update(unclaimed.id, { userId });
|
|
@@ -2161,6 +3317,7 @@ function createStorefrontApiHandler(config) {
|
|
|
2161
3317
|
deleted: false
|
|
2162
3318
|
})
|
|
2163
3319
|
);
|
|
3320
|
+
await syncContactToErp(created);
|
|
2164
3321
|
return { id: created.id };
|
|
2165
3322
|
}
|
|
2166
3323
|
async function getOrCreateCart(req) {
|
|
@@ -2249,24 +3406,42 @@ function createStorefrontApiHandler(config) {
|
|
|
2249
3406
|
slug: p.slug,
|
|
2250
3407
|
price: p.price,
|
|
2251
3408
|
sku: p.sku,
|
|
3409
|
+
type: p.type === "service" ? "service" : "product",
|
|
2252
3410
|
image: primaryProductImageUrl(p.metadata)
|
|
2253
3411
|
} : null
|
|
2254
3412
|
};
|
|
2255
3413
|
})
|
|
2256
3414
|
};
|
|
2257
3415
|
}
|
|
3416
|
+
function serializeSeo(seo) {
|
|
3417
|
+
if (!seo || typeof seo !== "object") return void 0;
|
|
3418
|
+
const s = seo;
|
|
3419
|
+
return {
|
|
3420
|
+
title: s.title ?? null,
|
|
3421
|
+
description: s.description ?? null,
|
|
3422
|
+
keywords: s.keywords ?? null,
|
|
3423
|
+
ogTitle: s.ogTitle ?? null,
|
|
3424
|
+
ogDescription: s.ogDescription ?? null,
|
|
3425
|
+
ogImage: s.ogImage ?? null,
|
|
3426
|
+
slug: s.slug ?? null
|
|
3427
|
+
};
|
|
3428
|
+
}
|
|
2258
3429
|
function serializeProduct(p) {
|
|
3430
|
+
const seo = serializeSeo(p.seo);
|
|
2259
3431
|
return {
|
|
2260
3432
|
id: p.id,
|
|
2261
3433
|
name: p.name,
|
|
2262
3434
|
slug: p.slug,
|
|
2263
3435
|
sku: p.sku,
|
|
2264
3436
|
hsn: p.hsn,
|
|
3437
|
+
uom: p.uom ?? null,
|
|
3438
|
+
type: p.type === "service" ? "service" : "product",
|
|
2265
3439
|
price: p.price,
|
|
2266
3440
|
compareAtPrice: p.compareAtPrice,
|
|
2267
3441
|
status: p.status,
|
|
2268
3442
|
collectionId: p.collectionId,
|
|
2269
|
-
metadata: p.metadata
|
|
3443
|
+
metadata: p.metadata,
|
|
3444
|
+
...seo ? { seo } : {}
|
|
2270
3445
|
};
|
|
2271
3446
|
}
|
|
2272
3447
|
return {
|
|
@@ -2333,7 +3508,7 @@ function createStorefrontApiHandler(config) {
|
|
|
2333
3508
|
const byId = /^\d+$/.test(idOrSlug);
|
|
2334
3509
|
const product = await productRepo().findOne({
|
|
2335
3510
|
where: byId ? { id: parseInt(idOrSlug, 10), status: "available", deleted: false } : { slug: idOrSlug, status: "available", deleted: false },
|
|
2336
|
-
relations: ["attributes", "attributes.attribute"]
|
|
3511
|
+
relations: ["attributes", "attributes.attribute", "seo"]
|
|
2337
3512
|
});
|
|
2338
3513
|
if (!product) return json({ error: "Not found" }, { status: 404 });
|
|
2339
3514
|
const p = product;
|
|
@@ -2377,7 +3552,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2377
3552
|
const idOrSlug = path[1];
|
|
2378
3553
|
const byId = /^\d+$/.test(idOrSlug);
|
|
2379
3554
|
const collection = await collectionRepo().findOne({
|
|
2380
|
-
where: byId ? { id: parseInt(idOrSlug, 10), active: true, deleted: false } : { slug: idOrSlug, active: true, deleted: false }
|
|
3555
|
+
where: byId ? { id: parseInt(idOrSlug, 10), active: true, deleted: false } : { slug: idOrSlug, active: true, deleted: false },
|
|
3556
|
+
relations: ["seo"]
|
|
2381
3557
|
});
|
|
2382
3558
|
if (!collection) return json({ error: "Not found" }, { status: 404 });
|
|
2383
3559
|
const col = collection;
|
|
@@ -2385,12 +3561,14 @@ function createStorefrontApiHandler(config) {
|
|
|
2385
3561
|
where: { collectionId: col.id, status: "available", deleted: false },
|
|
2386
3562
|
order: { id: "ASC" }
|
|
2387
3563
|
});
|
|
3564
|
+
const colSeo = serializeSeo(col.seo);
|
|
2388
3565
|
return json({
|
|
2389
3566
|
id: col.id,
|
|
2390
3567
|
name: col.name,
|
|
2391
3568
|
slug: col.slug,
|
|
2392
3569
|
description: col.description,
|
|
2393
3570
|
image: col.image,
|
|
3571
|
+
...colSeo ? { seo: colSeo } : {},
|
|
2394
3572
|
products: products.map((p) => serializeProduct(p))
|
|
2395
3573
|
});
|
|
2396
3574
|
}
|
|
@@ -2428,6 +3606,7 @@ function createStorefrontApiHandler(config) {
|
|
|
2428
3606
|
await userRepo().update(uid, { name: b.name.trim() });
|
|
2429
3607
|
}
|
|
2430
3608
|
const updatedContact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
3609
|
+
if (updatedContact) await syncContactToErp(updatedContact);
|
|
2431
3610
|
const updatedUser = await userRepo().findOne({ where: { id: uid }, select: ["id", "name", "email"] });
|
|
2432
3611
|
return json({
|
|
2433
3612
|
user: updatedUser ? { id: updatedUser.id, name: updatedUser.name, email: updatedUser.email } : null,
|
|
@@ -2515,13 +3694,155 @@ function createStorefrontApiHandler(config) {
|
|
|
2515
3694
|
const email = record.email;
|
|
2516
3695
|
const user = await userRepo().findOne({ where: { email }, select: ["id", "blocked"] });
|
|
2517
3696
|
if (!user) return json({ error: "User not found" }, { status: 400 });
|
|
2518
|
-
await userRepo().update(user.id, {
|
|
3697
|
+
await userRepo().update(user.id, {
|
|
3698
|
+
blocked: false,
|
|
3699
|
+
emailVerifiedAt: /* @__PURE__ */ new Date(),
|
|
3700
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3701
|
+
});
|
|
2519
3702
|
await tokenRepo().delete({ email });
|
|
2520
3703
|
return json({ success: true, message: "Email verified. You can sign in." });
|
|
2521
3704
|
}
|
|
3705
|
+
if (path[0] === "auth" && path[1] === "otp" && path[2] === "send" && path.length === 3 && method === "POST") {
|
|
3706
|
+
const b = await req.json().catch(() => ({}));
|
|
3707
|
+
const purposeRaw = typeof b.purpose === "string" ? b.purpose.trim() : "";
|
|
3708
|
+
const purpose = purposeRaw === "login" || purposeRaw === "verify_email" || purposeRaw === "verify_phone" ? purposeRaw : "";
|
|
3709
|
+
if (!purpose) return json({ error: "purpose must be login, verify_email, or verify_phone" }, { status: 400 });
|
|
3710
|
+
if (purpose === "login" && otpOff("login")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
3711
|
+
if (purpose === "verify_email" && otpOff("verifyEmail")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
3712
|
+
if (purpose === "verify_phone" && otpOff("verifyPhone")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
3713
|
+
const capOtp = await assertCaptchaOk(getCms, b, req, json);
|
|
3714
|
+
if (capOtp) return capOtp;
|
|
3715
|
+
const emailIn = typeof b.email === "string" ? b.email.trim().toLowerCase() : "";
|
|
3716
|
+
const phoneIn = typeof b.phone === "string" ? b.phone.trim() : "";
|
|
3717
|
+
let identifier;
|
|
3718
|
+
let channel;
|
|
3719
|
+
if (purpose === "login") {
|
|
3720
|
+
if (emailIn) {
|
|
3721
|
+
identifier = emailIn;
|
|
3722
|
+
channel = "email";
|
|
3723
|
+
} else if (phoneIn) {
|
|
3724
|
+
if (!otpAllowPhoneLogin) {
|
|
3725
|
+
return json({ error: "Phone sign-in is not enabled" }, { status: 403 });
|
|
3726
|
+
}
|
|
3727
|
+
const p = normalizePhoneE164(phoneIn, defaultPhoneCc);
|
|
3728
|
+
if (!p) return json({ error: "Invalid phone" }, { status: 400 });
|
|
3729
|
+
identifier = p;
|
|
3730
|
+
channel = "sms";
|
|
3731
|
+
} else {
|
|
3732
|
+
return json({ error: "email or phone required" }, { status: 400 });
|
|
3733
|
+
}
|
|
3734
|
+
const user = channel === "email" ? await userRepo().findOne({ where: { email: identifier } }) : await userRepo().findOne({ where: { phone: identifier } });
|
|
3735
|
+
if (!user || user.deleted || user.blocked) {
|
|
3736
|
+
return json({ ok: true });
|
|
3737
|
+
}
|
|
3738
|
+
} else if (purpose === "verify_email") {
|
|
3739
|
+
if (!emailIn || !isValidSignupEmail(emailIn)) return json({ error: "Valid email required" }, { status: 400 });
|
|
3740
|
+
identifier = emailIn;
|
|
3741
|
+
channel = "email";
|
|
3742
|
+
const user = await userRepo().findOne({ where: { email: identifier } });
|
|
3743
|
+
if (!user || user.deleted) return json({ ok: true });
|
|
3744
|
+
} else {
|
|
3745
|
+
const su = await getSessionUser();
|
|
3746
|
+
const uid = su?.id ? parseInt(String(su.id), 10) : NaN;
|
|
3747
|
+
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
3748
|
+
const p = normalizePhoneE164(phoneIn, defaultPhoneCc);
|
|
3749
|
+
if (!p) return json({ error: "Valid phone required" }, { status: 400 });
|
|
3750
|
+
identifier = p;
|
|
3751
|
+
channel = "sms";
|
|
3752
|
+
const taken = await userRepo().findOne({
|
|
3753
|
+
where: { phone: identifier },
|
|
3754
|
+
select: ["id"]
|
|
3755
|
+
});
|
|
3756
|
+
if (taken && taken.id !== uid) {
|
|
3757
|
+
return json({ error: "Phone already in use" }, { status: 400 });
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
const code = generateNumericOtp(6);
|
|
3761
|
+
const created = await createOtpChallenge(dataSource, entityMap, {
|
|
3762
|
+
purpose,
|
|
3763
|
+
channel,
|
|
3764
|
+
identifier,
|
|
3765
|
+
code,
|
|
3766
|
+
pepper: otpPepper
|
|
3767
|
+
});
|
|
3768
|
+
if (!created.ok) return json({ error: created.error }, { status: created.status });
|
|
3769
|
+
if (!getCms) return json({ error: "OTP delivery not configured" }, { status: 503 });
|
|
3770
|
+
try {
|
|
3771
|
+
const cms = await getCms();
|
|
3772
|
+
if (channel === "email") {
|
|
3773
|
+
if (!cms.getPlugin("email")) return json({ error: "Email not configured" }, { status: 503 });
|
|
3774
|
+
const companyDetails = getCompanyDetails ? await getCompanyDetails() : {};
|
|
3775
|
+
await queueEmail(cms, {
|
|
3776
|
+
to: identifier,
|
|
3777
|
+
templateName: "otp",
|
|
3778
|
+
ctx: { code, companyDetails: companyDetails ?? {} }
|
|
3779
|
+
});
|
|
3780
|
+
} else {
|
|
3781
|
+
if (!cms.getPlugin("sms")) return json({ error: "SMS not configured" }, { status: 503 });
|
|
3782
|
+
const templateKey = purpose === "verify_phone" ? "auth.otp_verify_phone" : "auth.otp_login";
|
|
3783
|
+
await queueSms(cms, { to: identifier, templateKey, variables: { code } });
|
|
3784
|
+
}
|
|
3785
|
+
} catch {
|
|
3786
|
+
return json({ error: "Failed to send code" }, { status: 500 });
|
|
3787
|
+
}
|
|
3788
|
+
return json({ ok: true });
|
|
3789
|
+
}
|
|
3790
|
+
if (path[0] === "auth" && path[1] === "otp" && path[2] === "verify-email" && path.length === 3 && method === "POST") {
|
|
3791
|
+
if (otpOff("verifyEmail")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
3792
|
+
const b = await req.json().catch(() => ({}));
|
|
3793
|
+
const email = typeof b.email === "string" ? b.email.trim().toLowerCase() : "";
|
|
3794
|
+
const code = typeof b.code === "string" ? b.code.trim() : "";
|
|
3795
|
+
if (!email || !code) return json({ error: "email and code required" }, { status: 400 });
|
|
3796
|
+
const v = await verifyAndConsumeOtpChallenge(dataSource, entityMap, {
|
|
3797
|
+
purpose: "verify_email",
|
|
3798
|
+
identifier: email,
|
|
3799
|
+
code,
|
|
3800
|
+
pepper: otpPepper
|
|
3801
|
+
});
|
|
3802
|
+
if (!v.ok) return json({ error: v.error }, { status: v.status });
|
|
3803
|
+
const user = await userRepo().findOne({ where: { email } });
|
|
3804
|
+
if (!user) return json({ error: "User not found" }, { status: 400 });
|
|
3805
|
+
await userRepo().update(user.id, {
|
|
3806
|
+
blocked: false,
|
|
3807
|
+
emailVerifiedAt: /* @__PURE__ */ new Date(),
|
|
3808
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3809
|
+
});
|
|
3810
|
+
await tokenRepo().delete({ email });
|
|
3811
|
+
return json({ success: true, message: "Email verified. You can sign in." });
|
|
3812
|
+
}
|
|
3813
|
+
if (path[0] === "auth" && path[1] === "otp" && path[2] === "verify-phone" && path.length === 3 && method === "POST") {
|
|
3814
|
+
if (otpOff("verifyPhone")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
3815
|
+
const su = await getSessionUser();
|
|
3816
|
+
const uid = su?.id ? parseInt(String(su.id), 10) : NaN;
|
|
3817
|
+
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
3818
|
+
const b = await req.json().catch(() => ({}));
|
|
3819
|
+
const phoneRaw = typeof b.phone === "string" ? b.phone.trim() : "";
|
|
3820
|
+
const code = typeof b.code === "string" ? b.code.trim() : "";
|
|
3821
|
+
const phone = normalizePhoneE164(phoneRaw, defaultPhoneCc);
|
|
3822
|
+
if (!phone || !code) return json({ error: "phone and code required" }, { status: 400 });
|
|
3823
|
+
const v = await verifyAndConsumeOtpChallenge(dataSource, entityMap, {
|
|
3824
|
+
purpose: "verify_phone",
|
|
3825
|
+
identifier: phone,
|
|
3826
|
+
code,
|
|
3827
|
+
pepper: otpPepper
|
|
3828
|
+
});
|
|
3829
|
+
if (!v.ok) return json({ error: v.error }, { status: v.status });
|
|
3830
|
+
const taken = await userRepo().findOne({ where: { phone }, select: ["id"] });
|
|
3831
|
+
if (taken && taken.id !== uid) {
|
|
3832
|
+
return json({ error: "Phone already in use" }, { status: 400 });
|
|
3833
|
+
}
|
|
3834
|
+
await userRepo().update(uid, { phone, phoneVerifiedAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() });
|
|
3835
|
+
const contact = await ensureContactForUser(uid);
|
|
3836
|
+
if (contact) {
|
|
3837
|
+
await contactRepo().update(contact.id, { phone });
|
|
3838
|
+
}
|
|
3839
|
+
return json({ success: true });
|
|
3840
|
+
}
|
|
2522
3841
|
if (path[0] === "register" && path.length === 1 && method === "POST") {
|
|
2523
3842
|
if (!config.hashPassword) return json({ error: "Registration not configured" }, { status: 501 });
|
|
2524
3843
|
const b = await req.json().catch(() => ({}));
|
|
3844
|
+
const capReg = await assertCaptchaOk(getCms, b, req, json);
|
|
3845
|
+
if (capReg) return capReg;
|
|
2525
3846
|
const name = typeof b.name === "string" ? b.name.trim() : "";
|
|
2526
3847
|
const email = typeof b.email === "string" ? b.email.trim().toLowerCase() : "";
|
|
2527
3848
|
const password = typeof b.password === "string" ? b.password : "";
|
|
@@ -2583,6 +3904,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2583
3904
|
}
|
|
2584
3905
|
if (path[0] === "cart" && path[1] === "items" && path.length === 2 && method === "POST") {
|
|
2585
3906
|
const body = await req.json().catch(() => ({}));
|
|
3907
|
+
const capCart = await assertCaptchaOk(getCms, body, req, json);
|
|
3908
|
+
if (capCart) return capCart;
|
|
2586
3909
|
const productId = Number(body.productId);
|
|
2587
3910
|
const quantity = Math.max(1, Number(body.quantity) || 1);
|
|
2588
3911
|
if (!Number.isFinite(productId)) return json({ error: "productId required" }, { status: 400 });
|
|
@@ -2782,6 +4105,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2782
4105
|
const { wishlist, setCookie, err } = await getOrCreateWishlist(req);
|
|
2783
4106
|
if (err) return err;
|
|
2784
4107
|
const b = await req.json().catch(() => ({}));
|
|
4108
|
+
const capWl = await assertCaptchaOk(getCms, b, req, json);
|
|
4109
|
+
if (capWl) return capWl;
|
|
2785
4110
|
const productId = Number(b.productId);
|
|
2786
4111
|
if (!Number.isFinite(productId)) return json({ error: "productId required" }, { status: 400 });
|
|
2787
4112
|
const wid = wishlist.id;
|
|
@@ -2800,6 +4125,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2800
4125
|
}
|
|
2801
4126
|
if (path[0] === "checkout" && path[1] === "order" && path.length === 2 && method === "POST") {
|
|
2802
4127
|
const b = await req.json().catch(() => ({}));
|
|
4128
|
+
const capOrd = await assertCaptchaOk(getCms, b, req, json);
|
|
4129
|
+
if (capOrd) return capOrd;
|
|
2803
4130
|
const u = await getSessionUser();
|
|
2804
4131
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
2805
4132
|
let contactId;
|
|
@@ -2810,11 +4137,11 @@ function createStorefrontApiHandler(config) {
|
|
|
2810
4137
|
contactId = contact.id;
|
|
2811
4138
|
cart = await cartRepo().findOne({
|
|
2812
4139
|
where: { contactId },
|
|
2813
|
-
relations: [
|
|
4140
|
+
relations: [...CART_CHECKOUT_RELATIONS]
|
|
2814
4141
|
});
|
|
2815
4142
|
} else {
|
|
2816
|
-
const email = (b.email
|
|
2817
|
-
const name = (b.name
|
|
4143
|
+
const email = String(b.email ?? "").trim();
|
|
4144
|
+
const name = String(b.name ?? "").trim();
|
|
2818
4145
|
if (!email || !name) return json({ error: "name and email required for guest checkout" }, { status: 400 });
|
|
2819
4146
|
let contact = await contactRepo().findOne({ where: { email, deleted: false } });
|
|
2820
4147
|
if (contact && contact.userId != null) {
|
|
@@ -2825,55 +4152,55 @@ function createStorefrontApiHandler(config) {
|
|
|
2825
4152
|
contactRepo().create({
|
|
2826
4153
|
name,
|
|
2827
4154
|
email,
|
|
2828
|
-
phone: b.phone
|
|
4155
|
+
phone: b.phone != null && b.phone !== "" ? String(b.phone) : null,
|
|
2829
4156
|
userId: null,
|
|
2830
4157
|
deleted: false
|
|
2831
4158
|
})
|
|
2832
4159
|
);
|
|
2833
|
-
} else if (name)
|
|
4160
|
+
} else if (name)
|
|
4161
|
+
await contactRepo().update(contact.id, {
|
|
4162
|
+
name,
|
|
4163
|
+
phone: b.phone != null && b.phone !== "" ? String(b.phone) : contact.phone
|
|
4164
|
+
});
|
|
2834
4165
|
contactId = contact.id;
|
|
4166
|
+
const guestForErp = await contactRepo().findOne({ where: { id: contactId } });
|
|
4167
|
+
if (guestForErp) await syncContactToErp(guestForErp);
|
|
2835
4168
|
const cookies = parseCookies(req.headers.get("cookie"));
|
|
2836
4169
|
const guestToken = cookies[cookieName];
|
|
2837
4170
|
if (!guestToken) return json({ error: "Cart not found" }, { status: 400 });
|
|
2838
4171
|
cart = await cartRepo().findOne({
|
|
2839
4172
|
where: { guestToken },
|
|
2840
|
-
relations: [
|
|
4173
|
+
relations: [...CART_CHECKOUT_RELATIONS]
|
|
2841
4174
|
});
|
|
2842
4175
|
}
|
|
2843
4176
|
if (!cart || !(cart.items || []).length) {
|
|
2844
4177
|
return json({ error: "Cart is empty" }, { status: 400 });
|
|
2845
4178
|
}
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
for (const it of cart.items || []) {
|
|
2849
|
-
const p = it.product;
|
|
2850
|
-
if (!p || p.deleted || p.status !== "available") continue;
|
|
2851
|
-
const unit = Number(p.price);
|
|
2852
|
-
const qty = it.quantity || 1;
|
|
2853
|
-
const lineTotal = unit * qty;
|
|
2854
|
-
subtotal += lineTotal;
|
|
2855
|
-
lines.push({ productId: p.id, quantity: qty, unitPrice: unit, tax: 0, total: lineTotal });
|
|
2856
|
-
}
|
|
2857
|
-
if (!lines.length) return json({ error: "No available items in cart" }, { status: 400 });
|
|
2858
|
-
const total = subtotal;
|
|
4179
|
+
const prepOrd = await prepareCheckoutFromCart(b, cart, contactId);
|
|
4180
|
+
if (!prepOrd.ok) return json({ error: prepOrd.message }, { status: prepOrd.status });
|
|
2859
4181
|
const cartId = cart.id;
|
|
2860
4182
|
const ord = await orderRepo().save(
|
|
2861
4183
|
orderRepo().create({
|
|
2862
|
-
orderNumber:
|
|
4184
|
+
orderNumber: temporaryOrderNumberPlaceholder(),
|
|
4185
|
+
orderKind: "sale",
|
|
4186
|
+
parentOrderId: null,
|
|
2863
4187
|
contactId,
|
|
2864
|
-
billingAddressId:
|
|
2865
|
-
shippingAddressId:
|
|
4188
|
+
billingAddressId: prepOrd.billingAddressId,
|
|
4189
|
+
shippingAddressId: prepOrd.shippingAddressId,
|
|
2866
4190
|
status: "pending",
|
|
2867
|
-
subtotal,
|
|
2868
|
-
tax:
|
|
4191
|
+
subtotal: prepOrd.subtotal,
|
|
4192
|
+
tax: prepOrd.orderTax,
|
|
2869
4193
|
discount: 0,
|
|
2870
|
-
total,
|
|
4194
|
+
total: prepOrd.orderTotal,
|
|
2871
4195
|
currency: cart.currency || "INR",
|
|
2872
4196
|
metadata: { cartId }
|
|
2873
4197
|
})
|
|
2874
4198
|
);
|
|
2875
4199
|
const oid = ord.id;
|
|
2876
|
-
|
|
4200
|
+
await orderRepo().update(oid, {
|
|
4201
|
+
orderNumber: buildCanonicalOrderNumber("sale", oid, ord.createdAt ?? /* @__PURE__ */ new Date())
|
|
4202
|
+
});
|
|
4203
|
+
for (const line of prepOrd.lines) {
|
|
2877
4204
|
await orderItemRepo().save(
|
|
2878
4205
|
orderItemRepo().create({
|
|
2879
4206
|
orderId: oid,
|
|
@@ -2881,19 +4208,28 @@ function createStorefrontApiHandler(config) {
|
|
|
2881
4208
|
quantity: line.quantity,
|
|
2882
4209
|
unitPrice: line.unitPrice,
|
|
2883
4210
|
tax: line.tax,
|
|
2884
|
-
total: line.total
|
|
4211
|
+
total: line.total,
|
|
4212
|
+
hsn: line.hsn,
|
|
4213
|
+
uom: line.uom,
|
|
4214
|
+
productType: line.productType,
|
|
4215
|
+
taxRate: line.taxRate,
|
|
4216
|
+
taxCode: line.taxCode
|
|
2885
4217
|
})
|
|
2886
4218
|
);
|
|
2887
4219
|
}
|
|
2888
4220
|
return json({
|
|
2889
4221
|
orderId: oid,
|
|
2890
4222
|
orderNumber: ord.orderNumber,
|
|
2891
|
-
|
|
4223
|
+
subtotal: prepOrd.subtotal,
|
|
4224
|
+
tax: prepOrd.orderTax,
|
|
4225
|
+
total: prepOrd.orderTotal,
|
|
2892
4226
|
currency: cart.currency || "INR"
|
|
2893
4227
|
});
|
|
2894
4228
|
}
|
|
2895
4229
|
if (path[0] === "checkout" && path.length === 1 && method === "POST") {
|
|
2896
4230
|
const b = await req.json().catch(() => ({}));
|
|
4231
|
+
const capChk = await assertCaptchaOk(getCms, b, req, json);
|
|
4232
|
+
if (capChk) return capChk;
|
|
2897
4233
|
const u = await getSessionUser();
|
|
2898
4234
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
2899
4235
|
let contactId;
|
|
@@ -2904,11 +4240,11 @@ function createStorefrontApiHandler(config) {
|
|
|
2904
4240
|
contactId = contact.id;
|
|
2905
4241
|
cart = await cartRepo().findOne({
|
|
2906
4242
|
where: { contactId },
|
|
2907
|
-
relations: [
|
|
4243
|
+
relations: [...CART_CHECKOUT_RELATIONS]
|
|
2908
4244
|
});
|
|
2909
4245
|
} else {
|
|
2910
|
-
const email = (b.email
|
|
2911
|
-
const name = (b.name
|
|
4246
|
+
const email = String(b.email ?? "").trim();
|
|
4247
|
+
const name = String(b.name ?? "").trim();
|
|
2912
4248
|
if (!email || !name) return json({ error: "name and email required for guest checkout" }, { status: 400 });
|
|
2913
4249
|
let contact = await contactRepo().findOne({ where: { email, deleted: false } });
|
|
2914
4250
|
if (contact && contact.userId != null) {
|
|
@@ -2919,53 +4255,53 @@ function createStorefrontApiHandler(config) {
|
|
|
2919
4255
|
contactRepo().create({
|
|
2920
4256
|
name,
|
|
2921
4257
|
email,
|
|
2922
|
-
phone: b.phone
|
|
4258
|
+
phone: b.phone != null && b.phone !== "" ? String(b.phone) : null,
|
|
2923
4259
|
userId: null,
|
|
2924
4260
|
deleted: false
|
|
2925
4261
|
})
|
|
2926
4262
|
);
|
|
2927
|
-
} else if (name)
|
|
4263
|
+
} else if (name)
|
|
4264
|
+
await contactRepo().update(contact.id, {
|
|
4265
|
+
name,
|
|
4266
|
+
phone: b.phone != null && b.phone !== "" ? String(b.phone) : contact.phone
|
|
4267
|
+
});
|
|
2928
4268
|
contactId = contact.id;
|
|
4269
|
+
const guestForErp2 = await contactRepo().findOne({ where: { id: contactId } });
|
|
4270
|
+
if (guestForErp2) await syncContactToErp(guestForErp2);
|
|
2929
4271
|
const cookies = parseCookies(req.headers.get("cookie"));
|
|
2930
4272
|
const guestToken = cookies[cookieName];
|
|
2931
4273
|
if (!guestToken) return json({ error: "Cart not found" }, { status: 400 });
|
|
2932
4274
|
cart = await cartRepo().findOne({
|
|
2933
4275
|
where: { guestToken },
|
|
2934
|
-
relations: [
|
|
4276
|
+
relations: [...CART_CHECKOUT_RELATIONS]
|
|
2935
4277
|
});
|
|
2936
4278
|
}
|
|
2937
4279
|
if (!cart || !(cart.items || []).length) {
|
|
2938
4280
|
return json({ error: "Cart is empty" }, { status: 400 });
|
|
2939
4281
|
}
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
for (const it of cart.items || []) {
|
|
2943
|
-
const p = it.product;
|
|
2944
|
-
if (!p || p.deleted || p.status !== "available") continue;
|
|
2945
|
-
const unit = Number(p.price);
|
|
2946
|
-
const qty = it.quantity || 1;
|
|
2947
|
-
const lineTotal = unit * qty;
|
|
2948
|
-
subtotal += lineTotal;
|
|
2949
|
-
lines.push({ productId: p.id, quantity: qty, unitPrice: unit, tax: 0, total: lineTotal });
|
|
2950
|
-
}
|
|
2951
|
-
if (!lines.length) return json({ error: "No available items in cart" }, { status: 400 });
|
|
2952
|
-
const total = subtotal;
|
|
4282
|
+
const prepChk = await prepareCheckoutFromCart(b, cart, contactId);
|
|
4283
|
+
if (!prepChk.ok) return json({ error: prepChk.message }, { status: prepChk.status });
|
|
2953
4284
|
const ord = await orderRepo().save(
|
|
2954
4285
|
orderRepo().create({
|
|
2955
|
-
orderNumber:
|
|
4286
|
+
orderNumber: temporaryOrderNumberPlaceholder(),
|
|
4287
|
+
orderKind: "sale",
|
|
4288
|
+
parentOrderId: null,
|
|
2956
4289
|
contactId,
|
|
2957
|
-
billingAddressId:
|
|
2958
|
-
shippingAddressId:
|
|
4290
|
+
billingAddressId: prepChk.billingAddressId,
|
|
4291
|
+
shippingAddressId: prepChk.shippingAddressId,
|
|
2959
4292
|
status: "pending",
|
|
2960
|
-
subtotal,
|
|
2961
|
-
tax:
|
|
4293
|
+
subtotal: prepChk.subtotal,
|
|
4294
|
+
tax: prepChk.orderTax,
|
|
2962
4295
|
discount: 0,
|
|
2963
|
-
total,
|
|
4296
|
+
total: prepChk.orderTotal,
|
|
2964
4297
|
currency: cart.currency || "INR"
|
|
2965
4298
|
})
|
|
2966
4299
|
);
|
|
2967
4300
|
const oid = ord.id;
|
|
2968
|
-
|
|
4301
|
+
await orderRepo().update(oid, {
|
|
4302
|
+
orderNumber: buildCanonicalOrderNumber("sale", oid, ord.createdAt ?? /* @__PURE__ */ new Date())
|
|
4303
|
+
});
|
|
4304
|
+
for (const line of prepChk.lines) {
|
|
2969
4305
|
await orderItemRepo().save(
|
|
2970
4306
|
orderItemRepo().create({
|
|
2971
4307
|
orderId: oid,
|
|
@@ -2973,7 +4309,12 @@ function createStorefrontApiHandler(config) {
|
|
|
2973
4309
|
quantity: line.quantity,
|
|
2974
4310
|
unitPrice: line.unitPrice,
|
|
2975
4311
|
tax: line.tax,
|
|
2976
|
-
total: line.total
|
|
4312
|
+
total: line.total,
|
|
4313
|
+
hsn: line.hsn,
|
|
4314
|
+
uom: line.uom,
|
|
4315
|
+
productType: line.productType,
|
|
4316
|
+
taxRate: line.taxRate,
|
|
4317
|
+
taxCode: line.taxCode
|
|
2977
4318
|
})
|
|
2978
4319
|
);
|
|
2979
4320
|
}
|
|
@@ -2982,7 +4323,9 @@ function createStorefrontApiHandler(config) {
|
|
|
2982
4323
|
return json({
|
|
2983
4324
|
orderId: oid,
|
|
2984
4325
|
orderNumber: ord.orderNumber,
|
|
2985
|
-
|
|
4326
|
+
subtotal: prepChk.subtotal,
|
|
4327
|
+
tax: prepChk.orderTax,
|
|
4328
|
+
total: prepChk.orderTotal
|
|
2986
4329
|
});
|
|
2987
4330
|
}
|
|
2988
4331
|
if (path[0] === "orders" && path.length === 1 && method === "GET") {
|
|
@@ -2992,7 +4335,7 @@ function createStorefrontApiHandler(config) {
|
|
|
2992
4335
|
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
2993
4336
|
if (!contact) return json({ orders: [] });
|
|
2994
4337
|
const orders = await orderRepo().find({
|
|
2995
|
-
where: { contactId: contact.id, deleted: false },
|
|
4338
|
+
where: { contactId: contact.id, deleted: false, orderKind: "sale" },
|
|
2996
4339
|
order: { createdAt: "DESC" },
|
|
2997
4340
|
take: 50
|
|
2998
4341
|
});
|
|
@@ -3027,6 +4370,20 @@ function createStorefrontApiHandler(config) {
|
|
|
3027
4370
|
})
|
|
3028
4371
|
});
|
|
3029
4372
|
}
|
|
4373
|
+
if (path[0] === "orders" && path.length === 3 && path[2] === "invoice" && method === "GET") {
|
|
4374
|
+
const u = await getSessionUser();
|
|
4375
|
+
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
4376
|
+
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
4377
|
+
if (!getCms) return json({ error: "Not found" }, { status: 404 });
|
|
4378
|
+
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
4379
|
+
if (!contact) return json({ error: "Not found" }, { status: 404 });
|
|
4380
|
+
const orderId = parseInt(path[1], 10);
|
|
4381
|
+
if (!Number.isFinite(orderId)) return json({ error: "Invalid id" }, { status: 400 });
|
|
4382
|
+
const cms = await getCms();
|
|
4383
|
+
return streamOrderInvoicePdf(cms, dataSource, entityMap, orderId, {
|
|
4384
|
+
ownerContactId: contact.id
|
|
4385
|
+
});
|
|
4386
|
+
}
|
|
3030
4387
|
if (path[0] === "orders" && path.length === 2 && method === "GET") {
|
|
3031
4388
|
const u = await getSessionUser();
|
|
3032
4389
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
@@ -3034,11 +4391,20 @@ function createStorefrontApiHandler(config) {
|
|
|
3034
4391
|
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
3035
4392
|
if (!contact) return json({ error: "Not found" }, { status: 404 });
|
|
3036
4393
|
const orderId = parseInt(path[1], 10);
|
|
3037
|
-
|
|
4394
|
+
let order = await orderRepo().findOne({
|
|
3038
4395
|
where: { id: orderId, contactId: contact.id, deleted: false },
|
|
3039
4396
|
relations: ["items", "items.product"]
|
|
3040
4397
|
});
|
|
3041
4398
|
if (!order) return json({ error: "Not found" }, { status: 404 });
|
|
4399
|
+
if (getCms) {
|
|
4400
|
+
const cms = await getCms();
|
|
4401
|
+
await tryRefreshOrderFromErpForStorefront(cms, dataSource, entityMap, order);
|
|
4402
|
+
order = await orderRepo().findOne({
|
|
4403
|
+
where: { id: orderId, contactId: contact.id, deleted: false },
|
|
4404
|
+
relations: ["items", "items.product"]
|
|
4405
|
+
});
|
|
4406
|
+
}
|
|
4407
|
+
if (!order) return json({ error: "Not found" }, { status: 404 });
|
|
3042
4408
|
const o = order;
|
|
3043
4409
|
const lines = (o.items || []).map((line) => {
|
|
3044
4410
|
const p = line.product;
|
|
@@ -3057,10 +4423,22 @@ function createStorefrontApiHandler(config) {
|
|
|
3057
4423
|
} : null
|
|
3058
4424
|
};
|
|
3059
4425
|
});
|
|
4426
|
+
const kind = o.orderKind || "sale";
|
|
4427
|
+
let relatedOrders = [];
|
|
4428
|
+
if (kind === "sale") {
|
|
4429
|
+
relatedOrders = await orderRepo().find({
|
|
4430
|
+
where: { parentOrderId: orderId, deleted: false },
|
|
4431
|
+
order: { id: "ASC" }
|
|
4432
|
+
});
|
|
4433
|
+
}
|
|
4434
|
+
const meta = o.metadata;
|
|
4435
|
+
const fulfillmentPreview = meta && typeof meta.fulfillment === "object" && meta.fulfillment && "status" in meta.fulfillment ? String(meta.fulfillment.status ?? "") : "";
|
|
3060
4436
|
return json({
|
|
3061
4437
|
order: {
|
|
3062
4438
|
id: o.id,
|
|
3063
4439
|
orderNumber: o.orderNumber,
|
|
4440
|
+
orderKind: kind,
|
|
4441
|
+
parentOrderId: o.parentOrderId ?? null,
|
|
3064
4442
|
status: o.status,
|
|
3065
4443
|
subtotal: o.subtotal,
|
|
3066
4444
|
tax: o.tax,
|
|
@@ -3068,8 +4446,18 @@ function createStorefrontApiHandler(config) {
|
|
|
3068
4446
|
total: o.total,
|
|
3069
4447
|
currency: o.currency,
|
|
3070
4448
|
createdAt: o.createdAt,
|
|
4449
|
+
metadata: o.metadata ?? null,
|
|
3071
4450
|
items: lines
|
|
3072
|
-
}
|
|
4451
|
+
},
|
|
4452
|
+
relatedOrders: relatedOrders.map((r) => ({
|
|
4453
|
+
id: r.id,
|
|
4454
|
+
orderNumber: r.orderNumber,
|
|
4455
|
+
orderKind: r.orderKind ?? "return",
|
|
4456
|
+
status: r.status,
|
|
4457
|
+
createdAt: r.createdAt,
|
|
4458
|
+
fulfillmentStatus: r.metadata && typeof r.metadata === "object" && r.metadata.fulfillment?.status
|
|
4459
|
+
})),
|
|
4460
|
+
fulfillmentPreview: fulfillmentPreview || void 0
|
|
3073
4461
|
});
|
|
3074
4462
|
}
|
|
3075
4463
|
return json({ error: "Not found" }, { status: 404 });
|