@infuro/cms-core 1.0.12 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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: orderNumber2, total, currency, customerName, customerEmail, salesTeamEmails, companyDetails, lineItems } = payload;
88
+ const { orderNumber, total, currency, customerName, customerEmail, salesTeamEmails, companyDetails, lineItems } = payload;
72
89
  const base = {
73
- orderNumber: orderNumber2,
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, {
@@ -145,6 +297,75 @@ module.exports = __toCommonJS(api_exports);
145
297
 
146
298
  // src/api/crud.ts
147
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
148
369
  var DATE_COLUMN_TYPES = /* @__PURE__ */ new Set([
149
370
  "date",
150
371
  "datetime",
@@ -197,8 +418,27 @@ function buildSearchWhereClause(repo, search) {
197
418
  if (ors.length === 0) return {};
198
419
  return ors.length === 1 ? ors[0] : ors;
199
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
+ }
200
439
  function createCrudHandler(dataSource, entityMap, options) {
201
- const { requireAuth, json, requireEntityPermission: reqPerm } = options;
440
+ const { requireAuth, json, requireEntityPermission: reqPerm, getCms } = options;
441
+ const syncContactRowToErp = makeContactErpSync(dataSource, entityMap, getCms);
202
442
  async function authz(req, resource, action) {
203
443
  const authError = await requireAuth(req);
204
444
  if (authError) return authError;
@@ -400,6 +640,13 @@ function createCrudHandler(dataSource, entityMap, options) {
400
640
  const repo = dataSource.getRepository(entity);
401
641
  sanitizeBodyForEntity(repo, body);
402
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
+ }
403
650
  return json(created, { status: 201 });
404
651
  },
405
652
  async GET_METADATA(req, resource) {
@@ -506,7 +753,8 @@ function createCrudHandler(dataSource, entityMap, options) {
506
753
  };
507
754
  }
508
755
  function createCrudByIdHandler(dataSource, entityMap, options) {
509
- const { requireAuth, json, requireEntityPermission: reqPerm } = options;
756
+ const { requireAuth, json, requireEntityPermission: reqPerm, getCms } = options;
757
+ const syncContactRowToErp = makeContactErpSync(dataSource, entityMap, getCms);
510
758
  async function authz(req, resource, action) {
511
759
  const authError = await requireAuth(req);
512
760
  if (authError) return authError;
@@ -529,7 +777,11 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
529
777
  relations: ["contact", "billingAddress", "shippingAddress", "items", "items.product", "items.product.collection", "payments"]
530
778
  });
531
779
  if (!order) return json({ message: "Not found" }, { status: 404 });
532
- return json(order);
780
+ const relatedOrders = await repo.find({
781
+ where: { parentOrderId: Number(id), deleted: false },
782
+ order: { id: "ASC" }
783
+ });
784
+ return json({ ...order, relatedOrders });
533
785
  }
534
786
  if (resource === "contacts") {
535
787
  const contact = await repo.findOne({
@@ -662,6 +914,13 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
662
914
  await repo.update(numericId, updatePayload);
663
915
  }
664
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
+ }
665
924
  return updated ? json(updated) : json({ message: "Not found" }, { status: 404 });
666
925
  },
667
926
  async DELETE(req, resource, id) {
@@ -826,6 +1085,24 @@ function createUserAuthApiRouter(config) {
826
1085
  // src/api/cms-handlers.ts
827
1086
  var import_typeorm3 = require("typeorm");
828
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
829
1106
  function createDashboardStatsHandler(config) {
830
1107
  const { dataSource, entityMap, json, requireAuth, requirePermission, requireEntityPermission } = config;
831
1108
  return async function GET(req) {
@@ -1067,6 +1344,32 @@ function createFormSaveHandlers(config) {
1067
1344
  }
1068
1345
  };
1069
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
+ }
1070
1373
  function createFormSubmissionGetByIdHandler(config) {
1071
1374
  const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
1072
1375
  return async function GET(req, id) {
@@ -1155,13 +1458,15 @@ function pickContactFromSubmission(fields, data) {
1155
1458
  return { name: name || email, email, phone: phone || null };
1156
1459
  }
1157
1460
  function createFormSubmissionHandler(config) {
1158
- const { dataSource, entityMap, json } = config;
1461
+ const { dataSource, entityMap, json, getCms } = config;
1159
1462
  return async function POST(req) {
1160
1463
  try {
1161
1464
  const body = await req.json();
1162
1465
  if (!body || typeof body !== "object") {
1163
1466
  return json({ error: "Invalid request payload" }, { status: 400 });
1164
1467
  }
1468
+ const captchaErr = await assertCaptchaOk(getCms, body, req, json);
1469
+ if (captchaErr) return captchaErr;
1165
1470
  const formId = typeof body.formId === "number" ? body.formId : Number(body.formId);
1166
1471
  if (!Number.isInteger(formId) || formId <= 0) {
1167
1472
  return json({ error: "formId is required and must be a positive integer" }, { status: 400 });
@@ -1228,28 +1533,44 @@ function createFormSubmissionHandler(config) {
1228
1533
  contactEmail = contactData.email;
1229
1534
  }
1230
1535
  }
1231
- if (config.getCms && config.getCompanyDetails && config.getRecipientForChannel) {
1536
+ if (config.getCms) {
1232
1537
  try {
1233
1538
  const cms = await config.getCms();
1234
- const to = await config.getRecipientForChannel("crm");
1235
- if (to) {
1236
- const companyDetails = await config.getCompanyDetails();
1237
- const formFieldRows = activeFields.map((f) => ({
1238
- label: f.label && String(f.label).trim() || `Field ${f.id}`,
1239
- value: formatSubmissionFieldValue(data[String(f.id)])
1240
- }));
1241
- await queueEmail(cms, {
1242
- to,
1243
- templateName: "formSubmission",
1244
- ctx: {
1245
- formName,
1246
- contactName,
1247
- contactEmail,
1248
- formData: data,
1249
- formFieldRows,
1250
- companyDetails: companyDetails ?? {}
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
+ );
1251
1572
  }
1252
- });
1573
+ }
1253
1574
  }
1254
1575
  } catch {
1255
1576
  }
@@ -1695,6 +2016,125 @@ ${contextParts.join("\n\n")}` : "You are a helpful assistant for the company. If
1695
2016
  };
1696
2017
  }
1697
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
+
1698
2138
  // src/auth/permission-entities.ts
1699
2139
  var PERMISSION_ENTITY_INTERNAL_EXCLUDE = /* @__PURE__ */ new Set([
1700
2140
  "users",
@@ -1708,7 +2148,8 @@ var PERMISSION_ENTITY_INTERNAL_EXCLUDE = /* @__PURE__ */ new Set([
1708
2148
  "carts",
1709
2149
  "cart_items",
1710
2150
  "wishlists",
1711
- "wishlist_items"
2151
+ "wishlist_items",
2152
+ "message_templates"
1712
2153
  ]);
1713
2154
  var PERMISSION_LOGICAL_ENTITIES = [
1714
2155
  "users",
@@ -1894,7 +2335,8 @@ var DEFAULT_EXCLUDE = /* @__PURE__ */ new Set([
1894
2335
  "carts",
1895
2336
  "cart_items",
1896
2337
  "wishlists",
1897
- "wishlist_items"
2338
+ "wishlist_items",
2339
+ "message_templates"
1898
2340
  ]);
1899
2341
  function createCmsApiHandler(config) {
1900
2342
  const {
@@ -1957,7 +2399,8 @@ function createCmsApiHandler(config) {
1957
2399
  const crudOpts = {
1958
2400
  requireAuth: config.requireAuth,
1959
2401
  json: config.json,
1960
- requireEntityPermission: reqEntityPerm
2402
+ requireEntityPermission: reqEntityPerm,
2403
+ getCms
1961
2404
  };
1962
2405
  const crud = createCrudHandler(dataSource, entityMap, crudOpts);
1963
2406
  const crudById = createCrudByIdHandler(dataSource, entityMap, crudOpts);
@@ -1987,6 +2430,13 @@ function createCmsApiHandler(config) {
1987
2430
  const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
1988
2431
  const profilePut = userProfile ? createUserProfileHandler(userProfile) : null;
1989
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
+ });
1990
2440
  const chatHandlers = chatConfig ? createChatHandlers(chatConfig) : null;
1991
2441
  function resolveResource(segment) {
1992
2442
  const model = pathToModel(segment);
@@ -2091,11 +2541,28 @@ function createCmsApiHandler(config) {
2091
2541
  return settingsHandlers.PUT(req, group);
2092
2542
  }
2093
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
+ }
2094
2548
  if (path[0] === "chat" && chatHandlers) {
2095
2549
  if (path.length === 2 && path[1] === "identify" && method === "POST") return chatHandlers.identify(req);
2096
2550
  if (path.length === 4 && path[1] === "conversations" && path[3] === "messages" && method === "GET") return chatHandlers.getMessages(req, path[2]);
2097
2551
  if (path.length === 2 && path[1] === "messages" && method === "POST") return chatHandlers.postMessage(req);
2098
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
+ }
2099
2566
  if (path.length === 0) return config.json({ error: "Not found" }, { status: 404 });
2100
2567
  const resource = resolveResource(path[0]);
2101
2568
  if (!crudResources.includes(resource)) return config.json({ error: "Invalid resource" }, { status: 400 });
@@ -2128,7 +2595,7 @@ function createCmsApiHandler(config) {
2128
2595
  }
2129
2596
 
2130
2597
  // src/api/storefront-handlers.ts
2131
- var import_typeorm4 = require("typeorm");
2598
+ var import_typeorm5 = require("typeorm");
2132
2599
 
2133
2600
  // src/lib/is-valid-signup-email.ts
2134
2601
  var MAX_EMAIL = 254;
@@ -2150,6 +2617,339 @@ function isValidSignupEmail(email) {
2150
2617
 
2151
2618
  // src/api/storefront-handlers.ts
2152
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
2153
2953
  var GUEST_COOKIE = "guest_id";
2154
2954
  var ONE_YEAR = 60 * 60 * 24 * 365;
2155
2955
  function parseCookies(header) {
@@ -2167,13 +2967,17 @@ function parseCookies(header) {
2167
2967
  function guestCookieHeader(name, token) {
2168
2968
  return `${name}=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${ONE_YEAR}`;
2169
2969
  }
2170
- function orderNumber() {
2171
- return `ORD-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
2172
- }
2173
2970
  var SIGNUP_VERIFY_EXPIRY_HOURS = 72;
2174
2971
  function createStorefrontApiHandler(config) {
2175
2972
  const { dataSource, entityMap, json, getSessionUser, getCms, getCompanyDetails, publicSiteUrl } = config;
2176
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
+ }
2177
2981
  const cartRepo = () => dataSource.getRepository(entityMap.carts);
2178
2982
  const cartItemRepo = () => dataSource.getRepository(entityMap.cart_items);
2179
2983
  const productRepo = () => dataSource.getRepository(entityMap.products);
@@ -2187,13 +2991,28 @@ function createStorefrontApiHandler(config) {
2187
2991
  const tokenRepo = () => dataSource.getRepository(entityMap.password_reset_tokens);
2188
2992
  const collectionRepo = () => dataSource.getRepository(entityMap.collections);
2189
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
+ }
2190
3009
  async function ensureContactForUser(userId) {
2191
3010
  let c = await contactRepo().findOne({ where: { userId, deleted: false } });
2192
3011
  if (c) return c;
2193
3012
  const u = await userRepo().findOne({ where: { id: userId } });
2194
3013
  if (!u) return null;
2195
3014
  const unclaimed = await contactRepo().findOne({
2196
- where: { email: u.email, userId: (0, import_typeorm4.IsNull)(), deleted: false }
3015
+ where: { email: u.email, userId: (0, import_typeorm5.IsNull)(), deleted: false }
2197
3016
  });
2198
3017
  if (unclaimed) {
2199
3018
  await contactRepo().update(unclaimed.id, { userId });
@@ -2208,6 +3027,7 @@ function createStorefrontApiHandler(config) {
2208
3027
  deleted: false
2209
3028
  })
2210
3029
  );
3030
+ await syncContactToErp(created);
2211
3031
  return { id: created.id };
2212
3032
  }
2213
3033
  async function getOrCreateCart(req) {
@@ -2302,7 +3122,21 @@ function createStorefrontApiHandler(config) {
2302
3122
  })
2303
3123
  };
2304
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
+ }
2305
3138
  function serializeProduct(p) {
3139
+ const seo = serializeSeo(p.seo);
2306
3140
  return {
2307
3141
  id: p.id,
2308
3142
  name: p.name,
@@ -2313,7 +3147,8 @@ function createStorefrontApiHandler(config) {
2313
3147
  compareAtPrice: p.compareAtPrice,
2314
3148
  status: p.status,
2315
3149
  collectionId: p.collectionId,
2316
- metadata: p.metadata
3150
+ metadata: p.metadata,
3151
+ ...seo ? { seo } : {}
2317
3152
  };
2318
3153
  }
2319
3154
  return {
@@ -2380,7 +3215,7 @@ function createStorefrontApiHandler(config) {
2380
3215
  const byId = /^\d+$/.test(idOrSlug);
2381
3216
  const product = await productRepo().findOne({
2382
3217
  where: byId ? { id: parseInt(idOrSlug, 10), status: "available", deleted: false } : { slug: idOrSlug, status: "available", deleted: false },
2383
- relations: ["attributes", "attributes.attribute"]
3218
+ relations: ["attributes", "attributes.attribute", "seo"]
2384
3219
  });
2385
3220
  if (!product) return json({ error: "Not found" }, { status: 404 });
2386
3221
  const p = product;
@@ -2424,7 +3259,8 @@ function createStorefrontApiHandler(config) {
2424
3259
  const idOrSlug = path[1];
2425
3260
  const byId = /^\d+$/.test(idOrSlug);
2426
3261
  const collection = await collectionRepo().findOne({
2427
- 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"]
2428
3264
  });
2429
3265
  if (!collection) return json({ error: "Not found" }, { status: 404 });
2430
3266
  const col = collection;
@@ -2432,12 +3268,14 @@ function createStorefrontApiHandler(config) {
2432
3268
  where: { collectionId: col.id, status: "available", deleted: false },
2433
3269
  order: { id: "ASC" }
2434
3270
  });
3271
+ const colSeo = serializeSeo(col.seo);
2435
3272
  return json({
2436
3273
  id: col.id,
2437
3274
  name: col.name,
2438
3275
  slug: col.slug,
2439
3276
  description: col.description,
2440
3277
  image: col.image,
3278
+ ...colSeo ? { seo: colSeo } : {},
2441
3279
  products: products.map((p) => serializeProduct(p))
2442
3280
  });
2443
3281
  }
@@ -2475,6 +3313,7 @@ function createStorefrontApiHandler(config) {
2475
3313
  await userRepo().update(uid, { name: b.name.trim() });
2476
3314
  }
2477
3315
  const updatedContact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
3316
+ if (updatedContact) await syncContactToErp(updatedContact);
2478
3317
  const updatedUser = await userRepo().findOne({ where: { id: uid }, select: ["id", "name", "email"] });
2479
3318
  return json({
2480
3319
  user: updatedUser ? { id: updatedUser.id, name: updatedUser.name, email: updatedUser.email } : null,
@@ -2562,13 +3401,155 @@ function createStorefrontApiHandler(config) {
2562
3401
  const email = record.email;
2563
3402
  const user = await userRepo().findOne({ where: { email }, select: ["id", "blocked"] });
2564
3403
  if (!user) return json({ error: "User not found" }, { status: 400 });
2565
- await userRepo().update(user.id, { blocked: false, updatedAt: /* @__PURE__ */ new Date() });
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
+ });
2566
3517
  await tokenRepo().delete({ email });
2567
3518
  return json({ success: true, message: "Email verified. You can sign in." });
2568
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
+ }
2569
3548
  if (path[0] === "register" && path.length === 1 && method === "POST") {
2570
3549
  if (!config.hashPassword) return json({ error: "Registration not configured" }, { status: 501 });
2571
3550
  const b = await req.json().catch(() => ({}));
3551
+ const capReg = await assertCaptchaOk(getCms, b, req, json);
3552
+ if (capReg) return capReg;
2572
3553
  const name = typeof b.name === "string" ? b.name.trim() : "";
2573
3554
  const email = typeof b.email === "string" ? b.email.trim().toLowerCase() : "";
2574
3555
  const password = typeof b.password === "string" ? b.password : "";
@@ -2630,6 +3611,8 @@ function createStorefrontApiHandler(config) {
2630
3611
  }
2631
3612
  if (path[0] === "cart" && path[1] === "items" && path.length === 2 && method === "POST") {
2632
3613
  const body = await req.json().catch(() => ({}));
3614
+ const capCart = await assertCaptchaOk(getCms, body, req, json);
3615
+ if (capCart) return capCart;
2633
3616
  const productId = Number(body.productId);
2634
3617
  const quantity = Math.max(1, Number(body.quantity) || 1);
2635
3618
  if (!Number.isFinite(productId)) return json({ error: "productId required" }, { status: 400 });
@@ -2829,6 +3812,8 @@ function createStorefrontApiHandler(config) {
2829
3812
  const { wishlist, setCookie, err } = await getOrCreateWishlist(req);
2830
3813
  if (err) return err;
2831
3814
  const b = await req.json().catch(() => ({}));
3815
+ const capWl = await assertCaptchaOk(getCms, b, req, json);
3816
+ if (capWl) return capWl;
2832
3817
  const productId = Number(b.productId);
2833
3818
  if (!Number.isFinite(productId)) return json({ error: "productId required" }, { status: 400 });
2834
3819
  const wid = wishlist.id;
@@ -2847,6 +3832,8 @@ function createStorefrontApiHandler(config) {
2847
3832
  }
2848
3833
  if (path[0] === "checkout" && path[1] === "order" && path.length === 2 && method === "POST") {
2849
3834
  const b = await req.json().catch(() => ({}));
3835
+ const capOrd = await assertCaptchaOk(getCms, b, req, json);
3836
+ if (capOrd) return capOrd;
2850
3837
  const u = await getSessionUser();
2851
3838
  const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
2852
3839
  let contactId;
@@ -2860,8 +3847,8 @@ function createStorefrontApiHandler(config) {
2860
3847
  relations: ["items", "items.product"]
2861
3848
  });
2862
3849
  } else {
2863
- const email = (b.email || "").trim();
2864
- const name = (b.name || "").trim();
3850
+ const email = String(b.email ?? "").trim();
3851
+ const name = String(b.name ?? "").trim();
2865
3852
  if (!email || !name) return json({ error: "name and email required for guest checkout" }, { status: 400 });
2866
3853
  let contact = await contactRepo().findOne({ where: { email, deleted: false } });
2867
3854
  if (contact && contact.userId != null) {
@@ -2872,13 +3859,19 @@ function createStorefrontApiHandler(config) {
2872
3859
  contactRepo().create({
2873
3860
  name,
2874
3861
  email,
2875
- phone: b.phone || null,
3862
+ phone: b.phone != null && b.phone !== "" ? String(b.phone) : null,
2876
3863
  userId: null,
2877
3864
  deleted: false
2878
3865
  })
2879
3866
  );
2880
- } else if (name) await contactRepo().update(contact.id, { name, phone: b.phone || contact.phone });
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
+ });
2881
3872
  contactId = contact.id;
3873
+ const guestForErp = await contactRepo().findOne({ where: { id: contactId } });
3874
+ if (guestForErp) await syncContactToErp(guestForErp);
2882
3875
  const cookies = parseCookies(req.headers.get("cookie"));
2883
3876
  const guestToken = cookies[cookieName];
2884
3877
  if (!guestToken) return json({ error: "Cart not found" }, { status: 400 });
@@ -2906,10 +3899,12 @@ function createStorefrontApiHandler(config) {
2906
3899
  const cartId = cart.id;
2907
3900
  const ord = await orderRepo().save(
2908
3901
  orderRepo().create({
2909
- orderNumber: orderNumber(),
3902
+ orderNumber: temporaryOrderNumberPlaceholder(),
3903
+ orderKind: "sale",
3904
+ parentOrderId: null,
2910
3905
  contactId,
2911
- billingAddressId: b.billingAddressId ?? null,
2912
- shippingAddressId: b.shippingAddressId ?? null,
3906
+ billingAddressId: typeof b.billingAddressId === "number" ? b.billingAddressId : null,
3907
+ shippingAddressId: typeof b.shippingAddressId === "number" ? b.shippingAddressId : null,
2913
3908
  status: "pending",
2914
3909
  subtotal,
2915
3910
  tax: 0,
@@ -2920,6 +3915,9 @@ function createStorefrontApiHandler(config) {
2920
3915
  })
2921
3916
  );
2922
3917
  const oid = ord.id;
3918
+ await orderRepo().update(oid, {
3919
+ orderNumber: buildCanonicalOrderNumber("sale", oid, ord.createdAt ?? /* @__PURE__ */ new Date())
3920
+ });
2923
3921
  for (const line of lines) {
2924
3922
  await orderItemRepo().save(
2925
3923
  orderItemRepo().create({
@@ -2941,6 +3939,8 @@ function createStorefrontApiHandler(config) {
2941
3939
  }
2942
3940
  if (path[0] === "checkout" && path.length === 1 && method === "POST") {
2943
3941
  const b = await req.json().catch(() => ({}));
3942
+ const capChk = await assertCaptchaOk(getCms, b, req, json);
3943
+ if (capChk) return capChk;
2944
3944
  const u = await getSessionUser();
2945
3945
  const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
2946
3946
  let contactId;
@@ -2954,8 +3954,8 @@ function createStorefrontApiHandler(config) {
2954
3954
  relations: ["items", "items.product"]
2955
3955
  });
2956
3956
  } else {
2957
- const email = (b.email || "").trim();
2958
- const name = (b.name || "").trim();
3957
+ const email = String(b.email ?? "").trim();
3958
+ const name = String(b.name ?? "").trim();
2959
3959
  if (!email || !name) return json({ error: "name and email required for guest checkout" }, { status: 400 });
2960
3960
  let contact = await contactRepo().findOne({ where: { email, deleted: false } });
2961
3961
  if (contact && contact.userId != null) {
@@ -2966,13 +3966,19 @@ function createStorefrontApiHandler(config) {
2966
3966
  contactRepo().create({
2967
3967
  name,
2968
3968
  email,
2969
- phone: b.phone || null,
3969
+ phone: b.phone != null && b.phone !== "" ? String(b.phone) : null,
2970
3970
  userId: null,
2971
3971
  deleted: false
2972
3972
  })
2973
3973
  );
2974
- } else if (name) await contactRepo().update(contact.id, { name, phone: b.phone || contact.phone });
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
+ });
2975
3979
  contactId = contact.id;
3980
+ const guestForErp2 = await contactRepo().findOne({ where: { id: contactId } });
3981
+ if (guestForErp2) await syncContactToErp(guestForErp2);
2976
3982
  const cookies = parseCookies(req.headers.get("cookie"));
2977
3983
  const guestToken = cookies[cookieName];
2978
3984
  if (!guestToken) return json({ error: "Cart not found" }, { status: 400 });
@@ -2999,10 +4005,12 @@ function createStorefrontApiHandler(config) {
2999
4005
  const total = subtotal;
3000
4006
  const ord = await orderRepo().save(
3001
4007
  orderRepo().create({
3002
- orderNumber: orderNumber(),
4008
+ orderNumber: temporaryOrderNumberPlaceholder(),
4009
+ orderKind: "sale",
4010
+ parentOrderId: null,
3003
4011
  contactId,
3004
- billingAddressId: b.billingAddressId ?? null,
3005
- shippingAddressId: b.shippingAddressId ?? null,
4012
+ billingAddressId: typeof b.billingAddressId === "number" ? b.billingAddressId : null,
4013
+ shippingAddressId: typeof b.shippingAddressId === "number" ? b.shippingAddressId : null,
3006
4014
  status: "pending",
3007
4015
  subtotal,
3008
4016
  tax: 0,
@@ -3012,6 +4020,9 @@ function createStorefrontApiHandler(config) {
3012
4020
  })
3013
4021
  );
3014
4022
  const oid = ord.id;
4023
+ await orderRepo().update(oid, {
4024
+ orderNumber: buildCanonicalOrderNumber("sale", oid, ord.createdAt ?? /* @__PURE__ */ new Date())
4025
+ });
3015
4026
  for (const line of lines) {
3016
4027
  await orderItemRepo().save(
3017
4028
  orderItemRepo().create({
@@ -3039,7 +4050,7 @@ function createStorefrontApiHandler(config) {
3039
4050
  const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
3040
4051
  if (!contact) return json({ orders: [] });
3041
4052
  const orders = await orderRepo().find({
3042
- where: { contactId: contact.id, deleted: false },
4053
+ where: { contactId: contact.id, deleted: false, orderKind: "sale" },
3043
4054
  order: { createdAt: "DESC" },
3044
4055
  take: 50
3045
4056
  });
@@ -3047,7 +4058,7 @@ function createStorefrontApiHandler(config) {
3047
4058
  const previewByOrder = {};
3048
4059
  if (orderIds.length) {
3049
4060
  const oItems = await orderItemRepo().find({
3050
- where: { orderId: (0, import_typeorm4.In)(orderIds) },
4061
+ where: { orderId: (0, import_typeorm5.In)(orderIds) },
3051
4062
  relations: ["product"],
3052
4063
  order: { id: "ASC" }
3053
4064
  });
@@ -3074,6 +4085,20 @@ function createStorefrontApiHandler(config) {
3074
4085
  })
3075
4086
  });
3076
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
+ }
3077
4102
  if (path[0] === "orders" && path.length === 2 && method === "GET") {
3078
4103
  const u = await getSessionUser();
3079
4104
  const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
@@ -3081,11 +4106,20 @@ function createStorefrontApiHandler(config) {
3081
4106
  const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
3082
4107
  if (!contact) return json({ error: "Not found" }, { status: 404 });
3083
4108
  const orderId = parseInt(path[1], 10);
3084
- const order = await orderRepo().findOne({
4109
+ let order = await orderRepo().findOne({
3085
4110
  where: { id: orderId, contactId: contact.id, deleted: false },
3086
4111
  relations: ["items", "items.product"]
3087
4112
  });
3088
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 });
3089
4123
  const o = order;
3090
4124
  const lines = (o.items || []).map((line) => {
3091
4125
  const p = line.product;
@@ -3104,10 +4138,22 @@ function createStorefrontApiHandler(config) {
3104
4138
  } : null
3105
4139
  };
3106
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 ?? "") : "";
3107
4151
  return json({
3108
4152
  order: {
3109
4153
  id: o.id,
3110
4154
  orderNumber: o.orderNumber,
4155
+ orderKind: kind,
4156
+ parentOrderId: o.parentOrderId ?? null,
3111
4157
  status: o.status,
3112
4158
  subtotal: o.subtotal,
3113
4159
  tax: o.tax,
@@ -3115,8 +4161,18 @@ function createStorefrontApiHandler(config) {
3115
4161
  total: o.total,
3116
4162
  currency: o.currency,
3117
4163
  createdAt: o.createdAt,
4164
+ metadata: o.metadata ?? null,
3118
4165
  items: lines
3119
- }
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
3120
4176
  });
3121
4177
  }
3122
4178
  return json({ error: "Not found" }, { status: 404 });