@infuro/cms-core 1.0.12 → 1.0.14
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 +1095 -541
- package/dist/admin.cjs.map +1 -1
- package/dist/admin.js +939 -385
- package/dist/admin.js.map +1 -1
- package/dist/api.cjs +1111 -55
- 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 +1110 -54
- 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-Be8NLxu-.d.cts} +28 -4
- package/dist/{index-C4Yl7js9.d.ts → index-CjBf9dAb.d.ts} +28 -4
- package/dist/index.cjs +2983 -914
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +345 -54
- package/dist/index.d.ts +345 -54
- package/dist/index.js +2783 -737
- 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/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -8,6 +8,23 @@ var __export = (target, all) => {
|
|
|
8
8
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
// src/plugins/erp/erp-config-enabled.ts
|
|
12
|
+
async function isErpIntegrationEnabled(cms, dataSource, entityMap) {
|
|
13
|
+
if (!cms.getPlugin("erp")) return false;
|
|
14
|
+
const configRepo = dataSource.getRepository(entityMap.configs);
|
|
15
|
+
const cfgRows = await configRepo.find({ where: { settings: "erp", deleted: false } });
|
|
16
|
+
for (const row of cfgRows) {
|
|
17
|
+
const r = row;
|
|
18
|
+
if (r.key === "enabled" && r.value === "false") return false;
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
var init_erp_config_enabled = __esm({
|
|
23
|
+
"src/plugins/erp/erp-config-enabled.ts"() {
|
|
24
|
+
"use strict";
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
11
28
|
// src/plugins/email/email-queue.ts
|
|
12
29
|
var email_queue_exports = {};
|
|
13
30
|
__export(email_queue_exports, {
|
|
@@ -46,9 +63,9 @@ async function queueEmail(cms, payload) {
|
|
|
46
63
|
}
|
|
47
64
|
}
|
|
48
65
|
async function queueOrderPlacedEmails(cms, payload) {
|
|
49
|
-
const { orderNumber
|
|
66
|
+
const { orderNumber, total, currency, customerName, customerEmail, salesTeamEmails, companyDetails, lineItems } = payload;
|
|
50
67
|
const base = {
|
|
51
|
-
orderNumber
|
|
68
|
+
orderNumber,
|
|
52
69
|
total: total != null ? String(total) : void 0,
|
|
53
70
|
currency,
|
|
54
71
|
customerName,
|
|
@@ -96,8 +113,212 @@ var init_email_queue = __esm({
|
|
|
96
113
|
}
|
|
97
114
|
});
|
|
98
115
|
|
|
116
|
+
// src/plugins/erp/erp-response-map.ts
|
|
117
|
+
function pickString(o, keys) {
|
|
118
|
+
for (const k of keys) {
|
|
119
|
+
const v = o[k];
|
|
120
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
121
|
+
}
|
|
122
|
+
return void 0;
|
|
123
|
+
}
|
|
124
|
+
function unwrapErpReadData(json) {
|
|
125
|
+
if (!json || typeof json !== "object") return null;
|
|
126
|
+
const o = json;
|
|
127
|
+
const d = o.data;
|
|
128
|
+
if (d && typeof d === "object" && !Array.isArray(d)) return d;
|
|
129
|
+
return o;
|
|
130
|
+
}
|
|
131
|
+
function firstObject(data, keys) {
|
|
132
|
+
for (const k of keys) {
|
|
133
|
+
const v = data[k];
|
|
134
|
+
if (v && typeof v === "object" && !Array.isArray(v)) return v;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
function extractEvents(src) {
|
|
139
|
+
const timeline = src.timeline ?? src.events ?? src.history ?? src.trackingEvents;
|
|
140
|
+
if (!Array.isArray(timeline) || !timeline.length) return void 0;
|
|
141
|
+
const events = [];
|
|
142
|
+
for (const row of timeline) {
|
|
143
|
+
if (!row || typeof row !== "object") continue;
|
|
144
|
+
const r = row;
|
|
145
|
+
const at = pickString(r, ["at", "timestamp", "date", "occurredAt"]) ?? (r.at instanceof Date ? r.at.toISOString() : void 0);
|
|
146
|
+
const label = pickString(r, ["label", "status", "title", "message", "description"]);
|
|
147
|
+
const detail = pickString(r, ["detail", "notes", "description"]);
|
|
148
|
+
if (at || label || detail) events.push({ at, label, detail });
|
|
149
|
+
}
|
|
150
|
+
return events.length ? events : void 0;
|
|
151
|
+
}
|
|
152
|
+
function mapErpPayloadToFulfillment(data) {
|
|
153
|
+
const nested = firstObject(data, ["fulfillment", "packaging", "shipment", "shipping", "delivery"]);
|
|
154
|
+
const src = nested || data;
|
|
155
|
+
const status = pickString(src, ["status", "fulfillmentStatus", "state", "label", "packagingStatus"]);
|
|
156
|
+
const trackingId = pickString(src, ["trackingId", "tracking_id", "trackingNumber", "awb", "trackingUrl"]);
|
|
157
|
+
const events = extractEvents(src);
|
|
158
|
+
if (!status && !trackingId && !(events && events.length)) return void 0;
|
|
159
|
+
return { status, trackingId, events };
|
|
160
|
+
}
|
|
161
|
+
function mapErpPayloadToInvoiceNumber(data) {
|
|
162
|
+
const nested = firstObject(data, ["invoice", "latestInvoice", "postedInvoice"]);
|
|
163
|
+
const src = nested || data;
|
|
164
|
+
return pickString(src, ["invoiceNumber", "invoice_number", "number", "name", "id"]);
|
|
165
|
+
}
|
|
166
|
+
function extractChildOrderRefsFromSalePayload(data) {
|
|
167
|
+
const lists = [data.returns, data.returnOrders, data.relatedReturns, data.childOrders, data.children];
|
|
168
|
+
const seen = /* @__PURE__ */ new Set();
|
|
169
|
+
const out = [];
|
|
170
|
+
for (const list of lists) {
|
|
171
|
+
if (!Array.isArray(list)) continue;
|
|
172
|
+
for (const item of list) {
|
|
173
|
+
if (!item || typeof item !== "object") continue;
|
|
174
|
+
const o = item;
|
|
175
|
+
const ref = pickString(o, ["platformReturnId", "platform_return_id", "refId", "ref_id"]) ?? (typeof o.id === "string" ? o.id : void 0);
|
|
176
|
+
if (!ref || seen.has(ref)) continue;
|
|
177
|
+
seen.add(ref);
|
|
178
|
+
const t = (pickString(o, ["kind", "type", "orderKind"]) || "").toLowerCase();
|
|
179
|
+
const orderKind = /replac/.test(t) ? "replacement" : "return";
|
|
180
|
+
out.push({ ref, orderKind });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
var init_erp_response_map = __esm({
|
|
186
|
+
"src/plugins/erp/erp-response-map.ts"() {
|
|
187
|
+
"use strict";
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// src/plugins/erp/erp-order-invoice.ts
|
|
192
|
+
var erp_order_invoice_exports = {};
|
|
193
|
+
__export(erp_order_invoice_exports, {
|
|
194
|
+
streamOrderInvoicePdf: () => streamOrderInvoicePdf
|
|
195
|
+
});
|
|
196
|
+
function pickInvoiceId(data) {
|
|
197
|
+
const nested = data.invoice && typeof data.invoice === "object" && !Array.isArray(data.invoice) ? data.invoice : null;
|
|
198
|
+
const src = nested || data;
|
|
199
|
+
for (const k of ["invoiceId", "invoice_id", "id"]) {
|
|
200
|
+
const v = src[k];
|
|
201
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
202
|
+
}
|
|
203
|
+
return void 0;
|
|
204
|
+
}
|
|
205
|
+
async function streamOrderInvoicePdf(cms, dataSource, entityMap, orderId, options) {
|
|
206
|
+
const jsonErr = (msg, status) => new Response(JSON.stringify({ error: msg }), {
|
|
207
|
+
status,
|
|
208
|
+
headers: { "Content-Type": "application/json" }
|
|
209
|
+
});
|
|
210
|
+
const on = await isErpIntegrationEnabled(cms, dataSource, entityMap);
|
|
211
|
+
if (!on) return jsonErr("Invoice not available", 503);
|
|
212
|
+
const erp = cms.getPlugin("erp");
|
|
213
|
+
if (!erp?.submission) return jsonErr("Invoice not available", 503);
|
|
214
|
+
const orderRepo = dataSource.getRepository(entityMap.orders);
|
|
215
|
+
const order = await orderRepo.findOne({ where: { id: orderId, deleted: false } });
|
|
216
|
+
if (!order) return jsonErr("Not found", 404);
|
|
217
|
+
const kind = order.orderKind || "sale";
|
|
218
|
+
if (kind !== "sale") return jsonErr("Invoice only for sale orders", 400);
|
|
219
|
+
if (options.ownerContactId != null && order.contactId !== options.ownerContactId) {
|
|
220
|
+
return jsonErr("Not found", 404);
|
|
221
|
+
}
|
|
222
|
+
const meta = order.metadata && typeof order.metadata === "object" && !Array.isArray(order.metadata) ? order.metadata : {};
|
|
223
|
+
const inv = meta.invoice && typeof meta.invoice === "object" && !Array.isArray(meta.invoice) ? meta.invoice : {};
|
|
224
|
+
let invoiceId = typeof inv.invoiceId === "string" ? inv.invoiceId.trim() : "";
|
|
225
|
+
if (!invoiceId) {
|
|
226
|
+
const refId = String(order.orderNumber || "");
|
|
227
|
+
const r = await erp.submission.postErpReadAction("get-invoice", { platformOrderId: refId });
|
|
228
|
+
const d = r.ok ? unwrapErpReadData(r.json) : null;
|
|
229
|
+
invoiceId = d ? pickInvoiceId(d) || "" : "";
|
|
230
|
+
}
|
|
231
|
+
if (!invoiceId) return jsonErr("Invoice not ready", 404);
|
|
232
|
+
const pdf = await erp.submission.fetchInvoicePdf(invoiceId);
|
|
233
|
+
if (!pdf.ok || !pdf.buffer) return jsonErr(pdf.error || "PDF fetch failed", 502);
|
|
234
|
+
const filename = `invoice-${orderId}.pdf`;
|
|
235
|
+
return new Response(pdf.buffer, {
|
|
236
|
+
status: 200,
|
|
237
|
+
headers: {
|
|
238
|
+
"Content-Type": pdf.contentType || "application/pdf",
|
|
239
|
+
"Content-Disposition": `attachment; filename="${filename}"`
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
var init_erp_order_invoice = __esm({
|
|
244
|
+
"src/plugins/erp/erp-order-invoice.ts"() {
|
|
245
|
+
"use strict";
|
|
246
|
+
init_erp_response_map();
|
|
247
|
+
init_erp_config_enabled();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
99
251
|
// src/api/crud.ts
|
|
100
252
|
import { ILike, Like, MoreThan } from "typeorm";
|
|
253
|
+
|
|
254
|
+
// src/plugins/erp/erp-queue.ts
|
|
255
|
+
var ERP_QUEUE_NAME = "erp";
|
|
256
|
+
async function queueErp(cms, payload) {
|
|
257
|
+
const queue = cms.getPlugin("queue");
|
|
258
|
+
if (!queue) return;
|
|
259
|
+
await queue.add(ERP_QUEUE_NAME, payload);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/plugins/erp/erp-contact-sync.ts
|
|
263
|
+
function splitName(full) {
|
|
264
|
+
const t = (full || "").trim();
|
|
265
|
+
if (!t) return { firstName: "Contact", lastName: "" };
|
|
266
|
+
const parts = t.split(/\s+/);
|
|
267
|
+
if (parts.length === 1) return { firstName: parts[0], lastName: "" };
|
|
268
|
+
return { firstName: parts[0], lastName: parts.slice(1).join(" ") };
|
|
269
|
+
}
|
|
270
|
+
async function queueErpCreateContactIfEnabled(cms, dataSource, entityMap, input) {
|
|
271
|
+
try {
|
|
272
|
+
const configRepo = dataSource.getRepository(entityMap.configs);
|
|
273
|
+
const cfgRows = await configRepo.find({ where: { settings: "erp", deleted: false } });
|
|
274
|
+
for (const row of cfgRows) {
|
|
275
|
+
const r = row;
|
|
276
|
+
if (r.key === "enabled" && r.value === "false") return;
|
|
277
|
+
}
|
|
278
|
+
if (!cms.getPlugin("erp")) return;
|
|
279
|
+
const email = (input.email ?? "").trim();
|
|
280
|
+
if (!email) return;
|
|
281
|
+
const { firstName, lastName } = splitName(input.name);
|
|
282
|
+
await queueErp(cms, {
|
|
283
|
+
kind: "createContact",
|
|
284
|
+
contact: {
|
|
285
|
+
email,
|
|
286
|
+
firstName,
|
|
287
|
+
lastName,
|
|
288
|
+
phone: input.phone?.trim() || void 0,
|
|
289
|
+
companyName: input.company?.trim() || void 0,
|
|
290
|
+
type: input.type?.trim() || void 0,
|
|
291
|
+
notes: input.notes?.trim() || void 0,
|
|
292
|
+
tags: input.tags?.length ? [...input.tags] : void 0
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
} catch {
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/plugins/erp/erp-product-sync.ts
|
|
300
|
+
init_erp_config_enabled();
|
|
301
|
+
async function queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, product) {
|
|
302
|
+
try {
|
|
303
|
+
const sku = typeof product.sku === "string" ? product.sku.trim() : "";
|
|
304
|
+
if (!sku) return;
|
|
305
|
+
const on = await isErpIntegrationEnabled(cms, dataSource, entityMap);
|
|
306
|
+
if (!on) return;
|
|
307
|
+
const payload = {
|
|
308
|
+
sku,
|
|
309
|
+
title: product.name || sku,
|
|
310
|
+
name: product.name,
|
|
311
|
+
description: typeof product.metadata === "object" && product.metadata && "description" in product.metadata ? String(product.metadata.description ?? "") : void 0,
|
|
312
|
+
hsn_number: product.hsn,
|
|
313
|
+
is_active: product.status === "available",
|
|
314
|
+
metadata: product.metadata ?? void 0
|
|
315
|
+
};
|
|
316
|
+
await queueErp(cms, { kind: "productUpsert", product: payload });
|
|
317
|
+
} catch {
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/api/crud.ts
|
|
101
322
|
var DATE_COLUMN_TYPES = /* @__PURE__ */ new Set([
|
|
102
323
|
"date",
|
|
103
324
|
"datetime",
|
|
@@ -150,8 +371,27 @@ function buildSearchWhereClause(repo, search) {
|
|
|
150
371
|
if (ors.length === 0) return {};
|
|
151
372
|
return ors.length === 1 ? ors[0] : ors;
|
|
152
373
|
}
|
|
374
|
+
function makeContactErpSync(dataSource, entityMap, getCms) {
|
|
375
|
+
return async function syncContactRowToErp(row) {
|
|
376
|
+
if (!getCms) return;
|
|
377
|
+
try {
|
|
378
|
+
const cms = await getCms();
|
|
379
|
+
const c = row;
|
|
380
|
+
await queueErpCreateContactIfEnabled(cms, dataSource, entityMap, {
|
|
381
|
+
name: c.name,
|
|
382
|
+
email: c.email,
|
|
383
|
+
phone: c.phone,
|
|
384
|
+
type: c.type,
|
|
385
|
+
company: c.company,
|
|
386
|
+
notes: c.notes
|
|
387
|
+
});
|
|
388
|
+
} catch {
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
153
392
|
function createCrudHandler(dataSource, entityMap, options) {
|
|
154
|
-
const { requireAuth, json, requireEntityPermission: reqPerm } = options;
|
|
393
|
+
const { requireAuth, json, requireEntityPermission: reqPerm, getCms } = options;
|
|
394
|
+
const syncContactRowToErp = makeContactErpSync(dataSource, entityMap, getCms);
|
|
155
395
|
async function authz(req, resource, action) {
|
|
156
396
|
const authError = await requireAuth(req);
|
|
157
397
|
if (authError) return authError;
|
|
@@ -353,6 +593,13 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
353
593
|
const repo = dataSource.getRepository(entity);
|
|
354
594
|
sanitizeBodyForEntity(repo, body);
|
|
355
595
|
const created = await repo.save(repo.create(body));
|
|
596
|
+
if (resource === "contacts") {
|
|
597
|
+
await syncContactRowToErp(created);
|
|
598
|
+
}
|
|
599
|
+
if (resource === "products" && getCms) {
|
|
600
|
+
const cms = await getCms();
|
|
601
|
+
await queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, created);
|
|
602
|
+
}
|
|
356
603
|
return json(created, { status: 201 });
|
|
357
604
|
},
|
|
358
605
|
async GET_METADATA(req, resource) {
|
|
@@ -459,7 +706,8 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
459
706
|
};
|
|
460
707
|
}
|
|
461
708
|
function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
462
|
-
const { requireAuth, json, requireEntityPermission: reqPerm } = options;
|
|
709
|
+
const { requireAuth, json, requireEntityPermission: reqPerm, getCms } = options;
|
|
710
|
+
const syncContactRowToErp = makeContactErpSync(dataSource, entityMap, getCms);
|
|
463
711
|
async function authz(req, resource, action) {
|
|
464
712
|
const authError = await requireAuth(req);
|
|
465
713
|
if (authError) return authError;
|
|
@@ -482,7 +730,11 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
482
730
|
relations: ["contact", "billingAddress", "shippingAddress", "items", "items.product", "items.product.collection", "payments"]
|
|
483
731
|
});
|
|
484
732
|
if (!order) return json({ message: "Not found" }, { status: 404 });
|
|
485
|
-
|
|
733
|
+
const relatedOrders = await repo.find({
|
|
734
|
+
where: { parentOrderId: Number(id), deleted: false },
|
|
735
|
+
order: { id: "ASC" }
|
|
736
|
+
});
|
|
737
|
+
return json({ ...order, relatedOrders });
|
|
486
738
|
}
|
|
487
739
|
if (resource === "contacts") {
|
|
488
740
|
const contact = await repo.findOne({
|
|
@@ -615,6 +867,13 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
615
867
|
await repo.update(numericId, updatePayload);
|
|
616
868
|
}
|
|
617
869
|
const updated = await repo.findOne({ where: { id: numericId } });
|
|
870
|
+
if (resource === "contacts" && updated) {
|
|
871
|
+
await syncContactRowToErp(updated);
|
|
872
|
+
}
|
|
873
|
+
if (resource === "products" && updated && getCms) {
|
|
874
|
+
const cms = await getCms();
|
|
875
|
+
await queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, updated);
|
|
876
|
+
}
|
|
618
877
|
return updated ? json(updated) : json({ message: "Not found" }, { status: 404 });
|
|
619
878
|
},
|
|
620
879
|
async DELETE(req, resource, id) {
|
|
@@ -779,6 +1038,24 @@ function createUserAuthApiRouter(config) {
|
|
|
779
1038
|
// src/api/cms-handlers.ts
|
|
780
1039
|
init_email_queue();
|
|
781
1040
|
import { MoreThanOrEqual, ILike as ILike2 } from "typeorm";
|
|
1041
|
+
|
|
1042
|
+
// src/plugins/captcha/assert.ts
|
|
1043
|
+
async function assertCaptchaOk(getCms, body, req, json) {
|
|
1044
|
+
if (!getCms) return null;
|
|
1045
|
+
let cms;
|
|
1046
|
+
try {
|
|
1047
|
+
cms = await getCms();
|
|
1048
|
+
} catch {
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
const svc = cms.getPlugin("captcha");
|
|
1052
|
+
if (!svc || typeof svc.verify !== "function") return null;
|
|
1053
|
+
const result = await svc.verify(body, req);
|
|
1054
|
+
if (result.ok) return null;
|
|
1055
|
+
return json({ error: result.message }, { status: result.status });
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// src/api/cms-handlers.ts
|
|
782
1059
|
function createDashboardStatsHandler(config) {
|
|
783
1060
|
const { dataSource, entityMap, json, requireAuth, requirePermission, requireEntityPermission } = config;
|
|
784
1061
|
return async function GET(req) {
|
|
@@ -1020,6 +1297,32 @@ function createFormSaveHandlers(config) {
|
|
|
1020
1297
|
}
|
|
1021
1298
|
};
|
|
1022
1299
|
}
|
|
1300
|
+
async function isErpIntegrationEnabled2(dataSource, entityMap) {
|
|
1301
|
+
const repo = dataSource.getRepository(entityMap.configs);
|
|
1302
|
+
const rows = await repo.find({ where: { settings: "erp", deleted: false } });
|
|
1303
|
+
for (const row of rows) {
|
|
1304
|
+
const r = row;
|
|
1305
|
+
if (r.key === "enabled") return r.value !== "false";
|
|
1306
|
+
}
|
|
1307
|
+
return true;
|
|
1308
|
+
}
|
|
1309
|
+
async function getErpOpportunityFormIds(dataSource, entityMap) {
|
|
1310
|
+
const repo = dataSource.getRepository(entityMap.configs);
|
|
1311
|
+
const row = await repo.findOne({
|
|
1312
|
+
where: { settings: "erp", key: "opportunityFormIds", deleted: false }
|
|
1313
|
+
});
|
|
1314
|
+
if (!row) return null;
|
|
1315
|
+
const raw = (row.value ?? "").trim();
|
|
1316
|
+
if (!raw) return [];
|
|
1317
|
+
try {
|
|
1318
|
+
const parsed = JSON.parse(raw);
|
|
1319
|
+
if (!Array.isArray(parsed)) return [];
|
|
1320
|
+
const ids = parsed.map((x) => typeof x === "number" ? x : Number(x)).filter((n) => Number.isInteger(n) && n > 0);
|
|
1321
|
+
return [...new Set(ids)];
|
|
1322
|
+
} catch {
|
|
1323
|
+
return [];
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1023
1326
|
function createFormSubmissionGetByIdHandler(config) {
|
|
1024
1327
|
const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
|
|
1025
1328
|
return async function GET(req, id) {
|
|
@@ -1108,13 +1411,15 @@ function pickContactFromSubmission(fields, data) {
|
|
|
1108
1411
|
return { name: name || email, email, phone: phone || null };
|
|
1109
1412
|
}
|
|
1110
1413
|
function createFormSubmissionHandler(config) {
|
|
1111
|
-
const { dataSource, entityMap, json } = config;
|
|
1414
|
+
const { dataSource, entityMap, json, getCms } = config;
|
|
1112
1415
|
return async function POST(req) {
|
|
1113
1416
|
try {
|
|
1114
1417
|
const body = await req.json();
|
|
1115
1418
|
if (!body || typeof body !== "object") {
|
|
1116
1419
|
return json({ error: "Invalid request payload" }, { status: 400 });
|
|
1117
1420
|
}
|
|
1421
|
+
const captchaErr = await assertCaptchaOk(getCms, body, req, json);
|
|
1422
|
+
if (captchaErr) return captchaErr;
|
|
1118
1423
|
const formId = typeof body.formId === "number" ? body.formId : Number(body.formId);
|
|
1119
1424
|
if (!Number.isInteger(formId) || formId <= 0) {
|
|
1120
1425
|
return json({ error: "formId is required and must be a positive integer" }, { status: 400 });
|
|
@@ -1181,28 +1486,44 @@ function createFormSubmissionHandler(config) {
|
|
|
1181
1486
|
contactEmail = contactData.email;
|
|
1182
1487
|
}
|
|
1183
1488
|
}
|
|
1184
|
-
if (config.getCms
|
|
1489
|
+
if (config.getCms) {
|
|
1185
1490
|
try {
|
|
1186
1491
|
const cms = await config.getCms();
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1492
|
+
if (config.getCompanyDetails && config.getRecipientForChannel) {
|
|
1493
|
+
const to = await config.getRecipientForChannel("crm");
|
|
1494
|
+
if (to) {
|
|
1495
|
+
const companyDetails = await config.getCompanyDetails();
|
|
1496
|
+
const formFieldRows = activeFields.map((f) => ({
|
|
1497
|
+
label: f.label && String(f.label).trim() || `Field ${f.id}`,
|
|
1498
|
+
value: formatSubmissionFieldValue(data[String(f.id)])
|
|
1499
|
+
}));
|
|
1500
|
+
await queueEmail(cms, {
|
|
1501
|
+
to,
|
|
1502
|
+
templateName: "formSubmission",
|
|
1503
|
+
ctx: {
|
|
1504
|
+
formName,
|
|
1505
|
+
contactName,
|
|
1506
|
+
contactEmail,
|
|
1507
|
+
formData: data,
|
|
1508
|
+
formFieldRows,
|
|
1509
|
+
companyDetails: companyDetails ?? {}
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
if (await isErpIntegrationEnabled2(dataSource, entityMap)) {
|
|
1515
|
+
const erp = cms.getPlugin("erp");
|
|
1516
|
+
if (erp) {
|
|
1517
|
+
const contact = erp.submission.extractContactData(data, activeFields);
|
|
1518
|
+
if (contact?.email?.trim()) {
|
|
1519
|
+
const opportunityFormIds = await getErpOpportunityFormIds(dataSource, entityMap);
|
|
1520
|
+
const asOpportunity = opportunityFormIds != null && opportunityFormIds.length > 0 && opportunityFormIds.includes(formId);
|
|
1521
|
+
await queueErp(
|
|
1522
|
+
cms,
|
|
1523
|
+
asOpportunity ? { kind: "formOpportunity", contact } : { kind: "lead", contact }
|
|
1524
|
+
);
|
|
1204
1525
|
}
|
|
1205
|
-
}
|
|
1526
|
+
}
|
|
1206
1527
|
}
|
|
1207
1528
|
} catch {
|
|
1208
1529
|
}
|
|
@@ -1648,6 +1969,125 @@ ${contextParts.join("\n\n")}` : "You are a helpful assistant for the company. If
|
|
|
1648
1969
|
};
|
|
1649
1970
|
}
|
|
1650
1971
|
|
|
1972
|
+
// src/message-templates/sms-defaults.ts
|
|
1973
|
+
var SMS_MESSAGE_TEMPLATE_DEFAULTS = [
|
|
1974
|
+
{
|
|
1975
|
+
templateKey: "auth.otp_login",
|
|
1976
|
+
name: "Sign-in OTP (SMS)",
|
|
1977
|
+
body: "Your sign-in code is {{code}}. Valid 10 minutes.",
|
|
1978
|
+
providerMeta: { otpVarKey: "var1" }
|
|
1979
|
+
},
|
|
1980
|
+
{
|
|
1981
|
+
templateKey: "auth.otp_verify_phone",
|
|
1982
|
+
name: "Verify phone OTP (SMS)",
|
|
1983
|
+
body: "Your verification code is {{code}}. Valid 10 minutes.",
|
|
1984
|
+
providerMeta: { otpVarKey: "var1" }
|
|
1985
|
+
}
|
|
1986
|
+
];
|
|
1987
|
+
function getSmsTemplateDefault(templateKey) {
|
|
1988
|
+
return SMS_MESSAGE_TEMPLATE_DEFAULTS.find((d) => d.templateKey === templateKey);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
// src/api/message-template-admin-handlers.ts
|
|
1992
|
+
function createSmsMessageTemplateHandlers(config) {
|
|
1993
|
+
const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
|
|
1994
|
+
const repo = () => dataSource.getRepository(entityMap.message_templates);
|
|
1995
|
+
async function requireSettingsRead(req) {
|
|
1996
|
+
const a = await requireAuth(req);
|
|
1997
|
+
if (a) return a;
|
|
1998
|
+
if (requireEntityPermission) {
|
|
1999
|
+
const pe = await requireEntityPermission(req, "settings", "read");
|
|
2000
|
+
if (pe) return pe;
|
|
2001
|
+
}
|
|
2002
|
+
return null;
|
|
2003
|
+
}
|
|
2004
|
+
async function requireSettingsUpdate(req) {
|
|
2005
|
+
const a = await requireAuth(req);
|
|
2006
|
+
if (a) return a;
|
|
2007
|
+
if (requireEntityPermission) {
|
|
2008
|
+
const pe = await requireEntityPermission(req, "settings", "update");
|
|
2009
|
+
if (pe) return pe;
|
|
2010
|
+
}
|
|
2011
|
+
return null;
|
|
2012
|
+
}
|
|
2013
|
+
return {
|
|
2014
|
+
async GET(req) {
|
|
2015
|
+
const err = await requireSettingsRead(req);
|
|
2016
|
+
if (err) return err;
|
|
2017
|
+
try {
|
|
2018
|
+
const rows = await repo().find({ where: { channel: "sms", deleted: false } });
|
|
2019
|
+
const byKey = new Map(rows.map((r) => [r.templateKey, r]));
|
|
2020
|
+
const items = SMS_MESSAGE_TEMPLATE_DEFAULTS.map((def) => {
|
|
2021
|
+
const row = byKey.get(def.templateKey);
|
|
2022
|
+
return {
|
|
2023
|
+
templateKey: def.templateKey,
|
|
2024
|
+
name: def.name,
|
|
2025
|
+
defaultBody: def.body,
|
|
2026
|
+
body: row?.body?.trim() ? row.body : def.body,
|
|
2027
|
+
externalTemplateRef: row?.externalTemplateRef?.trim() ?? "",
|
|
2028
|
+
otpVarKey: row?.providerMeta && typeof row.providerMeta.otpVarKey === "string" ? String(row.providerMeta.otpVarKey) : def.providerMeta?.otpVarKey ?? "var1",
|
|
2029
|
+
enabled: row ? row.enabled : false,
|
|
2030
|
+
dbId: row?.id ?? null
|
|
2031
|
+
};
|
|
2032
|
+
});
|
|
2033
|
+
return json({ items });
|
|
2034
|
+
} catch {
|
|
2035
|
+
return json({ error: "Failed to load templates" }, { status: 500 });
|
|
2036
|
+
}
|
|
2037
|
+
},
|
|
2038
|
+
async PUT(req) {
|
|
2039
|
+
const err = await requireSettingsUpdate(req);
|
|
2040
|
+
if (err) return err;
|
|
2041
|
+
try {
|
|
2042
|
+
const raw = await req.json().catch(() => null);
|
|
2043
|
+
if (!raw?.items || !Array.isArray(raw.items)) {
|
|
2044
|
+
return json({ error: "Invalid payload" }, { status: 400 });
|
|
2045
|
+
}
|
|
2046
|
+
for (const item of raw.items) {
|
|
2047
|
+
const templateKey = typeof item.templateKey === "string" ? item.templateKey.trim() : "";
|
|
2048
|
+
if (!getSmsTemplateDefault(templateKey)) continue;
|
|
2049
|
+
const body = typeof item.body === "string" ? item.body : "";
|
|
2050
|
+
const externalTemplateRef = typeof item.externalTemplateRef === "string" ? item.externalTemplateRef.trim() : "";
|
|
2051
|
+
const otpVarKey = typeof item.otpVarKey === "string" && item.otpVarKey.trim() ? item.otpVarKey.trim() : "var1";
|
|
2052
|
+
const enabled = item.enabled !== false;
|
|
2053
|
+
const existing = await repo().findOne({
|
|
2054
|
+
where: { channel: "sms", templateKey, deleted: false }
|
|
2055
|
+
});
|
|
2056
|
+
const def = getSmsTemplateDefault(templateKey);
|
|
2057
|
+
const providerMeta = { otpVarKey };
|
|
2058
|
+
if (existing) {
|
|
2059
|
+
await repo().update(existing.id, {
|
|
2060
|
+
name: def.name,
|
|
2061
|
+
body,
|
|
2062
|
+
externalTemplateRef: externalTemplateRef || null,
|
|
2063
|
+
providerMeta,
|
|
2064
|
+
enabled,
|
|
2065
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2066
|
+
});
|
|
2067
|
+
} else {
|
|
2068
|
+
await repo().save(
|
|
2069
|
+
repo().create({
|
|
2070
|
+
channel: "sms",
|
|
2071
|
+
templateKey,
|
|
2072
|
+
name: def.name,
|
|
2073
|
+
subject: null,
|
|
2074
|
+
body,
|
|
2075
|
+
externalTemplateRef: externalTemplateRef || null,
|
|
2076
|
+
providerMeta,
|
|
2077
|
+
enabled,
|
|
2078
|
+
deleted: false
|
|
2079
|
+
})
|
|
2080
|
+
);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
return json({ ok: true });
|
|
2084
|
+
} catch {
|
|
2085
|
+
return json({ error: "Failed to save templates" }, { status: 500 });
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
|
|
1651
2091
|
// src/auth/permission-entities.ts
|
|
1652
2092
|
var PERMISSION_ENTITY_INTERNAL_EXCLUDE = /* @__PURE__ */ new Set([
|
|
1653
2093
|
"users",
|
|
@@ -1661,7 +2101,8 @@ var PERMISSION_ENTITY_INTERNAL_EXCLUDE = /* @__PURE__ */ new Set([
|
|
|
1661
2101
|
"carts",
|
|
1662
2102
|
"cart_items",
|
|
1663
2103
|
"wishlists",
|
|
1664
|
-
"wishlist_items"
|
|
2104
|
+
"wishlist_items",
|
|
2105
|
+
"message_templates"
|
|
1665
2106
|
]);
|
|
1666
2107
|
var PERMISSION_LOGICAL_ENTITIES = [
|
|
1667
2108
|
"users",
|
|
@@ -1847,7 +2288,8 @@ var DEFAULT_EXCLUDE = /* @__PURE__ */ new Set([
|
|
|
1847
2288
|
"carts",
|
|
1848
2289
|
"cart_items",
|
|
1849
2290
|
"wishlists",
|
|
1850
|
-
"wishlist_items"
|
|
2291
|
+
"wishlist_items",
|
|
2292
|
+
"message_templates"
|
|
1851
2293
|
]);
|
|
1852
2294
|
function createCmsApiHandler(config) {
|
|
1853
2295
|
const {
|
|
@@ -1910,7 +2352,8 @@ function createCmsApiHandler(config) {
|
|
|
1910
2352
|
const crudOpts = {
|
|
1911
2353
|
requireAuth: config.requireAuth,
|
|
1912
2354
|
json: config.json,
|
|
1913
|
-
requireEntityPermission: reqEntityPerm
|
|
2355
|
+
requireEntityPermission: reqEntityPerm,
|
|
2356
|
+
getCms
|
|
1914
2357
|
};
|
|
1915
2358
|
const crud = createCrudHandler(dataSource, entityMap, crudOpts);
|
|
1916
2359
|
const crudById = createCrudByIdHandler(dataSource, entityMap, crudOpts);
|
|
@@ -1940,6 +2383,13 @@ function createCmsApiHandler(config) {
|
|
|
1940
2383
|
const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
|
|
1941
2384
|
const profilePut = userProfile ? createUserProfileHandler(userProfile) : null;
|
|
1942
2385
|
const settingsHandlers = settingsConfig ? createSettingsApiHandlers(settingsConfig) : null;
|
|
2386
|
+
const smsMessageTemplateHandlers = createSmsMessageTemplateHandlers({
|
|
2387
|
+
dataSource,
|
|
2388
|
+
entityMap,
|
|
2389
|
+
json: config.json,
|
|
2390
|
+
requireAuth: config.requireAuth,
|
|
2391
|
+
requireEntityPermission: reqEntityPerm
|
|
2392
|
+
});
|
|
1943
2393
|
const chatHandlers = chatConfig ? createChatHandlers(chatConfig) : null;
|
|
1944
2394
|
function resolveResource(segment) {
|
|
1945
2395
|
const model = pathToModel(segment);
|
|
@@ -2044,11 +2494,28 @@ function createCmsApiHandler(config) {
|
|
|
2044
2494
|
return settingsHandlers.PUT(req, group);
|
|
2045
2495
|
}
|
|
2046
2496
|
}
|
|
2497
|
+
if (path[0] === "message-templates" && path[1] === "sms" && path.length === 2) {
|
|
2498
|
+
if (method === "GET") return smsMessageTemplateHandlers.GET(req);
|
|
2499
|
+
if (method === "PUT") return smsMessageTemplateHandlers.PUT(req);
|
|
2500
|
+
}
|
|
2047
2501
|
if (path[0] === "chat" && chatHandlers) {
|
|
2048
2502
|
if (path.length === 2 && path[1] === "identify" && method === "POST") return chatHandlers.identify(req);
|
|
2049
2503
|
if (path.length === 4 && path[1] === "conversations" && path[3] === "messages" && method === "GET") return chatHandlers.getMessages(req, path[2]);
|
|
2050
2504
|
if (path.length === 2 && path[1] === "messages" && method === "POST") return chatHandlers.postMessage(req);
|
|
2051
2505
|
}
|
|
2506
|
+
if (path[0] === "orders" && path.length === 3 && path[2] === "invoice" && method === "GET" && getCms) {
|
|
2507
|
+
const a = await config.requireAuth(req);
|
|
2508
|
+
if (a) return a;
|
|
2509
|
+
if (perm) {
|
|
2510
|
+
const pe = await perm(req, "orders", "read");
|
|
2511
|
+
if (pe) return pe;
|
|
2512
|
+
}
|
|
2513
|
+
const cms = await getCms();
|
|
2514
|
+
const { streamOrderInvoicePdf: streamOrderInvoicePdf2 } = await Promise.resolve().then(() => (init_erp_order_invoice(), erp_order_invoice_exports));
|
|
2515
|
+
const oid = Number(path[1]);
|
|
2516
|
+
if (!Number.isFinite(oid)) return config.json({ error: "Invalid id" }, { status: 400 });
|
|
2517
|
+
return streamOrderInvoicePdf2(cms, dataSource, entityMap, oid, {});
|
|
2518
|
+
}
|
|
2052
2519
|
if (path.length === 0) return config.json({ error: "Not found" }, { status: 404 });
|
|
2053
2520
|
const resource = resolveResource(path[0]);
|
|
2054
2521
|
if (!crudResources.includes(resource)) return config.json({ error: "Invalid resource" }, { status: 400 });
|
|
@@ -2081,7 +2548,7 @@ function createCmsApiHandler(config) {
|
|
|
2081
2548
|
}
|
|
2082
2549
|
|
|
2083
2550
|
// src/api/storefront-handlers.ts
|
|
2084
|
-
import { In, IsNull as
|
|
2551
|
+
import { In, IsNull as IsNull3 } from "typeorm";
|
|
2085
2552
|
|
|
2086
2553
|
// src/lib/is-valid-signup-email.ts
|
|
2087
2554
|
var MAX_EMAIL = 254;
|
|
@@ -2103,6 +2570,339 @@ function isValidSignupEmail(email) {
|
|
|
2103
2570
|
|
|
2104
2571
|
// src/api/storefront-handlers.ts
|
|
2105
2572
|
init_email_queue();
|
|
2573
|
+
|
|
2574
|
+
// src/lib/order-number.ts
|
|
2575
|
+
var KIND_PREFIX = {
|
|
2576
|
+
sale: "OSL",
|
|
2577
|
+
return: "ORT",
|
|
2578
|
+
replacement: "ORP"
|
|
2579
|
+
};
|
|
2580
|
+
function orderNumberYymmUtc(at) {
|
|
2581
|
+
const yy = String(at.getUTCFullYear()).slice(-2);
|
|
2582
|
+
const mm = String(at.getUTCMonth() + 1).padStart(2, "0");
|
|
2583
|
+
return yy + mm;
|
|
2584
|
+
}
|
|
2585
|
+
function maskOrderIdSegment(id) {
|
|
2586
|
+
let x = id >>> 0 ^ 2779096485;
|
|
2587
|
+
x = Math.imul(x, 2654435761) >>> 0;
|
|
2588
|
+
return x.toString(36).toUpperCase().padStart(8, "0").slice(-8);
|
|
2589
|
+
}
|
|
2590
|
+
function buildCanonicalOrderNumber(kind, id, at) {
|
|
2591
|
+
return KIND_PREFIX[kind] + orderNumberYymmUtc(at) + maskOrderIdSegment(id);
|
|
2592
|
+
}
|
|
2593
|
+
function temporaryOrderNumberPlaceholder() {
|
|
2594
|
+
return `TMP${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`.toUpperCase();
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
// src/lib/order-storefront-metadata.ts
|
|
2598
|
+
function mergeOrderMetadataPatch(existing, patch) {
|
|
2599
|
+
const base = existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing } : {};
|
|
2600
|
+
if (patch.fulfillment !== void 0) {
|
|
2601
|
+
if (patch.fulfillment === null) delete base.fulfillment;
|
|
2602
|
+
else base.fulfillment = patch.fulfillment;
|
|
2603
|
+
}
|
|
2604
|
+
if (patch.invoice !== void 0) {
|
|
2605
|
+
if (patch.invoice === null) delete base.invoice;
|
|
2606
|
+
else base.invoice = patch.invoice;
|
|
2607
|
+
}
|
|
2608
|
+
return base;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
// src/plugins/erp/erp-order-status-map.ts
|
|
2612
|
+
function mapErpSaleStatusToOrderStatus(erpLabel) {
|
|
2613
|
+
if (!erpLabel || typeof erpLabel !== "string") return void 0;
|
|
2614
|
+
const k = erpLabel.trim().toLowerCase().replace(/\s+/g, "_");
|
|
2615
|
+
const map = {
|
|
2616
|
+
draft: "pending",
|
|
2617
|
+
pending: "pending",
|
|
2618
|
+
open: "pending",
|
|
2619
|
+
new: "pending",
|
|
2620
|
+
unconfirmed: "pending",
|
|
2621
|
+
confirmed: "confirmed",
|
|
2622
|
+
processing: "processing",
|
|
2623
|
+
packed: "processing",
|
|
2624
|
+
shipped: "processing",
|
|
2625
|
+
in_transit: "processing",
|
|
2626
|
+
out_for_delivery: "processing",
|
|
2627
|
+
delivered: "completed",
|
|
2628
|
+
completed: "completed",
|
|
2629
|
+
closed: "completed",
|
|
2630
|
+
fulfilled: "completed",
|
|
2631
|
+
cancelled: "cancelled",
|
|
2632
|
+
canceled: "cancelled",
|
|
2633
|
+
void: "cancelled"
|
|
2634
|
+
};
|
|
2635
|
+
return map[k];
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
// src/plugins/erp/erp-order-sync.ts
|
|
2639
|
+
init_erp_response_map();
|
|
2640
|
+
init_erp_config_enabled();
|
|
2641
|
+
function pickInvoiceId2(data) {
|
|
2642
|
+
const nested = data.invoice && typeof data.invoice === "object" && !Array.isArray(data.invoice) ? data.invoice : null;
|
|
2643
|
+
const src = nested || data;
|
|
2644
|
+
for (const k of ["invoiceId", "invoice_id", "id"]) {
|
|
2645
|
+
const v = src[k];
|
|
2646
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
2647
|
+
}
|
|
2648
|
+
return void 0;
|
|
2649
|
+
}
|
|
2650
|
+
async function ensureChildOrdersFromRefs(orderRepo, parent, refs, contactId, currency) {
|
|
2651
|
+
for (const { ref, orderKind } of refs) {
|
|
2652
|
+
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();
|
|
2653
|
+
if (existing) continue;
|
|
2654
|
+
const tmp = temporaryOrderNumberPlaceholder();
|
|
2655
|
+
const row = await orderRepo.save(
|
|
2656
|
+
orderRepo.create({
|
|
2657
|
+
orderNumber: tmp,
|
|
2658
|
+
orderKind,
|
|
2659
|
+
parentOrderId: parent.id,
|
|
2660
|
+
contactId,
|
|
2661
|
+
billingAddressId: null,
|
|
2662
|
+
shippingAddressId: null,
|
|
2663
|
+
status: "pending",
|
|
2664
|
+
subtotal: 0,
|
|
2665
|
+
tax: 0,
|
|
2666
|
+
discount: 0,
|
|
2667
|
+
total: 0,
|
|
2668
|
+
currency,
|
|
2669
|
+
metadata: { platformRef: ref },
|
|
2670
|
+
deleted: false
|
|
2671
|
+
})
|
|
2672
|
+
);
|
|
2673
|
+
const r = row;
|
|
2674
|
+
await orderRepo.update(r.id, {
|
|
2675
|
+
orderNumber: buildCanonicalOrderNumber(orderKind, r.id, r.createdAt ?? /* @__PURE__ */ new Date())
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
function deepMergeFulfillment(a, b) {
|
|
2680
|
+
if (!a) return b;
|
|
2681
|
+
if (!b) return a;
|
|
2682
|
+
return {
|
|
2683
|
+
...a,
|
|
2684
|
+
...b,
|
|
2685
|
+
events: b.events?.length ? b.events : a.events
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
async function refreshOrderFromErp(cms, dataSource, entityMap, submission, order) {
|
|
2689
|
+
const orderRepo = dataSource.getRepository(entityMap.orders);
|
|
2690
|
+
const kind = order.orderKind || "sale";
|
|
2691
|
+
const meta = order.metadata && typeof order.metadata === "object" && !Array.isArray(order.metadata) ? { ...order.metadata } : {};
|
|
2692
|
+
if (kind === "sale") {
|
|
2693
|
+
const refId = String(order.orderNumber || "");
|
|
2694
|
+
let fulfillment;
|
|
2695
|
+
let invoiceNumber;
|
|
2696
|
+
let invoiceId;
|
|
2697
|
+
let newStatus;
|
|
2698
|
+
const r1 = await submission.postErpReadAction("get-order-status", { platformOrderId: refId });
|
|
2699
|
+
const d1 = r1.ok ? unwrapErpReadData(r1.json) : null;
|
|
2700
|
+
if (d1) {
|
|
2701
|
+
const mapped = mapErpSaleStatusToOrderStatus(
|
|
2702
|
+
typeof d1.status === "string" ? d1.status : typeof d1.orderStatus === "string" ? d1.orderStatus : typeof d1.state === "string" ? d1.state : void 0
|
|
2703
|
+
);
|
|
2704
|
+
if (mapped) newStatus = mapped;
|
|
2705
|
+
fulfillment = mapErpPayloadToFulfillment(d1);
|
|
2706
|
+
const refs = extractChildOrderRefsFromSalePayload(d1);
|
|
2707
|
+
if (refs.length) {
|
|
2708
|
+
await ensureChildOrdersFromRefs(
|
|
2709
|
+
orderRepo,
|
|
2710
|
+
order,
|
|
2711
|
+
refs,
|
|
2712
|
+
order.contactId,
|
|
2713
|
+
String(order.currency || "INR")
|
|
2714
|
+
);
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
const r2 = await submission.postErpReadAction("get-fulfillment-status", { platformOrderId: refId });
|
|
2718
|
+
const d2 = r2.ok ? unwrapErpReadData(r2.json) : null;
|
|
2719
|
+
if (d2) {
|
|
2720
|
+
fulfillment = deepMergeFulfillment(fulfillment, mapErpPayloadToFulfillment(d2));
|
|
2721
|
+
}
|
|
2722
|
+
const r3 = await submission.postErpReadAction("get-invoice", { platformOrderId: refId });
|
|
2723
|
+
const d3 = r3.ok ? unwrapErpReadData(r3.json) : null;
|
|
2724
|
+
if (d3) {
|
|
2725
|
+
invoiceNumber = mapErpPayloadToInvoiceNumber(d3);
|
|
2726
|
+
invoiceId = pickInvoiceId2(d3);
|
|
2727
|
+
}
|
|
2728
|
+
const oid = order.id;
|
|
2729
|
+
const prevInv = meta.invoice && typeof meta.invoice === "object" && !Array.isArray(meta.invoice) ? { ...meta.invoice } : {};
|
|
2730
|
+
const nextInvoice = {
|
|
2731
|
+
...prevInv,
|
|
2732
|
+
link: `/api/storefront/orders/${oid}/invoice`,
|
|
2733
|
+
...invoiceNumber ? { invoiceNumber } : {},
|
|
2734
|
+
...invoiceId ? { invoiceId } : {}
|
|
2735
|
+
};
|
|
2736
|
+
const patch = { invoice: nextInvoice };
|
|
2737
|
+
if (fulfillment !== void 0) patch.fulfillment = fulfillment;
|
|
2738
|
+
const nextMeta = mergeOrderMetadataPatch(meta, patch);
|
|
2739
|
+
await orderRepo.update(oid, {
|
|
2740
|
+
...newStatus ? { status: newStatus } : {},
|
|
2741
|
+
metadata: nextMeta,
|
|
2742
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2743
|
+
});
|
|
2744
|
+
return;
|
|
2745
|
+
}
|
|
2746
|
+
if (kind === "return" || kind === "replacement") {
|
|
2747
|
+
const platformReturnId = String(order.orderNumber || "");
|
|
2748
|
+
const r = await submission.postErpReadAction("get-return-status", { platformReturnId });
|
|
2749
|
+
const d = r.ok ? unwrapErpReadData(r.json) : null;
|
|
2750
|
+
if (!d) return;
|
|
2751
|
+
const mapped = mapErpSaleStatusToOrderStatus(
|
|
2752
|
+
typeof d.status === "string" ? d.status : typeof d.returnStatus === "string" ? d.returnStatus : void 0
|
|
2753
|
+
);
|
|
2754
|
+
const fulfillment = mapErpPayloadToFulfillment(d);
|
|
2755
|
+
const patch = {};
|
|
2756
|
+
if (fulfillment !== void 0) patch.fulfillment = fulfillment;
|
|
2757
|
+
const nextMeta = Object.keys(patch).length ? mergeOrderMetadataPatch(meta, patch) : meta;
|
|
2758
|
+
await orderRepo.update(order.id, {
|
|
2759
|
+
...mapped ? { status: mapped } : {},
|
|
2760
|
+
metadata: nextMeta,
|
|
2761
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2762
|
+
});
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
async function tryRefreshOrderFromErpForStorefront(cms, dataSource, entityMap, order) {
|
|
2766
|
+
try {
|
|
2767
|
+
const on = await isErpIntegrationEnabled(cms, dataSource, entityMap);
|
|
2768
|
+
if (!on) return;
|
|
2769
|
+
const erp = cms.getPlugin("erp");
|
|
2770
|
+
if (!erp?.submission) return;
|
|
2771
|
+
await refreshOrderFromErp(cms, dataSource, entityMap, erp.submission, order);
|
|
2772
|
+
} catch {
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
// src/api/storefront-handlers.ts
|
|
2777
|
+
init_erp_order_invoice();
|
|
2778
|
+
|
|
2779
|
+
// src/plugins/sms/sms-queue.ts
|
|
2780
|
+
var SMS_QUEUE_NAME = "sms";
|
|
2781
|
+
async function queueSms(cms, payload) {
|
|
2782
|
+
const queue = cms.getPlugin("queue");
|
|
2783
|
+
const sms = cms.getPlugin("sms");
|
|
2784
|
+
if (queue) {
|
|
2785
|
+
await queue.add(SMS_QUEUE_NAME, payload);
|
|
2786
|
+
return;
|
|
2787
|
+
}
|
|
2788
|
+
if (sms && typeof sms.send === "function") {
|
|
2789
|
+
if (payload.templateKey?.trim()) {
|
|
2790
|
+
await sms.send({
|
|
2791
|
+
to: payload.to,
|
|
2792
|
+
templateKey: payload.templateKey.trim(),
|
|
2793
|
+
variables: payload.variables,
|
|
2794
|
+
otpCode: payload.otpCode
|
|
2795
|
+
});
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2798
|
+
if (payload.body?.trim()) {
|
|
2799
|
+
await sms.send({
|
|
2800
|
+
to: payload.to,
|
|
2801
|
+
body: payload.body,
|
|
2802
|
+
otpCode: payload.otpCode,
|
|
2803
|
+
variables: payload.variables
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
// src/lib/otp-challenge.ts
|
|
2810
|
+
import { createHmac, randomInt, timingSafeEqual } from "crypto";
|
|
2811
|
+
import { IsNull as IsNull2, MoreThan as MoreThan2 } from "typeorm";
|
|
2812
|
+
var OTP_TTL_MS = 10 * 60 * 1e3;
|
|
2813
|
+
var MAX_SENDS_PER_HOUR = 5;
|
|
2814
|
+
var MAX_VERIFY_ATTEMPTS = 8;
|
|
2815
|
+
function getPepper(explicit) {
|
|
2816
|
+
return (explicit || process.env.OTP_PEPPER || process.env.NEXTAUTH_SECRET || "dev-otp-pepper").trim();
|
|
2817
|
+
}
|
|
2818
|
+
function hashOtpCode(code, purpose, identifier, pepper) {
|
|
2819
|
+
return createHmac("sha256", getPepper(pepper)).update(`${purpose}|${identifier}|${code}`).digest("hex");
|
|
2820
|
+
}
|
|
2821
|
+
function verifyOtpCodeHash(code, storedHash, purpose, identifier, pepper) {
|
|
2822
|
+
const h = hashOtpCode(code, purpose, identifier, pepper);
|
|
2823
|
+
try {
|
|
2824
|
+
return timingSafeEqual(Buffer.from(h, "utf8"), Buffer.from(storedHash, "utf8"));
|
|
2825
|
+
} catch {
|
|
2826
|
+
return false;
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
function generateNumericOtp(length = 6) {
|
|
2830
|
+
const max = 10 ** length;
|
|
2831
|
+
return randomInt(0, max).toString().padStart(length, "0");
|
|
2832
|
+
}
|
|
2833
|
+
function normalizePhoneE164(raw, defaultCountryCode) {
|
|
2834
|
+
const t = raw.trim();
|
|
2835
|
+
const digitsOnly = t.replace(/\D/g, "");
|
|
2836
|
+
if (digitsOnly.length < 10) return null;
|
|
2837
|
+
if (t.startsWith("+")) return `+${digitsOnly}`;
|
|
2838
|
+
const cc = (defaultCountryCode || process.env.DEFAULT_PHONE_COUNTRY_CODE || "91").replace(/\D/g, "");
|
|
2839
|
+
if (digitsOnly.length > 10) return `+${digitsOnly}`;
|
|
2840
|
+
return `+${cc}${digitsOnly}`;
|
|
2841
|
+
}
|
|
2842
|
+
async function countRecentOtpSends(dataSource, entityMap, purpose, identifier, since) {
|
|
2843
|
+
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
2844
|
+
return repo.count({
|
|
2845
|
+
where: { purpose, identifier, createdAt: MoreThan2(since) }
|
|
2846
|
+
});
|
|
2847
|
+
}
|
|
2848
|
+
async function createOtpChallenge(dataSource, entityMap, input) {
|
|
2849
|
+
const { purpose, channel, identifier, code, pepper } = input;
|
|
2850
|
+
const since = new Date(Date.now() - 60 * 60 * 1e3);
|
|
2851
|
+
const recent = await countRecentOtpSends(dataSource, entityMap, purpose, identifier, since);
|
|
2852
|
+
if (recent >= MAX_SENDS_PER_HOUR) {
|
|
2853
|
+
return { ok: false, error: "Too many codes sent. Try again later.", status: 429 };
|
|
2854
|
+
}
|
|
2855
|
+
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
2856
|
+
await repo.delete({
|
|
2857
|
+
purpose,
|
|
2858
|
+
identifier,
|
|
2859
|
+
consumedAt: IsNull2()
|
|
2860
|
+
});
|
|
2861
|
+
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
2862
|
+
const codeHash = hashOtpCode(code, purpose, identifier, pepper);
|
|
2863
|
+
await repo.save(
|
|
2864
|
+
repo.create({
|
|
2865
|
+
purpose,
|
|
2866
|
+
channel,
|
|
2867
|
+
identifier,
|
|
2868
|
+
codeHash,
|
|
2869
|
+
expiresAt,
|
|
2870
|
+
attempts: 0,
|
|
2871
|
+
consumedAt: null
|
|
2872
|
+
})
|
|
2873
|
+
);
|
|
2874
|
+
return { ok: true };
|
|
2875
|
+
}
|
|
2876
|
+
async function verifyAndConsumeOtpChallenge(dataSource, entityMap, input) {
|
|
2877
|
+
const { purpose, identifier, code, pepper } = input;
|
|
2878
|
+
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
2879
|
+
const row = await repo.findOne({
|
|
2880
|
+
where: { purpose, identifier, consumedAt: IsNull2() },
|
|
2881
|
+
order: { id: "DESC" }
|
|
2882
|
+
});
|
|
2883
|
+
if (!row) {
|
|
2884
|
+
return { ok: false, error: "Invalid or expired code", status: 400 };
|
|
2885
|
+
}
|
|
2886
|
+
const r = row;
|
|
2887
|
+
if (new Date(r.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
2888
|
+
await repo.delete(row.id);
|
|
2889
|
+
return { ok: false, error: "Invalid or expired code", status: 400 };
|
|
2890
|
+
}
|
|
2891
|
+
const attempts = r.attempts || 0;
|
|
2892
|
+
if (attempts >= MAX_VERIFY_ATTEMPTS) {
|
|
2893
|
+
await repo.delete(row.id);
|
|
2894
|
+
return { ok: false, error: "Too many attempts", status: 400 };
|
|
2895
|
+
}
|
|
2896
|
+
const valid = verifyOtpCodeHash(code, r.codeHash, purpose, identifier, pepper);
|
|
2897
|
+
if (!valid) {
|
|
2898
|
+
await repo.update(row.id, { attempts: attempts + 1 });
|
|
2899
|
+
return { ok: false, error: "Invalid or expired code", status: 400 };
|
|
2900
|
+
}
|
|
2901
|
+
await repo.update(row.id, { consumedAt: /* @__PURE__ */ new Date(), attempts: attempts + 1 });
|
|
2902
|
+
return { ok: true };
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
// src/api/storefront-handlers.ts
|
|
2106
2906
|
var GUEST_COOKIE = "guest_id";
|
|
2107
2907
|
var ONE_YEAR = 60 * 60 * 24 * 365;
|
|
2108
2908
|
function parseCookies(header) {
|
|
@@ -2120,13 +2920,17 @@ function parseCookies(header) {
|
|
|
2120
2920
|
function guestCookieHeader(name, token) {
|
|
2121
2921
|
return `${name}=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${ONE_YEAR}`;
|
|
2122
2922
|
}
|
|
2123
|
-
function orderNumber() {
|
|
2124
|
-
return `ORD-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
2125
|
-
}
|
|
2126
2923
|
var SIGNUP_VERIFY_EXPIRY_HOURS = 72;
|
|
2127
2924
|
function createStorefrontApiHandler(config) {
|
|
2128
2925
|
const { dataSource, entityMap, json, getSessionUser, getCms, getCompanyDetails, publicSiteUrl } = config;
|
|
2129
2926
|
const cookieName = config.guestCookieName ?? GUEST_COOKIE;
|
|
2927
|
+
const otpFlags = config.otpFlags;
|
|
2928
|
+
const otpPepper = config.otpPepper;
|
|
2929
|
+
const defaultPhoneCc = config.defaultPhoneCountryCode;
|
|
2930
|
+
const otpAllowPhoneLogin = config.otpAllowPhoneLogin !== false;
|
|
2931
|
+
function otpOff(key) {
|
|
2932
|
+
return !otpFlags || otpFlags[key] !== true;
|
|
2933
|
+
}
|
|
2130
2934
|
const cartRepo = () => dataSource.getRepository(entityMap.carts);
|
|
2131
2935
|
const cartItemRepo = () => dataSource.getRepository(entityMap.cart_items);
|
|
2132
2936
|
const productRepo = () => dataSource.getRepository(entityMap.products);
|
|
@@ -2140,13 +2944,28 @@ function createStorefrontApiHandler(config) {
|
|
|
2140
2944
|
const tokenRepo = () => dataSource.getRepository(entityMap.password_reset_tokens);
|
|
2141
2945
|
const collectionRepo = () => dataSource.getRepository(entityMap.collections);
|
|
2142
2946
|
const groupRepo = () => dataSource.getRepository(entityMap.user_groups);
|
|
2947
|
+
async function syncContactToErp(contact) {
|
|
2948
|
+
if (!getCms) return;
|
|
2949
|
+
try {
|
|
2950
|
+
const cms = await getCms();
|
|
2951
|
+
await queueErpCreateContactIfEnabled(cms, dataSource, entityMap, {
|
|
2952
|
+
name: String(contact.name ?? ""),
|
|
2953
|
+
email: String(contact.email ?? "").trim(),
|
|
2954
|
+
phone: contact.phone,
|
|
2955
|
+
type: contact.type,
|
|
2956
|
+
company: contact.company,
|
|
2957
|
+
notes: contact.notes
|
|
2958
|
+
});
|
|
2959
|
+
} catch {
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2143
2962
|
async function ensureContactForUser(userId) {
|
|
2144
2963
|
let c = await contactRepo().findOne({ where: { userId, deleted: false } });
|
|
2145
2964
|
if (c) return c;
|
|
2146
2965
|
const u = await userRepo().findOne({ where: { id: userId } });
|
|
2147
2966
|
if (!u) return null;
|
|
2148
2967
|
const unclaimed = await contactRepo().findOne({
|
|
2149
|
-
where: { email: u.email, userId:
|
|
2968
|
+
where: { email: u.email, userId: IsNull3(), deleted: false }
|
|
2150
2969
|
});
|
|
2151
2970
|
if (unclaimed) {
|
|
2152
2971
|
await contactRepo().update(unclaimed.id, { userId });
|
|
@@ -2161,6 +2980,7 @@ function createStorefrontApiHandler(config) {
|
|
|
2161
2980
|
deleted: false
|
|
2162
2981
|
})
|
|
2163
2982
|
);
|
|
2983
|
+
await syncContactToErp(created);
|
|
2164
2984
|
return { id: created.id };
|
|
2165
2985
|
}
|
|
2166
2986
|
async function getOrCreateCart(req) {
|
|
@@ -2255,7 +3075,21 @@ function createStorefrontApiHandler(config) {
|
|
|
2255
3075
|
})
|
|
2256
3076
|
};
|
|
2257
3077
|
}
|
|
3078
|
+
function serializeSeo(seo) {
|
|
3079
|
+
if (!seo || typeof seo !== "object") return void 0;
|
|
3080
|
+
const s = seo;
|
|
3081
|
+
return {
|
|
3082
|
+
title: s.title ?? null,
|
|
3083
|
+
description: s.description ?? null,
|
|
3084
|
+
keywords: s.keywords ?? null,
|
|
3085
|
+
ogTitle: s.ogTitle ?? null,
|
|
3086
|
+
ogDescription: s.ogDescription ?? null,
|
|
3087
|
+
ogImage: s.ogImage ?? null,
|
|
3088
|
+
slug: s.slug ?? null
|
|
3089
|
+
};
|
|
3090
|
+
}
|
|
2258
3091
|
function serializeProduct(p) {
|
|
3092
|
+
const seo = serializeSeo(p.seo);
|
|
2259
3093
|
return {
|
|
2260
3094
|
id: p.id,
|
|
2261
3095
|
name: p.name,
|
|
@@ -2266,7 +3100,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2266
3100
|
compareAtPrice: p.compareAtPrice,
|
|
2267
3101
|
status: p.status,
|
|
2268
3102
|
collectionId: p.collectionId,
|
|
2269
|
-
metadata: p.metadata
|
|
3103
|
+
metadata: p.metadata,
|
|
3104
|
+
...seo ? { seo } : {}
|
|
2270
3105
|
};
|
|
2271
3106
|
}
|
|
2272
3107
|
return {
|
|
@@ -2333,7 +3168,7 @@ function createStorefrontApiHandler(config) {
|
|
|
2333
3168
|
const byId = /^\d+$/.test(idOrSlug);
|
|
2334
3169
|
const product = await productRepo().findOne({
|
|
2335
3170
|
where: byId ? { id: parseInt(idOrSlug, 10), status: "available", deleted: false } : { slug: idOrSlug, status: "available", deleted: false },
|
|
2336
|
-
relations: ["attributes", "attributes.attribute"]
|
|
3171
|
+
relations: ["attributes", "attributes.attribute", "seo"]
|
|
2337
3172
|
});
|
|
2338
3173
|
if (!product) return json({ error: "Not found" }, { status: 404 });
|
|
2339
3174
|
const p = product;
|
|
@@ -2377,7 +3212,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2377
3212
|
const idOrSlug = path[1];
|
|
2378
3213
|
const byId = /^\d+$/.test(idOrSlug);
|
|
2379
3214
|
const collection = await collectionRepo().findOne({
|
|
2380
|
-
where: byId ? { id: parseInt(idOrSlug, 10), active: true, deleted: false } : { slug: idOrSlug, active: true, deleted: false }
|
|
3215
|
+
where: byId ? { id: parseInt(idOrSlug, 10), active: true, deleted: false } : { slug: idOrSlug, active: true, deleted: false },
|
|
3216
|
+
relations: ["seo"]
|
|
2381
3217
|
});
|
|
2382
3218
|
if (!collection) return json({ error: "Not found" }, { status: 404 });
|
|
2383
3219
|
const col = collection;
|
|
@@ -2385,12 +3221,14 @@ function createStorefrontApiHandler(config) {
|
|
|
2385
3221
|
where: { collectionId: col.id, status: "available", deleted: false },
|
|
2386
3222
|
order: { id: "ASC" }
|
|
2387
3223
|
});
|
|
3224
|
+
const colSeo = serializeSeo(col.seo);
|
|
2388
3225
|
return json({
|
|
2389
3226
|
id: col.id,
|
|
2390
3227
|
name: col.name,
|
|
2391
3228
|
slug: col.slug,
|
|
2392
3229
|
description: col.description,
|
|
2393
3230
|
image: col.image,
|
|
3231
|
+
...colSeo ? { seo: colSeo } : {},
|
|
2394
3232
|
products: products.map((p) => serializeProduct(p))
|
|
2395
3233
|
});
|
|
2396
3234
|
}
|
|
@@ -2428,6 +3266,7 @@ function createStorefrontApiHandler(config) {
|
|
|
2428
3266
|
await userRepo().update(uid, { name: b.name.trim() });
|
|
2429
3267
|
}
|
|
2430
3268
|
const updatedContact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
3269
|
+
if (updatedContact) await syncContactToErp(updatedContact);
|
|
2431
3270
|
const updatedUser = await userRepo().findOne({ where: { id: uid }, select: ["id", "name", "email"] });
|
|
2432
3271
|
return json({
|
|
2433
3272
|
user: updatedUser ? { id: updatedUser.id, name: updatedUser.name, email: updatedUser.email } : null,
|
|
@@ -2515,13 +3354,155 @@ function createStorefrontApiHandler(config) {
|
|
|
2515
3354
|
const email = record.email;
|
|
2516
3355
|
const user = await userRepo().findOne({ where: { email }, select: ["id", "blocked"] });
|
|
2517
3356
|
if (!user) return json({ error: "User not found" }, { status: 400 });
|
|
2518
|
-
await userRepo().update(user.id, {
|
|
3357
|
+
await userRepo().update(user.id, {
|
|
3358
|
+
blocked: false,
|
|
3359
|
+
emailVerifiedAt: /* @__PURE__ */ new Date(),
|
|
3360
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3361
|
+
});
|
|
3362
|
+
await tokenRepo().delete({ email });
|
|
3363
|
+
return json({ success: true, message: "Email verified. You can sign in." });
|
|
3364
|
+
}
|
|
3365
|
+
if (path[0] === "auth" && path[1] === "otp" && path[2] === "send" && path.length === 3 && method === "POST") {
|
|
3366
|
+
const b = await req.json().catch(() => ({}));
|
|
3367
|
+
const purposeRaw = typeof b.purpose === "string" ? b.purpose.trim() : "";
|
|
3368
|
+
const purpose = purposeRaw === "login" || purposeRaw === "verify_email" || purposeRaw === "verify_phone" ? purposeRaw : "";
|
|
3369
|
+
if (!purpose) return json({ error: "purpose must be login, verify_email, or verify_phone" }, { status: 400 });
|
|
3370
|
+
if (purpose === "login" && otpOff("login")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
3371
|
+
if (purpose === "verify_email" && otpOff("verifyEmail")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
3372
|
+
if (purpose === "verify_phone" && otpOff("verifyPhone")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
3373
|
+
const capOtp = await assertCaptchaOk(getCms, b, req, json);
|
|
3374
|
+
if (capOtp) return capOtp;
|
|
3375
|
+
const emailIn = typeof b.email === "string" ? b.email.trim().toLowerCase() : "";
|
|
3376
|
+
const phoneIn = typeof b.phone === "string" ? b.phone.trim() : "";
|
|
3377
|
+
let identifier;
|
|
3378
|
+
let channel;
|
|
3379
|
+
if (purpose === "login") {
|
|
3380
|
+
if (emailIn) {
|
|
3381
|
+
identifier = emailIn;
|
|
3382
|
+
channel = "email";
|
|
3383
|
+
} else if (phoneIn) {
|
|
3384
|
+
if (!otpAllowPhoneLogin) {
|
|
3385
|
+
return json({ error: "Phone sign-in is not enabled" }, { status: 403 });
|
|
3386
|
+
}
|
|
3387
|
+
const p = normalizePhoneE164(phoneIn, defaultPhoneCc);
|
|
3388
|
+
if (!p) return json({ error: "Invalid phone" }, { status: 400 });
|
|
3389
|
+
identifier = p;
|
|
3390
|
+
channel = "sms";
|
|
3391
|
+
} else {
|
|
3392
|
+
return json({ error: "email or phone required" }, { status: 400 });
|
|
3393
|
+
}
|
|
3394
|
+
const user = channel === "email" ? await userRepo().findOne({ where: { email: identifier } }) : await userRepo().findOne({ where: { phone: identifier } });
|
|
3395
|
+
if (!user || user.deleted || user.blocked) {
|
|
3396
|
+
return json({ ok: true });
|
|
3397
|
+
}
|
|
3398
|
+
} else if (purpose === "verify_email") {
|
|
3399
|
+
if (!emailIn || !isValidSignupEmail(emailIn)) return json({ error: "Valid email required" }, { status: 400 });
|
|
3400
|
+
identifier = emailIn;
|
|
3401
|
+
channel = "email";
|
|
3402
|
+
const user = await userRepo().findOne({ where: { email: identifier } });
|
|
3403
|
+
if (!user || user.deleted) return json({ ok: true });
|
|
3404
|
+
} else {
|
|
3405
|
+
const su = await getSessionUser();
|
|
3406
|
+
const uid = su?.id ? parseInt(String(su.id), 10) : NaN;
|
|
3407
|
+
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
3408
|
+
const p = normalizePhoneE164(phoneIn, defaultPhoneCc);
|
|
3409
|
+
if (!p) return json({ error: "Valid phone required" }, { status: 400 });
|
|
3410
|
+
identifier = p;
|
|
3411
|
+
channel = "sms";
|
|
3412
|
+
const taken = await userRepo().findOne({
|
|
3413
|
+
where: { phone: identifier },
|
|
3414
|
+
select: ["id"]
|
|
3415
|
+
});
|
|
3416
|
+
if (taken && taken.id !== uid) {
|
|
3417
|
+
return json({ error: "Phone already in use" }, { status: 400 });
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
const code = generateNumericOtp(6);
|
|
3421
|
+
const created = await createOtpChallenge(dataSource, entityMap, {
|
|
3422
|
+
purpose,
|
|
3423
|
+
channel,
|
|
3424
|
+
identifier,
|
|
3425
|
+
code,
|
|
3426
|
+
pepper: otpPepper
|
|
3427
|
+
});
|
|
3428
|
+
if (!created.ok) return json({ error: created.error }, { status: created.status });
|
|
3429
|
+
if (!getCms) return json({ error: "OTP delivery not configured" }, { status: 503 });
|
|
3430
|
+
try {
|
|
3431
|
+
const cms = await getCms();
|
|
3432
|
+
if (channel === "email") {
|
|
3433
|
+
if (!cms.getPlugin("email")) return json({ error: "Email not configured" }, { status: 503 });
|
|
3434
|
+
const companyDetails = getCompanyDetails ? await getCompanyDetails() : {};
|
|
3435
|
+
await queueEmail(cms, {
|
|
3436
|
+
to: identifier,
|
|
3437
|
+
templateName: "otp",
|
|
3438
|
+
ctx: { code, companyDetails: companyDetails ?? {} }
|
|
3439
|
+
});
|
|
3440
|
+
} else {
|
|
3441
|
+
if (!cms.getPlugin("sms")) return json({ error: "SMS not configured" }, { status: 503 });
|
|
3442
|
+
const templateKey = purpose === "verify_phone" ? "auth.otp_verify_phone" : "auth.otp_login";
|
|
3443
|
+
await queueSms(cms, { to: identifier, templateKey, variables: { code } });
|
|
3444
|
+
}
|
|
3445
|
+
} catch {
|
|
3446
|
+
return json({ error: "Failed to send code" }, { status: 500 });
|
|
3447
|
+
}
|
|
3448
|
+
return json({ ok: true });
|
|
3449
|
+
}
|
|
3450
|
+
if (path[0] === "auth" && path[1] === "otp" && path[2] === "verify-email" && path.length === 3 && method === "POST") {
|
|
3451
|
+
if (otpOff("verifyEmail")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
3452
|
+
const b = await req.json().catch(() => ({}));
|
|
3453
|
+
const email = typeof b.email === "string" ? b.email.trim().toLowerCase() : "";
|
|
3454
|
+
const code = typeof b.code === "string" ? b.code.trim() : "";
|
|
3455
|
+
if (!email || !code) return json({ error: "email and code required" }, { status: 400 });
|
|
3456
|
+
const v = await verifyAndConsumeOtpChallenge(dataSource, entityMap, {
|
|
3457
|
+
purpose: "verify_email",
|
|
3458
|
+
identifier: email,
|
|
3459
|
+
code,
|
|
3460
|
+
pepper: otpPepper
|
|
3461
|
+
});
|
|
3462
|
+
if (!v.ok) return json({ error: v.error }, { status: v.status });
|
|
3463
|
+
const user = await userRepo().findOne({ where: { email } });
|
|
3464
|
+
if (!user) return json({ error: "User not found" }, { status: 400 });
|
|
3465
|
+
await userRepo().update(user.id, {
|
|
3466
|
+
blocked: false,
|
|
3467
|
+
emailVerifiedAt: /* @__PURE__ */ new Date(),
|
|
3468
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3469
|
+
});
|
|
2519
3470
|
await tokenRepo().delete({ email });
|
|
2520
3471
|
return json({ success: true, message: "Email verified. You can sign in." });
|
|
2521
3472
|
}
|
|
3473
|
+
if (path[0] === "auth" && path[1] === "otp" && path[2] === "verify-phone" && path.length === 3 && method === "POST") {
|
|
3474
|
+
if (otpOff("verifyPhone")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
3475
|
+
const su = await getSessionUser();
|
|
3476
|
+
const uid = su?.id ? parseInt(String(su.id), 10) : NaN;
|
|
3477
|
+
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
3478
|
+
const b = await req.json().catch(() => ({}));
|
|
3479
|
+
const phoneRaw = typeof b.phone === "string" ? b.phone.trim() : "";
|
|
3480
|
+
const code = typeof b.code === "string" ? b.code.trim() : "";
|
|
3481
|
+
const phone = normalizePhoneE164(phoneRaw, defaultPhoneCc);
|
|
3482
|
+
if (!phone || !code) return json({ error: "phone and code required" }, { status: 400 });
|
|
3483
|
+
const v = await verifyAndConsumeOtpChallenge(dataSource, entityMap, {
|
|
3484
|
+
purpose: "verify_phone",
|
|
3485
|
+
identifier: phone,
|
|
3486
|
+
code,
|
|
3487
|
+
pepper: otpPepper
|
|
3488
|
+
});
|
|
3489
|
+
if (!v.ok) return json({ error: v.error }, { status: v.status });
|
|
3490
|
+
const taken = await userRepo().findOne({ where: { phone }, select: ["id"] });
|
|
3491
|
+
if (taken && taken.id !== uid) {
|
|
3492
|
+
return json({ error: "Phone already in use" }, { status: 400 });
|
|
3493
|
+
}
|
|
3494
|
+
await userRepo().update(uid, { phone, phoneVerifiedAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() });
|
|
3495
|
+
const contact = await ensureContactForUser(uid);
|
|
3496
|
+
if (contact) {
|
|
3497
|
+
await contactRepo().update(contact.id, { phone });
|
|
3498
|
+
}
|
|
3499
|
+
return json({ success: true });
|
|
3500
|
+
}
|
|
2522
3501
|
if (path[0] === "register" && path.length === 1 && method === "POST") {
|
|
2523
3502
|
if (!config.hashPassword) return json({ error: "Registration not configured" }, { status: 501 });
|
|
2524
3503
|
const b = await req.json().catch(() => ({}));
|
|
3504
|
+
const capReg = await assertCaptchaOk(getCms, b, req, json);
|
|
3505
|
+
if (capReg) return capReg;
|
|
2525
3506
|
const name = typeof b.name === "string" ? b.name.trim() : "";
|
|
2526
3507
|
const email = typeof b.email === "string" ? b.email.trim().toLowerCase() : "";
|
|
2527
3508
|
const password = typeof b.password === "string" ? b.password : "";
|
|
@@ -2583,6 +3564,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2583
3564
|
}
|
|
2584
3565
|
if (path[0] === "cart" && path[1] === "items" && path.length === 2 && method === "POST") {
|
|
2585
3566
|
const body = await req.json().catch(() => ({}));
|
|
3567
|
+
const capCart = await assertCaptchaOk(getCms, body, req, json);
|
|
3568
|
+
if (capCart) return capCart;
|
|
2586
3569
|
const productId = Number(body.productId);
|
|
2587
3570
|
const quantity = Math.max(1, Number(body.quantity) || 1);
|
|
2588
3571
|
if (!Number.isFinite(productId)) return json({ error: "productId required" }, { status: 400 });
|
|
@@ -2782,6 +3765,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2782
3765
|
const { wishlist, setCookie, err } = await getOrCreateWishlist(req);
|
|
2783
3766
|
if (err) return err;
|
|
2784
3767
|
const b = await req.json().catch(() => ({}));
|
|
3768
|
+
const capWl = await assertCaptchaOk(getCms, b, req, json);
|
|
3769
|
+
if (capWl) return capWl;
|
|
2785
3770
|
const productId = Number(b.productId);
|
|
2786
3771
|
if (!Number.isFinite(productId)) return json({ error: "productId required" }, { status: 400 });
|
|
2787
3772
|
const wid = wishlist.id;
|
|
@@ -2800,6 +3785,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2800
3785
|
}
|
|
2801
3786
|
if (path[0] === "checkout" && path[1] === "order" && path.length === 2 && method === "POST") {
|
|
2802
3787
|
const b = await req.json().catch(() => ({}));
|
|
3788
|
+
const capOrd = await assertCaptchaOk(getCms, b, req, json);
|
|
3789
|
+
if (capOrd) return capOrd;
|
|
2803
3790
|
const u = await getSessionUser();
|
|
2804
3791
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
2805
3792
|
let contactId;
|
|
@@ -2813,8 +3800,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2813
3800
|
relations: ["items", "items.product"]
|
|
2814
3801
|
});
|
|
2815
3802
|
} else {
|
|
2816
|
-
const email = (b.email
|
|
2817
|
-
const name = (b.name
|
|
3803
|
+
const email = String(b.email ?? "").trim();
|
|
3804
|
+
const name = String(b.name ?? "").trim();
|
|
2818
3805
|
if (!email || !name) return json({ error: "name and email required for guest checkout" }, { status: 400 });
|
|
2819
3806
|
let contact = await contactRepo().findOne({ where: { email, deleted: false } });
|
|
2820
3807
|
if (contact && contact.userId != null) {
|
|
@@ -2825,13 +3812,19 @@ function createStorefrontApiHandler(config) {
|
|
|
2825
3812
|
contactRepo().create({
|
|
2826
3813
|
name,
|
|
2827
3814
|
email,
|
|
2828
|
-
phone: b.phone
|
|
3815
|
+
phone: b.phone != null && b.phone !== "" ? String(b.phone) : null,
|
|
2829
3816
|
userId: null,
|
|
2830
3817
|
deleted: false
|
|
2831
3818
|
})
|
|
2832
3819
|
);
|
|
2833
|
-
} else if (name)
|
|
3820
|
+
} else if (name)
|
|
3821
|
+
await contactRepo().update(contact.id, {
|
|
3822
|
+
name,
|
|
3823
|
+
phone: b.phone != null && b.phone !== "" ? String(b.phone) : contact.phone
|
|
3824
|
+
});
|
|
2834
3825
|
contactId = contact.id;
|
|
3826
|
+
const guestForErp = await contactRepo().findOne({ where: { id: contactId } });
|
|
3827
|
+
if (guestForErp) await syncContactToErp(guestForErp);
|
|
2835
3828
|
const cookies = parseCookies(req.headers.get("cookie"));
|
|
2836
3829
|
const guestToken = cookies[cookieName];
|
|
2837
3830
|
if (!guestToken) return json({ error: "Cart not found" }, { status: 400 });
|
|
@@ -2859,10 +3852,12 @@ function createStorefrontApiHandler(config) {
|
|
|
2859
3852
|
const cartId = cart.id;
|
|
2860
3853
|
const ord = await orderRepo().save(
|
|
2861
3854
|
orderRepo().create({
|
|
2862
|
-
orderNumber:
|
|
3855
|
+
orderNumber: temporaryOrderNumberPlaceholder(),
|
|
3856
|
+
orderKind: "sale",
|
|
3857
|
+
parentOrderId: null,
|
|
2863
3858
|
contactId,
|
|
2864
|
-
billingAddressId: b.billingAddressId
|
|
2865
|
-
shippingAddressId: b.shippingAddressId
|
|
3859
|
+
billingAddressId: typeof b.billingAddressId === "number" ? b.billingAddressId : null,
|
|
3860
|
+
shippingAddressId: typeof b.shippingAddressId === "number" ? b.shippingAddressId : null,
|
|
2866
3861
|
status: "pending",
|
|
2867
3862
|
subtotal,
|
|
2868
3863
|
tax: 0,
|
|
@@ -2873,6 +3868,9 @@ function createStorefrontApiHandler(config) {
|
|
|
2873
3868
|
})
|
|
2874
3869
|
);
|
|
2875
3870
|
const oid = ord.id;
|
|
3871
|
+
await orderRepo().update(oid, {
|
|
3872
|
+
orderNumber: buildCanonicalOrderNumber("sale", oid, ord.createdAt ?? /* @__PURE__ */ new Date())
|
|
3873
|
+
});
|
|
2876
3874
|
for (const line of lines) {
|
|
2877
3875
|
await orderItemRepo().save(
|
|
2878
3876
|
orderItemRepo().create({
|
|
@@ -2894,6 +3892,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2894
3892
|
}
|
|
2895
3893
|
if (path[0] === "checkout" && path.length === 1 && method === "POST") {
|
|
2896
3894
|
const b = await req.json().catch(() => ({}));
|
|
3895
|
+
const capChk = await assertCaptchaOk(getCms, b, req, json);
|
|
3896
|
+
if (capChk) return capChk;
|
|
2897
3897
|
const u = await getSessionUser();
|
|
2898
3898
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
2899
3899
|
let contactId;
|
|
@@ -2907,8 +3907,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2907
3907
|
relations: ["items", "items.product"]
|
|
2908
3908
|
});
|
|
2909
3909
|
} else {
|
|
2910
|
-
const email = (b.email
|
|
2911
|
-
const name = (b.name
|
|
3910
|
+
const email = String(b.email ?? "").trim();
|
|
3911
|
+
const name = String(b.name ?? "").trim();
|
|
2912
3912
|
if (!email || !name) return json({ error: "name and email required for guest checkout" }, { status: 400 });
|
|
2913
3913
|
let contact = await contactRepo().findOne({ where: { email, deleted: false } });
|
|
2914
3914
|
if (contact && contact.userId != null) {
|
|
@@ -2919,13 +3919,19 @@ function createStorefrontApiHandler(config) {
|
|
|
2919
3919
|
contactRepo().create({
|
|
2920
3920
|
name,
|
|
2921
3921
|
email,
|
|
2922
|
-
phone: b.phone
|
|
3922
|
+
phone: b.phone != null && b.phone !== "" ? String(b.phone) : null,
|
|
2923
3923
|
userId: null,
|
|
2924
3924
|
deleted: false
|
|
2925
3925
|
})
|
|
2926
3926
|
);
|
|
2927
|
-
} else if (name)
|
|
3927
|
+
} else if (name)
|
|
3928
|
+
await contactRepo().update(contact.id, {
|
|
3929
|
+
name,
|
|
3930
|
+
phone: b.phone != null && b.phone !== "" ? String(b.phone) : contact.phone
|
|
3931
|
+
});
|
|
2928
3932
|
contactId = contact.id;
|
|
3933
|
+
const guestForErp2 = await contactRepo().findOne({ where: { id: contactId } });
|
|
3934
|
+
if (guestForErp2) await syncContactToErp(guestForErp2);
|
|
2929
3935
|
const cookies = parseCookies(req.headers.get("cookie"));
|
|
2930
3936
|
const guestToken = cookies[cookieName];
|
|
2931
3937
|
if (!guestToken) return json({ error: "Cart not found" }, { status: 400 });
|
|
@@ -2952,10 +3958,12 @@ function createStorefrontApiHandler(config) {
|
|
|
2952
3958
|
const total = subtotal;
|
|
2953
3959
|
const ord = await orderRepo().save(
|
|
2954
3960
|
orderRepo().create({
|
|
2955
|
-
orderNumber:
|
|
3961
|
+
orderNumber: temporaryOrderNumberPlaceholder(),
|
|
3962
|
+
orderKind: "sale",
|
|
3963
|
+
parentOrderId: null,
|
|
2956
3964
|
contactId,
|
|
2957
|
-
billingAddressId: b.billingAddressId
|
|
2958
|
-
shippingAddressId: b.shippingAddressId
|
|
3965
|
+
billingAddressId: typeof b.billingAddressId === "number" ? b.billingAddressId : null,
|
|
3966
|
+
shippingAddressId: typeof b.shippingAddressId === "number" ? b.shippingAddressId : null,
|
|
2959
3967
|
status: "pending",
|
|
2960
3968
|
subtotal,
|
|
2961
3969
|
tax: 0,
|
|
@@ -2965,6 +3973,9 @@ function createStorefrontApiHandler(config) {
|
|
|
2965
3973
|
})
|
|
2966
3974
|
);
|
|
2967
3975
|
const oid = ord.id;
|
|
3976
|
+
await orderRepo().update(oid, {
|
|
3977
|
+
orderNumber: buildCanonicalOrderNumber("sale", oid, ord.createdAt ?? /* @__PURE__ */ new Date())
|
|
3978
|
+
});
|
|
2968
3979
|
for (const line of lines) {
|
|
2969
3980
|
await orderItemRepo().save(
|
|
2970
3981
|
orderItemRepo().create({
|
|
@@ -2992,7 +4003,7 @@ function createStorefrontApiHandler(config) {
|
|
|
2992
4003
|
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
2993
4004
|
if (!contact) return json({ orders: [] });
|
|
2994
4005
|
const orders = await orderRepo().find({
|
|
2995
|
-
where: { contactId: contact.id, deleted: false },
|
|
4006
|
+
where: { contactId: contact.id, deleted: false, orderKind: "sale" },
|
|
2996
4007
|
order: { createdAt: "DESC" },
|
|
2997
4008
|
take: 50
|
|
2998
4009
|
});
|
|
@@ -3027,6 +4038,20 @@ function createStorefrontApiHandler(config) {
|
|
|
3027
4038
|
})
|
|
3028
4039
|
});
|
|
3029
4040
|
}
|
|
4041
|
+
if (path[0] === "orders" && path.length === 3 && path[2] === "invoice" && method === "GET") {
|
|
4042
|
+
const u = await getSessionUser();
|
|
4043
|
+
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
4044
|
+
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
4045
|
+
if (!getCms) return json({ error: "Not found" }, { status: 404 });
|
|
4046
|
+
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
4047
|
+
if (!contact) return json({ error: "Not found" }, { status: 404 });
|
|
4048
|
+
const orderId = parseInt(path[1], 10);
|
|
4049
|
+
if (!Number.isFinite(orderId)) return json({ error: "Invalid id" }, { status: 400 });
|
|
4050
|
+
const cms = await getCms();
|
|
4051
|
+
return streamOrderInvoicePdf(cms, dataSource, entityMap, orderId, {
|
|
4052
|
+
ownerContactId: contact.id
|
|
4053
|
+
});
|
|
4054
|
+
}
|
|
3030
4055
|
if (path[0] === "orders" && path.length === 2 && method === "GET") {
|
|
3031
4056
|
const u = await getSessionUser();
|
|
3032
4057
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
@@ -3034,11 +4059,20 @@ function createStorefrontApiHandler(config) {
|
|
|
3034
4059
|
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
3035
4060
|
if (!contact) return json({ error: "Not found" }, { status: 404 });
|
|
3036
4061
|
const orderId = parseInt(path[1], 10);
|
|
3037
|
-
|
|
4062
|
+
let order = await orderRepo().findOne({
|
|
3038
4063
|
where: { id: orderId, contactId: contact.id, deleted: false },
|
|
3039
4064
|
relations: ["items", "items.product"]
|
|
3040
4065
|
});
|
|
3041
4066
|
if (!order) return json({ error: "Not found" }, { status: 404 });
|
|
4067
|
+
if (getCms) {
|
|
4068
|
+
const cms = await getCms();
|
|
4069
|
+
await tryRefreshOrderFromErpForStorefront(cms, dataSource, entityMap, order);
|
|
4070
|
+
order = await orderRepo().findOne({
|
|
4071
|
+
where: { id: orderId, contactId: contact.id, deleted: false },
|
|
4072
|
+
relations: ["items", "items.product"]
|
|
4073
|
+
});
|
|
4074
|
+
}
|
|
4075
|
+
if (!order) return json({ error: "Not found" }, { status: 404 });
|
|
3042
4076
|
const o = order;
|
|
3043
4077
|
const lines = (o.items || []).map((line) => {
|
|
3044
4078
|
const p = line.product;
|
|
@@ -3057,10 +4091,22 @@ function createStorefrontApiHandler(config) {
|
|
|
3057
4091
|
} : null
|
|
3058
4092
|
};
|
|
3059
4093
|
});
|
|
4094
|
+
const kind = o.orderKind || "sale";
|
|
4095
|
+
let relatedOrders = [];
|
|
4096
|
+
if (kind === "sale") {
|
|
4097
|
+
relatedOrders = await orderRepo().find({
|
|
4098
|
+
where: { parentOrderId: orderId, deleted: false },
|
|
4099
|
+
order: { id: "ASC" }
|
|
4100
|
+
});
|
|
4101
|
+
}
|
|
4102
|
+
const meta = o.metadata;
|
|
4103
|
+
const fulfillmentPreview = meta && typeof meta.fulfillment === "object" && meta.fulfillment && "status" in meta.fulfillment ? String(meta.fulfillment.status ?? "") : "";
|
|
3060
4104
|
return json({
|
|
3061
4105
|
order: {
|
|
3062
4106
|
id: o.id,
|
|
3063
4107
|
orderNumber: o.orderNumber,
|
|
4108
|
+
orderKind: kind,
|
|
4109
|
+
parentOrderId: o.parentOrderId ?? null,
|
|
3064
4110
|
status: o.status,
|
|
3065
4111
|
subtotal: o.subtotal,
|
|
3066
4112
|
tax: o.tax,
|
|
@@ -3068,8 +4114,18 @@ function createStorefrontApiHandler(config) {
|
|
|
3068
4114
|
total: o.total,
|
|
3069
4115
|
currency: o.currency,
|
|
3070
4116
|
createdAt: o.createdAt,
|
|
4117
|
+
metadata: o.metadata ?? null,
|
|
3071
4118
|
items: lines
|
|
3072
|
-
}
|
|
4119
|
+
},
|
|
4120
|
+
relatedOrders: relatedOrders.map((r) => ({
|
|
4121
|
+
id: r.id,
|
|
4122
|
+
orderNumber: r.orderNumber,
|
|
4123
|
+
orderKind: r.orderKind ?? "return",
|
|
4124
|
+
status: r.status,
|
|
4125
|
+
createdAt: r.createdAt,
|
|
4126
|
+
fulfillmentStatus: r.metadata && typeof r.metadata === "object" && r.metadata.fulfillment?.status
|
|
4127
|
+
})),
|
|
4128
|
+
fulfillmentPreview: fulfillmentPreview || void 0
|
|
3073
4129
|
});
|
|
3074
4130
|
}
|
|
3075
4131
|
return json({ error: "Not found" }, { status: 404 });
|