@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.cjs CHANGED
@@ -38,16 +38,51 @@ var __decorateClass = (decorators, target, key, kind) => {
38
38
  return result;
39
39
  };
40
40
 
41
+ // src/plugins/erp/erp-log.ts
42
+ function logErp(event, detail) {
43
+ if (detail && Object.keys(detail).length) console.info(ERP_LOG, event, detail);
44
+ else console.info(ERP_LOG, event);
45
+ }
46
+ function warnErp(event, detail) {
47
+ console.warn(ERP_LOG, event, detail);
48
+ }
49
+ function errorErp(event, detail) {
50
+ console.error(ERP_LOG, event, detail);
51
+ }
52
+ var ERP_LOG;
53
+ var init_erp_log = __esm({
54
+ "src/plugins/erp/erp-log.ts"() {
55
+ "use strict";
56
+ ERP_LOG = "[webcore:erp]";
57
+ }
58
+ });
59
+
41
60
  // src/plugins/erp/erp-queue.ts
61
+ function queuePayloadSummary(payload) {
62
+ if (payload.kind === "order") {
63
+ const o = payload.order;
64
+ return {
65
+ kind: payload.kind,
66
+ platformOrderId: o.platformOrderId ?? o.platformOrderNumber,
67
+ itemCount: Array.isArray(o.items) ? o.items.length : 0
68
+ };
69
+ }
70
+ return { kind: payload.kind };
71
+ }
42
72
  async function queueErp(cms, payload) {
43
73
  const queue = cms.getPlugin("queue");
44
- if (!queue) return;
74
+ if (!queue) {
75
+ warnErp("queue:add_skipped", { reason: "queue_plugin_missing", ...queuePayloadSummary(payload) });
76
+ return;
77
+ }
78
+ logErp("queue:add", { job: ERP_QUEUE_NAME, ...queuePayloadSummary(payload) });
45
79
  await queue.add(ERP_QUEUE_NAME, payload);
46
80
  }
47
81
  var ERP_QUEUE_NAME;
48
82
  var init_erp_queue = __esm({
49
83
  "src/plugins/erp/erp-queue.ts"() {
50
84
  "use strict";
85
+ init_erp_log();
51
86
  ERP_QUEUE_NAME = "erp";
52
87
  }
53
88
  });
@@ -360,21 +395,36 @@ async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId)
360
395
  const cfgRows = await configRepo.find({ where: { settings: "erp", deleted: false } });
361
396
  for (const row of cfgRows) {
362
397
  const r = row;
363
- if (r.key === "enabled" && r.value === "false") return;
398
+ if (r.key === "enabled" && r.value === "false") {
399
+ logErp("paid-order:skip", { orderId, reason: "erp_config_disabled" });
400
+ return;
401
+ }
402
+ }
403
+ if (!cms.getPlugin("erp")) {
404
+ logErp("paid-order:skip", { orderId, reason: "erp_plugin_missing" });
405
+ return;
364
406
  }
365
- if (!cms.getPlugin("erp")) return;
366
407
  const orderRepo = dataSource.getRepository(entityMap.orders);
367
408
  const ord = await orderRepo.findOne({
368
409
  where: { id: orderId },
369
410
  relations: ["items", "items.product", "contact", "billingAddress", "shippingAddress", "payments"]
370
411
  });
371
- if (!ord) return;
412
+ if (!ord) {
413
+ logErp("paid-order:skip", { orderId, reason: "order_not_found" });
414
+ return;
415
+ }
372
416
  const o = ord;
373
417
  const okKind = o.orderKind === void 0 || o.orderKind === null || o.orderKind === "sale";
374
- if (!okKind) return;
418
+ if (!okKind) {
419
+ logErp("paid-order:skip", { orderId, reason: "order_kind_not_sale", orderKind: o.orderKind });
420
+ return;
421
+ }
375
422
  const rawPayments = o.payments ?? [];
376
423
  const completedPayments = rawPayments.filter((pay) => pay.status === "completed" && pay.deleted !== true);
377
- if (!completedPayments.length) return;
424
+ if (!completedPayments.length) {
425
+ logErp("paid-order:skip", { orderId, reason: "no_completed_payments" });
426
+ return;
427
+ }
378
428
  const rawItems = o.items ?? [];
379
429
  const lines = rawItems.filter((it) => it.product).map((it) => {
380
430
  const p = it.product;
@@ -393,7 +443,10 @@ async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId)
393
443
  type: itemType
394
444
  };
395
445
  });
396
- if (!lines.length) return;
446
+ if (!lines.length) {
447
+ logErp("paid-order:skip", { orderId, reason: "no_line_items_with_product" });
448
+ return;
449
+ }
397
450
  const contact = o.contact;
398
451
  const orderTotalMajor = Number(o.total);
399
452
  const paymentDtos = completedPayments.length === 1 && Number.isFinite(orderTotalMajor) ? [paymentRowToWebhookDto(completedPayments[0], orderTotalMajor)] : completedPayments.map((pay) => paymentRowToWebhookDto(pay));
@@ -415,13 +468,28 @@ async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId)
415
468
  payments: paymentDtos,
416
469
  metadata: { ...baseMeta, source: "storefront" }
417
470
  };
471
+ logErp("paid-order:payload_built", {
472
+ orderId,
473
+ platformOrderId: orderDto.platformOrderId,
474
+ status: orderDto.status,
475
+ itemCount: lines.length,
476
+ skus: lines.map((l) => l.sku),
477
+ paymentCount: paymentDtos.length,
478
+ paymentIds: paymentDtos.map((p) => p.id),
479
+ total: orderTotalMajor
480
+ });
418
481
  await queueErp(cms, { kind: "order", order: orderDto });
419
- } catch {
482
+ } catch (e) {
483
+ errorErp("paid-order:enqueue_failed", {
484
+ orderId,
485
+ message: e instanceof Error ? e.message : String(e)
486
+ });
420
487
  }
421
488
  }
422
489
  var init_paid_order_erp = __esm({
423
490
  "src/plugins/erp/paid-order-erp.ts"() {
424
491
  "use strict";
492
+ init_erp_log();
425
493
  init_erp_queue();
426
494
  }
427
495
  });
@@ -529,6 +597,64 @@ async function queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, produc
529
597
  }
530
598
  }
531
599
 
600
+ // src/lib/address-geo-validation.ts
601
+ var import_country_state_city = require("country-state-city");
602
+ function norm(s) {
603
+ return typeof s === "string" ? s.trim() : "";
604
+ }
605
+ function resolveCountry(input) {
606
+ const t = input.trim();
607
+ if (!t) return void 0;
608
+ if (t.length === 2) {
609
+ const byCode = import_country_state_city.Country.getCountryByCode(t.toUpperCase());
610
+ if (byCode) return byCode;
611
+ }
612
+ const lower = t.toLowerCase();
613
+ return import_country_state_city.Country.getAllCountries().find((c) => c.name.toLowerCase() === lower);
614
+ }
615
+ function resolveState(countryIso, input) {
616
+ const t = input.trim();
617
+ if (!t || !countryIso) return void 0;
618
+ const states = import_country_state_city.State.getStatesOfCountry(countryIso);
619
+ const lower = t.toLowerCase();
620
+ return states.find((s) => s.isoCode.toLowerCase() === t.toLowerCase() || s.name.toLowerCase() === lower);
621
+ }
622
+ function resolveCity(countryIso, stateIso, input) {
623
+ const t = input.trim();
624
+ if (!t || !countryIso || !stateIso) return void 0;
625
+ const lower = t.toLowerCase();
626
+ const cities = import_country_state_city.City.getCitiesOfState(countryIso, stateIso);
627
+ return cities.find((c) => c.name.toLowerCase() === lower);
628
+ }
629
+ function assertValidAddressHierarchy(country, state, city) {
630
+ const c = resolveCountry(country);
631
+ if (!c) return { ok: false, error: "Invalid or unknown country." };
632
+ const st = resolveState(c.isoCode, state);
633
+ if (!st) return { ok: false, error: "State or province does not match the selected country." };
634
+ const ct = resolveCity(c.isoCode, st.isoCode, city);
635
+ if (!ct) return { ok: false, error: "City does not match the selected state." };
636
+ return { ok: true, country: c.name, state: st.name, city: ct.name };
637
+ }
638
+ function validateAndNormalizeAddressRow(row) {
639
+ const line1 = norm(row.line1);
640
+ const postalCode = norm(row.postalCode);
641
+ const countryIn = norm(row.country);
642
+ const stateIn = norm(row.state);
643
+ const cityIn = norm(row.city);
644
+ if (!line1) return "Street address (line 1) is required.";
645
+ if (!postalCode) return "Postal code is required.";
646
+ if (!countryIn || !stateIn || !cityIn) return "Country, state, and city are required.";
647
+ const geo = assertValidAddressHierarchy(countryIn, stateIn, cityIn);
648
+ if (!geo.ok) return geo.error;
649
+ row.line1 = line1;
650
+ row.line2 = norm(row.line2) || null;
651
+ row.postalCode = postalCode;
652
+ row.country = geo.country;
653
+ row.state = geo.state;
654
+ row.city = geo.city;
655
+ return null;
656
+ }
657
+
532
658
  // src/api/crud.ts
533
659
  var CRUD_LOG = "[cms-crud]";
534
660
  function logCrudClientError(op, detail) {
@@ -626,6 +752,156 @@ function buildSearchWhereClause(repo, search) {
626
752
  function entityHasSoftDelete(repo) {
627
753
  return repo.metadata.columns.some((c) => c.propertyName === "deleted");
628
754
  }
755
+ var LIST_QUERY_RESERVED = /* @__PURE__ */ new Set(["page", "limit", "sortField", "sortOrder", "search"]);
756
+ function dayStartUtc(isoDay) {
757
+ return /* @__PURE__ */ new Date(isoDay + "T00:00:00.000Z");
758
+ }
759
+ function dayEndUtc(isoDay) {
760
+ return /* @__PURE__ */ new Date(isoDay + "T23:59:59.999Z");
761
+ }
762
+ function columnTypeLabel(col) {
763
+ const t = col.type;
764
+ if (typeof t === "string") return t.toLowerCase();
765
+ if (typeof t === "function") return t.name?.toLowerCase?.() ?? "";
766
+ if (t && typeof t === "object" && "name" in t && typeof t.name === "string") {
767
+ return String(t.name).toLowerCase();
768
+ }
769
+ return "";
770
+ }
771
+ function isListDateColumn(col) {
772
+ const tl = columnTypeLabel(col);
773
+ return DATE_COLUMN_TYPES.has(tl) || col.type === Date || TIMESTAMP_PROP_NAMES.has(col.propertyName);
774
+ }
775
+ function isListNumericColumn(col) {
776
+ const tl = columnTypeLabel(col);
777
+ if (col.type === Number) return true;
778
+ const patterns = [
779
+ "int",
780
+ "integer",
781
+ "int2",
782
+ "int4",
783
+ "int8",
784
+ "smallint",
785
+ "bigint",
786
+ "float",
787
+ "double",
788
+ "decimal",
789
+ "numeric",
790
+ "real",
791
+ "tinyint",
792
+ "mediumint"
793
+ ];
794
+ if (patterns.some((x) => tl.includes(x))) return true;
795
+ if (tl.includes("unsigned") && (tl.includes("int") || tl.includes("bigint") || tl.includes("small"))) return true;
796
+ if (!tl && /Id$/i.test(col.propertyName) && !TIMESTAMP_PROP_NAMES.has(col.propertyName)) return true;
797
+ return false;
798
+ }
799
+ function isListBooleanColumn(col) {
800
+ const tl = columnTypeLabel(col);
801
+ return tl === "boolean" || tl === "bool" || col.type === Boolean;
802
+ }
803
+ function isListStringColumn(col) {
804
+ const tl = columnTypeLabel(col);
805
+ if (isListDateColumn(col) || isListNumericColumn(col) || isListBooleanColumn(col)) return false;
806
+ if (["varchar", "character varying", "text", "citext", "uuid", "char", "character", "enum"].some(
807
+ (x) => tl.includes(x)
808
+ )) {
809
+ return true;
810
+ }
811
+ if (col.type === String) return true;
812
+ return false;
813
+ }
814
+ function mergeListWhereAnd(where, patch) {
815
+ if (Object.keys(patch).length === 0) return where;
816
+ if (Array.isArray(where)) {
817
+ if (where.length === 0) return [patch];
818
+ return where.map((w) => ({ ...w, ...patch }));
819
+ }
820
+ if (where && typeof where === "object" && Object.keys(where).length > 0) {
821
+ return { ...where, ...patch };
822
+ }
823
+ return patch;
824
+ }
825
+ function buildListFilterAndFromSearchParams(repo, searchParams) {
826
+ const and = {};
827
+ const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
828
+ for (const col of repo.metadata.columns) {
829
+ const name = col.propertyName;
830
+ if (!columnNames.has(name)) continue;
831
+ if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
832
+ if (!isListDateColumn(col)) continue;
833
+ const from = searchParams.get(`${name}From`)?.trim();
834
+ const to = searchParams.get(`${name}To`)?.trim();
835
+ if (!from && !to) continue;
836
+ if (from && to) {
837
+ and[name] = (0, import_typeorm.Between)(dayStartUtc(from), dayEndUtc(to));
838
+ } else if (from) {
839
+ and[name] = (0, import_typeorm.MoreThanOrEqual)(dayStartUtc(from));
840
+ } else if (to) {
841
+ and[name] = (0, import_typeorm.LessThanOrEqual)(dayEndUtc(to));
842
+ }
843
+ }
844
+ for (const col of repo.metadata.columns) {
845
+ const name = col.propertyName;
846
+ if (!columnNames.has(name)) continue;
847
+ if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
848
+ if (!isListNumericColumn(col)) continue;
849
+ if (Object.prototype.hasOwnProperty.call(and, name)) continue;
850
+ const minRaw = searchParams.get(`${name}Min`)?.trim();
851
+ const maxRaw = searchParams.get(`${name}Max`)?.trim();
852
+ if (!minRaw && !maxRaw) continue;
853
+ const parseNum = (s) => {
854
+ const n = Number(s);
855
+ return Number.isFinite(n) ? n : null;
856
+ };
857
+ const nMin = minRaw ? parseNum(minRaw) : null;
858
+ const nMax = maxRaw ? parseNum(maxRaw) : null;
859
+ if (nMin != null && nMax != null) {
860
+ and[name] = (0, import_typeorm.Between)(nMin, nMax);
861
+ } else if (nMin != null) {
862
+ and[name] = (0, import_typeorm.MoreThanOrEqual)(nMin);
863
+ } else if (nMax != null) {
864
+ and[name] = (0, import_typeorm.LessThanOrEqual)(nMax);
865
+ }
866
+ }
867
+ for (const col of repo.metadata.columns) {
868
+ const name = col.propertyName;
869
+ if (!columnNames.has(name)) continue;
870
+ if (LIST_QUERY_RESERVED.has(name)) continue;
871
+ if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
872
+ if (!isListStringColumn(col)) continue;
873
+ if (Object.prototype.hasOwnProperty.call(and, name)) continue;
874
+ const raw = searchParams.get(name)?.trim();
875
+ if (!raw) continue;
876
+ and[name] = (0, import_typeorm.ILike)(`%${raw}%`);
877
+ }
878
+ return and;
879
+ }
880
+ function buildExactListParamWhere(repo, searchParams) {
881
+ const extraWhere = {};
882
+ const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
883
+ for (const col of repo.metadata.columns) {
884
+ const name = col.propertyName;
885
+ if (!columnNames.has(name)) continue;
886
+ if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
887
+ if (!isListNumericColumn(col)) continue;
888
+ const v = searchParams.get(name)?.trim();
889
+ if (v == null || v === "") continue;
890
+ const n = Number(v);
891
+ if (!Number.isFinite(n)) continue;
892
+ extraWhere[name] = n;
893
+ }
894
+ for (const col of repo.metadata.columns) {
895
+ if (String(col.type) !== "boolean") continue;
896
+ const name = col.propertyName;
897
+ if (!columnNames.has(name)) continue;
898
+ const raw = searchParams.get(name)?.trim();
899
+ if (raw === "true" || raw === "false") {
900
+ extraWhere[name] = raw === "true";
901
+ }
902
+ }
903
+ return extraWhere;
904
+ }
629
905
  function mergeDeletedFalseWhere(repo, where) {
630
906
  if (!entityHasSoftDelete(repo)) return where;
631
907
  const d = { deleted: false };
@@ -635,6 +911,12 @@ function mergeDeletedFalseWhere(repo, where) {
635
911
  }
636
912
  return Object.keys(where).length > 0 ? { ...where, ...d } : d;
637
913
  }
914
+ function pickDefaultListSortField(columnNames, columns) {
915
+ for (const candidate of ["createdAt", "updatedAt", "id", "name", "sortOrder", "title"]) {
916
+ if (columnNames.has(candidate)) return candidate;
917
+ }
918
+ return columns[0]?.propertyName ?? "id";
919
+ }
638
920
  function normalizeProductSku(value) {
639
921
  if (value == null) return null;
640
922
  const s = String(value).trim();
@@ -737,6 +1019,18 @@ function createCrudHandler(dataSource, entityMap, options) {
737
1019
  if (statusFilter) qb.andWhere("order.status = :status", { status: statusFilter });
738
1020
  if (dateFrom) qb.andWhere("order.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
739
1021
  if (dateTo) qb.andWhere("order.createdAt <= :dateTo", { dateTo: /* @__PURE__ */ new Date(dateTo + "T23:59:59.999Z") });
1022
+ const totalMin = searchParams.get("totalMin")?.trim();
1023
+ const totalMax = searchParams.get("totalMax")?.trim();
1024
+ if (totalMin) {
1025
+ const n = Number(totalMin);
1026
+ if (Number.isFinite(n)) qb.andWhere("order.total >= :totalMin", { totalMin: n });
1027
+ }
1028
+ if (totalMax) {
1029
+ const n = Number(totalMax);
1030
+ if (Number.isFinite(n)) qb.andWhere("order.total <= :totalMax", { totalMax: n });
1031
+ }
1032
+ const currency = searchParams.get("currency")?.trim();
1033
+ if (currency) qb.andWhere("order.currency ILIKE :orderCurrency", { orderCurrency: `%${currency}%` });
740
1034
  if (orderIdsFromPayment && orderIdsFromPayment.length) qb.andWhere("order.id IN (:...orderIds)", { orderIds: orderIdsFromPayment });
741
1035
  const [rows, total2] = await qb.getManyAndCount();
742
1036
  const data2 = rows.map((order) => {
@@ -775,8 +1069,30 @@ function createCrudHandler(dataSource, entityMap, options) {
775
1069
  if (statusFilter) qb.andWhere("payment.status = :status", { status: statusFilter });
776
1070
  if (dateFrom) qb.andWhere("payment.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
777
1071
  if (dateTo) qb.andWhere("payment.createdAt <= :dateTo", { dateTo: /* @__PURE__ */ new Date(dateTo + "T23:59:59.999Z") });
1072
+ const paidAtFrom = searchParams.get("paidAtFrom")?.trim();
1073
+ const paidAtTo = searchParams.get("paidAtTo")?.trim();
1074
+ if (paidAtFrom) {
1075
+ qb.andWhere("payment.paidAt >= :paidAtFrom", { paidAtFrom: /* @__PURE__ */ new Date(paidAtFrom + "T00:00:00.000Z") });
1076
+ }
1077
+ if (paidAtTo) {
1078
+ qb.andWhere("payment.paidAt <= :paidAtTo", { paidAtTo: /* @__PURE__ */ new Date(paidAtTo + "T23:59:59.999Z") });
1079
+ }
778
1080
  if (methodFilter) qb.andWhere("payment.method = :method", { method: methodFilter });
779
1081
  if (orderNumberParam) qb.andWhere("ord.orderNumber ILIKE :orderNumber", { orderNumber: `%${orderNumberParam}%` });
1082
+ const amountMin = searchParams.get("amountMin")?.trim();
1083
+ const amountMax = searchParams.get("amountMax")?.trim();
1084
+ if (amountMin) {
1085
+ const n = Number(amountMin);
1086
+ if (Number.isFinite(n)) qb.andWhere("payment.amount >= :amountMin", { amountMin: n });
1087
+ }
1088
+ if (amountMax) {
1089
+ const n = Number(amountMax);
1090
+ if (Number.isFinite(n)) qb.andWhere("payment.amount <= :amountMax", { amountMax: n });
1091
+ }
1092
+ const extRef = searchParams.get("externalReference")?.trim();
1093
+ if (extRef) {
1094
+ qb.andWhere("payment.externalReference ILIKE :extRef", { extRef: `%${extRef}%` });
1095
+ }
780
1096
  const [rows, total2] = await qb.getManyAndCount();
781
1097
  const data2 = rows.map((payment) => {
782
1098
  const order = payment.order;
@@ -795,18 +1111,36 @@ function createCrudHandler(dataSource, entityMap, options) {
795
1111
  const repo2 = dataSource.getRepository(entity);
796
1112
  const statusFilter = searchParams.get("status")?.trim();
797
1113
  const inventory = searchParams.get("inventory")?.trim();
798
- const productWhere = { deleted: false };
1114
+ const productWhere = {
1115
+ deleted: false,
1116
+ ...buildListFilterAndFromSearchParams(repo2, searchParams)
1117
+ };
799
1118
  if (statusFilter) productWhere.status = statusFilter;
800
1119
  if (inventory === "in_stock") productWhere.quantity = (0, import_typeorm.MoreThan)(0);
801
1120
  if (inventory === "out_of_stock") productWhere.quantity = 0;
1121
+ for (const key of ["brandId", "categoryId", "collectionId"]) {
1122
+ const raw = searchParams.get(key)?.trim();
1123
+ if (raw) {
1124
+ const n = Number(raw);
1125
+ if (Number.isFinite(n)) productWhere[key] = n;
1126
+ }
1127
+ }
1128
+ const featuredRaw = searchParams.get("featured")?.trim();
1129
+ if (featuredRaw === "true" || featuredRaw === "false") {
1130
+ productWhere.featured = featuredRaw === "true";
1131
+ }
802
1132
  if (search && typeof search === "string" && search.trim()) {
803
1133
  productWhere.name = (0, import_typeorm.ILike)(`%${search.trim()}%`);
804
1134
  }
1135
+ const productColumnNames = new Set(repo2.metadata.columns.map((c) => c.propertyName));
1136
+ const defaultProductSort = pickDefaultListSortField(productColumnNames, repo2.metadata.columns);
1137
+ const sortParam2 = (searchParams.get("sortField") ?? "").trim();
1138
+ const productSortField = sortParam2 && productColumnNames.has(sortParam2) ? sortParam2 : defaultProductSort;
805
1139
  const [data2, total2] = await repo2.findAndCount({
806
1140
  where: Object.keys(productWhere).length ? productWhere : void 0,
807
1141
  skip,
808
1142
  take: limit,
809
- order: { [sortFieldRaw]: sortOrder }
1143
+ order: { [productSortField]: sortOrder }
810
1144
  });
811
1145
  return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
812
1146
  }
@@ -899,27 +1233,22 @@ function createCrudHandler(dataSource, entityMap, options) {
899
1233
  }
900
1234
  return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
901
1235
  }
902
- const sortField = columnNames.has(sortFieldRaw) ? sortFieldRaw : "createdAt";
1236
+ const defaultSortField = pickDefaultListSortField(columnNames, repo.metadata.columns);
1237
+ const sortParam = (searchParams.get("sortField") ?? "").trim();
1238
+ const sortField = sortParam && columnNames.has(sortParam) ? sortParam : defaultSortField;
903
1239
  let where = {};
904
1240
  if (search) {
905
1241
  where = buildSearchWhereClause(repo, search);
906
1242
  }
907
- const intFilterKeys = ["productId", "attributeId", "taxId"];
908
- const extraWhere = {};
909
- for (const key of intFilterKeys) {
910
- const v = searchParams.get(key);
911
- if (v != null && v !== "" && columnNames.has(key)) {
912
- const n = Number(v);
913
- if (Number.isFinite(n)) extraWhere[key] = n;
914
- }
915
- }
916
- if (Object.keys(extraWhere).length > 0) {
1243
+ where = mergeListWhereAnd(where, buildListFilterAndFromSearchParams(repo, searchParams));
1244
+ const exactParamWhere = buildExactListParamWhere(repo, searchParams);
1245
+ if (Object.keys(exactParamWhere).length > 0) {
917
1246
  if (Array.isArray(where)) {
918
- where = where.map((w) => ({ ...w, ...extraWhere }));
1247
+ where = where.map((w) => ({ ...w, ...exactParamWhere }));
919
1248
  } else if (where && typeof where === "object" && Object.keys(where).length > 0) {
920
- where = { ...where, ...extraWhere };
1249
+ where = { ...where, ...exactParamWhere };
921
1250
  } else {
922
- where = extraWhere;
1251
+ where = exactParamWhere;
923
1252
  }
924
1253
  }
925
1254
  where = mergeDeletedFalseWhere(repo, where);
@@ -999,6 +1328,10 @@ function createCrudHandler(dataSource, entityMap, options) {
999
1328
  }
1000
1329
  const repo = dataSource.getRepository(entity);
1001
1330
  const persistBody = resource === "media" ? body : pickColumnUpdates(repo, body);
1331
+ if (resource === "contacts" && "type" in persistBody) {
1332
+ const t = persistBody.type;
1333
+ if (t === "" || t === "none" || t == null) persistBody.type = null;
1334
+ }
1002
1335
  if (resource !== "media" && Object.keys(persistBody).length === 0) {
1003
1336
  logCrudClientError("POST create", {
1004
1337
  reason: "no_scalar_columns_after_pick",
@@ -1021,6 +1354,17 @@ function createCrudHandler(dataSource, entityMap, options) {
1021
1354
  }
1022
1355
  }
1023
1356
  }
1357
+ if (resource === "addresses") {
1358
+ const cid = Number(persistBody.contactId);
1359
+ if (!Number.isFinite(cid)) {
1360
+ return json({ error: "Valid contactId is required." }, { status: 400 });
1361
+ }
1362
+ if (persistBody.tag === "") persistBody.tag = null;
1363
+ const addrErr = validateAndNormalizeAddressRow(persistBody);
1364
+ if (addrErr) {
1365
+ return json({ error: addrErr }, { status: 400 });
1366
+ }
1367
+ }
1024
1368
  sanitizeBodyForEntity(repo, persistBody);
1025
1369
  let created;
1026
1370
  try {
@@ -1338,10 +1682,60 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
1338
1682
  if (!cur) return json({ message: "Not found" }, { status: 404 });
1339
1683
  }
1340
1684
  const updatePayload = rawBody && typeof rawBody === "object" ? pickColumnUpdates(repo, rawBody) : {};
1685
+ if (resource === "contacts" && "type" in updatePayload) {
1686
+ const t = updatePayload.type;
1687
+ if (t === "" || t === "none" || t == null) updatePayload.type = null;
1688
+ }
1341
1689
  if (resource === "media") {
1342
1690
  const u = updatePayload;
1343
- delete u.parentId;
1344
1691
  delete u.kind;
1692
+ if (rawBody && typeof rawBody === "object" && "parentId" in rawBody) {
1693
+ let pid = null;
1694
+ const p = rawBody.parentId;
1695
+ if (p != null && p !== "") {
1696
+ const n = Number(p);
1697
+ if (!Number.isFinite(n)) {
1698
+ return json({ error: "Invalid parentId" }, { status: 400 });
1699
+ }
1700
+ pid = n;
1701
+ }
1702
+ if (pid != null) {
1703
+ const parent = await repo.findOne({
1704
+ where: { id: pid, deleted: false }
1705
+ });
1706
+ if (!parent || parent.kind !== "folder") {
1707
+ return json({ error: "parent must be a folder" }, { status: 400 });
1708
+ }
1709
+ }
1710
+ const row = await repo.findOne({
1711
+ where: { id: numericId, deleted: false }
1712
+ });
1713
+ if (!row) return json({ message: "Not found" }, { status: 404 });
1714
+ if (pid === numericId) {
1715
+ return json({ error: "Invalid parentId" }, { status: 400 });
1716
+ }
1717
+ if (row.kind === "folder" && pid != null) {
1718
+ let walk = pid;
1719
+ const seen = /* @__PURE__ */ new Set();
1720
+ while (walk != null) {
1721
+ if (walk === numericId) {
1722
+ return json(
1723
+ { error: "Cannot move a folder into itself or a descendant folder" },
1724
+ { status: 400 }
1725
+ );
1726
+ }
1727
+ if (seen.has(walk)) break;
1728
+ seen.add(walk);
1729
+ const anc = await repo.findOne({
1730
+ where: { id: walk, deleted: false }
1731
+ });
1732
+ walk = anc ? anc.parentId ?? null : null;
1733
+ }
1734
+ }
1735
+ u.parentId = pid;
1736
+ } else {
1737
+ delete u.parentId;
1738
+ }
1345
1739
  }
1346
1740
  if (resource === "products") {
1347
1741
  const currentRow = await repo.findOne({
@@ -1360,6 +1754,26 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
1360
1754
  updatePayload.sku = effSku;
1361
1755
  }
1362
1756
  }
1757
+ if (resource === "addresses" && Object.keys(updatePayload).length > 0) {
1758
+ const currentRow = await repo.findOne({
1759
+ where: { id: numericId }
1760
+ });
1761
+ if (!currentRow) return json({ message: "Not found" }, { status: 404 });
1762
+ const merged = {
1763
+ ...currentRow,
1764
+ ...updatePayload
1765
+ };
1766
+ if (merged.tag === "") merged.tag = null;
1767
+ const addrErr = validateAndNormalizeAddressRow(merged);
1768
+ if (addrErr) {
1769
+ return json({ error: addrErr }, { status: 400 });
1770
+ }
1771
+ for (const k of Object.keys(updatePayload)) {
1772
+ if (k in merged) {
1773
+ updatePayload[k] = merged[k];
1774
+ }
1775
+ }
1776
+ }
1363
1777
  if (Object.keys(updatePayload).length > 0) {
1364
1778
  sanitizeBodyForEntity(repo, updatePayload);
1365
1779
  await repo.update(numericId, updatePayload);
@@ -1713,11 +2127,11 @@ async function readBufferFromPublicUrl(url) {
1713
2127
  throw new Error("Unsupported media URL");
1714
2128
  }
1715
2129
  function sanitizeZipPath(entryName) {
1716
- const norm = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
1717
- for (const seg of norm) {
2130
+ const norm2 = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
2131
+ for (const seg of norm2) {
1718
2132
  if (seg === ".." || seg === ".") return null;
1719
2133
  }
1720
- return norm;
2134
+ return norm2;
1721
2135
  }
1722
2136
  function shouldSkipEntry(parts) {
1723
2137
  if (parts[0] === "__MACOSX") return true;
@@ -2737,18 +3151,37 @@ function createUsersApiHandlers(config) {
2737
3151
  try {
2738
3152
  const body = await req.json();
2739
3153
  if (!body?.name || !body?.email) return json({ error: "Name and email are required" }, { status: 400 });
2740
- const existing = await userRepo().findOne({ where: { email: body.email } });
2741
- if (existing) return json({ error: "User with this email already exists" }, { status: 400 });
3154
+ const email = body.email;
3155
+ const existing = await userRepo().findOne({ where: { email } });
3156
+ if (existing && !existing.deleted) {
3157
+ return json({ error: "User with this email already exists" }, { status: 400 });
3158
+ }
2742
3159
  const groupRepo = dataSource.getRepository(entityMap.user_groups);
2743
3160
  const customerG = await groupRepo.findOne({ where: { name: "Customer", deleted: false } });
2744
3161
  const gid = body.groupId ?? null;
2745
3162
  const isCustomer = !!(customerG && gid === customerG.id);
2746
3163
  const adminAccess = isCustomer ? false : body.adminAccess === false ? false : true;
2747
3164
  const blocked = body.blocked === true || body.blocked === "true" || body.blocked === 1 || body.blocked === "1";
2748
- const newUser = await userRepo().save(
3165
+ const newUser = existing?.deleted ? await (async () => {
3166
+ await userRepo().update(existing.id, {
3167
+ deleted: false,
3168
+ deletedAt: null,
3169
+ deletedBy: null,
3170
+ name: body.name,
3171
+ email,
3172
+ password: null,
3173
+ blocked,
3174
+ groupId: gid,
3175
+ adminAccess,
3176
+ updatedAt: /* @__PURE__ */ new Date()
3177
+ });
3178
+ const row = await userRepo().findOne({ where: { id: existing.id } });
3179
+ if (!row) throw new Error("user missing after restore");
3180
+ return row;
3181
+ })() : await userRepo().save(
2749
3182
  userRepo().create({
2750
3183
  name: body.name,
2751
- email: body.email,
3184
+ email,
2752
3185
  password: null,
2753
3186
  blocked,
2754
3187
  groupId: gid,
@@ -2901,21 +3334,110 @@ function createUserAvatarHandler(config) {
2901
3334
  }
2902
3335
  };
2903
3336
  }
3337
+ var PROFILE_EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2904
3338
  function createUserProfileHandler(config) {
2905
- const { dataSource, entityMap, json, getSession } = config;
2906
- return async function PUT(req) {
3339
+ const { dataSource, entityMap, json, getSession, onProfileUpdated } = config;
3340
+ async function loadCurrentUser() {
2907
3341
  const session = await getSession();
2908
- if (!session?.user?.email) return json({ error: "Unauthorized" }, { status: 401 });
2909
- try {
2910
- const body = await req.json();
2911
- if (!body?.name) return json({ error: "Name is required" }, { status: 400 });
2912
- const userRepo = dataSource.getRepository(entityMap.users);
2913
- await userRepo.update({ email: session.user.email }, { name: body.name, updatedAt: /* @__PURE__ */ new Date() });
2914
- const updated = await userRepo.findOne({ where: { email: session.user.email }, select: ["id", "name", "email"] });
2915
- if (!updated) return json({ error: "Not found" }, { status: 404 });
2916
- return json({ message: "Profile updated successfully", user: { id: updated.id, name: updated.name, email: updated.email } });
2917
- } catch {
2918
- return json({ error: "Internal server error" }, { status: 500 });
3342
+ const su = session?.user;
3343
+ if (!su?.email && su?.id == null) {
3344
+ return { ok: false, response: json({ error: "Unauthorized" }, { status: 401 }) };
3345
+ }
3346
+ const userRepo = dataSource.getRepository(entityMap.users);
3347
+ let user = null;
3348
+ const uidRaw = su.id != null ? String(su.id).trim() : "";
3349
+ const uid = uidRaw && /^\d+$/.test(uidRaw) ? parseInt(uidRaw, 10) : NaN;
3350
+ if (Number.isFinite(uid) && uid > 0) {
3351
+ user = await userRepo.findOne({
3352
+ where: { id: uid, deleted: false },
3353
+ select: ["id", "name", "email", "phone", "createdAt"]
3354
+ });
3355
+ }
3356
+ if (!user && su.email) {
3357
+ const em = String(su.email).trim().toLowerCase();
3358
+ if (em) {
3359
+ user = await userRepo.findOne({
3360
+ where: { email: em, deleted: false },
3361
+ select: ["id", "name", "email", "phone", "createdAt"]
3362
+ });
3363
+ }
3364
+ }
3365
+ if (!user) return { ok: false, response: json({ error: "Not found" }, { status: 404 }) };
3366
+ return { ok: true, user };
3367
+ }
3368
+ return {
3369
+ async GET(_req) {
3370
+ try {
3371
+ const r = await loadCurrentUser();
3372
+ if (!r.ok) return r.response;
3373
+ const u = r.user;
3374
+ return json({
3375
+ id: u.id,
3376
+ name: u.name ?? "",
3377
+ email: u.email ?? "",
3378
+ phone: u.phone ?? null,
3379
+ createdAt: u.createdAt instanceof Date ? u.createdAt.toISOString() : u.createdAt ?? void 0
3380
+ });
3381
+ } catch {
3382
+ return json({ error: "Internal server error" }, { status: 500 });
3383
+ }
3384
+ },
3385
+ async PUT(req) {
3386
+ try {
3387
+ const r = await loadCurrentUser();
3388
+ if (!r.ok) return r.response;
3389
+ const current = r.user;
3390
+ let body;
3391
+ try {
3392
+ body = await req.json();
3393
+ } catch {
3394
+ return json({ error: "Invalid JSON" }, { status: 400 });
3395
+ }
3396
+ const name = typeof body.name === "string" ? body.name.trim() : "";
3397
+ if (!name) return json({ error: "Name is required" }, { status: 400 });
3398
+ const emailRaw = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
3399
+ if (!emailRaw || !PROFILE_EMAIL_RE.test(emailRaw)) {
3400
+ return json({ error: "Valid email is required" }, { status: 400 });
3401
+ }
3402
+ const phone = body.phone === null || body.phone === void 0 ? null : typeof body.phone === "string" ? body.phone.trim() || null : null;
3403
+ const userRepo = dataSource.getRepository(entityMap.users);
3404
+ if (emailRaw !== String(current.email ?? "").toLowerCase()) {
3405
+ const taken = await userRepo.findOne({
3406
+ where: { email: emailRaw, deleted: false },
3407
+ select: ["id"]
3408
+ });
3409
+ if (taken && taken.id !== current.id) {
3410
+ return json({ error: "Email is already in use" }, { status: 409 });
3411
+ }
3412
+ }
3413
+ await userRepo.update(
3414
+ { id: current.id },
3415
+ {
3416
+ name,
3417
+ email: emailRaw,
3418
+ phone,
3419
+ updatedAt: /* @__PURE__ */ new Date()
3420
+ }
3421
+ );
3422
+ const updated = await userRepo.findOne({
3423
+ where: { id: current.id },
3424
+ select: ["id", "name", "email", "phone"]
3425
+ });
3426
+ if (!updated) return json({ error: "Not found" }, { status: 404 });
3427
+ const row = updated;
3428
+ if (onProfileUpdated) {
3429
+ try {
3430
+ await onProfileUpdated(req, row);
3431
+ } catch {
3432
+ }
3433
+ }
3434
+ return json({
3435
+ message: "Profile updated successfully",
3436
+ user: { id: row.id, name: row.name, email: row.email, phone: row.phone }
3437
+ });
3438
+ } catch {
3439
+ return json({ error: "Internal server error" }, { status: 500 });
3440
+ }
2919
3441
  }
2920
3442
  };
2921
3443
  }
@@ -6928,7 +7450,7 @@ function createCmsApiHandler(config) {
6928
7450
  } : usersApi;
6929
7451
  const usersHandlers = usersApiMerged ? createUsersApiHandlers(mergePerm(usersApiMerged) ?? usersApiMerged) : null;
6930
7452
  const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
6931
- const profilePut = userProfile ? createUserProfileHandler(userProfile) : null;
7453
+ const profileHandlers = userProfile ? createUserProfileHandler(userProfile) : null;
6932
7454
  const settingsHandlers = settingsConfig ? createSettingsApiHandlers(settingsConfig) : null;
6933
7455
  const smsMessageTemplateHandlers = createSmsMessageTemplateHandlers({
6934
7456
  dataSource,
@@ -7027,7 +7549,10 @@ function createCmsApiHandler(config) {
7027
7549
  }
7028
7550
  if (path.length === 2) {
7029
7551
  if (path[1] === "avatar" && m === "POST" && avatarPost) return avatarPost(req);
7030
- if (path[1] === "profile" && m === "PUT" && profilePut) return profilePut(req);
7552
+ if (path[1] === "profile" && profileHandlers) {
7553
+ if (m === "GET") return profileHandlers.GET(req);
7554
+ if (m === "PUT") return profileHandlers.PUT(req);
7555
+ }
7031
7556
  const id = path[1];
7032
7557
  if (m === "GET") return usersHandlers.getById(req, id);
7033
7558
  if (m === "PUT" || m === "PATCH") return usersHandlers.update(req, id);
@@ -8090,18 +8615,19 @@ function createStorefrontApiHandler(config) {
8090
8615
  const contactOrErr = await getContactForAddresses();
8091
8616
  if (contactOrErr instanceof Response) return contactOrErr;
8092
8617
  const b = await req.json().catch(() => ({}));
8093
- const created = await addressRepo().save(
8094
- addressRepo().create({
8095
- contactId: contactOrErr.contactId,
8096
- tag: typeof b.tag === "string" ? b.tag.trim() || null : null,
8097
- line1: typeof b.line1 === "string" ? b.line1.trim() || null : null,
8098
- line2: typeof b.line2 === "string" ? b.line2.trim() || null : null,
8099
- city: typeof b.city === "string" ? b.city.trim() || null : null,
8100
- state: typeof b.state === "string" ? b.state.trim() || null : null,
8101
- postalCode: typeof b.postalCode === "string" ? b.postalCode.trim() || null : null,
8102
- country: typeof b.country === "string" ? b.country.trim() || null : null
8103
- })
8104
- );
8618
+ const row = {
8619
+ contactId: contactOrErr.contactId,
8620
+ tag: typeof b.tag === "string" ? b.tag.trim() || null : null,
8621
+ line1: typeof b.line1 === "string" ? b.line1 : "",
8622
+ line2: typeof b.line2 === "string" ? b.line2.trim() || null : null,
8623
+ city: typeof b.city === "string" ? b.city : "",
8624
+ state: typeof b.state === "string" ? b.state : "",
8625
+ postalCode: typeof b.postalCode === "string" ? b.postalCode : "",
8626
+ country: typeof b.country === "string" ? b.country : ""
8627
+ };
8628
+ const addrErr = validateAndNormalizeAddressRow(row);
8629
+ if (addrErr) return json({ error: addrErr }, { status: 400 });
8630
+ const created = await addressRepo().save(addressRepo().create(row));
8105
8631
  return json(serializeAddress2(created));
8106
8632
  }
8107
8633
  if (path[0] === "addresses" && path.length === 2 && (method === "PATCH" || method === "PUT")) {
@@ -8120,7 +8646,16 @@ function createStorefrontApiHandler(config) {
8120
8646
  if (b.state !== void 0) updates.state = typeof b.state === "string" ? b.state.trim() || null : null;
8121
8647
  if (b.postalCode !== void 0) updates.postalCode = typeof b.postalCode === "string" ? b.postalCode.trim() || null : null;
8122
8648
  if (b.country !== void 0) updates.country = typeof b.country === "string" ? b.country.trim() || null : null;
8123
- if (Object.keys(updates).length) await addressRepo().update(id, updates);
8649
+ if (Object.keys(updates).length) {
8650
+ const merged = { ...existing, ...updates };
8651
+ if (merged.tag === "") merged.tag = null;
8652
+ const addrErr = validateAndNormalizeAddressRow(merged);
8653
+ if (addrErr) return json({ error: addrErr }, { status: 400 });
8654
+ for (const k of Object.keys(updates)) {
8655
+ if (k in merged) updates[k] = merged[k];
8656
+ }
8657
+ await addressRepo().update(id, updates);
8658
+ }
8124
8659
  const updated = await addressRepo().findOne({ where: { id } });
8125
8660
  return json(serializeAddress2(updated));
8126
8661
  }