@infuro/cms-core 1.0.44 → 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];
@@ -1090,11 +1120,118 @@ function pgErrorCode(err) {
1090
1120
  return driver?.code ?? err.code;
1091
1121
  }
1092
1122
  __name(pgErrorCode, "pgErrorCode");
1093
- function customerPhoneForUser(userId, phone) {
1123
+ function normalizeCustomerPhone(phone) {
1094
1124
  const p = typeof phone === "string" ? phone.trim() : "";
1095
- 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);
1096
1138
  }
1097
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");
1098
1235
  async function restoreCustomerRow(repo, row, user, name, email, phone) {
1099
1236
  const id = row.id;
1100
1237
  await repo.update(id, {
@@ -1116,7 +1253,7 @@ async function ensureCustomerForUser(dsOrEm, customerEntity, user, overrides) {
1116
1253
  const repo = dsOrEm.getRepository(customerEntity);
1117
1254
  const email = normalizeEmail(user.email);
1118
1255
  const name = String(user.name ?? "").trim() || email.split("@")[0] || "User";
1119
- const phone = customerPhoneForUser(user.id, overrides?.phone ?? user.phone);
1256
+ const phone = normalizeCustomerPhone(overrides?.phone ?? user.phone);
1120
1257
  let row = await repo.findOne({
1121
1258
  where: {
1122
1259
  userId: user.id,
@@ -1124,10 +1261,11 @@ async function ensureCustomerForUser(dsOrEm, customerEntity, user, overrides) {
1124
1261
  }
1125
1262
  });
1126
1263
  if (row) {
1264
+ const nextPhone = resolveNextPhone(phone, row.phone);
1127
1265
  await repo.update(row.id, {
1128
1266
  name,
1129
1267
  email,
1130
- phone,
1268
+ phone: nextPhone,
1131
1269
  updatedAt: /* @__PURE__ */ new Date()
1132
1270
  });
1133
1271
  return {
@@ -1145,10 +1283,11 @@ async function ensureCustomerForUser(dsOrEm, customerEntity, user, overrides) {
1145
1283
  if (existingUserId != null && existingUserId !== user.id) {
1146
1284
  return null;
1147
1285
  }
1286
+ const nextPhone = resolveNextPhone(phone, row.phone);
1148
1287
  await repo.update(row.id, {
1149
1288
  userId: user.id,
1150
1289
  name,
1151
- phone,
1290
+ phone: nextPhone,
1152
1291
  updatedAt: /* @__PURE__ */ new Date()
1153
1292
  });
1154
1293
  return {
@@ -1163,21 +1302,23 @@ async function ensureCustomerForUser(dsOrEm, customerEntity, user, overrides) {
1163
1302
  if (deletedByEmail) {
1164
1303
  const existingUserId = deletedByEmail.userId;
1165
1304
  if (existingUserId != null && existingUserId !== user.id) return null;
1166
- return restoreCustomerRow(repo, deletedByEmail, user, name, email, phone);
1305
+ return restoreCustomerRow(repo, deletedByEmail, user, name, email, resolveNextPhone(phone, deletedByEmail.phone));
1167
1306
  }
1168
- const deletedByPhone = await repo.findOne({
1169
- where: {
1170
- phone
1171
- }
1172
- });
1173
- if (deletedByPhone) {
1174
- const existingUserId = deletedByPhone.userId;
1175
- if (existingUserId != null && existingUserId !== user.id) {
1176
- return ensureCustomerForUser(dsOrEm, customerEntity, user, {
1177
- phone: `u-${user.id}-${Date.now()}`
1178
- });
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);
1179
1321
  }
1180
- return restoreCustomerRow(repo, deletedByPhone, user, name, email, phone);
1181
1322
  }
1182
1323
  try {
1183
1324
  const created = await repo.save(repo.create({
@@ -1198,34 +1339,22 @@ async function ensureCustomerForUser(dsOrEm, customerEntity, user, overrides) {
1198
1339
  userId: user.id
1199
1340
  }
1200
1341
  });
1201
- 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
+ }
1202
1345
  row = await repo.findOne({
1203
1346
  where: {
1204
1347
  email
1205
1348
  }
1206
1349
  });
1207
1350
  if (row && (row.userId ?? user.id) === user.id) {
1208
- return restoreCustomerRow(repo, row, user, name, email, phone);
1351
+ return restoreCustomerRow(repo, row, user, name, email, resolveNextPhone(phone, row.phone));
1209
1352
  }
1210
1353
  if (code === "23505") return null;
1211
1354
  throw err;
1212
1355
  }
1213
1356
  }
1214
1357
  __name(ensureCustomerForUser, "ensureCustomerForUser");
1215
- async function linkUnclaimedContactToUser(dataSource, contactsEntity, userId, email) {
1216
- const repo = dataSource.getRepository(contactsEntity);
1217
- const found = await repo.findOne({
1218
- where: {
1219
- email,
1220
- userId: IsNull(),
1221
- deleted: false
1222
- }
1223
- });
1224
- if (found) await repo.update(found.id, {
1225
- userId
1226
- });
1227
- }
1228
- __name(linkUnclaimedContactToUser, "linkUnclaimedContactToUser");
1229
1358
 
1230
1359
  // src/lib/vendor-customer-contacts.ts
1231
1360
  function isCustomerTypeContact(type) {
@@ -1318,8 +1447,27 @@ function resolveVendorIdForContactCheck(scope, bodyOrRow) {
1318
1447
  return Number.isFinite(n) ? n : null;
1319
1448
  }
1320
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");
1321
1462
  async function ensureVendorCustomerForOrderContact(dataSource, entityMap, vendorId, contactId, details) {
1322
- 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;
1323
1471
  const vcEntity = entityMap.vendor_customers;
1324
1472
  const contactEntity = entityMap.contacts;
1325
1473
  if (!vcEntity || !contactEntity) return;
@@ -1345,96 +1493,61 @@ async function ensureVendorCustomerForOrderContact(dataSource, entityMap, vendor
1345
1493
  }
1346
1494
  let customerId = null;
1347
1495
  if (entityMap.customer) {
1348
- const customerRepo = dataSource.getRepository(entityMap.customer);
1349
- const existingCustomer = await customerRepo.findOne({
1350
- where: {
1351
- email,
1352
- deleted: false
1353
- }
1496
+ const ensured = await ensureCustomerRecord(dataSource, entityMap.customer, {
1497
+ name,
1498
+ email,
1499
+ phone,
1500
+ userId: null
1354
1501
  });
1355
- if (existingCustomer) {
1356
- customerId = Number(existingCustomer.id);
1357
- } else if (entityMap.users) {
1358
- const userRepo = dataSource.getRepository(entityMap.users);
1359
- 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({
1360
1507
  where: {
1361
- email,
1362
- deleted: false
1508
+ vendorId,
1509
+ customerId
1363
1510
  }
1364
1511
  });
1365
- if (!user) {
1366
- let groupId = null;
1367
- if (entityMap.user_groups) {
1368
- const userGroupRepo = dataSource.getRepository(entityMap.user_groups);
1369
- const customerGroup = await userGroupRepo.findOne({
1370
- where: {
1371
- name: "Customer",
1372
- deleted: false
1373
- }
1512
+ if (byCustomer) {
1513
+ const existingContactId = byCustomer.contactId;
1514
+ if (existingContactId == null) {
1515
+ await vcRepo.update(byCustomer.id, {
1516
+ contactId
1374
1517
  });
1375
- if (customerGroup) groupId = Number(customerGroup.id);
1376
1518
  }
1377
- user = await userRepo.save(userRepo.create({
1378
- name,
1379
- email,
1380
- phone,
1381
- password: null,
1382
- blocked: false,
1383
- groupId,
1384
- adminAccess: false
1385
- }));
1519
+ continue;
1386
1520
  }
1387
- const userId = Number(user.id);
1388
- await linkUnclaimedContactToUser(dataSource, contactEntity, userId, email);
1389
- const ensured = await ensureCustomerForUser(dataSource, entityMap.customer, {
1390
- id: userId,
1391
- name,
1392
- email,
1393
- phone
1394
- }, {
1395
- phone
1396
- });
1397
- if (ensured) customerId = ensured.id;
1398
1521
  }
1399
- }
1400
- if (customerId != null) {
1401
- const byCustomer = await vcRepo.findOne({
1522
+ const byContact = await vcRepo.findOne({
1402
1523
  where: {
1403
1524
  vendorId,
1404
- customerId
1525
+ contactId
1405
1526
  }
1406
1527
  });
1407
- if (byCustomer) {
1408
- const existingContactId = byCustomer.contactId;
1409
- if (existingContactId == null) {
1410
- await vcRepo.update(byCustomer.id, {
1411
- contactId
1528
+ if (byContact) {
1529
+ if (customerId != null && byContact.customerId == null) {
1530
+ await vcRepo.update(byContact.id, {
1531
+ customerId
1412
1532
  });
1413
1533
  }
1414
- return;
1534
+ continue;
1415
1535
  }
1416
- }
1417
- const byContact = await vcRepo.findOne({
1418
- where: {
1419
- vendorId,
1420
- 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
+ }));
1421
1547
  }
1422
- });
1423
- if (byContact) return;
1424
- if (customerId != null) {
1425
- await vcRepo.save(vcRepo.create({
1426
- vendorId,
1427
- customerId,
1428
- contactId
1429
- }));
1430
- return;
1431
1548
  }
1432
- await vcRepo.save(vcRepo.create({
1433
- vendorId,
1434
- contactId
1435
- }));
1436
1549
  }
1437
- __name(ensureVendorCustomerForOrderContact, "ensureVendorCustomerForOrderContact");
1550
+ __name(ensureVendorCustomersForOrder, "ensureVendorCustomersForOrder");
1438
1551
 
1439
1552
  // src/lib/currency-prices.ts
1440
1553
  function normalizeCurrencyCode(code) {
@@ -3800,23 +3913,24 @@ function createCrudHandler(dataSource, entityMap, options) {
3800
3913
  if (resource === "vendor_customers" && entityMap["customer"]) {
3801
3914
  const scope = await resolveVendorCustomersScope(dataSource, entityMap, resolveScope);
3802
3915
  const repo2 = dataSource.getRepository(entity);
3803
- 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);
3804
3917
  applyVendorScopeToQueryBuilder(qb, "vc", scope);
3805
3918
  if (search && typeof search === "string" && search.trim()) {
3806
3919
  const term = `%${search.trim()}%`;
3807
- 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)", {
3808
3921
  term
3809
3922
  });
3810
3923
  }
3811
3924
  const [rows, total2] = await qb.getManyAndCount();
3812
3925
  const data2 = rows.map((row) => {
3813
3926
  const customer = row.customer;
3927
+ const contact = row.contact;
3814
3928
  return {
3815
3929
  ...row,
3816
- name: customer?.name ?? null,
3817
- email: customer?.email ?? null,
3818
- phone: customer?.phone ?? null,
3819
- 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
3820
3934
  };
3821
3935
  });
3822
3936
  return json({
@@ -4150,7 +4264,11 @@ function createCrudHandler(dataSource, entityMap, options) {
4150
4264
  } else if (resource === "collections") {
4151
4265
  if (scope.type === "vendor") {
4152
4266
  where = mergeVendorScopeIntoWhere(where, scope, resource, catalogFlags);
4153
- } 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") {
4154
4272
  where = mergeListWhereAnd(where, {
4155
4273
  isCatalog: false
4156
4274
  });
@@ -4347,41 +4465,17 @@ function createCrudHandler(dataSource, entityMap, options) {
4347
4465
  status: 503
4348
4466
  });
4349
4467
  }
4350
- if (!entityMap["users"]) {
4351
- return json({
4352
- error: "Users entity not configured"
4353
- }, {
4354
- status: 503
4355
- });
4356
- }
4357
- let customerGroupId = null;
4358
- if (entityMap["user_groups"]) {
4359
- const userGroupRepo = dataSource.getRepository(entityMap["user_groups"]);
4360
- const customerGroup = await userGroupRepo.findOne({
4361
- where: {
4362
- name: "Customer",
4363
- deleted: false
4364
- }
4365
- });
4366
- if (!customerGroup) {
4367
- return json({
4368
- error: "User group 'customer' not found"
4369
- }, {
4370
- status: 500
4371
- });
4372
- }
4373
- customerGroupId = Number(customerGroup.id);
4374
- } else {
4468
+ if (!entityMap["contacts"]) {
4375
4469
  return json({
4376
- error: "user_groups entity not configured"
4470
+ error: "Contacts entity not configured"
4377
4471
  }, {
4378
4472
  status: 503
4379
4473
  });
4380
4474
  }
4381
4475
  const name = String(body.name ?? "").trim();
4382
- const email = String(body.email ?? "").trim();
4383
- const phone = String(body.phone ?? "").trim();
4384
- 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;
4385
4479
  if (!name) return json({
4386
4480
  error: "name is required"
4387
4481
  }, {
@@ -4392,25 +4486,7 @@ function createCrudHandler(dataSource, entityMap, options) {
4392
4486
  }, {
4393
4487
  status: 400
4394
4488
  });
4395
- if (!phone) return json({
4396
- error: "phone is required"
4397
- }, {
4398
- status: 400
4399
- });
4400
- if (!rawPw) return json({
4401
- error: "password is required"
4402
- }, {
4403
- status: 400
4404
- });
4405
- if (rawPw.length < 6) {
4406
- return json({
4407
- error: "Password must be at least 6 characters"
4408
- }, {
4409
- status: 400
4410
- });
4411
- }
4412
4489
  const customerRepo = dataSource.getRepository(entityMap["customer"]);
4413
- const userRepo = dataSource.getRepository(entityMap["users"]);
4414
4490
  const dupCustEmail = await customerRepo.findOne({
4415
4491
  where: {
4416
4492
  email,
@@ -4424,49 +4500,26 @@ function createCrudHandler(dataSource, entityMap, options) {
4424
4500
  status: 409
4425
4501
  });
4426
4502
  }
4427
- const dupCustPhone = await customerRepo.findOne({
4428
- where: {
4429
- phone,
4430
- deleted: false
4431
- }
4432
- });
4433
- if (dupCustPhone) {
4434
- return json({
4435
- error: "A customer with this phone number already exists"
4436
- }, {
4437
- status: 409
4503
+ if (phone) {
4504
+ const dupCustPhone = await customerRepo.findOne({
4505
+ where: {
4506
+ phone,
4507
+ deleted: false
4508
+ }
4438
4509
  });
4439
- }
4440
- let userId;
4441
- const dupUser = await userRepo.findOne({
4442
- where: {
4443
- email,
4444
- deleted: false
4510
+ if (dupCustPhone) {
4511
+ return json({
4512
+ error: "A customer with this phone number already exists"
4513
+ }, {
4514
+ status: 409
4515
+ });
4445
4516
  }
4446
- });
4447
- if (dupUser) {
4448
- userId = Number(dupUser.id);
4449
- } else {
4450
- const bcrypt = await import('bcryptjs');
4451
- const hashedPassword = await bcrypt.hash(rawPw, 10);
4452
- const savedUser = await userRepo.save(userRepo.create({
4453
- name,
4454
- email,
4455
- phone,
4456
- password: hashedPassword,
4457
- groupId: customerGroupId,
4458
- adminAccess: false,
4459
- blocked: false
4460
- }));
4461
- userId = Number(savedUser.id);
4462
4517
  }
4463
- const savedCustomer = await ensureCustomerForUser(dataSource, entityMap["customer"], {
4464
- id: userId,
4518
+ const savedCustomer = await ensureCustomerRecord(dataSource, entityMap["customer"], {
4465
4519
  name,
4466
4520
  email,
4467
- phone
4468
- }, {
4469
- phone
4521
+ phone,
4522
+ userId: null
4470
4523
  });
4471
4524
  if (!savedCustomer) {
4472
4525
  return json({
@@ -4475,37 +4528,37 @@ function createCrudHandler(dataSource, entityMap, options) {
4475
4528
  status: 409
4476
4529
  });
4477
4530
  }
4478
- const customerRepo2 = dataSource.getRepository(entityMap["customer"]);
4479
- const customerRow = await customerRepo2.findOne({
4531
+ const contactRepo = dataSource.getRepository(entityMap["contacts"]);
4532
+ let contact = await contactRepo.findOne({
4480
4533
  where: {
4481
- id: savedCustomer.id,
4534
+ email,
4482
4535
  deleted: false
4483
4536
  }
4484
- }) ?? savedCustomer;
4485
- if (entityMap["contacts"]) {
4486
- const contactRepo = dataSource.getRepository(entityMap["contacts"]);
4487
- const existingContact = await contactRepo.findOne({
4488
- where: {
4489
- email,
4490
- deleted: false
4491
- }
4492
- });
4493
- if (!existingContact) {
4494
- await contactRepo.save(contactRepo.create({
4495
- name,
4496
- email,
4497
- phone: phone || null,
4498
- type: "customer"
4499
- }));
4500
- } else {
4501
- const t = existingContact.type;
4502
- if (t == null || t === "" || String(t).toLowerCase() !== "customer") {
4503
- await contactRepo.update(existingContact.id, {
4504
- type: "customer"
4505
- });
4506
- }
4507
- }
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
+ };
4508
4560
  }
4561
+ const contactId = Number(contact.id);
4509
4562
  const scope = await resolveVendorCustomersScope(dataSource, entityMap, resolveScope);
4510
4563
  let vendorId = null;
4511
4564
  if (scope.type === "vendor") {
@@ -4521,19 +4574,25 @@ function createCrudHandler(dataSource, entityMap, options) {
4521
4574
  status: 400
4522
4575
  });
4523
4576
  }
4524
- const vcRepo = dataSource.getRepository(entity);
4525
- 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({
4526
4585
  where: {
4527
- customerId: savedCustomer.id
4586
+ id: savedCustomer.id,
4587
+ deleted: false
4528
4588
  }
4529
4589
  });
4530
- if (!existingLink) {
4531
- await vcRepo.save(vcRepo.create({
4532
- customerId: savedCustomer.id,
4533
- vendorId
4534
- }));
4535
- }
4536
- return json(customerRow, {
4590
+ return json(customerRow ?? {
4591
+ id: savedCustomer.id,
4592
+ name,
4593
+ email,
4594
+ phone
4595
+ }, {
4537
4596
  status: 201
4538
4597
  });
4539
4598
  }
@@ -4981,6 +5040,7 @@ function createCrudHandler(dataSource, entityMap, options) {
4981
5040
  }
4982
5041
  const nameRaw = String(body["contact.name"] ?? contact.name ?? "").trim();
4983
5042
  const phoneRaw = body["contact.phone"] ?? contact.phone;
5043
+ const emailForVc = (emailRaw || String(contact.email ?? "")).trim().toLowerCase();
4984
5044
  const phoneToSave = phoneRaw == null || phoneRaw === "" ? null : String(phoneRaw).trim();
4985
5045
  if (phoneToSave && contact.phone !== phoneToSave) {
4986
5046
  await contactRepo.update(contact.id, {
@@ -4989,21 +5049,45 @@ function createCrudHandler(dataSource, entityMap, options) {
4989
5049
  contact.phone = phoneToSave;
4990
5050
  }
4991
5051
  const vendorIdForCustomer = resolveVendorIdForContactCheck(scopeCreate, persistBody);
4992
- const selectedExistingCustomer = Number(body.customerId);
4993
- if (vendorIdForCustomer != null && (!Number.isFinite(selectedExistingCustomer) || selectedExistingCustomer <= 0)) {
4994
- await ensureVendorCustomerForOrderContact(dataSource, entityMap, vendorIdForCustomer, contact.id, {
4995
- name: nameRaw || emailRaw.split("@")[0] || "Customer",
4996
- 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,
4997
5073
  phone: phoneRaw == null || phoneRaw === "" ? null : String(phoneRaw)
4998
5074
  });
4999
- } else if (vendorIdForCustomer != null) {
5000
- const contactErr = await assertContactAllowedForVendorOrder(dataSource, entityMap, vendorIdForCustomer, contact.id);
5001
- if (contactErr) {
5002
- return json({
5003
- error: contactErr
5004
- }, {
5005
- status: 400
5006
- });
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
+ }));
5007
5091
  }
5008
5092
  }
5009
5093
  const randomPart = Math.floor(1e4 + Math.random() * 9e4);
@@ -6783,6 +6867,14 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
6783
6867
  }
6784
6868
  }
6785
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
+ }
6786
6878
  return json({
6787
6879
  message: "Deleted successfully"
6788
6880
  }, {
@@ -6813,6 +6905,20 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
6813
6905
  };
6814
6906
  }
6815
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");
6816
6922
  var VENDOR_INVITE_METADATA_TOKEN = "inviteToken";
6817
6923
  var VENDOR_INVITE_METADATA_EXPIRES = "inviteExpiresAt";
6818
6924
  var VENDOR_INVITE_METADATA_SENT = "inviteSentAt";
@@ -7545,6 +7651,50 @@ async function findLlmAgentByScope(dataSource, entityMap, scope, options = {}) {
7545
7651
  }
7546
7652
  __name(findLlmAgentByScope, "findLlmAgentByScope");
7547
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
+
7548
7698
  // src/lib/media-folder-path.ts
7549
7699
  function sanitizeMediaFolderPath(input) {
7550
7700
  if (input == null) return "";
@@ -8938,6 +9088,17 @@ function createFormSaveHandlers(config) {
8938
9088
  });
8939
9089
  const fields = Array.isArray(body.fields) ? body.fields : [];
8940
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
+ }
8941
9102
  const form = await formRepo().save(formRepo().create(formRow));
8942
9103
  for (let i = 0; i < fields.length; i++) {
8943
9104
  const row = normalizeFieldRow(fields[i], form.id);
@@ -8961,6 +9122,14 @@ function createFormSaveHandlers(config) {
8961
9122
  status: 201
8962
9123
  });
8963
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
+ }
8964
9133
  return json({
8965
9134
  error: "Server Error"
8966
9135
  }, {
@@ -9009,6 +9178,20 @@ function createFormSaveHandlers(config) {
9009
9178
  ]) {
9010
9179
  if (body[key] !== void 0) formRow[key] = body[key];
9011
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
+ }
9012
9195
  if (Object.keys(formRow).length > 0) await formRepo().update(formId, formRow);
9013
9196
  await fieldRepo().delete({
9014
9197
  formId
@@ -9037,6 +9220,14 @@ function createFormSaveHandlers(config) {
9037
9220
  status: 404
9038
9221
  });
9039
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
+ }
9040
9231
  return json({
9041
9232
  error: "Server Error"
9042
9233
  }, {
@@ -9618,6 +9809,7 @@ function createUsersApiHandlers(config) {
9618
9809
  "id",
9619
9810
  "name",
9620
9811
  "email",
9812
+ "phone",
9621
9813
  "blocked",
9622
9814
  "createdAt",
9623
9815
  "updatedAt",
@@ -9647,6 +9839,11 @@ function createUsersApiHandlers(config) {
9647
9839
  }
9648
9840
  try {
9649
9841
  const uid = parseInt(id, 10);
9842
+ if (!Number.isFinite(uid)) return json({
9843
+ error: "Invalid id"
9844
+ }, {
9845
+ status: 400
9846
+ });
9650
9847
  const existing = await userRepo().findOne({
9651
9848
  where: {
9652
9849
  id: uid,
@@ -9659,8 +9856,59 @@ function createUsersApiHandlers(config) {
9659
9856
  status: 404
9660
9857
  });
9661
9858
  const body = await req.json();
9662
- const { password: _p, ...safe } = body;
9663
- 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);
9664
9912
  const updated = await userRepo().findOne({
9665
9913
  where: {
9666
9914
  id: uid,
@@ -9673,6 +9921,7 @@ function createUsersApiHandlers(config) {
9673
9921
  "id",
9674
9922
  "name",
9675
9923
  "email",
9924
+ "phone",
9676
9925
  "blocked",
9677
9926
  "createdAt",
9678
9927
  "updatedAt",
@@ -9684,7 +9933,8 @@ function createUsersApiHandlers(config) {
9684
9933
  }, {
9685
9934
  status: 404
9686
9935
  });
9687
- } catch {
9936
+ } catch (err) {
9937
+ console.error("[users.update]", err);
9688
9938
  return json({
9689
9939
  error: "Server Error"
9690
9940
  }, {
@@ -12732,6 +12982,39 @@ function slugify(input) {
12732
12982
  return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
12733
12983
  }
12734
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");
12735
13018
  function vendorOnboardErrorResponse(json, err) {
12736
13019
  const msg = err instanceof Error ? err.message : String(err);
12737
13020
  console.error("[vendor-onboard]", err);
@@ -12830,14 +13113,80 @@ function createVendorOnboardHandlers(config) {
12830
13113
  __name(gateAdmin, "gateAdmin");
12831
13114
  async function resolveActiveVendorId(u) {
12832
13115
  const scope = resolveVendorScopeFromSessionUser(u);
12833
- if (scope.type === "vendor") return scope.vendorId;
12834
- if (scope.type === "all") {
12835
- const id = u.activeVendorId ?? u.vendorIds?.[0];
12836
- 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
+ }
12837
13158
  }
12838
13159
  return null;
12839
13160
  }
12840
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");
12841
13190
  async function gateVendorTeam() {
12842
13191
  const u = await getSessionUser();
12843
13192
  if (!u?.email) return json({
@@ -12891,6 +13240,40 @@ function createVendorOnboardHandlers(config) {
12891
13240
  }
12892
13241
  __name(trySendVendorOnboardEmails, "trySendVendorOnboardEmails");
12893
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
+ },
12894
13277
  /** POST /api/admin/vendors/onboard — create vendor + owner user + vendor_users + invite */
12895
13278
  async onboard(req) {
12896
13279
  const err = await gateAdmin();
@@ -12959,6 +13342,7 @@ function createVendorOnboardHandlers(config) {
12959
13342
  }
12960
13343
  });
12961
13344
  if (dupVendor) throw new Error("VENDOR_SLUG_EXISTS");
13345
+ await retireSoftDeletedUniqueValue(vendorRepo, "slug", slug);
12962
13346
  let ownerGroup = await groupRepo.findOne({
12963
13347
  where: {
12964
13348
  name: VENDOR_OWNER_GROUP_NAME,
@@ -12987,8 +13371,15 @@ function createVendorOnboardHandlers(config) {
12987
13371
  email: userEmail
12988
13372
  }
12989
13373
  });
12990
- if (existingUser && !existingUser.deleted) throw new Error("USER_EMAIL_EXISTS");
12991
- 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 () => {
12992
13383
  await userRepo.update(existingUser.id, {
12993
13384
  deleted: false,
12994
13385
  deletedAt: null,
@@ -16071,10 +16462,12 @@ var Customer = class {
16071
16462
  __name(this, "Customer");
16072
16463
  }
16073
16464
  id;
16465
+ /** Set only when the customer can log in (linked `users` row). Admin/guest customers stay null. */
16074
16466
  userId;
16075
16467
  user;
16076
16468
  name;
16077
16469
  email;
16470
+ /** Optional; multiple customers may have null (PostgreSQL UNIQUE allows multiple NULLs). */
16078
16471
  phone;
16079
16472
  createdAt;
16080
16473
  updatedAt;
@@ -16089,8 +16482,10 @@ _ts_decorate18([
16089
16482
  _ts_metadata18("design:type", Number)
16090
16483
  ], Customer.prototype, "id", void 0);
16091
16484
  _ts_decorate18([
16092
- Column("int"),
16093
- _ts_metadata18("design:type", Number)
16485
+ Column("int", {
16486
+ nullable: true
16487
+ }),
16488
+ _ts_metadata18("design:type", Object)
16094
16489
  ], Customer.prototype, "userId", void 0);
16095
16490
  _ts_decorate18([
16096
16491
  ManyToOne(() => User, {
@@ -16113,9 +16508,10 @@ _ts_decorate18([
16113
16508
  ], Customer.prototype, "email", void 0);
16114
16509
  _ts_decorate18([
16115
16510
  Column("varchar", {
16116
- unique: true
16511
+ unique: true,
16512
+ nullable: true
16117
16513
  }),
16118
- _ts_metadata18("design:type", String)
16514
+ _ts_metadata18("design:type", Object)
16119
16515
  ], Customer.prototype, "phone", void 0);
16120
16516
  _ts_decorate18([
16121
16517
  Column({
@@ -26699,6 +27095,14 @@ function createCmsApiHandler(config) {
26699
27095
  });
26700
27096
  return vendorHandlers.switchVendor(req);
26701
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
+ }
26702
27106
  if (path2[0] === "admin" && path2[1] === "vendor" && path2[2] === "roles" && vendorRolesHandlers) {
26703
27107
  if (path2.length === 3 && m === "GET") return vendorRolesHandlers.list();
26704
27108
  if (path2.length === 3 && m === "POST") return vendorRolesHandlers.create(req);
@@ -29168,13 +29572,36 @@ function createStorefrontApiHandler(config) {
29168
29572
  status: 400
29169
29573
  };
29170
29574
  }
29575
+ const list = [
29576
+ ...vendorIds
29577
+ ];
29171
29578
  return {
29172
- vendorId: [
29173
- ...vendorIds
29174
- ][0]
29579
+ vendorId: list[0],
29580
+ vendorIds: list
29175
29581
  };
29176
29582
  }
29177
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");
29178
29605
  function roundMoney3(n) {
29179
29606
  return Math.round(n * 100) / 100;
29180
29607
  }
@@ -31496,6 +31923,7 @@ function createStorefrontApiHandler(config) {
31496
31923
  taxCode: line.taxCode
31497
31924
  }));
31498
31925
  }
31926
+ await linkOrderContactToVendors(contactId, vendorRes.vendorIds);
31499
31927
  fireOrderPlacedNotification(oid);
31500
31928
  return json({
31501
31929
  orderId: oid,
@@ -31568,6 +31996,7 @@ function createStorefrontApiHandler(config) {
31568
31996
  taxCode: line.taxCode
31569
31997
  }));
31570
31998
  }
31999
+ await linkOrderContactToVendors(contactId, vendorResChk.vendorIds);
31571
32000
  await cartItemRepo().delete({
31572
32001
  cartId: cart.id
31573
32002
  });
@@ -31814,4 +32243,4 @@ function createStorefrontApiHandler(config) {
31814
32243
  }
31815
32244
  __name(createStorefrontApiHandler, "createStorefrontApiHandler");
31816
32245
 
31817
- 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 };