@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.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
  });
@@ -684,6 +752,156 @@ function buildSearchWhereClause(repo, search) {
684
752
  function entityHasSoftDelete(repo) {
685
753
  return repo.metadata.columns.some((c) => c.propertyName === "deleted");
686
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
+ }
687
905
  function mergeDeletedFalseWhere(repo, where) {
688
906
  if (!entityHasSoftDelete(repo)) return where;
689
907
  const d = { deleted: false };
@@ -693,6 +911,12 @@ function mergeDeletedFalseWhere(repo, where) {
693
911
  }
694
912
  return Object.keys(where).length > 0 ? { ...where, ...d } : d;
695
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
+ }
696
920
  function normalizeProductSku(value) {
697
921
  if (value == null) return null;
698
922
  const s = String(value).trim();
@@ -795,6 +1019,18 @@ function createCrudHandler(dataSource, entityMap, options) {
795
1019
  if (statusFilter) qb.andWhere("order.status = :status", { status: statusFilter });
796
1020
  if (dateFrom) qb.andWhere("order.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
797
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}%` });
798
1034
  if (orderIdsFromPayment && orderIdsFromPayment.length) qb.andWhere("order.id IN (:...orderIds)", { orderIds: orderIdsFromPayment });
799
1035
  const [rows, total2] = await qb.getManyAndCount();
800
1036
  const data2 = rows.map((order) => {
@@ -833,8 +1069,30 @@ function createCrudHandler(dataSource, entityMap, options) {
833
1069
  if (statusFilter) qb.andWhere("payment.status = :status", { status: statusFilter });
834
1070
  if (dateFrom) qb.andWhere("payment.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
835
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
+ }
836
1080
  if (methodFilter) qb.andWhere("payment.method = :method", { method: methodFilter });
837
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
+ }
838
1096
  const [rows, total2] = await qb.getManyAndCount();
839
1097
  const data2 = rows.map((payment) => {
840
1098
  const order = payment.order;
@@ -853,7 +1111,10 @@ function createCrudHandler(dataSource, entityMap, options) {
853
1111
  const repo2 = dataSource.getRepository(entity);
854
1112
  const statusFilter = searchParams.get("status")?.trim();
855
1113
  const inventory = searchParams.get("inventory")?.trim();
856
- const productWhere = { deleted: false };
1114
+ const productWhere = {
1115
+ deleted: false,
1116
+ ...buildListFilterAndFromSearchParams(repo2, searchParams)
1117
+ };
857
1118
  if (statusFilter) productWhere.status = statusFilter;
858
1119
  if (inventory === "in_stock") productWhere.quantity = (0, import_typeorm.MoreThan)(0);
859
1120
  if (inventory === "out_of_stock") productWhere.quantity = 0;
@@ -871,11 +1132,15 @@ function createCrudHandler(dataSource, entityMap, options) {
871
1132
  if (search && typeof search === "string" && search.trim()) {
872
1133
  productWhere.name = (0, import_typeorm.ILike)(`%${search.trim()}%`);
873
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;
874
1139
  const [data2, total2] = await repo2.findAndCount({
875
1140
  where: Object.keys(productWhere).length ? productWhere : void 0,
876
1141
  skip,
877
1142
  take: limit,
878
- order: { [sortFieldRaw]: sortOrder }
1143
+ order: { [productSortField]: sortOrder }
879
1144
  });
880
1145
  return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
881
1146
  }
@@ -968,36 +1233,22 @@ function createCrudHandler(dataSource, entityMap, options) {
968
1233
  }
969
1234
  return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
970
1235
  }
971
- 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;
972
1239
  let where = {};
973
1240
  if (search) {
974
1241
  where = buildSearchWhereClause(repo, search);
975
1242
  }
976
- const intFilterKeys = ["productId", "attributeId", "taxId", "brandId", "categoryId", "collectionId", "parentId"];
977
- const extraWhere = {};
978
- for (const key of intFilterKeys) {
979
- const v = searchParams.get(key);
980
- if (v != null && v !== "" && columnNames.has(key)) {
981
- const n = Number(v);
982
- if (Number.isFinite(n)) extraWhere[key] = n;
983
- }
984
- }
985
- for (const col of repo.metadata.columns) {
986
- if (String(col.type) !== "boolean") continue;
987
- const name = col.propertyName;
988
- if (!columnNames.has(name)) continue;
989
- const raw = searchParams.get(name)?.trim();
990
- if (raw === "true" || raw === "false") {
991
- extraWhere[name] = raw === "true";
992
- }
993
- }
994
- 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) {
995
1246
  if (Array.isArray(where)) {
996
- where = where.map((w) => ({ ...w, ...extraWhere }));
1247
+ where = where.map((w) => ({ ...w, ...exactParamWhere }));
997
1248
  } else if (where && typeof where === "object" && Object.keys(where).length > 0) {
998
- where = { ...where, ...extraWhere };
1249
+ where = { ...where, ...exactParamWhere };
999
1250
  } else {
1000
- where = extraWhere;
1251
+ where = exactParamWhere;
1001
1252
  }
1002
1253
  }
1003
1254
  where = mergeDeletedFalseWhere(repo, where);
@@ -1077,6 +1328,10 @@ function createCrudHandler(dataSource, entityMap, options) {
1077
1328
  }
1078
1329
  const repo = dataSource.getRepository(entity);
1079
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
+ }
1080
1335
  if (resource !== "media" && Object.keys(persistBody).length === 0) {
1081
1336
  logCrudClientError("POST create", {
1082
1337
  reason: "no_scalar_columns_after_pick",
@@ -1427,6 +1682,10 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
1427
1682
  if (!cur) return json({ message: "Not found" }, { status: 404 });
1428
1683
  }
1429
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
+ }
1430
1689
  if (resource === "media") {
1431
1690
  const u = updatePayload;
1432
1691
  delete u.kind;
@@ -2892,18 +3151,37 @@ function createUsersApiHandlers(config) {
2892
3151
  try {
2893
3152
  const body = await req.json();
2894
3153
  if (!body?.name || !body?.email) return json({ error: "Name and email are required" }, { status: 400 });
2895
- const existing = await userRepo().findOne({ where: { email: body.email } });
2896
- 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
+ }
2897
3159
  const groupRepo = dataSource.getRepository(entityMap.user_groups);
2898
3160
  const customerG = await groupRepo.findOne({ where: { name: "Customer", deleted: false } });
2899
3161
  const gid = body.groupId ?? null;
2900
3162
  const isCustomer = !!(customerG && gid === customerG.id);
2901
3163
  const adminAccess = isCustomer ? false : body.adminAccess === false ? false : true;
2902
3164
  const blocked = body.blocked === true || body.blocked === "true" || body.blocked === 1 || body.blocked === "1";
2903
- 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(
2904
3182
  userRepo().create({
2905
3183
  name: body.name,
2906
- email: body.email,
3184
+ email,
2907
3185
  password: null,
2908
3186
  blocked,
2909
3187
  groupId: gid,
@@ -3056,21 +3334,110 @@ function createUserAvatarHandler(config) {
3056
3334
  }
3057
3335
  };
3058
3336
  }
3337
+ var PROFILE_EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3059
3338
  function createUserProfileHandler(config) {
3060
- const { dataSource, entityMap, json, getSession } = config;
3061
- return async function PUT(req) {
3339
+ const { dataSource, entityMap, json, getSession, onProfileUpdated } = config;
3340
+ async function loadCurrentUser() {
3062
3341
  const session = await getSession();
3063
- if (!session?.user?.email) return json({ error: "Unauthorized" }, { status: 401 });
3064
- try {
3065
- const body = await req.json();
3066
- if (!body?.name) return json({ error: "Name is required" }, { status: 400 });
3067
- const userRepo = dataSource.getRepository(entityMap.users);
3068
- await userRepo.update({ email: session.user.email }, { name: body.name, updatedAt: /* @__PURE__ */ new Date() });
3069
- const updated = await userRepo.findOne({ where: { email: session.user.email }, select: ["id", "name", "email"] });
3070
- if (!updated) return json({ error: "Not found" }, { status: 404 });
3071
- return json({ message: "Profile updated successfully", user: { id: updated.id, name: updated.name, email: updated.email } });
3072
- } catch {
3073
- 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
+ }
3074
3441
  }
3075
3442
  };
3076
3443
  }
@@ -7083,7 +7450,7 @@ function createCmsApiHandler(config) {
7083
7450
  } : usersApi;
7084
7451
  const usersHandlers = usersApiMerged ? createUsersApiHandlers(mergePerm(usersApiMerged) ?? usersApiMerged) : null;
7085
7452
  const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
7086
- const profilePut = userProfile ? createUserProfileHandler(userProfile) : null;
7453
+ const profileHandlers = userProfile ? createUserProfileHandler(userProfile) : null;
7087
7454
  const settingsHandlers = settingsConfig ? createSettingsApiHandlers(settingsConfig) : null;
7088
7455
  const smsMessageTemplateHandlers = createSmsMessageTemplateHandlers({
7089
7456
  dataSource,
@@ -7182,7 +7549,10 @@ function createCmsApiHandler(config) {
7182
7549
  }
7183
7550
  if (path.length === 2) {
7184
7551
  if (path[1] === "avatar" && m === "POST" && avatarPost) return avatarPost(req);
7185
- 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
+ }
7186
7556
  const id = path[1];
7187
7557
  if (m === "GET") return usersHandlers.getById(req, id);
7188
7558
  if (m === "PUT" || m === "PATCH") return usersHandlers.update(req, id);