@infuro/cms-core 1.0.23 → 1.0.25

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
@@ -17,16 +17,51 @@ var __decorateClass = (decorators, target, key, kind) => {
17
17
  return result;
18
18
  };
19
19
 
20
+ // src/plugins/erp/erp-log.ts
21
+ function logErp(event, detail) {
22
+ if (detail && Object.keys(detail).length) console.info(ERP_LOG, event, detail);
23
+ else console.info(ERP_LOG, event);
24
+ }
25
+ function warnErp(event, detail) {
26
+ console.warn(ERP_LOG, event, detail);
27
+ }
28
+ function errorErp(event, detail) {
29
+ console.error(ERP_LOG, event, detail);
30
+ }
31
+ var ERP_LOG;
32
+ var init_erp_log = __esm({
33
+ "src/plugins/erp/erp-log.ts"() {
34
+ "use strict";
35
+ ERP_LOG = "[webcore:erp]";
36
+ }
37
+ });
38
+
20
39
  // src/plugins/erp/erp-queue.ts
40
+ function queuePayloadSummary(payload) {
41
+ if (payload.kind === "order") {
42
+ const o = payload.order;
43
+ return {
44
+ kind: payload.kind,
45
+ platformOrderId: o.platformOrderId ?? o.platformOrderNumber,
46
+ itemCount: Array.isArray(o.items) ? o.items.length : 0
47
+ };
48
+ }
49
+ return { kind: payload.kind };
50
+ }
21
51
  async function queueErp(cms, payload) {
22
52
  const queue = cms.getPlugin("queue");
23
- if (!queue) return;
53
+ if (!queue) {
54
+ warnErp("queue:add_skipped", { reason: "queue_plugin_missing", ...queuePayloadSummary(payload) });
55
+ return;
56
+ }
57
+ logErp("queue:add", { job: ERP_QUEUE_NAME, ...queuePayloadSummary(payload) });
24
58
  await queue.add(ERP_QUEUE_NAME, payload);
25
59
  }
26
60
  var ERP_QUEUE_NAME;
27
61
  var init_erp_queue = __esm({
28
62
  "src/plugins/erp/erp-queue.ts"() {
29
63
  "use strict";
64
+ init_erp_log();
30
65
  ERP_QUEUE_NAME = "erp";
31
66
  }
32
67
  });
@@ -339,21 +374,36 @@ async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId)
339
374
  const cfgRows = await configRepo.find({ where: { settings: "erp", deleted: false } });
340
375
  for (const row of cfgRows) {
341
376
  const r = row;
342
- if (r.key === "enabled" && r.value === "false") return;
377
+ if (r.key === "enabled" && r.value === "false") {
378
+ logErp("paid-order:skip", { orderId, reason: "erp_config_disabled" });
379
+ return;
380
+ }
381
+ }
382
+ if (!cms.getPlugin("erp")) {
383
+ logErp("paid-order:skip", { orderId, reason: "erp_plugin_missing" });
384
+ return;
343
385
  }
344
- if (!cms.getPlugin("erp")) return;
345
386
  const orderRepo = dataSource.getRepository(entityMap.orders);
346
387
  const ord = await orderRepo.findOne({
347
388
  where: { id: orderId },
348
389
  relations: ["items", "items.product", "contact", "billingAddress", "shippingAddress", "payments"]
349
390
  });
350
- if (!ord) return;
391
+ if (!ord) {
392
+ logErp("paid-order:skip", { orderId, reason: "order_not_found" });
393
+ return;
394
+ }
351
395
  const o = ord;
352
396
  const okKind = o.orderKind === void 0 || o.orderKind === null || o.orderKind === "sale";
353
- if (!okKind) return;
397
+ if (!okKind) {
398
+ logErp("paid-order:skip", { orderId, reason: "order_kind_not_sale", orderKind: o.orderKind });
399
+ return;
400
+ }
354
401
  const rawPayments = o.payments ?? [];
355
402
  const completedPayments = rawPayments.filter((pay) => pay.status === "completed" && pay.deleted !== true);
356
- if (!completedPayments.length) return;
403
+ if (!completedPayments.length) {
404
+ logErp("paid-order:skip", { orderId, reason: "no_completed_payments" });
405
+ return;
406
+ }
357
407
  const rawItems = o.items ?? [];
358
408
  const lines = rawItems.filter((it) => it.product).map((it) => {
359
409
  const p = it.product;
@@ -372,7 +422,10 @@ async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId)
372
422
  type: itemType
373
423
  };
374
424
  });
375
- if (!lines.length) return;
425
+ if (!lines.length) {
426
+ logErp("paid-order:skip", { orderId, reason: "no_line_items_with_product" });
427
+ return;
428
+ }
376
429
  const contact = o.contact;
377
430
  const orderTotalMajor = Number(o.total);
378
431
  const paymentDtos = completedPayments.length === 1 && Number.isFinite(orderTotalMajor) ? [paymentRowToWebhookDto(completedPayments[0], orderTotalMajor)] : completedPayments.map((pay) => paymentRowToWebhookDto(pay));
@@ -394,19 +447,34 @@ async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId)
394
447
  payments: paymentDtos,
395
448
  metadata: { ...baseMeta, source: "storefront" }
396
449
  };
450
+ logErp("paid-order:payload_built", {
451
+ orderId,
452
+ platformOrderId: orderDto.platformOrderId,
453
+ status: orderDto.status,
454
+ itemCount: lines.length,
455
+ skus: lines.map((l) => l.sku),
456
+ paymentCount: paymentDtos.length,
457
+ paymentIds: paymentDtos.map((p) => p.id),
458
+ total: orderTotalMajor
459
+ });
397
460
  await queueErp(cms, { kind: "order", order: orderDto });
398
- } catch {
461
+ } catch (e) {
462
+ errorErp("paid-order:enqueue_failed", {
463
+ orderId,
464
+ message: e instanceof Error ? e.message : String(e)
465
+ });
399
466
  }
400
467
  }
401
468
  var init_paid_order_erp = __esm({
402
469
  "src/plugins/erp/paid-order-erp.ts"() {
403
470
  "use strict";
471
+ init_erp_log();
404
472
  init_erp_queue();
405
473
  }
406
474
  });
407
475
 
408
476
  // src/api/crud.ts
409
- import { ILike, MoreThan, Not } from "typeorm";
477
+ import { Between, ILike, LessThanOrEqual, MoreThan, MoreThanOrEqual, Not } from "typeorm";
410
478
 
411
479
  // src/plugins/erp/erp-contact-sync.ts
412
480
  init_erp_queue();
@@ -476,6 +544,64 @@ async function queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, produc
476
544
  }
477
545
  }
478
546
 
547
+ // src/lib/address-geo-validation.ts
548
+ import { Country, State, City } from "country-state-city";
549
+ function norm(s) {
550
+ return typeof s === "string" ? s.trim() : "";
551
+ }
552
+ function resolveCountry(input) {
553
+ const t = input.trim();
554
+ if (!t) return void 0;
555
+ if (t.length === 2) {
556
+ const byCode = Country.getCountryByCode(t.toUpperCase());
557
+ if (byCode) return byCode;
558
+ }
559
+ const lower = t.toLowerCase();
560
+ return Country.getAllCountries().find((c) => c.name.toLowerCase() === lower);
561
+ }
562
+ function resolveState(countryIso, input) {
563
+ const t = input.trim();
564
+ if (!t || !countryIso) return void 0;
565
+ const states = State.getStatesOfCountry(countryIso);
566
+ const lower = t.toLowerCase();
567
+ return states.find((s) => s.isoCode.toLowerCase() === t.toLowerCase() || s.name.toLowerCase() === lower);
568
+ }
569
+ function resolveCity(countryIso, stateIso, input) {
570
+ const t = input.trim();
571
+ if (!t || !countryIso || !stateIso) return void 0;
572
+ const lower = t.toLowerCase();
573
+ const cities = City.getCitiesOfState(countryIso, stateIso);
574
+ return cities.find((c) => c.name.toLowerCase() === lower);
575
+ }
576
+ function assertValidAddressHierarchy(country, state, city) {
577
+ const c = resolveCountry(country);
578
+ if (!c) return { ok: false, error: "Invalid or unknown country." };
579
+ const st = resolveState(c.isoCode, state);
580
+ if (!st) return { ok: false, error: "State or province does not match the selected country." };
581
+ const ct = resolveCity(c.isoCode, st.isoCode, city);
582
+ if (!ct) return { ok: false, error: "City does not match the selected state." };
583
+ return { ok: true, country: c.name, state: st.name, city: ct.name };
584
+ }
585
+ function validateAndNormalizeAddressRow(row) {
586
+ const line1 = norm(row.line1);
587
+ const postalCode = norm(row.postalCode);
588
+ const countryIn = norm(row.country);
589
+ const stateIn = norm(row.state);
590
+ const cityIn = norm(row.city);
591
+ if (!line1) return "Street address (line 1) is required.";
592
+ if (!postalCode) return "Postal code is required.";
593
+ if (!countryIn || !stateIn || !cityIn) return "Country, state, and city are required.";
594
+ const geo = assertValidAddressHierarchy(countryIn, stateIn, cityIn);
595
+ if (!geo.ok) return geo.error;
596
+ row.line1 = line1;
597
+ row.line2 = norm(row.line2) || null;
598
+ row.postalCode = postalCode;
599
+ row.country = geo.country;
600
+ row.state = geo.state;
601
+ row.city = geo.city;
602
+ return null;
603
+ }
604
+
479
605
  // src/api/crud.ts
480
606
  var CRUD_LOG = "[cms-crud]";
481
607
  function logCrudClientError(op, detail) {
@@ -573,6 +699,156 @@ function buildSearchWhereClause(repo, search) {
573
699
  function entityHasSoftDelete(repo) {
574
700
  return repo.metadata.columns.some((c) => c.propertyName === "deleted");
575
701
  }
702
+ var LIST_QUERY_RESERVED = /* @__PURE__ */ new Set(["page", "limit", "sortField", "sortOrder", "search"]);
703
+ function dayStartUtc(isoDay) {
704
+ return /* @__PURE__ */ new Date(isoDay + "T00:00:00.000Z");
705
+ }
706
+ function dayEndUtc(isoDay) {
707
+ return /* @__PURE__ */ new Date(isoDay + "T23:59:59.999Z");
708
+ }
709
+ function columnTypeLabel(col) {
710
+ const t = col.type;
711
+ if (typeof t === "string") return t.toLowerCase();
712
+ if (typeof t === "function") return t.name?.toLowerCase?.() ?? "";
713
+ if (t && typeof t === "object" && "name" in t && typeof t.name === "string") {
714
+ return String(t.name).toLowerCase();
715
+ }
716
+ return "";
717
+ }
718
+ function isListDateColumn(col) {
719
+ const tl = columnTypeLabel(col);
720
+ return DATE_COLUMN_TYPES.has(tl) || col.type === Date || TIMESTAMP_PROP_NAMES.has(col.propertyName);
721
+ }
722
+ function isListNumericColumn(col) {
723
+ const tl = columnTypeLabel(col);
724
+ if (col.type === Number) return true;
725
+ const patterns = [
726
+ "int",
727
+ "integer",
728
+ "int2",
729
+ "int4",
730
+ "int8",
731
+ "smallint",
732
+ "bigint",
733
+ "float",
734
+ "double",
735
+ "decimal",
736
+ "numeric",
737
+ "real",
738
+ "tinyint",
739
+ "mediumint"
740
+ ];
741
+ if (patterns.some((x) => tl.includes(x))) return true;
742
+ if (tl.includes("unsigned") && (tl.includes("int") || tl.includes("bigint") || tl.includes("small"))) return true;
743
+ if (!tl && /Id$/i.test(col.propertyName) && !TIMESTAMP_PROP_NAMES.has(col.propertyName)) return true;
744
+ return false;
745
+ }
746
+ function isListBooleanColumn(col) {
747
+ const tl = columnTypeLabel(col);
748
+ return tl === "boolean" || tl === "bool" || col.type === Boolean;
749
+ }
750
+ function isListStringColumn(col) {
751
+ const tl = columnTypeLabel(col);
752
+ if (isListDateColumn(col) || isListNumericColumn(col) || isListBooleanColumn(col)) return false;
753
+ if (["varchar", "character varying", "text", "citext", "uuid", "char", "character", "enum"].some(
754
+ (x) => tl.includes(x)
755
+ )) {
756
+ return true;
757
+ }
758
+ if (col.type === String) return true;
759
+ return false;
760
+ }
761
+ function mergeListWhereAnd(where, patch) {
762
+ if (Object.keys(patch).length === 0) return where;
763
+ if (Array.isArray(where)) {
764
+ if (where.length === 0) return [patch];
765
+ return where.map((w) => ({ ...w, ...patch }));
766
+ }
767
+ if (where && typeof where === "object" && Object.keys(where).length > 0) {
768
+ return { ...where, ...patch };
769
+ }
770
+ return patch;
771
+ }
772
+ function buildListFilterAndFromSearchParams(repo, searchParams) {
773
+ const and = {};
774
+ const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
775
+ for (const col of repo.metadata.columns) {
776
+ const name = col.propertyName;
777
+ if (!columnNames.has(name)) continue;
778
+ if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
779
+ if (!isListDateColumn(col)) continue;
780
+ const from = searchParams.get(`${name}From`)?.trim();
781
+ const to = searchParams.get(`${name}To`)?.trim();
782
+ if (!from && !to) continue;
783
+ if (from && to) {
784
+ and[name] = Between(dayStartUtc(from), dayEndUtc(to));
785
+ } else if (from) {
786
+ and[name] = MoreThanOrEqual(dayStartUtc(from));
787
+ } else if (to) {
788
+ and[name] = LessThanOrEqual(dayEndUtc(to));
789
+ }
790
+ }
791
+ for (const col of repo.metadata.columns) {
792
+ const name = col.propertyName;
793
+ if (!columnNames.has(name)) continue;
794
+ if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
795
+ if (!isListNumericColumn(col)) continue;
796
+ if (Object.prototype.hasOwnProperty.call(and, name)) continue;
797
+ const minRaw = searchParams.get(`${name}Min`)?.trim();
798
+ const maxRaw = searchParams.get(`${name}Max`)?.trim();
799
+ if (!minRaw && !maxRaw) continue;
800
+ const parseNum = (s) => {
801
+ const n = Number(s);
802
+ return Number.isFinite(n) ? n : null;
803
+ };
804
+ const nMin = minRaw ? parseNum(minRaw) : null;
805
+ const nMax = maxRaw ? parseNum(maxRaw) : null;
806
+ if (nMin != null && nMax != null) {
807
+ and[name] = Between(nMin, nMax);
808
+ } else if (nMin != null) {
809
+ and[name] = MoreThanOrEqual(nMin);
810
+ } else if (nMax != null) {
811
+ and[name] = LessThanOrEqual(nMax);
812
+ }
813
+ }
814
+ for (const col of repo.metadata.columns) {
815
+ const name = col.propertyName;
816
+ if (!columnNames.has(name)) continue;
817
+ if (LIST_QUERY_RESERVED.has(name)) continue;
818
+ if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
819
+ if (!isListStringColumn(col)) continue;
820
+ if (Object.prototype.hasOwnProperty.call(and, name)) continue;
821
+ const raw = searchParams.get(name)?.trim();
822
+ if (!raw) continue;
823
+ and[name] = ILike(`%${raw}%`);
824
+ }
825
+ return and;
826
+ }
827
+ function buildExactListParamWhere(repo, searchParams) {
828
+ const extraWhere = {};
829
+ const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
830
+ for (const col of repo.metadata.columns) {
831
+ const name = col.propertyName;
832
+ if (!columnNames.has(name)) continue;
833
+ if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
834
+ if (!isListNumericColumn(col)) continue;
835
+ const v = searchParams.get(name)?.trim();
836
+ if (v == null || v === "") continue;
837
+ const n = Number(v);
838
+ if (!Number.isFinite(n)) continue;
839
+ extraWhere[name] = n;
840
+ }
841
+ for (const col of repo.metadata.columns) {
842
+ if (String(col.type) !== "boolean") continue;
843
+ const name = col.propertyName;
844
+ if (!columnNames.has(name)) continue;
845
+ const raw = searchParams.get(name)?.trim();
846
+ if (raw === "true" || raw === "false") {
847
+ extraWhere[name] = raw === "true";
848
+ }
849
+ }
850
+ return extraWhere;
851
+ }
576
852
  function mergeDeletedFalseWhere(repo, where) {
577
853
  if (!entityHasSoftDelete(repo)) return where;
578
854
  const d = { deleted: false };
@@ -582,6 +858,12 @@ function mergeDeletedFalseWhere(repo, where) {
582
858
  }
583
859
  return Object.keys(where).length > 0 ? { ...where, ...d } : d;
584
860
  }
861
+ function pickDefaultListSortField(columnNames, columns) {
862
+ for (const candidate of ["createdAt", "updatedAt", "id", "name", "sortOrder", "title"]) {
863
+ if (columnNames.has(candidate)) return candidate;
864
+ }
865
+ return columns[0]?.propertyName ?? "id";
866
+ }
585
867
  function normalizeProductSku(value) {
586
868
  if (value == null) return null;
587
869
  const s = String(value).trim();
@@ -684,6 +966,18 @@ function createCrudHandler(dataSource, entityMap, options) {
684
966
  if (statusFilter) qb.andWhere("order.status = :status", { status: statusFilter });
685
967
  if (dateFrom) qb.andWhere("order.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
686
968
  if (dateTo) qb.andWhere("order.createdAt <= :dateTo", { dateTo: /* @__PURE__ */ new Date(dateTo + "T23:59:59.999Z") });
969
+ const totalMin = searchParams.get("totalMin")?.trim();
970
+ const totalMax = searchParams.get("totalMax")?.trim();
971
+ if (totalMin) {
972
+ const n = Number(totalMin);
973
+ if (Number.isFinite(n)) qb.andWhere("order.total >= :totalMin", { totalMin: n });
974
+ }
975
+ if (totalMax) {
976
+ const n = Number(totalMax);
977
+ if (Number.isFinite(n)) qb.andWhere("order.total <= :totalMax", { totalMax: n });
978
+ }
979
+ const currency = searchParams.get("currency")?.trim();
980
+ if (currency) qb.andWhere("order.currency ILIKE :orderCurrency", { orderCurrency: `%${currency}%` });
687
981
  if (orderIdsFromPayment && orderIdsFromPayment.length) qb.andWhere("order.id IN (:...orderIds)", { orderIds: orderIdsFromPayment });
688
982
  const [rows, total2] = await qb.getManyAndCount();
689
983
  const data2 = rows.map((order) => {
@@ -722,8 +1016,30 @@ function createCrudHandler(dataSource, entityMap, options) {
722
1016
  if (statusFilter) qb.andWhere("payment.status = :status", { status: statusFilter });
723
1017
  if (dateFrom) qb.andWhere("payment.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
724
1018
  if (dateTo) qb.andWhere("payment.createdAt <= :dateTo", { dateTo: /* @__PURE__ */ new Date(dateTo + "T23:59:59.999Z") });
1019
+ const paidAtFrom = searchParams.get("paidAtFrom")?.trim();
1020
+ const paidAtTo = searchParams.get("paidAtTo")?.trim();
1021
+ if (paidAtFrom) {
1022
+ qb.andWhere("payment.paidAt >= :paidAtFrom", { paidAtFrom: /* @__PURE__ */ new Date(paidAtFrom + "T00:00:00.000Z") });
1023
+ }
1024
+ if (paidAtTo) {
1025
+ qb.andWhere("payment.paidAt <= :paidAtTo", { paidAtTo: /* @__PURE__ */ new Date(paidAtTo + "T23:59:59.999Z") });
1026
+ }
725
1027
  if (methodFilter) qb.andWhere("payment.method = :method", { method: methodFilter });
726
1028
  if (orderNumberParam) qb.andWhere("ord.orderNumber ILIKE :orderNumber", { orderNumber: `%${orderNumberParam}%` });
1029
+ const amountMin = searchParams.get("amountMin")?.trim();
1030
+ const amountMax = searchParams.get("amountMax")?.trim();
1031
+ if (amountMin) {
1032
+ const n = Number(amountMin);
1033
+ if (Number.isFinite(n)) qb.andWhere("payment.amount >= :amountMin", { amountMin: n });
1034
+ }
1035
+ if (amountMax) {
1036
+ const n = Number(amountMax);
1037
+ if (Number.isFinite(n)) qb.andWhere("payment.amount <= :amountMax", { amountMax: n });
1038
+ }
1039
+ const extRef = searchParams.get("externalReference")?.trim();
1040
+ if (extRef) {
1041
+ qb.andWhere("payment.externalReference ILIKE :extRef", { extRef: `%${extRef}%` });
1042
+ }
727
1043
  const [rows, total2] = await qb.getManyAndCount();
728
1044
  const data2 = rows.map((payment) => {
729
1045
  const order = payment.order;
@@ -742,18 +1058,36 @@ function createCrudHandler(dataSource, entityMap, options) {
742
1058
  const repo2 = dataSource.getRepository(entity);
743
1059
  const statusFilter = searchParams.get("status")?.trim();
744
1060
  const inventory = searchParams.get("inventory")?.trim();
745
- const productWhere = { deleted: false };
1061
+ const productWhere = {
1062
+ deleted: false,
1063
+ ...buildListFilterAndFromSearchParams(repo2, searchParams)
1064
+ };
746
1065
  if (statusFilter) productWhere.status = statusFilter;
747
1066
  if (inventory === "in_stock") productWhere.quantity = MoreThan(0);
748
1067
  if (inventory === "out_of_stock") productWhere.quantity = 0;
1068
+ for (const key of ["brandId", "categoryId", "collectionId"]) {
1069
+ const raw = searchParams.get(key)?.trim();
1070
+ if (raw) {
1071
+ const n = Number(raw);
1072
+ if (Number.isFinite(n)) productWhere[key] = n;
1073
+ }
1074
+ }
1075
+ const featuredRaw = searchParams.get("featured")?.trim();
1076
+ if (featuredRaw === "true" || featuredRaw === "false") {
1077
+ productWhere.featured = featuredRaw === "true";
1078
+ }
749
1079
  if (search && typeof search === "string" && search.trim()) {
750
1080
  productWhere.name = ILike(`%${search.trim()}%`);
751
1081
  }
1082
+ const productColumnNames = new Set(repo2.metadata.columns.map((c) => c.propertyName));
1083
+ const defaultProductSort = pickDefaultListSortField(productColumnNames, repo2.metadata.columns);
1084
+ const sortParam2 = (searchParams.get("sortField") ?? "").trim();
1085
+ const productSortField = sortParam2 && productColumnNames.has(sortParam2) ? sortParam2 : defaultProductSort;
752
1086
  const [data2, total2] = await repo2.findAndCount({
753
1087
  where: Object.keys(productWhere).length ? productWhere : void 0,
754
1088
  skip,
755
1089
  take: limit,
756
- order: { [sortFieldRaw]: sortOrder }
1090
+ order: { [productSortField]: sortOrder }
757
1091
  });
758
1092
  return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
759
1093
  }
@@ -846,27 +1180,22 @@ function createCrudHandler(dataSource, entityMap, options) {
846
1180
  }
847
1181
  return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
848
1182
  }
849
- const sortField = columnNames.has(sortFieldRaw) ? sortFieldRaw : "createdAt";
1183
+ const defaultSortField = pickDefaultListSortField(columnNames, repo.metadata.columns);
1184
+ const sortParam = (searchParams.get("sortField") ?? "").trim();
1185
+ const sortField = sortParam && columnNames.has(sortParam) ? sortParam : defaultSortField;
850
1186
  let where = {};
851
1187
  if (search) {
852
1188
  where = buildSearchWhereClause(repo, search);
853
1189
  }
854
- const intFilterKeys = ["productId", "attributeId", "taxId"];
855
- const extraWhere = {};
856
- for (const key of intFilterKeys) {
857
- const v = searchParams.get(key);
858
- if (v != null && v !== "" && columnNames.has(key)) {
859
- const n = Number(v);
860
- if (Number.isFinite(n)) extraWhere[key] = n;
861
- }
862
- }
863
- if (Object.keys(extraWhere).length > 0) {
1190
+ where = mergeListWhereAnd(where, buildListFilterAndFromSearchParams(repo, searchParams));
1191
+ const exactParamWhere = buildExactListParamWhere(repo, searchParams);
1192
+ if (Object.keys(exactParamWhere).length > 0) {
864
1193
  if (Array.isArray(where)) {
865
- where = where.map((w) => ({ ...w, ...extraWhere }));
1194
+ where = where.map((w) => ({ ...w, ...exactParamWhere }));
866
1195
  } else if (where && typeof where === "object" && Object.keys(where).length > 0) {
867
- where = { ...where, ...extraWhere };
1196
+ where = { ...where, ...exactParamWhere };
868
1197
  } else {
869
- where = extraWhere;
1198
+ where = exactParamWhere;
870
1199
  }
871
1200
  }
872
1201
  where = mergeDeletedFalseWhere(repo, where);
@@ -946,6 +1275,10 @@ function createCrudHandler(dataSource, entityMap, options) {
946
1275
  }
947
1276
  const repo = dataSource.getRepository(entity);
948
1277
  const persistBody = resource === "media" ? body : pickColumnUpdates(repo, body);
1278
+ if (resource === "contacts" && "type" in persistBody) {
1279
+ const t = persistBody.type;
1280
+ if (t === "" || t === "none" || t == null) persistBody.type = null;
1281
+ }
949
1282
  if (resource !== "media" && Object.keys(persistBody).length === 0) {
950
1283
  logCrudClientError("POST create", {
951
1284
  reason: "no_scalar_columns_after_pick",
@@ -968,6 +1301,17 @@ function createCrudHandler(dataSource, entityMap, options) {
968
1301
  }
969
1302
  }
970
1303
  }
1304
+ if (resource === "addresses") {
1305
+ const cid = Number(persistBody.contactId);
1306
+ if (!Number.isFinite(cid)) {
1307
+ return json({ error: "Valid contactId is required." }, { status: 400 });
1308
+ }
1309
+ if (persistBody.tag === "") persistBody.tag = null;
1310
+ const addrErr = validateAndNormalizeAddressRow(persistBody);
1311
+ if (addrErr) {
1312
+ return json({ error: addrErr }, { status: 400 });
1313
+ }
1314
+ }
971
1315
  sanitizeBodyForEntity(repo, persistBody);
972
1316
  let created;
973
1317
  try {
@@ -1285,10 +1629,60 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
1285
1629
  if (!cur) return json({ message: "Not found" }, { status: 404 });
1286
1630
  }
1287
1631
  const updatePayload = rawBody && typeof rawBody === "object" ? pickColumnUpdates(repo, rawBody) : {};
1632
+ if (resource === "contacts" && "type" in updatePayload) {
1633
+ const t = updatePayload.type;
1634
+ if (t === "" || t === "none" || t == null) updatePayload.type = null;
1635
+ }
1288
1636
  if (resource === "media") {
1289
1637
  const u = updatePayload;
1290
- delete u.parentId;
1291
1638
  delete u.kind;
1639
+ if (rawBody && typeof rawBody === "object" && "parentId" in rawBody) {
1640
+ let pid = null;
1641
+ const p = rawBody.parentId;
1642
+ if (p != null && p !== "") {
1643
+ const n = Number(p);
1644
+ if (!Number.isFinite(n)) {
1645
+ return json({ error: "Invalid parentId" }, { status: 400 });
1646
+ }
1647
+ pid = n;
1648
+ }
1649
+ if (pid != null) {
1650
+ const parent = await repo.findOne({
1651
+ where: { id: pid, deleted: false }
1652
+ });
1653
+ if (!parent || parent.kind !== "folder") {
1654
+ return json({ error: "parent must be a folder" }, { status: 400 });
1655
+ }
1656
+ }
1657
+ const row = await repo.findOne({
1658
+ where: { id: numericId, deleted: false }
1659
+ });
1660
+ if (!row) return json({ message: "Not found" }, { status: 404 });
1661
+ if (pid === numericId) {
1662
+ return json({ error: "Invalid parentId" }, { status: 400 });
1663
+ }
1664
+ if (row.kind === "folder" && pid != null) {
1665
+ let walk = pid;
1666
+ const seen = /* @__PURE__ */ new Set();
1667
+ while (walk != null) {
1668
+ if (walk === numericId) {
1669
+ return json(
1670
+ { error: "Cannot move a folder into itself or a descendant folder" },
1671
+ { status: 400 }
1672
+ );
1673
+ }
1674
+ if (seen.has(walk)) break;
1675
+ seen.add(walk);
1676
+ const anc = await repo.findOne({
1677
+ where: { id: walk, deleted: false }
1678
+ });
1679
+ walk = anc ? anc.parentId ?? null : null;
1680
+ }
1681
+ }
1682
+ u.parentId = pid;
1683
+ } else {
1684
+ delete u.parentId;
1685
+ }
1292
1686
  }
1293
1687
  if (resource === "products") {
1294
1688
  const currentRow = await repo.findOne({
@@ -1307,6 +1701,26 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
1307
1701
  updatePayload.sku = effSku;
1308
1702
  }
1309
1703
  }
1704
+ if (resource === "addresses" && Object.keys(updatePayload).length > 0) {
1705
+ const currentRow = await repo.findOne({
1706
+ where: { id: numericId }
1707
+ });
1708
+ if (!currentRow) return json({ message: "Not found" }, { status: 404 });
1709
+ const merged = {
1710
+ ...currentRow,
1711
+ ...updatePayload
1712
+ };
1713
+ if (merged.tag === "") merged.tag = null;
1714
+ const addrErr = validateAndNormalizeAddressRow(merged);
1715
+ if (addrErr) {
1716
+ return json({ error: addrErr }, { status: 400 });
1717
+ }
1718
+ for (const k of Object.keys(updatePayload)) {
1719
+ if (k in merged) {
1720
+ updatePayload[k] = merged[k];
1721
+ }
1722
+ }
1723
+ }
1310
1724
  if (Object.keys(updatePayload).length > 0) {
1311
1725
  sanitizeBodyForEntity(repo, updatePayload);
1312
1726
  await repo.update(numericId, updatePayload);
@@ -1508,7 +1922,7 @@ function createUserAuthApiRouter(config) {
1508
1922
  // src/api/cms-handlers.ts
1509
1923
  init_email_queue();
1510
1924
  init_erp_queue();
1511
- import { MoreThanOrEqual, ILike as ILike2, In } from "typeorm";
1925
+ import { MoreThanOrEqual as MoreThanOrEqual2, ILike as ILike2, In } from "typeorm";
1512
1926
 
1513
1927
  // src/plugins/captcha/assert.ts
1514
1928
  async function assertCaptchaOk(getCms, body, req, json) {
@@ -1660,11 +2074,11 @@ async function readBufferFromPublicUrl(url) {
1660
2074
  throw new Error("Unsupported media URL");
1661
2075
  }
1662
2076
  function sanitizeZipPath(entryName) {
1663
- const norm = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
1664
- for (const seg of norm) {
2077
+ const norm2 = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
2078
+ for (const seg of norm2) {
1665
2079
  if (seg === ".." || seg === ".") return null;
1666
2080
  }
1667
- return norm;
2081
+ return norm2;
1668
2082
  }
1669
2083
  function shouldSkipEntry(parts) {
1670
2084
  if (parts[0] === "__MACOSX") return true;
@@ -1894,9 +2308,9 @@ function createDashboardStatsHandler(config) {
1894
2308
  repo("users")?.count({ where: { deleted: false } }) ?? 0,
1895
2309
  repo("blogs")?.count({ where: { deleted: false } }) ?? 0,
1896
2310
  repo("contacts")?.count({
1897
- where: { deleted: false, createdAt: MoreThanOrEqual(sevenDaysAgo) }
2311
+ where: { deleted: false, createdAt: MoreThanOrEqual2(sevenDaysAgo) }
1898
2312
  }) ?? 0,
1899
- repo("form_submissions")?.count({ where: { createdAt: MoreThanOrEqual(sevenDaysAgo) } }) ?? 0,
2313
+ repo("form_submissions")?.count({ where: { createdAt: MoreThanOrEqual2(sevenDaysAgo) } }) ?? 0,
1900
2314
  repo("contacts")?.createQueryBuilder("c").select("COALESCE(NULLIF(TRIM(c.type), ''), 'unknown')", "type").addSelect("COUNT(*)", "count").where("c.deleted = :deleted", { deleted: false }).groupBy("COALESCE(NULLIF(TRIM(c.type), ''), 'unknown')").getRawMany() ?? []
1901
2315
  ]);
1902
2316
  return json({
@@ -1945,19 +2359,19 @@ function createEcommerceAnalyticsHandler(config) {
1945
2359
  const productRepo = dataSource.getRepository(entityMap.products);
1946
2360
  const [salesOrders, returnOrders, replacementOrders, payments, products] = await Promise.all([
1947
2361
  orderRepo.find({
1948
- where: { deleted: false, createdAt: MoreThanOrEqual(start), orderKind: "sale", status: In(["confirmed", "processing", "completed"]) },
2362
+ where: { deleted: false, createdAt: MoreThanOrEqual2(start), orderKind: "sale", status: In(["confirmed", "processing", "completed"]) },
1949
2363
  select: ["id", "contactId", "createdAt", "subtotal", "discount", "tax", "total", "status"]
1950
2364
  }),
1951
2365
  orderRepo.find({
1952
- where: { deleted: false, createdAt: MoreThanOrEqual(start), orderKind: "return" },
2366
+ where: { deleted: false, createdAt: MoreThanOrEqual2(start), orderKind: "return" },
1953
2367
  select: ["id", "createdAt", "total"]
1954
2368
  }),
1955
2369
  orderRepo.find({
1956
- where: { deleted: false, createdAt: MoreThanOrEqual(start), orderKind: "replacement" },
2370
+ where: { deleted: false, createdAt: MoreThanOrEqual2(start), orderKind: "replacement" },
1957
2371
  select: ["id", "createdAt", "total"]
1958
2372
  }),
1959
2373
  paymentRepo.find({
1960
- where: { deleted: false, createdAt: MoreThanOrEqual(start) },
2374
+ where: { deleted: false, createdAt: MoreThanOrEqual2(start) },
1961
2375
  select: ["id", "status", "method", "amount", "createdAt"]
1962
2376
  }),
1963
2377
  productRepo.find({
@@ -2684,18 +3098,37 @@ function createUsersApiHandlers(config) {
2684
3098
  try {
2685
3099
  const body = await req.json();
2686
3100
  if (!body?.name || !body?.email) return json({ error: "Name and email are required" }, { status: 400 });
2687
- const existing = await userRepo().findOne({ where: { email: body.email } });
2688
- if (existing) return json({ error: "User with this email already exists" }, { status: 400 });
3101
+ const email = body.email;
3102
+ const existing = await userRepo().findOne({ where: { email } });
3103
+ if (existing && !existing.deleted) {
3104
+ return json({ error: "User with this email already exists" }, { status: 400 });
3105
+ }
2689
3106
  const groupRepo = dataSource.getRepository(entityMap.user_groups);
2690
3107
  const customerG = await groupRepo.findOne({ where: { name: "Customer", deleted: false } });
2691
3108
  const gid = body.groupId ?? null;
2692
3109
  const isCustomer = !!(customerG && gid === customerG.id);
2693
3110
  const adminAccess = isCustomer ? false : body.adminAccess === false ? false : true;
2694
3111
  const blocked = body.blocked === true || body.blocked === "true" || body.blocked === 1 || body.blocked === "1";
2695
- const newUser = await userRepo().save(
3112
+ const newUser = existing?.deleted ? await (async () => {
3113
+ await userRepo().update(existing.id, {
3114
+ deleted: false,
3115
+ deletedAt: null,
3116
+ deletedBy: null,
3117
+ name: body.name,
3118
+ email,
3119
+ password: null,
3120
+ blocked,
3121
+ groupId: gid,
3122
+ adminAccess,
3123
+ updatedAt: /* @__PURE__ */ new Date()
3124
+ });
3125
+ const row = await userRepo().findOne({ where: { id: existing.id } });
3126
+ if (!row) throw new Error("user missing after restore");
3127
+ return row;
3128
+ })() : await userRepo().save(
2696
3129
  userRepo().create({
2697
3130
  name: body.name,
2698
- email: body.email,
3131
+ email,
2699
3132
  password: null,
2700
3133
  blocked,
2701
3134
  groupId: gid,
@@ -2848,21 +3281,110 @@ function createUserAvatarHandler(config) {
2848
3281
  }
2849
3282
  };
2850
3283
  }
3284
+ var PROFILE_EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2851
3285
  function createUserProfileHandler(config) {
2852
- const { dataSource, entityMap, json, getSession } = config;
2853
- return async function PUT(req) {
3286
+ const { dataSource, entityMap, json, getSession, onProfileUpdated } = config;
3287
+ async function loadCurrentUser() {
2854
3288
  const session = await getSession();
2855
- if (!session?.user?.email) return json({ error: "Unauthorized" }, { status: 401 });
2856
- try {
2857
- const body = await req.json();
2858
- if (!body?.name) return json({ error: "Name is required" }, { status: 400 });
2859
- const userRepo = dataSource.getRepository(entityMap.users);
2860
- await userRepo.update({ email: session.user.email }, { name: body.name, updatedAt: /* @__PURE__ */ new Date() });
2861
- const updated = await userRepo.findOne({ where: { email: session.user.email }, select: ["id", "name", "email"] });
2862
- if (!updated) return json({ error: "Not found" }, { status: 404 });
2863
- return json({ message: "Profile updated successfully", user: { id: updated.id, name: updated.name, email: updated.email } });
2864
- } catch {
2865
- return json({ error: "Internal server error" }, { status: 500 });
3289
+ const su = session?.user;
3290
+ if (!su?.email && su?.id == null) {
3291
+ return { ok: false, response: json({ error: "Unauthorized" }, { status: 401 }) };
3292
+ }
3293
+ const userRepo = dataSource.getRepository(entityMap.users);
3294
+ let user = null;
3295
+ const uidRaw = su.id != null ? String(su.id).trim() : "";
3296
+ const uid = uidRaw && /^\d+$/.test(uidRaw) ? parseInt(uidRaw, 10) : NaN;
3297
+ if (Number.isFinite(uid) && uid > 0) {
3298
+ user = await userRepo.findOne({
3299
+ where: { id: uid, deleted: false },
3300
+ select: ["id", "name", "email", "phone", "createdAt"]
3301
+ });
3302
+ }
3303
+ if (!user && su.email) {
3304
+ const em = String(su.email).trim().toLowerCase();
3305
+ if (em) {
3306
+ user = await userRepo.findOne({
3307
+ where: { email: em, deleted: false },
3308
+ select: ["id", "name", "email", "phone", "createdAt"]
3309
+ });
3310
+ }
3311
+ }
3312
+ if (!user) return { ok: false, response: json({ error: "Not found" }, { status: 404 }) };
3313
+ return { ok: true, user };
3314
+ }
3315
+ return {
3316
+ async GET(_req) {
3317
+ try {
3318
+ const r = await loadCurrentUser();
3319
+ if (!r.ok) return r.response;
3320
+ const u = r.user;
3321
+ return json({
3322
+ id: u.id,
3323
+ name: u.name ?? "",
3324
+ email: u.email ?? "",
3325
+ phone: u.phone ?? null,
3326
+ createdAt: u.createdAt instanceof Date ? u.createdAt.toISOString() : u.createdAt ?? void 0
3327
+ });
3328
+ } catch {
3329
+ return json({ error: "Internal server error" }, { status: 500 });
3330
+ }
3331
+ },
3332
+ async PUT(req) {
3333
+ try {
3334
+ const r = await loadCurrentUser();
3335
+ if (!r.ok) return r.response;
3336
+ const current = r.user;
3337
+ let body;
3338
+ try {
3339
+ body = await req.json();
3340
+ } catch {
3341
+ return json({ error: "Invalid JSON" }, { status: 400 });
3342
+ }
3343
+ const name = typeof body.name === "string" ? body.name.trim() : "";
3344
+ if (!name) return json({ error: "Name is required" }, { status: 400 });
3345
+ const emailRaw = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
3346
+ if (!emailRaw || !PROFILE_EMAIL_RE.test(emailRaw)) {
3347
+ return json({ error: "Valid email is required" }, { status: 400 });
3348
+ }
3349
+ const phone = body.phone === null || body.phone === void 0 ? null : typeof body.phone === "string" ? body.phone.trim() || null : null;
3350
+ const userRepo = dataSource.getRepository(entityMap.users);
3351
+ if (emailRaw !== String(current.email ?? "").toLowerCase()) {
3352
+ const taken = await userRepo.findOne({
3353
+ where: { email: emailRaw, deleted: false },
3354
+ select: ["id"]
3355
+ });
3356
+ if (taken && taken.id !== current.id) {
3357
+ return json({ error: "Email is already in use" }, { status: 409 });
3358
+ }
3359
+ }
3360
+ await userRepo.update(
3361
+ { id: current.id },
3362
+ {
3363
+ name,
3364
+ email: emailRaw,
3365
+ phone,
3366
+ updatedAt: /* @__PURE__ */ new Date()
3367
+ }
3368
+ );
3369
+ const updated = await userRepo.findOne({
3370
+ where: { id: current.id },
3371
+ select: ["id", "name", "email", "phone"]
3372
+ });
3373
+ if (!updated) return json({ error: "Not found" }, { status: 404 });
3374
+ const row = updated;
3375
+ if (onProfileUpdated) {
3376
+ try {
3377
+ await onProfileUpdated(req, row);
3378
+ } catch {
3379
+ }
3380
+ }
3381
+ return json({
3382
+ message: "Profile updated successfully",
3383
+ user: { id: row.id, name: row.name, email: row.email, phone: row.phone }
3384
+ });
3385
+ } catch {
3386
+ return json({ error: "Internal server error" }, { status: 500 });
3387
+ }
2866
3388
  }
2867
3389
  };
2868
3390
  }
@@ -6884,7 +7406,7 @@ function createCmsApiHandler(config) {
6884
7406
  } : usersApi;
6885
7407
  const usersHandlers = usersApiMerged ? createUsersApiHandlers(mergePerm(usersApiMerged) ?? usersApiMerged) : null;
6886
7408
  const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
6887
- const profilePut = userProfile ? createUserProfileHandler(userProfile) : null;
7409
+ const profileHandlers = userProfile ? createUserProfileHandler(userProfile) : null;
6888
7410
  const settingsHandlers = settingsConfig ? createSettingsApiHandlers(settingsConfig) : null;
6889
7411
  const smsMessageTemplateHandlers = createSmsMessageTemplateHandlers({
6890
7412
  dataSource,
@@ -6983,7 +7505,10 @@ function createCmsApiHandler(config) {
6983
7505
  }
6984
7506
  if (path.length === 2) {
6985
7507
  if (path[1] === "avatar" && m === "POST" && avatarPost) return avatarPost(req);
6986
- if (path[1] === "profile" && m === "PUT" && profilePut) return profilePut(req);
7508
+ if (path[1] === "profile" && profileHandlers) {
7509
+ if (m === "GET") return profileHandlers.GET(req);
7510
+ if (m === "PUT") return profileHandlers.PUT(req);
7511
+ }
6987
7512
  const id = path[1];
6988
7513
  if (m === "GET") return usersHandlers.getById(req, id);
6989
7514
  if (m === "PUT" || m === "PATCH") return usersHandlers.update(req, id);
@@ -8046,18 +8571,19 @@ function createStorefrontApiHandler(config) {
8046
8571
  const contactOrErr = await getContactForAddresses();
8047
8572
  if (contactOrErr instanceof Response) return contactOrErr;
8048
8573
  const b = await req.json().catch(() => ({}));
8049
- const created = await addressRepo().save(
8050
- addressRepo().create({
8051
- contactId: contactOrErr.contactId,
8052
- tag: typeof b.tag === "string" ? b.tag.trim() || null : null,
8053
- line1: typeof b.line1 === "string" ? b.line1.trim() || null : null,
8054
- line2: typeof b.line2 === "string" ? b.line2.trim() || null : null,
8055
- city: typeof b.city === "string" ? b.city.trim() || null : null,
8056
- state: typeof b.state === "string" ? b.state.trim() || null : null,
8057
- postalCode: typeof b.postalCode === "string" ? b.postalCode.trim() || null : null,
8058
- country: typeof b.country === "string" ? b.country.trim() || null : null
8059
- })
8060
- );
8574
+ const row = {
8575
+ contactId: contactOrErr.contactId,
8576
+ tag: typeof b.tag === "string" ? b.tag.trim() || null : null,
8577
+ line1: typeof b.line1 === "string" ? b.line1 : "",
8578
+ line2: typeof b.line2 === "string" ? b.line2.trim() || null : null,
8579
+ city: typeof b.city === "string" ? b.city : "",
8580
+ state: typeof b.state === "string" ? b.state : "",
8581
+ postalCode: typeof b.postalCode === "string" ? b.postalCode : "",
8582
+ country: typeof b.country === "string" ? b.country : ""
8583
+ };
8584
+ const addrErr = validateAndNormalizeAddressRow(row);
8585
+ if (addrErr) return json({ error: addrErr }, { status: 400 });
8586
+ const created = await addressRepo().save(addressRepo().create(row));
8061
8587
  return json(serializeAddress2(created));
8062
8588
  }
8063
8589
  if (path[0] === "addresses" && path.length === 2 && (method === "PATCH" || method === "PUT")) {
@@ -8076,7 +8602,16 @@ function createStorefrontApiHandler(config) {
8076
8602
  if (b.state !== void 0) updates.state = typeof b.state === "string" ? b.state.trim() || null : null;
8077
8603
  if (b.postalCode !== void 0) updates.postalCode = typeof b.postalCode === "string" ? b.postalCode.trim() || null : null;
8078
8604
  if (b.country !== void 0) updates.country = typeof b.country === "string" ? b.country.trim() || null : null;
8079
- if (Object.keys(updates).length) await addressRepo().update(id, updates);
8605
+ if (Object.keys(updates).length) {
8606
+ const merged = { ...existing, ...updates };
8607
+ if (merged.tag === "") merged.tag = null;
8608
+ const addrErr = validateAndNormalizeAddressRow(merged);
8609
+ if (addrErr) return json({ error: addrErr }, { status: 400 });
8610
+ for (const k of Object.keys(updates)) {
8611
+ if (k in merged) updates[k] = merged[k];
8612
+ }
8613
+ await addressRepo().update(id, updates);
8614
+ }
8080
8615
  const updated = await addressRepo().findOne({ where: { id } });
8081
8616
  return json(serializeAddress2(updated));
8082
8617
  }