@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.js CHANGED
@@ -17,10 +17,52 @@ 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
+ function erpSafeWebhookUrl(url) {
32
+ try {
33
+ const u = new URL(url);
34
+ return `${u.origin}${u.pathname}`;
35
+ } catch {
36
+ return "(invalid webhook URL)";
37
+ }
38
+ }
39
+ var ERP_LOG;
40
+ var init_erp_log = __esm({
41
+ "src/plugins/erp/erp-log.ts"() {
42
+ "use strict";
43
+ ERP_LOG = "[webcore:erp]";
44
+ }
45
+ });
46
+
20
47
  // src/plugins/erp/erp-queue.ts
48
+ function queuePayloadSummary(payload) {
49
+ if (payload.kind === "order") {
50
+ const o = payload.order;
51
+ return {
52
+ kind: payload.kind,
53
+ platformOrderId: o.platformOrderId ?? o.platformOrderNumber,
54
+ itemCount: Array.isArray(o.items) ? o.items.length : 0
55
+ };
56
+ }
57
+ return { kind: payload.kind };
58
+ }
21
59
  async function queueErp(cms, payload) {
22
60
  const queue = cms.getPlugin("queue");
23
- if (!queue) return;
61
+ if (!queue) {
62
+ warnErp("queue:add_skipped", { reason: "queue_plugin_missing", ...queuePayloadSummary(payload) });
63
+ return;
64
+ }
65
+ logErp("queue:add", { job: ERP_QUEUE_NAME, ...queuePayloadSummary(payload) });
24
66
  await queue.add(ERP_QUEUE_NAME, payload);
25
67
  }
26
68
  function registerErpQueueProcessor(cms) {
@@ -28,18 +70,31 @@ function registerErpQueueProcessor(cms) {
28
70
  if (!queue) return;
29
71
  queue.registerProcessor(ERP_QUEUE_NAME, async (data) => {
30
72
  const erp = cms.getPlugin("erp");
31
- if (!erp) return;
73
+ if (!erp) {
74
+ warnErp("queue:processor_skip", { reason: "erp_plugin_missing" });
75
+ return;
76
+ }
32
77
  const payload = data;
33
- if (payload.kind === "lead") {
34
- await erp.submission.submitContact(payload.contact);
35
- } else if (payload.kind === "formOpportunity") {
36
- await erp.submission.submitFormOpportunity(payload.contact);
37
- } else if (payload.kind === "createContact") {
38
- await erp.submission.submitCreateContact(payload.contact);
39
- } else if (payload.kind === "order") {
40
- await erp.submission.submitOrder(payload.order);
41
- } else if (payload.kind === "productUpsert") {
42
- await erp.submission.submitProductUpsert(payload.product);
78
+ logErp("queue:job_start", queuePayloadSummary(payload));
79
+ try {
80
+ if (payload.kind === "lead") {
81
+ await erp.submission.submitContact(payload.contact);
82
+ } else if (payload.kind === "formOpportunity") {
83
+ await erp.submission.submitFormOpportunity(payload.contact);
84
+ } else if (payload.kind === "createContact") {
85
+ await erp.submission.submitCreateContact(payload.contact);
86
+ } else if (payload.kind === "order") {
87
+ await erp.submission.submitOrder(payload.order);
88
+ } else if (payload.kind === "productUpsert") {
89
+ await erp.submission.submitProductUpsert(payload.product);
90
+ }
91
+ logErp("queue:job_done", queuePayloadSummary(payload));
92
+ } catch (e) {
93
+ errorErp("queue:job_failed", {
94
+ ...queuePayloadSummary(payload),
95
+ message: e instanceof Error ? e.message : String(e)
96
+ });
97
+ throw e;
43
98
  }
44
99
  });
45
100
  }
@@ -47,6 +102,7 @@ var ERP_QUEUE_NAME;
47
102
  var init_erp_queue = __esm({
48
103
  "src/plugins/erp/erp-queue.ts"() {
49
104
  "use strict";
105
+ init_erp_log();
50
106
  ERP_QUEUE_NAME = "erp";
51
107
  }
52
108
  });
@@ -98,21 +154,36 @@ async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId)
98
154
  const cfgRows = await configRepo.find({ where: { settings: "erp", deleted: false } });
99
155
  for (const row of cfgRows) {
100
156
  const r = row;
101
- if (r.key === "enabled" && r.value === "false") return;
157
+ if (r.key === "enabled" && r.value === "false") {
158
+ logErp("paid-order:skip", { orderId, reason: "erp_config_disabled" });
159
+ return;
160
+ }
161
+ }
162
+ if (!cms.getPlugin("erp")) {
163
+ logErp("paid-order:skip", { orderId, reason: "erp_plugin_missing" });
164
+ return;
102
165
  }
103
- if (!cms.getPlugin("erp")) return;
104
166
  const orderRepo = dataSource.getRepository(entityMap.orders);
105
167
  const ord = await orderRepo.findOne({
106
168
  where: { id: orderId },
107
169
  relations: ["items", "items.product", "contact", "billingAddress", "shippingAddress", "payments"]
108
170
  });
109
- if (!ord) return;
171
+ if (!ord) {
172
+ logErp("paid-order:skip", { orderId, reason: "order_not_found" });
173
+ return;
174
+ }
110
175
  const o = ord;
111
176
  const okKind = o.orderKind === void 0 || o.orderKind === null || o.orderKind === "sale";
112
- if (!okKind) return;
177
+ if (!okKind) {
178
+ logErp("paid-order:skip", { orderId, reason: "order_kind_not_sale", orderKind: o.orderKind });
179
+ return;
180
+ }
113
181
  const rawPayments = o.payments ?? [];
114
182
  const completedPayments = rawPayments.filter((pay) => pay.status === "completed" && pay.deleted !== true);
115
- if (!completedPayments.length) return;
183
+ if (!completedPayments.length) {
184
+ logErp("paid-order:skip", { orderId, reason: "no_completed_payments" });
185
+ return;
186
+ }
116
187
  const rawItems = o.items ?? [];
117
188
  const lines = rawItems.filter((it) => it.product).map((it) => {
118
189
  const p = it.product;
@@ -131,7 +202,10 @@ async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId)
131
202
  type: itemType
132
203
  };
133
204
  });
134
- if (!lines.length) return;
205
+ if (!lines.length) {
206
+ logErp("paid-order:skip", { orderId, reason: "no_line_items_with_product" });
207
+ return;
208
+ }
135
209
  const contact = o.contact;
136
210
  const orderTotalMajor = Number(o.total);
137
211
  const paymentDtos = completedPayments.length === 1 && Number.isFinite(orderTotalMajor) ? [paymentRowToWebhookDto(completedPayments[0], orderTotalMajor)] : completedPayments.map((pay) => paymentRowToWebhookDto(pay));
@@ -153,13 +227,28 @@ async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId)
153
227
  payments: paymentDtos,
154
228
  metadata: { ...baseMeta, source: "storefront" }
155
229
  };
230
+ logErp("paid-order:payload_built", {
231
+ orderId,
232
+ platformOrderId: orderDto.platformOrderId,
233
+ status: orderDto.status,
234
+ itemCount: lines.length,
235
+ skus: lines.map((l) => l.sku),
236
+ paymentCount: paymentDtos.length,
237
+ paymentIds: paymentDtos.map((p) => p.id),
238
+ total: orderTotalMajor
239
+ });
156
240
  await queueErp(cms, { kind: "order", order: orderDto });
157
- } catch {
241
+ } catch (e) {
242
+ errorErp("paid-order:enqueue_failed", {
243
+ orderId,
244
+ message: e instanceof Error ? e.message : String(e)
245
+ });
158
246
  }
159
247
  }
160
248
  var init_paid_order_erp = __esm({
161
249
  "src/plugins/erp/paid-order-erp.ts"() {
162
250
  "use strict";
251
+ init_erp_log();
163
252
  init_erp_queue();
164
253
  }
165
254
  });
@@ -635,6 +724,7 @@ async function createCmsApp(options) {
635
724
  }
636
725
 
637
726
  // src/plugins/erp/erp-submission.ts
727
+ init_erp_log();
638
728
  var ERPSubmissionService = class {
639
729
  webhookUrl;
640
730
  webhookJwt;
@@ -714,7 +804,29 @@ var ERPSubmissionService = class {
714
804
  };
715
805
  return this.postWebhookJson(envelope);
716
806
  }
807
+ summarizeWebhookBody(body) {
808
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
809
+ return { bodyKind: typeof body };
810
+ }
811
+ const o = body;
812
+ const out = { event_type: o.event_type, timestamp: o.timestamp };
813
+ const data = o.data;
814
+ if (data && typeof data === "object" && !Array.isArray(data)) {
815
+ const d = data;
816
+ out.dataKeys = Object.keys(d);
817
+ out.platformOrderId = d.platformOrderId ?? d.platformOrderNumber;
818
+ out.itemCount = Array.isArray(d.items) ? d.items.length : void 0;
819
+ }
820
+ return out;
821
+ }
717
822
  async postWebhookJson(body) {
823
+ const safeUrl = erpSafeWebhookUrl(this.webhookUrl);
824
+ const bodyJson = JSON.stringify(body);
825
+ logErp("webhook:post_start", {
826
+ url: safeUrl,
827
+ bodyBytes: bodyJson.length,
828
+ ...this.summarizeWebhookBody(body)
829
+ });
718
830
  try {
719
831
  const res = await fetch(this.webhookUrl, {
720
832
  method: "POST",
@@ -722,13 +834,19 @@ var ERPSubmissionService = class {
722
834
  "Content-Type": "application/json",
723
835
  "X-External-Token": this.webhookJwt
724
836
  },
725
- body: JSON.stringify(body)
837
+ body: bodyJson
726
838
  });
727
- if (res.ok) return { success: true, status: res.status };
728
839
  const text = await res.text();
840
+ const preview = text.length > 500 ? `${text.slice(0, 500)}\u2026` : text;
841
+ if (res.ok) {
842
+ logErp("webhook:post_ok", { status: res.status, responsePreview: preview || "(empty body)" });
843
+ return { success: true, status: res.status };
844
+ }
845
+ warnErp("webhook:post_http_error", { status: res.status, responsePreview: preview });
729
846
  return { success: false, error: `${res.status} ${text.slice(0, 500)}`, status: res.status };
730
847
  } catch (e) {
731
848
  const message = e instanceof Error ? e.message : "ERP webhook request failed";
849
+ errorErp("webhook:post_fetch_failed", { url: safeUrl, message });
732
850
  return { success: false, error: message };
733
851
  }
734
852
  }
@@ -842,7 +960,20 @@ var ERPSubmissionService = class {
842
960
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
843
961
  data: orderDto
844
962
  };
845
- return this.postWebhookJson(envelope);
963
+ logErp("submitOrder:envelope_ready", {
964
+ event_type: envelope.event_type,
965
+ timestamp: envelope.timestamp,
966
+ platformOrderId: orderDto.platformOrderId ?? orderDto.platformOrderNumber,
967
+ itemCount: Array.isArray(orderDto.items) ? orderDto.items.length : 0,
968
+ paymentCount: Array.isArray(orderDto.payments) ? orderDto.payments.length : 0
969
+ });
970
+ const result = await this.postWebhookJson(envelope);
971
+ if (result.success) {
972
+ logErp("submitOrder:complete", { ok: true, status: result.status });
973
+ } else {
974
+ warnErp("submitOrder:complete", { ok: false, status: result.status, error: result.error });
975
+ }
976
+ return result;
846
977
  }
847
978
  extractContactData(formData, formFields) {
848
979
  const contactData = {
@@ -3979,11 +4110,11 @@ async function readBufferFromPublicUrl(url) {
3979
4110
  throw new Error("Unsupported media URL");
3980
4111
  }
3981
4112
  function sanitizeZipPath(entryName) {
3982
- const norm2 = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
3983
- for (const seg of norm2) {
4113
+ const norm3 = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
4114
+ for (const seg of norm3) {
3984
4115
  if (seg === ".." || seg === ".") return null;
3985
4116
  }
3986
- return norm2;
4117
+ return norm3;
3987
4118
  }
3988
4119
  function shouldSkipEntry(parts) {
3989
4120
  if (parts[0] === "__MACOSX") return true;
@@ -5003,18 +5134,37 @@ function createUsersApiHandlers(config) {
5003
5134
  try {
5004
5135
  const body = await req.json();
5005
5136
  if (!body?.name || !body?.email) return json({ error: "Name and email are required" }, { status: 400 });
5006
- const existing = await userRepo().findOne({ where: { email: body.email } });
5007
- if (existing) return json({ error: "User with this email already exists" }, { status: 400 });
5137
+ const email = body.email;
5138
+ const existing = await userRepo().findOne({ where: { email } });
5139
+ if (existing && !existing.deleted) {
5140
+ return json({ error: "User with this email already exists" }, { status: 400 });
5141
+ }
5008
5142
  const groupRepo = dataSource.getRepository(entityMap.user_groups);
5009
5143
  const customerG = await groupRepo.findOne({ where: { name: "Customer", deleted: false } });
5010
5144
  const gid = body.groupId ?? null;
5011
5145
  const isCustomer = !!(customerG && gid === customerG.id);
5012
5146
  const adminAccess = isCustomer ? false : body.adminAccess === false ? false : true;
5013
5147
  const blocked = body.blocked === true || body.blocked === "true" || body.blocked === 1 || body.blocked === "1";
5014
- const newUser = await userRepo().save(
5148
+ const newUser = existing?.deleted ? await (async () => {
5149
+ await userRepo().update(existing.id, {
5150
+ deleted: false,
5151
+ deletedAt: null,
5152
+ deletedBy: null,
5153
+ name: body.name,
5154
+ email,
5155
+ password: null,
5156
+ blocked,
5157
+ groupId: gid,
5158
+ adminAccess,
5159
+ updatedAt: /* @__PURE__ */ new Date()
5160
+ });
5161
+ const row = await userRepo().findOne({ where: { id: existing.id } });
5162
+ if (!row) throw new Error("user missing after restore");
5163
+ return row;
5164
+ })() : await userRepo().save(
5015
5165
  userRepo().create({
5016
5166
  name: body.name,
5017
- email: body.email,
5167
+ email,
5018
5168
  password: null,
5019
5169
  blocked,
5020
5170
  groupId: gid,
@@ -5167,21 +5317,110 @@ function createUserAvatarHandler(config) {
5167
5317
  }
5168
5318
  };
5169
5319
  }
5320
+ var PROFILE_EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
5170
5321
  function createUserProfileHandler(config) {
5171
- const { dataSource, entityMap, json, getSession } = config;
5172
- return async function PUT(req) {
5322
+ const { dataSource, entityMap, json, getSession, onProfileUpdated } = config;
5323
+ async function loadCurrentUser() {
5173
5324
  const session = await getSession();
5174
- if (!session?.user?.email) return json({ error: "Unauthorized" }, { status: 401 });
5175
- try {
5176
- const body = await req.json();
5177
- if (!body?.name) return json({ error: "Name is required" }, { status: 400 });
5178
- const userRepo = dataSource.getRepository(entityMap.users);
5179
- await userRepo.update({ email: session.user.email }, { name: body.name, updatedAt: /* @__PURE__ */ new Date() });
5180
- const updated = await userRepo.findOne({ where: { email: session.user.email }, select: ["id", "name", "email"] });
5181
- if (!updated) return json({ error: "Not found" }, { status: 404 });
5182
- return json({ message: "Profile updated successfully", user: { id: updated.id, name: updated.name, email: updated.email } });
5183
- } catch {
5184
- return json({ error: "Internal server error" }, { status: 500 });
5325
+ const su = session?.user;
5326
+ if (!su?.email && su?.id == null) {
5327
+ return { ok: false, response: json({ error: "Unauthorized" }, { status: 401 }) };
5328
+ }
5329
+ const userRepo = dataSource.getRepository(entityMap.users);
5330
+ let user = null;
5331
+ const uidRaw = su.id != null ? String(su.id).trim() : "";
5332
+ const uid = uidRaw && /^\d+$/.test(uidRaw) ? parseInt(uidRaw, 10) : NaN;
5333
+ if (Number.isFinite(uid) && uid > 0) {
5334
+ user = await userRepo.findOne({
5335
+ where: { id: uid, deleted: false },
5336
+ select: ["id", "name", "email", "phone", "createdAt"]
5337
+ });
5338
+ }
5339
+ if (!user && su.email) {
5340
+ const em = String(su.email).trim().toLowerCase();
5341
+ if (em) {
5342
+ user = await userRepo.findOne({
5343
+ where: { email: em, deleted: false },
5344
+ select: ["id", "name", "email", "phone", "createdAt"]
5345
+ });
5346
+ }
5347
+ }
5348
+ if (!user) return { ok: false, response: json({ error: "Not found" }, { status: 404 }) };
5349
+ return { ok: true, user };
5350
+ }
5351
+ return {
5352
+ async GET(_req) {
5353
+ try {
5354
+ const r = await loadCurrentUser();
5355
+ if (!r.ok) return r.response;
5356
+ const u = r.user;
5357
+ return json({
5358
+ id: u.id,
5359
+ name: u.name ?? "",
5360
+ email: u.email ?? "",
5361
+ phone: u.phone ?? null,
5362
+ createdAt: u.createdAt instanceof Date ? u.createdAt.toISOString() : u.createdAt ?? void 0
5363
+ });
5364
+ } catch {
5365
+ return json({ error: "Internal server error" }, { status: 500 });
5366
+ }
5367
+ },
5368
+ async PUT(req) {
5369
+ try {
5370
+ const r = await loadCurrentUser();
5371
+ if (!r.ok) return r.response;
5372
+ const current = r.user;
5373
+ let body;
5374
+ try {
5375
+ body = await req.json();
5376
+ } catch {
5377
+ return json({ error: "Invalid JSON" }, { status: 400 });
5378
+ }
5379
+ const name = typeof body.name === "string" ? body.name.trim() : "";
5380
+ if (!name) return json({ error: "Name is required" }, { status: 400 });
5381
+ const emailRaw = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
5382
+ if (!emailRaw || !PROFILE_EMAIL_RE.test(emailRaw)) {
5383
+ return json({ error: "Valid email is required" }, { status: 400 });
5384
+ }
5385
+ const phone = body.phone === null || body.phone === void 0 ? null : typeof body.phone === "string" ? body.phone.trim() || null : null;
5386
+ const userRepo = dataSource.getRepository(entityMap.users);
5387
+ if (emailRaw !== String(current.email ?? "").toLowerCase()) {
5388
+ const taken = await userRepo.findOne({
5389
+ where: { email: emailRaw, deleted: false },
5390
+ select: ["id"]
5391
+ });
5392
+ if (taken && taken.id !== current.id) {
5393
+ return json({ error: "Email is already in use" }, { status: 409 });
5394
+ }
5395
+ }
5396
+ await userRepo.update(
5397
+ { id: current.id },
5398
+ {
5399
+ name,
5400
+ email: emailRaw,
5401
+ phone,
5402
+ updatedAt: /* @__PURE__ */ new Date()
5403
+ }
5404
+ );
5405
+ const updated = await userRepo.findOne({
5406
+ where: { id: current.id },
5407
+ select: ["id", "name", "email", "phone"]
5408
+ });
5409
+ if (!updated) return json({ error: "Not found" }, { status: 404 });
5410
+ const row = updated;
5411
+ if (onProfileUpdated) {
5412
+ try {
5413
+ await onProfileUpdated(req, row);
5414
+ } catch {
5415
+ }
5416
+ }
5417
+ return json({
5418
+ message: "Profile updated successfully",
5419
+ user: { id: row.id, name: row.name, email: row.email, phone: row.phone }
5420
+ });
5421
+ } catch {
5422
+ return json({ error: "Internal server error" }, { status: 500 });
5423
+ }
5185
5424
  }
5186
5425
  };
5187
5426
  }
@@ -8515,7 +8754,7 @@ function getNextAuthOptions(config) {
8515
8754
  }
8516
8755
  },
8517
8756
  callbacks: {
8518
- async jwt({ token, user }) {
8757
+ async jwt({ token, user, trigger, session }) {
8519
8758
  if (user) {
8520
8759
  const u = user;
8521
8760
  token.id = u.id;
@@ -8524,11 +8763,19 @@ function getNextAuthOptions(config) {
8524
8763
  token.entityPerms = u.entityPerms;
8525
8764
  token.adminAccess = u.adminAccess;
8526
8765
  }
8766
+ if (trigger === "update" && session && typeof session === "object") {
8767
+ const s = session;
8768
+ const t = token;
8769
+ if (typeof s.name === "string") t.name = s.name;
8770
+ if (typeof s.email === "string") t.email = s.email;
8771
+ }
8527
8772
  return token;
8528
8773
  },
8529
8774
  async session({ session, token }) {
8530
8775
  if (session.user) {
8531
8776
  const t = token;
8777
+ if (typeof t.name === "string") session.user.name = t.name;
8778
+ if (typeof t.email === "string") session.user.email = t.email;
8532
8779
  session.user.id = t.id;
8533
8780
  session.user.groupId = t.groupId;
8534
8781
  session.user.isRBACAdmin = t.isRBACAdmin;
@@ -8543,7 +8790,67 @@ function getNextAuthOptions(config) {
8543
8790
  }
8544
8791
 
8545
8792
  // src/api/crud.ts
8546
- import { ILike as ILike2, MoreThan as MoreThan2, Not } from "typeorm";
8793
+ import { Between, ILike as ILike2, LessThanOrEqual, MoreThan as MoreThan2, MoreThanOrEqual as MoreThanOrEqual2, Not } from "typeorm";
8794
+
8795
+ // src/lib/address-geo-validation.ts
8796
+ import { Country, State, City } from "country-state-city";
8797
+ function norm2(s) {
8798
+ return typeof s === "string" ? s.trim() : "";
8799
+ }
8800
+ function resolveCountry(input) {
8801
+ const t = input.trim();
8802
+ if (!t) return void 0;
8803
+ if (t.length === 2) {
8804
+ const byCode = Country.getCountryByCode(t.toUpperCase());
8805
+ if (byCode) return byCode;
8806
+ }
8807
+ const lower = t.toLowerCase();
8808
+ return Country.getAllCountries().find((c) => c.name.toLowerCase() === lower);
8809
+ }
8810
+ function resolveState(countryIso, input) {
8811
+ const t = input.trim();
8812
+ if (!t || !countryIso) return void 0;
8813
+ const states = State.getStatesOfCountry(countryIso);
8814
+ const lower = t.toLowerCase();
8815
+ return states.find((s) => s.isoCode.toLowerCase() === t.toLowerCase() || s.name.toLowerCase() === lower);
8816
+ }
8817
+ function resolveCity(countryIso, stateIso, input) {
8818
+ const t = input.trim();
8819
+ if (!t || !countryIso || !stateIso) return void 0;
8820
+ const lower = t.toLowerCase();
8821
+ const cities = City.getCitiesOfState(countryIso, stateIso);
8822
+ return cities.find((c) => c.name.toLowerCase() === lower);
8823
+ }
8824
+ function assertValidAddressHierarchy(country, state, city) {
8825
+ const c = resolveCountry(country);
8826
+ if (!c) return { ok: false, error: "Invalid or unknown country." };
8827
+ const st = resolveState(c.isoCode, state);
8828
+ if (!st) return { ok: false, error: "State or province does not match the selected country." };
8829
+ const ct = resolveCity(c.isoCode, st.isoCode, city);
8830
+ if (!ct) return { ok: false, error: "City does not match the selected state." };
8831
+ return { ok: true, country: c.name, state: st.name, city: ct.name };
8832
+ }
8833
+ function validateAndNormalizeAddressRow(row) {
8834
+ const line1 = norm2(row.line1);
8835
+ const postalCode = norm2(row.postalCode);
8836
+ const countryIn = norm2(row.country);
8837
+ const stateIn = norm2(row.state);
8838
+ const cityIn = norm2(row.city);
8839
+ if (!line1) return "Street address (line 1) is required.";
8840
+ if (!postalCode) return "Postal code is required.";
8841
+ if (!countryIn || !stateIn || !cityIn) return "Country, state, and city are required.";
8842
+ const geo = assertValidAddressHierarchy(countryIn, stateIn, cityIn);
8843
+ if (!geo.ok) return geo.error;
8844
+ row.line1 = line1;
8845
+ row.line2 = norm2(row.line2) || null;
8846
+ row.postalCode = postalCode;
8847
+ row.country = geo.country;
8848
+ row.state = geo.state;
8849
+ row.city = geo.city;
8850
+ return null;
8851
+ }
8852
+
8853
+ // src/api/crud.ts
8547
8854
  var CRUD_LOG = "[cms-crud]";
8548
8855
  function logCrudClientError(op, detail) {
8549
8856
  console.warn(CRUD_LOG, op, detail);
@@ -8640,6 +8947,156 @@ function buildSearchWhereClause(repo, search) {
8640
8947
  function entityHasSoftDelete(repo) {
8641
8948
  return repo.metadata.columns.some((c) => c.propertyName === "deleted");
8642
8949
  }
8950
+ var LIST_QUERY_RESERVED = /* @__PURE__ */ new Set(["page", "limit", "sortField", "sortOrder", "search"]);
8951
+ function dayStartUtc(isoDay) {
8952
+ return /* @__PURE__ */ new Date(isoDay + "T00:00:00.000Z");
8953
+ }
8954
+ function dayEndUtc(isoDay) {
8955
+ return /* @__PURE__ */ new Date(isoDay + "T23:59:59.999Z");
8956
+ }
8957
+ function columnTypeLabel(col) {
8958
+ const t = col.type;
8959
+ if (typeof t === "string") return t.toLowerCase();
8960
+ if (typeof t === "function") return t.name?.toLowerCase?.() ?? "";
8961
+ if (t && typeof t === "object" && "name" in t && typeof t.name === "string") {
8962
+ return String(t.name).toLowerCase();
8963
+ }
8964
+ return "";
8965
+ }
8966
+ function isListDateColumn(col) {
8967
+ const tl = columnTypeLabel(col);
8968
+ return DATE_COLUMN_TYPES.has(tl) || col.type === Date || TIMESTAMP_PROP_NAMES.has(col.propertyName);
8969
+ }
8970
+ function isListNumericColumn(col) {
8971
+ const tl = columnTypeLabel(col);
8972
+ if (col.type === Number) return true;
8973
+ const patterns = [
8974
+ "int",
8975
+ "integer",
8976
+ "int2",
8977
+ "int4",
8978
+ "int8",
8979
+ "smallint",
8980
+ "bigint",
8981
+ "float",
8982
+ "double",
8983
+ "decimal",
8984
+ "numeric",
8985
+ "real",
8986
+ "tinyint",
8987
+ "mediumint"
8988
+ ];
8989
+ if (patterns.some((x) => tl.includes(x))) return true;
8990
+ if (tl.includes("unsigned") && (tl.includes("int") || tl.includes("bigint") || tl.includes("small"))) return true;
8991
+ if (!tl && /Id$/i.test(col.propertyName) && !TIMESTAMP_PROP_NAMES.has(col.propertyName)) return true;
8992
+ return false;
8993
+ }
8994
+ function isListBooleanColumn(col) {
8995
+ const tl = columnTypeLabel(col);
8996
+ return tl === "boolean" || tl === "bool" || col.type === Boolean;
8997
+ }
8998
+ function isListStringColumn(col) {
8999
+ const tl = columnTypeLabel(col);
9000
+ if (isListDateColumn(col) || isListNumericColumn(col) || isListBooleanColumn(col)) return false;
9001
+ if (["varchar", "character varying", "text", "citext", "uuid", "char", "character", "enum"].some(
9002
+ (x) => tl.includes(x)
9003
+ )) {
9004
+ return true;
9005
+ }
9006
+ if (col.type === String) return true;
9007
+ return false;
9008
+ }
9009
+ function mergeListWhereAnd(where, patch) {
9010
+ if (Object.keys(patch).length === 0) return where;
9011
+ if (Array.isArray(where)) {
9012
+ if (where.length === 0) return [patch];
9013
+ return where.map((w) => ({ ...w, ...patch }));
9014
+ }
9015
+ if (where && typeof where === "object" && Object.keys(where).length > 0) {
9016
+ return { ...where, ...patch };
9017
+ }
9018
+ return patch;
9019
+ }
9020
+ function buildListFilterAndFromSearchParams(repo, searchParams) {
9021
+ const and = {};
9022
+ const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
9023
+ for (const col of repo.metadata.columns) {
9024
+ const name = col.propertyName;
9025
+ if (!columnNames.has(name)) continue;
9026
+ if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
9027
+ if (!isListDateColumn(col)) continue;
9028
+ const from = searchParams.get(`${name}From`)?.trim();
9029
+ const to = searchParams.get(`${name}To`)?.trim();
9030
+ if (!from && !to) continue;
9031
+ if (from && to) {
9032
+ and[name] = Between(dayStartUtc(from), dayEndUtc(to));
9033
+ } else if (from) {
9034
+ and[name] = MoreThanOrEqual2(dayStartUtc(from));
9035
+ } else if (to) {
9036
+ and[name] = LessThanOrEqual(dayEndUtc(to));
9037
+ }
9038
+ }
9039
+ for (const col of repo.metadata.columns) {
9040
+ const name = col.propertyName;
9041
+ if (!columnNames.has(name)) continue;
9042
+ if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
9043
+ if (!isListNumericColumn(col)) continue;
9044
+ if (Object.prototype.hasOwnProperty.call(and, name)) continue;
9045
+ const minRaw = searchParams.get(`${name}Min`)?.trim();
9046
+ const maxRaw = searchParams.get(`${name}Max`)?.trim();
9047
+ if (!minRaw && !maxRaw) continue;
9048
+ const parseNum = (s) => {
9049
+ const n = Number(s);
9050
+ return Number.isFinite(n) ? n : null;
9051
+ };
9052
+ const nMin = minRaw ? parseNum(minRaw) : null;
9053
+ const nMax = maxRaw ? parseNum(maxRaw) : null;
9054
+ if (nMin != null && nMax != null) {
9055
+ and[name] = Between(nMin, nMax);
9056
+ } else if (nMin != null) {
9057
+ and[name] = MoreThanOrEqual2(nMin);
9058
+ } else if (nMax != null) {
9059
+ and[name] = LessThanOrEqual(nMax);
9060
+ }
9061
+ }
9062
+ for (const col of repo.metadata.columns) {
9063
+ const name = col.propertyName;
9064
+ if (!columnNames.has(name)) continue;
9065
+ if (LIST_QUERY_RESERVED.has(name)) continue;
9066
+ if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
9067
+ if (!isListStringColumn(col)) continue;
9068
+ if (Object.prototype.hasOwnProperty.call(and, name)) continue;
9069
+ const raw = searchParams.get(name)?.trim();
9070
+ if (!raw) continue;
9071
+ and[name] = ILike2(`%${raw}%`);
9072
+ }
9073
+ return and;
9074
+ }
9075
+ function buildExactListParamWhere(repo, searchParams) {
9076
+ const extraWhere = {};
9077
+ const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
9078
+ for (const col of repo.metadata.columns) {
9079
+ const name = col.propertyName;
9080
+ if (!columnNames.has(name)) continue;
9081
+ if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
9082
+ if (!isListNumericColumn(col)) continue;
9083
+ const v = searchParams.get(name)?.trim();
9084
+ if (v == null || v === "") continue;
9085
+ const n = Number(v);
9086
+ if (!Number.isFinite(n)) continue;
9087
+ extraWhere[name] = n;
9088
+ }
9089
+ for (const col of repo.metadata.columns) {
9090
+ if (String(col.type) !== "boolean") continue;
9091
+ const name = col.propertyName;
9092
+ if (!columnNames.has(name)) continue;
9093
+ const raw = searchParams.get(name)?.trim();
9094
+ if (raw === "true" || raw === "false") {
9095
+ extraWhere[name] = raw === "true";
9096
+ }
9097
+ }
9098
+ return extraWhere;
9099
+ }
8643
9100
  function mergeDeletedFalseWhere(repo, where) {
8644
9101
  if (!entityHasSoftDelete(repo)) return where;
8645
9102
  const d = { deleted: false };
@@ -8649,6 +9106,12 @@ function mergeDeletedFalseWhere(repo, where) {
8649
9106
  }
8650
9107
  return Object.keys(where).length > 0 ? { ...where, ...d } : d;
8651
9108
  }
9109
+ function pickDefaultListSortField(columnNames, columns) {
9110
+ for (const candidate of ["createdAt", "updatedAt", "id", "name", "sortOrder", "title"]) {
9111
+ if (columnNames.has(candidate)) return candidate;
9112
+ }
9113
+ return columns[0]?.propertyName ?? "id";
9114
+ }
8652
9115
  function normalizeProductSku(value) {
8653
9116
  if (value == null) return null;
8654
9117
  const s = String(value).trim();
@@ -8751,6 +9214,18 @@ function createCrudHandler(dataSource, entityMap, options) {
8751
9214
  if (statusFilter) qb.andWhere("order.status = :status", { status: statusFilter });
8752
9215
  if (dateFrom) qb.andWhere("order.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
8753
9216
  if (dateTo) qb.andWhere("order.createdAt <= :dateTo", { dateTo: /* @__PURE__ */ new Date(dateTo + "T23:59:59.999Z") });
9217
+ const totalMin = searchParams.get("totalMin")?.trim();
9218
+ const totalMax = searchParams.get("totalMax")?.trim();
9219
+ if (totalMin) {
9220
+ const n = Number(totalMin);
9221
+ if (Number.isFinite(n)) qb.andWhere("order.total >= :totalMin", { totalMin: n });
9222
+ }
9223
+ if (totalMax) {
9224
+ const n = Number(totalMax);
9225
+ if (Number.isFinite(n)) qb.andWhere("order.total <= :totalMax", { totalMax: n });
9226
+ }
9227
+ const currency = searchParams.get("currency")?.trim();
9228
+ if (currency) qb.andWhere("order.currency ILIKE :orderCurrency", { orderCurrency: `%${currency}%` });
8754
9229
  if (orderIdsFromPayment && orderIdsFromPayment.length) qb.andWhere("order.id IN (:...orderIds)", { orderIds: orderIdsFromPayment });
8755
9230
  const [rows, total2] = await qb.getManyAndCount();
8756
9231
  const data2 = rows.map((order) => {
@@ -8789,8 +9264,30 @@ function createCrudHandler(dataSource, entityMap, options) {
8789
9264
  if (statusFilter) qb.andWhere("payment.status = :status", { status: statusFilter });
8790
9265
  if (dateFrom) qb.andWhere("payment.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
8791
9266
  if (dateTo) qb.andWhere("payment.createdAt <= :dateTo", { dateTo: /* @__PURE__ */ new Date(dateTo + "T23:59:59.999Z") });
9267
+ const paidAtFrom = searchParams.get("paidAtFrom")?.trim();
9268
+ const paidAtTo = searchParams.get("paidAtTo")?.trim();
9269
+ if (paidAtFrom) {
9270
+ qb.andWhere("payment.paidAt >= :paidAtFrom", { paidAtFrom: /* @__PURE__ */ new Date(paidAtFrom + "T00:00:00.000Z") });
9271
+ }
9272
+ if (paidAtTo) {
9273
+ qb.andWhere("payment.paidAt <= :paidAtTo", { paidAtTo: /* @__PURE__ */ new Date(paidAtTo + "T23:59:59.999Z") });
9274
+ }
8792
9275
  if (methodFilter) qb.andWhere("payment.method = :method", { method: methodFilter });
8793
9276
  if (orderNumberParam) qb.andWhere("ord.orderNumber ILIKE :orderNumber", { orderNumber: `%${orderNumberParam}%` });
9277
+ const amountMin = searchParams.get("amountMin")?.trim();
9278
+ const amountMax = searchParams.get("amountMax")?.trim();
9279
+ if (amountMin) {
9280
+ const n = Number(amountMin);
9281
+ if (Number.isFinite(n)) qb.andWhere("payment.amount >= :amountMin", { amountMin: n });
9282
+ }
9283
+ if (amountMax) {
9284
+ const n = Number(amountMax);
9285
+ if (Number.isFinite(n)) qb.andWhere("payment.amount <= :amountMax", { amountMax: n });
9286
+ }
9287
+ const extRef = searchParams.get("externalReference")?.trim();
9288
+ if (extRef) {
9289
+ qb.andWhere("payment.externalReference ILIKE :extRef", { extRef: `%${extRef}%` });
9290
+ }
8794
9291
  const [rows, total2] = await qb.getManyAndCount();
8795
9292
  const data2 = rows.map((payment) => {
8796
9293
  const order = payment.order;
@@ -8809,18 +9306,36 @@ function createCrudHandler(dataSource, entityMap, options) {
8809
9306
  const repo2 = dataSource.getRepository(entity);
8810
9307
  const statusFilter = searchParams.get("status")?.trim();
8811
9308
  const inventory = searchParams.get("inventory")?.trim();
8812
- const productWhere = { deleted: false };
9309
+ const productWhere = {
9310
+ deleted: false,
9311
+ ...buildListFilterAndFromSearchParams(repo2, searchParams)
9312
+ };
8813
9313
  if (statusFilter) productWhere.status = statusFilter;
8814
9314
  if (inventory === "in_stock") productWhere.quantity = MoreThan2(0);
8815
9315
  if (inventory === "out_of_stock") productWhere.quantity = 0;
9316
+ for (const key of ["brandId", "categoryId", "collectionId"]) {
9317
+ const raw = searchParams.get(key)?.trim();
9318
+ if (raw) {
9319
+ const n = Number(raw);
9320
+ if (Number.isFinite(n)) productWhere[key] = n;
9321
+ }
9322
+ }
9323
+ const featuredRaw = searchParams.get("featured")?.trim();
9324
+ if (featuredRaw === "true" || featuredRaw === "false") {
9325
+ productWhere.featured = featuredRaw === "true";
9326
+ }
8816
9327
  if (search && typeof search === "string" && search.trim()) {
8817
9328
  productWhere.name = ILike2(`%${search.trim()}%`);
8818
9329
  }
9330
+ const productColumnNames = new Set(repo2.metadata.columns.map((c) => c.propertyName));
9331
+ const defaultProductSort = pickDefaultListSortField(productColumnNames, repo2.metadata.columns);
9332
+ const sortParam2 = (searchParams.get("sortField") ?? "").trim();
9333
+ const productSortField = sortParam2 && productColumnNames.has(sortParam2) ? sortParam2 : defaultProductSort;
8819
9334
  const [data2, total2] = await repo2.findAndCount({
8820
9335
  where: Object.keys(productWhere).length ? productWhere : void 0,
8821
9336
  skip,
8822
9337
  take: limit,
8823
- order: { [sortFieldRaw]: sortOrder }
9338
+ order: { [productSortField]: sortOrder }
8824
9339
  });
8825
9340
  return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
8826
9341
  }
@@ -8913,27 +9428,22 @@ function createCrudHandler(dataSource, entityMap, options) {
8913
9428
  }
8914
9429
  return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
8915
9430
  }
8916
- const sortField = columnNames.has(sortFieldRaw) ? sortFieldRaw : "createdAt";
9431
+ const defaultSortField = pickDefaultListSortField(columnNames, repo.metadata.columns);
9432
+ const sortParam = (searchParams.get("sortField") ?? "").trim();
9433
+ const sortField = sortParam && columnNames.has(sortParam) ? sortParam : defaultSortField;
8917
9434
  let where = {};
8918
9435
  if (search) {
8919
9436
  where = buildSearchWhereClause(repo, search);
8920
9437
  }
8921
- const intFilterKeys = ["productId", "attributeId", "taxId"];
8922
- const extraWhere = {};
8923
- for (const key of intFilterKeys) {
8924
- const v = searchParams.get(key);
8925
- if (v != null && v !== "" && columnNames.has(key)) {
8926
- const n = Number(v);
8927
- if (Number.isFinite(n)) extraWhere[key] = n;
8928
- }
8929
- }
8930
- if (Object.keys(extraWhere).length > 0) {
9438
+ where = mergeListWhereAnd(where, buildListFilterAndFromSearchParams(repo, searchParams));
9439
+ const exactParamWhere = buildExactListParamWhere(repo, searchParams);
9440
+ if (Object.keys(exactParamWhere).length > 0) {
8931
9441
  if (Array.isArray(where)) {
8932
- where = where.map((w) => ({ ...w, ...extraWhere }));
9442
+ where = where.map((w) => ({ ...w, ...exactParamWhere }));
8933
9443
  } else if (where && typeof where === "object" && Object.keys(where).length > 0) {
8934
- where = { ...where, ...extraWhere };
9444
+ where = { ...where, ...exactParamWhere };
8935
9445
  } else {
8936
- where = extraWhere;
9446
+ where = exactParamWhere;
8937
9447
  }
8938
9448
  }
8939
9449
  where = mergeDeletedFalseWhere(repo, where);
@@ -9013,6 +9523,10 @@ function createCrudHandler(dataSource, entityMap, options) {
9013
9523
  }
9014
9524
  const repo = dataSource.getRepository(entity);
9015
9525
  const persistBody = resource === "media" ? body : pickColumnUpdates(repo, body);
9526
+ if (resource === "contacts" && "type" in persistBody) {
9527
+ const t = persistBody.type;
9528
+ if (t === "" || t === "none" || t == null) persistBody.type = null;
9529
+ }
9016
9530
  if (resource !== "media" && Object.keys(persistBody).length === 0) {
9017
9531
  logCrudClientError("POST create", {
9018
9532
  reason: "no_scalar_columns_after_pick",
@@ -9035,6 +9549,17 @@ function createCrudHandler(dataSource, entityMap, options) {
9035
9549
  }
9036
9550
  }
9037
9551
  }
9552
+ if (resource === "addresses") {
9553
+ const cid = Number(persistBody.contactId);
9554
+ if (!Number.isFinite(cid)) {
9555
+ return json({ error: "Valid contactId is required." }, { status: 400 });
9556
+ }
9557
+ if (persistBody.tag === "") persistBody.tag = null;
9558
+ const addrErr = validateAndNormalizeAddressRow(persistBody);
9559
+ if (addrErr) {
9560
+ return json({ error: addrErr }, { status: 400 });
9561
+ }
9562
+ }
9038
9563
  sanitizeBodyForEntity(repo, persistBody);
9039
9564
  let created;
9040
9565
  try {
@@ -9352,10 +9877,60 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
9352
9877
  if (!cur) return json({ message: "Not found" }, { status: 404 });
9353
9878
  }
9354
9879
  const updatePayload = rawBody && typeof rawBody === "object" ? pickColumnUpdates(repo, rawBody) : {};
9880
+ if (resource === "contacts" && "type" in updatePayload) {
9881
+ const t = updatePayload.type;
9882
+ if (t === "" || t === "none" || t == null) updatePayload.type = null;
9883
+ }
9355
9884
  if (resource === "media") {
9356
9885
  const u = updatePayload;
9357
- delete u.parentId;
9358
9886
  delete u.kind;
9887
+ if (rawBody && typeof rawBody === "object" && "parentId" in rawBody) {
9888
+ let pid = null;
9889
+ const p = rawBody.parentId;
9890
+ if (p != null && p !== "") {
9891
+ const n = Number(p);
9892
+ if (!Number.isFinite(n)) {
9893
+ return json({ error: "Invalid parentId" }, { status: 400 });
9894
+ }
9895
+ pid = n;
9896
+ }
9897
+ if (pid != null) {
9898
+ const parent = await repo.findOne({
9899
+ where: { id: pid, deleted: false }
9900
+ });
9901
+ if (!parent || parent.kind !== "folder") {
9902
+ return json({ error: "parent must be a folder" }, { status: 400 });
9903
+ }
9904
+ }
9905
+ const row = await repo.findOne({
9906
+ where: { id: numericId, deleted: false }
9907
+ });
9908
+ if (!row) return json({ message: "Not found" }, { status: 404 });
9909
+ if (pid === numericId) {
9910
+ return json({ error: "Invalid parentId" }, { status: 400 });
9911
+ }
9912
+ if (row.kind === "folder" && pid != null) {
9913
+ let walk = pid;
9914
+ const seen = /* @__PURE__ */ new Set();
9915
+ while (walk != null) {
9916
+ if (walk === numericId) {
9917
+ return json(
9918
+ { error: "Cannot move a folder into itself or a descendant folder" },
9919
+ { status: 400 }
9920
+ );
9921
+ }
9922
+ if (seen.has(walk)) break;
9923
+ seen.add(walk);
9924
+ const anc = await repo.findOne({
9925
+ where: { id: walk, deleted: false }
9926
+ });
9927
+ walk = anc ? anc.parentId ?? null : null;
9928
+ }
9929
+ }
9930
+ u.parentId = pid;
9931
+ } else {
9932
+ delete u.parentId;
9933
+ }
9359
9934
  }
9360
9935
  if (resource === "products") {
9361
9936
  const currentRow = await repo.findOne({
@@ -9374,6 +9949,26 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
9374
9949
  updatePayload.sku = effSku;
9375
9950
  }
9376
9951
  }
9952
+ if (resource === "addresses" && Object.keys(updatePayload).length > 0) {
9953
+ const currentRow = await repo.findOne({
9954
+ where: { id: numericId }
9955
+ });
9956
+ if (!currentRow) return json({ message: "Not found" }, { status: 404 });
9957
+ const merged = {
9958
+ ...currentRow,
9959
+ ...updatePayload
9960
+ };
9961
+ if (merged.tag === "") merged.tag = null;
9962
+ const addrErr = validateAndNormalizeAddressRow(merged);
9963
+ if (addrErr) {
9964
+ return json({ error: addrErr }, { status: 400 });
9965
+ }
9966
+ for (const k of Object.keys(updatePayload)) {
9967
+ if (k in merged) {
9968
+ updatePayload[k] = merged[k];
9969
+ }
9970
+ }
9971
+ }
9377
9972
  if (Object.keys(updatePayload).length > 0) {
9378
9973
  sanitizeBodyForEntity(repo, updatePayload);
9379
9974
  await repo.update(numericId, updatePayload);
@@ -10545,7 +11140,7 @@ function createCmsApiHandler(config) {
10545
11140
  } : usersApi;
10546
11141
  const usersHandlers = usersApiMerged ? createUsersApiHandlers(mergePerm(usersApiMerged) ?? usersApiMerged) : null;
10547
11142
  const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
10548
- const profilePut = userProfile ? createUserProfileHandler(userProfile) : null;
11143
+ const profileHandlers = userProfile ? createUserProfileHandler(userProfile) : null;
10549
11144
  const settingsHandlers = settingsConfig ? createSettingsApiHandlers(settingsConfig) : null;
10550
11145
  const smsMessageTemplateHandlers = createSmsMessageTemplateHandlers({
10551
11146
  dataSource,
@@ -10644,7 +11239,10 @@ function createCmsApiHandler(config) {
10644
11239
  }
10645
11240
  if (path.length === 2) {
10646
11241
  if (path[1] === "avatar" && m === "POST" && avatarPost) return avatarPost(req);
10647
- if (path[1] === "profile" && m === "PUT" && profilePut) return profilePut(req);
11242
+ if (path[1] === "profile" && profileHandlers) {
11243
+ if (m === "GET") return profileHandlers.GET(req);
11244
+ if (m === "PUT") return profileHandlers.PUT(req);
11245
+ }
10648
11246
  const id = path[1];
10649
11247
  if (m === "GET") return usersHandlers.getById(req, id);
10650
11248
  if (m === "PUT" || m === "PATCH") return usersHandlers.update(req, id);
@@ -11375,18 +11973,19 @@ function createStorefrontApiHandler(config) {
11375
11973
  const contactOrErr = await getContactForAddresses();
11376
11974
  if (contactOrErr instanceof Response) return contactOrErr;
11377
11975
  const b = await req.json().catch(() => ({}));
11378
- const created = await addressRepo().save(
11379
- addressRepo().create({
11380
- contactId: contactOrErr.contactId,
11381
- tag: typeof b.tag === "string" ? b.tag.trim() || null : null,
11382
- line1: typeof b.line1 === "string" ? b.line1.trim() || null : null,
11383
- line2: typeof b.line2 === "string" ? b.line2.trim() || null : null,
11384
- city: typeof b.city === "string" ? b.city.trim() || null : null,
11385
- state: typeof b.state === "string" ? b.state.trim() || null : null,
11386
- postalCode: typeof b.postalCode === "string" ? b.postalCode.trim() || null : null,
11387
- country: typeof b.country === "string" ? b.country.trim() || null : null
11388
- })
11389
- );
11976
+ const row = {
11977
+ contactId: contactOrErr.contactId,
11978
+ tag: typeof b.tag === "string" ? b.tag.trim() || null : null,
11979
+ line1: typeof b.line1 === "string" ? b.line1 : "",
11980
+ line2: typeof b.line2 === "string" ? b.line2.trim() || null : null,
11981
+ city: typeof b.city === "string" ? b.city : "",
11982
+ state: typeof b.state === "string" ? b.state : "",
11983
+ postalCode: typeof b.postalCode === "string" ? b.postalCode : "",
11984
+ country: typeof b.country === "string" ? b.country : ""
11985
+ };
11986
+ const addrErr = validateAndNormalizeAddressRow(row);
11987
+ if (addrErr) return json({ error: addrErr }, { status: 400 });
11988
+ const created = await addressRepo().save(addressRepo().create(row));
11390
11989
  return json(serializeAddress2(created));
11391
11990
  }
11392
11991
  if (path[0] === "addresses" && path.length === 2 && (method === "PATCH" || method === "PUT")) {
@@ -11405,7 +12004,16 @@ function createStorefrontApiHandler(config) {
11405
12004
  if (b.state !== void 0) updates.state = typeof b.state === "string" ? b.state.trim() || null : null;
11406
12005
  if (b.postalCode !== void 0) updates.postalCode = typeof b.postalCode === "string" ? b.postalCode.trim() || null : null;
11407
12006
  if (b.country !== void 0) updates.country = typeof b.country === "string" ? b.country.trim() || null : null;
11408
- if (Object.keys(updates).length) await addressRepo().update(id, updates);
12007
+ if (Object.keys(updates).length) {
12008
+ const merged = { ...existing, ...updates };
12009
+ if (merged.tag === "") merged.tag = null;
12010
+ const addrErr = validateAndNormalizeAddressRow(merged);
12011
+ if (addrErr) return json({ error: addrErr }, { status: 400 });
12012
+ for (const k of Object.keys(updates)) {
12013
+ if (k in merged) updates[k] = merged[k];
12014
+ }
12015
+ await addressRepo().update(id, updates);
12016
+ }
11409
12017
  const updated = await addressRepo().findOne({ where: { id } });
11410
12018
  return json(serializeAddress2(updated));
11411
12019
  }