@infuro/cms-core 1.0.22 → 1.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.js CHANGED
@@ -406,7 +406,7 @@ var init_paid_order_erp = __esm({
406
406
  });
407
407
 
408
408
  // src/api/crud.ts
409
- import { Brackets, ILike, MoreThan } from "typeorm";
409
+ import { ILike, MoreThan, Not } from "typeorm";
410
410
 
411
411
  // src/plugins/erp/erp-contact-sync.ts
412
412
  init_erp_queue();
@@ -476,6 +476,64 @@ async function queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, produc
476
476
  }
477
477
  }
478
478
 
479
+ // src/lib/address-geo-validation.ts
480
+ import { Country, State, City } from "country-state-city";
481
+ function norm(s) {
482
+ return typeof s === "string" ? s.trim() : "";
483
+ }
484
+ function resolveCountry(input) {
485
+ const t = input.trim();
486
+ if (!t) return void 0;
487
+ if (t.length === 2) {
488
+ const byCode = Country.getCountryByCode(t.toUpperCase());
489
+ if (byCode) return byCode;
490
+ }
491
+ const lower = t.toLowerCase();
492
+ return Country.getAllCountries().find((c) => c.name.toLowerCase() === lower);
493
+ }
494
+ function resolveState(countryIso, input) {
495
+ const t = input.trim();
496
+ if (!t || !countryIso) return void 0;
497
+ const states = State.getStatesOfCountry(countryIso);
498
+ const lower = t.toLowerCase();
499
+ return states.find((s) => s.isoCode.toLowerCase() === t.toLowerCase() || s.name.toLowerCase() === lower);
500
+ }
501
+ function resolveCity(countryIso, stateIso, input) {
502
+ const t = input.trim();
503
+ if (!t || !countryIso || !stateIso) return void 0;
504
+ const lower = t.toLowerCase();
505
+ const cities = City.getCitiesOfState(countryIso, stateIso);
506
+ return cities.find((c) => c.name.toLowerCase() === lower);
507
+ }
508
+ function assertValidAddressHierarchy(country, state, city) {
509
+ const c = resolveCountry(country);
510
+ if (!c) return { ok: false, error: "Invalid or unknown country." };
511
+ const st = resolveState(c.isoCode, state);
512
+ if (!st) return { ok: false, error: "State or province does not match the selected country." };
513
+ const ct = resolveCity(c.isoCode, st.isoCode, city);
514
+ if (!ct) return { ok: false, error: "City does not match the selected state." };
515
+ return { ok: true, country: c.name, state: st.name, city: ct.name };
516
+ }
517
+ function validateAndNormalizeAddressRow(row) {
518
+ const line1 = norm(row.line1);
519
+ const postalCode = norm(row.postalCode);
520
+ const countryIn = norm(row.country);
521
+ const stateIn = norm(row.state);
522
+ const cityIn = norm(row.city);
523
+ if (!line1) return "Street address (line 1) is required.";
524
+ if (!postalCode) return "Postal code is required.";
525
+ if (!countryIn || !stateIn || !cityIn) return "Country, state, and city are required.";
526
+ const geo = assertValidAddressHierarchy(countryIn, stateIn, cityIn);
527
+ if (!geo.ok) return geo.error;
528
+ row.line1 = line1;
529
+ row.line2 = norm(row.line2) || null;
530
+ row.postalCode = postalCode;
531
+ row.country = geo.country;
532
+ row.state = geo.state;
533
+ row.city = geo.city;
534
+ return null;
535
+ }
536
+
479
537
  // src/api/crud.ts
480
538
  var CRUD_LOG = "[cms-crud]";
481
539
  function logCrudClientError(op, detail) {
@@ -484,6 +542,40 @@ function logCrudClientError(op, detail) {
484
542
  function logCrudServerError(op, detail) {
485
543
  console.error(CRUD_LOG, op, detail);
486
544
  }
545
+ async function aggregateMediaFolderFileSizes(dataSource, folderIds) {
546
+ const map = /* @__PURE__ */ new Map();
547
+ if (folderIds.length === 0) return map;
548
+ try {
549
+ const rows = await dataSource.query(
550
+ `
551
+ WITH RECURSIVE walk AS (
552
+ SELECT m."parentId" AS root_id, m.id, m.kind, m.size
553
+ FROM media m
554
+ WHERE m."parentId" = ANY($1) AND m.deleted = false
555
+ UNION ALL
556
+ SELECT w.root_id, m.id, m.kind, m.size
557
+ FROM walk w
558
+ INNER JOIN media m ON m."parentId" = w.id AND m.deleted = false
559
+ )
560
+ SELECT root_id AS "rootId", COALESCE(SUM(size) FILTER (WHERE kind = 'file'), 0)::bigint AS "totalSize"
561
+ FROM walk
562
+ GROUP BY root_id
563
+ `,
564
+ [folderIds]
565
+ );
566
+ for (const r of rows) {
567
+ map.set(Number(r.rootId), Number(r.totalSize));
568
+ }
569
+ for (const id of folderIds) {
570
+ if (!map.has(id)) map.set(id, 0);
571
+ }
572
+ } catch (err) {
573
+ logCrudServerError("media folder size aggregate failed", {
574
+ message: err instanceof Error ? err.message : String(err)
575
+ });
576
+ }
577
+ return map;
578
+ }
487
579
  var DATE_COLUMN_TYPES = /* @__PURE__ */ new Set([
488
580
  "date",
489
581
  "datetime",
@@ -548,6 +640,16 @@ function mergeDeletedFalseWhere(repo, where) {
548
640
  }
549
641
  return Object.keys(where).length > 0 ? { ...where, ...d } : d;
550
642
  }
643
+ function normalizeProductSku(value) {
644
+ if (value == null) return null;
645
+ const s = String(value).trim();
646
+ return s === "" ? null : s;
647
+ }
648
+ async function assertProductSkuUnique(repo, sku, excludeId) {
649
+ const where = excludeId != null ? { sku, deleted: false, id: Not(excludeId) } : { sku, deleted: false };
650
+ const row = await repo.findOne({ where });
651
+ return row == null;
652
+ }
551
653
  function buildSoftDeletePayload(meta, deletedBy) {
552
654
  const payload = { deleted: true };
553
655
  if (meta.columns.some((c) => c.propertyName === "deletedAt")) {
@@ -702,6 +804,17 @@ function createCrudHandler(dataSource, entityMap, options) {
702
804
  if (statusFilter) productWhere.status = statusFilter;
703
805
  if (inventory === "in_stock") productWhere.quantity = MoreThan(0);
704
806
  if (inventory === "out_of_stock") productWhere.quantity = 0;
807
+ for (const key of ["brandId", "categoryId", "collectionId"]) {
808
+ const raw = searchParams.get(key)?.trim();
809
+ if (raw) {
810
+ const n = Number(raw);
811
+ if (Number.isFinite(n)) productWhere[key] = n;
812
+ }
813
+ }
814
+ const featuredRaw = searchParams.get("featured")?.trim();
815
+ if (featuredRaw === "true" || featuredRaw === "false") {
816
+ productWhere.featured = featuredRaw === "true";
817
+ }
705
818
  if (search && typeof search === "string" && search.trim()) {
706
819
  productWhere.name = ILike(`%${search.trim()}%`);
707
820
  }
@@ -770,19 +883,36 @@ function createCrudHandler(dataSource, entityMap, options) {
770
883
  qb.andWhere("m.filename ILIKE :search", { search: `%${search.trim()}%` });
771
884
  }
772
885
  if (typeFilter) {
773
- qb.andWhere(
774
- new Brackets((sq) => {
775
- sq.where("m.kind = :folderKind", { folderKind: "folder" }).orWhere("m.mimeType LIKE :mtp", {
776
- mtp: `${typeFilter}/%`
777
- });
778
- })
779
- );
886
+ if (typeFilter === "folder") {
887
+ qb.andWhere("m.kind = :folderKind", { folderKind: "folder" });
888
+ } else if (typeFilter === "file") {
889
+ qb.andWhere("m.kind = :fileKind", { fileKind: "file" });
890
+ } else if (typeFilter === "image") {
891
+ qb.andWhere("m.mimeType LIKE :imageMimeType", { imageMimeType: "image/%" });
892
+ } else if (typeFilter === "video") {
893
+ qb.andWhere("m.mimeType LIKE :videoMimeType", { videoMimeType: "video/%" });
894
+ } else if (typeFilter === "audio") {
895
+ qb.andWhere("m.mimeType LIKE :audioMimeType", { audioMimeType: "audio/%" });
896
+ } else if (typeFilter === "Document") {
897
+ qb.andWhere("m.mimeType LIKE :documentMimeType", { documentMimeType: "application/pdf" });
898
+ } else if (typeFilter === "application") {
899
+ qb.andWhere("m.kind = :folderKind", { folderKind: "folder" });
900
+ }
780
901
  }
781
902
  const allowedSort = ["filename", "createdAt", "id"];
782
903
  const sf = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "filename";
783
904
  const so = sortOrder === "DESC" ? "DESC" : "ASC";
784
- qb.orderBy("CASE WHEN m.kind = :fk THEN 0 ELSE 1 END", "ASC").addOrderBy(`m.${sf}`, so).setParameter("fk", "folder").skip(skip).take(limit);
785
- const [data2, total2] = await qb.getManyAndCount();
905
+ qb.orderBy(`m.${sf}`, so).skip(skip).take(limit);
906
+ const [rows, total2] = await qb.getManyAndCount();
907
+ const mediaRows = rows;
908
+ const folderIds = mediaRows.filter((m) => m.kind === "folder").map((m) => m.id);
909
+ let data2 = rows;
910
+ if (folderIds.length > 0) {
911
+ const sizeMap = await aggregateMediaFolderFileSizes(dataSource, folderIds);
912
+ data2 = mediaRows.map(
913
+ (m) => m.kind === "folder" ? { ...m, size: sizeMap.get(m.id) ?? 0 } : m
914
+ );
915
+ }
786
916
  return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
787
917
  }
788
918
  const sortField = columnNames.has(sortFieldRaw) ? sortFieldRaw : "createdAt";
@@ -790,7 +920,7 @@ function createCrudHandler(dataSource, entityMap, options) {
790
920
  if (search) {
791
921
  where = buildSearchWhereClause(repo, search);
792
922
  }
793
- const intFilterKeys = ["productId", "attributeId", "taxId"];
923
+ const intFilterKeys = ["productId", "attributeId", "taxId", "brandId", "categoryId", "collectionId", "parentId"];
794
924
  const extraWhere = {};
795
925
  for (const key of intFilterKeys) {
796
926
  const v = searchParams.get(key);
@@ -799,6 +929,15 @@ function createCrudHandler(dataSource, entityMap, options) {
799
929
  if (Number.isFinite(n)) extraWhere[key] = n;
800
930
  }
801
931
  }
932
+ for (const col of repo.metadata.columns) {
933
+ if (String(col.type) !== "boolean") continue;
934
+ const name = col.propertyName;
935
+ if (!columnNames.has(name)) continue;
936
+ const raw = searchParams.get(name)?.trim();
937
+ if (raw === "true" || raw === "false") {
938
+ extraWhere[name] = raw === "true";
939
+ }
940
+ }
802
941
  if (Object.keys(extraWhere).length > 0) {
803
942
  if (Array.isArray(where)) {
804
943
  where = where.map((w) => ({ ...w, ...extraWhere }));
@@ -893,6 +1032,31 @@ function createCrudHandler(dataSource, entityMap, options) {
893
1032
  });
894
1033
  return json({ error: "Invalid request payload" }, { status: 400 });
895
1034
  }
1035
+ if (resource === "products") {
1036
+ if ("sku" in persistBody) {
1037
+ const skuNorm = normalizeProductSku(persistBody.sku);
1038
+ if (skuNorm) {
1039
+ const ok = await assertProductSkuUnique(repo, skuNorm);
1040
+ if (!ok) {
1041
+ return json({ error: "SKU already exists. Please use a unique SKU." }, { status: 400 });
1042
+ }
1043
+ persistBody.sku = skuNorm;
1044
+ } else {
1045
+ persistBody.sku = null;
1046
+ }
1047
+ }
1048
+ }
1049
+ if (resource === "addresses") {
1050
+ const cid = Number(persistBody.contactId);
1051
+ if (!Number.isFinite(cid)) {
1052
+ return json({ error: "Valid contactId is required." }, { status: 400 });
1053
+ }
1054
+ if (persistBody.tag === "") persistBody.tag = null;
1055
+ const addrErr = validateAndNormalizeAddressRow(persistBody);
1056
+ if (addrErr) {
1057
+ return json({ error: addrErr }, { status: 400 });
1058
+ }
1059
+ }
896
1060
  sanitizeBodyForEntity(repo, persistBody);
897
1061
  let created;
898
1062
  try {
@@ -1087,7 +1251,20 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
1087
1251
  relations: ["order", "order.contact", "contact"]
1088
1252
  });
1089
1253
  if (!payment) return json({ message: "Not found" }, { status: 404 });
1090
- return json(payment);
1254
+ const p = payment;
1255
+ const order = p.order;
1256
+ const orderContact = order?.contact;
1257
+ const contact = p.contact;
1258
+ const customer = orderContact ?? contact;
1259
+ return json({
1260
+ ...p,
1261
+ order: order ? {
1262
+ id: order.id,
1263
+ orderNumber: order.orderNumber,
1264
+ contact: orderContact ? { name: orderContact.name, email: orderContact.email } : null
1265
+ } : null,
1266
+ contact: customer ? { id: customer.id, name: customer.name, email: customer.email } : null
1267
+ });
1091
1268
  }
1092
1269
  if (resource === "blogs") {
1093
1270
  const blog = await repo.findOne({
@@ -1199,8 +1376,91 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
1199
1376
  const updatePayload = rawBody && typeof rawBody === "object" ? pickColumnUpdates(repo, rawBody) : {};
1200
1377
  if (resource === "media") {
1201
1378
  const u = updatePayload;
1202
- delete u.parentId;
1203
1379
  delete u.kind;
1380
+ if (rawBody && typeof rawBody === "object" && "parentId" in rawBody) {
1381
+ let pid = null;
1382
+ const p = rawBody.parentId;
1383
+ if (p != null && p !== "") {
1384
+ const n = Number(p);
1385
+ if (!Number.isFinite(n)) {
1386
+ return json({ error: "Invalid parentId" }, { status: 400 });
1387
+ }
1388
+ pid = n;
1389
+ }
1390
+ if (pid != null) {
1391
+ const parent = await repo.findOne({
1392
+ where: { id: pid, deleted: false }
1393
+ });
1394
+ if (!parent || parent.kind !== "folder") {
1395
+ return json({ error: "parent must be a folder" }, { status: 400 });
1396
+ }
1397
+ }
1398
+ const row = await repo.findOne({
1399
+ where: { id: numericId, deleted: false }
1400
+ });
1401
+ if (!row) return json({ message: "Not found" }, { status: 404 });
1402
+ if (pid === numericId) {
1403
+ return json({ error: "Invalid parentId" }, { status: 400 });
1404
+ }
1405
+ if (row.kind === "folder" && pid != null) {
1406
+ let walk = pid;
1407
+ const seen = /* @__PURE__ */ new Set();
1408
+ while (walk != null) {
1409
+ if (walk === numericId) {
1410
+ return json(
1411
+ { error: "Cannot move a folder into itself or a descendant folder" },
1412
+ { status: 400 }
1413
+ );
1414
+ }
1415
+ if (seen.has(walk)) break;
1416
+ seen.add(walk);
1417
+ const anc = await repo.findOne({
1418
+ where: { id: walk, deleted: false }
1419
+ });
1420
+ walk = anc ? anc.parentId ?? null : null;
1421
+ }
1422
+ }
1423
+ u.parentId = pid;
1424
+ } else {
1425
+ delete u.parentId;
1426
+ }
1427
+ }
1428
+ if (resource === "products") {
1429
+ const currentRow = await repo.findOne({
1430
+ where: { id: numericId, deleted: false }
1431
+ });
1432
+ if (!currentRow) return json({ message: "Not found" }, { status: 404 });
1433
+ const merged = { ...currentRow, ...updatePayload };
1434
+ const effSku = normalizeProductSku(merged.sku);
1435
+ if (effSku) {
1436
+ const ok = await assertProductSkuUnique(repo, effSku, numericId);
1437
+ if (!ok) {
1438
+ return json({ error: "SKU already exists. Please use a unique SKU." }, { status: 400 });
1439
+ }
1440
+ }
1441
+ if ("sku" in updatePayload) {
1442
+ updatePayload.sku = effSku;
1443
+ }
1444
+ }
1445
+ if (resource === "addresses" && Object.keys(updatePayload).length > 0) {
1446
+ const currentRow = await repo.findOne({
1447
+ where: { id: numericId }
1448
+ });
1449
+ if (!currentRow) return json({ message: "Not found" }, { status: 404 });
1450
+ const merged = {
1451
+ ...currentRow,
1452
+ ...updatePayload
1453
+ };
1454
+ if (merged.tag === "") merged.tag = null;
1455
+ const addrErr = validateAndNormalizeAddressRow(merged);
1456
+ if (addrErr) {
1457
+ return json({ error: addrErr }, { status: 400 });
1458
+ }
1459
+ for (const k of Object.keys(updatePayload)) {
1460
+ if (k in merged) {
1461
+ updatePayload[k] = merged[k];
1462
+ }
1463
+ }
1204
1464
  }
1205
1465
  if (Object.keys(updatePayload).length > 0) {
1206
1466
  sanitizeBodyForEntity(repo, updatePayload);
@@ -1231,6 +1491,11 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
1231
1491
  where: { id: numericId, deleted: false }
1232
1492
  });
1233
1493
  if (!existing) return json({ message: "Not found" }, { status: 404 });
1494
+ if (resource === "contacts") {
1495
+ const result2 = await repo.delete(numericId);
1496
+ if (result2.affected === 0) return json({ message: "Not found" }, { status: 404 });
1497
+ return json({ message: "Deleted successfully" }, { status: 200 });
1498
+ }
1234
1499
  let deletedBy = null;
1235
1500
  if (getDeletedByUserId) {
1236
1501
  try {
@@ -1550,11 +1815,11 @@ async function readBufferFromPublicUrl(url) {
1550
1815
  throw new Error("Unsupported media URL");
1551
1816
  }
1552
1817
  function sanitizeZipPath(entryName) {
1553
- const norm = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
1554
- for (const seg of norm) {
1818
+ const norm2 = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
1819
+ for (const seg of norm2) {
1555
1820
  if (seg === ".." || seg === ".") return null;
1556
1821
  }
1557
- return norm;
1822
+ return norm2;
1558
1823
  }
1559
1824
  function shouldSkipEntry(parts) {
1560
1825
  if (parts[0] === "__MACOSX") return true;
@@ -1778,12 +2043,14 @@ function createDashboardStatsHandler(config) {
1778
2043
  const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3);
1779
2044
  const repo = (name) => entityMap[name] ? dataSource.getRepository(entityMap[name]) : void 0;
1780
2045
  const [contactsCount, formsCount, formSubmissionsCount, usersCount, blogsCount, recentContacts, recentSubmissions, contactTypeRows] = await Promise.all([
1781
- repo("contacts")?.count() ?? 0,
2046
+ repo("contacts")?.count({ where: { deleted: false } }) ?? 0,
1782
2047
  repo("forms")?.count({ where: { deleted: false } }) ?? 0,
1783
2048
  repo("form_submissions")?.count() ?? 0,
1784
2049
  repo("users")?.count({ where: { deleted: false } }) ?? 0,
1785
2050
  repo("blogs")?.count({ where: { deleted: false } }) ?? 0,
1786
- repo("contacts")?.count({ where: { createdAt: MoreThanOrEqual(sevenDaysAgo) } }) ?? 0,
2051
+ repo("contacts")?.count({
2052
+ where: { deleted: false, createdAt: MoreThanOrEqual(sevenDaysAgo) }
2053
+ }) ?? 0,
1787
2054
  repo("form_submissions")?.count({ where: { createdAt: MoreThanOrEqual(sevenDaysAgo) } }) ?? 0,
1788
2055
  repo("contacts")?.createQueryBuilder("c").select("COALESCE(NULLIF(TRIM(c.type), ''), 'unknown')", "type").addSelect("COUNT(*)", "count").where("c.deleted = :deleted", { deleted: false }).groupBy("COALESCE(NULLIF(TRIM(c.type), ''), 'unknown')").getRawMany() ?? []
1789
2056
  ]);
@@ -2416,11 +2683,14 @@ function createFormSubmissionHandler(config) {
2416
2683
  const contactRepo = dataSource.getRepository(entityMap.contacts);
2417
2684
  let contact = await contactRepo.findOne({ where: { email: contactData.email } });
2418
2685
  if (!contact) {
2686
+ const createdAt = /* @__PURE__ */ new Date();
2419
2687
  contact = await contactRepo.save(
2420
2688
  contactRepo.create({
2421
2689
  name: contactData.name,
2422
2690
  email: contactData.email,
2423
- phone: contactData.phone
2691
+ phone: contactData.phone,
2692
+ createdAt,
2693
+ updatedAt: createdAt
2424
2694
  })
2425
2695
  );
2426
2696
  }
@@ -2576,12 +2846,13 @@ function createUsersApiHandlers(config) {
2576
2846
  const gid = body.groupId ?? null;
2577
2847
  const isCustomer = !!(customerG && gid === customerG.id);
2578
2848
  const adminAccess = isCustomer ? false : body.adminAccess === false ? false : true;
2849
+ const blocked = body.blocked === true || body.blocked === "true" || body.blocked === 1 || body.blocked === "1";
2579
2850
  const newUser = await userRepo().save(
2580
2851
  userRepo().create({
2581
2852
  name: body.name,
2582
2853
  email: body.email,
2583
2854
  password: null,
2584
- blocked: true,
2855
+ blocked,
2585
2856
  groupId: gid,
2586
2857
  adminAccess
2587
2858
  })
@@ -2596,7 +2867,14 @@ function createUsersApiHandlers(config) {
2596
2867
  inviteLink,
2597
2868
  newUser.name ?? ""
2598
2869
  );
2599
- return json({ message: "User created successfully (blocked until password is set)", user: newUser, inviteLink }, { status: 201 });
2870
+ return json(
2871
+ {
2872
+ message: blocked ? "User created successfully (blocked until password is set)" : "User created successfully",
2873
+ user: newUser,
2874
+ inviteLink
2875
+ },
2876
+ { status: 201 }
2877
+ );
2600
2878
  } catch {
2601
2879
  return json({ error: "Server Error" }, { status: 500 });
2602
2880
  }
@@ -2898,7 +3176,10 @@ function createChatHandlers(config) {
2898
3176
  const existing = await repo.findOne({ where: { email } });
2899
3177
  let contact;
2900
3178
  if (!existing) {
2901
- contact = await repo.save(repo.create({ name, email, phone }));
3179
+ const createdAt = /* @__PURE__ */ new Date();
3180
+ contact = await repo.save(
3181
+ repo.create({ name, email, phone, createdAt, updatedAt: createdAt })
3182
+ );
2902
3183
  } else {
2903
3184
  const row = existing;
2904
3185
  if (row.deleted) {
@@ -7920,18 +8201,19 @@ function createStorefrontApiHandler(config) {
7920
8201
  const contactOrErr = await getContactForAddresses();
7921
8202
  if (contactOrErr instanceof Response) return contactOrErr;
7922
8203
  const b = await req.json().catch(() => ({}));
7923
- const created = await addressRepo().save(
7924
- addressRepo().create({
7925
- contactId: contactOrErr.contactId,
7926
- tag: typeof b.tag === "string" ? b.tag.trim() || null : null,
7927
- line1: typeof b.line1 === "string" ? b.line1.trim() || null : null,
7928
- line2: typeof b.line2 === "string" ? b.line2.trim() || null : null,
7929
- city: typeof b.city === "string" ? b.city.trim() || null : null,
7930
- state: typeof b.state === "string" ? b.state.trim() || null : null,
7931
- postalCode: typeof b.postalCode === "string" ? b.postalCode.trim() || null : null,
7932
- country: typeof b.country === "string" ? b.country.trim() || null : null
7933
- })
7934
- );
8204
+ const row = {
8205
+ contactId: contactOrErr.contactId,
8206
+ tag: typeof b.tag === "string" ? b.tag.trim() || null : null,
8207
+ line1: typeof b.line1 === "string" ? b.line1 : "",
8208
+ line2: typeof b.line2 === "string" ? b.line2.trim() || null : null,
8209
+ city: typeof b.city === "string" ? b.city : "",
8210
+ state: typeof b.state === "string" ? b.state : "",
8211
+ postalCode: typeof b.postalCode === "string" ? b.postalCode : "",
8212
+ country: typeof b.country === "string" ? b.country : ""
8213
+ };
8214
+ const addrErr = validateAndNormalizeAddressRow(row);
8215
+ if (addrErr) return json({ error: addrErr }, { status: 400 });
8216
+ const created = await addressRepo().save(addressRepo().create(row));
7935
8217
  return json(serializeAddress2(created));
7936
8218
  }
7937
8219
  if (path[0] === "addresses" && path.length === 2 && (method === "PATCH" || method === "PUT")) {
@@ -7950,7 +8232,16 @@ function createStorefrontApiHandler(config) {
7950
8232
  if (b.state !== void 0) updates.state = typeof b.state === "string" ? b.state.trim() || null : null;
7951
8233
  if (b.postalCode !== void 0) updates.postalCode = typeof b.postalCode === "string" ? b.postalCode.trim() || null : null;
7952
8234
  if (b.country !== void 0) updates.country = typeof b.country === "string" ? b.country.trim() || null : null;
7953
- if (Object.keys(updates).length) await addressRepo().update(id, updates);
8235
+ if (Object.keys(updates).length) {
8236
+ const merged = { ...existing, ...updates };
8237
+ if (merged.tag === "") merged.tag = null;
8238
+ const addrErr = validateAndNormalizeAddressRow(merged);
8239
+ if (addrErr) return json({ error: addrErr }, { status: 400 });
8240
+ for (const k of Object.keys(updates)) {
8241
+ if (k in merged) updates[k] = merged[k];
8242
+ }
8243
+ await addressRepo().update(id, updates);
8244
+ }
7954
8245
  const updated = await addressRepo().findOne({ where: { id } });
7955
8246
  return json(serializeAddress2(updated));
7956
8247
  }