@infuro/cms-core 1.0.11 → 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 +1104 -543
- package/dist/admin.cjs.map +1 -1
- package/dist/admin.js +950 -389
- package/dist/admin.js.map +1 -1
- package/dist/api.cjs +1140 -57
- 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 +1137 -55
- 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-C_CZLmHD.d.cts → index-Be8NLxu-.d.cts} +33 -2
- package/dist/{index-DeO4AnAj.d.ts → index-CjBf9dAb.d.ts} +33 -2
- package/dist/index.cjs +3011 -915
- 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 +2812 -740
- 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
|
}
|
|
@@ -1441,6 +1762,24 @@ function simpleDecrypt(encoded, key) {
|
|
|
1441
1762
|
for (let i = 0; i < buf.length; i++) out[i] = buf[i] ^ keyBuf[i % keyBuf.length];
|
|
1442
1763
|
return out.toString("utf8");
|
|
1443
1764
|
}
|
|
1765
|
+
async function getPublicSettingsGroup(config, group) {
|
|
1766
|
+
const { dataSource, entityMap, encryptionKey } = config;
|
|
1767
|
+
const repo = dataSource.getRepository(entityMap.configs);
|
|
1768
|
+
const rows = await repo.find({ where: { settings: group, deleted: false } });
|
|
1769
|
+
const result = {};
|
|
1770
|
+
for (const row of rows) {
|
|
1771
|
+
const r = row;
|
|
1772
|
+
let val = r.value;
|
|
1773
|
+
if (r.encrypted && encryptionKey) {
|
|
1774
|
+
try {
|
|
1775
|
+
val = simpleDecrypt(val, encryptionKey);
|
|
1776
|
+
} catch {
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
result[r.key] = val;
|
|
1780
|
+
}
|
|
1781
|
+
return result;
|
|
1782
|
+
}
|
|
1444
1783
|
function createSettingsApiHandlers(config) {
|
|
1445
1784
|
const { dataSource, entityMap, json, requireAuth, encryptionKey, publicGetGroups } = config;
|
|
1446
1785
|
const configRepo = () => dataSource.getRepository(entityMap.configs);
|
|
@@ -1450,6 +1789,13 @@ function createSettingsApiHandlers(config) {
|
|
|
1450
1789
|
const authErr = isPublicGroup ? null : await requireAuth(req);
|
|
1451
1790
|
const isAuthed = !authErr;
|
|
1452
1791
|
try {
|
|
1792
|
+
if (isPublicGroup) {
|
|
1793
|
+
const result2 = await getPublicSettingsGroup(
|
|
1794
|
+
{ dataSource, entityMap, encryptionKey },
|
|
1795
|
+
group
|
|
1796
|
+
);
|
|
1797
|
+
return json(result2);
|
|
1798
|
+
}
|
|
1453
1799
|
const where = { settings: group, deleted: false };
|
|
1454
1800
|
if (!isAuthed && !isPublicGroup) where.type = "public";
|
|
1455
1801
|
const rows = await configRepo().find({ where });
|
|
@@ -1623,6 +1969,125 @@ ${contextParts.join("\n\n")}` : "You are a helpful assistant for the company. If
|
|
|
1623
1969
|
};
|
|
1624
1970
|
}
|
|
1625
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
|
+
|
|
1626
2091
|
// src/auth/permission-entities.ts
|
|
1627
2092
|
var PERMISSION_ENTITY_INTERNAL_EXCLUDE = /* @__PURE__ */ new Set([
|
|
1628
2093
|
"users",
|
|
@@ -1636,7 +2101,8 @@ var PERMISSION_ENTITY_INTERNAL_EXCLUDE = /* @__PURE__ */ new Set([
|
|
|
1636
2101
|
"carts",
|
|
1637
2102
|
"cart_items",
|
|
1638
2103
|
"wishlists",
|
|
1639
|
-
"wishlist_items"
|
|
2104
|
+
"wishlist_items",
|
|
2105
|
+
"message_templates"
|
|
1640
2106
|
]);
|
|
1641
2107
|
var PERMISSION_LOGICAL_ENTITIES = [
|
|
1642
2108
|
"users",
|
|
@@ -1822,7 +2288,8 @@ var DEFAULT_EXCLUDE = /* @__PURE__ */ new Set([
|
|
|
1822
2288
|
"carts",
|
|
1823
2289
|
"cart_items",
|
|
1824
2290
|
"wishlists",
|
|
1825
|
-
"wishlist_items"
|
|
2291
|
+
"wishlist_items",
|
|
2292
|
+
"message_templates"
|
|
1826
2293
|
]);
|
|
1827
2294
|
function createCmsApiHandler(config) {
|
|
1828
2295
|
const {
|
|
@@ -1885,7 +2352,8 @@ function createCmsApiHandler(config) {
|
|
|
1885
2352
|
const crudOpts = {
|
|
1886
2353
|
requireAuth: config.requireAuth,
|
|
1887
2354
|
json: config.json,
|
|
1888
|
-
requireEntityPermission: reqEntityPerm
|
|
2355
|
+
requireEntityPermission: reqEntityPerm,
|
|
2356
|
+
getCms
|
|
1889
2357
|
};
|
|
1890
2358
|
const crud = createCrudHandler(dataSource, entityMap, crudOpts);
|
|
1891
2359
|
const crudById = createCrudByIdHandler(dataSource, entityMap, crudOpts);
|
|
@@ -1915,6 +2383,13 @@ function createCmsApiHandler(config) {
|
|
|
1915
2383
|
const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
|
|
1916
2384
|
const profilePut = userProfile ? createUserProfileHandler(userProfile) : null;
|
|
1917
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
|
+
});
|
|
1918
2393
|
const chatHandlers = chatConfig ? createChatHandlers(chatConfig) : null;
|
|
1919
2394
|
function resolveResource(segment) {
|
|
1920
2395
|
const model = pathToModel(segment);
|
|
@@ -2019,11 +2494,28 @@ function createCmsApiHandler(config) {
|
|
|
2019
2494
|
return settingsHandlers.PUT(req, group);
|
|
2020
2495
|
}
|
|
2021
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
|
+
}
|
|
2022
2501
|
if (path[0] === "chat" && chatHandlers) {
|
|
2023
2502
|
if (path.length === 2 && path[1] === "identify" && method === "POST") return chatHandlers.identify(req);
|
|
2024
2503
|
if (path.length === 4 && path[1] === "conversations" && path[3] === "messages" && method === "GET") return chatHandlers.getMessages(req, path[2]);
|
|
2025
2504
|
if (path.length === 2 && path[1] === "messages" && method === "POST") return chatHandlers.postMessage(req);
|
|
2026
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
|
+
}
|
|
2027
2519
|
if (path.length === 0) return config.json({ error: "Not found" }, { status: 404 });
|
|
2028
2520
|
const resource = resolveResource(path[0]);
|
|
2029
2521
|
if (!crudResources.includes(resource)) return config.json({ error: "Invalid resource" }, { status: 400 });
|
|
@@ -2056,7 +2548,7 @@ function createCmsApiHandler(config) {
|
|
|
2056
2548
|
}
|
|
2057
2549
|
|
|
2058
2550
|
// src/api/storefront-handlers.ts
|
|
2059
|
-
import { In, IsNull as
|
|
2551
|
+
import { In, IsNull as IsNull3 } from "typeorm";
|
|
2060
2552
|
|
|
2061
2553
|
// src/lib/is-valid-signup-email.ts
|
|
2062
2554
|
var MAX_EMAIL = 254;
|
|
@@ -2078,6 +2570,339 @@ function isValidSignupEmail(email) {
|
|
|
2078
2570
|
|
|
2079
2571
|
// src/api/storefront-handlers.ts
|
|
2080
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
|
|
2081
2906
|
var GUEST_COOKIE = "guest_id";
|
|
2082
2907
|
var ONE_YEAR = 60 * 60 * 24 * 365;
|
|
2083
2908
|
function parseCookies(header) {
|
|
@@ -2095,13 +2920,17 @@ function parseCookies(header) {
|
|
|
2095
2920
|
function guestCookieHeader(name, token) {
|
|
2096
2921
|
return `${name}=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${ONE_YEAR}`;
|
|
2097
2922
|
}
|
|
2098
|
-
function orderNumber() {
|
|
2099
|
-
return `ORD-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
2100
|
-
}
|
|
2101
2923
|
var SIGNUP_VERIFY_EXPIRY_HOURS = 72;
|
|
2102
2924
|
function createStorefrontApiHandler(config) {
|
|
2103
2925
|
const { dataSource, entityMap, json, getSessionUser, getCms, getCompanyDetails, publicSiteUrl } = config;
|
|
2104
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
|
+
}
|
|
2105
2934
|
const cartRepo = () => dataSource.getRepository(entityMap.carts);
|
|
2106
2935
|
const cartItemRepo = () => dataSource.getRepository(entityMap.cart_items);
|
|
2107
2936
|
const productRepo = () => dataSource.getRepository(entityMap.products);
|
|
@@ -2115,13 +2944,28 @@ function createStorefrontApiHandler(config) {
|
|
|
2115
2944
|
const tokenRepo = () => dataSource.getRepository(entityMap.password_reset_tokens);
|
|
2116
2945
|
const collectionRepo = () => dataSource.getRepository(entityMap.collections);
|
|
2117
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
|
+
}
|
|
2118
2962
|
async function ensureContactForUser(userId) {
|
|
2119
2963
|
let c = await contactRepo().findOne({ where: { userId, deleted: false } });
|
|
2120
2964
|
if (c) return c;
|
|
2121
2965
|
const u = await userRepo().findOne({ where: { id: userId } });
|
|
2122
2966
|
if (!u) return null;
|
|
2123
2967
|
const unclaimed = await contactRepo().findOne({
|
|
2124
|
-
where: { email: u.email, userId:
|
|
2968
|
+
where: { email: u.email, userId: IsNull3(), deleted: false }
|
|
2125
2969
|
});
|
|
2126
2970
|
if (unclaimed) {
|
|
2127
2971
|
await contactRepo().update(unclaimed.id, { userId });
|
|
@@ -2136,6 +2980,7 @@ function createStorefrontApiHandler(config) {
|
|
|
2136
2980
|
deleted: false
|
|
2137
2981
|
})
|
|
2138
2982
|
);
|
|
2983
|
+
await syncContactToErp(created);
|
|
2139
2984
|
return { id: created.id };
|
|
2140
2985
|
}
|
|
2141
2986
|
async function getOrCreateCart(req) {
|
|
@@ -2230,7 +3075,21 @@ function createStorefrontApiHandler(config) {
|
|
|
2230
3075
|
})
|
|
2231
3076
|
};
|
|
2232
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
|
+
}
|
|
2233
3091
|
function serializeProduct(p) {
|
|
3092
|
+
const seo = serializeSeo(p.seo);
|
|
2234
3093
|
return {
|
|
2235
3094
|
id: p.id,
|
|
2236
3095
|
name: p.name,
|
|
@@ -2241,7 +3100,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2241
3100
|
compareAtPrice: p.compareAtPrice,
|
|
2242
3101
|
status: p.status,
|
|
2243
3102
|
collectionId: p.collectionId,
|
|
2244
|
-
metadata: p.metadata
|
|
3103
|
+
metadata: p.metadata,
|
|
3104
|
+
...seo ? { seo } : {}
|
|
2245
3105
|
};
|
|
2246
3106
|
}
|
|
2247
3107
|
return {
|
|
@@ -2308,7 +3168,7 @@ function createStorefrontApiHandler(config) {
|
|
|
2308
3168
|
const byId = /^\d+$/.test(idOrSlug);
|
|
2309
3169
|
const product = await productRepo().findOne({
|
|
2310
3170
|
where: byId ? { id: parseInt(idOrSlug, 10), status: "available", deleted: false } : { slug: idOrSlug, status: "available", deleted: false },
|
|
2311
|
-
relations: ["attributes", "attributes.attribute"]
|
|
3171
|
+
relations: ["attributes", "attributes.attribute", "seo"]
|
|
2312
3172
|
});
|
|
2313
3173
|
if (!product) return json({ error: "Not found" }, { status: 404 });
|
|
2314
3174
|
const p = product;
|
|
@@ -2352,7 +3212,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2352
3212
|
const idOrSlug = path[1];
|
|
2353
3213
|
const byId = /^\d+$/.test(idOrSlug);
|
|
2354
3214
|
const collection = await collectionRepo().findOne({
|
|
2355
|
-
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"]
|
|
2356
3217
|
});
|
|
2357
3218
|
if (!collection) return json({ error: "Not found" }, { status: 404 });
|
|
2358
3219
|
const col = collection;
|
|
@@ -2360,12 +3221,14 @@ function createStorefrontApiHandler(config) {
|
|
|
2360
3221
|
where: { collectionId: col.id, status: "available", deleted: false },
|
|
2361
3222
|
order: { id: "ASC" }
|
|
2362
3223
|
});
|
|
3224
|
+
const colSeo = serializeSeo(col.seo);
|
|
2363
3225
|
return json({
|
|
2364
3226
|
id: col.id,
|
|
2365
3227
|
name: col.name,
|
|
2366
3228
|
slug: col.slug,
|
|
2367
3229
|
description: col.description,
|
|
2368
3230
|
image: col.image,
|
|
3231
|
+
...colSeo ? { seo: colSeo } : {},
|
|
2369
3232
|
products: products.map((p) => serializeProduct(p))
|
|
2370
3233
|
});
|
|
2371
3234
|
}
|
|
@@ -2403,6 +3266,7 @@ function createStorefrontApiHandler(config) {
|
|
|
2403
3266
|
await userRepo().update(uid, { name: b.name.trim() });
|
|
2404
3267
|
}
|
|
2405
3268
|
const updatedContact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
3269
|
+
if (updatedContact) await syncContactToErp(updatedContact);
|
|
2406
3270
|
const updatedUser = await userRepo().findOne({ where: { id: uid }, select: ["id", "name", "email"] });
|
|
2407
3271
|
return json({
|
|
2408
3272
|
user: updatedUser ? { id: updatedUser.id, name: updatedUser.name, email: updatedUser.email } : null,
|
|
@@ -2490,13 +3354,155 @@ function createStorefrontApiHandler(config) {
|
|
|
2490
3354
|
const email = record.email;
|
|
2491
3355
|
const user = await userRepo().findOne({ where: { email }, select: ["id", "blocked"] });
|
|
2492
3356
|
if (!user) return json({ error: "User not found" }, { status: 400 });
|
|
2493
|
-
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
|
+
});
|
|
2494
3470
|
await tokenRepo().delete({ email });
|
|
2495
3471
|
return json({ success: true, message: "Email verified. You can sign in." });
|
|
2496
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
|
+
}
|
|
2497
3501
|
if (path[0] === "register" && path.length === 1 && method === "POST") {
|
|
2498
3502
|
if (!config.hashPassword) return json({ error: "Registration not configured" }, { status: 501 });
|
|
2499
3503
|
const b = await req.json().catch(() => ({}));
|
|
3504
|
+
const capReg = await assertCaptchaOk(getCms, b, req, json);
|
|
3505
|
+
if (capReg) return capReg;
|
|
2500
3506
|
const name = typeof b.name === "string" ? b.name.trim() : "";
|
|
2501
3507
|
const email = typeof b.email === "string" ? b.email.trim().toLowerCase() : "";
|
|
2502
3508
|
const password = typeof b.password === "string" ? b.password : "";
|
|
@@ -2558,6 +3564,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2558
3564
|
}
|
|
2559
3565
|
if (path[0] === "cart" && path[1] === "items" && path.length === 2 && method === "POST") {
|
|
2560
3566
|
const body = await req.json().catch(() => ({}));
|
|
3567
|
+
const capCart = await assertCaptchaOk(getCms, body, req, json);
|
|
3568
|
+
if (capCart) return capCart;
|
|
2561
3569
|
const productId = Number(body.productId);
|
|
2562
3570
|
const quantity = Math.max(1, Number(body.quantity) || 1);
|
|
2563
3571
|
if (!Number.isFinite(productId)) return json({ error: "productId required" }, { status: 400 });
|
|
@@ -2757,6 +3765,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2757
3765
|
const { wishlist, setCookie, err } = await getOrCreateWishlist(req);
|
|
2758
3766
|
if (err) return err;
|
|
2759
3767
|
const b = await req.json().catch(() => ({}));
|
|
3768
|
+
const capWl = await assertCaptchaOk(getCms, b, req, json);
|
|
3769
|
+
if (capWl) return capWl;
|
|
2760
3770
|
const productId = Number(b.productId);
|
|
2761
3771
|
if (!Number.isFinite(productId)) return json({ error: "productId required" }, { status: 400 });
|
|
2762
3772
|
const wid = wishlist.id;
|
|
@@ -2775,6 +3785,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2775
3785
|
}
|
|
2776
3786
|
if (path[0] === "checkout" && path[1] === "order" && path.length === 2 && method === "POST") {
|
|
2777
3787
|
const b = await req.json().catch(() => ({}));
|
|
3788
|
+
const capOrd = await assertCaptchaOk(getCms, b, req, json);
|
|
3789
|
+
if (capOrd) return capOrd;
|
|
2778
3790
|
const u = await getSessionUser();
|
|
2779
3791
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
2780
3792
|
let contactId;
|
|
@@ -2788,8 +3800,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2788
3800
|
relations: ["items", "items.product"]
|
|
2789
3801
|
});
|
|
2790
3802
|
} else {
|
|
2791
|
-
const email = (b.email
|
|
2792
|
-
const name = (b.name
|
|
3803
|
+
const email = String(b.email ?? "").trim();
|
|
3804
|
+
const name = String(b.name ?? "").trim();
|
|
2793
3805
|
if (!email || !name) return json({ error: "name and email required for guest checkout" }, { status: 400 });
|
|
2794
3806
|
let contact = await contactRepo().findOne({ where: { email, deleted: false } });
|
|
2795
3807
|
if (contact && contact.userId != null) {
|
|
@@ -2800,13 +3812,19 @@ function createStorefrontApiHandler(config) {
|
|
|
2800
3812
|
contactRepo().create({
|
|
2801
3813
|
name,
|
|
2802
3814
|
email,
|
|
2803
|
-
phone: b.phone
|
|
3815
|
+
phone: b.phone != null && b.phone !== "" ? String(b.phone) : null,
|
|
2804
3816
|
userId: null,
|
|
2805
3817
|
deleted: false
|
|
2806
3818
|
})
|
|
2807
3819
|
);
|
|
2808
|
-
} 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
|
+
});
|
|
2809
3825
|
contactId = contact.id;
|
|
3826
|
+
const guestForErp = await contactRepo().findOne({ where: { id: contactId } });
|
|
3827
|
+
if (guestForErp) await syncContactToErp(guestForErp);
|
|
2810
3828
|
const cookies = parseCookies(req.headers.get("cookie"));
|
|
2811
3829
|
const guestToken = cookies[cookieName];
|
|
2812
3830
|
if (!guestToken) return json({ error: "Cart not found" }, { status: 400 });
|
|
@@ -2834,10 +3852,12 @@ function createStorefrontApiHandler(config) {
|
|
|
2834
3852
|
const cartId = cart.id;
|
|
2835
3853
|
const ord = await orderRepo().save(
|
|
2836
3854
|
orderRepo().create({
|
|
2837
|
-
orderNumber:
|
|
3855
|
+
orderNumber: temporaryOrderNumberPlaceholder(),
|
|
3856
|
+
orderKind: "sale",
|
|
3857
|
+
parentOrderId: null,
|
|
2838
3858
|
contactId,
|
|
2839
|
-
billingAddressId: b.billingAddressId
|
|
2840
|
-
shippingAddressId: b.shippingAddressId
|
|
3859
|
+
billingAddressId: typeof b.billingAddressId === "number" ? b.billingAddressId : null,
|
|
3860
|
+
shippingAddressId: typeof b.shippingAddressId === "number" ? b.shippingAddressId : null,
|
|
2841
3861
|
status: "pending",
|
|
2842
3862
|
subtotal,
|
|
2843
3863
|
tax: 0,
|
|
@@ -2848,6 +3868,9 @@ function createStorefrontApiHandler(config) {
|
|
|
2848
3868
|
})
|
|
2849
3869
|
);
|
|
2850
3870
|
const oid = ord.id;
|
|
3871
|
+
await orderRepo().update(oid, {
|
|
3872
|
+
orderNumber: buildCanonicalOrderNumber("sale", oid, ord.createdAt ?? /* @__PURE__ */ new Date())
|
|
3873
|
+
});
|
|
2851
3874
|
for (const line of lines) {
|
|
2852
3875
|
await orderItemRepo().save(
|
|
2853
3876
|
orderItemRepo().create({
|
|
@@ -2869,6 +3892,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2869
3892
|
}
|
|
2870
3893
|
if (path[0] === "checkout" && path.length === 1 && method === "POST") {
|
|
2871
3894
|
const b = await req.json().catch(() => ({}));
|
|
3895
|
+
const capChk = await assertCaptchaOk(getCms, b, req, json);
|
|
3896
|
+
if (capChk) return capChk;
|
|
2872
3897
|
const u = await getSessionUser();
|
|
2873
3898
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
2874
3899
|
let contactId;
|
|
@@ -2882,8 +3907,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2882
3907
|
relations: ["items", "items.product"]
|
|
2883
3908
|
});
|
|
2884
3909
|
} else {
|
|
2885
|
-
const email = (b.email
|
|
2886
|
-
const name = (b.name
|
|
3910
|
+
const email = String(b.email ?? "").trim();
|
|
3911
|
+
const name = String(b.name ?? "").trim();
|
|
2887
3912
|
if (!email || !name) return json({ error: "name and email required for guest checkout" }, { status: 400 });
|
|
2888
3913
|
let contact = await contactRepo().findOne({ where: { email, deleted: false } });
|
|
2889
3914
|
if (contact && contact.userId != null) {
|
|
@@ -2894,13 +3919,19 @@ function createStorefrontApiHandler(config) {
|
|
|
2894
3919
|
contactRepo().create({
|
|
2895
3920
|
name,
|
|
2896
3921
|
email,
|
|
2897
|
-
phone: b.phone
|
|
3922
|
+
phone: b.phone != null && b.phone !== "" ? String(b.phone) : null,
|
|
2898
3923
|
userId: null,
|
|
2899
3924
|
deleted: false
|
|
2900
3925
|
})
|
|
2901
3926
|
);
|
|
2902
|
-
} 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
|
+
});
|
|
2903
3932
|
contactId = contact.id;
|
|
3933
|
+
const guestForErp2 = await contactRepo().findOne({ where: { id: contactId } });
|
|
3934
|
+
if (guestForErp2) await syncContactToErp(guestForErp2);
|
|
2904
3935
|
const cookies = parseCookies(req.headers.get("cookie"));
|
|
2905
3936
|
const guestToken = cookies[cookieName];
|
|
2906
3937
|
if (!guestToken) return json({ error: "Cart not found" }, { status: 400 });
|
|
@@ -2927,10 +3958,12 @@ function createStorefrontApiHandler(config) {
|
|
|
2927
3958
|
const total = subtotal;
|
|
2928
3959
|
const ord = await orderRepo().save(
|
|
2929
3960
|
orderRepo().create({
|
|
2930
|
-
orderNumber:
|
|
3961
|
+
orderNumber: temporaryOrderNumberPlaceholder(),
|
|
3962
|
+
orderKind: "sale",
|
|
3963
|
+
parentOrderId: null,
|
|
2931
3964
|
contactId,
|
|
2932
|
-
billingAddressId: b.billingAddressId
|
|
2933
|
-
shippingAddressId: b.shippingAddressId
|
|
3965
|
+
billingAddressId: typeof b.billingAddressId === "number" ? b.billingAddressId : null,
|
|
3966
|
+
shippingAddressId: typeof b.shippingAddressId === "number" ? b.shippingAddressId : null,
|
|
2934
3967
|
status: "pending",
|
|
2935
3968
|
subtotal,
|
|
2936
3969
|
tax: 0,
|
|
@@ -2940,6 +3973,9 @@ function createStorefrontApiHandler(config) {
|
|
|
2940
3973
|
})
|
|
2941
3974
|
);
|
|
2942
3975
|
const oid = ord.id;
|
|
3976
|
+
await orderRepo().update(oid, {
|
|
3977
|
+
orderNumber: buildCanonicalOrderNumber("sale", oid, ord.createdAt ?? /* @__PURE__ */ new Date())
|
|
3978
|
+
});
|
|
2943
3979
|
for (const line of lines) {
|
|
2944
3980
|
await orderItemRepo().save(
|
|
2945
3981
|
orderItemRepo().create({
|
|
@@ -2967,7 +4003,7 @@ function createStorefrontApiHandler(config) {
|
|
|
2967
4003
|
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
2968
4004
|
if (!contact) return json({ orders: [] });
|
|
2969
4005
|
const orders = await orderRepo().find({
|
|
2970
|
-
where: { contactId: contact.id, deleted: false },
|
|
4006
|
+
where: { contactId: contact.id, deleted: false, orderKind: "sale" },
|
|
2971
4007
|
order: { createdAt: "DESC" },
|
|
2972
4008
|
take: 50
|
|
2973
4009
|
});
|
|
@@ -3002,6 +4038,20 @@ function createStorefrontApiHandler(config) {
|
|
|
3002
4038
|
})
|
|
3003
4039
|
});
|
|
3004
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
|
+
}
|
|
3005
4055
|
if (path[0] === "orders" && path.length === 2 && method === "GET") {
|
|
3006
4056
|
const u = await getSessionUser();
|
|
3007
4057
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
@@ -3009,11 +4059,20 @@ function createStorefrontApiHandler(config) {
|
|
|
3009
4059
|
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
3010
4060
|
if (!contact) return json({ error: "Not found" }, { status: 404 });
|
|
3011
4061
|
const orderId = parseInt(path[1], 10);
|
|
3012
|
-
|
|
4062
|
+
let order = await orderRepo().findOne({
|
|
3013
4063
|
where: { id: orderId, contactId: contact.id, deleted: false },
|
|
3014
4064
|
relations: ["items", "items.product"]
|
|
3015
4065
|
});
|
|
3016
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 });
|
|
3017
4076
|
const o = order;
|
|
3018
4077
|
const lines = (o.items || []).map((line) => {
|
|
3019
4078
|
const p = line.product;
|
|
@@ -3032,10 +4091,22 @@ function createStorefrontApiHandler(config) {
|
|
|
3032
4091
|
} : null
|
|
3033
4092
|
};
|
|
3034
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 ?? "") : "";
|
|
3035
4104
|
return json({
|
|
3036
4105
|
order: {
|
|
3037
4106
|
id: o.id,
|
|
3038
4107
|
orderNumber: o.orderNumber,
|
|
4108
|
+
orderKind: kind,
|
|
4109
|
+
parentOrderId: o.parentOrderId ?? null,
|
|
3039
4110
|
status: o.status,
|
|
3040
4111
|
subtotal: o.subtotal,
|
|
3041
4112
|
tax: o.tax,
|
|
@@ -3043,8 +4114,18 @@ function createStorefrontApiHandler(config) {
|
|
|
3043
4114
|
total: o.total,
|
|
3044
4115
|
currency: o.currency,
|
|
3045
4116
|
createdAt: o.createdAt,
|
|
4117
|
+
metadata: o.metadata ?? null,
|
|
3046
4118
|
items: lines
|
|
3047
|
-
}
|
|
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
|
|
3048
4129
|
});
|
|
3049
4130
|
}
|
|
3050
4131
|
return json({ error: "Not found" }, { status: 404 });
|
|
@@ -3072,6 +4153,7 @@ export {
|
|
|
3072
4153
|
createUserAuthApiRouter,
|
|
3073
4154
|
createUserAvatarHandler,
|
|
3074
4155
|
createUserProfileHandler,
|
|
3075
|
-
createUsersApiHandlers
|
|
4156
|
+
createUsersApiHandlers,
|
|
4157
|
+
getPublicSettingsGroup
|
|
3076
4158
|
};
|
|
3077
4159
|
//# sourceMappingURL=api.js.map
|