@infuro/cms-core 1.0.24 → 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();
@@ -631,6 +699,156 @@ function buildSearchWhereClause(repo, search) {
631
699
  function entityHasSoftDelete(repo) {
632
700
  return repo.metadata.columns.some((c) => c.propertyName === "deleted");
633
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
+ }
634
852
  function mergeDeletedFalseWhere(repo, where) {
635
853
  if (!entityHasSoftDelete(repo)) return where;
636
854
  const d = { deleted: false };
@@ -640,6 +858,12 @@ function mergeDeletedFalseWhere(repo, where) {
640
858
  }
641
859
  return Object.keys(where).length > 0 ? { ...where, ...d } : d;
642
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
+ }
643
867
  function normalizeProductSku(value) {
644
868
  if (value == null) return null;
645
869
  const s = String(value).trim();
@@ -742,6 +966,18 @@ function createCrudHandler(dataSource, entityMap, options) {
742
966
  if (statusFilter) qb.andWhere("order.status = :status", { status: statusFilter });
743
967
  if (dateFrom) qb.andWhere("order.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
744
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}%` });
745
981
  if (orderIdsFromPayment && orderIdsFromPayment.length) qb.andWhere("order.id IN (:...orderIds)", { orderIds: orderIdsFromPayment });
746
982
  const [rows, total2] = await qb.getManyAndCount();
747
983
  const data2 = rows.map((order) => {
@@ -780,8 +1016,30 @@ function createCrudHandler(dataSource, entityMap, options) {
780
1016
  if (statusFilter) qb.andWhere("payment.status = :status", { status: statusFilter });
781
1017
  if (dateFrom) qb.andWhere("payment.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
782
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
+ }
783
1027
  if (methodFilter) qb.andWhere("payment.method = :method", { method: methodFilter });
784
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
+ }
785
1043
  const [rows, total2] = await qb.getManyAndCount();
786
1044
  const data2 = rows.map((payment) => {
787
1045
  const order = payment.order;
@@ -800,7 +1058,10 @@ function createCrudHandler(dataSource, entityMap, options) {
800
1058
  const repo2 = dataSource.getRepository(entity);
801
1059
  const statusFilter = searchParams.get("status")?.trim();
802
1060
  const inventory = searchParams.get("inventory")?.trim();
803
- const productWhere = { deleted: false };
1061
+ const productWhere = {
1062
+ deleted: false,
1063
+ ...buildListFilterAndFromSearchParams(repo2, searchParams)
1064
+ };
804
1065
  if (statusFilter) productWhere.status = statusFilter;
805
1066
  if (inventory === "in_stock") productWhere.quantity = MoreThan(0);
806
1067
  if (inventory === "out_of_stock") productWhere.quantity = 0;
@@ -818,11 +1079,15 @@ function createCrudHandler(dataSource, entityMap, options) {
818
1079
  if (search && typeof search === "string" && search.trim()) {
819
1080
  productWhere.name = ILike(`%${search.trim()}%`);
820
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;
821
1086
  const [data2, total2] = await repo2.findAndCount({
822
1087
  where: Object.keys(productWhere).length ? productWhere : void 0,
823
1088
  skip,
824
1089
  take: limit,
825
- order: { [sortFieldRaw]: sortOrder }
1090
+ order: { [productSortField]: sortOrder }
826
1091
  });
827
1092
  return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
828
1093
  }
@@ -915,36 +1180,22 @@ function createCrudHandler(dataSource, entityMap, options) {
915
1180
  }
916
1181
  return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
917
1182
  }
918
- 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;
919
1186
  let where = {};
920
1187
  if (search) {
921
1188
  where = buildSearchWhereClause(repo, search);
922
1189
  }
923
- const intFilterKeys = ["productId", "attributeId", "taxId", "brandId", "categoryId", "collectionId", "parentId"];
924
- const extraWhere = {};
925
- for (const key of intFilterKeys) {
926
- const v = searchParams.get(key);
927
- if (v != null && v !== "" && columnNames.has(key)) {
928
- const n = Number(v);
929
- if (Number.isFinite(n)) extraWhere[key] = n;
930
- }
931
- }
932
- for (const col of repo.metadata.columns) {
933
- if (String(col.type) !== "boolean") continue;
934
- const name = col.propertyName;
935
- if (!columnNames.has(name)) continue;
936
- const raw = searchParams.get(name)?.trim();
937
- if (raw === "true" || raw === "false") {
938
- extraWhere[name] = raw === "true";
939
- }
940
- }
941
- 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) {
942
1193
  if (Array.isArray(where)) {
943
- where = where.map((w) => ({ ...w, ...extraWhere }));
1194
+ where = where.map((w) => ({ ...w, ...exactParamWhere }));
944
1195
  } else if (where && typeof where === "object" && Object.keys(where).length > 0) {
945
- where = { ...where, ...extraWhere };
1196
+ where = { ...where, ...exactParamWhere };
946
1197
  } else {
947
- where = extraWhere;
1198
+ where = exactParamWhere;
948
1199
  }
949
1200
  }
950
1201
  where = mergeDeletedFalseWhere(repo, where);
@@ -1024,6 +1275,10 @@ function createCrudHandler(dataSource, entityMap, options) {
1024
1275
  }
1025
1276
  const repo = dataSource.getRepository(entity);
1026
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
+ }
1027
1282
  if (resource !== "media" && Object.keys(persistBody).length === 0) {
1028
1283
  logCrudClientError("POST create", {
1029
1284
  reason: "no_scalar_columns_after_pick",
@@ -1374,6 +1629,10 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
1374
1629
  if (!cur) return json({ message: "Not found" }, { status: 404 });
1375
1630
  }
1376
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
+ }
1377
1636
  if (resource === "media") {
1378
1637
  const u = updatePayload;
1379
1638
  delete u.kind;
@@ -1663,7 +1922,7 @@ function createUserAuthApiRouter(config) {
1663
1922
  // src/api/cms-handlers.ts
1664
1923
  init_email_queue();
1665
1924
  init_erp_queue();
1666
- import { MoreThanOrEqual, ILike as ILike2, In } from "typeorm";
1925
+ import { MoreThanOrEqual as MoreThanOrEqual2, ILike as ILike2, In } from "typeorm";
1667
1926
 
1668
1927
  // src/plugins/captcha/assert.ts
1669
1928
  async function assertCaptchaOk(getCms, body, req, json) {
@@ -2049,9 +2308,9 @@ function createDashboardStatsHandler(config) {
2049
2308
  repo("users")?.count({ where: { deleted: false } }) ?? 0,
2050
2309
  repo("blogs")?.count({ where: { deleted: false } }) ?? 0,
2051
2310
  repo("contacts")?.count({
2052
- where: { deleted: false, createdAt: MoreThanOrEqual(sevenDaysAgo) }
2311
+ where: { deleted: false, createdAt: MoreThanOrEqual2(sevenDaysAgo) }
2053
2312
  }) ?? 0,
2054
- repo("form_submissions")?.count({ where: { createdAt: MoreThanOrEqual(sevenDaysAgo) } }) ?? 0,
2313
+ repo("form_submissions")?.count({ where: { createdAt: MoreThanOrEqual2(sevenDaysAgo) } }) ?? 0,
2055
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() ?? []
2056
2315
  ]);
2057
2316
  return json({
@@ -2100,19 +2359,19 @@ function createEcommerceAnalyticsHandler(config) {
2100
2359
  const productRepo = dataSource.getRepository(entityMap.products);
2101
2360
  const [salesOrders, returnOrders, replacementOrders, payments, products] = await Promise.all([
2102
2361
  orderRepo.find({
2103
- 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"]) },
2104
2363
  select: ["id", "contactId", "createdAt", "subtotal", "discount", "tax", "total", "status"]
2105
2364
  }),
2106
2365
  orderRepo.find({
2107
- where: { deleted: false, createdAt: MoreThanOrEqual(start), orderKind: "return" },
2366
+ where: { deleted: false, createdAt: MoreThanOrEqual2(start), orderKind: "return" },
2108
2367
  select: ["id", "createdAt", "total"]
2109
2368
  }),
2110
2369
  orderRepo.find({
2111
- where: { deleted: false, createdAt: MoreThanOrEqual(start), orderKind: "replacement" },
2370
+ where: { deleted: false, createdAt: MoreThanOrEqual2(start), orderKind: "replacement" },
2112
2371
  select: ["id", "createdAt", "total"]
2113
2372
  }),
2114
2373
  paymentRepo.find({
2115
- where: { deleted: false, createdAt: MoreThanOrEqual(start) },
2374
+ where: { deleted: false, createdAt: MoreThanOrEqual2(start) },
2116
2375
  select: ["id", "status", "method", "amount", "createdAt"]
2117
2376
  }),
2118
2377
  productRepo.find({
@@ -2839,18 +3098,37 @@ function createUsersApiHandlers(config) {
2839
3098
  try {
2840
3099
  const body = await req.json();
2841
3100
  if (!body?.name || !body?.email) return json({ error: "Name and email are required" }, { status: 400 });
2842
- const existing = await userRepo().findOne({ where: { email: body.email } });
2843
- 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
+ }
2844
3106
  const groupRepo = dataSource.getRepository(entityMap.user_groups);
2845
3107
  const customerG = await groupRepo.findOne({ where: { name: "Customer", deleted: false } });
2846
3108
  const gid = body.groupId ?? null;
2847
3109
  const isCustomer = !!(customerG && gid === customerG.id);
2848
3110
  const adminAccess = isCustomer ? false : body.adminAccess === false ? false : true;
2849
3111
  const blocked = body.blocked === true || body.blocked === "true" || body.blocked === 1 || body.blocked === "1";
2850
- 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(
2851
3129
  userRepo().create({
2852
3130
  name: body.name,
2853
- email: body.email,
3131
+ email,
2854
3132
  password: null,
2855
3133
  blocked,
2856
3134
  groupId: gid,
@@ -3003,21 +3281,110 @@ function createUserAvatarHandler(config) {
3003
3281
  }
3004
3282
  };
3005
3283
  }
3284
+ var PROFILE_EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3006
3285
  function createUserProfileHandler(config) {
3007
- const { dataSource, entityMap, json, getSession } = config;
3008
- return async function PUT(req) {
3286
+ const { dataSource, entityMap, json, getSession, onProfileUpdated } = config;
3287
+ async function loadCurrentUser() {
3009
3288
  const session = await getSession();
3010
- if (!session?.user?.email) return json({ error: "Unauthorized" }, { status: 401 });
3011
- try {
3012
- const body = await req.json();
3013
- if (!body?.name) return json({ error: "Name is required" }, { status: 400 });
3014
- const userRepo = dataSource.getRepository(entityMap.users);
3015
- await userRepo.update({ email: session.user.email }, { name: body.name, updatedAt: /* @__PURE__ */ new Date() });
3016
- const updated = await userRepo.findOne({ where: { email: session.user.email }, select: ["id", "name", "email"] });
3017
- if (!updated) return json({ error: "Not found" }, { status: 404 });
3018
- return json({ message: "Profile updated successfully", user: { id: updated.id, name: updated.name, email: updated.email } });
3019
- } catch {
3020
- 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
+ }
3021
3388
  }
3022
3389
  };
3023
3390
  }
@@ -7039,7 +7406,7 @@ function createCmsApiHandler(config) {
7039
7406
  } : usersApi;
7040
7407
  const usersHandlers = usersApiMerged ? createUsersApiHandlers(mergePerm(usersApiMerged) ?? usersApiMerged) : null;
7041
7408
  const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
7042
- const profilePut = userProfile ? createUserProfileHandler(userProfile) : null;
7409
+ const profileHandlers = userProfile ? createUserProfileHandler(userProfile) : null;
7043
7410
  const settingsHandlers = settingsConfig ? createSettingsApiHandlers(settingsConfig) : null;
7044
7411
  const smsMessageTemplateHandlers = createSmsMessageTemplateHandlers({
7045
7412
  dataSource,
@@ -7138,7 +7505,10 @@ function createCmsApiHandler(config) {
7138
7505
  }
7139
7506
  if (path.length === 2) {
7140
7507
  if (path[1] === "avatar" && m === "POST" && avatarPost) return avatarPost(req);
7141
- 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
+ }
7142
7512
  const id = path[1];
7143
7513
  if (m === "GET") return usersHandlers.getById(req, id);
7144
7514
  if (m === "PUT" || m === "PATCH") return usersHandlers.update(req, id);