@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/index.cjs CHANGED
@@ -38,10 +38,52 @@ 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
+ function erpSafeWebhookUrl(url) {
53
+ try {
54
+ const u = new URL(url);
55
+ return `${u.origin}${u.pathname}`;
56
+ } catch {
57
+ return "(invalid webhook URL)";
58
+ }
59
+ }
60
+ var ERP_LOG;
61
+ var init_erp_log = __esm({
62
+ "src/plugins/erp/erp-log.ts"() {
63
+ "use strict";
64
+ ERP_LOG = "[webcore:erp]";
65
+ }
66
+ });
67
+
41
68
  // src/plugins/erp/erp-queue.ts
69
+ function queuePayloadSummary(payload) {
70
+ if (payload.kind === "order") {
71
+ const o = payload.order;
72
+ return {
73
+ kind: payload.kind,
74
+ platformOrderId: o.platformOrderId ?? o.platformOrderNumber,
75
+ itemCount: Array.isArray(o.items) ? o.items.length : 0
76
+ };
77
+ }
78
+ return { kind: payload.kind };
79
+ }
42
80
  async function queueErp(cms, payload) {
43
81
  const queue = cms.getPlugin("queue");
44
- if (!queue) return;
82
+ if (!queue) {
83
+ warnErp("queue:add_skipped", { reason: "queue_plugin_missing", ...queuePayloadSummary(payload) });
84
+ return;
85
+ }
86
+ logErp("queue:add", { job: ERP_QUEUE_NAME, ...queuePayloadSummary(payload) });
45
87
  await queue.add(ERP_QUEUE_NAME, payload);
46
88
  }
47
89
  function registerErpQueueProcessor(cms) {
@@ -49,18 +91,31 @@ function registerErpQueueProcessor(cms) {
49
91
  if (!queue) return;
50
92
  queue.registerProcessor(ERP_QUEUE_NAME, async (data) => {
51
93
  const erp = cms.getPlugin("erp");
52
- if (!erp) return;
94
+ if (!erp) {
95
+ warnErp("queue:processor_skip", { reason: "erp_plugin_missing" });
96
+ return;
97
+ }
53
98
  const payload = data;
54
- if (payload.kind === "lead") {
55
- await erp.submission.submitContact(payload.contact);
56
- } else if (payload.kind === "formOpportunity") {
57
- await erp.submission.submitFormOpportunity(payload.contact);
58
- } else if (payload.kind === "createContact") {
59
- await erp.submission.submitCreateContact(payload.contact);
60
- } else if (payload.kind === "order") {
61
- await erp.submission.submitOrder(payload.order);
62
- } else if (payload.kind === "productUpsert") {
63
- await erp.submission.submitProductUpsert(payload.product);
99
+ logErp("queue:job_start", queuePayloadSummary(payload));
100
+ try {
101
+ if (payload.kind === "lead") {
102
+ await erp.submission.submitContact(payload.contact);
103
+ } else if (payload.kind === "formOpportunity") {
104
+ await erp.submission.submitFormOpportunity(payload.contact);
105
+ } else if (payload.kind === "createContact") {
106
+ await erp.submission.submitCreateContact(payload.contact);
107
+ } else if (payload.kind === "order") {
108
+ await erp.submission.submitOrder(payload.order);
109
+ } else if (payload.kind === "productUpsert") {
110
+ await erp.submission.submitProductUpsert(payload.product);
111
+ }
112
+ logErp("queue:job_done", queuePayloadSummary(payload));
113
+ } catch (e) {
114
+ errorErp("queue:job_failed", {
115
+ ...queuePayloadSummary(payload),
116
+ message: e instanceof Error ? e.message : String(e)
117
+ });
118
+ throw e;
64
119
  }
65
120
  });
66
121
  }
@@ -68,6 +123,7 @@ var ERP_QUEUE_NAME;
68
123
  var init_erp_queue = __esm({
69
124
  "src/plugins/erp/erp-queue.ts"() {
70
125
  "use strict";
126
+ init_erp_log();
71
127
  ERP_QUEUE_NAME = "erp";
72
128
  }
73
129
  });
@@ -119,21 +175,36 @@ async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId)
119
175
  const cfgRows = await configRepo.find({ where: { settings: "erp", deleted: false } });
120
176
  for (const row of cfgRows) {
121
177
  const r = row;
122
- if (r.key === "enabled" && r.value === "false") return;
178
+ if (r.key === "enabled" && r.value === "false") {
179
+ logErp("paid-order:skip", { orderId, reason: "erp_config_disabled" });
180
+ return;
181
+ }
182
+ }
183
+ if (!cms.getPlugin("erp")) {
184
+ logErp("paid-order:skip", { orderId, reason: "erp_plugin_missing" });
185
+ return;
123
186
  }
124
- if (!cms.getPlugin("erp")) return;
125
187
  const orderRepo = dataSource.getRepository(entityMap.orders);
126
188
  const ord = await orderRepo.findOne({
127
189
  where: { id: orderId },
128
190
  relations: ["items", "items.product", "contact", "billingAddress", "shippingAddress", "payments"]
129
191
  });
130
- if (!ord) return;
192
+ if (!ord) {
193
+ logErp("paid-order:skip", { orderId, reason: "order_not_found" });
194
+ return;
195
+ }
131
196
  const o = ord;
132
197
  const okKind = o.orderKind === void 0 || o.orderKind === null || o.orderKind === "sale";
133
- if (!okKind) return;
198
+ if (!okKind) {
199
+ logErp("paid-order:skip", { orderId, reason: "order_kind_not_sale", orderKind: o.orderKind });
200
+ return;
201
+ }
134
202
  const rawPayments = o.payments ?? [];
135
203
  const completedPayments = rawPayments.filter((pay) => pay.status === "completed" && pay.deleted !== true);
136
- if (!completedPayments.length) return;
204
+ if (!completedPayments.length) {
205
+ logErp("paid-order:skip", { orderId, reason: "no_completed_payments" });
206
+ return;
207
+ }
137
208
  const rawItems = o.items ?? [];
138
209
  const lines = rawItems.filter((it) => it.product).map((it) => {
139
210
  const p = it.product;
@@ -152,7 +223,10 @@ async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId)
152
223
  type: itemType
153
224
  };
154
225
  });
155
- if (!lines.length) return;
226
+ if (!lines.length) {
227
+ logErp("paid-order:skip", { orderId, reason: "no_line_items_with_product" });
228
+ return;
229
+ }
156
230
  const contact = o.contact;
157
231
  const orderTotalMajor = Number(o.total);
158
232
  const paymentDtos = completedPayments.length === 1 && Number.isFinite(orderTotalMajor) ? [paymentRowToWebhookDto(completedPayments[0], orderTotalMajor)] : completedPayments.map((pay) => paymentRowToWebhookDto(pay));
@@ -174,13 +248,28 @@ async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId)
174
248
  payments: paymentDtos,
175
249
  metadata: { ...baseMeta, source: "storefront" }
176
250
  };
251
+ logErp("paid-order:payload_built", {
252
+ orderId,
253
+ platformOrderId: orderDto.platformOrderId,
254
+ status: orderDto.status,
255
+ itemCount: lines.length,
256
+ skus: lines.map((l) => l.sku),
257
+ paymentCount: paymentDtos.length,
258
+ paymentIds: paymentDtos.map((p) => p.id),
259
+ total: orderTotalMajor
260
+ });
177
261
  await queueErp(cms, { kind: "order", order: orderDto });
178
- } catch {
262
+ } catch (e) {
263
+ errorErp("paid-order:enqueue_failed", {
264
+ orderId,
265
+ message: e instanceof Error ? e.message : String(e)
266
+ });
179
267
  }
180
268
  }
181
269
  var init_paid_order_erp = __esm({
182
270
  "src/plugins/erp/paid-order-erp.ts"() {
183
271
  "use strict";
272
+ init_erp_log();
184
273
  init_erp_queue();
185
274
  }
186
275
  });
@@ -816,6 +905,7 @@ async function createCmsApp(options) {
816
905
  }
817
906
 
818
907
  // src/plugins/erp/erp-submission.ts
908
+ init_erp_log();
819
909
  var ERPSubmissionService = class {
820
910
  webhookUrl;
821
911
  webhookJwt;
@@ -895,7 +985,29 @@ var ERPSubmissionService = class {
895
985
  };
896
986
  return this.postWebhookJson(envelope);
897
987
  }
988
+ summarizeWebhookBody(body) {
989
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
990
+ return { bodyKind: typeof body };
991
+ }
992
+ const o = body;
993
+ const out = { event_type: o.event_type, timestamp: o.timestamp };
994
+ const data = o.data;
995
+ if (data && typeof data === "object" && !Array.isArray(data)) {
996
+ const d = data;
997
+ out.dataKeys = Object.keys(d);
998
+ out.platformOrderId = d.platformOrderId ?? d.platformOrderNumber;
999
+ out.itemCount = Array.isArray(d.items) ? d.items.length : void 0;
1000
+ }
1001
+ return out;
1002
+ }
898
1003
  async postWebhookJson(body) {
1004
+ const safeUrl = erpSafeWebhookUrl(this.webhookUrl);
1005
+ const bodyJson = JSON.stringify(body);
1006
+ logErp("webhook:post_start", {
1007
+ url: safeUrl,
1008
+ bodyBytes: bodyJson.length,
1009
+ ...this.summarizeWebhookBody(body)
1010
+ });
899
1011
  try {
900
1012
  const res = await fetch(this.webhookUrl, {
901
1013
  method: "POST",
@@ -903,13 +1015,19 @@ var ERPSubmissionService = class {
903
1015
  "Content-Type": "application/json",
904
1016
  "X-External-Token": this.webhookJwt
905
1017
  },
906
- body: JSON.stringify(body)
1018
+ body: bodyJson
907
1019
  });
908
- if (res.ok) return { success: true, status: res.status };
909
1020
  const text = await res.text();
1021
+ const preview = text.length > 500 ? `${text.slice(0, 500)}\u2026` : text;
1022
+ if (res.ok) {
1023
+ logErp("webhook:post_ok", { status: res.status, responsePreview: preview || "(empty body)" });
1024
+ return { success: true, status: res.status };
1025
+ }
1026
+ warnErp("webhook:post_http_error", { status: res.status, responsePreview: preview });
910
1027
  return { success: false, error: `${res.status} ${text.slice(0, 500)}`, status: res.status };
911
1028
  } catch (e) {
912
1029
  const message = e instanceof Error ? e.message : "ERP webhook request failed";
1030
+ errorErp("webhook:post_fetch_failed", { url: safeUrl, message });
913
1031
  return { success: false, error: message };
914
1032
  }
915
1033
  }
@@ -1023,7 +1141,20 @@ var ERPSubmissionService = class {
1023
1141
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1024
1142
  data: orderDto
1025
1143
  };
1026
- return this.postWebhookJson(envelope);
1144
+ logErp("submitOrder:envelope_ready", {
1145
+ event_type: envelope.event_type,
1146
+ timestamp: envelope.timestamp,
1147
+ platformOrderId: orderDto.platformOrderId ?? orderDto.platformOrderNumber,
1148
+ itemCount: Array.isArray(orderDto.items) ? orderDto.items.length : 0,
1149
+ paymentCount: Array.isArray(orderDto.payments) ? orderDto.payments.length : 0
1150
+ });
1151
+ const result = await this.postWebhookJson(envelope);
1152
+ if (result.success) {
1153
+ logErp("submitOrder:complete", { ok: true, status: result.status });
1154
+ } else {
1155
+ warnErp("submitOrder:complete", { ok: false, status: result.status, error: result.error });
1156
+ }
1157
+ return result;
1027
1158
  }
1028
1159
  extractContactData(formData, formFields) {
1029
1160
  const contactData = {
@@ -4160,11 +4291,11 @@ async function readBufferFromPublicUrl(url) {
4160
4291
  throw new Error("Unsupported media URL");
4161
4292
  }
4162
4293
  function sanitizeZipPath(entryName) {
4163
- const norm2 = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
4164
- for (const seg of norm2) {
4294
+ const norm3 = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
4295
+ for (const seg of norm3) {
4165
4296
  if (seg === ".." || seg === ".") return null;
4166
4297
  }
4167
- return norm2;
4298
+ return norm3;
4168
4299
  }
4169
4300
  function shouldSkipEntry(parts) {
4170
4301
  if (parts[0] === "__MACOSX") return true;
@@ -5184,18 +5315,37 @@ function createUsersApiHandlers(config) {
5184
5315
  try {
5185
5316
  const body = await req.json();
5186
5317
  if (!body?.name || !body?.email) return json({ error: "Name and email are required" }, { status: 400 });
5187
- const existing = await userRepo().findOne({ where: { email: body.email } });
5188
- if (existing) return json({ error: "User with this email already exists" }, { status: 400 });
5318
+ const email = body.email;
5319
+ const existing = await userRepo().findOne({ where: { email } });
5320
+ if (existing && !existing.deleted) {
5321
+ return json({ error: "User with this email already exists" }, { status: 400 });
5322
+ }
5189
5323
  const groupRepo = dataSource.getRepository(entityMap.user_groups);
5190
5324
  const customerG = await groupRepo.findOne({ where: { name: "Customer", deleted: false } });
5191
5325
  const gid = body.groupId ?? null;
5192
5326
  const isCustomer = !!(customerG && gid === customerG.id);
5193
5327
  const adminAccess = isCustomer ? false : body.adminAccess === false ? false : true;
5194
5328
  const blocked = body.blocked === true || body.blocked === "true" || body.blocked === 1 || body.blocked === "1";
5195
- const newUser = await userRepo().save(
5329
+ const newUser = existing?.deleted ? await (async () => {
5330
+ await userRepo().update(existing.id, {
5331
+ deleted: false,
5332
+ deletedAt: null,
5333
+ deletedBy: null,
5334
+ name: body.name,
5335
+ email,
5336
+ password: null,
5337
+ blocked,
5338
+ groupId: gid,
5339
+ adminAccess,
5340
+ updatedAt: /* @__PURE__ */ new Date()
5341
+ });
5342
+ const row = await userRepo().findOne({ where: { id: existing.id } });
5343
+ if (!row) throw new Error("user missing after restore");
5344
+ return row;
5345
+ })() : await userRepo().save(
5196
5346
  userRepo().create({
5197
5347
  name: body.name,
5198
- email: body.email,
5348
+ email,
5199
5349
  password: null,
5200
5350
  blocked,
5201
5351
  groupId: gid,
@@ -5348,21 +5498,110 @@ function createUserAvatarHandler(config) {
5348
5498
  }
5349
5499
  };
5350
5500
  }
5501
+ var PROFILE_EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
5351
5502
  function createUserProfileHandler(config) {
5352
- const { dataSource, entityMap, json, getSession } = config;
5353
- return async function PUT(req) {
5503
+ const { dataSource, entityMap, json, getSession, onProfileUpdated } = config;
5504
+ async function loadCurrentUser() {
5354
5505
  const session = await getSession();
5355
- if (!session?.user?.email) return json({ error: "Unauthorized" }, { status: 401 });
5356
- try {
5357
- const body = await req.json();
5358
- if (!body?.name) return json({ error: "Name is required" }, { status: 400 });
5359
- const userRepo = dataSource.getRepository(entityMap.users);
5360
- await userRepo.update({ email: session.user.email }, { name: body.name, updatedAt: /* @__PURE__ */ new Date() });
5361
- const updated = await userRepo.findOne({ where: { email: session.user.email }, select: ["id", "name", "email"] });
5362
- if (!updated) return json({ error: "Not found" }, { status: 404 });
5363
- return json({ message: "Profile updated successfully", user: { id: updated.id, name: updated.name, email: updated.email } });
5364
- } catch {
5365
- return json({ error: "Internal server error" }, { status: 500 });
5506
+ const su = session?.user;
5507
+ if (!su?.email && su?.id == null) {
5508
+ return { ok: false, response: json({ error: "Unauthorized" }, { status: 401 }) };
5509
+ }
5510
+ const userRepo = dataSource.getRepository(entityMap.users);
5511
+ let user = null;
5512
+ const uidRaw = su.id != null ? String(su.id).trim() : "";
5513
+ const uid = uidRaw && /^\d+$/.test(uidRaw) ? parseInt(uidRaw, 10) : NaN;
5514
+ if (Number.isFinite(uid) && uid > 0) {
5515
+ user = await userRepo.findOne({
5516
+ where: { id: uid, deleted: false },
5517
+ select: ["id", "name", "email", "phone", "createdAt"]
5518
+ });
5519
+ }
5520
+ if (!user && su.email) {
5521
+ const em = String(su.email).trim().toLowerCase();
5522
+ if (em) {
5523
+ user = await userRepo.findOne({
5524
+ where: { email: em, deleted: false },
5525
+ select: ["id", "name", "email", "phone", "createdAt"]
5526
+ });
5527
+ }
5528
+ }
5529
+ if (!user) return { ok: false, response: json({ error: "Not found" }, { status: 404 }) };
5530
+ return { ok: true, user };
5531
+ }
5532
+ return {
5533
+ async GET(_req) {
5534
+ try {
5535
+ const r = await loadCurrentUser();
5536
+ if (!r.ok) return r.response;
5537
+ const u = r.user;
5538
+ return json({
5539
+ id: u.id,
5540
+ name: u.name ?? "",
5541
+ email: u.email ?? "",
5542
+ phone: u.phone ?? null,
5543
+ createdAt: u.createdAt instanceof Date ? u.createdAt.toISOString() : u.createdAt ?? void 0
5544
+ });
5545
+ } catch {
5546
+ return json({ error: "Internal server error" }, { status: 500 });
5547
+ }
5548
+ },
5549
+ async PUT(req) {
5550
+ try {
5551
+ const r = await loadCurrentUser();
5552
+ if (!r.ok) return r.response;
5553
+ const current = r.user;
5554
+ let body;
5555
+ try {
5556
+ body = await req.json();
5557
+ } catch {
5558
+ return json({ error: "Invalid JSON" }, { status: 400 });
5559
+ }
5560
+ const name = typeof body.name === "string" ? body.name.trim() : "";
5561
+ if (!name) return json({ error: "Name is required" }, { status: 400 });
5562
+ const emailRaw = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
5563
+ if (!emailRaw || !PROFILE_EMAIL_RE.test(emailRaw)) {
5564
+ return json({ error: "Valid email is required" }, { status: 400 });
5565
+ }
5566
+ const phone = body.phone === null || body.phone === void 0 ? null : typeof body.phone === "string" ? body.phone.trim() || null : null;
5567
+ const userRepo = dataSource.getRepository(entityMap.users);
5568
+ if (emailRaw !== String(current.email ?? "").toLowerCase()) {
5569
+ const taken = await userRepo.findOne({
5570
+ where: { email: emailRaw, deleted: false },
5571
+ select: ["id"]
5572
+ });
5573
+ if (taken && taken.id !== current.id) {
5574
+ return json({ error: "Email is already in use" }, { status: 409 });
5575
+ }
5576
+ }
5577
+ await userRepo.update(
5578
+ { id: current.id },
5579
+ {
5580
+ name,
5581
+ email: emailRaw,
5582
+ phone,
5583
+ updatedAt: /* @__PURE__ */ new Date()
5584
+ }
5585
+ );
5586
+ const updated = await userRepo.findOne({
5587
+ where: { id: current.id },
5588
+ select: ["id", "name", "email", "phone"]
5589
+ });
5590
+ if (!updated) return json({ error: "Not found" }, { status: 404 });
5591
+ const row = updated;
5592
+ if (onProfileUpdated) {
5593
+ try {
5594
+ await onProfileUpdated(req, row);
5595
+ } catch {
5596
+ }
5597
+ }
5598
+ return json({
5599
+ message: "Profile updated successfully",
5600
+ user: { id: row.id, name: row.name, email: row.email, phone: row.phone }
5601
+ });
5602
+ } catch {
5603
+ return json({ error: "Internal server error" }, { status: 500 });
5604
+ }
5366
5605
  }
5367
5606
  };
5368
5607
  }
@@ -8687,7 +8926,7 @@ function getNextAuthOptions(config) {
8687
8926
  }
8688
8927
  },
8689
8928
  callbacks: {
8690
- async jwt({ token, user }) {
8929
+ async jwt({ token, user, trigger, session }) {
8691
8930
  if (user) {
8692
8931
  const u = user;
8693
8932
  token.id = u.id;
@@ -8696,11 +8935,19 @@ function getNextAuthOptions(config) {
8696
8935
  token.entityPerms = u.entityPerms;
8697
8936
  token.adminAccess = u.adminAccess;
8698
8937
  }
8938
+ if (trigger === "update" && session && typeof session === "object") {
8939
+ const s = session;
8940
+ const t = token;
8941
+ if (typeof s.name === "string") t.name = s.name;
8942
+ if (typeof s.email === "string") t.email = s.email;
8943
+ }
8699
8944
  return token;
8700
8945
  },
8701
8946
  async session({ session, token }) {
8702
8947
  if (session.user) {
8703
8948
  const t = token;
8949
+ if (typeof t.name === "string") session.user.name = t.name;
8950
+ if (typeof t.email === "string") session.user.email = t.email;
8704
8951
  session.user.id = t.id;
8705
8952
  session.user.groupId = t.groupId;
8706
8953
  session.user.isRBACAdmin = t.isRBACAdmin;
@@ -8716,6 +8963,66 @@ function getNextAuthOptions(config) {
8716
8963
 
8717
8964
  // src/api/crud.ts
8718
8965
  var import_typeorm46 = require("typeorm");
8966
+
8967
+ // src/lib/address-geo-validation.ts
8968
+ var import_country_state_city = require("country-state-city");
8969
+ function norm2(s) {
8970
+ return typeof s === "string" ? s.trim() : "";
8971
+ }
8972
+ function resolveCountry(input) {
8973
+ const t = input.trim();
8974
+ if (!t) return void 0;
8975
+ if (t.length === 2) {
8976
+ const byCode = import_country_state_city.Country.getCountryByCode(t.toUpperCase());
8977
+ if (byCode) return byCode;
8978
+ }
8979
+ const lower = t.toLowerCase();
8980
+ return import_country_state_city.Country.getAllCountries().find((c) => c.name.toLowerCase() === lower);
8981
+ }
8982
+ function resolveState(countryIso, input) {
8983
+ const t = input.trim();
8984
+ if (!t || !countryIso) return void 0;
8985
+ const states = import_country_state_city.State.getStatesOfCountry(countryIso);
8986
+ const lower = t.toLowerCase();
8987
+ return states.find((s) => s.isoCode.toLowerCase() === t.toLowerCase() || s.name.toLowerCase() === lower);
8988
+ }
8989
+ function resolveCity(countryIso, stateIso, input) {
8990
+ const t = input.trim();
8991
+ if (!t || !countryIso || !stateIso) return void 0;
8992
+ const lower = t.toLowerCase();
8993
+ const cities = import_country_state_city.City.getCitiesOfState(countryIso, stateIso);
8994
+ return cities.find((c) => c.name.toLowerCase() === lower);
8995
+ }
8996
+ function assertValidAddressHierarchy(country, state, city) {
8997
+ const c = resolveCountry(country);
8998
+ if (!c) return { ok: false, error: "Invalid or unknown country." };
8999
+ const st = resolveState(c.isoCode, state);
9000
+ if (!st) return { ok: false, error: "State or province does not match the selected country." };
9001
+ const ct = resolveCity(c.isoCode, st.isoCode, city);
9002
+ if (!ct) return { ok: false, error: "City does not match the selected state." };
9003
+ return { ok: true, country: c.name, state: st.name, city: ct.name };
9004
+ }
9005
+ function validateAndNormalizeAddressRow(row) {
9006
+ const line1 = norm2(row.line1);
9007
+ const postalCode = norm2(row.postalCode);
9008
+ const countryIn = norm2(row.country);
9009
+ const stateIn = norm2(row.state);
9010
+ const cityIn = norm2(row.city);
9011
+ if (!line1) return "Street address (line 1) is required.";
9012
+ if (!postalCode) return "Postal code is required.";
9013
+ if (!countryIn || !stateIn || !cityIn) return "Country, state, and city are required.";
9014
+ const geo = assertValidAddressHierarchy(countryIn, stateIn, cityIn);
9015
+ if (!geo.ok) return geo.error;
9016
+ row.line1 = line1;
9017
+ row.line2 = norm2(row.line2) || null;
9018
+ row.postalCode = postalCode;
9019
+ row.country = geo.country;
9020
+ row.state = geo.state;
9021
+ row.city = geo.city;
9022
+ return null;
9023
+ }
9024
+
9025
+ // src/api/crud.ts
8719
9026
  var CRUD_LOG = "[cms-crud]";
8720
9027
  function logCrudClientError(op, detail) {
8721
9028
  console.warn(CRUD_LOG, op, detail);
@@ -8812,6 +9119,156 @@ function buildSearchWhereClause(repo, search) {
8812
9119
  function entityHasSoftDelete(repo) {
8813
9120
  return repo.metadata.columns.some((c) => c.propertyName === "deleted");
8814
9121
  }
9122
+ var LIST_QUERY_RESERVED = /* @__PURE__ */ new Set(["page", "limit", "sortField", "sortOrder", "search"]);
9123
+ function dayStartUtc(isoDay) {
9124
+ return /* @__PURE__ */ new Date(isoDay + "T00:00:00.000Z");
9125
+ }
9126
+ function dayEndUtc(isoDay) {
9127
+ return /* @__PURE__ */ new Date(isoDay + "T23:59:59.999Z");
9128
+ }
9129
+ function columnTypeLabel(col) {
9130
+ const t = col.type;
9131
+ if (typeof t === "string") return t.toLowerCase();
9132
+ if (typeof t === "function") return t.name?.toLowerCase?.() ?? "";
9133
+ if (t && typeof t === "object" && "name" in t && typeof t.name === "string") {
9134
+ return String(t.name).toLowerCase();
9135
+ }
9136
+ return "";
9137
+ }
9138
+ function isListDateColumn(col) {
9139
+ const tl = columnTypeLabel(col);
9140
+ return DATE_COLUMN_TYPES.has(tl) || col.type === Date || TIMESTAMP_PROP_NAMES.has(col.propertyName);
9141
+ }
9142
+ function isListNumericColumn(col) {
9143
+ const tl = columnTypeLabel(col);
9144
+ if (col.type === Number) return true;
9145
+ const patterns = [
9146
+ "int",
9147
+ "integer",
9148
+ "int2",
9149
+ "int4",
9150
+ "int8",
9151
+ "smallint",
9152
+ "bigint",
9153
+ "float",
9154
+ "double",
9155
+ "decimal",
9156
+ "numeric",
9157
+ "real",
9158
+ "tinyint",
9159
+ "mediumint"
9160
+ ];
9161
+ if (patterns.some((x) => tl.includes(x))) return true;
9162
+ if (tl.includes("unsigned") && (tl.includes("int") || tl.includes("bigint") || tl.includes("small"))) return true;
9163
+ if (!tl && /Id$/i.test(col.propertyName) && !TIMESTAMP_PROP_NAMES.has(col.propertyName)) return true;
9164
+ return false;
9165
+ }
9166
+ function isListBooleanColumn(col) {
9167
+ const tl = columnTypeLabel(col);
9168
+ return tl === "boolean" || tl === "bool" || col.type === Boolean;
9169
+ }
9170
+ function isListStringColumn(col) {
9171
+ const tl = columnTypeLabel(col);
9172
+ if (isListDateColumn(col) || isListNumericColumn(col) || isListBooleanColumn(col)) return false;
9173
+ if (["varchar", "character varying", "text", "citext", "uuid", "char", "character", "enum"].some(
9174
+ (x) => tl.includes(x)
9175
+ )) {
9176
+ return true;
9177
+ }
9178
+ if (col.type === String) return true;
9179
+ return false;
9180
+ }
9181
+ function mergeListWhereAnd(where, patch) {
9182
+ if (Object.keys(patch).length === 0) return where;
9183
+ if (Array.isArray(where)) {
9184
+ if (where.length === 0) return [patch];
9185
+ return where.map((w) => ({ ...w, ...patch }));
9186
+ }
9187
+ if (where && typeof where === "object" && Object.keys(where).length > 0) {
9188
+ return { ...where, ...patch };
9189
+ }
9190
+ return patch;
9191
+ }
9192
+ function buildListFilterAndFromSearchParams(repo, searchParams) {
9193
+ const and = {};
9194
+ const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
9195
+ for (const col of repo.metadata.columns) {
9196
+ const name = col.propertyName;
9197
+ if (!columnNames.has(name)) continue;
9198
+ if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
9199
+ if (!isListDateColumn(col)) continue;
9200
+ const from = searchParams.get(`${name}From`)?.trim();
9201
+ const to = searchParams.get(`${name}To`)?.trim();
9202
+ if (!from && !to) continue;
9203
+ if (from && to) {
9204
+ and[name] = (0, import_typeorm46.Between)(dayStartUtc(from), dayEndUtc(to));
9205
+ } else if (from) {
9206
+ and[name] = (0, import_typeorm46.MoreThanOrEqual)(dayStartUtc(from));
9207
+ } else if (to) {
9208
+ and[name] = (0, import_typeorm46.LessThanOrEqual)(dayEndUtc(to));
9209
+ }
9210
+ }
9211
+ for (const col of repo.metadata.columns) {
9212
+ const name = col.propertyName;
9213
+ if (!columnNames.has(name)) continue;
9214
+ if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
9215
+ if (!isListNumericColumn(col)) continue;
9216
+ if (Object.prototype.hasOwnProperty.call(and, name)) continue;
9217
+ const minRaw = searchParams.get(`${name}Min`)?.trim();
9218
+ const maxRaw = searchParams.get(`${name}Max`)?.trim();
9219
+ if (!minRaw && !maxRaw) continue;
9220
+ const parseNum = (s) => {
9221
+ const n = Number(s);
9222
+ return Number.isFinite(n) ? n : null;
9223
+ };
9224
+ const nMin = minRaw ? parseNum(minRaw) : null;
9225
+ const nMax = maxRaw ? parseNum(maxRaw) : null;
9226
+ if (nMin != null && nMax != null) {
9227
+ and[name] = (0, import_typeorm46.Between)(nMin, nMax);
9228
+ } else if (nMin != null) {
9229
+ and[name] = (0, import_typeorm46.MoreThanOrEqual)(nMin);
9230
+ } else if (nMax != null) {
9231
+ and[name] = (0, import_typeorm46.LessThanOrEqual)(nMax);
9232
+ }
9233
+ }
9234
+ for (const col of repo.metadata.columns) {
9235
+ const name = col.propertyName;
9236
+ if (!columnNames.has(name)) continue;
9237
+ if (LIST_QUERY_RESERVED.has(name)) continue;
9238
+ if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
9239
+ if (!isListStringColumn(col)) continue;
9240
+ if (Object.prototype.hasOwnProperty.call(and, name)) continue;
9241
+ const raw = searchParams.get(name)?.trim();
9242
+ if (!raw) continue;
9243
+ and[name] = (0, import_typeorm46.ILike)(`%${raw}%`);
9244
+ }
9245
+ return and;
9246
+ }
9247
+ function buildExactListParamWhere(repo, searchParams) {
9248
+ const extraWhere = {};
9249
+ const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
9250
+ for (const col of repo.metadata.columns) {
9251
+ const name = col.propertyName;
9252
+ if (!columnNames.has(name)) continue;
9253
+ if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
9254
+ if (!isListNumericColumn(col)) continue;
9255
+ const v = searchParams.get(name)?.trim();
9256
+ if (v == null || v === "") continue;
9257
+ const n = Number(v);
9258
+ if (!Number.isFinite(n)) continue;
9259
+ extraWhere[name] = n;
9260
+ }
9261
+ for (const col of repo.metadata.columns) {
9262
+ if (String(col.type) !== "boolean") continue;
9263
+ const name = col.propertyName;
9264
+ if (!columnNames.has(name)) continue;
9265
+ const raw = searchParams.get(name)?.trim();
9266
+ if (raw === "true" || raw === "false") {
9267
+ extraWhere[name] = raw === "true";
9268
+ }
9269
+ }
9270
+ return extraWhere;
9271
+ }
8815
9272
  function mergeDeletedFalseWhere(repo, where) {
8816
9273
  if (!entityHasSoftDelete(repo)) return where;
8817
9274
  const d = { deleted: false };
@@ -8821,6 +9278,12 @@ function mergeDeletedFalseWhere(repo, where) {
8821
9278
  }
8822
9279
  return Object.keys(where).length > 0 ? { ...where, ...d } : d;
8823
9280
  }
9281
+ function pickDefaultListSortField(columnNames, columns) {
9282
+ for (const candidate of ["createdAt", "updatedAt", "id", "name", "sortOrder", "title"]) {
9283
+ if (columnNames.has(candidate)) return candidate;
9284
+ }
9285
+ return columns[0]?.propertyName ?? "id";
9286
+ }
8824
9287
  function normalizeProductSku(value) {
8825
9288
  if (value == null) return null;
8826
9289
  const s = String(value).trim();
@@ -8923,6 +9386,18 @@ function createCrudHandler(dataSource, entityMap, options) {
8923
9386
  if (statusFilter) qb.andWhere("order.status = :status", { status: statusFilter });
8924
9387
  if (dateFrom) qb.andWhere("order.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
8925
9388
  if (dateTo) qb.andWhere("order.createdAt <= :dateTo", { dateTo: /* @__PURE__ */ new Date(dateTo + "T23:59:59.999Z") });
9389
+ const totalMin = searchParams.get("totalMin")?.trim();
9390
+ const totalMax = searchParams.get("totalMax")?.trim();
9391
+ if (totalMin) {
9392
+ const n = Number(totalMin);
9393
+ if (Number.isFinite(n)) qb.andWhere("order.total >= :totalMin", { totalMin: n });
9394
+ }
9395
+ if (totalMax) {
9396
+ const n = Number(totalMax);
9397
+ if (Number.isFinite(n)) qb.andWhere("order.total <= :totalMax", { totalMax: n });
9398
+ }
9399
+ const currency = searchParams.get("currency")?.trim();
9400
+ if (currency) qb.andWhere("order.currency ILIKE :orderCurrency", { orderCurrency: `%${currency}%` });
8926
9401
  if (orderIdsFromPayment && orderIdsFromPayment.length) qb.andWhere("order.id IN (:...orderIds)", { orderIds: orderIdsFromPayment });
8927
9402
  const [rows, total2] = await qb.getManyAndCount();
8928
9403
  const data2 = rows.map((order) => {
@@ -8961,8 +9436,30 @@ function createCrudHandler(dataSource, entityMap, options) {
8961
9436
  if (statusFilter) qb.andWhere("payment.status = :status", { status: statusFilter });
8962
9437
  if (dateFrom) qb.andWhere("payment.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
8963
9438
  if (dateTo) qb.andWhere("payment.createdAt <= :dateTo", { dateTo: /* @__PURE__ */ new Date(dateTo + "T23:59:59.999Z") });
9439
+ const paidAtFrom = searchParams.get("paidAtFrom")?.trim();
9440
+ const paidAtTo = searchParams.get("paidAtTo")?.trim();
9441
+ if (paidAtFrom) {
9442
+ qb.andWhere("payment.paidAt >= :paidAtFrom", { paidAtFrom: /* @__PURE__ */ new Date(paidAtFrom + "T00:00:00.000Z") });
9443
+ }
9444
+ if (paidAtTo) {
9445
+ qb.andWhere("payment.paidAt <= :paidAtTo", { paidAtTo: /* @__PURE__ */ new Date(paidAtTo + "T23:59:59.999Z") });
9446
+ }
8964
9447
  if (methodFilter) qb.andWhere("payment.method = :method", { method: methodFilter });
8965
9448
  if (orderNumberParam) qb.andWhere("ord.orderNumber ILIKE :orderNumber", { orderNumber: `%${orderNumberParam}%` });
9449
+ const amountMin = searchParams.get("amountMin")?.trim();
9450
+ const amountMax = searchParams.get("amountMax")?.trim();
9451
+ if (amountMin) {
9452
+ const n = Number(amountMin);
9453
+ if (Number.isFinite(n)) qb.andWhere("payment.amount >= :amountMin", { amountMin: n });
9454
+ }
9455
+ if (amountMax) {
9456
+ const n = Number(amountMax);
9457
+ if (Number.isFinite(n)) qb.andWhere("payment.amount <= :amountMax", { amountMax: n });
9458
+ }
9459
+ const extRef = searchParams.get("externalReference")?.trim();
9460
+ if (extRef) {
9461
+ qb.andWhere("payment.externalReference ILIKE :extRef", { extRef: `%${extRef}%` });
9462
+ }
8966
9463
  const [rows, total2] = await qb.getManyAndCount();
8967
9464
  const data2 = rows.map((payment) => {
8968
9465
  const order = payment.order;
@@ -8981,18 +9478,36 @@ function createCrudHandler(dataSource, entityMap, options) {
8981
9478
  const repo2 = dataSource.getRepository(entity);
8982
9479
  const statusFilter = searchParams.get("status")?.trim();
8983
9480
  const inventory = searchParams.get("inventory")?.trim();
8984
- const productWhere = { deleted: false };
9481
+ const productWhere = {
9482
+ deleted: false,
9483
+ ...buildListFilterAndFromSearchParams(repo2, searchParams)
9484
+ };
8985
9485
  if (statusFilter) productWhere.status = statusFilter;
8986
9486
  if (inventory === "in_stock") productWhere.quantity = (0, import_typeorm46.MoreThan)(0);
8987
9487
  if (inventory === "out_of_stock") productWhere.quantity = 0;
9488
+ for (const key of ["brandId", "categoryId", "collectionId"]) {
9489
+ const raw = searchParams.get(key)?.trim();
9490
+ if (raw) {
9491
+ const n = Number(raw);
9492
+ if (Number.isFinite(n)) productWhere[key] = n;
9493
+ }
9494
+ }
9495
+ const featuredRaw = searchParams.get("featured")?.trim();
9496
+ if (featuredRaw === "true" || featuredRaw === "false") {
9497
+ productWhere.featured = featuredRaw === "true";
9498
+ }
8988
9499
  if (search && typeof search === "string" && search.trim()) {
8989
9500
  productWhere.name = (0, import_typeorm46.ILike)(`%${search.trim()}%`);
8990
9501
  }
9502
+ const productColumnNames = new Set(repo2.metadata.columns.map((c) => c.propertyName));
9503
+ const defaultProductSort = pickDefaultListSortField(productColumnNames, repo2.metadata.columns);
9504
+ const sortParam2 = (searchParams.get("sortField") ?? "").trim();
9505
+ const productSortField = sortParam2 && productColumnNames.has(sortParam2) ? sortParam2 : defaultProductSort;
8991
9506
  const [data2, total2] = await repo2.findAndCount({
8992
9507
  where: Object.keys(productWhere).length ? productWhere : void 0,
8993
9508
  skip,
8994
9509
  take: limit,
8995
- order: { [sortFieldRaw]: sortOrder }
9510
+ order: { [productSortField]: sortOrder }
8996
9511
  });
8997
9512
  return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
8998
9513
  }
@@ -9085,27 +9600,22 @@ function createCrudHandler(dataSource, entityMap, options) {
9085
9600
  }
9086
9601
  return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
9087
9602
  }
9088
- const sortField = columnNames.has(sortFieldRaw) ? sortFieldRaw : "createdAt";
9603
+ const defaultSortField = pickDefaultListSortField(columnNames, repo.metadata.columns);
9604
+ const sortParam = (searchParams.get("sortField") ?? "").trim();
9605
+ const sortField = sortParam && columnNames.has(sortParam) ? sortParam : defaultSortField;
9089
9606
  let where = {};
9090
9607
  if (search) {
9091
9608
  where = buildSearchWhereClause(repo, search);
9092
9609
  }
9093
- const intFilterKeys = ["productId", "attributeId", "taxId"];
9094
- const extraWhere = {};
9095
- for (const key of intFilterKeys) {
9096
- const v = searchParams.get(key);
9097
- if (v != null && v !== "" && columnNames.has(key)) {
9098
- const n = Number(v);
9099
- if (Number.isFinite(n)) extraWhere[key] = n;
9100
- }
9101
- }
9102
- if (Object.keys(extraWhere).length > 0) {
9610
+ where = mergeListWhereAnd(where, buildListFilterAndFromSearchParams(repo, searchParams));
9611
+ const exactParamWhere = buildExactListParamWhere(repo, searchParams);
9612
+ if (Object.keys(exactParamWhere).length > 0) {
9103
9613
  if (Array.isArray(where)) {
9104
- where = where.map((w) => ({ ...w, ...extraWhere }));
9614
+ where = where.map((w) => ({ ...w, ...exactParamWhere }));
9105
9615
  } else if (where && typeof where === "object" && Object.keys(where).length > 0) {
9106
- where = { ...where, ...extraWhere };
9616
+ where = { ...where, ...exactParamWhere };
9107
9617
  } else {
9108
- where = extraWhere;
9618
+ where = exactParamWhere;
9109
9619
  }
9110
9620
  }
9111
9621
  where = mergeDeletedFalseWhere(repo, where);
@@ -9185,6 +9695,10 @@ function createCrudHandler(dataSource, entityMap, options) {
9185
9695
  }
9186
9696
  const repo = dataSource.getRepository(entity);
9187
9697
  const persistBody = resource === "media" ? body : pickColumnUpdates(repo, body);
9698
+ if (resource === "contacts" && "type" in persistBody) {
9699
+ const t = persistBody.type;
9700
+ if (t === "" || t === "none" || t == null) persistBody.type = null;
9701
+ }
9188
9702
  if (resource !== "media" && Object.keys(persistBody).length === 0) {
9189
9703
  logCrudClientError("POST create", {
9190
9704
  reason: "no_scalar_columns_after_pick",
@@ -9207,6 +9721,17 @@ function createCrudHandler(dataSource, entityMap, options) {
9207
9721
  }
9208
9722
  }
9209
9723
  }
9724
+ if (resource === "addresses") {
9725
+ const cid = Number(persistBody.contactId);
9726
+ if (!Number.isFinite(cid)) {
9727
+ return json({ error: "Valid contactId is required." }, { status: 400 });
9728
+ }
9729
+ if (persistBody.tag === "") persistBody.tag = null;
9730
+ const addrErr = validateAndNormalizeAddressRow(persistBody);
9731
+ if (addrErr) {
9732
+ return json({ error: addrErr }, { status: 400 });
9733
+ }
9734
+ }
9210
9735
  sanitizeBodyForEntity(repo, persistBody);
9211
9736
  let created;
9212
9737
  try {
@@ -9524,10 +10049,60 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
9524
10049
  if (!cur) return json({ message: "Not found" }, { status: 404 });
9525
10050
  }
9526
10051
  const updatePayload = rawBody && typeof rawBody === "object" ? pickColumnUpdates(repo, rawBody) : {};
10052
+ if (resource === "contacts" && "type" in updatePayload) {
10053
+ const t = updatePayload.type;
10054
+ if (t === "" || t === "none" || t == null) updatePayload.type = null;
10055
+ }
9527
10056
  if (resource === "media") {
9528
10057
  const u = updatePayload;
9529
- delete u.parentId;
9530
10058
  delete u.kind;
10059
+ if (rawBody && typeof rawBody === "object" && "parentId" in rawBody) {
10060
+ let pid = null;
10061
+ const p = rawBody.parentId;
10062
+ if (p != null && p !== "") {
10063
+ const n = Number(p);
10064
+ if (!Number.isFinite(n)) {
10065
+ return json({ error: "Invalid parentId" }, { status: 400 });
10066
+ }
10067
+ pid = n;
10068
+ }
10069
+ if (pid != null) {
10070
+ const parent = await repo.findOne({
10071
+ where: { id: pid, deleted: false }
10072
+ });
10073
+ if (!parent || parent.kind !== "folder") {
10074
+ return json({ error: "parent must be a folder" }, { status: 400 });
10075
+ }
10076
+ }
10077
+ const row = await repo.findOne({
10078
+ where: { id: numericId, deleted: false }
10079
+ });
10080
+ if (!row) return json({ message: "Not found" }, { status: 404 });
10081
+ if (pid === numericId) {
10082
+ return json({ error: "Invalid parentId" }, { status: 400 });
10083
+ }
10084
+ if (row.kind === "folder" && pid != null) {
10085
+ let walk = pid;
10086
+ const seen = /* @__PURE__ */ new Set();
10087
+ while (walk != null) {
10088
+ if (walk === numericId) {
10089
+ return json(
10090
+ { error: "Cannot move a folder into itself or a descendant folder" },
10091
+ { status: 400 }
10092
+ );
10093
+ }
10094
+ if (seen.has(walk)) break;
10095
+ seen.add(walk);
10096
+ const anc = await repo.findOne({
10097
+ where: { id: walk, deleted: false }
10098
+ });
10099
+ walk = anc ? anc.parentId ?? null : null;
10100
+ }
10101
+ }
10102
+ u.parentId = pid;
10103
+ } else {
10104
+ delete u.parentId;
10105
+ }
9531
10106
  }
9532
10107
  if (resource === "products") {
9533
10108
  const currentRow = await repo.findOne({
@@ -9546,6 +10121,26 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
9546
10121
  updatePayload.sku = effSku;
9547
10122
  }
9548
10123
  }
10124
+ if (resource === "addresses" && Object.keys(updatePayload).length > 0) {
10125
+ const currentRow = await repo.findOne({
10126
+ where: { id: numericId }
10127
+ });
10128
+ if (!currentRow) return json({ message: "Not found" }, { status: 404 });
10129
+ const merged = {
10130
+ ...currentRow,
10131
+ ...updatePayload
10132
+ };
10133
+ if (merged.tag === "") merged.tag = null;
10134
+ const addrErr = validateAndNormalizeAddressRow(merged);
10135
+ if (addrErr) {
10136
+ return json({ error: addrErr }, { status: 400 });
10137
+ }
10138
+ for (const k of Object.keys(updatePayload)) {
10139
+ if (k in merged) {
10140
+ updatePayload[k] = merged[k];
10141
+ }
10142
+ }
10143
+ }
9549
10144
  if (Object.keys(updatePayload).length > 0) {
9550
10145
  sanitizeBodyForEntity(repo, updatePayload);
9551
10146
  await repo.update(numericId, updatePayload);
@@ -10717,7 +11312,7 @@ function createCmsApiHandler(config) {
10717
11312
  } : usersApi;
10718
11313
  const usersHandlers = usersApiMerged ? createUsersApiHandlers(mergePerm(usersApiMerged) ?? usersApiMerged) : null;
10719
11314
  const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
10720
- const profilePut = userProfile ? createUserProfileHandler(userProfile) : null;
11315
+ const profileHandlers = userProfile ? createUserProfileHandler(userProfile) : null;
10721
11316
  const settingsHandlers = settingsConfig ? createSettingsApiHandlers(settingsConfig) : null;
10722
11317
  const smsMessageTemplateHandlers = createSmsMessageTemplateHandlers({
10723
11318
  dataSource,
@@ -10816,7 +11411,10 @@ function createCmsApiHandler(config) {
10816
11411
  }
10817
11412
  if (path.length === 2) {
10818
11413
  if (path[1] === "avatar" && m === "POST" && avatarPost) return avatarPost(req);
10819
- if (path[1] === "profile" && m === "PUT" && profilePut) return profilePut(req);
11414
+ if (path[1] === "profile" && profileHandlers) {
11415
+ if (m === "GET") return profileHandlers.GET(req);
11416
+ if (m === "PUT") return profileHandlers.PUT(req);
11417
+ }
10820
11418
  const id = path[1];
10821
11419
  if (m === "GET") return usersHandlers.getById(req, id);
10822
11420
  if (m === "PUT" || m === "PATCH") return usersHandlers.update(req, id);
@@ -11547,18 +12145,19 @@ function createStorefrontApiHandler(config) {
11547
12145
  const contactOrErr = await getContactForAddresses();
11548
12146
  if (contactOrErr instanceof Response) return contactOrErr;
11549
12147
  const b = await req.json().catch(() => ({}));
11550
- const created = await addressRepo().save(
11551
- addressRepo().create({
11552
- contactId: contactOrErr.contactId,
11553
- tag: typeof b.tag === "string" ? b.tag.trim() || null : null,
11554
- line1: typeof b.line1 === "string" ? b.line1.trim() || null : null,
11555
- line2: typeof b.line2 === "string" ? b.line2.trim() || null : null,
11556
- city: typeof b.city === "string" ? b.city.trim() || null : null,
11557
- state: typeof b.state === "string" ? b.state.trim() || null : null,
11558
- postalCode: typeof b.postalCode === "string" ? b.postalCode.trim() || null : null,
11559
- country: typeof b.country === "string" ? b.country.trim() || null : null
11560
- })
11561
- );
12148
+ const row = {
12149
+ contactId: contactOrErr.contactId,
12150
+ tag: typeof b.tag === "string" ? b.tag.trim() || null : null,
12151
+ line1: typeof b.line1 === "string" ? b.line1 : "",
12152
+ line2: typeof b.line2 === "string" ? b.line2.trim() || null : null,
12153
+ city: typeof b.city === "string" ? b.city : "",
12154
+ state: typeof b.state === "string" ? b.state : "",
12155
+ postalCode: typeof b.postalCode === "string" ? b.postalCode : "",
12156
+ country: typeof b.country === "string" ? b.country : ""
12157
+ };
12158
+ const addrErr = validateAndNormalizeAddressRow(row);
12159
+ if (addrErr) return json({ error: addrErr }, { status: 400 });
12160
+ const created = await addressRepo().save(addressRepo().create(row));
11562
12161
  return json(serializeAddress2(created));
11563
12162
  }
11564
12163
  if (path[0] === "addresses" && path.length === 2 && (method === "PATCH" || method === "PUT")) {
@@ -11577,7 +12176,16 @@ function createStorefrontApiHandler(config) {
11577
12176
  if (b.state !== void 0) updates.state = typeof b.state === "string" ? b.state.trim() || null : null;
11578
12177
  if (b.postalCode !== void 0) updates.postalCode = typeof b.postalCode === "string" ? b.postalCode.trim() || null : null;
11579
12178
  if (b.country !== void 0) updates.country = typeof b.country === "string" ? b.country.trim() || null : null;
11580
- if (Object.keys(updates).length) await addressRepo().update(id, updates);
12179
+ if (Object.keys(updates).length) {
12180
+ const merged = { ...existing, ...updates };
12181
+ if (merged.tag === "") merged.tag = null;
12182
+ const addrErr = validateAndNormalizeAddressRow(merged);
12183
+ if (addrErr) return json({ error: addrErr }, { status: 400 });
12184
+ for (const k of Object.keys(updates)) {
12185
+ if (k in merged) updates[k] = merged[k];
12186
+ }
12187
+ await addressRepo().update(id, updates);
12188
+ }
11581
12189
  const updated = await addressRepo().findOne({ where: { id } });
11582
12190
  return json(serializeAddress2(updated));
11583
12191
  }