@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.cjs
CHANGED
|
@@ -30,6 +30,23 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
));
|
|
31
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
32
|
|
|
33
|
+
// src/plugins/erp/erp-config-enabled.ts
|
|
34
|
+
async function isErpIntegrationEnabled(cms, dataSource, entityMap) {
|
|
35
|
+
if (!cms.getPlugin("erp")) return false;
|
|
36
|
+
const configRepo = dataSource.getRepository(entityMap.configs);
|
|
37
|
+
const cfgRows = await configRepo.find({ where: { settings: "erp", deleted: false } });
|
|
38
|
+
for (const row of cfgRows) {
|
|
39
|
+
const r = row;
|
|
40
|
+
if (r.key === "enabled" && r.value === "false") return false;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
var init_erp_config_enabled = __esm({
|
|
45
|
+
"src/plugins/erp/erp-config-enabled.ts"() {
|
|
46
|
+
"use strict";
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
33
50
|
// src/plugins/email/email-queue.ts
|
|
34
51
|
var email_queue_exports = {};
|
|
35
52
|
__export(email_queue_exports, {
|
|
@@ -68,9 +85,9 @@ async function queueEmail(cms, payload) {
|
|
|
68
85
|
}
|
|
69
86
|
}
|
|
70
87
|
async function queueOrderPlacedEmails(cms, payload) {
|
|
71
|
-
const { orderNumber
|
|
88
|
+
const { orderNumber, total, currency, customerName, customerEmail, salesTeamEmails, companyDetails, lineItems } = payload;
|
|
72
89
|
const base = {
|
|
73
|
-
orderNumber
|
|
90
|
+
orderNumber,
|
|
74
91
|
total: total != null ? String(total) : void 0,
|
|
75
92
|
currency,
|
|
76
93
|
customerName,
|
|
@@ -118,6 +135,141 @@ var init_email_queue = __esm({
|
|
|
118
135
|
}
|
|
119
136
|
});
|
|
120
137
|
|
|
138
|
+
// src/plugins/erp/erp-response-map.ts
|
|
139
|
+
function pickString(o, keys) {
|
|
140
|
+
for (const k of keys) {
|
|
141
|
+
const v = o[k];
|
|
142
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
143
|
+
}
|
|
144
|
+
return void 0;
|
|
145
|
+
}
|
|
146
|
+
function unwrapErpReadData(json) {
|
|
147
|
+
if (!json || typeof json !== "object") return null;
|
|
148
|
+
const o = json;
|
|
149
|
+
const d = o.data;
|
|
150
|
+
if (d && typeof d === "object" && !Array.isArray(d)) return d;
|
|
151
|
+
return o;
|
|
152
|
+
}
|
|
153
|
+
function firstObject(data, keys) {
|
|
154
|
+
for (const k of keys) {
|
|
155
|
+
const v = data[k];
|
|
156
|
+
if (v && typeof v === "object" && !Array.isArray(v)) return v;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
function extractEvents(src) {
|
|
161
|
+
const timeline = src.timeline ?? src.events ?? src.history ?? src.trackingEvents;
|
|
162
|
+
if (!Array.isArray(timeline) || !timeline.length) return void 0;
|
|
163
|
+
const events = [];
|
|
164
|
+
for (const row of timeline) {
|
|
165
|
+
if (!row || typeof row !== "object") continue;
|
|
166
|
+
const r = row;
|
|
167
|
+
const at = pickString(r, ["at", "timestamp", "date", "occurredAt"]) ?? (r.at instanceof Date ? r.at.toISOString() : void 0);
|
|
168
|
+
const label = pickString(r, ["label", "status", "title", "message", "description"]);
|
|
169
|
+
const detail = pickString(r, ["detail", "notes", "description"]);
|
|
170
|
+
if (at || label || detail) events.push({ at, label, detail });
|
|
171
|
+
}
|
|
172
|
+
return events.length ? events : void 0;
|
|
173
|
+
}
|
|
174
|
+
function mapErpPayloadToFulfillment(data) {
|
|
175
|
+
const nested = firstObject(data, ["fulfillment", "packaging", "shipment", "shipping", "delivery"]);
|
|
176
|
+
const src = nested || data;
|
|
177
|
+
const status = pickString(src, ["status", "fulfillmentStatus", "state", "label", "packagingStatus"]);
|
|
178
|
+
const trackingId = pickString(src, ["trackingId", "tracking_id", "trackingNumber", "awb", "trackingUrl"]);
|
|
179
|
+
const events = extractEvents(src);
|
|
180
|
+
if (!status && !trackingId && !(events && events.length)) return void 0;
|
|
181
|
+
return { status, trackingId, events };
|
|
182
|
+
}
|
|
183
|
+
function mapErpPayloadToInvoiceNumber(data) {
|
|
184
|
+
const nested = firstObject(data, ["invoice", "latestInvoice", "postedInvoice"]);
|
|
185
|
+
const src = nested || data;
|
|
186
|
+
return pickString(src, ["invoiceNumber", "invoice_number", "number", "name", "id"]);
|
|
187
|
+
}
|
|
188
|
+
function extractChildOrderRefsFromSalePayload(data) {
|
|
189
|
+
const lists = [data.returns, data.returnOrders, data.relatedReturns, data.childOrders, data.children];
|
|
190
|
+
const seen = /* @__PURE__ */ new Set();
|
|
191
|
+
const out = [];
|
|
192
|
+
for (const list of lists) {
|
|
193
|
+
if (!Array.isArray(list)) continue;
|
|
194
|
+
for (const item of list) {
|
|
195
|
+
if (!item || typeof item !== "object") continue;
|
|
196
|
+
const o = item;
|
|
197
|
+
const ref = pickString(o, ["platformReturnId", "platform_return_id", "refId", "ref_id"]) ?? (typeof o.id === "string" ? o.id : void 0);
|
|
198
|
+
if (!ref || seen.has(ref)) continue;
|
|
199
|
+
seen.add(ref);
|
|
200
|
+
const t = (pickString(o, ["kind", "type", "orderKind"]) || "").toLowerCase();
|
|
201
|
+
const orderKind = /replac/.test(t) ? "replacement" : "return";
|
|
202
|
+
out.push({ ref, orderKind });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
var init_erp_response_map = __esm({
|
|
208
|
+
"src/plugins/erp/erp-response-map.ts"() {
|
|
209
|
+
"use strict";
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// src/plugins/erp/erp-order-invoice.ts
|
|
214
|
+
var erp_order_invoice_exports = {};
|
|
215
|
+
__export(erp_order_invoice_exports, {
|
|
216
|
+
streamOrderInvoicePdf: () => streamOrderInvoicePdf
|
|
217
|
+
});
|
|
218
|
+
function pickInvoiceId(data) {
|
|
219
|
+
const nested = data.invoice && typeof data.invoice === "object" && !Array.isArray(data.invoice) ? data.invoice : null;
|
|
220
|
+
const src = nested || data;
|
|
221
|
+
for (const k of ["invoiceId", "invoice_id", "id"]) {
|
|
222
|
+
const v = src[k];
|
|
223
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
224
|
+
}
|
|
225
|
+
return void 0;
|
|
226
|
+
}
|
|
227
|
+
async function streamOrderInvoicePdf(cms, dataSource, entityMap, orderId, options) {
|
|
228
|
+
const jsonErr = (msg, status) => new Response(JSON.stringify({ error: msg }), {
|
|
229
|
+
status,
|
|
230
|
+
headers: { "Content-Type": "application/json" }
|
|
231
|
+
});
|
|
232
|
+
const on = await isErpIntegrationEnabled(cms, dataSource, entityMap);
|
|
233
|
+
if (!on) return jsonErr("Invoice not available", 503);
|
|
234
|
+
const erp = cms.getPlugin("erp");
|
|
235
|
+
if (!erp?.submission) return jsonErr("Invoice not available", 503);
|
|
236
|
+
const orderRepo = dataSource.getRepository(entityMap.orders);
|
|
237
|
+
const order = await orderRepo.findOne({ where: { id: orderId, deleted: false } });
|
|
238
|
+
if (!order) return jsonErr("Not found", 404);
|
|
239
|
+
const kind = order.orderKind || "sale";
|
|
240
|
+
if (kind !== "sale") return jsonErr("Invoice only for sale orders", 400);
|
|
241
|
+
if (options.ownerContactId != null && order.contactId !== options.ownerContactId) {
|
|
242
|
+
return jsonErr("Not found", 404);
|
|
243
|
+
}
|
|
244
|
+
const meta = order.metadata && typeof order.metadata === "object" && !Array.isArray(order.metadata) ? order.metadata : {};
|
|
245
|
+
const inv = meta.invoice && typeof meta.invoice === "object" && !Array.isArray(meta.invoice) ? meta.invoice : {};
|
|
246
|
+
let invoiceId = typeof inv.invoiceId === "string" ? inv.invoiceId.trim() : "";
|
|
247
|
+
if (!invoiceId) {
|
|
248
|
+
const refId = String(order.orderNumber || "");
|
|
249
|
+
const r = await erp.submission.postErpReadAction("get-invoice", { platformOrderId: refId });
|
|
250
|
+
const d = r.ok ? unwrapErpReadData(r.json) : null;
|
|
251
|
+
invoiceId = d ? pickInvoiceId(d) || "" : "";
|
|
252
|
+
}
|
|
253
|
+
if (!invoiceId) return jsonErr("Invoice not ready", 404);
|
|
254
|
+
const pdf = await erp.submission.fetchInvoicePdf(invoiceId);
|
|
255
|
+
if (!pdf.ok || !pdf.buffer) return jsonErr(pdf.error || "PDF fetch failed", 502);
|
|
256
|
+
const filename = `invoice-${orderId}.pdf`;
|
|
257
|
+
return new Response(pdf.buffer, {
|
|
258
|
+
status: 200,
|
|
259
|
+
headers: {
|
|
260
|
+
"Content-Type": pdf.contentType || "application/pdf",
|
|
261
|
+
"Content-Disposition": `attachment; filename="${filename}"`
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
var init_erp_order_invoice = __esm({
|
|
266
|
+
"src/plugins/erp/erp-order-invoice.ts"() {
|
|
267
|
+
"use strict";
|
|
268
|
+
init_erp_response_map();
|
|
269
|
+
init_erp_config_enabled();
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
121
273
|
// src/api/index.ts
|
|
122
274
|
var api_exports = {};
|
|
123
275
|
__export(api_exports, {
|
|
@@ -138,12 +290,82 @@ __export(api_exports, {
|
|
|
138
290
|
createUserAuthApiRouter: () => createUserAuthApiRouter,
|
|
139
291
|
createUserAvatarHandler: () => createUserAvatarHandler,
|
|
140
292
|
createUserProfileHandler: () => createUserProfileHandler,
|
|
141
|
-
createUsersApiHandlers: () => createUsersApiHandlers
|
|
293
|
+
createUsersApiHandlers: () => createUsersApiHandlers,
|
|
294
|
+
getPublicSettingsGroup: () => getPublicSettingsGroup
|
|
142
295
|
});
|
|
143
296
|
module.exports = __toCommonJS(api_exports);
|
|
144
297
|
|
|
145
298
|
// src/api/crud.ts
|
|
146
299
|
var import_typeorm = require("typeorm");
|
|
300
|
+
|
|
301
|
+
// src/plugins/erp/erp-queue.ts
|
|
302
|
+
var ERP_QUEUE_NAME = "erp";
|
|
303
|
+
async function queueErp(cms, payload) {
|
|
304
|
+
const queue = cms.getPlugin("queue");
|
|
305
|
+
if (!queue) return;
|
|
306
|
+
await queue.add(ERP_QUEUE_NAME, payload);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/plugins/erp/erp-contact-sync.ts
|
|
310
|
+
function splitName(full) {
|
|
311
|
+
const t = (full || "").trim();
|
|
312
|
+
if (!t) return { firstName: "Contact", lastName: "" };
|
|
313
|
+
const parts = t.split(/\s+/);
|
|
314
|
+
if (parts.length === 1) return { firstName: parts[0], lastName: "" };
|
|
315
|
+
return { firstName: parts[0], lastName: parts.slice(1).join(" ") };
|
|
316
|
+
}
|
|
317
|
+
async function queueErpCreateContactIfEnabled(cms, dataSource, entityMap, input) {
|
|
318
|
+
try {
|
|
319
|
+
const configRepo = dataSource.getRepository(entityMap.configs);
|
|
320
|
+
const cfgRows = await configRepo.find({ where: { settings: "erp", deleted: false } });
|
|
321
|
+
for (const row of cfgRows) {
|
|
322
|
+
const r = row;
|
|
323
|
+
if (r.key === "enabled" && r.value === "false") return;
|
|
324
|
+
}
|
|
325
|
+
if (!cms.getPlugin("erp")) return;
|
|
326
|
+
const email = (input.email ?? "").trim();
|
|
327
|
+
if (!email) return;
|
|
328
|
+
const { firstName, lastName } = splitName(input.name);
|
|
329
|
+
await queueErp(cms, {
|
|
330
|
+
kind: "createContact",
|
|
331
|
+
contact: {
|
|
332
|
+
email,
|
|
333
|
+
firstName,
|
|
334
|
+
lastName,
|
|
335
|
+
phone: input.phone?.trim() || void 0,
|
|
336
|
+
companyName: input.company?.trim() || void 0,
|
|
337
|
+
type: input.type?.trim() || void 0,
|
|
338
|
+
notes: input.notes?.trim() || void 0,
|
|
339
|
+
tags: input.tags?.length ? [...input.tags] : void 0
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
} catch {
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/plugins/erp/erp-product-sync.ts
|
|
347
|
+
init_erp_config_enabled();
|
|
348
|
+
async function queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, product) {
|
|
349
|
+
try {
|
|
350
|
+
const sku = typeof product.sku === "string" ? product.sku.trim() : "";
|
|
351
|
+
if (!sku) return;
|
|
352
|
+
const on = await isErpIntegrationEnabled(cms, dataSource, entityMap);
|
|
353
|
+
if (!on) return;
|
|
354
|
+
const payload = {
|
|
355
|
+
sku,
|
|
356
|
+
title: product.name || sku,
|
|
357
|
+
name: product.name,
|
|
358
|
+
description: typeof product.metadata === "object" && product.metadata && "description" in product.metadata ? String(product.metadata.description ?? "") : void 0,
|
|
359
|
+
hsn_number: product.hsn,
|
|
360
|
+
is_active: product.status === "available",
|
|
361
|
+
metadata: product.metadata ?? void 0
|
|
362
|
+
};
|
|
363
|
+
await queueErp(cms, { kind: "productUpsert", product: payload });
|
|
364
|
+
} catch {
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/api/crud.ts
|
|
147
369
|
var DATE_COLUMN_TYPES = /* @__PURE__ */ new Set([
|
|
148
370
|
"date",
|
|
149
371
|
"datetime",
|
|
@@ -196,8 +418,27 @@ function buildSearchWhereClause(repo, search) {
|
|
|
196
418
|
if (ors.length === 0) return {};
|
|
197
419
|
return ors.length === 1 ? ors[0] : ors;
|
|
198
420
|
}
|
|
421
|
+
function makeContactErpSync(dataSource, entityMap, getCms) {
|
|
422
|
+
return async function syncContactRowToErp(row) {
|
|
423
|
+
if (!getCms) return;
|
|
424
|
+
try {
|
|
425
|
+
const cms = await getCms();
|
|
426
|
+
const c = row;
|
|
427
|
+
await queueErpCreateContactIfEnabled(cms, dataSource, entityMap, {
|
|
428
|
+
name: c.name,
|
|
429
|
+
email: c.email,
|
|
430
|
+
phone: c.phone,
|
|
431
|
+
type: c.type,
|
|
432
|
+
company: c.company,
|
|
433
|
+
notes: c.notes
|
|
434
|
+
});
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
199
439
|
function createCrudHandler(dataSource, entityMap, options) {
|
|
200
|
-
const { requireAuth, json, requireEntityPermission: reqPerm } = options;
|
|
440
|
+
const { requireAuth, json, requireEntityPermission: reqPerm, getCms } = options;
|
|
441
|
+
const syncContactRowToErp = makeContactErpSync(dataSource, entityMap, getCms);
|
|
201
442
|
async function authz(req, resource, action) {
|
|
202
443
|
const authError = await requireAuth(req);
|
|
203
444
|
if (authError) return authError;
|
|
@@ -399,6 +640,13 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
399
640
|
const repo = dataSource.getRepository(entity);
|
|
400
641
|
sanitizeBodyForEntity(repo, body);
|
|
401
642
|
const created = await repo.save(repo.create(body));
|
|
643
|
+
if (resource === "contacts") {
|
|
644
|
+
await syncContactRowToErp(created);
|
|
645
|
+
}
|
|
646
|
+
if (resource === "products" && getCms) {
|
|
647
|
+
const cms = await getCms();
|
|
648
|
+
await queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, created);
|
|
649
|
+
}
|
|
402
650
|
return json(created, { status: 201 });
|
|
403
651
|
},
|
|
404
652
|
async GET_METADATA(req, resource) {
|
|
@@ -505,7 +753,8 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
505
753
|
};
|
|
506
754
|
}
|
|
507
755
|
function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
508
|
-
const { requireAuth, json, requireEntityPermission: reqPerm } = options;
|
|
756
|
+
const { requireAuth, json, requireEntityPermission: reqPerm, getCms } = options;
|
|
757
|
+
const syncContactRowToErp = makeContactErpSync(dataSource, entityMap, getCms);
|
|
509
758
|
async function authz(req, resource, action) {
|
|
510
759
|
const authError = await requireAuth(req);
|
|
511
760
|
if (authError) return authError;
|
|
@@ -528,7 +777,11 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
528
777
|
relations: ["contact", "billingAddress", "shippingAddress", "items", "items.product", "items.product.collection", "payments"]
|
|
529
778
|
});
|
|
530
779
|
if (!order) return json({ message: "Not found" }, { status: 404 });
|
|
531
|
-
|
|
780
|
+
const relatedOrders = await repo.find({
|
|
781
|
+
where: { parentOrderId: Number(id), deleted: false },
|
|
782
|
+
order: { id: "ASC" }
|
|
783
|
+
});
|
|
784
|
+
return json({ ...order, relatedOrders });
|
|
532
785
|
}
|
|
533
786
|
if (resource === "contacts") {
|
|
534
787
|
const contact = await repo.findOne({
|
|
@@ -661,6 +914,13 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
661
914
|
await repo.update(numericId, updatePayload);
|
|
662
915
|
}
|
|
663
916
|
const updated = await repo.findOne({ where: { id: numericId } });
|
|
917
|
+
if (resource === "contacts" && updated) {
|
|
918
|
+
await syncContactRowToErp(updated);
|
|
919
|
+
}
|
|
920
|
+
if (resource === "products" && updated && getCms) {
|
|
921
|
+
const cms = await getCms();
|
|
922
|
+
await queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, updated);
|
|
923
|
+
}
|
|
664
924
|
return updated ? json(updated) : json({ message: "Not found" }, { status: 404 });
|
|
665
925
|
},
|
|
666
926
|
async DELETE(req, resource, id) {
|
|
@@ -825,6 +1085,24 @@ function createUserAuthApiRouter(config) {
|
|
|
825
1085
|
// src/api/cms-handlers.ts
|
|
826
1086
|
var import_typeorm3 = require("typeorm");
|
|
827
1087
|
init_email_queue();
|
|
1088
|
+
|
|
1089
|
+
// src/plugins/captcha/assert.ts
|
|
1090
|
+
async function assertCaptchaOk(getCms, body, req, json) {
|
|
1091
|
+
if (!getCms) return null;
|
|
1092
|
+
let cms;
|
|
1093
|
+
try {
|
|
1094
|
+
cms = await getCms();
|
|
1095
|
+
} catch {
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
const svc = cms.getPlugin("captcha");
|
|
1099
|
+
if (!svc || typeof svc.verify !== "function") return null;
|
|
1100
|
+
const result = await svc.verify(body, req);
|
|
1101
|
+
if (result.ok) return null;
|
|
1102
|
+
return json({ error: result.message }, { status: result.status });
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// src/api/cms-handlers.ts
|
|
828
1106
|
function createDashboardStatsHandler(config) {
|
|
829
1107
|
const { dataSource, entityMap, json, requireAuth, requirePermission, requireEntityPermission } = config;
|
|
830
1108
|
return async function GET(req) {
|
|
@@ -1066,6 +1344,32 @@ function createFormSaveHandlers(config) {
|
|
|
1066
1344
|
}
|
|
1067
1345
|
};
|
|
1068
1346
|
}
|
|
1347
|
+
async function isErpIntegrationEnabled2(dataSource, entityMap) {
|
|
1348
|
+
const repo = dataSource.getRepository(entityMap.configs);
|
|
1349
|
+
const rows = await repo.find({ where: { settings: "erp", deleted: false } });
|
|
1350
|
+
for (const row of rows) {
|
|
1351
|
+
const r = row;
|
|
1352
|
+
if (r.key === "enabled") return r.value !== "false";
|
|
1353
|
+
}
|
|
1354
|
+
return true;
|
|
1355
|
+
}
|
|
1356
|
+
async function getErpOpportunityFormIds(dataSource, entityMap) {
|
|
1357
|
+
const repo = dataSource.getRepository(entityMap.configs);
|
|
1358
|
+
const row = await repo.findOne({
|
|
1359
|
+
where: { settings: "erp", key: "opportunityFormIds", deleted: false }
|
|
1360
|
+
});
|
|
1361
|
+
if (!row) return null;
|
|
1362
|
+
const raw = (row.value ?? "").trim();
|
|
1363
|
+
if (!raw) return [];
|
|
1364
|
+
try {
|
|
1365
|
+
const parsed = JSON.parse(raw);
|
|
1366
|
+
if (!Array.isArray(parsed)) return [];
|
|
1367
|
+
const ids = parsed.map((x) => typeof x === "number" ? x : Number(x)).filter((n) => Number.isInteger(n) && n > 0);
|
|
1368
|
+
return [...new Set(ids)];
|
|
1369
|
+
} catch {
|
|
1370
|
+
return [];
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1069
1373
|
function createFormSubmissionGetByIdHandler(config) {
|
|
1070
1374
|
const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
|
|
1071
1375
|
return async function GET(req, id) {
|
|
@@ -1154,13 +1458,15 @@ function pickContactFromSubmission(fields, data) {
|
|
|
1154
1458
|
return { name: name || email, email, phone: phone || null };
|
|
1155
1459
|
}
|
|
1156
1460
|
function createFormSubmissionHandler(config) {
|
|
1157
|
-
const { dataSource, entityMap, json } = config;
|
|
1461
|
+
const { dataSource, entityMap, json, getCms } = config;
|
|
1158
1462
|
return async function POST(req) {
|
|
1159
1463
|
try {
|
|
1160
1464
|
const body = await req.json();
|
|
1161
1465
|
if (!body || typeof body !== "object") {
|
|
1162
1466
|
return json({ error: "Invalid request payload" }, { status: 400 });
|
|
1163
1467
|
}
|
|
1468
|
+
const captchaErr = await assertCaptchaOk(getCms, body, req, json);
|
|
1469
|
+
if (captchaErr) return captchaErr;
|
|
1164
1470
|
const formId = typeof body.formId === "number" ? body.formId : Number(body.formId);
|
|
1165
1471
|
if (!Number.isInteger(formId) || formId <= 0) {
|
|
1166
1472
|
return json({ error: "formId is required and must be a positive integer" }, { status: 400 });
|
|
@@ -1227,28 +1533,44 @@ function createFormSubmissionHandler(config) {
|
|
|
1227
1533
|
contactEmail = contactData.email;
|
|
1228
1534
|
}
|
|
1229
1535
|
}
|
|
1230
|
-
if (config.getCms
|
|
1536
|
+
if (config.getCms) {
|
|
1231
1537
|
try {
|
|
1232
1538
|
const cms = await config.getCms();
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1539
|
+
if (config.getCompanyDetails && config.getRecipientForChannel) {
|
|
1540
|
+
const to = await config.getRecipientForChannel("crm");
|
|
1541
|
+
if (to) {
|
|
1542
|
+
const companyDetails = await config.getCompanyDetails();
|
|
1543
|
+
const formFieldRows = activeFields.map((f) => ({
|
|
1544
|
+
label: f.label && String(f.label).trim() || `Field ${f.id}`,
|
|
1545
|
+
value: formatSubmissionFieldValue(data[String(f.id)])
|
|
1546
|
+
}));
|
|
1547
|
+
await queueEmail(cms, {
|
|
1548
|
+
to,
|
|
1549
|
+
templateName: "formSubmission",
|
|
1550
|
+
ctx: {
|
|
1551
|
+
formName,
|
|
1552
|
+
contactName,
|
|
1553
|
+
contactEmail,
|
|
1554
|
+
formData: data,
|
|
1555
|
+
formFieldRows,
|
|
1556
|
+
companyDetails: companyDetails ?? {}
|
|
1557
|
+
}
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
if (await isErpIntegrationEnabled2(dataSource, entityMap)) {
|
|
1562
|
+
const erp = cms.getPlugin("erp");
|
|
1563
|
+
if (erp) {
|
|
1564
|
+
const contact = erp.submission.extractContactData(data, activeFields);
|
|
1565
|
+
if (contact?.email?.trim()) {
|
|
1566
|
+
const opportunityFormIds = await getErpOpportunityFormIds(dataSource, entityMap);
|
|
1567
|
+
const asOpportunity = opportunityFormIds != null && opportunityFormIds.length > 0 && opportunityFormIds.includes(formId);
|
|
1568
|
+
await queueErp(
|
|
1569
|
+
cms,
|
|
1570
|
+
asOpportunity ? { kind: "formOpportunity", contact } : { kind: "lead", contact }
|
|
1571
|
+
);
|
|
1250
1572
|
}
|
|
1251
|
-
}
|
|
1573
|
+
}
|
|
1252
1574
|
}
|
|
1253
1575
|
} catch {
|
|
1254
1576
|
}
|
|
@@ -1487,6 +1809,24 @@ function simpleDecrypt(encoded, key) {
|
|
|
1487
1809
|
for (let i = 0; i < buf.length; i++) out[i] = buf[i] ^ keyBuf[i % keyBuf.length];
|
|
1488
1810
|
return out.toString("utf8");
|
|
1489
1811
|
}
|
|
1812
|
+
async function getPublicSettingsGroup(config, group) {
|
|
1813
|
+
const { dataSource, entityMap, encryptionKey } = config;
|
|
1814
|
+
const repo = dataSource.getRepository(entityMap.configs);
|
|
1815
|
+
const rows = await repo.find({ where: { settings: group, deleted: false } });
|
|
1816
|
+
const result = {};
|
|
1817
|
+
for (const row of rows) {
|
|
1818
|
+
const r = row;
|
|
1819
|
+
let val = r.value;
|
|
1820
|
+
if (r.encrypted && encryptionKey) {
|
|
1821
|
+
try {
|
|
1822
|
+
val = simpleDecrypt(val, encryptionKey);
|
|
1823
|
+
} catch {
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
result[r.key] = val;
|
|
1827
|
+
}
|
|
1828
|
+
return result;
|
|
1829
|
+
}
|
|
1490
1830
|
function createSettingsApiHandlers(config) {
|
|
1491
1831
|
const { dataSource, entityMap, json, requireAuth, encryptionKey, publicGetGroups } = config;
|
|
1492
1832
|
const configRepo = () => dataSource.getRepository(entityMap.configs);
|
|
@@ -1496,6 +1836,13 @@ function createSettingsApiHandlers(config) {
|
|
|
1496
1836
|
const authErr = isPublicGroup ? null : await requireAuth(req);
|
|
1497
1837
|
const isAuthed = !authErr;
|
|
1498
1838
|
try {
|
|
1839
|
+
if (isPublicGroup) {
|
|
1840
|
+
const result2 = await getPublicSettingsGroup(
|
|
1841
|
+
{ dataSource, entityMap, encryptionKey },
|
|
1842
|
+
group
|
|
1843
|
+
);
|
|
1844
|
+
return json(result2);
|
|
1845
|
+
}
|
|
1499
1846
|
const where = { settings: group, deleted: false };
|
|
1500
1847
|
if (!isAuthed && !isPublicGroup) where.type = "public";
|
|
1501
1848
|
const rows = await configRepo().find({ where });
|
|
@@ -1669,6 +2016,125 @@ ${contextParts.join("\n\n")}` : "You are a helpful assistant for the company. If
|
|
|
1669
2016
|
};
|
|
1670
2017
|
}
|
|
1671
2018
|
|
|
2019
|
+
// src/message-templates/sms-defaults.ts
|
|
2020
|
+
var SMS_MESSAGE_TEMPLATE_DEFAULTS = [
|
|
2021
|
+
{
|
|
2022
|
+
templateKey: "auth.otp_login",
|
|
2023
|
+
name: "Sign-in OTP (SMS)",
|
|
2024
|
+
body: "Your sign-in code is {{code}}. Valid 10 minutes.",
|
|
2025
|
+
providerMeta: { otpVarKey: "var1" }
|
|
2026
|
+
},
|
|
2027
|
+
{
|
|
2028
|
+
templateKey: "auth.otp_verify_phone",
|
|
2029
|
+
name: "Verify phone OTP (SMS)",
|
|
2030
|
+
body: "Your verification code is {{code}}. Valid 10 minutes.",
|
|
2031
|
+
providerMeta: { otpVarKey: "var1" }
|
|
2032
|
+
}
|
|
2033
|
+
];
|
|
2034
|
+
function getSmsTemplateDefault(templateKey) {
|
|
2035
|
+
return SMS_MESSAGE_TEMPLATE_DEFAULTS.find((d) => d.templateKey === templateKey);
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// src/api/message-template-admin-handlers.ts
|
|
2039
|
+
function createSmsMessageTemplateHandlers(config) {
|
|
2040
|
+
const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
|
|
2041
|
+
const repo = () => dataSource.getRepository(entityMap.message_templates);
|
|
2042
|
+
async function requireSettingsRead(req) {
|
|
2043
|
+
const a = await requireAuth(req);
|
|
2044
|
+
if (a) return a;
|
|
2045
|
+
if (requireEntityPermission) {
|
|
2046
|
+
const pe = await requireEntityPermission(req, "settings", "read");
|
|
2047
|
+
if (pe) return pe;
|
|
2048
|
+
}
|
|
2049
|
+
return null;
|
|
2050
|
+
}
|
|
2051
|
+
async function requireSettingsUpdate(req) {
|
|
2052
|
+
const a = await requireAuth(req);
|
|
2053
|
+
if (a) return a;
|
|
2054
|
+
if (requireEntityPermission) {
|
|
2055
|
+
const pe = await requireEntityPermission(req, "settings", "update");
|
|
2056
|
+
if (pe) return pe;
|
|
2057
|
+
}
|
|
2058
|
+
return null;
|
|
2059
|
+
}
|
|
2060
|
+
return {
|
|
2061
|
+
async GET(req) {
|
|
2062
|
+
const err = await requireSettingsRead(req);
|
|
2063
|
+
if (err) return err;
|
|
2064
|
+
try {
|
|
2065
|
+
const rows = await repo().find({ where: { channel: "sms", deleted: false } });
|
|
2066
|
+
const byKey = new Map(rows.map((r) => [r.templateKey, r]));
|
|
2067
|
+
const items = SMS_MESSAGE_TEMPLATE_DEFAULTS.map((def) => {
|
|
2068
|
+
const row = byKey.get(def.templateKey);
|
|
2069
|
+
return {
|
|
2070
|
+
templateKey: def.templateKey,
|
|
2071
|
+
name: def.name,
|
|
2072
|
+
defaultBody: def.body,
|
|
2073
|
+
body: row?.body?.trim() ? row.body : def.body,
|
|
2074
|
+
externalTemplateRef: row?.externalTemplateRef?.trim() ?? "",
|
|
2075
|
+
otpVarKey: row?.providerMeta && typeof row.providerMeta.otpVarKey === "string" ? String(row.providerMeta.otpVarKey) : def.providerMeta?.otpVarKey ?? "var1",
|
|
2076
|
+
enabled: row ? row.enabled : false,
|
|
2077
|
+
dbId: row?.id ?? null
|
|
2078
|
+
};
|
|
2079
|
+
});
|
|
2080
|
+
return json({ items });
|
|
2081
|
+
} catch {
|
|
2082
|
+
return json({ error: "Failed to load templates" }, { status: 500 });
|
|
2083
|
+
}
|
|
2084
|
+
},
|
|
2085
|
+
async PUT(req) {
|
|
2086
|
+
const err = await requireSettingsUpdate(req);
|
|
2087
|
+
if (err) return err;
|
|
2088
|
+
try {
|
|
2089
|
+
const raw = await req.json().catch(() => null);
|
|
2090
|
+
if (!raw?.items || !Array.isArray(raw.items)) {
|
|
2091
|
+
return json({ error: "Invalid payload" }, { status: 400 });
|
|
2092
|
+
}
|
|
2093
|
+
for (const item of raw.items) {
|
|
2094
|
+
const templateKey = typeof item.templateKey === "string" ? item.templateKey.trim() : "";
|
|
2095
|
+
if (!getSmsTemplateDefault(templateKey)) continue;
|
|
2096
|
+
const body = typeof item.body === "string" ? item.body : "";
|
|
2097
|
+
const externalTemplateRef = typeof item.externalTemplateRef === "string" ? item.externalTemplateRef.trim() : "";
|
|
2098
|
+
const otpVarKey = typeof item.otpVarKey === "string" && item.otpVarKey.trim() ? item.otpVarKey.trim() : "var1";
|
|
2099
|
+
const enabled = item.enabled !== false;
|
|
2100
|
+
const existing = await repo().findOne({
|
|
2101
|
+
where: { channel: "sms", templateKey, deleted: false }
|
|
2102
|
+
});
|
|
2103
|
+
const def = getSmsTemplateDefault(templateKey);
|
|
2104
|
+
const providerMeta = { otpVarKey };
|
|
2105
|
+
if (existing) {
|
|
2106
|
+
await repo().update(existing.id, {
|
|
2107
|
+
name: def.name,
|
|
2108
|
+
body,
|
|
2109
|
+
externalTemplateRef: externalTemplateRef || null,
|
|
2110
|
+
providerMeta,
|
|
2111
|
+
enabled,
|
|
2112
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2113
|
+
});
|
|
2114
|
+
} else {
|
|
2115
|
+
await repo().save(
|
|
2116
|
+
repo().create({
|
|
2117
|
+
channel: "sms",
|
|
2118
|
+
templateKey,
|
|
2119
|
+
name: def.name,
|
|
2120
|
+
subject: null,
|
|
2121
|
+
body,
|
|
2122
|
+
externalTemplateRef: externalTemplateRef || null,
|
|
2123
|
+
providerMeta,
|
|
2124
|
+
enabled,
|
|
2125
|
+
deleted: false
|
|
2126
|
+
})
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
return json({ ok: true });
|
|
2131
|
+
} catch {
|
|
2132
|
+
return json({ error: "Failed to save templates" }, { status: 500 });
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
|
|
1672
2138
|
// src/auth/permission-entities.ts
|
|
1673
2139
|
var PERMISSION_ENTITY_INTERNAL_EXCLUDE = /* @__PURE__ */ new Set([
|
|
1674
2140
|
"users",
|
|
@@ -1682,7 +2148,8 @@ var PERMISSION_ENTITY_INTERNAL_EXCLUDE = /* @__PURE__ */ new Set([
|
|
|
1682
2148
|
"carts",
|
|
1683
2149
|
"cart_items",
|
|
1684
2150
|
"wishlists",
|
|
1685
|
-
"wishlist_items"
|
|
2151
|
+
"wishlist_items",
|
|
2152
|
+
"message_templates"
|
|
1686
2153
|
]);
|
|
1687
2154
|
var PERMISSION_LOGICAL_ENTITIES = [
|
|
1688
2155
|
"users",
|
|
@@ -1868,7 +2335,8 @@ var DEFAULT_EXCLUDE = /* @__PURE__ */ new Set([
|
|
|
1868
2335
|
"carts",
|
|
1869
2336
|
"cart_items",
|
|
1870
2337
|
"wishlists",
|
|
1871
|
-
"wishlist_items"
|
|
2338
|
+
"wishlist_items",
|
|
2339
|
+
"message_templates"
|
|
1872
2340
|
]);
|
|
1873
2341
|
function createCmsApiHandler(config) {
|
|
1874
2342
|
const {
|
|
@@ -1931,7 +2399,8 @@ function createCmsApiHandler(config) {
|
|
|
1931
2399
|
const crudOpts = {
|
|
1932
2400
|
requireAuth: config.requireAuth,
|
|
1933
2401
|
json: config.json,
|
|
1934
|
-
requireEntityPermission: reqEntityPerm
|
|
2402
|
+
requireEntityPermission: reqEntityPerm,
|
|
2403
|
+
getCms
|
|
1935
2404
|
};
|
|
1936
2405
|
const crud = createCrudHandler(dataSource, entityMap, crudOpts);
|
|
1937
2406
|
const crudById = createCrudByIdHandler(dataSource, entityMap, crudOpts);
|
|
@@ -1961,6 +2430,13 @@ function createCmsApiHandler(config) {
|
|
|
1961
2430
|
const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
|
|
1962
2431
|
const profilePut = userProfile ? createUserProfileHandler(userProfile) : null;
|
|
1963
2432
|
const settingsHandlers = settingsConfig ? createSettingsApiHandlers(settingsConfig) : null;
|
|
2433
|
+
const smsMessageTemplateHandlers = createSmsMessageTemplateHandlers({
|
|
2434
|
+
dataSource,
|
|
2435
|
+
entityMap,
|
|
2436
|
+
json: config.json,
|
|
2437
|
+
requireAuth: config.requireAuth,
|
|
2438
|
+
requireEntityPermission: reqEntityPerm
|
|
2439
|
+
});
|
|
1964
2440
|
const chatHandlers = chatConfig ? createChatHandlers(chatConfig) : null;
|
|
1965
2441
|
function resolveResource(segment) {
|
|
1966
2442
|
const model = pathToModel(segment);
|
|
@@ -2065,11 +2541,28 @@ function createCmsApiHandler(config) {
|
|
|
2065
2541
|
return settingsHandlers.PUT(req, group);
|
|
2066
2542
|
}
|
|
2067
2543
|
}
|
|
2544
|
+
if (path[0] === "message-templates" && path[1] === "sms" && path.length === 2) {
|
|
2545
|
+
if (method === "GET") return smsMessageTemplateHandlers.GET(req);
|
|
2546
|
+
if (method === "PUT") return smsMessageTemplateHandlers.PUT(req);
|
|
2547
|
+
}
|
|
2068
2548
|
if (path[0] === "chat" && chatHandlers) {
|
|
2069
2549
|
if (path.length === 2 && path[1] === "identify" && method === "POST") return chatHandlers.identify(req);
|
|
2070
2550
|
if (path.length === 4 && path[1] === "conversations" && path[3] === "messages" && method === "GET") return chatHandlers.getMessages(req, path[2]);
|
|
2071
2551
|
if (path.length === 2 && path[1] === "messages" && method === "POST") return chatHandlers.postMessage(req);
|
|
2072
2552
|
}
|
|
2553
|
+
if (path[0] === "orders" && path.length === 3 && path[2] === "invoice" && method === "GET" && getCms) {
|
|
2554
|
+
const a = await config.requireAuth(req);
|
|
2555
|
+
if (a) return a;
|
|
2556
|
+
if (perm) {
|
|
2557
|
+
const pe = await perm(req, "orders", "read");
|
|
2558
|
+
if (pe) return pe;
|
|
2559
|
+
}
|
|
2560
|
+
const cms = await getCms();
|
|
2561
|
+
const { streamOrderInvoicePdf: streamOrderInvoicePdf2 } = await Promise.resolve().then(() => (init_erp_order_invoice(), erp_order_invoice_exports));
|
|
2562
|
+
const oid = Number(path[1]);
|
|
2563
|
+
if (!Number.isFinite(oid)) return config.json({ error: "Invalid id" }, { status: 400 });
|
|
2564
|
+
return streamOrderInvoicePdf2(cms, dataSource, entityMap, oid, {});
|
|
2565
|
+
}
|
|
2073
2566
|
if (path.length === 0) return config.json({ error: "Not found" }, { status: 404 });
|
|
2074
2567
|
const resource = resolveResource(path[0]);
|
|
2075
2568
|
if (!crudResources.includes(resource)) return config.json({ error: "Invalid resource" }, { status: 400 });
|
|
@@ -2102,7 +2595,7 @@ function createCmsApiHandler(config) {
|
|
|
2102
2595
|
}
|
|
2103
2596
|
|
|
2104
2597
|
// src/api/storefront-handlers.ts
|
|
2105
|
-
var
|
|
2598
|
+
var import_typeorm5 = require("typeorm");
|
|
2106
2599
|
|
|
2107
2600
|
// src/lib/is-valid-signup-email.ts
|
|
2108
2601
|
var MAX_EMAIL = 254;
|
|
@@ -2124,6 +2617,339 @@ function isValidSignupEmail(email) {
|
|
|
2124
2617
|
|
|
2125
2618
|
// src/api/storefront-handlers.ts
|
|
2126
2619
|
init_email_queue();
|
|
2620
|
+
|
|
2621
|
+
// src/lib/order-number.ts
|
|
2622
|
+
var KIND_PREFIX = {
|
|
2623
|
+
sale: "OSL",
|
|
2624
|
+
return: "ORT",
|
|
2625
|
+
replacement: "ORP"
|
|
2626
|
+
};
|
|
2627
|
+
function orderNumberYymmUtc(at) {
|
|
2628
|
+
const yy = String(at.getUTCFullYear()).slice(-2);
|
|
2629
|
+
const mm = String(at.getUTCMonth() + 1).padStart(2, "0");
|
|
2630
|
+
return yy + mm;
|
|
2631
|
+
}
|
|
2632
|
+
function maskOrderIdSegment(id) {
|
|
2633
|
+
let x = id >>> 0 ^ 2779096485;
|
|
2634
|
+
x = Math.imul(x, 2654435761) >>> 0;
|
|
2635
|
+
return x.toString(36).toUpperCase().padStart(8, "0").slice(-8);
|
|
2636
|
+
}
|
|
2637
|
+
function buildCanonicalOrderNumber(kind, id, at) {
|
|
2638
|
+
return KIND_PREFIX[kind] + orderNumberYymmUtc(at) + maskOrderIdSegment(id);
|
|
2639
|
+
}
|
|
2640
|
+
function temporaryOrderNumberPlaceholder() {
|
|
2641
|
+
return `TMP${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`.toUpperCase();
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
// src/lib/order-storefront-metadata.ts
|
|
2645
|
+
function mergeOrderMetadataPatch(existing, patch) {
|
|
2646
|
+
const base = existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing } : {};
|
|
2647
|
+
if (patch.fulfillment !== void 0) {
|
|
2648
|
+
if (patch.fulfillment === null) delete base.fulfillment;
|
|
2649
|
+
else base.fulfillment = patch.fulfillment;
|
|
2650
|
+
}
|
|
2651
|
+
if (patch.invoice !== void 0) {
|
|
2652
|
+
if (patch.invoice === null) delete base.invoice;
|
|
2653
|
+
else base.invoice = patch.invoice;
|
|
2654
|
+
}
|
|
2655
|
+
return base;
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
// src/plugins/erp/erp-order-status-map.ts
|
|
2659
|
+
function mapErpSaleStatusToOrderStatus(erpLabel) {
|
|
2660
|
+
if (!erpLabel || typeof erpLabel !== "string") return void 0;
|
|
2661
|
+
const k = erpLabel.trim().toLowerCase().replace(/\s+/g, "_");
|
|
2662
|
+
const map = {
|
|
2663
|
+
draft: "pending",
|
|
2664
|
+
pending: "pending",
|
|
2665
|
+
open: "pending",
|
|
2666
|
+
new: "pending",
|
|
2667
|
+
unconfirmed: "pending",
|
|
2668
|
+
confirmed: "confirmed",
|
|
2669
|
+
processing: "processing",
|
|
2670
|
+
packed: "processing",
|
|
2671
|
+
shipped: "processing",
|
|
2672
|
+
in_transit: "processing",
|
|
2673
|
+
out_for_delivery: "processing",
|
|
2674
|
+
delivered: "completed",
|
|
2675
|
+
completed: "completed",
|
|
2676
|
+
closed: "completed",
|
|
2677
|
+
fulfilled: "completed",
|
|
2678
|
+
cancelled: "cancelled",
|
|
2679
|
+
canceled: "cancelled",
|
|
2680
|
+
void: "cancelled"
|
|
2681
|
+
};
|
|
2682
|
+
return map[k];
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
// src/plugins/erp/erp-order-sync.ts
|
|
2686
|
+
init_erp_response_map();
|
|
2687
|
+
init_erp_config_enabled();
|
|
2688
|
+
function pickInvoiceId2(data) {
|
|
2689
|
+
const nested = data.invoice && typeof data.invoice === "object" && !Array.isArray(data.invoice) ? data.invoice : null;
|
|
2690
|
+
const src = nested || data;
|
|
2691
|
+
for (const k of ["invoiceId", "invoice_id", "id"]) {
|
|
2692
|
+
const v = src[k];
|
|
2693
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
2694
|
+
}
|
|
2695
|
+
return void 0;
|
|
2696
|
+
}
|
|
2697
|
+
async function ensureChildOrdersFromRefs(orderRepo, parent, refs, contactId, currency) {
|
|
2698
|
+
for (const { ref, orderKind } of refs) {
|
|
2699
|
+
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();
|
|
2700
|
+
if (existing) continue;
|
|
2701
|
+
const tmp = temporaryOrderNumberPlaceholder();
|
|
2702
|
+
const row = await orderRepo.save(
|
|
2703
|
+
orderRepo.create({
|
|
2704
|
+
orderNumber: tmp,
|
|
2705
|
+
orderKind,
|
|
2706
|
+
parentOrderId: parent.id,
|
|
2707
|
+
contactId,
|
|
2708
|
+
billingAddressId: null,
|
|
2709
|
+
shippingAddressId: null,
|
|
2710
|
+
status: "pending",
|
|
2711
|
+
subtotal: 0,
|
|
2712
|
+
tax: 0,
|
|
2713
|
+
discount: 0,
|
|
2714
|
+
total: 0,
|
|
2715
|
+
currency,
|
|
2716
|
+
metadata: { platformRef: ref },
|
|
2717
|
+
deleted: false
|
|
2718
|
+
})
|
|
2719
|
+
);
|
|
2720
|
+
const r = row;
|
|
2721
|
+
await orderRepo.update(r.id, {
|
|
2722
|
+
orderNumber: buildCanonicalOrderNumber(orderKind, r.id, r.createdAt ?? /* @__PURE__ */ new Date())
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
function deepMergeFulfillment(a, b) {
|
|
2727
|
+
if (!a) return b;
|
|
2728
|
+
if (!b) return a;
|
|
2729
|
+
return {
|
|
2730
|
+
...a,
|
|
2731
|
+
...b,
|
|
2732
|
+
events: b.events?.length ? b.events : a.events
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
async function refreshOrderFromErp(cms, dataSource, entityMap, submission, order) {
|
|
2736
|
+
const orderRepo = dataSource.getRepository(entityMap.orders);
|
|
2737
|
+
const kind = order.orderKind || "sale";
|
|
2738
|
+
const meta = order.metadata && typeof order.metadata === "object" && !Array.isArray(order.metadata) ? { ...order.metadata } : {};
|
|
2739
|
+
if (kind === "sale") {
|
|
2740
|
+
const refId = String(order.orderNumber || "");
|
|
2741
|
+
let fulfillment;
|
|
2742
|
+
let invoiceNumber;
|
|
2743
|
+
let invoiceId;
|
|
2744
|
+
let newStatus;
|
|
2745
|
+
const r1 = await submission.postErpReadAction("get-order-status", { platformOrderId: refId });
|
|
2746
|
+
const d1 = r1.ok ? unwrapErpReadData(r1.json) : null;
|
|
2747
|
+
if (d1) {
|
|
2748
|
+
const mapped = mapErpSaleStatusToOrderStatus(
|
|
2749
|
+
typeof d1.status === "string" ? d1.status : typeof d1.orderStatus === "string" ? d1.orderStatus : typeof d1.state === "string" ? d1.state : void 0
|
|
2750
|
+
);
|
|
2751
|
+
if (mapped) newStatus = mapped;
|
|
2752
|
+
fulfillment = mapErpPayloadToFulfillment(d1);
|
|
2753
|
+
const refs = extractChildOrderRefsFromSalePayload(d1);
|
|
2754
|
+
if (refs.length) {
|
|
2755
|
+
await ensureChildOrdersFromRefs(
|
|
2756
|
+
orderRepo,
|
|
2757
|
+
order,
|
|
2758
|
+
refs,
|
|
2759
|
+
order.contactId,
|
|
2760
|
+
String(order.currency || "INR")
|
|
2761
|
+
);
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
const r2 = await submission.postErpReadAction("get-fulfillment-status", { platformOrderId: refId });
|
|
2765
|
+
const d2 = r2.ok ? unwrapErpReadData(r2.json) : null;
|
|
2766
|
+
if (d2) {
|
|
2767
|
+
fulfillment = deepMergeFulfillment(fulfillment, mapErpPayloadToFulfillment(d2));
|
|
2768
|
+
}
|
|
2769
|
+
const r3 = await submission.postErpReadAction("get-invoice", { platformOrderId: refId });
|
|
2770
|
+
const d3 = r3.ok ? unwrapErpReadData(r3.json) : null;
|
|
2771
|
+
if (d3) {
|
|
2772
|
+
invoiceNumber = mapErpPayloadToInvoiceNumber(d3);
|
|
2773
|
+
invoiceId = pickInvoiceId2(d3);
|
|
2774
|
+
}
|
|
2775
|
+
const oid = order.id;
|
|
2776
|
+
const prevInv = meta.invoice && typeof meta.invoice === "object" && !Array.isArray(meta.invoice) ? { ...meta.invoice } : {};
|
|
2777
|
+
const nextInvoice = {
|
|
2778
|
+
...prevInv,
|
|
2779
|
+
link: `/api/storefront/orders/${oid}/invoice`,
|
|
2780
|
+
...invoiceNumber ? { invoiceNumber } : {},
|
|
2781
|
+
...invoiceId ? { invoiceId } : {}
|
|
2782
|
+
};
|
|
2783
|
+
const patch = { invoice: nextInvoice };
|
|
2784
|
+
if (fulfillment !== void 0) patch.fulfillment = fulfillment;
|
|
2785
|
+
const nextMeta = mergeOrderMetadataPatch(meta, patch);
|
|
2786
|
+
await orderRepo.update(oid, {
|
|
2787
|
+
...newStatus ? { status: newStatus } : {},
|
|
2788
|
+
metadata: nextMeta,
|
|
2789
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2790
|
+
});
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
if (kind === "return" || kind === "replacement") {
|
|
2794
|
+
const platformReturnId = String(order.orderNumber || "");
|
|
2795
|
+
const r = await submission.postErpReadAction("get-return-status", { platformReturnId });
|
|
2796
|
+
const d = r.ok ? unwrapErpReadData(r.json) : null;
|
|
2797
|
+
if (!d) return;
|
|
2798
|
+
const mapped = mapErpSaleStatusToOrderStatus(
|
|
2799
|
+
typeof d.status === "string" ? d.status : typeof d.returnStatus === "string" ? d.returnStatus : void 0
|
|
2800
|
+
);
|
|
2801
|
+
const fulfillment = mapErpPayloadToFulfillment(d);
|
|
2802
|
+
const patch = {};
|
|
2803
|
+
if (fulfillment !== void 0) patch.fulfillment = fulfillment;
|
|
2804
|
+
const nextMeta = Object.keys(patch).length ? mergeOrderMetadataPatch(meta, patch) : meta;
|
|
2805
|
+
await orderRepo.update(order.id, {
|
|
2806
|
+
...mapped ? { status: mapped } : {},
|
|
2807
|
+
metadata: nextMeta,
|
|
2808
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
async function tryRefreshOrderFromErpForStorefront(cms, dataSource, entityMap, order) {
|
|
2813
|
+
try {
|
|
2814
|
+
const on = await isErpIntegrationEnabled(cms, dataSource, entityMap);
|
|
2815
|
+
if (!on) return;
|
|
2816
|
+
const erp = cms.getPlugin("erp");
|
|
2817
|
+
if (!erp?.submission) return;
|
|
2818
|
+
await refreshOrderFromErp(cms, dataSource, entityMap, erp.submission, order);
|
|
2819
|
+
} catch {
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
// src/api/storefront-handlers.ts
|
|
2824
|
+
init_erp_order_invoice();
|
|
2825
|
+
|
|
2826
|
+
// src/plugins/sms/sms-queue.ts
|
|
2827
|
+
var SMS_QUEUE_NAME = "sms";
|
|
2828
|
+
async function queueSms(cms, payload) {
|
|
2829
|
+
const queue = cms.getPlugin("queue");
|
|
2830
|
+
const sms = cms.getPlugin("sms");
|
|
2831
|
+
if (queue) {
|
|
2832
|
+
await queue.add(SMS_QUEUE_NAME, payload);
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
if (sms && typeof sms.send === "function") {
|
|
2836
|
+
if (payload.templateKey?.trim()) {
|
|
2837
|
+
await sms.send({
|
|
2838
|
+
to: payload.to,
|
|
2839
|
+
templateKey: payload.templateKey.trim(),
|
|
2840
|
+
variables: payload.variables,
|
|
2841
|
+
otpCode: payload.otpCode
|
|
2842
|
+
});
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
if (payload.body?.trim()) {
|
|
2846
|
+
await sms.send({
|
|
2847
|
+
to: payload.to,
|
|
2848
|
+
body: payload.body,
|
|
2849
|
+
otpCode: payload.otpCode,
|
|
2850
|
+
variables: payload.variables
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
// src/lib/otp-challenge.ts
|
|
2857
|
+
var import_crypto = require("crypto");
|
|
2858
|
+
var import_typeorm4 = require("typeorm");
|
|
2859
|
+
var OTP_TTL_MS = 10 * 60 * 1e3;
|
|
2860
|
+
var MAX_SENDS_PER_HOUR = 5;
|
|
2861
|
+
var MAX_VERIFY_ATTEMPTS = 8;
|
|
2862
|
+
function getPepper(explicit) {
|
|
2863
|
+
return (explicit || process.env.OTP_PEPPER || process.env.NEXTAUTH_SECRET || "dev-otp-pepper").trim();
|
|
2864
|
+
}
|
|
2865
|
+
function hashOtpCode(code, purpose, identifier, pepper) {
|
|
2866
|
+
return (0, import_crypto.createHmac)("sha256", getPepper(pepper)).update(`${purpose}|${identifier}|${code}`).digest("hex");
|
|
2867
|
+
}
|
|
2868
|
+
function verifyOtpCodeHash(code, storedHash, purpose, identifier, pepper) {
|
|
2869
|
+
const h = hashOtpCode(code, purpose, identifier, pepper);
|
|
2870
|
+
try {
|
|
2871
|
+
return (0, import_crypto.timingSafeEqual)(Buffer.from(h, "utf8"), Buffer.from(storedHash, "utf8"));
|
|
2872
|
+
} catch {
|
|
2873
|
+
return false;
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
function generateNumericOtp(length = 6) {
|
|
2877
|
+
const max = 10 ** length;
|
|
2878
|
+
return (0, import_crypto.randomInt)(0, max).toString().padStart(length, "0");
|
|
2879
|
+
}
|
|
2880
|
+
function normalizePhoneE164(raw, defaultCountryCode) {
|
|
2881
|
+
const t = raw.trim();
|
|
2882
|
+
const digitsOnly = t.replace(/\D/g, "");
|
|
2883
|
+
if (digitsOnly.length < 10) return null;
|
|
2884
|
+
if (t.startsWith("+")) return `+${digitsOnly}`;
|
|
2885
|
+
const cc = (defaultCountryCode || process.env.DEFAULT_PHONE_COUNTRY_CODE || "91").replace(/\D/g, "");
|
|
2886
|
+
if (digitsOnly.length > 10) return `+${digitsOnly}`;
|
|
2887
|
+
return `+${cc}${digitsOnly}`;
|
|
2888
|
+
}
|
|
2889
|
+
async function countRecentOtpSends(dataSource, entityMap, purpose, identifier, since) {
|
|
2890
|
+
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
2891
|
+
return repo.count({
|
|
2892
|
+
where: { purpose, identifier, createdAt: (0, import_typeorm4.MoreThan)(since) }
|
|
2893
|
+
});
|
|
2894
|
+
}
|
|
2895
|
+
async function createOtpChallenge(dataSource, entityMap, input) {
|
|
2896
|
+
const { purpose, channel, identifier, code, pepper } = input;
|
|
2897
|
+
const since = new Date(Date.now() - 60 * 60 * 1e3);
|
|
2898
|
+
const recent = await countRecentOtpSends(dataSource, entityMap, purpose, identifier, since);
|
|
2899
|
+
if (recent >= MAX_SENDS_PER_HOUR) {
|
|
2900
|
+
return { ok: false, error: "Too many codes sent. Try again later.", status: 429 };
|
|
2901
|
+
}
|
|
2902
|
+
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
2903
|
+
await repo.delete({
|
|
2904
|
+
purpose,
|
|
2905
|
+
identifier,
|
|
2906
|
+
consumedAt: (0, import_typeorm4.IsNull)()
|
|
2907
|
+
});
|
|
2908
|
+
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
2909
|
+
const codeHash = hashOtpCode(code, purpose, identifier, pepper);
|
|
2910
|
+
await repo.save(
|
|
2911
|
+
repo.create({
|
|
2912
|
+
purpose,
|
|
2913
|
+
channel,
|
|
2914
|
+
identifier,
|
|
2915
|
+
codeHash,
|
|
2916
|
+
expiresAt,
|
|
2917
|
+
attempts: 0,
|
|
2918
|
+
consumedAt: null
|
|
2919
|
+
})
|
|
2920
|
+
);
|
|
2921
|
+
return { ok: true };
|
|
2922
|
+
}
|
|
2923
|
+
async function verifyAndConsumeOtpChallenge(dataSource, entityMap, input) {
|
|
2924
|
+
const { purpose, identifier, code, pepper } = input;
|
|
2925
|
+
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
2926
|
+
const row = await repo.findOne({
|
|
2927
|
+
where: { purpose, identifier, consumedAt: (0, import_typeorm4.IsNull)() },
|
|
2928
|
+
order: { id: "DESC" }
|
|
2929
|
+
});
|
|
2930
|
+
if (!row) {
|
|
2931
|
+
return { ok: false, error: "Invalid or expired code", status: 400 };
|
|
2932
|
+
}
|
|
2933
|
+
const r = row;
|
|
2934
|
+
if (new Date(r.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
2935
|
+
await repo.delete(row.id);
|
|
2936
|
+
return { ok: false, error: "Invalid or expired code", status: 400 };
|
|
2937
|
+
}
|
|
2938
|
+
const attempts = r.attempts || 0;
|
|
2939
|
+
if (attempts >= MAX_VERIFY_ATTEMPTS) {
|
|
2940
|
+
await repo.delete(row.id);
|
|
2941
|
+
return { ok: false, error: "Too many attempts", status: 400 };
|
|
2942
|
+
}
|
|
2943
|
+
const valid = verifyOtpCodeHash(code, r.codeHash, purpose, identifier, pepper);
|
|
2944
|
+
if (!valid) {
|
|
2945
|
+
await repo.update(row.id, { attempts: attempts + 1 });
|
|
2946
|
+
return { ok: false, error: "Invalid or expired code", status: 400 };
|
|
2947
|
+
}
|
|
2948
|
+
await repo.update(row.id, { consumedAt: /* @__PURE__ */ new Date(), attempts: attempts + 1 });
|
|
2949
|
+
return { ok: true };
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
// src/api/storefront-handlers.ts
|
|
2127
2953
|
var GUEST_COOKIE = "guest_id";
|
|
2128
2954
|
var ONE_YEAR = 60 * 60 * 24 * 365;
|
|
2129
2955
|
function parseCookies(header) {
|
|
@@ -2141,13 +2967,17 @@ function parseCookies(header) {
|
|
|
2141
2967
|
function guestCookieHeader(name, token) {
|
|
2142
2968
|
return `${name}=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${ONE_YEAR}`;
|
|
2143
2969
|
}
|
|
2144
|
-
function orderNumber() {
|
|
2145
|
-
return `ORD-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
2146
|
-
}
|
|
2147
2970
|
var SIGNUP_VERIFY_EXPIRY_HOURS = 72;
|
|
2148
2971
|
function createStorefrontApiHandler(config) {
|
|
2149
2972
|
const { dataSource, entityMap, json, getSessionUser, getCms, getCompanyDetails, publicSiteUrl } = config;
|
|
2150
2973
|
const cookieName = config.guestCookieName ?? GUEST_COOKIE;
|
|
2974
|
+
const otpFlags = config.otpFlags;
|
|
2975
|
+
const otpPepper = config.otpPepper;
|
|
2976
|
+
const defaultPhoneCc = config.defaultPhoneCountryCode;
|
|
2977
|
+
const otpAllowPhoneLogin = config.otpAllowPhoneLogin !== false;
|
|
2978
|
+
function otpOff(key) {
|
|
2979
|
+
return !otpFlags || otpFlags[key] !== true;
|
|
2980
|
+
}
|
|
2151
2981
|
const cartRepo = () => dataSource.getRepository(entityMap.carts);
|
|
2152
2982
|
const cartItemRepo = () => dataSource.getRepository(entityMap.cart_items);
|
|
2153
2983
|
const productRepo = () => dataSource.getRepository(entityMap.products);
|
|
@@ -2161,13 +2991,28 @@ function createStorefrontApiHandler(config) {
|
|
|
2161
2991
|
const tokenRepo = () => dataSource.getRepository(entityMap.password_reset_tokens);
|
|
2162
2992
|
const collectionRepo = () => dataSource.getRepository(entityMap.collections);
|
|
2163
2993
|
const groupRepo = () => dataSource.getRepository(entityMap.user_groups);
|
|
2994
|
+
async function syncContactToErp(contact) {
|
|
2995
|
+
if (!getCms) return;
|
|
2996
|
+
try {
|
|
2997
|
+
const cms = await getCms();
|
|
2998
|
+
await queueErpCreateContactIfEnabled(cms, dataSource, entityMap, {
|
|
2999
|
+
name: String(contact.name ?? ""),
|
|
3000
|
+
email: String(contact.email ?? "").trim(),
|
|
3001
|
+
phone: contact.phone,
|
|
3002
|
+
type: contact.type,
|
|
3003
|
+
company: contact.company,
|
|
3004
|
+
notes: contact.notes
|
|
3005
|
+
});
|
|
3006
|
+
} catch {
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
2164
3009
|
async function ensureContactForUser(userId) {
|
|
2165
3010
|
let c = await contactRepo().findOne({ where: { userId, deleted: false } });
|
|
2166
3011
|
if (c) return c;
|
|
2167
3012
|
const u = await userRepo().findOne({ where: { id: userId } });
|
|
2168
3013
|
if (!u) return null;
|
|
2169
3014
|
const unclaimed = await contactRepo().findOne({
|
|
2170
|
-
where: { email: u.email, userId: (0,
|
|
3015
|
+
where: { email: u.email, userId: (0, import_typeorm5.IsNull)(), deleted: false }
|
|
2171
3016
|
});
|
|
2172
3017
|
if (unclaimed) {
|
|
2173
3018
|
await contactRepo().update(unclaimed.id, { userId });
|
|
@@ -2182,6 +3027,7 @@ function createStorefrontApiHandler(config) {
|
|
|
2182
3027
|
deleted: false
|
|
2183
3028
|
})
|
|
2184
3029
|
);
|
|
3030
|
+
await syncContactToErp(created);
|
|
2185
3031
|
return { id: created.id };
|
|
2186
3032
|
}
|
|
2187
3033
|
async function getOrCreateCart(req) {
|
|
@@ -2276,7 +3122,21 @@ function createStorefrontApiHandler(config) {
|
|
|
2276
3122
|
})
|
|
2277
3123
|
};
|
|
2278
3124
|
}
|
|
3125
|
+
function serializeSeo(seo) {
|
|
3126
|
+
if (!seo || typeof seo !== "object") return void 0;
|
|
3127
|
+
const s = seo;
|
|
3128
|
+
return {
|
|
3129
|
+
title: s.title ?? null,
|
|
3130
|
+
description: s.description ?? null,
|
|
3131
|
+
keywords: s.keywords ?? null,
|
|
3132
|
+
ogTitle: s.ogTitle ?? null,
|
|
3133
|
+
ogDescription: s.ogDescription ?? null,
|
|
3134
|
+
ogImage: s.ogImage ?? null,
|
|
3135
|
+
slug: s.slug ?? null
|
|
3136
|
+
};
|
|
3137
|
+
}
|
|
2279
3138
|
function serializeProduct(p) {
|
|
3139
|
+
const seo = serializeSeo(p.seo);
|
|
2280
3140
|
return {
|
|
2281
3141
|
id: p.id,
|
|
2282
3142
|
name: p.name,
|
|
@@ -2287,7 +3147,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2287
3147
|
compareAtPrice: p.compareAtPrice,
|
|
2288
3148
|
status: p.status,
|
|
2289
3149
|
collectionId: p.collectionId,
|
|
2290
|
-
metadata: p.metadata
|
|
3150
|
+
metadata: p.metadata,
|
|
3151
|
+
...seo ? { seo } : {}
|
|
2291
3152
|
};
|
|
2292
3153
|
}
|
|
2293
3154
|
return {
|
|
@@ -2354,7 +3215,7 @@ function createStorefrontApiHandler(config) {
|
|
|
2354
3215
|
const byId = /^\d+$/.test(idOrSlug);
|
|
2355
3216
|
const product = await productRepo().findOne({
|
|
2356
3217
|
where: byId ? { id: parseInt(idOrSlug, 10), status: "available", deleted: false } : { slug: idOrSlug, status: "available", deleted: false },
|
|
2357
|
-
relations: ["attributes", "attributes.attribute"]
|
|
3218
|
+
relations: ["attributes", "attributes.attribute", "seo"]
|
|
2358
3219
|
});
|
|
2359
3220
|
if (!product) return json({ error: "Not found" }, { status: 404 });
|
|
2360
3221
|
const p = product;
|
|
@@ -2398,7 +3259,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2398
3259
|
const idOrSlug = path[1];
|
|
2399
3260
|
const byId = /^\d+$/.test(idOrSlug);
|
|
2400
3261
|
const collection = await collectionRepo().findOne({
|
|
2401
|
-
where: byId ? { id: parseInt(idOrSlug, 10), active: true, deleted: false } : { slug: idOrSlug, active: true, deleted: false }
|
|
3262
|
+
where: byId ? { id: parseInt(idOrSlug, 10), active: true, deleted: false } : { slug: idOrSlug, active: true, deleted: false },
|
|
3263
|
+
relations: ["seo"]
|
|
2402
3264
|
});
|
|
2403
3265
|
if (!collection) return json({ error: "Not found" }, { status: 404 });
|
|
2404
3266
|
const col = collection;
|
|
@@ -2406,12 +3268,14 @@ function createStorefrontApiHandler(config) {
|
|
|
2406
3268
|
where: { collectionId: col.id, status: "available", deleted: false },
|
|
2407
3269
|
order: { id: "ASC" }
|
|
2408
3270
|
});
|
|
3271
|
+
const colSeo = serializeSeo(col.seo);
|
|
2409
3272
|
return json({
|
|
2410
3273
|
id: col.id,
|
|
2411
3274
|
name: col.name,
|
|
2412
3275
|
slug: col.slug,
|
|
2413
3276
|
description: col.description,
|
|
2414
3277
|
image: col.image,
|
|
3278
|
+
...colSeo ? { seo: colSeo } : {},
|
|
2415
3279
|
products: products.map((p) => serializeProduct(p))
|
|
2416
3280
|
});
|
|
2417
3281
|
}
|
|
@@ -2449,6 +3313,7 @@ function createStorefrontApiHandler(config) {
|
|
|
2449
3313
|
await userRepo().update(uid, { name: b.name.trim() });
|
|
2450
3314
|
}
|
|
2451
3315
|
const updatedContact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
3316
|
+
if (updatedContact) await syncContactToErp(updatedContact);
|
|
2452
3317
|
const updatedUser = await userRepo().findOne({ where: { id: uid }, select: ["id", "name", "email"] });
|
|
2453
3318
|
return json({
|
|
2454
3319
|
user: updatedUser ? { id: updatedUser.id, name: updatedUser.name, email: updatedUser.email } : null,
|
|
@@ -2536,13 +3401,155 @@ function createStorefrontApiHandler(config) {
|
|
|
2536
3401
|
const email = record.email;
|
|
2537
3402
|
const user = await userRepo().findOne({ where: { email }, select: ["id", "blocked"] });
|
|
2538
3403
|
if (!user) return json({ error: "User not found" }, { status: 400 });
|
|
2539
|
-
await userRepo().update(user.id, {
|
|
3404
|
+
await userRepo().update(user.id, {
|
|
3405
|
+
blocked: false,
|
|
3406
|
+
emailVerifiedAt: /* @__PURE__ */ new Date(),
|
|
3407
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3408
|
+
});
|
|
3409
|
+
await tokenRepo().delete({ email });
|
|
3410
|
+
return json({ success: true, message: "Email verified. You can sign in." });
|
|
3411
|
+
}
|
|
3412
|
+
if (path[0] === "auth" && path[1] === "otp" && path[2] === "send" && path.length === 3 && method === "POST") {
|
|
3413
|
+
const b = await req.json().catch(() => ({}));
|
|
3414
|
+
const purposeRaw = typeof b.purpose === "string" ? b.purpose.trim() : "";
|
|
3415
|
+
const purpose = purposeRaw === "login" || purposeRaw === "verify_email" || purposeRaw === "verify_phone" ? purposeRaw : "";
|
|
3416
|
+
if (!purpose) return json({ error: "purpose must be login, verify_email, or verify_phone" }, { status: 400 });
|
|
3417
|
+
if (purpose === "login" && otpOff("login")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
3418
|
+
if (purpose === "verify_email" && otpOff("verifyEmail")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
3419
|
+
if (purpose === "verify_phone" && otpOff("verifyPhone")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
3420
|
+
const capOtp = await assertCaptchaOk(getCms, b, req, json);
|
|
3421
|
+
if (capOtp) return capOtp;
|
|
3422
|
+
const emailIn = typeof b.email === "string" ? b.email.trim().toLowerCase() : "";
|
|
3423
|
+
const phoneIn = typeof b.phone === "string" ? b.phone.trim() : "";
|
|
3424
|
+
let identifier;
|
|
3425
|
+
let channel;
|
|
3426
|
+
if (purpose === "login") {
|
|
3427
|
+
if (emailIn) {
|
|
3428
|
+
identifier = emailIn;
|
|
3429
|
+
channel = "email";
|
|
3430
|
+
} else if (phoneIn) {
|
|
3431
|
+
if (!otpAllowPhoneLogin) {
|
|
3432
|
+
return json({ error: "Phone sign-in is not enabled" }, { status: 403 });
|
|
3433
|
+
}
|
|
3434
|
+
const p = normalizePhoneE164(phoneIn, defaultPhoneCc);
|
|
3435
|
+
if (!p) return json({ error: "Invalid phone" }, { status: 400 });
|
|
3436
|
+
identifier = p;
|
|
3437
|
+
channel = "sms";
|
|
3438
|
+
} else {
|
|
3439
|
+
return json({ error: "email or phone required" }, { status: 400 });
|
|
3440
|
+
}
|
|
3441
|
+
const user = channel === "email" ? await userRepo().findOne({ where: { email: identifier } }) : await userRepo().findOne({ where: { phone: identifier } });
|
|
3442
|
+
if (!user || user.deleted || user.blocked) {
|
|
3443
|
+
return json({ ok: true });
|
|
3444
|
+
}
|
|
3445
|
+
} else if (purpose === "verify_email") {
|
|
3446
|
+
if (!emailIn || !isValidSignupEmail(emailIn)) return json({ error: "Valid email required" }, { status: 400 });
|
|
3447
|
+
identifier = emailIn;
|
|
3448
|
+
channel = "email";
|
|
3449
|
+
const user = await userRepo().findOne({ where: { email: identifier } });
|
|
3450
|
+
if (!user || user.deleted) return json({ ok: true });
|
|
3451
|
+
} else {
|
|
3452
|
+
const su = await getSessionUser();
|
|
3453
|
+
const uid = su?.id ? parseInt(String(su.id), 10) : NaN;
|
|
3454
|
+
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
3455
|
+
const p = normalizePhoneE164(phoneIn, defaultPhoneCc);
|
|
3456
|
+
if (!p) return json({ error: "Valid phone required" }, { status: 400 });
|
|
3457
|
+
identifier = p;
|
|
3458
|
+
channel = "sms";
|
|
3459
|
+
const taken = await userRepo().findOne({
|
|
3460
|
+
where: { phone: identifier },
|
|
3461
|
+
select: ["id"]
|
|
3462
|
+
});
|
|
3463
|
+
if (taken && taken.id !== uid) {
|
|
3464
|
+
return json({ error: "Phone already in use" }, { status: 400 });
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
const code = generateNumericOtp(6);
|
|
3468
|
+
const created = await createOtpChallenge(dataSource, entityMap, {
|
|
3469
|
+
purpose,
|
|
3470
|
+
channel,
|
|
3471
|
+
identifier,
|
|
3472
|
+
code,
|
|
3473
|
+
pepper: otpPepper
|
|
3474
|
+
});
|
|
3475
|
+
if (!created.ok) return json({ error: created.error }, { status: created.status });
|
|
3476
|
+
if (!getCms) return json({ error: "OTP delivery not configured" }, { status: 503 });
|
|
3477
|
+
try {
|
|
3478
|
+
const cms = await getCms();
|
|
3479
|
+
if (channel === "email") {
|
|
3480
|
+
if (!cms.getPlugin("email")) return json({ error: "Email not configured" }, { status: 503 });
|
|
3481
|
+
const companyDetails = getCompanyDetails ? await getCompanyDetails() : {};
|
|
3482
|
+
await queueEmail(cms, {
|
|
3483
|
+
to: identifier,
|
|
3484
|
+
templateName: "otp",
|
|
3485
|
+
ctx: { code, companyDetails: companyDetails ?? {} }
|
|
3486
|
+
});
|
|
3487
|
+
} else {
|
|
3488
|
+
if (!cms.getPlugin("sms")) return json({ error: "SMS not configured" }, { status: 503 });
|
|
3489
|
+
const templateKey = purpose === "verify_phone" ? "auth.otp_verify_phone" : "auth.otp_login";
|
|
3490
|
+
await queueSms(cms, { to: identifier, templateKey, variables: { code } });
|
|
3491
|
+
}
|
|
3492
|
+
} catch {
|
|
3493
|
+
return json({ error: "Failed to send code" }, { status: 500 });
|
|
3494
|
+
}
|
|
3495
|
+
return json({ ok: true });
|
|
3496
|
+
}
|
|
3497
|
+
if (path[0] === "auth" && path[1] === "otp" && path[2] === "verify-email" && path.length === 3 && method === "POST") {
|
|
3498
|
+
if (otpOff("verifyEmail")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
3499
|
+
const b = await req.json().catch(() => ({}));
|
|
3500
|
+
const email = typeof b.email === "string" ? b.email.trim().toLowerCase() : "";
|
|
3501
|
+
const code = typeof b.code === "string" ? b.code.trim() : "";
|
|
3502
|
+
if (!email || !code) return json({ error: "email and code required" }, { status: 400 });
|
|
3503
|
+
const v = await verifyAndConsumeOtpChallenge(dataSource, entityMap, {
|
|
3504
|
+
purpose: "verify_email",
|
|
3505
|
+
identifier: email,
|
|
3506
|
+
code,
|
|
3507
|
+
pepper: otpPepper
|
|
3508
|
+
});
|
|
3509
|
+
if (!v.ok) return json({ error: v.error }, { status: v.status });
|
|
3510
|
+
const user = await userRepo().findOne({ where: { email } });
|
|
3511
|
+
if (!user) return json({ error: "User not found" }, { status: 400 });
|
|
3512
|
+
await userRepo().update(user.id, {
|
|
3513
|
+
blocked: false,
|
|
3514
|
+
emailVerifiedAt: /* @__PURE__ */ new Date(),
|
|
3515
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3516
|
+
});
|
|
2540
3517
|
await tokenRepo().delete({ email });
|
|
2541
3518
|
return json({ success: true, message: "Email verified. You can sign in." });
|
|
2542
3519
|
}
|
|
3520
|
+
if (path[0] === "auth" && path[1] === "otp" && path[2] === "verify-phone" && path.length === 3 && method === "POST") {
|
|
3521
|
+
if (otpOff("verifyPhone")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
3522
|
+
const su = await getSessionUser();
|
|
3523
|
+
const uid = su?.id ? parseInt(String(su.id), 10) : NaN;
|
|
3524
|
+
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
3525
|
+
const b = await req.json().catch(() => ({}));
|
|
3526
|
+
const phoneRaw = typeof b.phone === "string" ? b.phone.trim() : "";
|
|
3527
|
+
const code = typeof b.code === "string" ? b.code.trim() : "";
|
|
3528
|
+
const phone = normalizePhoneE164(phoneRaw, defaultPhoneCc);
|
|
3529
|
+
if (!phone || !code) return json({ error: "phone and code required" }, { status: 400 });
|
|
3530
|
+
const v = await verifyAndConsumeOtpChallenge(dataSource, entityMap, {
|
|
3531
|
+
purpose: "verify_phone",
|
|
3532
|
+
identifier: phone,
|
|
3533
|
+
code,
|
|
3534
|
+
pepper: otpPepper
|
|
3535
|
+
});
|
|
3536
|
+
if (!v.ok) return json({ error: v.error }, { status: v.status });
|
|
3537
|
+
const taken = await userRepo().findOne({ where: { phone }, select: ["id"] });
|
|
3538
|
+
if (taken && taken.id !== uid) {
|
|
3539
|
+
return json({ error: "Phone already in use" }, { status: 400 });
|
|
3540
|
+
}
|
|
3541
|
+
await userRepo().update(uid, { phone, phoneVerifiedAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() });
|
|
3542
|
+
const contact = await ensureContactForUser(uid);
|
|
3543
|
+
if (contact) {
|
|
3544
|
+
await contactRepo().update(contact.id, { phone });
|
|
3545
|
+
}
|
|
3546
|
+
return json({ success: true });
|
|
3547
|
+
}
|
|
2543
3548
|
if (path[0] === "register" && path.length === 1 && method === "POST") {
|
|
2544
3549
|
if (!config.hashPassword) return json({ error: "Registration not configured" }, { status: 501 });
|
|
2545
3550
|
const b = await req.json().catch(() => ({}));
|
|
3551
|
+
const capReg = await assertCaptchaOk(getCms, b, req, json);
|
|
3552
|
+
if (capReg) return capReg;
|
|
2546
3553
|
const name = typeof b.name === "string" ? b.name.trim() : "";
|
|
2547
3554
|
const email = typeof b.email === "string" ? b.email.trim().toLowerCase() : "";
|
|
2548
3555
|
const password = typeof b.password === "string" ? b.password : "";
|
|
@@ -2604,6 +3611,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2604
3611
|
}
|
|
2605
3612
|
if (path[0] === "cart" && path[1] === "items" && path.length === 2 && method === "POST") {
|
|
2606
3613
|
const body = await req.json().catch(() => ({}));
|
|
3614
|
+
const capCart = await assertCaptchaOk(getCms, body, req, json);
|
|
3615
|
+
if (capCart) return capCart;
|
|
2607
3616
|
const productId = Number(body.productId);
|
|
2608
3617
|
const quantity = Math.max(1, Number(body.quantity) || 1);
|
|
2609
3618
|
if (!Number.isFinite(productId)) return json({ error: "productId required" }, { status: 400 });
|
|
@@ -2803,6 +3812,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2803
3812
|
const { wishlist, setCookie, err } = await getOrCreateWishlist(req);
|
|
2804
3813
|
if (err) return err;
|
|
2805
3814
|
const b = await req.json().catch(() => ({}));
|
|
3815
|
+
const capWl = await assertCaptchaOk(getCms, b, req, json);
|
|
3816
|
+
if (capWl) return capWl;
|
|
2806
3817
|
const productId = Number(b.productId);
|
|
2807
3818
|
if (!Number.isFinite(productId)) return json({ error: "productId required" }, { status: 400 });
|
|
2808
3819
|
const wid = wishlist.id;
|
|
@@ -2821,6 +3832,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2821
3832
|
}
|
|
2822
3833
|
if (path[0] === "checkout" && path[1] === "order" && path.length === 2 && method === "POST") {
|
|
2823
3834
|
const b = await req.json().catch(() => ({}));
|
|
3835
|
+
const capOrd = await assertCaptchaOk(getCms, b, req, json);
|
|
3836
|
+
if (capOrd) return capOrd;
|
|
2824
3837
|
const u = await getSessionUser();
|
|
2825
3838
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
2826
3839
|
let contactId;
|
|
@@ -2834,8 +3847,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2834
3847
|
relations: ["items", "items.product"]
|
|
2835
3848
|
});
|
|
2836
3849
|
} else {
|
|
2837
|
-
const email = (b.email
|
|
2838
|
-
const name = (b.name
|
|
3850
|
+
const email = String(b.email ?? "").trim();
|
|
3851
|
+
const name = String(b.name ?? "").trim();
|
|
2839
3852
|
if (!email || !name) return json({ error: "name and email required for guest checkout" }, { status: 400 });
|
|
2840
3853
|
let contact = await contactRepo().findOne({ where: { email, deleted: false } });
|
|
2841
3854
|
if (contact && contact.userId != null) {
|
|
@@ -2846,13 +3859,19 @@ function createStorefrontApiHandler(config) {
|
|
|
2846
3859
|
contactRepo().create({
|
|
2847
3860
|
name,
|
|
2848
3861
|
email,
|
|
2849
|
-
phone: b.phone
|
|
3862
|
+
phone: b.phone != null && b.phone !== "" ? String(b.phone) : null,
|
|
2850
3863
|
userId: null,
|
|
2851
3864
|
deleted: false
|
|
2852
3865
|
})
|
|
2853
3866
|
);
|
|
2854
|
-
} else if (name)
|
|
3867
|
+
} else if (name)
|
|
3868
|
+
await contactRepo().update(contact.id, {
|
|
3869
|
+
name,
|
|
3870
|
+
phone: b.phone != null && b.phone !== "" ? String(b.phone) : contact.phone
|
|
3871
|
+
});
|
|
2855
3872
|
contactId = contact.id;
|
|
3873
|
+
const guestForErp = await contactRepo().findOne({ where: { id: contactId } });
|
|
3874
|
+
if (guestForErp) await syncContactToErp(guestForErp);
|
|
2856
3875
|
const cookies = parseCookies(req.headers.get("cookie"));
|
|
2857
3876
|
const guestToken = cookies[cookieName];
|
|
2858
3877
|
if (!guestToken) return json({ error: "Cart not found" }, { status: 400 });
|
|
@@ -2880,10 +3899,12 @@ function createStorefrontApiHandler(config) {
|
|
|
2880
3899
|
const cartId = cart.id;
|
|
2881
3900
|
const ord = await orderRepo().save(
|
|
2882
3901
|
orderRepo().create({
|
|
2883
|
-
orderNumber:
|
|
3902
|
+
orderNumber: temporaryOrderNumberPlaceholder(),
|
|
3903
|
+
orderKind: "sale",
|
|
3904
|
+
parentOrderId: null,
|
|
2884
3905
|
contactId,
|
|
2885
|
-
billingAddressId: b.billingAddressId
|
|
2886
|
-
shippingAddressId: b.shippingAddressId
|
|
3906
|
+
billingAddressId: typeof b.billingAddressId === "number" ? b.billingAddressId : null,
|
|
3907
|
+
shippingAddressId: typeof b.shippingAddressId === "number" ? b.shippingAddressId : null,
|
|
2887
3908
|
status: "pending",
|
|
2888
3909
|
subtotal,
|
|
2889
3910
|
tax: 0,
|
|
@@ -2894,6 +3915,9 @@ function createStorefrontApiHandler(config) {
|
|
|
2894
3915
|
})
|
|
2895
3916
|
);
|
|
2896
3917
|
const oid = ord.id;
|
|
3918
|
+
await orderRepo().update(oid, {
|
|
3919
|
+
orderNumber: buildCanonicalOrderNumber("sale", oid, ord.createdAt ?? /* @__PURE__ */ new Date())
|
|
3920
|
+
});
|
|
2897
3921
|
for (const line of lines) {
|
|
2898
3922
|
await orderItemRepo().save(
|
|
2899
3923
|
orderItemRepo().create({
|
|
@@ -2915,6 +3939,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2915
3939
|
}
|
|
2916
3940
|
if (path[0] === "checkout" && path.length === 1 && method === "POST") {
|
|
2917
3941
|
const b = await req.json().catch(() => ({}));
|
|
3942
|
+
const capChk = await assertCaptchaOk(getCms, b, req, json);
|
|
3943
|
+
if (capChk) return capChk;
|
|
2918
3944
|
const u = await getSessionUser();
|
|
2919
3945
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
2920
3946
|
let contactId;
|
|
@@ -2928,8 +3954,8 @@ function createStorefrontApiHandler(config) {
|
|
|
2928
3954
|
relations: ["items", "items.product"]
|
|
2929
3955
|
});
|
|
2930
3956
|
} else {
|
|
2931
|
-
const email = (b.email
|
|
2932
|
-
const name = (b.name
|
|
3957
|
+
const email = String(b.email ?? "").trim();
|
|
3958
|
+
const name = String(b.name ?? "").trim();
|
|
2933
3959
|
if (!email || !name) return json({ error: "name and email required for guest checkout" }, { status: 400 });
|
|
2934
3960
|
let contact = await contactRepo().findOne({ where: { email, deleted: false } });
|
|
2935
3961
|
if (contact && contact.userId != null) {
|
|
@@ -2940,13 +3966,19 @@ function createStorefrontApiHandler(config) {
|
|
|
2940
3966
|
contactRepo().create({
|
|
2941
3967
|
name,
|
|
2942
3968
|
email,
|
|
2943
|
-
phone: b.phone
|
|
3969
|
+
phone: b.phone != null && b.phone !== "" ? String(b.phone) : null,
|
|
2944
3970
|
userId: null,
|
|
2945
3971
|
deleted: false
|
|
2946
3972
|
})
|
|
2947
3973
|
);
|
|
2948
|
-
} else if (name)
|
|
3974
|
+
} else if (name)
|
|
3975
|
+
await contactRepo().update(contact.id, {
|
|
3976
|
+
name,
|
|
3977
|
+
phone: b.phone != null && b.phone !== "" ? String(b.phone) : contact.phone
|
|
3978
|
+
});
|
|
2949
3979
|
contactId = contact.id;
|
|
3980
|
+
const guestForErp2 = await contactRepo().findOne({ where: { id: contactId } });
|
|
3981
|
+
if (guestForErp2) await syncContactToErp(guestForErp2);
|
|
2950
3982
|
const cookies = parseCookies(req.headers.get("cookie"));
|
|
2951
3983
|
const guestToken = cookies[cookieName];
|
|
2952
3984
|
if (!guestToken) return json({ error: "Cart not found" }, { status: 400 });
|
|
@@ -2973,10 +4005,12 @@ function createStorefrontApiHandler(config) {
|
|
|
2973
4005
|
const total = subtotal;
|
|
2974
4006
|
const ord = await orderRepo().save(
|
|
2975
4007
|
orderRepo().create({
|
|
2976
|
-
orderNumber:
|
|
4008
|
+
orderNumber: temporaryOrderNumberPlaceholder(),
|
|
4009
|
+
orderKind: "sale",
|
|
4010
|
+
parentOrderId: null,
|
|
2977
4011
|
contactId,
|
|
2978
|
-
billingAddressId: b.billingAddressId
|
|
2979
|
-
shippingAddressId: b.shippingAddressId
|
|
4012
|
+
billingAddressId: typeof b.billingAddressId === "number" ? b.billingAddressId : null,
|
|
4013
|
+
shippingAddressId: typeof b.shippingAddressId === "number" ? b.shippingAddressId : null,
|
|
2980
4014
|
status: "pending",
|
|
2981
4015
|
subtotal,
|
|
2982
4016
|
tax: 0,
|
|
@@ -2986,6 +4020,9 @@ function createStorefrontApiHandler(config) {
|
|
|
2986
4020
|
})
|
|
2987
4021
|
);
|
|
2988
4022
|
const oid = ord.id;
|
|
4023
|
+
await orderRepo().update(oid, {
|
|
4024
|
+
orderNumber: buildCanonicalOrderNumber("sale", oid, ord.createdAt ?? /* @__PURE__ */ new Date())
|
|
4025
|
+
});
|
|
2989
4026
|
for (const line of lines) {
|
|
2990
4027
|
await orderItemRepo().save(
|
|
2991
4028
|
orderItemRepo().create({
|
|
@@ -3013,7 +4050,7 @@ function createStorefrontApiHandler(config) {
|
|
|
3013
4050
|
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
3014
4051
|
if (!contact) return json({ orders: [] });
|
|
3015
4052
|
const orders = await orderRepo().find({
|
|
3016
|
-
where: { contactId: contact.id, deleted: false },
|
|
4053
|
+
where: { contactId: contact.id, deleted: false, orderKind: "sale" },
|
|
3017
4054
|
order: { createdAt: "DESC" },
|
|
3018
4055
|
take: 50
|
|
3019
4056
|
});
|
|
@@ -3021,7 +4058,7 @@ function createStorefrontApiHandler(config) {
|
|
|
3021
4058
|
const previewByOrder = {};
|
|
3022
4059
|
if (orderIds.length) {
|
|
3023
4060
|
const oItems = await orderItemRepo().find({
|
|
3024
|
-
where: { orderId: (0,
|
|
4061
|
+
where: { orderId: (0, import_typeorm5.In)(orderIds) },
|
|
3025
4062
|
relations: ["product"],
|
|
3026
4063
|
order: { id: "ASC" }
|
|
3027
4064
|
});
|
|
@@ -3048,6 +4085,20 @@ function createStorefrontApiHandler(config) {
|
|
|
3048
4085
|
})
|
|
3049
4086
|
});
|
|
3050
4087
|
}
|
|
4088
|
+
if (path[0] === "orders" && path.length === 3 && path[2] === "invoice" && method === "GET") {
|
|
4089
|
+
const u = await getSessionUser();
|
|
4090
|
+
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
4091
|
+
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
4092
|
+
if (!getCms) return json({ error: "Not found" }, { status: 404 });
|
|
4093
|
+
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
4094
|
+
if (!contact) return json({ error: "Not found" }, { status: 404 });
|
|
4095
|
+
const orderId = parseInt(path[1], 10);
|
|
4096
|
+
if (!Number.isFinite(orderId)) return json({ error: "Invalid id" }, { status: 400 });
|
|
4097
|
+
const cms = await getCms();
|
|
4098
|
+
return streamOrderInvoicePdf(cms, dataSource, entityMap, orderId, {
|
|
4099
|
+
ownerContactId: contact.id
|
|
4100
|
+
});
|
|
4101
|
+
}
|
|
3051
4102
|
if (path[0] === "orders" && path.length === 2 && method === "GET") {
|
|
3052
4103
|
const u = await getSessionUser();
|
|
3053
4104
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
@@ -3055,11 +4106,20 @@ function createStorefrontApiHandler(config) {
|
|
|
3055
4106
|
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
3056
4107
|
if (!contact) return json({ error: "Not found" }, { status: 404 });
|
|
3057
4108
|
const orderId = parseInt(path[1], 10);
|
|
3058
|
-
|
|
4109
|
+
let order = await orderRepo().findOne({
|
|
3059
4110
|
where: { id: orderId, contactId: contact.id, deleted: false },
|
|
3060
4111
|
relations: ["items", "items.product"]
|
|
3061
4112
|
});
|
|
3062
4113
|
if (!order) return json({ error: "Not found" }, { status: 404 });
|
|
4114
|
+
if (getCms) {
|
|
4115
|
+
const cms = await getCms();
|
|
4116
|
+
await tryRefreshOrderFromErpForStorefront(cms, dataSource, entityMap, order);
|
|
4117
|
+
order = await orderRepo().findOne({
|
|
4118
|
+
where: { id: orderId, contactId: contact.id, deleted: false },
|
|
4119
|
+
relations: ["items", "items.product"]
|
|
4120
|
+
});
|
|
4121
|
+
}
|
|
4122
|
+
if (!order) return json({ error: "Not found" }, { status: 404 });
|
|
3063
4123
|
const o = order;
|
|
3064
4124
|
const lines = (o.items || []).map((line) => {
|
|
3065
4125
|
const p = line.product;
|
|
@@ -3078,10 +4138,22 @@ function createStorefrontApiHandler(config) {
|
|
|
3078
4138
|
} : null
|
|
3079
4139
|
};
|
|
3080
4140
|
});
|
|
4141
|
+
const kind = o.orderKind || "sale";
|
|
4142
|
+
let relatedOrders = [];
|
|
4143
|
+
if (kind === "sale") {
|
|
4144
|
+
relatedOrders = await orderRepo().find({
|
|
4145
|
+
where: { parentOrderId: orderId, deleted: false },
|
|
4146
|
+
order: { id: "ASC" }
|
|
4147
|
+
});
|
|
4148
|
+
}
|
|
4149
|
+
const meta = o.metadata;
|
|
4150
|
+
const fulfillmentPreview = meta && typeof meta.fulfillment === "object" && meta.fulfillment && "status" in meta.fulfillment ? String(meta.fulfillment.status ?? "") : "";
|
|
3081
4151
|
return json({
|
|
3082
4152
|
order: {
|
|
3083
4153
|
id: o.id,
|
|
3084
4154
|
orderNumber: o.orderNumber,
|
|
4155
|
+
orderKind: kind,
|
|
4156
|
+
parentOrderId: o.parentOrderId ?? null,
|
|
3085
4157
|
status: o.status,
|
|
3086
4158
|
subtotal: o.subtotal,
|
|
3087
4159
|
tax: o.tax,
|
|
@@ -3089,8 +4161,18 @@ function createStorefrontApiHandler(config) {
|
|
|
3089
4161
|
total: o.total,
|
|
3090
4162
|
currency: o.currency,
|
|
3091
4163
|
createdAt: o.createdAt,
|
|
4164
|
+
metadata: o.metadata ?? null,
|
|
3092
4165
|
items: lines
|
|
3093
|
-
}
|
|
4166
|
+
},
|
|
4167
|
+
relatedOrders: relatedOrders.map((r) => ({
|
|
4168
|
+
id: r.id,
|
|
4169
|
+
orderNumber: r.orderNumber,
|
|
4170
|
+
orderKind: r.orderKind ?? "return",
|
|
4171
|
+
status: r.status,
|
|
4172
|
+
createdAt: r.createdAt,
|
|
4173
|
+
fulfillmentStatus: r.metadata && typeof r.metadata === "object" && r.metadata.fulfillment?.status
|
|
4174
|
+
})),
|
|
4175
|
+
fulfillmentPreview: fulfillmentPreview || void 0
|
|
3094
4176
|
});
|
|
3095
4177
|
}
|
|
3096
4178
|
return json({ error: "Not found" }, { status: 404 });
|
|
@@ -3119,6 +4201,7 @@ function createStorefrontApiHandler(config) {
|
|
|
3119
4201
|
createUserAuthApiRouter,
|
|
3120
4202
|
createUserAvatarHandler,
|
|
3121
4203
|
createUserProfileHandler,
|
|
3122
|
-
createUsersApiHandlers
|
|
4204
|
+
createUsersApiHandlers,
|
|
4205
|
+
getPublicSettingsGroup
|
|
3123
4206
|
});
|
|
3124
4207
|
//# sourceMappingURL=api.cjs.map
|