@infuro/cms-core 1.0.44 → 1.0.46

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.
Files changed (66) hide show
  1. package/README.md +19 -2
  2. package/dist/admin.cjs +1413 -620
  3. package/dist/admin.d.cts +8 -2
  4. package/dist/admin.d.ts +8 -2
  5. package/dist/admin.js +1414 -621
  6. package/dist/api.cjs +40 -37
  7. package/dist/api.d.cts +3 -1
  8. package/dist/api.d.ts +3 -1
  9. package/dist/api.js +8 -5
  10. package/dist/auth.cjs +23 -23
  11. package/dist/auth.d.cts +1 -0
  12. package/dist/auth.d.ts +1 -0
  13. package/dist/auth.js +2 -2
  14. package/dist/chunk-2KUCQVAQ.js +936 -0
  15. package/dist/chunk-3K5AIEIZ.js +256 -0
  16. package/dist/{chunk-I6NH72MO.js → chunk-57O3HZPG.js} +9 -1
  17. package/dist/chunk-7PMHZRF3.cjs +872 -0
  18. package/dist/chunk-BXYZDMTZ.cjs +953 -0
  19. package/dist/chunk-CQHXW4TR.js +107 -0
  20. package/dist/{chunk-NNQBWDCI.js → chunk-CRYKVJTO.js} +55 -959
  21. package/dist/chunk-DBPSJYLZ.js +47 -0
  22. package/dist/{chunk-TAHX46EI.cjs → chunk-FBKDMRQL.cjs} +9 -1
  23. package/dist/chunk-GUSHM5HN.js +862 -0
  24. package/dist/chunk-HY3AXLOI.cjs +265 -0
  25. package/dist/chunk-L3525VXI.js +243 -0
  26. package/dist/{chunk-ZMRQ72F6.cjs → chunk-NBY4NVTY.cjs} +3 -3
  27. package/dist/chunk-OY2YTBMP.cjs +50 -0
  28. package/dist/{chunk-BLR6H5GL.js → chunk-SF2XKMCI.js} +3 -3
  29. package/dist/{chunk-22FFHLTO.cjs → chunk-TCSGFAWB.cjs} +56 -970
  30. package/dist/{chunk-P2IWWBJV.cjs → chunk-UDVLFVEC.cjs} +1314 -601
  31. package/dist/{chunk-SQGBHYJF.js → chunk-UKC3HYXI.js} +1245 -548
  32. package/dist/chunk-V25RDUOU.cjs +248 -0
  33. package/dist/chunk-XMRMZ6NQ.cjs +109 -0
  34. package/dist/cli.cjs +9 -4
  35. package/dist/cli.js +9 -4
  36. package/dist/{email-queue-TPZWLTIV.cjs → email-queue-6NJY6HNM.cjs} +7 -5
  37. package/dist/email-queue-OI2HSCGE.js +4 -0
  38. package/dist/erp-order-invoice-3GXDE443.js +3 -0
  39. package/dist/erp-order-invoice-EYYNR526.cjs +24 -0
  40. package/dist/index.cjs +426 -359
  41. package/dist/index.d.cts +308 -20
  42. package/dist/index.d.ts +308 -20
  43. package/dist/index.js +31 -110
  44. package/dist/migrations/1781800000000-CustomerPhoneNullable.ts +53 -0
  45. package/dist/migrations/1781810000000-OrderItemsVariantId.ts +44 -0
  46. package/dist/migrations/1781900000000-UserInviteStatusToken.ts +46 -0
  47. package/dist/migrations/1782000000000-ComboSlug.ts +58 -0
  48. package/dist/migrations/1782100000000-DiscountDeviceType.ts +20 -0
  49. package/dist/order-inventory-2MYRKEPC.cjs +39 -0
  50. package/dist/order-inventory-76V4XIGW.js +2 -0
  51. package/dist/order-notification-dispatcher-SBQAMM5V.cjs +21 -0
  52. package/dist/{order-notification-dispatcher-XWIJYDGA.js → order-notification-dispatcher-ZJZB2HUN.js} +5 -2
  53. package/dist/retire-soft-deleted-unique-5VXBA5XX.cjs +15 -0
  54. package/dist/retire-soft-deleted-unique-NXQIH4BC.js +2 -0
  55. package/dist/send-order-placed-emails-B5ZVJT7T.cjs +15 -0
  56. package/dist/send-order-placed-emails-WPI37KRZ.js +6 -0
  57. package/package.json +5 -4
  58. package/src/admin/admin.css +27 -24
  59. package/dist/chunk-5ELSW2UP.js +0 -150
  60. package/dist/chunk-PNOKNSG2.cjs +0 -222
  61. package/dist/chunk-WEMDMVHQ.js +0 -216
  62. package/dist/chunk-XW5ATPRW.cjs +0 -155
  63. package/dist/email-queue-CGZBVVAS.js +0 -2
  64. package/dist/erp-order-invoice-3YISSOWW.js +0 -3
  65. package/dist/erp-order-invoice-ZR5F2KTA.cjs +0 -12
  66. package/dist/order-notification-dispatcher-RU7BTGGX.cjs +0 -18
@@ -1,16 +1,19 @@
1
- import { resolveChatEmailToolSettings, CHAT_EMAIL_LOG, detectChatLeadIntent, parseIntentRecipientEmails, mergeEmailLayoutCompanyDetails, sendChatLeadEmail, buildTranscriptForLeadEmail, buildChatbotSystemPromptWithEmailTool, parseEmailRecipientsFromConfig, isWhatsAppPluginEnabled, mergeWhatsAppConfigLayers, whatsAppConfigured, WhatsAppService, registerWhatsAppQueueProcessor, emailPlugin, initWhatsappTriggerDispatcher } from './chunk-NNQBWDCI.js';
1
+ import { validateInventoryForConfirmedOrderLines, orderStatusHoldsStock, reconcileOrderInventoryBetweenSnapshots, orderInventorySnapshotFromRow, validateInventoryForOrderBecomingConfirmed } from './chunk-3K5AIEIZ.js';
2
+ import { retireSoftDeletedUniqueValue, activeUniqueValueExists } from './chunk-DBPSJYLZ.js';
3
+ import { isWhatsAppPluginEnabled, mergeWhatsAppConfigLayers, whatsAppConfigured, WhatsAppService, registerWhatsAppQueueProcessor, initWhatsappTriggerDispatcher } from './chunk-GUSHM5HN.js';
4
+ import { resolveChatEmailToolSettings, CHAT_EMAIL_LOG, detectChatLeadIntent, parseIntentRecipientEmails, sendChatLeadEmail, buildTranscriptForLeadEmail, buildChatbotSystemPromptWithEmailTool, parseEmailRecipientsFromConfig, emailPlugin } from './chunk-CRYKVJTO.js';
2
5
  import { inviteStatusForActivation } from './chunk-JXF23MPG.js';
3
- import { unwrapErpReadData, mapErpPayloadToFulfillment, extractChildOrderRefsFromSalePayload, mapErpPayloadToInvoiceNumber, streamOrderInvoicePdf } from './chunk-WEMDMVHQ.js';
4
- import { queueEmail, queueVendorOnboardEmails, registerEmailQueueProcessor } from './chunk-5ELSW2UP.js';
6
+ import { queueEmail, queueVendorOnboardEmails, registerEmailQueueProcessor } from './chunk-L3525VXI.js';
7
+ import { mergeEmailLayoutCompanyDetails, unwrapErpReadData, mapErpPayloadToFulfillment, extractChildOrderRefsFromSalePayload, mapErpPayloadToInvoiceNumber, streamOrderInvoicePdf } from './chunk-2KUCQVAQ.js';
5
8
  import { getSmsTemplateDefault, SMS_MESSAGE_TEMPLATE_DEFAULTS } from './chunk-MQBT33IV.js';
6
9
  import { EVENT_ORDER_MESSAGE_TEMPLATE_DEFAULTS, EVENT_ORDER_TEMPLATE_KEY, EVENT_ORDER_TEMPLATE_VARIABLES } from './chunk-KFFB6EMZ.js';
7
10
  import { JOB_RUNNER_QUEUE } from './chunk-GO7PPYNU.js';
8
- import { canManageRoles, sessionHasEntityAccess } from './chunk-BLR6H5GL.js';
11
+ import { canManageRoles, sessionHasEntityAccess } from './chunk-SF2XKMCI.js';
9
12
  import { permissionRowsToRecord, logRbac, isSuperAdmin, vendorPortalFlagsFromUser, VENDOR_SCOPED_STORE_ENTITIES, isVendorPortalUser, getPermissionableEntityKeys, VENDOR_STORE_RBAC_ENTITIES, isSuperAdminGroupName, VENDOR_RBAC_ENTITIES, VENDOR_VIEWER_ROLE_NAME, DEFAULT_VENDOR_ROLE_DEFS, VENDOR_OWNER_GROUP_NAME, resolveVendorScopeFromSessionUser, canManageVendorRoles, isPlatformAdministrator, canManageVendorTeam, resolveDisabledMultiVendorScope, logEntityAccessDecision, explainSessionEntityAccess } from './chunk-DN65KDIA.js';
10
13
  import { queueErp } from './chunk-SYBOCAWB.js';
11
14
  import { isErpIntegrationEnabled } from './chunk-JC6DLWTE.js';
12
15
  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';
16
+ import { PrimaryGeneratedColumn, Column, Entity, ManyToOne, JoinColumn, OneToMany, Index, ManyToMany, JoinTable, Unique, CreateDateColumn, UpdateDateColumn, In, ILike, Between, MoreThanOrEqual, LessThanOrEqual, Not, MoreThan, IsNull } from 'typeorm';
14
17
  import { Country, State, City } from 'country-state-city';
15
18
  import crypto2, { randomBytes, randomUUID, createHmac, timingSafeEqual, randomInt } from 'crypto';
16
19
  import Parser from 'rss-parser';
@@ -36,6 +39,7 @@ async function queryVendorLinks(dataSource, userId) {
36
39
  vr."name" AS "vendorRoleName",
37
40
  vr."isOwnerRole" AS "isOwnerRole"
38
41
  FROM "vendor_users" vu
42
+ INNER JOIN "vendors" v ON v.id = vu."vendorId" AND v.deleted = false
39
43
  LEFT JOIN "vendor_roles" vr ON vr.id = vu."vendorRoleId" AND vr.deleted = false
40
44
  WHERE vu."userId" = $1
41
45
  ORDER BY vu.id ASC
@@ -45,10 +49,11 @@ async function queryVendorLinks(dataSource, userId) {
45
49
  } catch (err) {
46
50
  if (!isMissingVendorRoleSchemaError(err)) throw err;
47
51
  const legacy = await dataSource.query(`
48
- SELECT "vendorId", "role"
49
- FROM "vendor_users"
50
- WHERE "userId" = $1
51
- ORDER BY id ASC
52
+ SELECT vu."vendorId" AS "vendorId", vu."role" AS "role"
53
+ FROM "vendor_users" vu
54
+ INNER JOIN "vendors" v ON v.id = vu."vendorId" AND v.deleted = false
55
+ WHERE vu."userId" = $1
56
+ ORDER BY vu.id ASC
52
57
  `, [
53
58
  userId
54
59
  ]);
@@ -62,8 +67,36 @@ async function queryVendorLinks(dataSource, userId) {
62
67
  }
63
68
  }
64
69
  __name(queryVendorLinks, "queryVendorLinks");
70
+ async function queryOwnedVendorIds(dataSource, userId) {
71
+ try {
72
+ const rows = await dataSource.query(`
73
+ SELECT id
74
+ FROM "vendors"
75
+ WHERE "userId" = $1 AND deleted = false
76
+ ORDER BY id ASC
77
+ `, [
78
+ userId
79
+ ]);
80
+ return rows.map((r) => Number(r.id)).filter((id) => Number.isFinite(id) && id > 0);
81
+ } catch {
82
+ return [];
83
+ }
84
+ }
85
+ __name(queryOwnedVendorIds, "queryOwnedVendorIds");
65
86
  async function loadUserVendorContext(dataSource, userId, preferredVendorId) {
66
87
  const rows = await queryVendorLinks(dataSource, userId);
88
+ const linkIds = new Set(rows.map((r) => Number(r.vendorId)));
89
+ for (const ownedId of await queryOwnedVendorIds(dataSource, userId)) {
90
+ if (linkIds.has(ownedId)) continue;
91
+ rows.push({
92
+ vendorId: ownedId,
93
+ role: "owner",
94
+ vendorRoleId: null,
95
+ vendorRoleName: null,
96
+ isOwnerRole: true
97
+ });
98
+ linkIds.add(ownedId);
99
+ }
67
100
  const vendorIds = rows.map((r) => Number(r.vendorId));
68
101
  const activeRow = preferredVendorId != null ? rows.find((r) => Number(r.vendorId) === preferredVendorId) : void 0;
69
102
  const primary = activeRow ?? rows[0];
@@ -616,6 +649,144 @@ function applyEventApprovalStatusSideEffects(updatePayload, opts) {
616
649
  }
617
650
  __name(applyEventApprovalStatusSideEffects, "applyEventApprovalStatusSideEffects");
618
651
 
652
+ // src/lib/discount-usage.ts
653
+ async function countDiscountOrdersForContact(dataSource, discountId, contactId) {
654
+ try {
655
+ const rows = await dataSource.query(`SELECT COUNT(DISTINCT od."orderId")::int AS count
656
+ FROM order_discounts od
657
+ WHERE od."discountId" = $1 AND od."userId" = $2`, [
658
+ discountId,
659
+ contactId
660
+ ]);
661
+ return Number(rows?.[0]?.count ?? 0);
662
+ } catch {
663
+ return 0;
664
+ }
665
+ }
666
+ __name(countDiscountOrdersForContact, "countDiscountOrdersForContact");
667
+ function isMaxTotalUsageReached(discount) {
668
+ if (discount.maxTotalUsage == null) return false;
669
+ return Number(discount.usedCount ?? 0) >= Number(discount.maxTotalUsage);
670
+ }
671
+ __name(isMaxTotalUsageReached, "isMaxTotalUsageReached");
672
+ async function isMaxPerUserUsageReached(dataSource, discount, contactId) {
673
+ if (discount.maxUsagePerUser == null || contactId == null) return false;
674
+ const count = await countDiscountOrdersForContact(dataSource, discount.id, contactId);
675
+ return count >= Number(discount.maxUsagePerUser);
676
+ }
677
+ __name(isMaxPerUserUsageReached, "isMaxPerUserUsageReached");
678
+ async function checkAndIncrementDiscountUsage(dataSource, entityMap, discountId, contactId) {
679
+ if (!entityMap.discounts) return null;
680
+ const discountRepo = dataSource.getRepository(entityMap.discounts);
681
+ const discount = await discountRepo.findOne({
682
+ where: {
683
+ id: discountId
684
+ }
685
+ });
686
+ if (!discount) return `Discount #${discountId} not found`;
687
+ const d = discount;
688
+ if (d.maxUsagePerUser != null) {
689
+ const usageCount = await countDiscountOrdersForContact(dataSource, discountId, contactId);
690
+ if (usageCount >= Number(d.maxUsagePerUser)) {
691
+ return "You have reached the maximum usage limit for this discount";
692
+ }
693
+ }
694
+ const updated = await dataSource.query(`UPDATE discounts
695
+ SET "usedCount" = "usedCount" + 1
696
+ WHERE id = $1
697
+ AND ("maxTotalUsage" IS NULL OR "usedCount" < "maxTotalUsage")
698
+ RETURNING id`, [
699
+ discountId
700
+ ]);
701
+ if (!Array.isArray(updated) || updated.length === 0) {
702
+ return "Discount usage limit has been reached";
703
+ }
704
+ return null;
705
+ }
706
+ __name(checkAndIncrementDiscountUsage, "checkAndIncrementDiscountUsage");
707
+ async function decrementDiscountUsage(dataSource, entityMap, discountId, orderId) {
708
+ if (!entityMap.discounts) return;
709
+ try {
710
+ await dataSource.query(`UPDATE discounts SET "usedCount" = GREATEST("usedCount" - 1, 0) WHERE id = $1`, [
711
+ discountId
712
+ ]);
713
+ if (entityMap.order_discounts) {
714
+ await dataSource.getRepository(entityMap.order_discounts).delete({
715
+ orderId,
716
+ discountId
717
+ });
718
+ }
719
+ } catch (err) {
720
+ console.error("[discountEngine] decrementDiscountUsage failed:", err);
721
+ }
722
+ }
723
+ __name(decrementDiscountUsage, "decrementDiscountUsage");
724
+ async function recordDiscountUsage(dataSource, entityMap, orderId, discountId, contactId, discountAmount) {
725
+ if (!entityMap.order_discounts) return;
726
+ try {
727
+ let discountCode = "";
728
+ if (entityMap.discounts) {
729
+ const discountRow = await dataSource.getRepository(entityMap.discounts).findOne({
730
+ where: {
731
+ id: discountId
732
+ }
733
+ });
734
+ discountCode = discountRow?.couponCode ?? "";
735
+ }
736
+ const odRepo = dataSource.getRepository(entityMap.order_discounts);
737
+ const orderItemRepo = dataSource.getRepository(entityMap.order_items);
738
+ const items = await orderItemRepo.find({
739
+ where: {
740
+ orderId
741
+ }
742
+ });
743
+ const discountedItems = items.filter((item) => Number(item.discount ?? 0) > 0);
744
+ if (discountedItems.length === 0) {
745
+ const firstItem = items[0];
746
+ if (!firstItem) return;
747
+ await odRepo.save(odRepo.create({
748
+ orderId,
749
+ orderItemId: firstItem.id,
750
+ discountId,
751
+ discountCode,
752
+ discountAmount,
753
+ userId: contactId
754
+ }));
755
+ return;
756
+ }
757
+ for (const item of discountedItems) {
758
+ const i = item;
759
+ await odRepo.save(odRepo.create({
760
+ orderId,
761
+ orderItemId: i.id,
762
+ discountId,
763
+ discountCode,
764
+ discountAmount: Number(i.discount ?? 0),
765
+ userId: contactId
766
+ }));
767
+ }
768
+ } catch (err) {
769
+ console.error("[discountEngine] recordDiscountUsage failed:", err);
770
+ }
771
+ }
772
+ __name(recordDiscountUsage, "recordDiscountUsage");
773
+ async function consumeAppliedDiscountUsages(dataSource, entityMap, orderId, contactId, applied) {
774
+ const unique = /* @__PURE__ */ new Map();
775
+ for (const row of applied) {
776
+ const id = Number(row.id);
777
+ if (!Number.isFinite(id) || id < 1) continue;
778
+ const amount = Math.max(0, Number(row.calculatedDiscount) || 0);
779
+ unique.set(id, (unique.get(id) ?? 0) + amount);
780
+ }
781
+ for (const [discountId, discountAmount] of unique) {
782
+ const usageErr = await checkAndIncrementDiscountUsage(dataSource, entityMap, discountId, contactId);
783
+ if (usageErr) return usageErr;
784
+ await recordDiscountUsage(dataSource, entityMap, orderId, discountId, contactId, discountAmount);
785
+ }
786
+ return null;
787
+ }
788
+ __name(consumeAppliedDiscountUsages, "consumeAppliedDiscountUsages");
789
+
619
790
  // src/lib/default-vendor-id.ts
620
791
  function getDefaultVendorId() {
621
792
  const raw = process.env.DEFAULT_VENDOR_ID;
@@ -1090,11 +1261,118 @@ function pgErrorCode(err) {
1090
1261
  return driver?.code ?? err.code;
1091
1262
  }
1092
1263
  __name(pgErrorCode, "pgErrorCode");
1093
- function customerPhoneForUser(userId, phone) {
1264
+ function normalizeCustomerPhone(phone) {
1094
1265
  const p = typeof phone === "string" ? phone.trim() : "";
1095
- return p || `u-${userId}`;
1266
+ return p || null;
1267
+ }
1268
+ __name(normalizeCustomerPhone, "normalizeCustomerPhone");
1269
+ function isSyntheticCustomerPhone(phone) {
1270
+ const p = String(phone ?? "").trim();
1271
+ if (!p) return true;
1272
+ if (p.startsWith("e-") || p.startsWith("u-")) return true;
1273
+ if (p.includes("@")) return true;
1274
+ return false;
1275
+ }
1276
+ __name(isSyntheticCustomerPhone, "isSyntheticCustomerPhone");
1277
+ function customerPhoneForUser(_userId, phone) {
1278
+ return normalizeCustomerPhone(phone);
1096
1279
  }
1097
1280
  __name(customerPhoneForUser, "customerPhoneForUser");
1281
+ function customerPhoneForEmail(_email, phone) {
1282
+ return normalizeCustomerPhone(phone);
1283
+ }
1284
+ __name(customerPhoneForEmail, "customerPhoneForEmail");
1285
+ function resolveNextPhone(inputPhone, existingPhone) {
1286
+ if (inputPhone) return inputPhone;
1287
+ const existing = normalizeCustomerPhone(existingPhone);
1288
+ if (!existing || isSyntheticCustomerPhone(existing)) return null;
1289
+ return existing;
1290
+ }
1291
+ __name(resolveNextPhone, "resolveNextPhone");
1292
+ async function ensureCustomerRecord(dsOrEm, customerEntity, input) {
1293
+ const repo = dsOrEm.getRepository(customerEntity);
1294
+ const email = normalizeEmail(input.email);
1295
+ if (!email) return null;
1296
+ const name = String(input.name ?? "").trim() || email.split("@")[0] || "Customer";
1297
+ const userId = input.userId != null && Number.isFinite(Number(input.userId)) && Number(input.userId) > 0 ? Number(input.userId) : null;
1298
+ const phone = normalizeCustomerPhone(input.phone);
1299
+ let row = await repo.findOne({
1300
+ where: {
1301
+ email,
1302
+ deleted: false
1303
+ }
1304
+ });
1305
+ if (!row) {
1306
+ row = await repo.findOne({
1307
+ where: {
1308
+ email
1309
+ }
1310
+ });
1311
+ }
1312
+ if (row) {
1313
+ const existingUserId = row.userId;
1314
+ if (userId != null && existingUserId != null && existingUserId !== userId) {
1315
+ return null;
1316
+ }
1317
+ const nextPhone = resolveNextPhone(phone, row.phone);
1318
+ const patch = {
1319
+ name,
1320
+ phone: nextPhone,
1321
+ deleted: false,
1322
+ deletedAt: null,
1323
+ deletedBy: null,
1324
+ updatedAt: /* @__PURE__ */ new Date()
1325
+ };
1326
+ if (userId != null && existingUserId == null) {
1327
+ patch.userId = userId;
1328
+ }
1329
+ await repo.update(row.id, patch);
1330
+ return {
1331
+ id: row.id
1332
+ };
1333
+ }
1334
+ try {
1335
+ const created = await repo.save(repo.create({
1336
+ userId,
1337
+ name,
1338
+ email,
1339
+ phone,
1340
+ deleted: false
1341
+ }));
1342
+ return {
1343
+ id: created.id
1344
+ };
1345
+ } catch (err) {
1346
+ const code = pgErrorCode(err);
1347
+ if (code === "25P02") throw err;
1348
+ row = await repo.findOne({
1349
+ where: {
1350
+ email
1351
+ }
1352
+ });
1353
+ if (row) {
1354
+ const existingUserId = row.userId;
1355
+ if (userId != null && existingUserId != null && existingUserId !== userId) return null;
1356
+ await repo.update(row.id, {
1357
+ name,
1358
+ phone: resolveNextPhone(phone, row.phone),
1359
+ ...userId != null && existingUserId == null ? {
1360
+ userId
1361
+ } : {},
1362
+ deleted: false,
1363
+ deletedAt: null,
1364
+ deletedBy: null,
1365
+ updatedAt: /* @__PURE__ */ new Date()
1366
+ });
1367
+ return {
1368
+ id: row.id
1369
+ };
1370
+ }
1371
+ if (code === "23505") return null;
1372
+ throw err;
1373
+ }
1374
+ }
1375
+ __name(ensureCustomerRecord, "ensureCustomerRecord");
1098
1376
  async function restoreCustomerRow(repo, row, user, name, email, phone) {
1099
1377
  const id = row.id;
1100
1378
  await repo.update(id, {
@@ -1116,7 +1394,7 @@ async function ensureCustomerForUser(dsOrEm, customerEntity, user, overrides) {
1116
1394
  const repo = dsOrEm.getRepository(customerEntity);
1117
1395
  const email = normalizeEmail(user.email);
1118
1396
  const name = String(user.name ?? "").trim() || email.split("@")[0] || "User";
1119
- const phone = customerPhoneForUser(user.id, overrides?.phone ?? user.phone);
1397
+ const phone = normalizeCustomerPhone(overrides?.phone ?? user.phone);
1120
1398
  let row = await repo.findOne({
1121
1399
  where: {
1122
1400
  userId: user.id,
@@ -1124,10 +1402,11 @@ async function ensureCustomerForUser(dsOrEm, customerEntity, user, overrides) {
1124
1402
  }
1125
1403
  });
1126
1404
  if (row) {
1405
+ const nextPhone = resolveNextPhone(phone, row.phone);
1127
1406
  await repo.update(row.id, {
1128
1407
  name,
1129
1408
  email,
1130
- phone,
1409
+ phone: nextPhone,
1131
1410
  updatedAt: /* @__PURE__ */ new Date()
1132
1411
  });
1133
1412
  return {
@@ -1145,10 +1424,11 @@ async function ensureCustomerForUser(dsOrEm, customerEntity, user, overrides) {
1145
1424
  if (existingUserId != null && existingUserId !== user.id) {
1146
1425
  return null;
1147
1426
  }
1427
+ const nextPhone = resolveNextPhone(phone, row.phone);
1148
1428
  await repo.update(row.id, {
1149
1429
  userId: user.id,
1150
1430
  name,
1151
- phone,
1431
+ phone: nextPhone,
1152
1432
  updatedAt: /* @__PURE__ */ new Date()
1153
1433
  });
1154
1434
  return {
@@ -1163,21 +1443,23 @@ async function ensureCustomerForUser(dsOrEm, customerEntity, user, overrides) {
1163
1443
  if (deletedByEmail) {
1164
1444
  const existingUserId = deletedByEmail.userId;
1165
1445
  if (existingUserId != null && existingUserId !== user.id) return null;
1166
- return restoreCustomerRow(repo, deletedByEmail, user, name, email, phone);
1446
+ return restoreCustomerRow(repo, deletedByEmail, user, name, email, resolveNextPhone(phone, deletedByEmail.phone));
1167
1447
  }
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
- });
1448
+ if (phone) {
1449
+ const deletedByPhone = await repo.findOne({
1450
+ where: {
1451
+ phone
1452
+ }
1453
+ });
1454
+ if (deletedByPhone) {
1455
+ const existingUserId = deletedByPhone.userId;
1456
+ if (existingUserId != null && existingUserId !== user.id) {
1457
+ return ensureCustomerForUser(dsOrEm, customerEntity, user, {
1458
+ phone: null
1459
+ });
1460
+ }
1461
+ return restoreCustomerRow(repo, deletedByPhone, user, name, email, phone);
1179
1462
  }
1180
- return restoreCustomerRow(repo, deletedByPhone, user, name, email, phone);
1181
1463
  }
1182
1464
  try {
1183
1465
  const created = await repo.save(repo.create({
@@ -1198,34 +1480,22 @@ async function ensureCustomerForUser(dsOrEm, customerEntity, user, overrides) {
1198
1480
  userId: user.id
1199
1481
  }
1200
1482
  });
1201
- if (row) return restoreCustomerRow(repo, row, user, name, email, phone);
1483
+ if (row) {
1484
+ return restoreCustomerRow(repo, row, user, name, email, resolveNextPhone(phone, row.phone));
1485
+ }
1202
1486
  row = await repo.findOne({
1203
1487
  where: {
1204
1488
  email
1205
1489
  }
1206
1490
  });
1207
1491
  if (row && (row.userId ?? user.id) === user.id) {
1208
- return restoreCustomerRow(repo, row, user, name, email, phone);
1492
+ return restoreCustomerRow(repo, row, user, name, email, resolveNextPhone(phone, row.phone));
1209
1493
  }
1210
1494
  if (code === "23505") return null;
1211
1495
  throw err;
1212
1496
  }
1213
1497
  }
1214
1498
  __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
1499
 
1230
1500
  // src/lib/vendor-customer-contacts.ts
1231
1501
  function isCustomerTypeContact(type) {
@@ -1318,8 +1588,27 @@ function resolveVendorIdForContactCheck(scope, bodyOrRow) {
1318
1588
  return Number.isFinite(n) ? n : null;
1319
1589
  }
1320
1590
  __name(resolveVendorIdForContactCheck, "resolveVendorIdForContactCheck");
1591
+ function uniquePositiveIds(ids) {
1592
+ const out = [];
1593
+ const seen = /* @__PURE__ */ new Set();
1594
+ for (const raw of ids) {
1595
+ const n = Number(raw);
1596
+ if (!Number.isFinite(n) || n <= 0 || seen.has(n)) continue;
1597
+ seen.add(n);
1598
+ out.push(n);
1599
+ }
1600
+ return out;
1601
+ }
1602
+ __name(uniquePositiveIds, "uniquePositiveIds");
1321
1603
  async function ensureVendorCustomerForOrderContact(dataSource, entityMap, vendorId, contactId, details) {
1322
- if (!Number.isFinite(vendorId) || !Number.isFinite(contactId)) return;
1604
+ await ensureVendorCustomersForOrder(dataSource, entityMap, [
1605
+ vendorId
1606
+ ], contactId, details);
1607
+ }
1608
+ __name(ensureVendorCustomerForOrderContact, "ensureVendorCustomerForOrderContact");
1609
+ async function ensureVendorCustomersForOrder(dataSource, entityMap, vendorIds, contactId, details) {
1610
+ const vendors = uniquePositiveIds(vendorIds);
1611
+ if (vendors.length === 0 || !Number.isFinite(contactId) || contactId <= 0) return;
1323
1612
  const vcEntity = entityMap.vendor_customers;
1324
1613
  const contactEntity = entityMap.contacts;
1325
1614
  if (!vcEntity || !contactEntity) return;
@@ -1345,96 +1634,61 @@ async function ensureVendorCustomerForOrderContact(dataSource, entityMap, vendor
1345
1634
  }
1346
1635
  let customerId = null;
1347
1636
  if (entityMap.customer) {
1348
- const customerRepo = dataSource.getRepository(entityMap.customer);
1349
- const existingCustomer = await customerRepo.findOne({
1350
- where: {
1351
- email,
1352
- deleted: false
1353
- }
1637
+ const ensured = await ensureCustomerRecord(dataSource, entityMap.customer, {
1638
+ name,
1639
+ email,
1640
+ phone,
1641
+ userId: null
1354
1642
  });
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({
1643
+ if (ensured) customerId = ensured.id;
1644
+ }
1645
+ for (const vendorId of vendors) {
1646
+ if (customerId != null) {
1647
+ const byCustomer = await vcRepo.findOne({
1360
1648
  where: {
1361
- email,
1362
- deleted: false
1649
+ vendorId,
1650
+ customerId
1363
1651
  }
1364
1652
  });
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
- }
1653
+ if (byCustomer) {
1654
+ const existingContactId = byCustomer.contactId;
1655
+ if (existingContactId == null) {
1656
+ await vcRepo.update(byCustomer.id, {
1657
+ contactId
1374
1658
  });
1375
- if (customerGroup) groupId = Number(customerGroup.id);
1376
1659
  }
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
- }));
1660
+ continue;
1386
1661
  }
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
1662
  }
1399
- }
1400
- if (customerId != null) {
1401
- const byCustomer = await vcRepo.findOne({
1663
+ const byContact = await vcRepo.findOne({
1402
1664
  where: {
1403
1665
  vendorId,
1404
- customerId
1666
+ contactId
1405
1667
  }
1406
1668
  });
1407
- if (byCustomer) {
1408
- const existingContactId = byCustomer.contactId;
1409
- if (existingContactId == null) {
1410
- await vcRepo.update(byCustomer.id, {
1411
- contactId
1669
+ if (byContact) {
1670
+ if (customerId != null && byContact.customerId == null) {
1671
+ await vcRepo.update(byContact.id, {
1672
+ customerId
1412
1673
  });
1413
1674
  }
1414
- return;
1675
+ continue;
1415
1676
  }
1416
- }
1417
- const byContact = await vcRepo.findOne({
1418
- where: {
1419
- vendorId,
1420
- contactId
1677
+ if (customerId != null) {
1678
+ await vcRepo.save(vcRepo.create({
1679
+ vendorId,
1680
+ customerId,
1681
+ contactId
1682
+ }));
1683
+ } else {
1684
+ await vcRepo.save(vcRepo.create({
1685
+ vendorId,
1686
+ contactId
1687
+ }));
1421
1688
  }
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
1689
  }
1432
- await vcRepo.save(vcRepo.create({
1433
- vendorId,
1434
- contactId
1435
- }));
1436
1690
  }
1437
- __name(ensureVendorCustomerForOrderContact, "ensureVendorCustomerForOrderContact");
1691
+ __name(ensureVendorCustomersForOrder, "ensureVendorCustomersForOrder");
1438
1692
 
1439
1693
  // src/lib/currency-prices.ts
1440
1694
  function normalizeCurrencyCode(code) {
@@ -1876,6 +2130,7 @@ async function ensureUniqueProductSlug(repo, baseSlug, vendorId, excludeId) {
1876
2130
  let slug = baseSlug;
1877
2131
  let suffix = 0;
1878
2132
  while (true) {
2133
+ await retireSoftDeletedUniqueValue(repo, "slug", slug);
1879
2134
  const where = excludeId != null ? {
1880
2135
  slug,
1881
2136
  vendorId,
@@ -1895,6 +2150,21 @@ async function ensureUniqueProductSlug(repo, baseSlug, vendorId, excludeId) {
1895
2150
  }
1896
2151
  }
1897
2152
  __name(ensureUniqueProductSlug, "ensureUniqueProductSlug");
2153
+ async function normalizeComboNameAndSlug(repo, body, vendorId, existing, excludeId) {
2154
+ const incomingName = typeof body.name === "string" ? body.name.trim() : "";
2155
+ const incomingSlugRaw = typeof body.slug === "string" ? body.slug.trim() : "";
2156
+ const existingName = typeof existing?.name === "string" ? existing.name.trim() : "";
2157
+ const existingSlug = typeof existing?.slug === "string" ? existing.slug.trim() : "";
2158
+ const name = incomingName || existingName;
2159
+ if (!name) return "Name is required";
2160
+ if (incomingName) body.name = name;
2161
+ const nameBasedSlug = slugifyProductLabel(name);
2162
+ const typedSlug = incomingSlugRaw ? slugifyProductLabel(incomingSlugRaw) : "";
2163
+ const slug = typedSlug || (excludeId != null ? existingSlug : "") || nameBasedSlug;
2164
+ body.slug = await ensureUniqueProductSlug(repo, slug, vendorId, excludeId);
2165
+ return null;
2166
+ }
2167
+ __name(normalizeComboNameAndSlug, "normalizeComboNameAndSlug");
1898
2168
  async function hydrateProductEventNames(dataSource, entityMap, products) {
1899
2169
  if (!products.length) return products;
1900
2170
  if (!entityMap.event_products || !entityMap.events) {
@@ -2141,6 +2411,20 @@ async function normalizeProductNameAndSlug(repo, body, vendorId, existing, exclu
2141
2411
  return null;
2142
2412
  }
2143
2413
  __name(normalizeProductNameAndSlug, "normalizeProductNameAndSlug");
2414
+ async function retireSoftDeletedUniquesFromBody(repo, body) {
2415
+ const cols = new Set(repo.metadata.columns.map((c) => c.propertyName));
2416
+ if (!cols.has("deleted")) return;
2417
+ for (const col of [
2418
+ "slug",
2419
+ "email"
2420
+ ]) {
2421
+ if (!cols.has(col)) continue;
2422
+ const raw = body[col];
2423
+ if (typeof raw !== "string" || !raw.trim()) continue;
2424
+ await retireSoftDeletedUniqueValue(repo, col, raw.trim());
2425
+ }
2426
+ }
2427
+ __name(retireSoftDeletedUniquesFromBody, "retireSoftDeletedUniquesFromBody");
2144
2428
  function buildSoftDeletePayload(meta, deletedBy) {
2145
2429
  const payload = {
2146
2430
  deleted: true
@@ -2377,6 +2661,7 @@ function discountEvaluateRule(rule, cartLines, cartTotal) {
2377
2661
  case "category":
2378
2662
  case "cart":
2379
2663
  case "paymentMethod":
2664
+ case "deviceType":
2380
2665
  case "order": {
2381
2666
  if (rule.subType === "nthOrder") {
2382
2667
  return true;
@@ -2560,87 +2845,22 @@ function applyDiscountToOrderLines(lines, evalResult, maxDiscountAmount) {
2560
2845
  return totalDiscountApplied;
2561
2846
  }
2562
2847
  __name(applyDiscountToOrderLines, "applyDiscountToOrderLines");
2563
- async function checkAndIncrementDiscountUsage(dataSource, entityMap, discountId, contactId) {
2564
- if (!entityMap["discounts"]) return null;
2565
- const discountRepo = dataSource.getRepository(entityMap["discounts"]);
2566
- const discount = await discountRepo.findOne({
2567
- where: {
2568
- id: discountId
2569
- }
2570
- });
2571
- if (!discount) return `Discount #${discountId} not found`;
2572
- await dataSource.query(`UPDATE discounts SET "usedCount" = "usedCount" + 1 WHERE id = $1`, [
2573
- discountId
2574
- ]);
2575
- return null;
2848
+ function asDiscountUsageEntityMap(entityMap) {
2849
+ return entityMap;
2576
2850
  }
2577
- __name(checkAndIncrementDiscountUsage, "checkAndIncrementDiscountUsage");
2578
- async function decrementDiscountUsage(dataSource, entityMap, discountId, orderId) {
2579
- if (!entityMap["discounts"]) return;
2580
- try {
2581
- await dataSource.query(`UPDATE discounts SET "usedCount" = GREATEST("usedCount" - 1, 0) WHERE id = $1`, [
2582
- discountId
2583
- ]);
2584
- if (entityMap["order_discounts"]) {
2585
- await dataSource.getRepository(entityMap["order_discounts"]).delete({
2586
- orderId,
2587
- discountId
2588
- });
2589
- }
2590
- } catch (err) {
2591
- console.error("[discountEngine] decrementDiscountUsage failed:", err);
2592
- }
2851
+ __name(asDiscountUsageEntityMap, "asDiscountUsageEntityMap");
2852
+ async function checkAndIncrementDiscountUsage2(dataSource, entityMap, discountId, contactId) {
2853
+ return checkAndIncrementDiscountUsage(dataSource, asDiscountUsageEntityMap(entityMap), discountId, contactId);
2593
2854
  }
2594
- __name(decrementDiscountUsage, "decrementDiscountUsage");
2595
- async function recordDiscountUsage(dataSource, entityMap, orderId, discountId, contactId, discountAmount) {
2596
- if (!entityMap["order_discounts"]) return;
2597
- try {
2598
- let discountCode = "";
2599
- if (entityMap["discounts"]) {
2600
- const discountRow = await dataSource.getRepository(entityMap["discounts"]).findOne({
2601
- where: {
2602
- id: discountId
2603
- }
2604
- });
2605
- discountCode = discountRow?.couponCode ?? "";
2606
- }
2607
- const odRepo = dataSource.getRepository(entityMap["order_discounts"]);
2608
- const orderItemRepo = dataSource.getRepository(entityMap.order_items);
2609
- const items = await orderItemRepo.find({
2610
- where: {
2611
- orderId
2612
- }
2613
- });
2614
- const discountedItems = items.filter((item) => Number(item.discount ?? 0) > 0);
2615
- if (discountedItems.length === 0) {
2616
- const firstItem = items[0];
2617
- if (!firstItem) return;
2618
- await odRepo.save(odRepo.create({
2619
- orderId,
2620
- orderItemId: firstItem.id,
2621
- discountId,
2622
- discountCode,
2623
- discountAmount,
2624
- userId: contactId
2625
- }));
2626
- return;
2627
- }
2628
- for (const item of discountedItems) {
2629
- const i = item;
2630
- await odRepo.save(odRepo.create({
2631
- orderId,
2632
- orderItemId: i.id,
2633
- discountId,
2634
- discountCode,
2635
- discountAmount: Number(i.discount ?? 0),
2636
- userId: contactId
2637
- }));
2638
- }
2639
- } catch (err) {
2640
- console.error("[discountEngine] recordDiscountUsage failed:", err);
2641
- }
2855
+ __name(checkAndIncrementDiscountUsage2, "checkAndIncrementDiscountUsage");
2856
+ async function decrementDiscountUsage2(dataSource, entityMap, discountId, orderId) {
2857
+ return decrementDiscountUsage(dataSource, asDiscountUsageEntityMap(entityMap), discountId, orderId);
2642
2858
  }
2643
- __name(recordDiscountUsage, "recordDiscountUsage");
2859
+ __name(decrementDiscountUsage2, "decrementDiscountUsage");
2860
+ async function recordDiscountUsage2(dataSource, entityMap, orderId, discountId, contactId, discountAmount) {
2861
+ return recordDiscountUsage(dataSource, asDiscountUsageEntityMap(entityMap), orderId, discountId, contactId, discountAmount);
2862
+ }
2863
+ __name(recordDiscountUsage2, "recordDiscountUsage");
2644
2864
  async function validateCoupon(dataSource, entityMap, couponCode, orderLines, currency = "INR", contactId, isAutomatic, discountId, vendorId) {
2645
2865
  if (!entityMap["discounts"]) return {
2646
2866
  valid: false,
@@ -2685,21 +2905,12 @@ async function validateCoupon(dataSource, entityMap, couponCode, orderLines, cur
2685
2905
  };
2686
2906
  }
2687
2907
  if (contactId != null && d.maxUsagePerUser != null) {
2688
- try {
2689
- const userUsage = await dataSource.query(`SELECT COUNT(DISTINCT od."orderId") as count FROM order_discounts od
2690
- WHERE od."discountId" = $1
2691
- AND od."userId" = $2`, [
2692
- d.id,
2693
- contactId
2694
- ]);
2695
- const usageCount = Number(userUsage?.[0]?.count ?? 0);
2696
- if (usageCount >= d.maxUsagePerUser) {
2697
- return {
2698
- valid: false,
2699
- error: "You have reached the maximum usage limit for this discount"
2700
- };
2701
- }
2702
- } catch {
2908
+ const usageCount = await countDiscountOrdersForContact(dataSource, d.id, contactId);
2909
+ if (usageCount >= d.maxUsagePerUser) {
2910
+ return {
2911
+ valid: false,
2912
+ error: "You have reached the maximum usage limit for this discount"
2913
+ };
2703
2914
  }
2704
2915
  }
2705
2916
  const flatRules = entityMap["discount_rules"] ? await dataSource.getRepository(entityMap["discount_rules"]).find({
@@ -2777,128 +2988,32 @@ function normalizeOrderLinesInput(raw) {
2777
2988
  if (!Number.isFinite(productId) || productId <= 0) continue;
2778
2989
  const qRaw = Number(r.quantity ?? 1);
2779
2990
  const q = Number.isFinite(qRaw) ? Math.min(99999, Math.max(1, Math.floor(qRaw))) : 1;
2780
- merged.set(productId, (merged.get(productId) ?? 0) + q);
2991
+ const vidRaw = r.variantId ?? r.variant_id;
2992
+ const vid = vidRaw != null && String(vidRaw).trim() !== "" ? Number(vidRaw) : NaN;
2993
+ const variantId = Number.isFinite(vid) && vid > 0 ? vid : null;
2994
+ const key = `${productId}:${variantId ?? 0}`;
2995
+ const prev = merged.get(key);
2996
+ if (prev) prev.quantity += q;
2997
+ else merged.set(key, {
2998
+ productId,
2999
+ quantity: q,
3000
+ variantId
3001
+ });
2781
3002
  }
2782
3003
  return [
2783
- ...merged.entries()
2784
- ].map(([productId, quantity]) => ({
2785
- productId,
2786
- quantity
2787
- }));
3004
+ ...merged.values()
3005
+ ];
2788
3006
  }
2789
3007
  __name(normalizeOrderLinesInput, "normalizeOrderLinesInput");
2790
- var ORDER_STATUS_CONFIRMED = "confirmed";
2791
- function orderInventorySnapshotFromRow(order) {
2792
- return {
2793
- status: order.status ?? "pending",
2794
- lines: (order.items ?? []).map((i) => ({
2795
- productId: Number(i.productId),
2796
- quantity: Math.max(0, Math.floor(Number(i.quantity) || 0))
2797
- })).filter((l) => l.productId > 0 && l.quantity > 0)
2798
- };
2799
- }
2800
- __name(orderInventorySnapshotFromRow, "orderInventorySnapshotFromRow");
2801
- function mergeInventoryLinesByProduct(lines) {
2802
- const m = /* @__PURE__ */ new Map();
2803
- for (const { productId, quantity } of lines) {
2804
- if (!Number.isFinite(productId) || productId <= 0 || quantity <= 0) continue;
2805
- m.set(productId, (m.get(productId) ?? 0) + quantity);
2806
- }
2807
- return m;
2808
- }
2809
- __name(mergeInventoryLinesByProduct, "mergeInventoryLinesByProduct");
2810
- async function validateInventoryForConfirmedOrderLines(dataSource, entityMap, calcLines) {
2811
- const productRepo = dataSource.getRepository(entityMap.products);
2812
- const merged = mergeInventoryLinesByProduct(calcLines.filter((l) => l.found && l.productId != null).map((l) => ({
2813
- productId: l.productId,
2814
- quantity: l.quantity
2815
- })));
2816
- for (const [productId, qty] of merged) {
2817
- const row = await productRepo.findOne({
2818
- where: {
2819
- id: productId,
2820
- deleted: false
2821
- }
2822
- });
2823
- if (!row) return `Product #${productId} not found`;
2824
- if (row.type === "service") continue;
2825
- const cur = Number(row.quantity ?? 0);
2826
- if (cur < qty) {
2827
- const name = row.name ?? String(productId);
2828
- return `Insufficient stock for ${name}: available ${cur}, required ${qty}`;
2829
- }
2830
- }
2831
- return null;
2832
- }
2833
- __name(validateInventoryForConfirmedOrderLines, "validateInventoryForConfirmedOrderLines");
2834
- async function applyInventoryDeltaForMergedLines(dataSource, entityMap, merged, sign, validateDeduct) {
2835
- const productRepo = dataSource.getRepository(entityMap.products);
2836
- for (const [productId, qty] of merged) {
2837
- const row = await productRepo.findOne({
2838
- where: {
2839
- id: productId,
2840
- deleted: false
2841
- }
2842
- });
2843
- if (!row) return `Product #${productId} not found`;
2844
- if (row.type === "service") continue;
2845
- const cur = Number(row.quantity ?? 0);
2846
- if (sign === -1) {
2847
- if (validateDeduct && cur < qty) {
2848
- const name = row.name ?? String(productId);
2849
- return `Insufficient stock for ${name}: available ${cur}, required ${qty}`;
2850
- }
2851
- await productRepo.update(productId, {
2852
- quantity: Math.max(0, cur - qty)
2853
- });
2854
- } else {
2855
- await productRepo.update(productId, {
2856
- quantity: cur + qty
2857
- });
2858
- }
2859
- }
2860
- return null;
2861
- }
2862
- __name(applyInventoryDeltaForMergedLines, "applyInventoryDeltaForMergedLines");
2863
- async function validateInventoryForOrderBecomingConfirmed(dataSource, entityMap, prevSnap, nextLines) {
2864
- const mergedNext = mergeInventoryLinesByProduct(nextLines);
2865
- const productRepo = dataSource.getRepository(entityMap.products);
2866
- const prevByProduct = prevSnap?.status === ORDER_STATUS_CONFIRMED ? mergeInventoryLinesByProduct(prevSnap.lines) : /* @__PURE__ */ new Map();
2867
- for (const [productId, need] of mergedNext) {
2868
- const row = await productRepo.findOne({
2869
- where: {
2870
- id: productId,
2871
- deleted: false
2872
- }
2873
- });
2874
- if (!row) return `Product #${productId} not found`;
2875
- if (row.type === "service") continue;
2876
- let available = Number(row.quantity ?? 0);
2877
- available += prevByProduct.get(productId) ?? 0;
2878
- if (available < need) {
2879
- const name = row.name ?? String(productId);
2880
- return `Insufficient stock for ${name}: available ${available}, required ${need}`;
2881
- }
2882
- }
2883
- return null;
2884
- }
2885
- __name(validateInventoryForOrderBecomingConfirmed, "validateInventoryForOrderBecomingConfirmed");
2886
- async function reconcileOrderInventoryBetweenSnapshots(dataSource, entityMap, prev, next) {
2887
- const wasConfirmed = prev.status === ORDER_STATUS_CONFIRMED;
2888
- const isConfirmed = next.status === ORDER_STATUS_CONFIRMED;
2889
- if (wasConfirmed) {
2890
- const merged = mergeInventoryLinesByProduct(prev.lines);
2891
- const err = await applyInventoryDeltaForMergedLines(dataSource, entityMap, merged, 1, false);
2892
- if (err) return err;
2893
- }
2894
- if (isConfirmed) {
2895
- const merged = mergeInventoryLinesByProduct(next.lines);
2896
- const err = await applyInventoryDeltaForMergedLines(dataSource, entityMap, merged, -1, true);
2897
- if (err) return err;
2898
- }
2899
- return null;
3008
+ var ORDER_STATUS_CONFIRMED = "confirmed";
3009
+ async function validateInventoryForConfirmedOrderLines2(dataSource, entityMap, calcLines) {
3010
+ const lines = calcLines.filter((l) => l.found && l.productId != null).map((l) => ({
3011
+ productId: l.productId,
3012
+ quantity: l.quantity
3013
+ }));
3014
+ return validateInventoryForConfirmedOrderLines(dataSource, entityMap, lines);
2900
3015
  }
2901
- __name(reconcileOrderInventoryBetweenSnapshots, "reconcileOrderInventoryBetweenSnapshots");
3016
+ __name(validateInventoryForConfirmedOrderLines2, "validateInventoryForConfirmedOrderLines");
2902
3017
  async function resolveProductTaxRate(dataSource, entityMap, productId, productVendorId) {
2903
3018
  if (!entityMap.product_config || !entityMap.taxes) {
2904
3019
  return {
@@ -3800,23 +3915,24 @@ function createCrudHandler(dataSource, entityMap, options) {
3800
3915
  if (resource === "vendor_customers" && entityMap["customer"]) {
3801
3916
  const scope = await resolveVendorCustomersScope(dataSource, entityMap, resolveScope);
3802
3917
  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);
3918
+ 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
3919
  applyVendorScopeToQueryBuilder(qb, "vc", scope);
3805
3920
  if (search && typeof search === "string" && search.trim()) {
3806
3921
  const term = `%${search.trim()}%`;
3807
- qb.andWhere("(customer.name ILIKE :term OR customer.email ILIKE :term OR customer.phone ILIKE :term)", {
3922
+ 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
3923
  term
3809
3924
  });
3810
3925
  }
3811
3926
  const [rows, total2] = await qb.getManyAndCount();
3812
3927
  const data2 = rows.map((row) => {
3813
3928
  const customer = row.customer;
3929
+ const contact = row.contact;
3814
3930
  return {
3815
3931
  ...row,
3816
- name: customer?.name ?? null,
3817
- email: customer?.email ?? null,
3818
- phone: customer?.phone ?? null,
3819
- company: customer?.company ?? null
3932
+ name: customer?.name ?? contact?.name ?? null,
3933
+ email: customer?.email ?? contact?.email ?? null,
3934
+ phone: customer?.phone ?? contact?.phone ?? null,
3935
+ company: customer?.company ?? contact?.company ?? null
3820
3936
  };
3821
3937
  });
3822
3938
  return json({
@@ -4150,7 +4266,11 @@ function createCrudHandler(dataSource, entityMap, options) {
4150
4266
  } else if (resource === "collections") {
4151
4267
  if (scope.type === "vendor") {
4152
4268
  where = mergeVendorScopeIntoWhere(where, scope, resource, catalogFlags);
4153
- } else if (searchParams.get("isCatalog") !== "true") {
4269
+ } else if (searchParams.get("isCatalog") === "true") {
4270
+ where = mergeListWhereAnd(where, {
4271
+ isCatalog: true
4272
+ });
4273
+ } else if (searchParams.get("isCatalog") === "false") {
4154
4274
  where = mergeListWhereAnd(where, {
4155
4275
  isCatalog: false
4156
4276
  });
@@ -4347,41 +4467,17 @@ function createCrudHandler(dataSource, entityMap, options) {
4347
4467
  status: 503
4348
4468
  });
4349
4469
  }
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 {
4470
+ if (!entityMap["contacts"]) {
4375
4471
  return json({
4376
- error: "user_groups entity not configured"
4472
+ error: "Contacts entity not configured"
4377
4473
  }, {
4378
4474
  status: 503
4379
4475
  });
4380
4476
  }
4381
4477
  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();
4478
+ const email = String(body.email ?? "").trim().toLowerCase();
4479
+ const phoneRaw = String(body.phone ?? "").trim();
4480
+ const phone = phoneRaw || null;
4385
4481
  if (!name) return json({
4386
4482
  error: "name is required"
4387
4483
  }, {
@@ -4392,25 +4488,7 @@ function createCrudHandler(dataSource, entityMap, options) {
4392
4488
  }, {
4393
4489
  status: 400
4394
4490
  });
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
4491
  const customerRepo = dataSource.getRepository(entityMap["customer"]);
4413
- const userRepo = dataSource.getRepository(entityMap["users"]);
4414
4492
  const dupCustEmail = await customerRepo.findOne({
4415
4493
  where: {
4416
4494
  email,
@@ -4424,49 +4502,26 @@ function createCrudHandler(dataSource, entityMap, options) {
4424
4502
  status: 409
4425
4503
  });
4426
4504
  }
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
4505
+ if (phone) {
4506
+ const dupCustPhone = await customerRepo.findOne({
4507
+ where: {
4508
+ phone,
4509
+ deleted: false
4510
+ }
4438
4511
  });
4439
- }
4440
- let userId;
4441
- const dupUser = await userRepo.findOne({
4442
- where: {
4443
- email,
4444
- deleted: false
4512
+ if (dupCustPhone) {
4513
+ return json({
4514
+ error: "A customer with this phone number already exists"
4515
+ }, {
4516
+ status: 409
4517
+ });
4445
4518
  }
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
4519
  }
4463
- const savedCustomer = await ensureCustomerForUser(dataSource, entityMap["customer"], {
4464
- id: userId,
4520
+ const savedCustomer = await ensureCustomerRecord(dataSource, entityMap["customer"], {
4465
4521
  name,
4466
4522
  email,
4467
- phone
4468
- }, {
4469
- phone
4523
+ phone,
4524
+ userId: null
4470
4525
  });
4471
4526
  if (!savedCustomer) {
4472
4527
  return json({
@@ -4475,37 +4530,37 @@ function createCrudHandler(dataSource, entityMap, options) {
4475
4530
  status: 409
4476
4531
  });
4477
4532
  }
4478
- const customerRepo2 = dataSource.getRepository(entityMap["customer"]);
4479
- const customerRow = await customerRepo2.findOne({
4533
+ const contactRepo = dataSource.getRepository(entityMap["contacts"]);
4534
+ let contact = await contactRepo.findOne({
4480
4535
  where: {
4481
- id: savedCustomer.id,
4536
+ email,
4482
4537
  deleted: false
4483
4538
  }
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
- }
4539
+ });
4540
+ if (!contact) {
4541
+ contact = await contactRepo.save(contactRepo.create({
4542
+ name,
4543
+ email,
4544
+ phone: phone || null,
4545
+ type: "customer"
4546
+ }));
4547
+ } else {
4548
+ const t = contact.type;
4549
+ const patch = {
4550
+ name,
4551
+ phone: phone || contact.phone || null
4552
+ };
4553
+ if (t == null || t === "" || String(t).toLowerCase() !== "customer") {
4554
+ patch.type = "customer";
4555
+ }
4556
+ await contactRepo.update(contact.id, patch);
4557
+ contact = {
4558
+ ...contact,
4559
+ ...patch,
4560
+ id: contact.id
4561
+ };
4508
4562
  }
4563
+ const contactId = Number(contact.id);
4509
4564
  const scope = await resolveVendorCustomersScope(dataSource, entityMap, resolveScope);
4510
4565
  let vendorId = null;
4511
4566
  if (scope.type === "vendor") {
@@ -4521,19 +4576,25 @@ function createCrudHandler(dataSource, entityMap, options) {
4521
4576
  status: 400
4522
4577
  });
4523
4578
  }
4524
- const vcRepo = dataSource.getRepository(entity);
4525
- const existingLink = await vcRepo.findOne({
4579
+ await ensureVendorCustomersForOrder(dataSource, entityMap, [
4580
+ vendorId
4581
+ ], contactId, {
4582
+ name,
4583
+ email,
4584
+ phone
4585
+ });
4586
+ const customerRow = await customerRepo.findOne({
4526
4587
  where: {
4527
- customerId: savedCustomer.id
4588
+ id: savedCustomer.id,
4589
+ deleted: false
4528
4590
  }
4529
4591
  });
4530
- if (!existingLink) {
4531
- await vcRepo.save(vcRepo.create({
4532
- customerId: savedCustomer.id,
4533
- vendorId
4534
- }));
4535
- }
4536
- return json(customerRow, {
4592
+ return json(customerRow ?? {
4593
+ id: savedCustomer.id,
4594
+ name,
4595
+ email,
4596
+ phone
4597
+ }, {
4537
4598
  status: 201
4538
4599
  });
4539
4600
  }
@@ -4938,6 +4999,22 @@ function createCrudHandler(dataSource, entityMap, options) {
4938
4999
  status: 400
4939
5000
  });
4940
5001
  }
5002
+ if (resource === "combos") {
5003
+ const vendorIdForSlug = scopeCreate.type === "vendor" ? scopeCreate.vendorId : Number(persistBody.vendorId);
5004
+ if (!Number.isFinite(vendorIdForSlug) || vendorIdForSlug <= 0) {
5005
+ return json({
5006
+ error: "vendorId is required"
5007
+ }, {
5008
+ status: 400
5009
+ });
5010
+ }
5011
+ const comboSlugErr = await normalizeComboNameAndSlug(repo, persistBody, vendorIdForSlug);
5012
+ if (comboSlugErr) return json({
5013
+ error: comboSlugErr
5014
+ }, {
5015
+ status: 400
5016
+ });
5017
+ }
4941
5018
  sanitizeBodyForEntity(repo, persistBody);
4942
5019
  let created;
4943
5020
  try {
@@ -4981,6 +5058,7 @@ function createCrudHandler(dataSource, entityMap, options) {
4981
5058
  }
4982
5059
  const nameRaw = String(body["contact.name"] ?? contact.name ?? "").trim();
4983
5060
  const phoneRaw = body["contact.phone"] ?? contact.phone;
5061
+ const emailForVc = (emailRaw || String(contact.email ?? "")).trim().toLowerCase();
4984
5062
  const phoneToSave = phoneRaw == null || phoneRaw === "" ? null : String(phoneRaw).trim();
4985
5063
  if (phoneToSave && contact.phone !== phoneToSave) {
4986
5064
  await contactRepo.update(contact.id, {
@@ -4989,21 +5067,45 @@ function createCrudHandler(dataSource, entityMap, options) {
4989
5067
  contact.phone = phoneToSave;
4990
5068
  }
4991
5069
  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,
5070
+ const orderLinesForVendors = normalizeOrderLinesInput(body.orderLines);
5071
+ const vendorIdsForVc = [];
5072
+ if (vendorIdForCustomer != null) vendorIdsForVc.push(vendorIdForCustomer);
5073
+ if (orderLinesForVendors?.length && entityMap.products) {
5074
+ const productRepoForVc = dataSource.getRepository(entityMap.products);
5075
+ for (const line of orderLinesForVendors) {
5076
+ const pid = Number(line.productId);
5077
+ if (!Number.isFinite(pid)) continue;
5078
+ const product = await productRepoForVc.findOne({
5079
+ where: {
5080
+ id: pid
5081
+ }
5082
+ });
5083
+ const pVid = Number(product?.vendorId);
5084
+ if (Number.isFinite(pVid) && pVid > 0) vendorIdsForVc.push(pVid);
5085
+ }
5086
+ }
5087
+ if (vendorIdsForVc.length > 0 && emailForVc) {
5088
+ await ensureVendorCustomersForOrder(dataSource, entityMap, vendorIdsForVc, contact.id, {
5089
+ name: nameRaw || emailForVc.split("@")[0] || "Customer",
5090
+ email: emailForVc,
4997
5091
  phone: phoneRaw == null || phoneRaw === "" ? null : String(phoneRaw)
4998
5092
  });
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
- });
5093
+ }
5094
+ const accountCustomerId = Number(body.accountCustomerId);
5095
+ if (entityMap.customer_contacts && Number.isFinite(accountCustomerId) && accountCustomerId > 0) {
5096
+ const ccRepo = dataSource.getRepository(entityMap.customer_contacts);
5097
+ const orderContactId = contact.id;
5098
+ const existingLink = await ccRepo.findOne({
5099
+ where: {
5100
+ customerId: accountCustomerId,
5101
+ contactId: orderContactId
5102
+ }
5103
+ });
5104
+ if (!existingLink) {
5105
+ await ccRepo.save(ccRepo.create({
5106
+ customerId: accountCustomerId,
5107
+ contactId: orderContactId
5108
+ }));
5007
5109
  }
5008
5110
  }
5009
5111
  const randomPart = Math.floor(1e4 + Math.random() * 9e4);
@@ -5011,6 +5113,7 @@ function createCrudHandler(dataSource, entityMap, options) {
5011
5113
  persistBody.orderNumber = `ORD00${randomPart}`;
5012
5114
  persistBody.qrToken = crypto2.randomBytes(16).toString("hex");
5013
5115
  }
5116
+ await retireSoftDeletedUniquesFromBody(repo, persistBody);
5014
5117
  created = await repo.save(repo.create(persistBody));
5015
5118
  if (resource === "orders") {
5016
5119
  const orderItemRepo = dataSource.getRepository(entityMap.order_items);
@@ -5052,8 +5155,8 @@ function createCrudHandler(dataSource, entityMap, options) {
5052
5155
  }
5053
5156
  }
5054
5157
  const createdStatus = String(persistBody.status ?? "pending");
5055
- if (createdStatus === ORDER_STATUS_CONFIRMED) {
5056
- const stockErr = await validateInventoryForConfirmedOrderLines(dataSource, entityMap, lines);
5158
+ if (orderStatusHoldsStock(createdStatus)) {
5159
+ const stockErr = await validateInventoryForConfirmedOrderLines2(dataSource, entityMap, lines);
5057
5160
  if (stockErr) {
5058
5161
  await repo.delete(created.id);
5059
5162
  return json({
@@ -5075,6 +5178,7 @@ function createCrudHandler(dataSource, entityMap, options) {
5075
5178
  await orderItemRepo.save(orderItemRepo.create({
5076
5179
  orderId: created.id,
5077
5180
  productId: line.productId,
5181
+ variantId: null,
5078
5182
  quantity: line.quantity,
5079
5183
  unitPrice: line.unitPrice,
5080
5184
  subTotal: line.subtotal,
@@ -5168,7 +5272,7 @@ function createCrudHandler(dataSource, entityMap, options) {
5168
5272
  }
5169
5273
  if (orderDiscountId) {
5170
5274
  if (createdStatus === ORDER_STATUS_CONFIRMED) {
5171
- const usageErr = await checkAndIncrementDiscountUsage(dataSource, entityMap, orderDiscountId, persistBody.contactId);
5275
+ const usageErr = await checkAndIncrementDiscountUsage2(dataSource, entityMap, orderDiscountId, persistBody.contactId);
5172
5276
  if (usageErr) {
5173
5277
  await repo.delete(created.id);
5174
5278
  return json({
@@ -5178,7 +5282,7 @@ function createCrudHandler(dataSource, entityMap, options) {
5178
5282
  });
5179
5283
  }
5180
5284
  }
5181
- await recordDiscountUsage(dataSource, entityMap, created.id, orderDiscountId, persistBody.contactId, discount);
5285
+ await recordDiscountUsage2(dataSource, entityMap, created.id, orderDiscountId, persistBody.contactId, discount);
5182
5286
  }
5183
5287
  const orderIdForNotify = created.id;
5184
5288
  void import('./emit-order-notification-trigger-IVLWKKJL.js').then(({ fireOrderNotificationTrigger }) => {
@@ -5187,6 +5291,16 @@ function createCrudHandler(dataSource, entityMap, options) {
5187
5291
  entityMap
5188
5292
  });
5189
5293
  });
5294
+ if (getCms) {
5295
+ void import('./send-order-placed-emails-WPI37KRZ.js').then(({ sendOrderPlacedEmailsAfterConfirmation }) => {
5296
+ return sendOrderPlacedEmailsAfterConfirmation(orderIdForNotify, {
5297
+ getDataSource: /* @__PURE__ */ __name(async () => dataSource, "getDataSource"),
5298
+ entityMap,
5299
+ getCms: /* @__PURE__ */ __name(async () => await getCms(), "getCms")
5300
+ });
5301
+ }).catch(() => {
5302
+ });
5303
+ }
5190
5304
  }
5191
5305
  } catch (err) {
5192
5306
  const message = err instanceof Error ? err.message : String(err);
@@ -5832,6 +5946,7 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
5832
5946
  }
5833
5947
  }
5834
5948
  sanitizeBodyForEntity(repo, updatePayload2);
5949
+ await retireSoftDeletedUniquesFromBody(repo, updatePayload2);
5835
5950
  await repo.update(numericId, updatePayload2);
5836
5951
  if (Array.isArray(rawBody.tags)) {
5837
5952
  const tagNames = rawBody.tags.map((t) => String(t).trim()).filter(Boolean);
@@ -5920,7 +6035,7 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
5920
6035
  } : await calculateOrderTotalsFromLines(dataSource, entityMap, orderLinesNorm, effectiveCurrency, putDiscountId, Number.isFinite(putOrderVendorId) ? putOrderVendorId : null);
5921
6036
  const scalarUpdate = pickColumnUpdates(repo, rawBody);
5922
6037
  const nextStatus = typeof scalarUpdate.status === "string" && scalarUpdate.status ? String(scalarUpdate.status) : orderInvPrev?.status ?? "pending";
5923
- if (nextStatus === ORDER_STATUS_CONFIRMED && lines.length > 0) {
6038
+ if (orderStatusHoldsStock(nextStatus) && lines.length > 0) {
5924
6039
  const verr = await validateInventoryForOrderBecomingConfirmed(dataSource, entityMap, orderInvPrev, lines.filter((l) => l.found && l.productId != null).map((l) => ({
5925
6040
  productId: l.productId,
5926
6041
  quantity: l.quantity
@@ -6049,17 +6164,17 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
6049
6164
  }) : null;
6050
6165
  const existingDiscountId = existingOd ? existingOd.discountId : null;
6051
6166
  if (!putDiscountId && existingDiscountId && wasConfirmed) {
6052
- await decrementDiscountUsage(dataSource, entityMap, existingDiscountId, numericId);
6167
+ await decrementDiscountUsage2(dataSource, entityMap, existingDiscountId, numericId);
6053
6168
  if (entityMap["order_discounts"] && existingOd) {
6054
6169
  await dataSource.getRepository(entityMap["order_discounts"]).delete(existingOd.id);
6055
6170
  }
6056
6171
  }
6057
6172
  if (putDiscountId && existingDiscountId && putDiscountId !== existingDiscountId) {
6058
6173
  if (wasConfirmed) {
6059
- await decrementDiscountUsage(dataSource, entityMap, existingDiscountId, numericId);
6174
+ await decrementDiscountUsage2(dataSource, entityMap, existingDiscountId, numericId);
6060
6175
  }
6061
6176
  if (isNowConfirmed) {
6062
- const usageErr = await checkAndIncrementDiscountUsage(dataSource, entityMap, putDiscountId);
6177
+ const usageErr = await checkAndIncrementDiscountUsage2(dataSource, entityMap, putDiscountId, cid);
6063
6178
  if (usageErr) return json({
6064
6179
  error: usageErr
6065
6180
  }, {
@@ -6072,12 +6187,12 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
6072
6187
  discountAmount: discount
6073
6188
  });
6074
6189
  } else {
6075
- await recordDiscountUsage(dataSource, entityMap, numericId, putDiscountId, cid, discount);
6190
+ await recordDiscountUsage2(dataSource, entityMap, numericId, putDiscountId, cid, discount);
6076
6191
  }
6077
6192
  }
6078
6193
  if (putDiscountId && existingDiscountId && putDiscountId === existingDiscountId) {
6079
6194
  if (!wasConfirmed && isNowConfirmed) {
6080
- const usageErr = await checkAndIncrementDiscountUsage(dataSource, entityMap, putDiscountId);
6195
+ const usageErr = await checkAndIncrementDiscountUsage2(dataSource, entityMap, putDiscountId, cid);
6081
6196
  if (usageErr) return json({
6082
6197
  error: usageErr
6083
6198
  }, {
@@ -6085,7 +6200,7 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
6085
6200
  });
6086
6201
  }
6087
6202
  if (wasConfirmed && !isNowConfirmed) {
6088
- await decrementDiscountUsage(dataSource, entityMap, putDiscountId, numericId);
6203
+ await decrementDiscountUsage2(dataSource, entityMap, putDiscountId, numericId);
6089
6204
  }
6090
6205
  if (existingOd && entityMap["order_discounts"]) {
6091
6206
  await dataSource.getRepository(entityMap["order_discounts"]).update(existingOd.id, {
@@ -6095,14 +6210,14 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
6095
6210
  }
6096
6211
  if (putDiscountId && !existingDiscountId) {
6097
6212
  if (isNowConfirmed) {
6098
- const usageErr = await checkAndIncrementDiscountUsage(dataSource, entityMap, putDiscountId);
6213
+ const usageErr = await checkAndIncrementDiscountUsage2(dataSource, entityMap, putDiscountId, cid);
6099
6214
  if (usageErr) return json({
6100
6215
  error: usageErr
6101
6216
  }, {
6102
6217
  status: 400
6103
6218
  });
6104
6219
  }
6105
- await recordDiscountUsage(dataSource, entityMap, numericId, putDiscountId, cid, discount);
6220
+ await recordDiscountUsage2(dataSource, entityMap, numericId, putDiscountId, cid, discount);
6106
6221
  }
6107
6222
  }
6108
6223
  }
@@ -6573,7 +6688,7 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
6573
6688
  }
6574
6689
  if (resource === "orders" && orderInvPrev) {
6575
6690
  const nextSt = typeof updatePayload.status === "string" && updatePayload.status ? String(updatePayload.status) : orderInvPrev.status;
6576
- if (nextSt === ORDER_STATUS_CONFIRMED && orderInvPrev.lines.length > 0) {
6691
+ if (orderStatusHoldsStock(nextSt) && orderInvPrev.lines.length > 0) {
6577
6692
  const verr = await validateInventoryForOrderBecomingConfirmed(dataSource, entityMap, orderInvPrev, orderInvPrev.lines);
6578
6693
  if (verr) return json({
6579
6694
  error: verr
@@ -6582,6 +6697,33 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
6582
6697
  });
6583
6698
  }
6584
6699
  }
6700
+ if (resource === "combos") {
6701
+ const currentCombo = await repo.findOne({
6702
+ where: {
6703
+ id: numericId,
6704
+ deleted: false
6705
+ }
6706
+ });
6707
+ if (!currentCombo) return json({
6708
+ message: "Not found"
6709
+ }, {
6710
+ status: 404
6711
+ });
6712
+ const vendorIdForSlug = scopePut.type === "vendor" ? scopePut.vendorId : Number(currentCombo.vendorId);
6713
+ if (!Number.isFinite(vendorIdForSlug) || vendorIdForSlug <= 0) {
6714
+ return json({
6715
+ error: "vendorId is required"
6716
+ }, {
6717
+ status: 400
6718
+ });
6719
+ }
6720
+ const comboSlugErr = await normalizeComboNameAndSlug(repo, updatePayload, vendorIdForSlug, currentCombo, numericId);
6721
+ if (comboSlugErr) return json({
6722
+ error: comboSlugErr
6723
+ }, {
6724
+ status: 400
6725
+ });
6726
+ }
6585
6727
  if (Object.keys(updatePayload).length > 0) {
6586
6728
  if (resource === "payments" && updatePayload.status === "paid" && updatePayload.paidAt == null) {
6587
6729
  const existingPayment = await repo.findOne({
@@ -6595,6 +6737,7 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
6595
6737
  }
6596
6738
  }
6597
6739
  sanitizeBodyForEntity(repo, updatePayload);
6740
+ await retireSoftDeletedUniquesFromBody(repo, updatePayload);
6598
6741
  await repo.update(numericId, updatePayload);
6599
6742
  }
6600
6743
  if (resource === "orders" && rawBody && entityMap["order_addresses"]) {
@@ -6770,7 +6913,7 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
6770
6913
  });
6771
6914
  if (od) {
6772
6915
  const odRow = od;
6773
- await decrementDiscountUsage(dataSource, entityMap, odRow.discountId, numericId);
6916
+ await decrementDiscountUsage2(dataSource, entityMap, odRow.discountId, numericId);
6774
6917
  }
6775
6918
  }
6776
6919
  }
@@ -6783,6 +6926,22 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
6783
6926
  }
6784
6927
  }
6785
6928
  await repo.update(numericId, buildSoftDeletePayload(repo.metadata, deletedBy));
6929
+ const softDeletedCols = repo.metadata.columns.map((c) => c.propertyName);
6930
+ const uniqueColsToRetire = [
6931
+ "slug",
6932
+ "email"
6933
+ ].filter((col) => softDeletedCols.includes(col));
6934
+ if (existing && uniqueColsToRetire.length > 0) {
6935
+ for (const col of uniqueColsToRetire) {
6936
+ const raw = existing[col];
6937
+ if (typeof raw !== "string") continue;
6938
+ const value = raw.trim();
6939
+ if (!value || value.includes("__deleted_")) continue;
6940
+ await repo.update(numericId, {
6941
+ [col]: `${value}__deleted_${numericId}`
6942
+ });
6943
+ }
6944
+ }
6786
6945
  return json({
6787
6946
  message: "Deleted successfully"
6788
6947
  }, {
@@ -6813,6 +6972,63 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
6813
6972
  };
6814
6973
  }
6815
6974
  __name(createCrudByIdHandler, "createCrudByIdHandler");
6975
+ async function linkUnclaimedContactToUser(dataSource, contactsEntity, userId, email) {
6976
+ const repo = dataSource.getRepository(contactsEntity);
6977
+ const found = await repo.findOne({
6978
+ where: {
6979
+ email,
6980
+ userId: IsNull(),
6981
+ deleted: false
6982
+ }
6983
+ });
6984
+ if (found) await repo.update(found.id, {
6985
+ userId
6986
+ });
6987
+ }
6988
+ __name(linkUnclaimedContactToUser, "linkUnclaimedContactToUser");
6989
+ function buildUserInviteLink(baseUrl, token) {
6990
+ const base = baseUrl.replace(/\/+$/, "");
6991
+ return `${base}/admin/invite?token=${encodeURIComponent(token)}`;
6992
+ }
6993
+ __name(buildUserInviteLink, "buildUserInviteLink");
6994
+ function newUserInviteToken() {
6995
+ return randomBytes(32).toString("hex");
6996
+ }
6997
+ __name(newUserInviteToken, "newUserInviteToken");
6998
+ async function findUserByInviteToken(dataSource, userEntity, token) {
6999
+ const trimmed = token.trim();
7000
+ if (!trimmed) return null;
7001
+ const repo = dataSource.getRepository(userEntity);
7002
+ const row = await repo.findOne({
7003
+ where: {
7004
+ inviteToken: trimmed,
7005
+ deleted: false
7006
+ },
7007
+ select: [
7008
+ "id",
7009
+ "email",
7010
+ "blocked",
7011
+ "password",
7012
+ "inviteStatus",
7013
+ "inviteToken"
7014
+ ]
7015
+ });
7016
+ if (!row) return null;
7017
+ const u = row;
7018
+ return u;
7019
+ }
7020
+ __name(findUserByInviteToken, "findUserByInviteToken");
7021
+ async function completeUserInviteAccept(dataSource, userEntity, userId, passwordHash) {
7022
+ await dataSource.getRepository(userEntity).update(userId, {
7023
+ password: passwordHash,
7024
+ blocked: false,
7025
+ inviteStatus: "active",
7026
+ inviteToken: null
7027
+ });
7028
+ }
7029
+ __name(completeUserInviteAccept, "completeUserInviteAccept");
7030
+
7031
+ // src/lib/vendor-invite-token.ts
6816
7032
  var VENDOR_INVITE_METADATA_TOKEN = "inviteToken";
6817
7033
  var VENDOR_INVITE_METADATA_EXPIRES = "inviteExpiresAt";
6818
7034
  var VENDOR_INVITE_METADATA_SENT = "inviteSentAt";
@@ -6837,6 +7053,13 @@ function invalidatedInviteMessage() {
6837
7053
  return "This invitation is no longer valid. Ask an admin to resend the invite.";
6838
7054
  }
6839
7055
  __name(invalidatedInviteMessage, "invalidatedInviteMessage");
7056
+ function userAlreadyActive(user) {
7057
+ const hasPassword = !!(user.password && String(user.password).trim());
7058
+ if (user.inviteStatus === "active" && hasPassword) return true;
7059
+ if (user.inviteStatus === "pending") return false;
7060
+ return !user.blocked && hasPassword;
7061
+ }
7062
+ __name(userAlreadyActive, "userAlreadyActive");
6840
7063
  async function validateInviteTokenForActivation(dataSource, entityMap, token) {
6841
7064
  const trimmedToken = String(token ?? "").trim();
6842
7065
  if (!trimmedToken) {
@@ -6883,15 +7106,15 @@ async function validateInviteTokenForActivation(dataSource, entityMap, token) {
6883
7106
  "id",
6884
7107
  "email",
6885
7108
  "blocked",
6886
- "password"
7109
+ "password",
7110
+ "inviteStatus"
6887
7111
  ]
6888
7112
  });
6889
7113
  if (!user2) return {
6890
7114
  ok: false,
6891
7115
  error: "User not found"
6892
7116
  };
6893
- const hasPassword2 = !!user2.password;
6894
- if (!user2.blocked && hasPassword2) {
7117
+ if (userAlreadyActive(user2)) {
6895
7118
  return {
6896
7119
  ok: false,
6897
7120
  error: "User is already active"
@@ -6907,6 +7130,24 @@ async function validateInviteTokenForActivation(dataSource, entityMap, token) {
6907
7130
  };
6908
7131
  }
6909
7132
  }
7133
+ if (userRepo && entityMap.users) {
7134
+ const userMatch = await findUserByInviteToken(dataSource, entityMap.users, trimmedToken);
7135
+ if (userMatch) {
7136
+ if (userMatch.inviteStatus === "active" || userAlreadyActive(userMatch)) {
7137
+ return {
7138
+ ok: false,
7139
+ error: "This invitation has already been accepted"
7140
+ };
7141
+ }
7142
+ return {
7143
+ ok: true,
7144
+ target: {
7145
+ userId: userMatch.id,
7146
+ email: userMatch.email
7147
+ }
7148
+ };
7149
+ }
7150
+ }
6910
7151
  let email;
6911
7152
  try {
6912
7153
  email = Buffer.from(trimmedToken, "base64").toString("utf8").trim();
@@ -6936,22 +7177,30 @@ async function validateInviteTokenForActivation(dataSource, entityMap, token) {
6936
7177
  "id",
6937
7178
  "email",
6938
7179
  "blocked",
6939
- "password"
7180
+ "password",
7181
+ "inviteStatus",
7182
+ "inviteToken"
6940
7183
  ]
6941
7184
  });
6942
7185
  if (!user) return {
6943
7186
  ok: false,
6944
7187
  error: "User not found"
6945
7188
  };
6946
- const hasPassword = !!user.password;
6947
- if (!user.blocked && hasPassword) {
7189
+ const u = user;
7190
+ if (userAlreadyActive(u)) {
6948
7191
  return {
6949
7192
  ok: false,
6950
7193
  error: "User is already active"
6951
7194
  };
6952
7195
  }
7196
+ if (u.inviteToken && String(u.inviteToken).trim()) {
7197
+ return {
7198
+ ok: false,
7199
+ error: invalidatedInviteMessage()
7200
+ };
7201
+ }
6953
7202
  if (entityMap.vendors) {
6954
- const ownerVendor = await loadOwnerVendorForUser(dataSource, entityMap.vendors, user.id);
7203
+ const ownerVendor = await loadOwnerVendorForUser(dataSource, entityMap.vendors, u.id);
6955
7204
  if (ownerVendor && readVendorInviteToken(ownerVendor.metadata)) {
6956
7205
  return {
6957
7206
  ok: false,
@@ -6962,8 +7211,8 @@ async function validateInviteTokenForActivation(dataSource, entityMap, token) {
6962
7211
  return {
6963
7212
  ok: true,
6964
7213
  target: {
6965
- userId: user.id,
6966
- email: String(user.email)
7214
+ userId: u.id,
7215
+ email: String(u.email)
6967
7216
  }
6968
7217
  };
6969
7218
  }
@@ -7181,10 +7430,7 @@ function createInviteAcceptHandler(config) {
7181
7430
  }
7182
7431
  if (beforeActivate) await beforeActivate(email, userId);
7183
7432
  const hashedPassword = await hashPassword(password);
7184
- await dataSource.getRepository(entityMap.users).update(userId, {
7185
- password: hashedPassword,
7186
- blocked: false
7187
- });
7433
+ await completeUserInviteAccept(dataSource, entityMap.users, userId, hashedPassword);
7188
7434
  if (vendorId != null && entityMap.vendors) {
7189
7435
  await completeVendorInviteAccept(dataSource, entityMap.vendors, vendorId);
7190
7436
  }
@@ -8611,6 +8857,9 @@ function createUploadHandler(config) {
8611
8857
  "image/png",
8612
8858
  "image/gif",
8613
8859
  "image/webp",
8860
+ "video/mp4",
8861
+ "video/webm",
8862
+ "video/quicktime",
8614
8863
  "application/pdf",
8615
8864
  "text/plain",
8616
8865
  "application/zip",
@@ -8638,8 +8887,11 @@ function createUploadHandler(config) {
8638
8887
  });
8639
8888
  const defaultMax = 10 * 1024 * 1024;
8640
8889
  const maxZipBytes = 80 * 1024 * 1024;
8890
+ const maxVideoBytes = 100 * 1024 * 1024;
8641
8891
  const baseMax = maxSizeBytes ?? defaultMax;
8642
- const effectiveMax = file.type === "application/zip" || file.type === "application/x-zip-compressed" ? Math.max(baseMax, maxZipBytes) : baseMax;
8892
+ const isZip = file.type === "application/zip" || file.type === "application/x-zip-compressed";
8893
+ const isVideo = file.type === "video/mp4" || file.type === "video/webm" || file.type === "video/quicktime";
8894
+ const effectiveMax = isZip ? Math.max(baseMax, maxZipBytes) : isVideo ? Math.max(baseMax, maxVideoBytes) : baseMax;
8643
8895
  if (file.size > effectiveMax) return json({
8644
8896
  error: "File size exceeds limit"
8645
8897
  }, {
@@ -8712,12 +8964,20 @@ function createUploadHandler(config) {
8712
8964
  parentId
8713
8965
  });
8714
8966
  } catch (err) {
8967
+ const message = err instanceof Error ? err.message : String(err);
8715
8968
  console.error("[upload] File upload failed", {
8716
- error: err instanceof Error ? err.message : String(err),
8969
+ error: message,
8717
8970
  stack: err instanceof Error ? err.stack : void 0,
8718
8971
  name: err instanceof Error ? err.name : void 0,
8719
8972
  metadata: err && typeof err === "object" && "$metadata" in err ? err.$metadata : void 0
8720
8973
  });
8974
+ if (/Failed to parse body as FormData/i.test(message)) {
8975
+ return json({
8976
+ error: "File upload failed: request body could not be read as FormData. Exclude /api/upload from middleware matcher and raise proxyClientMaxBodySize."
8977
+ }, {
8978
+ status: 500
8979
+ });
8980
+ }
8721
8981
  return json({
8722
8982
  error: "File upload failed"
8723
8983
  }, {
@@ -8938,6 +9198,17 @@ function createFormSaveHandlers(config) {
8938
9198
  });
8939
9199
  const fields = Array.isArray(body.fields) ? body.fields : [];
8940
9200
  const { fields: _f, ...formRow } = body;
9201
+ const slug = typeof formRow.slug === "string" ? formRow.slug.trim() : "";
9202
+ if (slug) {
9203
+ if (await activeUniqueValueExists(formRepo(), "slug", slug)) {
9204
+ return json({
9205
+ error: "A form with this slug already exists"
9206
+ }, {
9207
+ status: 400
9208
+ });
9209
+ }
9210
+ await retireSoftDeletedUniqueValue(formRepo(), "slug", slug);
9211
+ }
8941
9212
  const form = await formRepo().save(formRepo().create(formRow));
8942
9213
  for (let i = 0; i < fields.length; i++) {
8943
9214
  const row = normalizeFieldRow(fields[i], form.id);
@@ -8961,6 +9232,14 @@ function createFormSaveHandlers(config) {
8961
9232
  status: 201
8962
9233
  });
8963
9234
  } catch (e) {
9235
+ const msg = e instanceof Error ? e.message : String(e);
9236
+ if (/duplicate key|unique constraint/i.test(msg) && /slug/i.test(msg)) {
9237
+ return json({
9238
+ error: "A form with this slug already exists"
9239
+ }, {
9240
+ status: 400
9241
+ });
9242
+ }
8964
9243
  return json({
8965
9244
  error: "Server Error"
8966
9245
  }, {
@@ -9009,6 +9288,20 @@ function createFormSaveHandlers(config) {
9009
9288
  ]) {
9010
9289
  if (body[key] !== void 0) formRow[key] = body[key];
9011
9290
  }
9291
+ if (typeof formRow.slug === "string") {
9292
+ const slug = formRow.slug.trim();
9293
+ formRow.slug = slug;
9294
+ if (slug) {
9295
+ if (await activeUniqueValueExists(formRepo(), "slug", slug, formId)) {
9296
+ return json({
9297
+ error: "A form with this slug already exists"
9298
+ }, {
9299
+ status: 400
9300
+ });
9301
+ }
9302
+ await retireSoftDeletedUniqueValue(formRepo(), "slug", slug);
9303
+ }
9304
+ }
9012
9305
  if (Object.keys(formRow).length > 0) await formRepo().update(formId, formRow);
9013
9306
  await fieldRepo().delete({
9014
9307
  formId
@@ -9037,6 +9330,14 @@ function createFormSaveHandlers(config) {
9037
9330
  status: 404
9038
9331
  });
9039
9332
  } catch (e) {
9333
+ const msg = e instanceof Error ? e.message : String(e);
9334
+ if (/duplicate key|unique constraint/i.test(msg) && /slug/i.test(msg)) {
9335
+ return json({
9336
+ error: "A form with this slug already exists"
9337
+ }, {
9338
+ status: 400
9339
+ });
9340
+ }
9040
9341
  return json({
9041
9342
  error: "Server Error"
9042
9343
  }, {
@@ -9430,7 +9731,8 @@ function createUsersApiHandlers(config) {
9430
9731
  "name",
9431
9732
  "email",
9432
9733
  "id",
9433
- "blocked"
9734
+ "blocked",
9735
+ "inviteStatus"
9434
9736
  ]);
9435
9737
  const sortField = allowedSort.has(sortFieldRaw) ? sortFieldRaw : "createdAt";
9436
9738
  const sortOrder = url.searchParams.get("sortOrder") === "desc" ? "DESC" : "ASC";
@@ -9443,6 +9745,7 @@ function createUsersApiHandlers(config) {
9443
9745
  "u.name",
9444
9746
  "u.email",
9445
9747
  "u.blocked",
9748
+ "u.inviteStatus",
9446
9749
  "u.createdAt",
9447
9750
  "u.updatedAt",
9448
9751
  "u.groupId",
@@ -9538,6 +9841,7 @@ function createUsersApiHandlers(config) {
9538
9841
  const isCustomer = !!(customerG && gid === customerG.id);
9539
9842
  const adminAccess = isCustomer ? false : body.adminAccess === false ? false : true;
9540
9843
  const blocked = body.blocked === true || body.blocked === "true" || body.blocked === 1 || body.blocked === "1";
9844
+ const inviteToken = newUserInviteToken();
9541
9845
  const newUser = existing?.deleted ? await (async () => {
9542
9846
  await userRepo().update(existing.id, {
9543
9847
  deleted: false,
@@ -9547,6 +9851,8 @@ function createUsersApiHandlers(config) {
9547
9851
  email,
9548
9852
  password: null,
9549
9853
  blocked,
9854
+ inviteStatus: "pending",
9855
+ inviteToken,
9550
9856
  groupId: gid,
9551
9857
  adminAccess,
9552
9858
  updatedAt: /* @__PURE__ */ new Date()
@@ -9563,6 +9869,8 @@ function createUsersApiHandlers(config) {
9563
9869
  email,
9564
9870
  password: null,
9565
9871
  blocked,
9872
+ inviteStatus: "pending",
9873
+ inviteToken,
9566
9874
  groupId: gid,
9567
9875
  adminAccess
9568
9876
  }));
@@ -9580,13 +9888,12 @@ function createUsersApiHandlers(config) {
9580
9888
  phone
9581
9889
  });
9582
9890
  }
9583
- const emailToken = Buffer.from(newUser.email).toString("base64");
9584
- const inviteLink = `${baseUrl}/admin/invite?token=${emailToken}`;
9585
- await trySendInviteEmail(newUser.email, inviteLink, newUser.name ?? "");
9891
+ const inviteLink = buildUserInviteLink(baseUrl, inviteToken);
9586
9892
  return json({
9587
- message: blocked ? "User created successfully (blocked until password is set)" : "User created successfully",
9893
+ message: "User created successfully. Invite is pending until they set a password.",
9588
9894
  user: newUser,
9589
- inviteLink
9895
+ inviteLink,
9896
+ emailSent: false
9590
9897
  }, {
9591
9898
  status: 201
9592
9899
  });
@@ -9618,7 +9925,9 @@ function createUsersApiHandlers(config) {
9618
9925
  "id",
9619
9926
  "name",
9620
9927
  "email",
9928
+ "phone",
9621
9929
  "blocked",
9930
+ "inviteStatus",
9622
9931
  "createdAt",
9623
9932
  "updatedAt",
9624
9933
  "groupId"
@@ -9647,6 +9956,11 @@ function createUsersApiHandlers(config) {
9647
9956
  }
9648
9957
  try {
9649
9958
  const uid = parseInt(id, 10);
9959
+ if (!Number.isFinite(uid)) return json({
9960
+ error: "Invalid id"
9961
+ }, {
9962
+ status: 400
9963
+ });
9650
9964
  const existing = await userRepo().findOne({
9651
9965
  where: {
9652
9966
  id: uid,
@@ -9659,8 +9973,59 @@ function createUsersApiHandlers(config) {
9659
9973
  status: 404
9660
9974
  });
9661
9975
  const body = await req.json();
9662
- const { password: _p, ...safe } = body;
9663
- await userRepo().update(uid, safe);
9976
+ const patch = {
9977
+ updatedAt: /* @__PURE__ */ new Date()
9978
+ };
9979
+ if (typeof body.name === "string") patch.name = body.name.trim();
9980
+ if (typeof body.email === "string") patch.email = body.email.trim().toLowerCase();
9981
+ if (body.blocked !== void 0) {
9982
+ patch.blocked = body.blocked === true || body.blocked === "true" || body.blocked === 1 || body.blocked === "1";
9983
+ }
9984
+ if (body.adminAccess !== void 0) {
9985
+ patch.adminAccess = body.adminAccess === true || body.adminAccess === "true" || body.adminAccess === 1 || body.adminAccess === "1";
9986
+ }
9987
+ if (body.groupId !== void 0) {
9988
+ if (body.groupId === null || body.groupId === "") {
9989
+ patch.groupId = null;
9990
+ } else {
9991
+ const gid = Number(body.groupId);
9992
+ if (!Number.isFinite(gid)) {
9993
+ return json({
9994
+ error: "Invalid groupId"
9995
+ }, {
9996
+ status: 400
9997
+ });
9998
+ }
9999
+ patch.groupId = gid;
10000
+ }
10001
+ }
10002
+ if (body.phone !== void 0) {
10003
+ const phone = body.phone == null ? null : String(body.phone).trim();
10004
+ patch.phone = phone || null;
10005
+ }
10006
+ if (Object.keys(patch).length <= 1) {
10007
+ return json({
10008
+ error: "No valid fields to update"
10009
+ }, {
10010
+ status: 400
10011
+ });
10012
+ }
10013
+ if (typeof patch.email === "string" && patch.email !== existing.email) {
10014
+ const emailTaken = await userRepo().findOne({
10015
+ where: {
10016
+ email: patch.email,
10017
+ deleted: false
10018
+ }
10019
+ });
10020
+ if (emailTaken && Number(emailTaken.id) !== uid) {
10021
+ return json({
10022
+ error: "Email already in use"
10023
+ }, {
10024
+ status: 400
10025
+ });
10026
+ }
10027
+ }
10028
+ await userRepo().update(uid, patch);
9664
10029
  const updated = await userRepo().findOne({
9665
10030
  where: {
9666
10031
  id: uid,
@@ -9673,7 +10038,9 @@ function createUsersApiHandlers(config) {
9673
10038
  "id",
9674
10039
  "name",
9675
10040
  "email",
10041
+ "phone",
9676
10042
  "blocked",
10043
+ "inviteStatus",
9677
10044
  "createdAt",
9678
10045
  "updatedAt",
9679
10046
  "groupId"
@@ -9684,7 +10051,8 @@ function createUsersApiHandlers(config) {
9684
10051
  }, {
9685
10052
  status: 404
9686
10053
  });
9687
- } catch {
10054
+ } catch (err) {
10055
+ console.error("[users.update]", err);
9688
10056
  return json({
9689
10057
  error: "Server Error"
9690
10058
  }, {
@@ -9740,13 +10108,21 @@ function createUsersApiHandlers(config) {
9740
10108
  });
9741
10109
  }
9742
10110
  },
9743
- async regenerateInvite(_req, id) {
9744
- const authErr = await requireAuth(new Request(_req.url));
10111
+ async regenerateInvite(req, id) {
10112
+ const authErr = await requireAuth(new Request(req.url));
9745
10113
  if (authErr) return authErr;
9746
10114
  if (requireEntityPermission) {
9747
- const pe = await requireEntityPermission(_req, "users", "update");
10115
+ const pe = await requireEntityPermission(req, "users", "update");
9748
10116
  if (pe) return pe;
9749
10117
  }
10118
+ let sendEmail = true;
10119
+ let rotate = true;
10120
+ try {
10121
+ const body = await req.json().catch(() => ({}));
10122
+ if (body.sendEmail === false) sendEmail = false;
10123
+ if (body.rotate === false) rotate = false;
10124
+ } catch {
10125
+ }
9750
10126
  try {
9751
10127
  const user = await userRepo().findOne({
9752
10128
  where: {
@@ -9756,7 +10132,11 @@ function createUsersApiHandlers(config) {
9756
10132
  select: [
9757
10133
  "id",
9758
10134
  "email",
9759
- "name"
10135
+ "name",
10136
+ "password",
10137
+ "inviteStatus",
10138
+ "inviteToken",
10139
+ "blocked"
9760
10140
  ]
9761
10141
  });
9762
10142
  if (!user) return json({
@@ -9764,34 +10144,75 @@ function createUsersApiHandlers(config) {
9764
10144
  }, {
9765
10145
  status: 404
9766
10146
  });
9767
- let inviteLink = `${baseUrl}/admin/invite?token=${Buffer.from(user.email).toString("base64")}`;
10147
+ const u = user;
10148
+ const hasPassword = !!(u.password && String(u.password).trim());
10149
+ if (u.inviteStatus === "active" && hasPassword) {
10150
+ return json({
10151
+ error: "User has already accepted the invite"
10152
+ }, {
10153
+ status: 400
10154
+ });
10155
+ }
10156
+ let inviteLink = "";
9768
10157
  if (entityMap.vendors) {
9769
10158
  const vendorRepo = dataSource.getRepository(entityMap.vendors);
9770
10159
  const vendor = await vendorRepo.findOne({
9771
10160
  where: {
9772
- userId: user.id,
10161
+ userId: u.id,
9773
10162
  deleted: false
9774
10163
  }
9775
10164
  });
9776
10165
  const v = vendor;
9777
10166
  if (v && (v.inviteStatus === "pending" || v.inviteStatus === "expired")) {
9778
- const rotatedMetadata = applyRotatingVendorInvite(v.metadata);
9779
- await vendorRepo.update(v.id, {
9780
- metadata: rotatedMetadata,
9781
- inviteStatus: "pending"
9782
- });
9783
- const token = readVendorInviteToken(rotatedMetadata);
10167
+ const nextMetadata = rotate ? applyRotatingVendorInvite(v.metadata) : {
10168
+ ...v.metadata ?? {}
10169
+ };
10170
+ if (rotate || v.inviteStatus !== "pending") {
10171
+ await vendorRepo.update(v.id, {
10172
+ metadata: nextMetadata,
10173
+ inviteStatus: "pending"
10174
+ });
10175
+ }
10176
+ const token = readVendorInviteToken(nextMetadata);
9784
10177
  if (token) {
9785
10178
  inviteLink = buildVendorInviteLink(baseUrl, token);
9786
10179
  }
10180
+ const { markVendorInvitePendingForUser } = await import('./vendor-invite-status-PWJ3DE76.js');
10181
+ await markVendorInvitePendingForUser(dataSource, entityMap.vendors, u.id);
10182
+ }
10183
+ }
10184
+ if (!inviteLink) {
10185
+ let token = u.inviteToken?.trim() || "";
10186
+ if (rotate || !token) {
10187
+ token = newUserInviteToken();
10188
+ await userRepo().update(u.id, {
10189
+ inviteToken: token,
10190
+ inviteStatus: "pending",
10191
+ password: null
10192
+ });
10193
+ } else if (u.inviteStatus !== "pending") {
10194
+ await userRepo().update(u.id, {
10195
+ inviteStatus: "pending"
10196
+ });
9787
10197
  }
9788
- const { markVendorInvitePendingForUser } = await import('./vendor-invite-status-PWJ3DE76.js');
9789
- await markVendorInvitePendingForUser(dataSource, entityMap.vendors, user.id);
10198
+ inviteLink = buildUserInviteLink(baseUrl, token);
10199
+ } else if (rotate) {
10200
+ await userRepo().update(u.id, {
10201
+ inviteStatus: "pending",
10202
+ password: null
10203
+ });
9790
10204
  }
9791
- await trySendInviteEmail(user.email, inviteLink, user.name ?? "");
10205
+ let emailSent = false;
10206
+ if (sendEmail && inviteLink) {
10207
+ await trySendInviteEmail(u.email, inviteLink, u.name ?? "");
10208
+ emailSent = true;
10209
+ }
10210
+ const rotatedNote = rotate ? " Previous invite link is no longer valid." : "";
9792
10211
  return json({
9793
- message: "New invite link generated successfully",
9794
- inviteLink
10212
+ message: emailSent ? `Invite email sent.${rotatedNote}` : sendEmail ? `Invite link ready (email may not have been sent \u2014 check email plugin).${rotatedNote}` : rotate ? "New invite link created. Previous invite link is no longer valid." : "Invite link ready.",
10213
+ emailSent,
10214
+ inviteLink,
10215
+ inviteStatus: "pending"
9795
10216
  });
9796
10217
  } catch {
9797
10218
  return json({
@@ -12732,6 +13153,39 @@ function slugify(input) {
12732
13153
  return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
12733
13154
  }
12734
13155
  __name(slugify, "slugify");
13156
+ async function userHasActiveVendor(em, entityMap, userId) {
13157
+ const vendorRepo = em.getRepository(entityMap.vendors);
13158
+ const owned = await vendorRepo.findOne({
13159
+ where: {
13160
+ userId,
13161
+ deleted: false
13162
+ }
13163
+ });
13164
+ if (owned) return true;
13165
+ if (!entityMap.vendor_users) return false;
13166
+ 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', {
13167
+ userId
13168
+ }).getOne();
13169
+ return !!link;
13170
+ }
13171
+ __name(userHasActiveVendor, "userHasActiveVendor");
13172
+ async function userHadAnyVendor(em, entityMap, userId) {
13173
+ const vendorRepo = em.getRepository(entityMap.vendors);
13174
+ const owned = await vendorRepo.findOne({
13175
+ where: {
13176
+ userId
13177
+ }
13178
+ });
13179
+ if (owned) return true;
13180
+ if (!entityMap.vendor_users) return false;
13181
+ const link = await em.getRepository(entityMap.vendor_users).findOne({
13182
+ where: {
13183
+ userId
13184
+ }
13185
+ });
13186
+ return !!link;
13187
+ }
13188
+ __name(userHadAnyVendor, "userHadAnyVendor");
12735
13189
  function vendorOnboardErrorResponse(json, err) {
12736
13190
  const msg = err instanceof Error ? err.message : String(err);
12737
13191
  console.error("[vendor-onboard]", err);
@@ -12830,14 +13284,80 @@ function createVendorOnboardHandlers(config) {
12830
13284
  __name(gateAdmin, "gateAdmin");
12831
13285
  async function resolveActiveVendorId(u) {
12832
13286
  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;
13287
+ const candidates = [];
13288
+ if (scope.type === "vendor") candidates.push(scope.vendorId);
13289
+ if (scope.type === "all" || scope.type === "vendor") {
13290
+ const preferred = u.activeVendorId ?? u.vendorIds?.[0];
13291
+ if (preferred != null && Number.isFinite(preferred)) candidates.push(Number(preferred));
13292
+ for (const id of u.vendorIds ?? []) {
13293
+ if (Number.isFinite(id)) candidates.push(Number(id));
13294
+ }
13295
+ }
13296
+ if (entityMap.vendors && candidates.length > 0) {
13297
+ const unique = [
13298
+ ...new Set(candidates.filter((id) => id > 0))
13299
+ ];
13300
+ for (const id of unique) {
13301
+ try {
13302
+ const row = await dataSource.getRepository(entityMap.vendors).findOne({
13303
+ where: {
13304
+ id,
13305
+ deleted: false
13306
+ }
13307
+ });
13308
+ if (row) return id;
13309
+ } catch {
13310
+ }
13311
+ }
13312
+ }
13313
+ const uid = u.id != null ? Number(u.id) : NaN;
13314
+ if (Number.isFinite(uid) && entityMap.vendors) {
13315
+ try {
13316
+ const owned = await dataSource.getRepository(entityMap.vendors).findOne({
13317
+ where: {
13318
+ userId: uid,
13319
+ deleted: false
13320
+ },
13321
+ order: {
13322
+ id: "ASC"
13323
+ }
13324
+ });
13325
+ const vid = owned ? Number(owned.id) : NaN;
13326
+ if (Number.isFinite(vid) && vid > 0) return vid;
13327
+ } catch {
13328
+ }
12837
13329
  }
12838
13330
  return null;
12839
13331
  }
12840
13332
  __name(resolveActiveVendorId, "resolveActiveVendorId");
13333
+ async function gateVendorPortal() {
13334
+ const u = await getSessionUser();
13335
+ if (!u?.email) return json({
13336
+ error: "Unauthorized"
13337
+ }, {
13338
+ status: 401
13339
+ });
13340
+ if (!isVendorPortalUser(u) && !isPlatformAdministrator(u)) {
13341
+ return json({
13342
+ error: "Forbidden"
13343
+ }, {
13344
+ status: 403
13345
+ });
13346
+ }
13347
+ const vendorId = await resolveActiveVendorId(u);
13348
+ if (vendorId == null) {
13349
+ return json({
13350
+ error: "No vendor is linked to your account."
13351
+ }, {
13352
+ status: 400
13353
+ });
13354
+ }
13355
+ return {
13356
+ user: u,
13357
+ vendorId
13358
+ };
13359
+ }
13360
+ __name(gateVendorPortal, "gateVendorPortal");
12841
13361
  async function gateVendorTeam() {
12842
13362
  const u = await getSessionUser();
12843
13363
  if (!u?.email) return json({
@@ -12891,6 +13411,40 @@ function createVendorOnboardHandlers(config) {
12891
13411
  }
12892
13412
  __name(trySendVendorOnboardEmails, "trySendVendorOnboardEmails");
12893
13413
  return {
13414
+ /** GET /api/admin/vendor/profile — current user's store (server-resolved vendor id). */
13415
+ async getProfile() {
13416
+ const gated = await gateVendorPortal();
13417
+ if (gated instanceof Response) return gated;
13418
+ const { vendorId } = gated;
13419
+ if (!entityMap.vendors) {
13420
+ return json({
13421
+ error: "Vendors not configured"
13422
+ }, {
13423
+ status: 500
13424
+ });
13425
+ }
13426
+ try {
13427
+ const row = await dataSource.getRepository(entityMap.vendors).findOne({
13428
+ where: {
13429
+ id: vendorId,
13430
+ deleted: false
13431
+ }
13432
+ });
13433
+ if (!row) return json({
13434
+ error: "Vendor not found"
13435
+ }, {
13436
+ status: 404
13437
+ });
13438
+ return json(row);
13439
+ } catch (e) {
13440
+ console.error("[vendor.profile]", e);
13441
+ return json({
13442
+ error: "Failed to load vendor profile"
13443
+ }, {
13444
+ status: 500
13445
+ });
13446
+ }
13447
+ },
12894
13448
  /** POST /api/admin/vendors/onboard — create vendor + owner user + vendor_users + invite */
12895
13449
  async onboard(req) {
12896
13450
  const err = await gateAdmin();
@@ -12959,6 +13513,7 @@ function createVendorOnboardHandlers(config) {
12959
13513
  }
12960
13514
  });
12961
13515
  if (dupVendor) throw new Error("VENDOR_SLUG_EXISTS");
13516
+ await retireSoftDeletedUniqueValue(vendorRepo, "slug", slug);
12962
13517
  let ownerGroup = await groupRepo.findOne({
12963
13518
  where: {
12964
13519
  name: VENDOR_OWNER_GROUP_NAME,
@@ -12987,8 +13542,15 @@ function createVendorOnboardHandlers(config) {
12987
13542
  email: userEmail
12988
13543
  }
12989
13544
  });
12990
- if (existingUser && !existingUser.deleted) throw new Error("USER_EMAIL_EXISTS");
12991
- const newUser = existingUser?.deleted ? await (async () => {
13545
+ if (existingUser && !existingUser.deleted) {
13546
+ if (await userHasActiveVendor(em, entityMap, existingUser.id)) {
13547
+ throw new Error("USER_EMAIL_EXISTS");
13548
+ }
13549
+ if (!await userHadAnyVendor(em, entityMap, existingUser.id)) {
13550
+ throw new Error("USER_EMAIL_EXISTS");
13551
+ }
13552
+ }
13553
+ const newUser = existingUser ? await (async () => {
12992
13554
  await userRepo.update(existingUser.id, {
12993
13555
  deleted: false,
12994
13556
  deletedAt: null,
@@ -14355,6 +14917,13 @@ var User = class {
14355
14917
  phoneVerifiedAt;
14356
14918
  emailVerifiedAt;
14357
14919
  password;
14920
+ /**
14921
+ * `pending` = invite outstanding (cannot sign in until password is set).
14922
+ * `active` = invite accepted / account usable (subject to `blocked`).
14923
+ */
14924
+ inviteStatus;
14925
+ /** Rotating invite token; null when active or no invite outstanding. */
14926
+ inviteToken;
14358
14927
  blocked;
14359
14928
  adminAccess;
14360
14929
  groupId;
@@ -14407,6 +14976,24 @@ _ts_decorate4([
14407
14976
  }),
14408
14977
  _ts_metadata4("design:type", Object)
14409
14978
  ], User.prototype, "password", void 0);
14979
+ _ts_decorate4([
14980
+ Column({
14981
+ type: "enum",
14982
+ enum: [
14983
+ "pending",
14984
+ "active"
14985
+ ],
14986
+ enumName: "users_invitestatus_enum",
14987
+ default: "active"
14988
+ }),
14989
+ _ts_metadata4("design:type", typeof UserInviteStatus === "undefined" ? Object : UserInviteStatus)
14990
+ ], User.prototype, "inviteStatus", void 0);
14991
+ _ts_decorate4([
14992
+ Column("varchar", {
14993
+ nullable: true
14994
+ }),
14995
+ _ts_metadata4("design:type", Object)
14996
+ ], User.prototype, "inviteToken", void 0);
14410
14997
  _ts_decorate4([
14411
14998
  Column("boolean", {
14412
14999
  default: false
@@ -16071,10 +16658,12 @@ var Customer = class {
16071
16658
  __name(this, "Customer");
16072
16659
  }
16073
16660
  id;
16661
+ /** Set only when the customer can log in (linked `users` row). Admin/guest customers stay null. */
16074
16662
  userId;
16075
16663
  user;
16076
16664
  name;
16077
16665
  email;
16666
+ /** Optional; multiple customers may have null (PostgreSQL UNIQUE allows multiple NULLs). */
16078
16667
  phone;
16079
16668
  createdAt;
16080
16669
  updatedAt;
@@ -16089,8 +16678,10 @@ _ts_decorate18([
16089
16678
  _ts_metadata18("design:type", Number)
16090
16679
  ], Customer.prototype, "id", void 0);
16091
16680
  _ts_decorate18([
16092
- Column("int"),
16093
- _ts_metadata18("design:type", Number)
16681
+ Column("int", {
16682
+ nullable: true
16683
+ }),
16684
+ _ts_metadata18("design:type", Object)
16094
16685
  ], Customer.prototype, "userId", void 0);
16095
16686
  _ts_decorate18([
16096
16687
  ManyToOne(() => User, {
@@ -16113,9 +16704,10 @@ _ts_decorate18([
16113
16704
  ], Customer.prototype, "email", void 0);
16114
16705
  _ts_decorate18([
16115
16706
  Column("varchar", {
16116
- unique: true
16707
+ unique: true,
16708
+ nullable: true
16117
16709
  }),
16118
- _ts_metadata18("design:type", String)
16710
+ _ts_metadata18("design:type", Object)
16119
16711
  ], Customer.prototype, "phone", void 0);
16120
16712
  _ts_decorate18([
16121
16713
  Column({
@@ -19135,6 +19727,8 @@ var OrderItem = class {
19135
19727
  id;
19136
19728
  orderId;
19137
19729
  productId;
19730
+ /** Selected product variant at order time (optional). */
19731
+ variantId;
19138
19732
  discountIds;
19139
19733
  vendorId;
19140
19734
  vendor;
@@ -19170,6 +19764,12 @@ _ts_decorate39([
19170
19764
  Column("int"),
19171
19765
  _ts_metadata39("design:type", Number)
19172
19766
  ], OrderItem.prototype, "productId", void 0);
19767
+ _ts_decorate39([
19768
+ Column("int", {
19769
+ nullable: true
19770
+ }),
19771
+ _ts_metadata39("design:type", Object)
19772
+ ], OrderItem.prototype, "variantId", void 0);
19173
19773
  _ts_decorate39([
19174
19774
  Column({
19175
19775
  type: "int",
@@ -19979,6 +20579,7 @@ var Type = /* @__PURE__ */ (function(Type2) {
19979
20579
  Type2["CATEGORY"] = "category";
19980
20580
  Type2["CART"] = "cart";
19981
20581
  Type2["PAYMENT_METHOD"] = "paymentMethod";
20582
+ Type2["DEVICE_TYPE"] = "deviceType";
19982
20583
  Type2["ORDER"] = "order";
19983
20584
  Type2["QUANTITY"] = "quantity";
19984
20585
  Type2["TIME"] = "time";
@@ -22001,6 +22602,7 @@ var Combo = class {
22001
22602
  vendorId;
22002
22603
  vendor;
22003
22604
  name;
22605
+ slug;
22004
22606
  desc;
22005
22607
  eventId;
22006
22608
  event;
@@ -22044,6 +22646,12 @@ _ts_decorate63([
22044
22646
  }),
22045
22647
  _ts_metadata63("design:type", String)
22046
22648
  ], Combo.prototype, "name", void 0);
22649
+ _ts_decorate63([
22650
+ Column({
22651
+ type: "varchar"
22652
+ }),
22653
+ _ts_metadata63("design:type", String)
22654
+ ], Combo.prototype, "slug", void 0);
22047
22655
  _ts_decorate63([
22048
22656
  Column({
22049
22657
  type: "text",
@@ -22181,7 +22789,11 @@ _ts_decorate63([
22181
22789
  _ts_metadata63("design:type", Number)
22182
22790
  ], Combo.prototype, "deletedBy", void 0);
22183
22791
  Combo = _ts_decorate63([
22184
- Entity("combos")
22792
+ Entity("combos"),
22793
+ Unique("UQ_combos_vendor_slug", [
22794
+ "vendorId",
22795
+ "slug"
22796
+ ])
22185
22797
  ], Combo);
22186
22798
  function _ts_decorate64(decorators, target, key, desc) {
22187
22799
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -26107,8 +26719,11 @@ function createMessageTemplateRowLoader(dataSource, entityMap) {
26107
26719
  };
26108
26720
  }
26109
26721
  __name(createMessageTemplateRowLoader, "createMessageTemplateRowLoader");
26110
- function registerMessagingQueueProcessors(cms) {
26111
- registerEmailQueueProcessor(cms);
26722
+ function registerMessagingQueueProcessors(cms, entityMap) {
26723
+ registerEmailQueueProcessor(cms, {
26724
+ getDataSource: /* @__PURE__ */ __name(async () => cms.dataSource, "getDataSource"),
26725
+ entityMap
26726
+ });
26112
26727
  registerWhatsAppQueueProcessor(cms);
26113
26728
  }
26114
26729
  __name(registerMessagingQueueProcessors, "registerMessagingQueueProcessors");
@@ -26160,7 +26775,7 @@ async function ensureMessagingPluginsOnCms(base, options) {
26160
26775
  }
26161
26776
  }
26162
26777
  const merged = Object.keys(overlay).length > 0 ? overlayCmsPlugins(cms, overlay) : cms;
26163
- registerMessagingQueueProcessors(merged);
26778
+ registerMessagingQueueProcessors(merged, entityMap);
26164
26779
  return merged;
26165
26780
  }
26166
26781
  __name(ensureMessagingPluginsOnCms, "ensureMessagingPluginsOnCms");
@@ -26209,7 +26824,7 @@ async function createCmsAppWithMessaging(options) {
26209
26824
  logger,
26210
26825
  plugins: mergedPlugins
26211
26826
  });
26212
- registerMessagingQueueProcessors(cms);
26827
+ registerMessagingQueueProcessors(cms, entityMap);
26213
26828
  return cms;
26214
26829
  }
26215
26830
  __name(createCmsAppWithMessaging, "createCmsAppWithMessaging");
@@ -26406,7 +27021,7 @@ function createCmsApiHandler(config) {
26406
27021
  companyDetails
26407
27022
  };
26408
27023
  if (queue) {
26409
- const { queueEmail: queueEmail2 } = await import('./email-queue-CGZBVVAS.js');
27024
+ const { queueEmail: queueEmail2 } = await import('./email-queue-OI2HSCGE.js');
26410
27025
  await queueEmail2(cms, {
26411
27026
  to: opts.to,
26412
27027
  templateName: "passwordReset",
@@ -26699,6 +27314,14 @@ function createCmsApiHandler(config) {
26699
27314
  });
26700
27315
  return vendorHandlers.switchVendor(req);
26701
27316
  }
27317
+ if (path2[0] === "admin" && path2[1] === "vendor" && path2[2] === "profile" && path2.length === 3 && m === "GET") {
27318
+ if (!vendorHandlers) return config.json({
27319
+ error: "Not found"
27320
+ }, {
27321
+ status: 404
27322
+ });
27323
+ return vendorHandlers.getProfile();
27324
+ }
26702
27325
  if (path2[0] === "admin" && path2[1] === "vendor" && path2[2] === "roles" && vendorRolesHandlers) {
26703
27326
  if (path2.length === 3 && m === "GET") return vendorRolesHandlers.list();
26704
27327
  if (path2.length === 3 && m === "POST") return vendorRolesHandlers.create(req);
@@ -27652,14 +28275,27 @@ function createCmsApiHandler(config) {
27652
28275
  const pe = await requireEntityPermissionEffective(req, "orders", "read");
27653
28276
  if (pe) return pe;
27654
28277
  const cms = await getCms();
27655
- const { streamOrderInvoicePdf: streamOrderInvoicePdf2 } = await import('./erp-order-invoice-3YISSOWW.js');
28278
+ const { streamOrderInvoicePdf: streamOrderInvoicePdf2 } = await import('./erp-order-invoice-3GXDE443.js');
27656
28279
  const oid = Number(path2[1]);
27657
28280
  if (!Number.isFinite(oid)) return config.json({
27658
28281
  error: "Invalid id"
27659
28282
  }, {
27660
28283
  status: 400
27661
28284
  });
27662
- return streamOrderInvoicePdf2(cms, dataSource, entityMap, oid, {});
28285
+ try {
28286
+ return await streamOrderInvoicePdf2(cms, dataSource, entityMap, oid, {});
28287
+ } catch (err) {
28288
+ const msg = err instanceof Error ? err.message : String(err);
28289
+ console.error("[invoice] route failed", {
28290
+ orderId: oid,
28291
+ error: msg
28292
+ });
28293
+ return config.json({
28294
+ error: `Invoice failed: ${msg}`
28295
+ }, {
28296
+ status: 500
28297
+ });
28298
+ }
27663
28299
  }
27664
28300
  if (path2[0] === "orders" && path2.length === 3 && path2[2] === "repost-erp" && getCms) {
27665
28301
  const a = await config.requireAuth(req);
@@ -28006,7 +28642,7 @@ function createCmsApiHandler(config) {
28006
28642
  status: 400
28007
28643
  });
28008
28644
  }
28009
- const { resendOrderNotification } = await import('./order-notification-dispatcher-XWIJYDGA.js');
28645
+ const { resendOrderNotification } = await import('./order-notification-dispatcher-ZJZB2HUN.js');
28010
28646
  const result = await resendOrderNotification(triggerKey, orderId, {
28011
28647
  dataSource,
28012
28648
  entityMap,
@@ -28078,6 +28714,30 @@ function createCmsApiHandler(config) {
28078
28714
  paymentId = Number(payment.id);
28079
28715
  }
28080
28716
  }
28717
+ const orderWithItems = await orderRepo.findOne({
28718
+ where: {
28719
+ id: orderId,
28720
+ deleted: false
28721
+ },
28722
+ relations: [
28723
+ "items"
28724
+ ]
28725
+ });
28726
+ if (orderWithItems) {
28727
+ const { reconcileOrderInventoryBetweenSnapshots: reconcileOrderInventoryBetweenSnapshots2, orderInventorySnapshotFromRow: orderInventorySnapshotFromRow2 } = await import('./order-inventory-76V4XIGW.js');
28728
+ const prevSnap = orderInventorySnapshotFromRow2(orderWithItems);
28729
+ const invErr = await reconcileOrderInventoryBetweenSnapshots2(dataSource, entityMap, prevSnap, {
28730
+ status: "cancelled",
28731
+ lines: prevSnap.lines
28732
+ });
28733
+ if (invErr) {
28734
+ return config.json({
28735
+ error: invErr
28736
+ }, {
28737
+ status: 400
28738
+ });
28739
+ }
28740
+ }
28081
28741
  await orderRepo.update(orderId, {
28082
28742
  status: "cancelled"
28083
28743
  });
@@ -29121,6 +29781,14 @@ function createStorefrontApiHandler(config) {
29121
29781
  entityMap
29122
29782
  });
29123
29783
  }).catch((err) => console.error("[notification-triggers] import failed", err));
29784
+ if (!getCms) return;
29785
+ void import('./send-order-placed-emails-WPI37KRZ.js').then(({ sendOrderPlacedEmailsAfterConfirmation }) => {
29786
+ return sendOrderPlacedEmailsAfterConfirmation(orderId, {
29787
+ getDataSource: /* @__PURE__ */ __name(async () => dataSource, "getDataSource"),
29788
+ entityMap,
29789
+ getCms
29790
+ });
29791
+ }).catch((err) => console.error("[order-emails] sendOrderPlacedEmailsAfterConfirmation failed", err));
29124
29792
  }
29125
29793
  __name(fireOrderPlacedNotification, "fireOrderPlacedNotification");
29126
29794
  const otpAllowPhoneLogin = config.otpAllowPhoneLogin !== false;
@@ -29168,13 +29836,36 @@ function createStorefrontApiHandler(config) {
29168
29836
  status: 400
29169
29837
  };
29170
29838
  }
29839
+ const list = [
29840
+ ...vendorIds
29841
+ ];
29171
29842
  return {
29172
- vendorId: [
29173
- ...vendorIds
29174
- ][0]
29843
+ vendorId: list[0],
29844
+ vendorIds: list
29175
29845
  };
29176
29846
  }
29177
29847
  __name(resolveSingleVendorIdFromCart, "resolveSingleVendorIdFromCart");
29848
+ async function linkOrderContactToVendors(contactId, vendorIds) {
29849
+ try {
29850
+ const contact = await contactRepo().findOne({
29851
+ where: {
29852
+ id: contactId,
29853
+ deleted: false
29854
+ }
29855
+ });
29856
+ if (!contact) return;
29857
+ const email = String(contact.email ?? "").trim().toLowerCase();
29858
+ if (!email) return;
29859
+ await ensureVendorCustomersForOrder(dataSource, entityMap, vendorIds, contactId, {
29860
+ name: String(contact.name ?? "").trim() || email.split("@")[0] || "Customer",
29861
+ email,
29862
+ phone: contact.phone ?? null
29863
+ });
29864
+ } catch (err) {
29865
+ console.error("[storefront] vendor_customers link failed", err);
29866
+ }
29867
+ }
29868
+ __name(linkOrderContactToVendors, "linkOrderContactToVendors");
29178
29869
  function roundMoney3(n) {
29179
29870
  return Math.round(n * 100) / 100;
29180
29871
  }
@@ -30866,9 +31557,12 @@ function createStorefrontApiHandler(config) {
30866
31557
  }, {
30867
31558
  status: 400
30868
31559
  });
31560
+ const { retireSoftDeletedUniqueValue: retireSoftDeletedUniqueValue2 } = await import('./retire-soft-deleted-unique-NXQIH4BC.js');
31561
+ await retireSoftDeletedUniqueValue2(userRepo(), "email", email);
30869
31562
  const existing = await userRepo().findOne({
30870
31563
  where: {
30871
- email
31564
+ email,
31565
+ deleted: false
30872
31566
  }
30873
31567
  });
30874
31568
  if (existing) return json({
@@ -31496,6 +32190,7 @@ function createStorefrontApiHandler(config) {
31496
32190
  taxCode: line.taxCode
31497
32191
  }));
31498
32192
  }
32193
+ await linkOrderContactToVendors(contactId, vendorRes.vendorIds);
31499
32194
  fireOrderPlacedNotification(oid);
31500
32195
  return json({
31501
32196
  orderId: oid,
@@ -31568,6 +32263,7 @@ function createStorefrontApiHandler(config) {
31568
32263
  taxCode: line.taxCode
31569
32264
  }));
31570
32265
  }
32266
+ await linkOrderContactToVendors(contactId, vendorResChk.vendorIds);
31571
32267
  await cartItemRepo().delete({
31572
32268
  cartId: cart.id
31573
32269
  });
@@ -31747,6 +32443,7 @@ function createStorefrontApiHandler(config) {
31747
32443
  unitPrice: line.unitPrice,
31748
32444
  tax: line.tax,
31749
32445
  total: line.total,
32446
+ metadata: line.metadata ?? null,
31750
32447
  product: p ? {
31751
32448
  name: p.name,
31752
32449
  slug: p.slug,
@@ -31814,4 +32511,4 @@ function createStorefrontApiHandler(config) {
31814
32511
  }
31815
32512
  __name(createStorefrontApiHandler, "createStorefrontApiHandler");
31816
32513
 
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 };
32514
+ 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, buildUserInviteLink, buildVendorInviteLink, calculateOrderRefundPreview, calculateRefundFromPolicy, checkAndIncrementDiscountUsage, checkEventsEnabled, checkMultiVendorEnabled, completeUserInviteAccept, consumeAppliedDiscountUsages, contactIsVendorCustomer, countDiscountOrdersForContact, 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, decrementDiscountUsage, describeEventTierPolicy, ensureCustomerForUser, ensureCustomerRecord, ensureMessagingPluginsOnCms, ensureScheduleQueueWorker, ensureVendorCustomerForOrderContact, ensureVendorCustomersForOrder, findActiveRefundPolicyForVendor, findUserByInviteToken, findVendorByInviteToken, formatTierRange, generateNumericOtp, getPublicSettingsGroup, getRequireEventApproval, getRequireProductApproval, getRssArticleSummaryFromItem, getVendorCatalogCreateFlags, hashOtpCode, hydrateVendorSessionUser, invalidateEventsCache, invalidateMultiVendorCache, invalidateRequireEventApprovalCache, invalidateRequireProductApprovalCache, invalidateVendorCatalogCreateFlagsCache, isCustomerTypeContact, isMaxPerUserUsageReached, isMaxTotalUsageReached, isSyntheticCustomerPhone, isZipMedia, linkUnclaimedContactToUser, llmAgentToChatAgentOptions, loadSettingsGroupFromDb, loadUserVendorContext, mergeGuardrailsIntoSystemPrompt, messagingPlugins, metaFetchUserManagedPages, metaPostPageFeed, metaPostPagePhoto, metaResolvePageAccessToken, newUserInviteToken, normalizeCustomerPhone, normalizePhoneE164, normalizeRefundTiers, overlayCmsPlugins, parseBlogGeneratorAgentContent, parseBlogGeneratorModelOutput, parseBlogMetadataEnrichmentJson, parseLlmAgentValidationRules, pgBossScheduleNameForId, queueErpCreateContactIfEnabled, queueJobScheduleNow, queuePlugin, queueSms, recordDiscountUsage, registerJobRunnerWorker, registerMessagingQueueProcessors, registerSmsQueueProcessor, relativePathFromMediaParentId, resolveBlogCategoryIdByName, resolveSettingsEncryptionKey, resolveVendorIdForContactCheck, sanitizeMediaFolderPath, sanitizeStorageSegment, sendVendorOnboardEmails, simpleDecrypt, simpleEncrypt, syncJobScheduleToPgBoss, validateRefundTiers, validateScheduleInput, validateUserMessageAgainstAgentRules, validateUserMessageAgainstStructuredRules, verifyAndConsumeOtpChallenge, verifyOtpCodeHash, whatsappPlugin, wrapGetCmsWithMessaging };