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