@infuro/cms-core 1.0.43 → 1.0.45

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.
@@ -10,7 +10,7 @@ import { permissionRowsToRecord, logRbac, isSuperAdmin, vendorPortalFlagsFromUse
10
10
  import { queueErp } from './chunk-SYBOCAWB.js';
11
11
  import { isErpIntegrationEnabled } from './chunk-JC6DLWTE.js';
12
12
  import { __name } from './chunk-SHUYVCID.js';
13
- import { PrimaryGeneratedColumn, Column, Entity, ManyToOne, JoinColumn, OneToMany, Index, ManyToMany, JoinTable, Unique, CreateDateColumn, UpdateDateColumn, In, IsNull, ILike, Between, MoreThanOrEqual, LessThanOrEqual, Not, MoreThan } from 'typeorm';
13
+ import { PrimaryGeneratedColumn, Column, Entity, ManyToOne, JoinColumn, OneToMany, Index, ManyToMany, JoinTable, Unique, CreateDateColumn, UpdateDateColumn, In, ILike, Between, MoreThanOrEqual, LessThanOrEqual, Not, MoreThan, IsNull } from 'typeorm';
14
14
  import { Country, State, City } from 'country-state-city';
15
15
  import crypto2, { randomBytes, randomUUID, createHmac, timingSafeEqual, randomInt } from 'crypto';
16
16
  import Parser from 'rss-parser';
@@ -36,6 +36,7 @@ async function queryVendorLinks(dataSource, userId) {
36
36
  vr."name" AS "vendorRoleName",
37
37
  vr."isOwnerRole" AS "isOwnerRole"
38
38
  FROM "vendor_users" vu
39
+ INNER JOIN "vendors" v ON v.id = vu."vendorId" AND v.deleted = false
39
40
  LEFT JOIN "vendor_roles" vr ON vr.id = vu."vendorRoleId" AND vr.deleted = false
40
41
  WHERE vu."userId" = $1
41
42
  ORDER BY vu.id ASC
@@ -45,10 +46,11 @@ async function queryVendorLinks(dataSource, userId) {
45
46
  } catch (err) {
46
47
  if (!isMissingVendorRoleSchemaError(err)) throw err;
47
48
  const legacy = await dataSource.query(`
48
- SELECT "vendorId", "role"
49
- FROM "vendor_users"
50
- WHERE "userId" = $1
51
- ORDER BY id ASC
49
+ SELECT vu."vendorId" AS "vendorId", vu."role" AS "role"
50
+ FROM "vendor_users" vu
51
+ INNER JOIN "vendors" v ON v.id = vu."vendorId" AND v.deleted = false
52
+ WHERE vu."userId" = $1
53
+ ORDER BY vu.id ASC
52
54
  `, [
53
55
  userId
54
56
  ]);
@@ -62,8 +64,36 @@ async function queryVendorLinks(dataSource, userId) {
62
64
  }
63
65
  }
64
66
  __name(queryVendorLinks, "queryVendorLinks");
67
+ async function queryOwnedVendorIds(dataSource, userId) {
68
+ try {
69
+ const rows = await dataSource.query(`
70
+ SELECT id
71
+ FROM "vendors"
72
+ WHERE "userId" = $1 AND deleted = false
73
+ ORDER BY id ASC
74
+ `, [
75
+ userId
76
+ ]);
77
+ return rows.map((r) => Number(r.id)).filter((id) => Number.isFinite(id) && id > 0);
78
+ } catch {
79
+ return [];
80
+ }
81
+ }
82
+ __name(queryOwnedVendorIds, "queryOwnedVendorIds");
65
83
  async function loadUserVendorContext(dataSource, userId, preferredVendorId) {
66
84
  const rows = await queryVendorLinks(dataSource, userId);
85
+ const linkIds = new Set(rows.map((r) => Number(r.vendorId)));
86
+ for (const ownedId of await queryOwnedVendorIds(dataSource, userId)) {
87
+ if (linkIds.has(ownedId)) continue;
88
+ rows.push({
89
+ vendorId: ownedId,
90
+ role: "owner",
91
+ vendorRoleId: null,
92
+ vendorRoleName: null,
93
+ isOwnerRole: true
94
+ });
95
+ linkIds.add(ownedId);
96
+ }
67
97
  const vendorIds = rows.map((r) => Number(r.vendorId));
68
98
  const activeRow = preferredVendorId != null ? rows.find((r) => Number(r.vendorId) === preferredVendorId) : void 0;
69
99
  const primary = activeRow ?? rows[0];
@@ -452,6 +482,41 @@ function applyApprovalStatusSideEffects(updatePayload, opts) {
452
482
  }
453
483
  }
454
484
  __name(applyApprovalStatusSideEffects, "applyApprovalStatusSideEffects");
485
+ async function syncProductVariantsStatusWithProduct(dataSource, entityMap, productId, productStatus) {
486
+ if (!entityMap.product_variants || !Number.isFinite(productId) || productId < 1) return;
487
+ const status = String(productStatus || "draft");
488
+ const repo = dataSource.getRepository(entityMap.product_variants);
489
+ if (status === "available") {
490
+ await repo.createQueryBuilder().update().set({
491
+ status: "available"
492
+ }).where('"productId" = :productId', {
493
+ productId
494
+ }).andWhere("status = :from", {
495
+ from: "draft"
496
+ }).execute();
497
+ return;
498
+ }
499
+ if (status === "draft") {
500
+ await repo.createQueryBuilder().update().set({
501
+ status: "draft"
502
+ }).where('"productId" = :productId', {
503
+ productId
504
+ }).andWhere("status = :from", {
505
+ from: "available"
506
+ }).execute();
507
+ }
508
+ }
509
+ __name(syncProductVariantsStatusWithProduct, "syncProductVariantsStatusWithProduct");
510
+ function coerceVariantStatusForProduct(variantStatus, productStatus, opts) {
511
+ const next = String(variantStatus ?? "draft").trim() || "draft";
512
+ const productLive = String(productStatus ?? "draft") === "available";
513
+ const approvalOk = !opts?.requireApproval || String(opts?.approvalStatus ?? "") === "approved";
514
+ if (!productLive || !approvalOk) {
515
+ return "draft";
516
+ }
517
+ return next;
518
+ }
519
+ __name(coerceVariantStatusForProduct, "coerceVariantStatusForProduct");
455
520
 
456
521
  // src/lib/event-approval.ts
457
522
  var EVENT_APPROVAL_STATUSES = [
@@ -1055,11 +1120,118 @@ function pgErrorCode(err) {
1055
1120
  return driver?.code ?? err.code;
1056
1121
  }
1057
1122
  __name(pgErrorCode, "pgErrorCode");
1058
- function customerPhoneForUser(userId, phone) {
1123
+ function normalizeCustomerPhone(phone) {
1059
1124
  const p = typeof phone === "string" ? phone.trim() : "";
1060
- return p || `u-${userId}`;
1125
+ return p || null;
1126
+ }
1127
+ __name(normalizeCustomerPhone, "normalizeCustomerPhone");
1128
+ function isSyntheticCustomerPhone(phone) {
1129
+ const p = String(phone ?? "").trim();
1130
+ if (!p) return true;
1131
+ if (p.startsWith("e-") || p.startsWith("u-")) return true;
1132
+ if (p.includes("@")) return true;
1133
+ return false;
1134
+ }
1135
+ __name(isSyntheticCustomerPhone, "isSyntheticCustomerPhone");
1136
+ function customerPhoneForUser(_userId, phone) {
1137
+ return normalizeCustomerPhone(phone);
1061
1138
  }
1062
1139
  __name(customerPhoneForUser, "customerPhoneForUser");
1140
+ function customerPhoneForEmail(_email, phone) {
1141
+ return normalizeCustomerPhone(phone);
1142
+ }
1143
+ __name(customerPhoneForEmail, "customerPhoneForEmail");
1144
+ function resolveNextPhone(inputPhone, existingPhone) {
1145
+ if (inputPhone) return inputPhone;
1146
+ const existing = normalizeCustomerPhone(existingPhone);
1147
+ if (!existing || isSyntheticCustomerPhone(existing)) return null;
1148
+ return existing;
1149
+ }
1150
+ __name(resolveNextPhone, "resolveNextPhone");
1151
+ async function ensureCustomerRecord(dsOrEm, customerEntity, input) {
1152
+ const repo = dsOrEm.getRepository(customerEntity);
1153
+ const email = normalizeEmail(input.email);
1154
+ if (!email) return null;
1155
+ const name = String(input.name ?? "").trim() || email.split("@")[0] || "Customer";
1156
+ const userId = input.userId != null && Number.isFinite(Number(input.userId)) && Number(input.userId) > 0 ? Number(input.userId) : null;
1157
+ const phone = normalizeCustomerPhone(input.phone);
1158
+ let row = await repo.findOne({
1159
+ where: {
1160
+ email,
1161
+ deleted: false
1162
+ }
1163
+ });
1164
+ if (!row) {
1165
+ row = await repo.findOne({
1166
+ where: {
1167
+ email
1168
+ }
1169
+ });
1170
+ }
1171
+ if (row) {
1172
+ const existingUserId = row.userId;
1173
+ if (userId != null && existingUserId != null && existingUserId !== userId) {
1174
+ return null;
1175
+ }
1176
+ const nextPhone = resolveNextPhone(phone, row.phone);
1177
+ const patch = {
1178
+ name,
1179
+ phone: nextPhone,
1180
+ deleted: false,
1181
+ deletedAt: null,
1182
+ deletedBy: null,
1183
+ updatedAt: /* @__PURE__ */ new Date()
1184
+ };
1185
+ if (userId != null && existingUserId == null) {
1186
+ patch.userId = userId;
1187
+ }
1188
+ await repo.update(row.id, patch);
1189
+ return {
1190
+ id: row.id
1191
+ };
1192
+ }
1193
+ try {
1194
+ const created = await repo.save(repo.create({
1195
+ userId,
1196
+ name,
1197
+ email,
1198
+ phone,
1199
+ deleted: false
1200
+ }));
1201
+ return {
1202
+ id: created.id
1203
+ };
1204
+ } catch (err) {
1205
+ const code = pgErrorCode(err);
1206
+ if (code === "25P02") throw err;
1207
+ row = await repo.findOne({
1208
+ where: {
1209
+ email
1210
+ }
1211
+ });
1212
+ if (row) {
1213
+ const existingUserId = row.userId;
1214
+ if (userId != null && existingUserId != null && existingUserId !== userId) return null;
1215
+ await repo.update(row.id, {
1216
+ name,
1217
+ phone: resolveNextPhone(phone, row.phone),
1218
+ ...userId != null && existingUserId == null ? {
1219
+ userId
1220
+ } : {},
1221
+ deleted: false,
1222
+ deletedAt: null,
1223
+ deletedBy: null,
1224
+ updatedAt: /* @__PURE__ */ new Date()
1225
+ });
1226
+ return {
1227
+ id: row.id
1228
+ };
1229
+ }
1230
+ if (code === "23505") return null;
1231
+ throw err;
1232
+ }
1233
+ }
1234
+ __name(ensureCustomerRecord, "ensureCustomerRecord");
1063
1235
  async function restoreCustomerRow(repo, row, user, name, email, phone) {
1064
1236
  const id = row.id;
1065
1237
  await repo.update(id, {
@@ -1081,7 +1253,7 @@ async function ensureCustomerForUser(dsOrEm, customerEntity, user, overrides) {
1081
1253
  const repo = dsOrEm.getRepository(customerEntity);
1082
1254
  const email = normalizeEmail(user.email);
1083
1255
  const name = String(user.name ?? "").trim() || email.split("@")[0] || "User";
1084
- const phone = customerPhoneForUser(user.id, overrides?.phone ?? user.phone);
1256
+ const phone = normalizeCustomerPhone(overrides?.phone ?? user.phone);
1085
1257
  let row = await repo.findOne({
1086
1258
  where: {
1087
1259
  userId: user.id,
@@ -1089,10 +1261,11 @@ async function ensureCustomerForUser(dsOrEm, customerEntity, user, overrides) {
1089
1261
  }
1090
1262
  });
1091
1263
  if (row) {
1264
+ const nextPhone = resolveNextPhone(phone, row.phone);
1092
1265
  await repo.update(row.id, {
1093
1266
  name,
1094
1267
  email,
1095
- phone,
1268
+ phone: nextPhone,
1096
1269
  updatedAt: /* @__PURE__ */ new Date()
1097
1270
  });
1098
1271
  return {
@@ -1110,10 +1283,11 @@ async function ensureCustomerForUser(dsOrEm, customerEntity, user, overrides) {
1110
1283
  if (existingUserId != null && existingUserId !== user.id) {
1111
1284
  return null;
1112
1285
  }
1286
+ const nextPhone = resolveNextPhone(phone, row.phone);
1113
1287
  await repo.update(row.id, {
1114
1288
  userId: user.id,
1115
1289
  name,
1116
- phone,
1290
+ phone: nextPhone,
1117
1291
  updatedAt: /* @__PURE__ */ new Date()
1118
1292
  });
1119
1293
  return {
@@ -1128,21 +1302,23 @@ async function ensureCustomerForUser(dsOrEm, customerEntity, user, overrides) {
1128
1302
  if (deletedByEmail) {
1129
1303
  const existingUserId = deletedByEmail.userId;
1130
1304
  if (existingUserId != null && existingUserId !== user.id) return null;
1131
- return restoreCustomerRow(repo, deletedByEmail, user, name, email, phone);
1305
+ return restoreCustomerRow(repo, deletedByEmail, user, name, email, resolveNextPhone(phone, deletedByEmail.phone));
1132
1306
  }
1133
- const deletedByPhone = await repo.findOne({
1134
- where: {
1135
- phone
1136
- }
1137
- });
1138
- if (deletedByPhone) {
1139
- const existingUserId = deletedByPhone.userId;
1140
- if (existingUserId != null && existingUserId !== user.id) {
1141
- return ensureCustomerForUser(dsOrEm, customerEntity, user, {
1142
- phone: `u-${user.id}-${Date.now()}`
1143
- });
1307
+ if (phone) {
1308
+ const deletedByPhone = await repo.findOne({
1309
+ where: {
1310
+ phone
1311
+ }
1312
+ });
1313
+ if (deletedByPhone) {
1314
+ const existingUserId = deletedByPhone.userId;
1315
+ if (existingUserId != null && existingUserId !== user.id) {
1316
+ return ensureCustomerForUser(dsOrEm, customerEntity, user, {
1317
+ phone: null
1318
+ });
1319
+ }
1320
+ return restoreCustomerRow(repo, deletedByPhone, user, name, email, phone);
1144
1321
  }
1145
- return restoreCustomerRow(repo, deletedByPhone, user, name, email, phone);
1146
1322
  }
1147
1323
  try {
1148
1324
  const created = await repo.save(repo.create({
@@ -1163,34 +1339,22 @@ async function ensureCustomerForUser(dsOrEm, customerEntity, user, overrides) {
1163
1339
  userId: user.id
1164
1340
  }
1165
1341
  });
1166
- if (row) return restoreCustomerRow(repo, row, user, name, email, phone);
1342
+ if (row) {
1343
+ return restoreCustomerRow(repo, row, user, name, email, resolveNextPhone(phone, row.phone));
1344
+ }
1167
1345
  row = await repo.findOne({
1168
1346
  where: {
1169
1347
  email
1170
1348
  }
1171
1349
  });
1172
1350
  if (row && (row.userId ?? user.id) === user.id) {
1173
- return restoreCustomerRow(repo, row, user, name, email, phone);
1351
+ return restoreCustomerRow(repo, row, user, name, email, resolveNextPhone(phone, row.phone));
1174
1352
  }
1175
1353
  if (code === "23505") return null;
1176
1354
  throw err;
1177
1355
  }
1178
1356
  }
1179
1357
  __name(ensureCustomerForUser, "ensureCustomerForUser");
1180
- async function linkUnclaimedContactToUser(dataSource, contactsEntity, userId, email) {
1181
- const repo = dataSource.getRepository(contactsEntity);
1182
- const found = await repo.findOne({
1183
- where: {
1184
- email,
1185
- userId: IsNull(),
1186
- deleted: false
1187
- }
1188
- });
1189
- if (found) await repo.update(found.id, {
1190
- userId
1191
- });
1192
- }
1193
- __name(linkUnclaimedContactToUser, "linkUnclaimedContactToUser");
1194
1358
 
1195
1359
  // src/lib/vendor-customer-contacts.ts
1196
1360
  function isCustomerTypeContact(type) {
@@ -1283,8 +1447,27 @@ function resolveVendorIdForContactCheck(scope, bodyOrRow) {
1283
1447
  return Number.isFinite(n) ? n : null;
1284
1448
  }
1285
1449
  __name(resolveVendorIdForContactCheck, "resolveVendorIdForContactCheck");
1450
+ function uniquePositiveIds(ids) {
1451
+ const out = [];
1452
+ const seen = /* @__PURE__ */ new Set();
1453
+ for (const raw of ids) {
1454
+ const n = Number(raw);
1455
+ if (!Number.isFinite(n) || n <= 0 || seen.has(n)) continue;
1456
+ seen.add(n);
1457
+ out.push(n);
1458
+ }
1459
+ return out;
1460
+ }
1461
+ __name(uniquePositiveIds, "uniquePositiveIds");
1286
1462
  async function ensureVendorCustomerForOrderContact(dataSource, entityMap, vendorId, contactId, details) {
1287
- if (!Number.isFinite(vendorId) || !Number.isFinite(contactId)) return;
1463
+ await ensureVendorCustomersForOrder(dataSource, entityMap, [
1464
+ vendorId
1465
+ ], contactId, details);
1466
+ }
1467
+ __name(ensureVendorCustomerForOrderContact, "ensureVendorCustomerForOrderContact");
1468
+ async function ensureVendorCustomersForOrder(dataSource, entityMap, vendorIds, contactId, details) {
1469
+ const vendors = uniquePositiveIds(vendorIds);
1470
+ if (vendors.length === 0 || !Number.isFinite(contactId) || contactId <= 0) return;
1288
1471
  const vcEntity = entityMap.vendor_customers;
1289
1472
  const contactEntity = entityMap.contacts;
1290
1473
  if (!vcEntity || !contactEntity) return;
@@ -1310,96 +1493,61 @@ async function ensureVendorCustomerForOrderContact(dataSource, entityMap, vendor
1310
1493
  }
1311
1494
  let customerId = null;
1312
1495
  if (entityMap.customer) {
1313
- const customerRepo = dataSource.getRepository(entityMap.customer);
1314
- const existingCustomer = await customerRepo.findOne({
1315
- where: {
1316
- email,
1317
- deleted: false
1318
- }
1496
+ const ensured = await ensureCustomerRecord(dataSource, entityMap.customer, {
1497
+ name,
1498
+ email,
1499
+ phone,
1500
+ userId: null
1319
1501
  });
1320
- if (existingCustomer) {
1321
- customerId = Number(existingCustomer.id);
1322
- } else if (entityMap.users) {
1323
- const userRepo = dataSource.getRepository(entityMap.users);
1324
- let user = await userRepo.findOne({
1502
+ if (ensured) customerId = ensured.id;
1503
+ }
1504
+ for (const vendorId of vendors) {
1505
+ if (customerId != null) {
1506
+ const byCustomer = await vcRepo.findOne({
1325
1507
  where: {
1326
- email,
1327
- deleted: false
1508
+ vendorId,
1509
+ customerId
1328
1510
  }
1329
1511
  });
1330
- if (!user) {
1331
- let groupId = null;
1332
- if (entityMap.user_groups) {
1333
- const userGroupRepo = dataSource.getRepository(entityMap.user_groups);
1334
- const customerGroup = await userGroupRepo.findOne({
1335
- where: {
1336
- name: "Customer",
1337
- deleted: false
1338
- }
1512
+ if (byCustomer) {
1513
+ const existingContactId = byCustomer.contactId;
1514
+ if (existingContactId == null) {
1515
+ await vcRepo.update(byCustomer.id, {
1516
+ contactId
1339
1517
  });
1340
- if (customerGroup) groupId = Number(customerGroup.id);
1341
1518
  }
1342
- user = await userRepo.save(userRepo.create({
1343
- name,
1344
- email,
1345
- phone,
1346
- password: null,
1347
- blocked: false,
1348
- groupId,
1349
- adminAccess: false
1350
- }));
1519
+ continue;
1351
1520
  }
1352
- const userId = Number(user.id);
1353
- await linkUnclaimedContactToUser(dataSource, contactEntity, userId, email);
1354
- const ensured = await ensureCustomerForUser(dataSource, entityMap.customer, {
1355
- id: userId,
1356
- name,
1357
- email,
1358
- phone
1359
- }, {
1360
- phone
1361
- });
1362
- if (ensured) customerId = ensured.id;
1363
1521
  }
1364
- }
1365
- if (customerId != null) {
1366
- const byCustomer = await vcRepo.findOne({
1522
+ const byContact = await vcRepo.findOne({
1367
1523
  where: {
1368
1524
  vendorId,
1369
- customerId
1525
+ contactId
1370
1526
  }
1371
1527
  });
1372
- if (byCustomer) {
1373
- const existingContactId = byCustomer.contactId;
1374
- if (existingContactId == null) {
1375
- await vcRepo.update(byCustomer.id, {
1376
- contactId
1528
+ if (byContact) {
1529
+ if (customerId != null && byContact.customerId == null) {
1530
+ await vcRepo.update(byContact.id, {
1531
+ customerId
1377
1532
  });
1378
1533
  }
1379
- return;
1534
+ continue;
1380
1535
  }
1381
- }
1382
- const byContact = await vcRepo.findOne({
1383
- where: {
1384
- vendorId,
1385
- contactId
1536
+ if (customerId != null) {
1537
+ await vcRepo.save(vcRepo.create({
1538
+ vendorId,
1539
+ customerId,
1540
+ contactId
1541
+ }));
1542
+ } else {
1543
+ await vcRepo.save(vcRepo.create({
1544
+ vendorId,
1545
+ contactId
1546
+ }));
1386
1547
  }
1387
- });
1388
- if (byContact) return;
1389
- if (customerId != null) {
1390
- await vcRepo.save(vcRepo.create({
1391
- vendorId,
1392
- customerId,
1393
- contactId
1394
- }));
1395
- return;
1396
1548
  }
1397
- await vcRepo.save(vcRepo.create({
1398
- vendorId,
1399
- contactId
1400
- }));
1401
1549
  }
1402
- __name(ensureVendorCustomerForOrderContact, "ensureVendorCustomerForOrderContact");
1550
+ __name(ensureVendorCustomersForOrder, "ensureVendorCustomersForOrder");
1403
1551
 
1404
1552
  // src/lib/currency-prices.ts
1405
1553
  function normalizeCurrencyCode(code) {
@@ -3765,23 +3913,24 @@ function createCrudHandler(dataSource, entityMap, options) {
3765
3913
  if (resource === "vendor_customers" && entityMap["customer"]) {
3766
3914
  const scope = await resolveVendorCustomersScope(dataSource, entityMap, resolveScope);
3767
3915
  const repo2 = dataSource.getRepository(entity);
3768
- const qb = repo2.createQueryBuilder("vc").leftJoinAndSelect("vc.customer", "customer").leftJoinAndSelect("customer.user", "user").orderBy("vc.createdAt", sortOrder === "DESC" ? "DESC" : "ASC").skip(skip).take(limit);
3916
+ const qb = repo2.createQueryBuilder("vc").leftJoinAndSelect("vc.customer", "customer").leftJoinAndSelect("customer.user", "user").leftJoinAndSelect("vc.contact", "contact").orderBy("vc.createdAt", sortOrder === "DESC" ? "DESC" : "ASC").skip(skip).take(limit);
3769
3917
  applyVendorScopeToQueryBuilder(qb, "vc", scope);
3770
3918
  if (search && typeof search === "string" && search.trim()) {
3771
3919
  const term = `%${search.trim()}%`;
3772
- qb.andWhere("(customer.name ILIKE :term OR customer.email ILIKE :term OR customer.phone ILIKE :term)", {
3920
+ qb.andWhere("(customer.name ILIKE :term OR customer.email ILIKE :term OR customer.phone ILIKE :term OR contact.name ILIKE :term OR contact.email ILIKE :term OR contact.phone ILIKE :term)", {
3773
3921
  term
3774
3922
  });
3775
3923
  }
3776
3924
  const [rows, total2] = await qb.getManyAndCount();
3777
3925
  const data2 = rows.map((row) => {
3778
3926
  const customer = row.customer;
3927
+ const contact = row.contact;
3779
3928
  return {
3780
3929
  ...row,
3781
- name: customer?.name ?? null,
3782
- email: customer?.email ?? null,
3783
- phone: customer?.phone ?? null,
3784
- company: customer?.company ?? null
3930
+ name: customer?.name ?? contact?.name ?? null,
3931
+ email: customer?.email ?? contact?.email ?? null,
3932
+ phone: customer?.phone ?? contact?.phone ?? null,
3933
+ company: customer?.company ?? contact?.company ?? null
3785
3934
  };
3786
3935
  });
3787
3936
  return json({
@@ -4115,7 +4264,11 @@ function createCrudHandler(dataSource, entityMap, options) {
4115
4264
  } else if (resource === "collections") {
4116
4265
  if (scope.type === "vendor") {
4117
4266
  where = mergeVendorScopeIntoWhere(where, scope, resource, catalogFlags);
4118
- } else if (searchParams.get("isCatalog") !== "true") {
4267
+ } else if (searchParams.get("isCatalog") === "true") {
4268
+ where = mergeListWhereAnd(where, {
4269
+ isCatalog: true
4270
+ });
4271
+ } else if (searchParams.get("isCatalog") === "false") {
4119
4272
  where = mergeListWhereAnd(where, {
4120
4273
  isCatalog: false
4121
4274
  });
@@ -4312,41 +4465,17 @@ function createCrudHandler(dataSource, entityMap, options) {
4312
4465
  status: 503
4313
4466
  });
4314
4467
  }
4315
- if (!entityMap["users"]) {
4316
- return json({
4317
- error: "Users entity not configured"
4318
- }, {
4319
- status: 503
4320
- });
4321
- }
4322
- let customerGroupId = null;
4323
- if (entityMap["user_groups"]) {
4324
- const userGroupRepo = dataSource.getRepository(entityMap["user_groups"]);
4325
- const customerGroup = await userGroupRepo.findOne({
4326
- where: {
4327
- name: "Customer",
4328
- deleted: false
4329
- }
4330
- });
4331
- if (!customerGroup) {
4332
- return json({
4333
- error: "User group 'customer' not found"
4334
- }, {
4335
- status: 500
4336
- });
4337
- }
4338
- customerGroupId = Number(customerGroup.id);
4339
- } else {
4468
+ if (!entityMap["contacts"]) {
4340
4469
  return json({
4341
- error: "user_groups entity not configured"
4470
+ error: "Contacts entity not configured"
4342
4471
  }, {
4343
4472
  status: 503
4344
4473
  });
4345
4474
  }
4346
4475
  const name = String(body.name ?? "").trim();
4347
- const email = String(body.email ?? "").trim();
4348
- const phone = String(body.phone ?? "").trim();
4349
- const rawPw = String(body._password ?? "").trim();
4476
+ const email = String(body.email ?? "").trim().toLowerCase();
4477
+ const phoneRaw = String(body.phone ?? "").trim();
4478
+ const phone = phoneRaw || null;
4350
4479
  if (!name) return json({
4351
4480
  error: "name is required"
4352
4481
  }, {
@@ -4357,25 +4486,7 @@ function createCrudHandler(dataSource, entityMap, options) {
4357
4486
  }, {
4358
4487
  status: 400
4359
4488
  });
4360
- if (!phone) return json({
4361
- error: "phone is required"
4362
- }, {
4363
- status: 400
4364
- });
4365
- if (!rawPw) return json({
4366
- error: "password is required"
4367
- }, {
4368
- status: 400
4369
- });
4370
- if (rawPw.length < 6) {
4371
- return json({
4372
- error: "Password must be at least 6 characters"
4373
- }, {
4374
- status: 400
4375
- });
4376
- }
4377
4489
  const customerRepo = dataSource.getRepository(entityMap["customer"]);
4378
- const userRepo = dataSource.getRepository(entityMap["users"]);
4379
4490
  const dupCustEmail = await customerRepo.findOne({
4380
4491
  where: {
4381
4492
  email,
@@ -4389,49 +4500,26 @@ function createCrudHandler(dataSource, entityMap, options) {
4389
4500
  status: 409
4390
4501
  });
4391
4502
  }
4392
- const dupCustPhone = await customerRepo.findOne({
4393
- where: {
4394
- phone,
4395
- deleted: false
4396
- }
4397
- });
4398
- if (dupCustPhone) {
4399
- return json({
4400
- error: "A customer with this phone number already exists"
4401
- }, {
4402
- status: 409
4503
+ if (phone) {
4504
+ const dupCustPhone = await customerRepo.findOne({
4505
+ where: {
4506
+ phone,
4507
+ deleted: false
4508
+ }
4403
4509
  });
4404
- }
4405
- let userId;
4406
- const dupUser = await userRepo.findOne({
4407
- where: {
4408
- email,
4409
- deleted: false
4510
+ if (dupCustPhone) {
4511
+ return json({
4512
+ error: "A customer with this phone number already exists"
4513
+ }, {
4514
+ status: 409
4515
+ });
4410
4516
  }
4411
- });
4412
- if (dupUser) {
4413
- userId = Number(dupUser.id);
4414
- } else {
4415
- const bcrypt = await import('bcryptjs');
4416
- const hashedPassword = await bcrypt.hash(rawPw, 10);
4417
- const savedUser = await userRepo.save(userRepo.create({
4418
- name,
4419
- email,
4420
- phone,
4421
- password: hashedPassword,
4422
- groupId: customerGroupId,
4423
- adminAccess: false,
4424
- blocked: false
4425
- }));
4426
- userId = Number(savedUser.id);
4427
4517
  }
4428
- const savedCustomer = await ensureCustomerForUser(dataSource, entityMap["customer"], {
4429
- id: userId,
4518
+ const savedCustomer = await ensureCustomerRecord(dataSource, entityMap["customer"], {
4430
4519
  name,
4431
4520
  email,
4432
- phone
4433
- }, {
4434
- phone
4521
+ phone,
4522
+ userId: null
4435
4523
  });
4436
4524
  if (!savedCustomer) {
4437
4525
  return json({
@@ -4440,37 +4528,37 @@ function createCrudHandler(dataSource, entityMap, options) {
4440
4528
  status: 409
4441
4529
  });
4442
4530
  }
4443
- const customerRepo2 = dataSource.getRepository(entityMap["customer"]);
4444
- const customerRow = await customerRepo2.findOne({
4531
+ const contactRepo = dataSource.getRepository(entityMap["contacts"]);
4532
+ let contact = await contactRepo.findOne({
4445
4533
  where: {
4446
- id: savedCustomer.id,
4534
+ email,
4447
4535
  deleted: false
4448
4536
  }
4449
- }) ?? savedCustomer;
4450
- if (entityMap["contacts"]) {
4451
- const contactRepo = dataSource.getRepository(entityMap["contacts"]);
4452
- const existingContact = await contactRepo.findOne({
4453
- where: {
4454
- email,
4455
- deleted: false
4456
- }
4457
- });
4458
- if (!existingContact) {
4459
- await contactRepo.save(contactRepo.create({
4460
- name,
4461
- email,
4462
- phone: phone || null,
4463
- type: "customer"
4464
- }));
4465
- } else {
4466
- const t = existingContact.type;
4467
- if (t == null || t === "" || String(t).toLowerCase() !== "customer") {
4468
- await contactRepo.update(existingContact.id, {
4469
- type: "customer"
4470
- });
4471
- }
4472
- }
4537
+ });
4538
+ if (!contact) {
4539
+ contact = await contactRepo.save(contactRepo.create({
4540
+ name,
4541
+ email,
4542
+ phone: phone || null,
4543
+ type: "customer"
4544
+ }));
4545
+ } else {
4546
+ const t = contact.type;
4547
+ const patch = {
4548
+ name,
4549
+ phone: phone || contact.phone || null
4550
+ };
4551
+ if (t == null || t === "" || String(t).toLowerCase() !== "customer") {
4552
+ patch.type = "customer";
4553
+ }
4554
+ await contactRepo.update(contact.id, patch);
4555
+ contact = {
4556
+ ...contact,
4557
+ ...patch,
4558
+ id: contact.id
4559
+ };
4473
4560
  }
4561
+ const contactId = Number(contact.id);
4474
4562
  const scope = await resolveVendorCustomersScope(dataSource, entityMap, resolveScope);
4475
4563
  let vendorId = null;
4476
4564
  if (scope.type === "vendor") {
@@ -4486,19 +4574,25 @@ function createCrudHandler(dataSource, entityMap, options) {
4486
4574
  status: 400
4487
4575
  });
4488
4576
  }
4489
- const vcRepo = dataSource.getRepository(entity);
4490
- const existingLink = await vcRepo.findOne({
4577
+ await ensureVendorCustomersForOrder(dataSource, entityMap, [
4578
+ vendorId
4579
+ ], contactId, {
4580
+ name,
4581
+ email,
4582
+ phone
4583
+ });
4584
+ const customerRow = await customerRepo.findOne({
4491
4585
  where: {
4492
- customerId: savedCustomer.id
4586
+ id: savedCustomer.id,
4587
+ deleted: false
4493
4588
  }
4494
4589
  });
4495
- if (!existingLink) {
4496
- await vcRepo.save(vcRepo.create({
4497
- customerId: savedCustomer.id,
4498
- vendorId
4499
- }));
4500
- }
4501
- return json(customerRow, {
4590
+ return json(customerRow ?? {
4591
+ id: savedCustomer.id,
4592
+ name,
4593
+ email,
4594
+ phone
4595
+ }, {
4502
4596
  status: 201
4503
4597
  });
4504
4598
  }
@@ -4594,6 +4688,26 @@ function createCrudHandler(dataSource, entityMap, options) {
4594
4688
  }
4595
4689
  }
4596
4690
  }
4691
+ if (resource === "product_variants" && entityMap.products) {
4692
+ const productIdRaw = persistBody.productId;
4693
+ const productId = typeof productIdRaw === "number" ? productIdRaw : typeof productIdRaw === "string" && /^\d+$/.test(productIdRaw) ? parseInt(productIdRaw, 10) : NaN;
4694
+ if (Number.isFinite(productId)) {
4695
+ const parent = await dataSource.getRepository(entityMap.products).findOne({
4696
+ where: {
4697
+ id: productId
4698
+ }
4699
+ });
4700
+ const parentStatus = parent?.status;
4701
+ const parentApproval = parent?.approvalStatus;
4702
+ const requireApproval = await getRequireProductApproval(dataSource);
4703
+ persistBody.status = coerceVariantStatusForProduct(persistBody.status, parentStatus, {
4704
+ approvalStatus: parentApproval,
4705
+ requireApproval
4706
+ });
4707
+ } else if (!("status" in persistBody)) {
4708
+ persistBody.status = "draft";
4709
+ }
4710
+ }
4597
4711
  if (resource === "products") {
4598
4712
  const scopeForProduct = await resolveScope();
4599
4713
  const productFlags = await getVendorCatalogCreateFlags(dataSource);
@@ -4926,6 +5040,7 @@ function createCrudHandler(dataSource, entityMap, options) {
4926
5040
  }
4927
5041
  const nameRaw = String(body["contact.name"] ?? contact.name ?? "").trim();
4928
5042
  const phoneRaw = body["contact.phone"] ?? contact.phone;
5043
+ const emailForVc = (emailRaw || String(contact.email ?? "")).trim().toLowerCase();
4929
5044
  const phoneToSave = phoneRaw == null || phoneRaw === "" ? null : String(phoneRaw).trim();
4930
5045
  if (phoneToSave && contact.phone !== phoneToSave) {
4931
5046
  await contactRepo.update(contact.id, {
@@ -4934,21 +5049,45 @@ function createCrudHandler(dataSource, entityMap, options) {
4934
5049
  contact.phone = phoneToSave;
4935
5050
  }
4936
5051
  const vendorIdForCustomer = resolveVendorIdForContactCheck(scopeCreate, persistBody);
4937
- const selectedExistingCustomer = Number(body.customerId);
4938
- if (vendorIdForCustomer != null && (!Number.isFinite(selectedExistingCustomer) || selectedExistingCustomer <= 0)) {
4939
- await ensureVendorCustomerForOrderContact(dataSource, entityMap, vendorIdForCustomer, contact.id, {
4940
- name: nameRaw || emailRaw.split("@")[0] || "Customer",
4941
- email: emailRaw,
5052
+ const orderLinesForVendors = normalizeOrderLinesInput(body.orderLines);
5053
+ const vendorIdsForVc = [];
5054
+ if (vendorIdForCustomer != null) vendorIdsForVc.push(vendorIdForCustomer);
5055
+ if (orderLinesForVendors?.length && entityMap.products) {
5056
+ const productRepoForVc = dataSource.getRepository(entityMap.products);
5057
+ for (const line of orderLinesForVendors) {
5058
+ const pid = Number(line.productId);
5059
+ if (!Number.isFinite(pid)) continue;
5060
+ const product = await productRepoForVc.findOne({
5061
+ where: {
5062
+ id: pid
5063
+ }
5064
+ });
5065
+ const pVid = Number(product?.vendorId);
5066
+ if (Number.isFinite(pVid) && pVid > 0) vendorIdsForVc.push(pVid);
5067
+ }
5068
+ }
5069
+ if (vendorIdsForVc.length > 0 && emailForVc) {
5070
+ await ensureVendorCustomersForOrder(dataSource, entityMap, vendorIdsForVc, contact.id, {
5071
+ name: nameRaw || emailForVc.split("@")[0] || "Customer",
5072
+ email: emailForVc,
4942
5073
  phone: phoneRaw == null || phoneRaw === "" ? null : String(phoneRaw)
4943
5074
  });
4944
- } else if (vendorIdForCustomer != null) {
4945
- const contactErr = await assertContactAllowedForVendorOrder(dataSource, entityMap, vendorIdForCustomer, contact.id);
4946
- if (contactErr) {
4947
- return json({
4948
- error: contactErr
4949
- }, {
4950
- status: 400
4951
- });
5075
+ }
5076
+ const accountCustomerId = Number(body.accountCustomerId);
5077
+ if (entityMap.customer_contacts && Number.isFinite(accountCustomerId) && accountCustomerId > 0) {
5078
+ const ccRepo = dataSource.getRepository(entityMap.customer_contacts);
5079
+ const orderContactId = contact.id;
5080
+ const existingLink = await ccRepo.findOne({
5081
+ where: {
5082
+ customerId: accountCustomerId,
5083
+ contactId: orderContactId
5084
+ }
5085
+ });
5086
+ if (!existingLink) {
5087
+ await ccRepo.save(ccRepo.create({
5088
+ customerId: accountCustomerId,
5089
+ contactId: orderContactId
5090
+ }));
4952
5091
  }
4953
5092
  }
4954
5093
  const randomPart = Math.floor(1e4 + Math.random() * 9e4);
@@ -6233,6 +6372,34 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
6233
6372
  delete u.parentId;
6234
6373
  }
6235
6374
  }
6375
+ if (resource === "product_variants" && entityMap.products) {
6376
+ const currentVariant = await repo.findOne({
6377
+ where: {
6378
+ id: numericId
6379
+ }
6380
+ });
6381
+ if (!currentVariant) return json({
6382
+ message: "Not found"
6383
+ }, {
6384
+ status: 404
6385
+ });
6386
+ const productId = "productId" in updatePayload && updatePayload.productId != null ? Number(updatePayload.productId) : Number(currentVariant.productId);
6387
+ if (Number.isFinite(productId)) {
6388
+ const parent = await dataSource.getRepository(entityMap.products).findOne({
6389
+ where: {
6390
+ id: productId
6391
+ }
6392
+ });
6393
+ const parentStatus = parent?.status;
6394
+ const parentApproval = parent?.approvalStatus;
6395
+ const requireApproval = await getRequireProductApproval(dataSource);
6396
+ const nextStatus = "status" in updatePayload ? updatePayload.status : currentVariant.status;
6397
+ updatePayload.status = coerceVariantStatusForProduct(nextStatus, parentStatus, {
6398
+ approvalStatus: parentApproval,
6399
+ requireApproval
6400
+ });
6401
+ }
6402
+ }
6236
6403
  if (resource === "products") {
6237
6404
  const currentRow = await repo.findOne({
6238
6405
  where: {
@@ -6588,6 +6755,10 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
6588
6755
  if (reloaded) updated = reloaded;
6589
6756
  }
6590
6757
  updated = hydrateProductDisplayName(updated);
6758
+ const productStatus = String(updated.status ?? "draft");
6759
+ if (Number.isFinite(updatedId) && updatedId > 0) {
6760
+ await syncProductVariantsStatusWithProduct(dataSource, entityMap, updatedId, productStatus);
6761
+ }
6591
6762
  if (getCms) {
6592
6763
  const cms = await getCms();
6593
6764
  await queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, updated);
@@ -6696,6 +6867,14 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
6696
6867
  }
6697
6868
  }
6698
6869
  await repo.update(numericId, buildSoftDeletePayload(repo.metadata, deletedBy));
6870
+ if ((resource === "forms" || resource === "vendors") && existing && typeof existing.slug === "string") {
6871
+ const slug = String(existing.slug).trim();
6872
+ if (slug && !slug.includes("__deleted_")) {
6873
+ await repo.update(numericId, {
6874
+ slug: `${slug}__deleted_${numericId}`
6875
+ });
6876
+ }
6877
+ }
6699
6878
  return json({
6700
6879
  message: "Deleted successfully"
6701
6880
  }, {
@@ -6725,7 +6904,21 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
6725
6904
  }
6726
6905
  };
6727
6906
  }
6728
- __name(createCrudByIdHandler, "createCrudByIdHandler");
6907
+ __name(createCrudByIdHandler, "createCrudByIdHandler");
6908
+ async function linkUnclaimedContactToUser(dataSource, contactsEntity, userId, email) {
6909
+ const repo = dataSource.getRepository(contactsEntity);
6910
+ const found = await repo.findOne({
6911
+ where: {
6912
+ email,
6913
+ userId: IsNull(),
6914
+ deleted: false
6915
+ }
6916
+ });
6917
+ if (found) await repo.update(found.id, {
6918
+ userId
6919
+ });
6920
+ }
6921
+ __name(linkUnclaimedContactToUser, "linkUnclaimedContactToUser");
6729
6922
  var VENDOR_INVITE_METADATA_TOKEN = "inviteToken";
6730
6923
  var VENDOR_INVITE_METADATA_EXPIRES = "inviteExpiresAt";
6731
6924
  var VENDOR_INVITE_METADATA_SENT = "inviteSentAt";
@@ -7458,6 +7651,50 @@ async function findLlmAgentByScope(dataSource, entityMap, scope, options = {}) {
7458
7651
  }
7459
7652
  __name(findLlmAgentByScope, "findLlmAgentByScope");
7460
7653
 
7654
+ // src/lib/retire-soft-deleted-unique.ts
7655
+ async function retireSoftDeletedUniqueValue(repo, column, value) {
7656
+ const trimmed = typeof value === "string" ? value.trim() : "";
7657
+ if (!trimmed) return;
7658
+ const cols = new Set(repo.metadata.columns.map((c) => c.propertyName));
7659
+ if (!cols.has(column) || !cols.has("deleted")) return;
7660
+ const rows = await repo.find({
7661
+ where: {
7662
+ [column]: trimmed,
7663
+ deleted: true
7664
+ }
7665
+ });
7666
+ for (const row of rows) {
7667
+ const id = row.id;
7668
+ if (!Number.isFinite(id)) continue;
7669
+ const current = String(row[column] ?? "");
7670
+ if (current.includes("__deleted_")) continue;
7671
+ await repo.update(id, {
7672
+ [column]: `${trimmed}__deleted_${id}`
7673
+ });
7674
+ }
7675
+ }
7676
+ __name(retireSoftDeletedUniqueValue, "retireSoftDeletedUniqueValue");
7677
+ async function activeUniqueValueExists(repo, column, value, excludeId) {
7678
+ const trimmed = typeof value === "string" ? value.trim() : "";
7679
+ if (!trimmed) return false;
7680
+ const cols = new Set(repo.metadata.columns.map((c) => c.propertyName));
7681
+ if (!cols.has(column)) return false;
7682
+ const qb = repo.createQueryBuilder("row").where(`row.${column} = :value`, {
7683
+ value: trimmed
7684
+ });
7685
+ if (cols.has("deleted")) {
7686
+ qb.andWhere("row.deleted = false");
7687
+ }
7688
+ if (excludeId != null && Number.isFinite(excludeId) && excludeId > 0) {
7689
+ qb.andWhere("row.id != :excludeId", {
7690
+ excludeId
7691
+ });
7692
+ }
7693
+ const hit = await qb.getOne();
7694
+ return !!hit;
7695
+ }
7696
+ __name(activeUniqueValueExists, "activeUniqueValueExists");
7697
+
7461
7698
  // src/lib/media-folder-path.ts
7462
7699
  function sanitizeMediaFolderPath(input) {
7463
7700
  if (input == null) return "";
@@ -8851,6 +9088,17 @@ function createFormSaveHandlers(config) {
8851
9088
  });
8852
9089
  const fields = Array.isArray(body.fields) ? body.fields : [];
8853
9090
  const { fields: _f, ...formRow } = body;
9091
+ const slug = typeof formRow.slug === "string" ? formRow.slug.trim() : "";
9092
+ if (slug) {
9093
+ if (await activeUniqueValueExists(formRepo(), "slug", slug)) {
9094
+ return json({
9095
+ error: "A form with this slug already exists"
9096
+ }, {
9097
+ status: 400
9098
+ });
9099
+ }
9100
+ await retireSoftDeletedUniqueValue(formRepo(), "slug", slug);
9101
+ }
8854
9102
  const form = await formRepo().save(formRepo().create(formRow));
8855
9103
  for (let i = 0; i < fields.length; i++) {
8856
9104
  const row = normalizeFieldRow(fields[i], form.id);
@@ -8874,6 +9122,14 @@ function createFormSaveHandlers(config) {
8874
9122
  status: 201
8875
9123
  });
8876
9124
  } catch (e) {
9125
+ const msg = e instanceof Error ? e.message : String(e);
9126
+ if (/duplicate key|unique constraint/i.test(msg) && /slug/i.test(msg)) {
9127
+ return json({
9128
+ error: "A form with this slug already exists"
9129
+ }, {
9130
+ status: 400
9131
+ });
9132
+ }
8877
9133
  return json({
8878
9134
  error: "Server Error"
8879
9135
  }, {
@@ -8922,6 +9178,20 @@ function createFormSaveHandlers(config) {
8922
9178
  ]) {
8923
9179
  if (body[key] !== void 0) formRow[key] = body[key];
8924
9180
  }
9181
+ if (typeof formRow.slug === "string") {
9182
+ const slug = formRow.slug.trim();
9183
+ formRow.slug = slug;
9184
+ if (slug) {
9185
+ if (await activeUniqueValueExists(formRepo(), "slug", slug, formId)) {
9186
+ return json({
9187
+ error: "A form with this slug already exists"
9188
+ }, {
9189
+ status: 400
9190
+ });
9191
+ }
9192
+ await retireSoftDeletedUniqueValue(formRepo(), "slug", slug);
9193
+ }
9194
+ }
8925
9195
  if (Object.keys(formRow).length > 0) await formRepo().update(formId, formRow);
8926
9196
  await fieldRepo().delete({
8927
9197
  formId
@@ -8950,6 +9220,14 @@ function createFormSaveHandlers(config) {
8950
9220
  status: 404
8951
9221
  });
8952
9222
  } catch (e) {
9223
+ const msg = e instanceof Error ? e.message : String(e);
9224
+ if (/duplicate key|unique constraint/i.test(msg) && /slug/i.test(msg)) {
9225
+ return json({
9226
+ error: "A form with this slug already exists"
9227
+ }, {
9228
+ status: 400
9229
+ });
9230
+ }
8953
9231
  return json({
8954
9232
  error: "Server Error"
8955
9233
  }, {
@@ -9531,6 +9809,7 @@ function createUsersApiHandlers(config) {
9531
9809
  "id",
9532
9810
  "name",
9533
9811
  "email",
9812
+ "phone",
9534
9813
  "blocked",
9535
9814
  "createdAt",
9536
9815
  "updatedAt",
@@ -9560,6 +9839,11 @@ function createUsersApiHandlers(config) {
9560
9839
  }
9561
9840
  try {
9562
9841
  const uid = parseInt(id, 10);
9842
+ if (!Number.isFinite(uid)) return json({
9843
+ error: "Invalid id"
9844
+ }, {
9845
+ status: 400
9846
+ });
9563
9847
  const existing = await userRepo().findOne({
9564
9848
  where: {
9565
9849
  id: uid,
@@ -9572,8 +9856,59 @@ function createUsersApiHandlers(config) {
9572
9856
  status: 404
9573
9857
  });
9574
9858
  const body = await req.json();
9575
- const { password: _p, ...safe } = body;
9576
- await userRepo().update(uid, safe);
9859
+ const patch = {
9860
+ updatedAt: /* @__PURE__ */ new Date()
9861
+ };
9862
+ if (typeof body.name === "string") patch.name = body.name.trim();
9863
+ if (typeof body.email === "string") patch.email = body.email.trim().toLowerCase();
9864
+ if (body.blocked !== void 0) {
9865
+ patch.blocked = body.blocked === true || body.blocked === "true" || body.blocked === 1 || body.blocked === "1";
9866
+ }
9867
+ if (body.adminAccess !== void 0) {
9868
+ patch.adminAccess = body.adminAccess === true || body.adminAccess === "true" || body.adminAccess === 1 || body.adminAccess === "1";
9869
+ }
9870
+ if (body.groupId !== void 0) {
9871
+ if (body.groupId === null || body.groupId === "") {
9872
+ patch.groupId = null;
9873
+ } else {
9874
+ const gid = Number(body.groupId);
9875
+ if (!Number.isFinite(gid)) {
9876
+ return json({
9877
+ error: "Invalid groupId"
9878
+ }, {
9879
+ status: 400
9880
+ });
9881
+ }
9882
+ patch.groupId = gid;
9883
+ }
9884
+ }
9885
+ if (body.phone !== void 0) {
9886
+ const phone = body.phone == null ? null : String(body.phone).trim();
9887
+ patch.phone = phone || null;
9888
+ }
9889
+ if (Object.keys(patch).length <= 1) {
9890
+ return json({
9891
+ error: "No valid fields to update"
9892
+ }, {
9893
+ status: 400
9894
+ });
9895
+ }
9896
+ if (typeof patch.email === "string" && patch.email !== existing.email) {
9897
+ const emailTaken = await userRepo().findOne({
9898
+ where: {
9899
+ email: patch.email,
9900
+ deleted: false
9901
+ }
9902
+ });
9903
+ if (emailTaken && Number(emailTaken.id) !== uid) {
9904
+ return json({
9905
+ error: "Email already in use"
9906
+ }, {
9907
+ status: 400
9908
+ });
9909
+ }
9910
+ }
9911
+ await userRepo().update(uid, patch);
9577
9912
  const updated = await userRepo().findOne({
9578
9913
  where: {
9579
9914
  id: uid,
@@ -9586,6 +9921,7 @@ function createUsersApiHandlers(config) {
9586
9921
  "id",
9587
9922
  "name",
9588
9923
  "email",
9924
+ "phone",
9589
9925
  "blocked",
9590
9926
  "createdAt",
9591
9927
  "updatedAt",
@@ -9597,7 +9933,8 @@ function createUsersApiHandlers(config) {
9597
9933
  }, {
9598
9934
  status: 404
9599
9935
  });
9600
- } catch {
9936
+ } catch (err) {
9937
+ console.error("[users.update]", err);
9601
9938
  return json({
9602
9939
  error: "Server Error"
9603
9940
  }, {
@@ -12645,6 +12982,39 @@ function slugify(input) {
12645
12982
  return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
12646
12983
  }
12647
12984
  __name(slugify, "slugify");
12985
+ async function userHasActiveVendor(em, entityMap, userId) {
12986
+ const vendorRepo = em.getRepository(entityMap.vendors);
12987
+ const owned = await vendorRepo.findOne({
12988
+ where: {
12989
+ userId,
12990
+ deleted: false
12991
+ }
12992
+ });
12993
+ if (owned) return true;
12994
+ if (!entityMap.vendor_users) return false;
12995
+ const link = await em.getRepository(entityMap.vendor_users).createQueryBuilder("vu").innerJoin("vendors", "v", 'v.id = vu."vendorId" AND v.deleted = false').where('vu."userId" = :userId', {
12996
+ userId
12997
+ }).getOne();
12998
+ return !!link;
12999
+ }
13000
+ __name(userHasActiveVendor, "userHasActiveVendor");
13001
+ async function userHadAnyVendor(em, entityMap, userId) {
13002
+ const vendorRepo = em.getRepository(entityMap.vendors);
13003
+ const owned = await vendorRepo.findOne({
13004
+ where: {
13005
+ userId
13006
+ }
13007
+ });
13008
+ if (owned) return true;
13009
+ if (!entityMap.vendor_users) return false;
13010
+ const link = await em.getRepository(entityMap.vendor_users).findOne({
13011
+ where: {
13012
+ userId
13013
+ }
13014
+ });
13015
+ return !!link;
13016
+ }
13017
+ __name(userHadAnyVendor, "userHadAnyVendor");
12648
13018
  function vendorOnboardErrorResponse(json, err) {
12649
13019
  const msg = err instanceof Error ? err.message : String(err);
12650
13020
  console.error("[vendor-onboard]", err);
@@ -12743,14 +13113,80 @@ function createVendorOnboardHandlers(config) {
12743
13113
  __name(gateAdmin, "gateAdmin");
12744
13114
  async function resolveActiveVendorId(u) {
12745
13115
  const scope = resolveVendorScopeFromSessionUser(u);
12746
- if (scope.type === "vendor") return scope.vendorId;
12747
- if (scope.type === "all") {
12748
- const id = u.activeVendorId ?? u.vendorIds?.[0];
12749
- return id != null && Number.isFinite(id) ? id : null;
13116
+ const candidates = [];
13117
+ if (scope.type === "vendor") candidates.push(scope.vendorId);
13118
+ if (scope.type === "all" || scope.type === "vendor") {
13119
+ const preferred = u.activeVendorId ?? u.vendorIds?.[0];
13120
+ if (preferred != null && Number.isFinite(preferred)) candidates.push(Number(preferred));
13121
+ for (const id of u.vendorIds ?? []) {
13122
+ if (Number.isFinite(id)) candidates.push(Number(id));
13123
+ }
13124
+ }
13125
+ if (entityMap.vendors && candidates.length > 0) {
13126
+ const unique = [
13127
+ ...new Set(candidates.filter((id) => id > 0))
13128
+ ];
13129
+ for (const id of unique) {
13130
+ try {
13131
+ const row = await dataSource.getRepository(entityMap.vendors).findOne({
13132
+ where: {
13133
+ id,
13134
+ deleted: false
13135
+ }
13136
+ });
13137
+ if (row) return id;
13138
+ } catch {
13139
+ }
13140
+ }
13141
+ }
13142
+ const uid = u.id != null ? Number(u.id) : NaN;
13143
+ if (Number.isFinite(uid) && entityMap.vendors) {
13144
+ try {
13145
+ const owned = await dataSource.getRepository(entityMap.vendors).findOne({
13146
+ where: {
13147
+ userId: uid,
13148
+ deleted: false
13149
+ },
13150
+ order: {
13151
+ id: "ASC"
13152
+ }
13153
+ });
13154
+ const vid = owned ? Number(owned.id) : NaN;
13155
+ if (Number.isFinite(vid) && vid > 0) return vid;
13156
+ } catch {
13157
+ }
12750
13158
  }
12751
13159
  return null;
12752
13160
  }
12753
13161
  __name(resolveActiveVendorId, "resolveActiveVendorId");
13162
+ async function gateVendorPortal() {
13163
+ const u = await getSessionUser();
13164
+ if (!u?.email) return json({
13165
+ error: "Unauthorized"
13166
+ }, {
13167
+ status: 401
13168
+ });
13169
+ if (!isVendorPortalUser(u) && !isPlatformAdministrator(u)) {
13170
+ return json({
13171
+ error: "Forbidden"
13172
+ }, {
13173
+ status: 403
13174
+ });
13175
+ }
13176
+ const vendorId = await resolveActiveVendorId(u);
13177
+ if (vendorId == null) {
13178
+ return json({
13179
+ error: "No vendor is linked to your account."
13180
+ }, {
13181
+ status: 400
13182
+ });
13183
+ }
13184
+ return {
13185
+ user: u,
13186
+ vendorId
13187
+ };
13188
+ }
13189
+ __name(gateVendorPortal, "gateVendorPortal");
12754
13190
  async function gateVendorTeam() {
12755
13191
  const u = await getSessionUser();
12756
13192
  if (!u?.email) return json({
@@ -12804,6 +13240,40 @@ function createVendorOnboardHandlers(config) {
12804
13240
  }
12805
13241
  __name(trySendVendorOnboardEmails, "trySendVendorOnboardEmails");
12806
13242
  return {
13243
+ /** GET /api/admin/vendor/profile — current user's store (server-resolved vendor id). */
13244
+ async getProfile() {
13245
+ const gated = await gateVendorPortal();
13246
+ if (gated instanceof Response) return gated;
13247
+ const { vendorId } = gated;
13248
+ if (!entityMap.vendors) {
13249
+ return json({
13250
+ error: "Vendors not configured"
13251
+ }, {
13252
+ status: 500
13253
+ });
13254
+ }
13255
+ try {
13256
+ const row = await dataSource.getRepository(entityMap.vendors).findOne({
13257
+ where: {
13258
+ id: vendorId,
13259
+ deleted: false
13260
+ }
13261
+ });
13262
+ if (!row) return json({
13263
+ error: "Vendor not found"
13264
+ }, {
13265
+ status: 404
13266
+ });
13267
+ return json(row);
13268
+ } catch (e) {
13269
+ console.error("[vendor.profile]", e);
13270
+ return json({
13271
+ error: "Failed to load vendor profile"
13272
+ }, {
13273
+ status: 500
13274
+ });
13275
+ }
13276
+ },
12807
13277
  /** POST /api/admin/vendors/onboard — create vendor + owner user + vendor_users + invite */
12808
13278
  async onboard(req) {
12809
13279
  const err = await gateAdmin();
@@ -12872,6 +13342,7 @@ function createVendorOnboardHandlers(config) {
12872
13342
  }
12873
13343
  });
12874
13344
  if (dupVendor) throw new Error("VENDOR_SLUG_EXISTS");
13345
+ await retireSoftDeletedUniqueValue(vendorRepo, "slug", slug);
12875
13346
  let ownerGroup = await groupRepo.findOne({
12876
13347
  where: {
12877
13348
  name: VENDOR_OWNER_GROUP_NAME,
@@ -12900,8 +13371,15 @@ function createVendorOnboardHandlers(config) {
12900
13371
  email: userEmail
12901
13372
  }
12902
13373
  });
12903
- if (existingUser && !existingUser.deleted) throw new Error("USER_EMAIL_EXISTS");
12904
- const newUser = existingUser?.deleted ? await (async () => {
13374
+ if (existingUser && !existingUser.deleted) {
13375
+ if (await userHasActiveVendor(em, entityMap, existingUser.id)) {
13376
+ throw new Error("USER_EMAIL_EXISTS");
13377
+ }
13378
+ if (!await userHadAnyVendor(em, entityMap, existingUser.id)) {
13379
+ throw new Error("USER_EMAIL_EXISTS");
13380
+ }
13381
+ }
13382
+ const newUser = existingUser ? await (async () => {
12905
13383
  await userRepo.update(existingUser.id, {
12906
13384
  deleted: false,
12907
13385
  deletedAt: null,
@@ -15984,10 +16462,12 @@ var Customer = class {
15984
16462
  __name(this, "Customer");
15985
16463
  }
15986
16464
  id;
16465
+ /** Set only when the customer can log in (linked `users` row). Admin/guest customers stay null. */
15987
16466
  userId;
15988
16467
  user;
15989
16468
  name;
15990
16469
  email;
16470
+ /** Optional; multiple customers may have null (PostgreSQL UNIQUE allows multiple NULLs). */
15991
16471
  phone;
15992
16472
  createdAt;
15993
16473
  updatedAt;
@@ -16002,8 +16482,10 @@ _ts_decorate18([
16002
16482
  _ts_metadata18("design:type", Number)
16003
16483
  ], Customer.prototype, "id", void 0);
16004
16484
  _ts_decorate18([
16005
- Column("int"),
16006
- _ts_metadata18("design:type", Number)
16485
+ Column("int", {
16486
+ nullable: true
16487
+ }),
16488
+ _ts_metadata18("design:type", Object)
16007
16489
  ], Customer.prototype, "userId", void 0);
16008
16490
  _ts_decorate18([
16009
16491
  ManyToOne(() => User, {
@@ -16026,9 +16508,10 @@ _ts_decorate18([
16026
16508
  ], Customer.prototype, "email", void 0);
16027
16509
  _ts_decorate18([
16028
16510
  Column("varchar", {
16029
- unique: true
16511
+ unique: true,
16512
+ nullable: true
16030
16513
  }),
16031
- _ts_metadata18("design:type", String)
16514
+ _ts_metadata18("design:type", Object)
16032
16515
  ], Customer.prototype, "phone", void 0);
16033
16516
  _ts_decorate18([
16034
16517
  Column({
@@ -26612,6 +27095,14 @@ function createCmsApiHandler(config) {
26612
27095
  });
26613
27096
  return vendorHandlers.switchVendor(req);
26614
27097
  }
27098
+ if (path2[0] === "admin" && path2[1] === "vendor" && path2[2] === "profile" && path2.length === 3 && m === "GET") {
27099
+ if (!vendorHandlers) return config.json({
27100
+ error: "Not found"
27101
+ }, {
27102
+ status: 404
27103
+ });
27104
+ return vendorHandlers.getProfile();
27105
+ }
26615
27106
  if (path2[0] === "admin" && path2[1] === "vendor" && path2[2] === "roles" && vendorRolesHandlers) {
26616
27107
  if (path2.length === 3 && m === "GET") return vendorRolesHandlers.list();
26617
27108
  if (path2.length === 3 && m === "POST") return vendorRolesHandlers.create(req);
@@ -29081,13 +29572,36 @@ function createStorefrontApiHandler(config) {
29081
29572
  status: 400
29082
29573
  };
29083
29574
  }
29575
+ const list = [
29576
+ ...vendorIds
29577
+ ];
29084
29578
  return {
29085
- vendorId: [
29086
- ...vendorIds
29087
- ][0]
29579
+ vendorId: list[0],
29580
+ vendorIds: list
29088
29581
  };
29089
29582
  }
29090
29583
  __name(resolveSingleVendorIdFromCart, "resolveSingleVendorIdFromCart");
29584
+ async function linkOrderContactToVendors(contactId, vendorIds) {
29585
+ try {
29586
+ const contact = await contactRepo().findOne({
29587
+ where: {
29588
+ id: contactId,
29589
+ deleted: false
29590
+ }
29591
+ });
29592
+ if (!contact) return;
29593
+ const email = String(contact.email ?? "").trim().toLowerCase();
29594
+ if (!email) return;
29595
+ await ensureVendorCustomersForOrder(dataSource, entityMap, vendorIds, contactId, {
29596
+ name: String(contact.name ?? "").trim() || email.split("@")[0] || "Customer",
29597
+ email,
29598
+ phone: contact.phone ?? null
29599
+ });
29600
+ } catch (err) {
29601
+ console.error("[storefront] vendor_customers link failed", err);
29602
+ }
29603
+ }
29604
+ __name(linkOrderContactToVendors, "linkOrderContactToVendors");
29091
29605
  function roundMoney3(n) {
29092
29606
  return Math.round(n * 100) / 100;
29093
29607
  }
@@ -29805,7 +30319,8 @@ function createStorefrontApiHandler(config) {
29805
30319
  const repo = dataSource.getRepository(entityMap.product_variants);
29806
30320
  const rows = await repo.find({
29807
30321
  where: {
29808
- productId
30322
+ productId,
30323
+ status: "available"
29809
30324
  },
29810
30325
  order: {
29811
30326
  id: "ASC"
@@ -29838,6 +30353,52 @@ function createStorefrontApiHandler(config) {
29838
30353
  return result;
29839
30354
  }
29840
30355
  __name(loadProductVariantsWithPricing, "loadProductVariantsWithPricing");
30356
+ async function loadApplicableVendorPolicies(productId, vendorId) {
30357
+ if (!entityMap.refund_policies) return [];
30358
+ const policyRepo = dataSource.getRepository(entityMap.refund_policies);
30359
+ const format = /* @__PURE__ */ __name((row) => ({
30360
+ id: Number(row.id),
30361
+ name: String(row.name ?? "").trim(),
30362
+ desc: row.desc != null ? String(row.desc) : null,
30363
+ refundWindowDays: Number(row.refundWindowDays) || 0,
30364
+ type: String(row.type ?? "percentage"),
30365
+ value: Number(row.value) || 0
30366
+ }), "format");
30367
+ const linkedIds = /* @__PURE__ */ new Set();
30368
+ if (entityMap.product_config) {
30369
+ const configs = await dataSource.getRepository(entityMap.product_config).find({
30370
+ where: {
30371
+ productId
30372
+ }
30373
+ });
30374
+ for (const c of configs) {
30375
+ const id = Number(c.refundPolicyId);
30376
+ if (Number.isFinite(id) && id > 0) linkedIds.add(id);
30377
+ }
30378
+ }
30379
+ if (linkedIds.size > 0) {
30380
+ const rows = await policyRepo.find({
30381
+ where: {
30382
+ id: In([
30383
+ ...linkedIds
30384
+ ])
30385
+ }
30386
+ });
30387
+ return rows.map((r) => format(r)).filter((p) => p.name).sort((a, b) => a.id - b.id);
30388
+ }
30389
+ if (vendorId == null || !Number.isFinite(Number(vendorId))) return [];
30390
+ const vendorRows = await policyRepo.find({
30391
+ where: {
30392
+ vendorId: Number(vendorId),
30393
+ status: "active"
30394
+ },
30395
+ order: {
30396
+ id: "ASC"
30397
+ }
30398
+ });
30399
+ return vendorRows.map((r) => format(r)).filter((p) => p.name);
30400
+ }
30401
+ __name(loadApplicableVendorPolicies, "loadApplicableVendorPolicies");
29841
30402
  return {
29842
30403
  async handle(method, path2, req) {
29843
30404
  try {
@@ -29905,13 +30466,11 @@ function createStorefrontApiHandler(config) {
29905
30466
  const url = new URL(req.url || "", "http://localhost");
29906
30467
  const collectionSlug = url.searchParams.get("collection")?.trim();
29907
30468
  const collectionId = url.searchParams.get("collectionId");
30469
+ const q = url.searchParams.get("q")?.trim() ?? "";
29908
30470
  const limit = Math.min(100, Math.max(1, parseInt(url.searchParams.get("limit") || "20", 10)));
29909
30471
  const offset = Math.max(0, parseInt(url.searchParams.get("offset") || "0", 10));
29910
- const where = {
29911
- status: "available",
29912
- deleted: false
29913
- };
29914
30472
  let collectionFilter = null;
30473
+ let collectionIdFilter = null;
29915
30474
  if (collectionSlug) {
29916
30475
  let col = null;
29917
30476
  if (/^\d+$/.test(collectionSlug)) {
@@ -29938,30 +30497,59 @@ function createStorefrontApiHandler(config) {
29938
30497
  collection: null
29939
30498
  });
29940
30499
  }
29941
- where.collectionId = col.id;
30500
+ collectionIdFilter = Number(col.id);
29942
30501
  collectionFilter = {
29943
30502
  name: col.name,
29944
30503
  slug: col.slug
29945
30504
  };
29946
30505
  } else if (collectionId) {
29947
30506
  const cid = parseInt(collectionId, 10);
29948
- if (Number.isFinite(cid)) where.collectionId = cid;
30507
+ if (Number.isFinite(cid)) collectionIdFilter = cid;
30508
+ }
30509
+ let items = [];
30510
+ let total = 0;
30511
+ if (q) {
30512
+ const like = `%${q.replace(/[%_]/g, "\\$&")}%`;
30513
+ const qb = productRepo().createQueryBuilder("p").where("p.status = :status", {
30514
+ status: "available"
30515
+ }).andWhere("p.deleted = :del", {
30516
+ del: false
30517
+ }).andWhere(`(p.name ILIKE :like OR p.slug ILIKE :like OR COALESCE(p.sku, '') ILIKE :like OR COALESCE(p.title, '') ILIKE :like OR COALESCE(p.metadata->>'description', '') ILIKE :like)`, {
30518
+ like
30519
+ }).orderBy("p.id", "ASC").take(limit).skip(offset);
30520
+ if (collectionIdFilter != null) {
30521
+ qb.andWhere("p.collectionId = :cid", {
30522
+ cid: collectionIdFilter
30523
+ });
30524
+ }
30525
+ [items, total] = await qb.getManyAndCount();
30526
+ } else {
30527
+ const where = {
30528
+ status: "available",
30529
+ deleted: false
30530
+ };
30531
+ if (collectionIdFilter != null) where.collectionId = collectionIdFilter;
30532
+ const result = await productRepo().findAndCount({
30533
+ where,
30534
+ order: {
30535
+ id: "ASC"
30536
+ },
30537
+ take: limit,
30538
+ skip: offset
30539
+ });
30540
+ items = result[0];
30541
+ total = result[1];
29949
30542
  }
29950
- const [items, total] = await productRepo().findAndCount({
29951
- where,
29952
- order: {
29953
- id: "ASC"
29954
- },
29955
- take: limit,
29956
- skip: offset
29957
- });
29958
30543
  const products = await Promise.all(items.map((item) => enrichProductPricing(item)));
29959
30544
  return json({
29960
30545
  products,
29961
30546
  total,
29962
30547
  ...collectionFilter && {
29963
30548
  collection: collectionFilter
29964
- }
30549
+ },
30550
+ ...q ? {
30551
+ q
30552
+ } : {}
29965
30553
  });
29966
30554
  }
29967
30555
  if (path2[0] === "products" && path2.length === 2 && method === "GET") {
@@ -29997,12 +30585,14 @@ function createStorefrontApiHandler(config) {
29997
30585
  const pricing = await resolveProductEventPricing(Number(p.id));
29998
30586
  const enriched = await enrichProductPricing(p, pricing);
29999
30587
  const variants = await loadProductVariantsWithPricing(Number(p.id), pricing);
30588
+ const policies = await loadApplicableVendorPolicies(Number(p.id), p.vendorId != null ? Number(p.vendorId) : null);
30000
30589
  return json({
30001
30590
  ...enriched,
30002
30591
  attributes: attributeTags,
30003
30592
  ...variants.length ? {
30004
30593
  variants
30005
- } : {}
30594
+ } : {},
30595
+ policies
30006
30596
  });
30007
30597
  }
30008
30598
  if (path2[0] === "collections" && path2.length === 1 && method === "GET") {
@@ -30814,24 +31404,61 @@ function createStorefrontApiHandler(config) {
30814
31404
  }, {
30815
31405
  status: 404
30816
31406
  });
31407
+ const rawVariantId = body.variantId ?? body.variant_id;
31408
+ const variantIdNum = rawVariantId != null && String(rawVariantId).trim() !== "" ? Number(rawVariantId) : NaN;
31409
+ const hasVariantId = Number.isFinite(variantIdNum) && variantIdNum > 0;
31410
+ const bodyMeta = body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata) ? body.metadata : {};
31411
+ const optionsRaw = body.options && typeof body.options === "object" && !Array.isArray(body.options) ? body.options : bodyMeta.options && typeof bodyMeta.options === "object" && !Array.isArray(bodyMeta.options) ? bodyMeta.options : null;
31412
+ const options = optionsRaw && Object.fromEntries(Object.entries(optionsRaw).map(([k, v]) => [
31413
+ String(k),
31414
+ String(v ?? "").trim()
31415
+ ]).filter(([, v]) => v.length > 0));
31416
+ const lineMetadata = {
31417
+ ...bodyMeta,
31418
+ ...hasVariantId ? {
31419
+ variantId: variantIdNum
31420
+ } : {},
31421
+ ...options && Object.keys(options).length ? {
31422
+ options
31423
+ } : {}
31424
+ };
31425
+ if (typeof bodyMeta.imageUrl === "string" && bodyMeta.imageUrl.trim()) {
31426
+ lineMetadata.imageUrl = bodyMeta.imageUrl.trim();
31427
+ }
31428
+ if (typeof bodyMeta.title === "string" && bodyMeta.title.trim()) {
31429
+ lineMetadata.title = bodyMeta.title.trim();
31430
+ }
31431
+ const metadataPayload = Object.keys(lineMetadata).length ? lineMetadata : null;
30817
31432
  const { cart, setCookie, err } = await getOrCreateCart(req);
30818
31433
  if (err) return err;
30819
31434
  const cartId = cart.id;
30820
- const existing = await cartItemRepo().findOne({
31435
+ const sameProductLines = await cartItemRepo().find({
30821
31436
  where: {
30822
31437
  cartId,
30823
31438
  productId
30824
31439
  }
30825
31440
  });
31441
+ const existing = sameProductLines.find((row) => {
31442
+ const m = row.metadata;
31443
+ const existingVid = m?.variantId ?? m?.variant_id;
31444
+ if (hasVariantId) {
31445
+ return Number(existingVid) === variantIdNum;
31446
+ }
31447
+ return existingVid == null || existingVid === "";
31448
+ });
30826
31449
  if (existing) {
30827
31450
  await cartItemRepo().update(existing.id, {
30828
- quantity: existing.quantity + quantity
31451
+ quantity: existing.quantity + quantity,
31452
+ ...metadataPayload ? {
31453
+ metadata: metadataPayload
31454
+ } : {}
30829
31455
  });
30830
31456
  } else {
30831
31457
  await cartItemRepo().save(cartItemRepo().create({
30832
31458
  cartId,
30833
31459
  productId,
30834
- quantity
31460
+ quantity,
31461
+ metadata: metadataPayload
30835
31462
  }));
30836
31463
  }
30837
31464
  await cartRepo().update(cartId, {
@@ -31296,6 +31923,7 @@ function createStorefrontApiHandler(config) {
31296
31923
  taxCode: line.taxCode
31297
31924
  }));
31298
31925
  }
31926
+ await linkOrderContactToVendors(contactId, vendorRes.vendorIds);
31299
31927
  fireOrderPlacedNotification(oid);
31300
31928
  return json({
31301
31929
  orderId: oid,
@@ -31368,6 +31996,7 @@ function createStorefrontApiHandler(config) {
31368
31996
  taxCode: line.taxCode
31369
31997
  }));
31370
31998
  }
31999
+ await linkOrderContactToVendors(contactId, vendorResChk.vendorIds);
31371
32000
  await cartItemRepo().delete({
31372
32001
  cartId: cart.id
31373
32002
  });
@@ -31614,4 +32243,4 @@ function createStorefrontApiHandler(config) {
31614
32243
  }
31615
32244
  __name(createStorefrontApiHandler, "createStorefrontApiHandler");
31616
32245
 
31617
- export { Address, Attendee, Attribute, BLOG_GENERATOR_AGENT_NAME, BLOG_GENERATOR_DEFAULT_SYSTEM_INSTRUCTION, BLOG_GENERATOR_DEFAULT_VALIDATION_RULES, BLOG_GENERATOR_LLM_AGENT_SLUG, BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR, BLOG_METADATA_ENRICHER_AGENT_NAME, BLOG_METADATA_ENRICHER_DEFAULT_SYSTEM_INSTRUCTION, BLOG_METADATA_ENRICHER_DEFAULT_VALIDATION_RULES, BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG, Blog, BlogGeneratorService, Brand, CMS_ENTITY_MAP, Cart, CartItem, Category, ChatConversation, ChatMessage, Collection, Combo, ComboItem, Comment, Config, Contact, Currency, CurrencyExchange, Customer, Customer_Contacts, Discount, DiscountRules, Event, EventProduct, Form, FormField, FormSubmission, JobSchedule, JobScheduleRun, KnowledgeBaseChunk, KnowledgeBaseDocument, LlmAgent, Media, MessageTemplate, Order, OrderAddresses, OrderDiscounts, OrderItem, OrderNotificationBinding, OrderNotificationTrigger, OtpChallenge, Page, PasswordResetToken, Payment, Permission, Product, ProductAttribute, ProductCategory, ProductConfig, ProductVariant, RefundPolicy, RefundRequest, RssArticle, RssFeed, Seo, Tag, Tax, User, UserGroup, Vendor, VendorCustomer, VendorRole, VendorRolePermission, VendorUser, Wishlist, WishlistItem, ZIP_MIME_TYPES, applyApprovalStatusSideEffects, applyEventApprovalStatusSideEffects, applyRotatingVendorInvite, applyVendorCustomersContactFilter, applyVendorEventCreateApproval, applyVendorProductCreateApproval, assertCaptchaOk, assertContactAllowedForVendorOrder, assertEventApprovalUpdate, assertProductApprovalUpdate, buildBlogMetadataUserPrompt, buildCronFromSchedule, buildRssUserPromptFromFeeds, buildVendorInviteLink, calculateOrderRefundPreview, calculateRefundFromPolicy, checkEventsEnabled, checkMultiVendorEnabled, contactIsVendorCustomer, countRecentOtpSends, createAnalyticsHandlers, createBlogBySlugHandler, createChangePasswordHandler, createCmsApiHandler, createCmsApp, createCmsAppWithMessaging, createCrudByIdHandler, createCrudHandler, createDashboardStatsHandler, createEcommerceAnalyticsHandler, createEventOrderMessageTemplateHandlers, createForgotPasswordHandler, createFormBySlugHandler, createInviteAcceptHandler, createJobScheduleHandlers, createLlmAgentKnowledgeHandlers, createMediaZipExtractHandler, createMessageTemplateRowLoader, createOtpChallenge, createSetPasswordHandler, createSettingsApiHandlers, createSocialMediaHandlers, createStorefrontApiHandler, createUploadHandler, createUserAuthApiRouter, createUserAvatarHandler, createUserProfileHandler, createUsersApiHandlers, createVendorDashboardHandler, createVendorOnboardHandlers, customerPhoneForUser, daysBeforeEventStart, describeEventTierPolicy, ensureCustomerForUser, ensureMessagingPluginsOnCms, ensureScheduleQueueWorker, ensureVendorCustomerForOrderContact, findActiveRefundPolicyForVendor, findVendorByInviteToken, formatTierRange, generateNumericOtp, getPublicSettingsGroup, getRequireEventApproval, getRequireProductApproval, getRssArticleSummaryFromItem, getVendorCatalogCreateFlags, hashOtpCode, hydrateVendorSessionUser, invalidateEventsCache, invalidateMultiVendorCache, invalidateRequireEventApprovalCache, invalidateRequireProductApprovalCache, invalidateVendorCatalogCreateFlagsCache, isCustomerTypeContact, isZipMedia, linkUnclaimedContactToUser, llmAgentToChatAgentOptions, loadSettingsGroupFromDb, loadUserVendorContext, mergeGuardrailsIntoSystemPrompt, messagingPlugins, metaFetchUserManagedPages, metaPostPageFeed, metaPostPagePhoto, metaResolvePageAccessToken, normalizePhoneE164, normalizeRefundTiers, overlayCmsPlugins, parseBlogGeneratorAgentContent, parseBlogGeneratorModelOutput, parseBlogMetadataEnrichmentJson, parseLlmAgentValidationRules, pgBossScheduleNameForId, queueErpCreateContactIfEnabled, queueJobScheduleNow, queuePlugin, queueSms, registerJobRunnerWorker, registerMessagingQueueProcessors, registerSmsQueueProcessor, relativePathFromMediaParentId, resolveBlogCategoryIdByName, resolveSettingsEncryptionKey, resolveVendorIdForContactCheck, sanitizeMediaFolderPath, sanitizeStorageSegment, sendVendorOnboardEmails, simpleDecrypt, simpleEncrypt, syncJobScheduleToPgBoss, validateRefundTiers, validateScheduleInput, validateUserMessageAgainstAgentRules, validateUserMessageAgainstStructuredRules, verifyAndConsumeOtpChallenge, verifyOtpCodeHash, whatsappPlugin, wrapGetCmsWithMessaging };
32246
+ export { Address, Attendee, Attribute, BLOG_GENERATOR_AGENT_NAME, BLOG_GENERATOR_DEFAULT_SYSTEM_INSTRUCTION, BLOG_GENERATOR_DEFAULT_VALIDATION_RULES, BLOG_GENERATOR_LLM_AGENT_SLUG, BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR, BLOG_METADATA_ENRICHER_AGENT_NAME, BLOG_METADATA_ENRICHER_DEFAULT_SYSTEM_INSTRUCTION, BLOG_METADATA_ENRICHER_DEFAULT_VALIDATION_RULES, BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG, Blog, BlogGeneratorService, Brand, CMS_ENTITY_MAP, Cart, CartItem, Category, ChatConversation, ChatMessage, Collection, Combo, ComboItem, Comment, Config, Contact, Currency, CurrencyExchange, Customer, Customer_Contacts, Discount, DiscountRules, Event, EventProduct, Form, FormField, FormSubmission, JobSchedule, JobScheduleRun, KnowledgeBaseChunk, KnowledgeBaseDocument, LlmAgent, Media, MessageTemplate, Order, OrderAddresses, OrderDiscounts, OrderItem, OrderNotificationBinding, OrderNotificationTrigger, OtpChallenge, Page, PasswordResetToken, Payment, Permission, Product, ProductAttribute, ProductCategory, ProductConfig, ProductVariant, RefundPolicy, RefundRequest, RssArticle, RssFeed, Seo, Tag, Tax, User, UserGroup, Vendor, VendorCustomer, VendorRole, VendorRolePermission, VendorUser, Wishlist, WishlistItem, ZIP_MIME_TYPES, applyApprovalStatusSideEffects, applyEventApprovalStatusSideEffects, applyRotatingVendorInvite, applyVendorCustomersContactFilter, applyVendorEventCreateApproval, applyVendorProductCreateApproval, assertCaptchaOk, assertContactAllowedForVendorOrder, assertEventApprovalUpdate, assertProductApprovalUpdate, buildBlogMetadataUserPrompt, buildCronFromSchedule, buildRssUserPromptFromFeeds, buildVendorInviteLink, calculateOrderRefundPreview, calculateRefundFromPolicy, checkEventsEnabled, checkMultiVendorEnabled, contactIsVendorCustomer, countRecentOtpSends, createAnalyticsHandlers, createBlogBySlugHandler, createChangePasswordHandler, createCmsApiHandler, createCmsApp, createCmsAppWithMessaging, createCrudByIdHandler, createCrudHandler, createDashboardStatsHandler, createEcommerceAnalyticsHandler, createEventOrderMessageTemplateHandlers, createForgotPasswordHandler, createFormBySlugHandler, createInviteAcceptHandler, createJobScheduleHandlers, createLlmAgentKnowledgeHandlers, createMediaZipExtractHandler, createMessageTemplateRowLoader, createOtpChallenge, createSetPasswordHandler, createSettingsApiHandlers, createSocialMediaHandlers, createStorefrontApiHandler, createUploadHandler, createUserAuthApiRouter, createUserAvatarHandler, createUserProfileHandler, createUsersApiHandlers, createVendorDashboardHandler, createVendorOnboardHandlers, customerPhoneForEmail, customerPhoneForUser, daysBeforeEventStart, describeEventTierPolicy, ensureCustomerForUser, ensureCustomerRecord, ensureMessagingPluginsOnCms, ensureScheduleQueueWorker, ensureVendorCustomerForOrderContact, ensureVendorCustomersForOrder, findActiveRefundPolicyForVendor, findVendorByInviteToken, formatTierRange, generateNumericOtp, getPublicSettingsGroup, getRequireEventApproval, getRequireProductApproval, getRssArticleSummaryFromItem, getVendorCatalogCreateFlags, hashOtpCode, hydrateVendorSessionUser, invalidateEventsCache, invalidateMultiVendorCache, invalidateRequireEventApprovalCache, invalidateRequireProductApprovalCache, invalidateVendorCatalogCreateFlagsCache, isCustomerTypeContact, isSyntheticCustomerPhone, isZipMedia, linkUnclaimedContactToUser, llmAgentToChatAgentOptions, loadSettingsGroupFromDb, loadUserVendorContext, mergeGuardrailsIntoSystemPrompt, messagingPlugins, metaFetchUserManagedPages, metaPostPageFeed, metaPostPagePhoto, metaResolvePageAccessToken, normalizeCustomerPhone, normalizePhoneE164, normalizeRefundTiers, overlayCmsPlugins, parseBlogGeneratorAgentContent, parseBlogGeneratorModelOutput, parseBlogMetadataEnrichmentJson, parseLlmAgentValidationRules, pgBossScheduleNameForId, queueErpCreateContactIfEnabled, queueJobScheduleNow, queuePlugin, queueSms, registerJobRunnerWorker, registerMessagingQueueProcessors, registerSmsQueueProcessor, relativePathFromMediaParentId, resolveBlogCategoryIdByName, resolveSettingsEncryptionKey, resolveVendorIdForContactCheck, sanitizeMediaFolderPath, sanitizeStorageSegment, sendVendorOnboardEmails, simpleDecrypt, simpleEncrypt, syncJobScheduleToPgBoss, validateRefundTiers, validateScheduleInput, validateUserMessageAgainstAgentRules, validateUserMessageAgainstStructuredRules, verifyAndConsumeOtpChallenge, verifyOtpCodeHash, whatsappPlugin, wrapGetCmsWithMessaging };