@infuro/cms-core 1.0.61 → 1.0.62

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 (50) hide show
  1. package/dist/admin.cjs +3621 -2639
  2. package/dist/admin.js +2412 -1430
  3. package/dist/api.cjs +41 -37
  4. package/dist/api.js +9 -5
  5. package/dist/{chunk-URN7GS23.js → chunk-6LV36PCL.js} +199 -1059
  6. package/dist/{chunk-O2642WVS.js → chunk-6XAVZE4I.js} +17 -3
  7. package/dist/{chunk-BXYZDMTZ.cjs → chunk-AXMP2U43.cjs} +3 -94
  8. package/dist/{chunk-T3HPSF7K.js → chunk-AYWDQFJZ.js} +3 -0
  9. package/dist/chunk-CTIEAHQL.cjs +96 -0
  10. package/dist/chunk-GWDI752Q.js +83 -0
  11. package/dist/chunk-GZGFNVTK.js +886 -0
  12. package/dist/chunk-H3IVLIPR.cjs +86 -0
  13. package/dist/{chunk-2KUCQVAQ.js → chunk-HMLVPIJR.js} +2 -91
  14. package/dist/{chunk-V25RDUOU.cjs → chunk-LRD4CGOG.cjs} +38 -4
  15. package/dist/{chunk-73WVHINE.cjs → chunk-LVQSG25Y.cjs} +18 -4
  16. package/dist/chunk-NMYGQWHV.js +93 -0
  17. package/dist/chunk-OVTOP42W.js +199 -0
  18. package/dist/{chunk-CNAHVRRU.cjs → chunk-QA5C477U.cjs} +676 -312
  19. package/dist/{chunk-UHT4INVW.js → chunk-R6Q67HAS.js} +625 -252
  20. package/dist/{chunk-YSYX4GUD.cjs → chunk-TWTISTZB.cjs} +210 -1080
  21. package/dist/{chunk-77I3KS7V.cjs → chunk-UEE4PTTB.cjs} +3 -0
  22. package/dist/chunk-WFMVD567.cjs +209 -0
  23. package/dist/{chunk-L3525VXI.js → chunk-YW7EF7K7.js} +36 -2
  24. package/dist/chunk-ZPAPCGCR.cjs +899 -0
  25. package/dist/{email-queue-OI2HSCGE.js → email-queue-65PQAG4C.js} +3 -2
  26. package/dist/{email-queue-6NJY6HNM.cjs → email-queue-J7DXYNCE.cjs} +7 -6
  27. package/dist/email-service-F7HA4X63.js +3 -0
  28. package/dist/email-service-RDOTY3CO.cjs +28 -0
  29. package/dist/{emit-order-notification-trigger-TNEZOYEE.cjs → emit-order-notification-trigger-K7URAZHJ.cjs} +4 -4
  30. package/dist/{emit-order-notification-trigger-CZU3V2WE.js → emit-order-notification-trigger-MD7EBODC.js} +2 -2
  31. package/dist/{erp-order-invoice-EYYNR526.cjs → erp-order-invoice-C5GZCOBM.cjs} +6 -5
  32. package/dist/{erp-order-invoice-3GXDE443.js → erp-order-invoice-MYFCXOCY.js} +2 -1
  33. package/dist/index.cjs +363 -314
  34. package/dist/index.d.cts +53 -19
  35. package/dist/index.d.ts +53 -19
  36. package/dist/index.js +75 -26
  37. package/dist/migrations/1783000000000-AddOrderPendingProcessingCompletedTriggers.ts +56 -0
  38. package/dist/migrations/1783100000000-AddSmsToOrderNotificationBindingsChannelEnum.ts +17 -0
  39. package/dist/migrations/1783200000000-AddOrderAssigneeColumns.ts +30 -0
  40. package/dist/order-assignees-handlers-LKMAGLG6.js +150 -0
  41. package/dist/order-assignees-handlers-NPKD64P6.cjs +152 -0
  42. package/dist/order-completion-otp-handlers-3NTOGEA5.js +361 -0
  43. package/dist/order-completion-otp-handlers-4EOLYLAP.cjs +363 -0
  44. package/dist/order-notification-dispatcher-IYMOS4VY.js +10 -0
  45. package/dist/order-notification-dispatcher-X2XM5VUY.cjs +35 -0
  46. package/dist/sms-notification-templates-handlers-SJLO7JGZ.js +47 -0
  47. package/dist/sms-notification-templates-handlers-YSB3L2PK.cjs +49 -0
  48. package/package.json +1 -1
  49. package/dist/order-notification-dispatcher-KJVGLK5E.cjs +0 -24
  50. package/dist/order-notification-dispatcher-ZSFKEYMP.js +0 -7
@@ -0,0 +1,150 @@
1
+ import { __name } from './chunk-SHUYVCID.js';
2
+
3
+ // src/api/order-assignees-handlers.ts
4
+ function json(data, init) {
5
+ return new Response(JSON.stringify(data), {
6
+ status: init?.status ?? 200,
7
+ headers: {
8
+ "Content-Type": "application/json",
9
+ ...init?.headers || {}
10
+ }
11
+ });
12
+ }
13
+ __name(json, "json");
14
+ function createOrderAssigneesHandlers(dataSource, entityMap, getHydratedSessionUser) {
15
+ return {
16
+ /** GET /api/orders/:id/eligible-assignees */
17
+ async listEligibleAssignees(req, orderId) {
18
+ if (!entityMap.orders || !entityMap.users) {
19
+ return json({
20
+ error: "Order or User entity not configured"
21
+ }, {
22
+ status: 500
23
+ });
24
+ }
25
+ const orderRepo = dataSource.getRepository(entityMap.orders);
26
+ const order = await orderRepo.findOne({
27
+ where: {
28
+ id: orderId,
29
+ deleted: false
30
+ },
31
+ select: [
32
+ "id",
33
+ "vendorId",
34
+ "assignedUserId"
35
+ ]
36
+ });
37
+ if (!order) {
38
+ return json({
39
+ error: "Order not found"
40
+ }, {
41
+ status: 404
42
+ });
43
+ }
44
+ const orderVendorId = order.vendorId;
45
+ const session = await getHydratedSessionUser?.() ?? null;
46
+ const userVendorIds = session?.vendorIds ?? [];
47
+ const isVendorSession = Boolean(session?.isVendorPortal || !session?.isRBACAdmin && userVendorIds.length > 0);
48
+ Boolean(session?.isRBACAdmin && !session?.isVendorPortal);
49
+ if (isVendorSession && orderVendorId && !userVendorIds.includes(orderVendorId)) {
50
+ return json({
51
+ error: "Forbidden: Access denied to vendor order"
52
+ }, {
53
+ status: 403
54
+ });
55
+ }
56
+ const assigneesMap = /* @__PURE__ */ new Map();
57
+ if (isVendorSession) {
58
+ const targetVendorId = orderVendorId ?? session?.activeVendorId ?? userVendorIds[0];
59
+ if (entityMap.vendor_users && targetVendorId) {
60
+ const vuRepo = dataSource.getRepository(entityMap.vendor_users);
61
+ const vendorUsers = await vuRepo.find({
62
+ where: {
63
+ vendorId: targetVendorId
64
+ },
65
+ relations: [
66
+ "user",
67
+ "vendorRole"
68
+ ]
69
+ });
70
+ for (const vu of vendorUsers) {
71
+ const u = vu.user;
72
+ const r = vu.vendorRole;
73
+ if (u && u.id && !u.deleted && !u.blocked && u.inviteStatus === "active") {
74
+ const roleName = r?.name || (r?.isOwnerRole ? "Owner" : "Vendor Staff");
75
+ assigneesMap.set(u.id, {
76
+ id: u.id,
77
+ name: u.name || u.email?.split("@")[0] || `User #${u.id}`,
78
+ email: u.email || "",
79
+ role: roleName
80
+ });
81
+ }
82
+ }
83
+ }
84
+ } else {
85
+ if (entityMap.vendor_users) {
86
+ const vuRepo = dataSource.getRepository(entityMap.vendor_users);
87
+ const vendorUsers = await vuRepo.find({
88
+ relations: [
89
+ "user",
90
+ "vendorRole"
91
+ ]
92
+ });
93
+ for (const vu of vendorUsers) {
94
+ const u = vu.user;
95
+ const r = vu.vendorRole;
96
+ if (u && u.id && !u.deleted && !u.blocked && u.inviteStatus === "active") {
97
+ const roleName = r?.name || (r?.isOwnerRole ? "Owner" : "Vendor Staff");
98
+ assigneesMap.set(u.id, {
99
+ id: u.id,
100
+ name: u.name || u.email?.split("@")[0] || `User #${u.id}`,
101
+ email: u.email || "",
102
+ role: roleName
103
+ });
104
+ }
105
+ }
106
+ }
107
+ const userRepo = dataSource.getRepository(entityMap.users);
108
+ const users = await userRepo.find({
109
+ where: {
110
+ deleted: false,
111
+ blocked: false,
112
+ inviteStatus: "active"
113
+ },
114
+ relations: [
115
+ "group"
116
+ ],
117
+ order: {
118
+ name: "ASC"
119
+ }
120
+ });
121
+ for (const u of users) {
122
+ const groupName = u.group?.name ? String(u.group.name).trim() : "";
123
+ const isCustomer = groupName.toLowerCase() === "customer";
124
+ if (isCustomer) continue;
125
+ if (!assigneesMap.has(u.id)) {
126
+ let roleLabel = "Staff";
127
+ if (groupName) {
128
+ roleLabel = groupName;
129
+ } else if (u.adminAccess) {
130
+ roleLabel = "Administrator";
131
+ }
132
+ assigneesMap.set(u.id, {
133
+ id: u.id,
134
+ name: u.name || u.email?.split("@")[0] || `User #${u.id}`,
135
+ email: u.email || "",
136
+ role: roleLabel
137
+ });
138
+ }
139
+ }
140
+ }
141
+ return json({
142
+ assignees: Array.from(assigneesMap.values()),
143
+ currentAssignedUserId: order.assignedUserId ?? null
144
+ });
145
+ }
146
+ };
147
+ }
148
+ __name(createOrderAssigneesHandlers, "createOrderAssigneesHandlers");
149
+
150
+ export { createOrderAssigneesHandlers };
@@ -0,0 +1,152 @@
1
+ 'use strict';
2
+
3
+ var chunkUSNT2KNT_cjs = require('./chunk-USNT2KNT.cjs');
4
+
5
+ // src/api/order-assignees-handlers.ts
6
+ function json(data, init) {
7
+ return new Response(JSON.stringify(data), {
8
+ status: init?.status ?? 200,
9
+ headers: {
10
+ "Content-Type": "application/json",
11
+ ...init?.headers || {}
12
+ }
13
+ });
14
+ }
15
+ chunkUSNT2KNT_cjs.__name(json, "json");
16
+ function createOrderAssigneesHandlers(dataSource, entityMap, getHydratedSessionUser) {
17
+ return {
18
+ /** GET /api/orders/:id/eligible-assignees */
19
+ async listEligibleAssignees(req, orderId) {
20
+ if (!entityMap.orders || !entityMap.users) {
21
+ return json({
22
+ error: "Order or User entity not configured"
23
+ }, {
24
+ status: 500
25
+ });
26
+ }
27
+ const orderRepo = dataSource.getRepository(entityMap.orders);
28
+ const order = await orderRepo.findOne({
29
+ where: {
30
+ id: orderId,
31
+ deleted: false
32
+ },
33
+ select: [
34
+ "id",
35
+ "vendorId",
36
+ "assignedUserId"
37
+ ]
38
+ });
39
+ if (!order) {
40
+ return json({
41
+ error: "Order not found"
42
+ }, {
43
+ status: 404
44
+ });
45
+ }
46
+ const orderVendorId = order.vendorId;
47
+ const session = await getHydratedSessionUser?.() ?? null;
48
+ const userVendorIds = session?.vendorIds ?? [];
49
+ const isVendorSession = Boolean(session?.isVendorPortal || !session?.isRBACAdmin && userVendorIds.length > 0);
50
+ Boolean(session?.isRBACAdmin && !session?.isVendorPortal);
51
+ if (isVendorSession && orderVendorId && !userVendorIds.includes(orderVendorId)) {
52
+ return json({
53
+ error: "Forbidden: Access denied to vendor order"
54
+ }, {
55
+ status: 403
56
+ });
57
+ }
58
+ const assigneesMap = /* @__PURE__ */ new Map();
59
+ if (isVendorSession) {
60
+ const targetVendorId = orderVendorId ?? session?.activeVendorId ?? userVendorIds[0];
61
+ if (entityMap.vendor_users && targetVendorId) {
62
+ const vuRepo = dataSource.getRepository(entityMap.vendor_users);
63
+ const vendorUsers = await vuRepo.find({
64
+ where: {
65
+ vendorId: targetVendorId
66
+ },
67
+ relations: [
68
+ "user",
69
+ "vendorRole"
70
+ ]
71
+ });
72
+ for (const vu of vendorUsers) {
73
+ const u = vu.user;
74
+ const r = vu.vendorRole;
75
+ if (u && u.id && !u.deleted && !u.blocked && u.inviteStatus === "active") {
76
+ const roleName = r?.name || (r?.isOwnerRole ? "Owner" : "Vendor Staff");
77
+ assigneesMap.set(u.id, {
78
+ id: u.id,
79
+ name: u.name || u.email?.split("@")[0] || `User #${u.id}`,
80
+ email: u.email || "",
81
+ role: roleName
82
+ });
83
+ }
84
+ }
85
+ }
86
+ } else {
87
+ if (entityMap.vendor_users) {
88
+ const vuRepo = dataSource.getRepository(entityMap.vendor_users);
89
+ const vendorUsers = await vuRepo.find({
90
+ relations: [
91
+ "user",
92
+ "vendorRole"
93
+ ]
94
+ });
95
+ for (const vu of vendorUsers) {
96
+ const u = vu.user;
97
+ const r = vu.vendorRole;
98
+ if (u && u.id && !u.deleted && !u.blocked && u.inviteStatus === "active") {
99
+ const roleName = r?.name || (r?.isOwnerRole ? "Owner" : "Vendor Staff");
100
+ assigneesMap.set(u.id, {
101
+ id: u.id,
102
+ name: u.name || u.email?.split("@")[0] || `User #${u.id}`,
103
+ email: u.email || "",
104
+ role: roleName
105
+ });
106
+ }
107
+ }
108
+ }
109
+ const userRepo = dataSource.getRepository(entityMap.users);
110
+ const users = await userRepo.find({
111
+ where: {
112
+ deleted: false,
113
+ blocked: false,
114
+ inviteStatus: "active"
115
+ },
116
+ relations: [
117
+ "group"
118
+ ],
119
+ order: {
120
+ name: "ASC"
121
+ }
122
+ });
123
+ for (const u of users) {
124
+ const groupName = u.group?.name ? String(u.group.name).trim() : "";
125
+ const isCustomer = groupName.toLowerCase() === "customer";
126
+ if (isCustomer) continue;
127
+ if (!assigneesMap.has(u.id)) {
128
+ let roleLabel = "Staff";
129
+ if (groupName) {
130
+ roleLabel = groupName;
131
+ } else if (u.adminAccess) {
132
+ roleLabel = "Administrator";
133
+ }
134
+ assigneesMap.set(u.id, {
135
+ id: u.id,
136
+ name: u.name || u.email?.split("@")[0] || `User #${u.id}`,
137
+ email: u.email || "",
138
+ role: roleLabel
139
+ });
140
+ }
141
+ }
142
+ }
143
+ return json({
144
+ assignees: Array.from(assigneesMap.values()),
145
+ currentAssignedUserId: order.assignedUserId ?? null
146
+ });
147
+ }
148
+ };
149
+ }
150
+ chunkUSNT2KNT_cjs.__name(createOrderAssigneesHandlers, "createOrderAssigneesHandlers");
151
+
152
+ exports.createOrderAssigneesHandlers = createOrderAssigneesHandlers;
@@ -0,0 +1,361 @@
1
+ import { fireOrderNotificationTrigger } from './chunk-6XAVZE4I.js';
2
+ import { verifyAndConsumeOtpChallenge, generateNumericOtp, createOtpChallenge, queueSms } from './chunk-OVTOP42W.js';
3
+ import { queueWhatsApp } from './chunk-GWDI752Q.js';
4
+ import './chunk-AYWDQFJZ.js';
5
+ import { queueEmail } from './chunk-YW7EF7K7.js';
6
+ import './chunk-HMLVPIJR.js';
7
+ import './chunk-JC6DLWTE.js';
8
+ import './chunk-NMYGQWHV.js';
9
+ import { __name } from './chunk-SHUYVCID.js';
10
+
11
+ // src/api/order-completion-otp-handlers.ts
12
+ function json(data, init) {
13
+ return new Response(JSON.stringify(data), {
14
+ status: init?.status ?? 200,
15
+ headers: {
16
+ "Content-Type": "application/json",
17
+ ...init?.headers || {}
18
+ }
19
+ });
20
+ }
21
+ __name(json, "json");
22
+ function maskEmail(email) {
23
+ if (!email) return "";
24
+ const parts = email.split("@");
25
+ if (parts.length !== 2) return "***";
26
+ const name = parts[0];
27
+ const domain = parts[1];
28
+ if (name.length <= 2) return `${name[0]}***@${domain}`;
29
+ return `${name.slice(0, 2)}***${name.slice(-1)}@${domain}`;
30
+ }
31
+ __name(maskEmail, "maskEmail");
32
+ function maskPhone(phone) {
33
+ if (!phone) return "";
34
+ const digits = String(phone).replace(/\D/g, "");
35
+ if (digits.length <= 4) return "***";
36
+ return `+${digits.slice(0, 2)} ***${digits.slice(-4)}`;
37
+ }
38
+ __name(maskPhone, "maskPhone");
39
+ async function getOtpChannelsConfig(dataSource, entityMap) {
40
+ if (!entityMap.configs) {
41
+ return {
42
+ email: true,
43
+ whatsapp: true,
44
+ sms: true,
45
+ push: false
46
+ };
47
+ }
48
+ const rows = await dataSource.getRepository(entityMap.configs).find({
49
+ where: {
50
+ settings: "event_notifications",
51
+ deleted: false
52
+ }
53
+ });
54
+ const map = Object.fromEntries(rows.map((r) => [
55
+ r.key,
56
+ r.value
57
+ ]));
58
+ return {
59
+ email: map.otp_email_enabled !== "false" && map.otp_email_enabled !== "0",
60
+ whatsapp: map.otp_whatsapp_enabled !== "false" && map.otp_whatsapp_enabled !== "0",
61
+ sms: map.otp_sms_enabled !== "false" && map.otp_sms_enabled !== "0",
62
+ push: map.otp_push_enabled === "true" || map.otp_push_enabled === "1"
63
+ };
64
+ }
65
+ __name(getOtpChannelsConfig, "getOtpChannelsConfig");
66
+ function createOrderCompletionOtpHandlers(dataSource, entityMap, getCms) {
67
+ return {
68
+ /** GET /api/orders/:id/completion-otp/channels */
69
+ async getChannels(req, orderId) {
70
+ if (!entityMap.orders) {
71
+ return json({
72
+ error: "Orders entity not found"
73
+ }, {
74
+ status: 500
75
+ });
76
+ }
77
+ const orderRepo = dataSource.getRepository(entityMap.orders);
78
+ const order = await orderRepo.findOne({
79
+ where: {
80
+ id: orderId,
81
+ deleted: false
82
+ },
83
+ relations: [
84
+ "contact"
85
+ ]
86
+ });
87
+ if (!order) {
88
+ return json({
89
+ error: "Order not found"
90
+ }, {
91
+ status: 404
92
+ });
93
+ }
94
+ const o = order;
95
+ const cfg = await getOtpChannelsConfig(dataSource, entityMap);
96
+ const customerEmail = o.contact?.email?.trim();
97
+ const customerPhone = o.contact?.phone?.trim();
98
+ const configuredChannels = [];
99
+ if (cfg.email) configuredChannels.push("email");
100
+ if (cfg.whatsapp) configuredChannels.push("whatsapp");
101
+ if (cfg.sms) configuredChannels.push("sms");
102
+ if (cfg.push) configuredChannels.push("push");
103
+ const activeChannels = [];
104
+ if (cfg.email && customerEmail) activeChannels.push("email");
105
+ if (cfg.whatsapp && customerPhone) activeChannels.push("whatsapp");
106
+ if (cfg.sms && customerPhone) activeChannels.push("sms");
107
+ if (cfg.push) activeChannels.push("push");
108
+ return json({
109
+ configuredChannels,
110
+ activeChannels,
111
+ maskedEmail: maskEmail(customerEmail),
112
+ maskedPhone: maskPhone(customerPhone),
113
+ rawEmail: customerEmail ?? null,
114
+ rawPhone: customerPhone ?? null
115
+ });
116
+ },
117
+ /** POST /api/orders/:id/completion-otp/send */
118
+ async sendOtp(req, orderId) {
119
+ if (!entityMap.orders) {
120
+ return json({
121
+ error: "Orders entity not found"
122
+ }, {
123
+ status: 500
124
+ });
125
+ }
126
+ let body = {};
127
+ try {
128
+ body = await req.json();
129
+ } catch {
130
+ }
131
+ const orderRepo = dataSource.getRepository(entityMap.orders);
132
+ const order = await orderRepo.findOne({
133
+ where: {
134
+ id: orderId,
135
+ deleted: false
136
+ },
137
+ relations: [
138
+ "contact"
139
+ ]
140
+ });
141
+ if (!order) {
142
+ return json({
143
+ error: "Order not found"
144
+ }, {
145
+ status: 404
146
+ });
147
+ }
148
+ const o = order;
149
+ if (o.status === "completed") {
150
+ return json({
151
+ error: "Order is already completed"
152
+ }, {
153
+ status: 400
154
+ });
155
+ }
156
+ if (o.status === "cancelled") {
157
+ return json({
158
+ error: "Cannot complete a cancelled order"
159
+ }, {
160
+ status: 400
161
+ });
162
+ }
163
+ const customerEmail = o.contact?.email?.trim();
164
+ const customerPhone = o.contact?.phone?.trim();
165
+ if (!customerEmail && !customerPhone) {
166
+ return json({
167
+ error: "Customer email and phone number are both missing for this order."
168
+ }, {
169
+ status: 400
170
+ });
171
+ }
172
+ const cfg = await getOtpChannelsConfig(dataSource, entityMap);
173
+ const requestedChannels = Array.isArray(body.channels) && body.channels.length > 0 ? body.channels : [
174
+ ...cfg.email ? [
175
+ "email"
176
+ ] : [],
177
+ ...cfg.whatsapp ? [
178
+ "whatsapp"
179
+ ] : [],
180
+ ...cfg.sms ? [
181
+ "sms"
182
+ ] : [],
183
+ ...cfg.push ? [
184
+ "push"
185
+ ] : []
186
+ ];
187
+ const targetChannels = [];
188
+ if (requestedChannels.includes("email") && customerEmail && cfg.email) targetChannels.push("email");
189
+ if (requestedChannels.includes("whatsapp") && customerPhone && cfg.whatsapp) targetChannels.push("whatsapp");
190
+ if (requestedChannels.includes("sms") && customerPhone && cfg.sms) targetChannels.push("sms");
191
+ if (requestedChannels.includes("push") && cfg.push) targetChannels.push("push");
192
+ if (targetChannels.length === 0) {
193
+ if (customerEmail) targetChannels.push("email");
194
+ else if (customerPhone) targetChannels.push("whatsapp");
195
+ }
196
+ const code = generateNumericOtp(6);
197
+ const challengeResult = await createOtpChallenge(dataSource, entityMap, {
198
+ purpose: "order_completion",
199
+ channel: targetChannels[0] || "email",
200
+ identifier: `order:${orderId}`,
201
+ code
202
+ });
203
+ if (!challengeResult.ok) {
204
+ return json({
205
+ error: challengeResult.error
206
+ }, {
207
+ status: challengeResult.status
208
+ });
209
+ }
210
+ const cms = await getCms();
211
+ const customerName = o.contact?.name || "Valued Customer";
212
+ const channelsSent = [];
213
+ if (targetChannels.includes("email") && customerEmail) {
214
+ try {
215
+ const html = `
216
+ <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; color: #1f2937; line-height: 1.6; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e5e7eb; border-radius: 8px;">
217
+ <h2 style="color: #111827; font-size: 20px; font-weight: 600; margin-bottom: 16px;">Order Completion Verification Code</h2>
218
+ <p>Hi ${customerName},</p>
219
+ <p>Your service/delivery representative is ready to mark your Order <strong>#${o.orderNumber}</strong> as completed.</p>
220
+ <p>Please share the following verification code with them to confirm completion:</p>
221
+ <div style="background-color: #f3f4f6; border-radius: 8px; padding: 20px; text-align: center; margin: 24px 0;">
222
+ <span style="font-size: 32px; font-weight: 700; letter-spacing: 8px; color: #111827;">${code}</span>
223
+ </div>
224
+ <p style="color: #6b7280; font-size: 13px;">This code is valid for 10 minutes. If you did not authorize this completion, please contact support immediately.</p>
225
+ </div>
226
+ `;
227
+ await queueEmail(cms, {
228
+ to: customerEmail,
229
+ subject: `Completion Code ${code} for Order #${o.orderNumber}`,
230
+ html,
231
+ text: `Your completion code for Order #${o.orderNumber} is ${code}. Valid for 10 minutes.`
232
+ });
233
+ channelsSent.push("email");
234
+ } catch (err) {
235
+ console.error("[completion-otp] failed to queue email", err);
236
+ }
237
+ }
238
+ if (targetChannels.includes("whatsapp") && customerPhone) {
239
+ try {
240
+ const sent = await queueWhatsApp(cms, {
241
+ to: customerPhone,
242
+ body: `Your completion code for Order #${o.orderNumber} is ${code}. Share this with your representative to complete your order. Valid for 10 minutes.`,
243
+ variables: {
244
+ code,
245
+ orderNumber: o.orderNumber,
246
+ customerName
247
+ }
248
+ });
249
+ if (sent) channelsSent.push("whatsapp");
250
+ } catch (err) {
251
+ console.error("[completion-otp] failed to queue WhatsApp", err);
252
+ }
253
+ }
254
+ if (targetChannels.includes("sms") && customerPhone) {
255
+ try {
256
+ await queueSms(cms, {
257
+ to: customerPhone,
258
+ body: `Your verification code for Order #${o.orderNumber} is ${code}. Valid for 10 minutes.`,
259
+ otpCode: code,
260
+ variables: {
261
+ code,
262
+ orderNumber: o.orderNumber
263
+ }
264
+ });
265
+ channelsSent.push("sms");
266
+ } catch (err) {
267
+ console.error("[completion-otp] failed to queue SMS", err);
268
+ }
269
+ }
270
+ return json({
271
+ ok: true,
272
+ message: "Verification code sent to customer",
273
+ channelsSent: channelsSent.length > 0 ? channelsSent : targetChannels,
274
+ maskedEmail: maskEmail(customerEmail),
275
+ maskedPhone: maskPhone(customerPhone)
276
+ });
277
+ },
278
+ /** POST /api/orders/:id/completion-otp/verify */
279
+ async verifyOtp(req, orderId) {
280
+ if (!entityMap.orders) {
281
+ return json({
282
+ error: "Orders entity not found"
283
+ }, {
284
+ status: 500
285
+ });
286
+ }
287
+ let body = {};
288
+ try {
289
+ body = await req.json();
290
+ } catch {
291
+ return json({
292
+ error: "Invalid request body"
293
+ }, {
294
+ status: 400
295
+ });
296
+ }
297
+ const code = String(body.code ?? "").trim();
298
+ if (!code || code.length < 4) {
299
+ return json({
300
+ error: "Please enter a valid verification code"
301
+ }, {
302
+ status: 400
303
+ });
304
+ }
305
+ const verifyResult = await verifyAndConsumeOtpChallenge(dataSource, entityMap, {
306
+ purpose: "order_completion",
307
+ identifier: `order:${orderId}`,
308
+ code
309
+ });
310
+ if (!verifyResult.ok) {
311
+ return json({
312
+ error: verifyResult.error
313
+ }, {
314
+ status: verifyResult.status
315
+ });
316
+ }
317
+ const orderRepo = dataSource.getRepository(entityMap.orders);
318
+ const order = await orderRepo.findOne({
319
+ where: {
320
+ id: orderId,
321
+ deleted: false
322
+ },
323
+ relations: [
324
+ "contact"
325
+ ]
326
+ });
327
+ if (!order) {
328
+ return json({
329
+ error: "Order not found"
330
+ }, {
331
+ status: 404
332
+ });
333
+ }
334
+ const now = /* @__PURE__ */ new Date();
335
+ order.status = "completed";
336
+ order.completedAt = now;
337
+ order.updatedAt = now;
338
+ await orderRepo.save(order);
339
+ try {
340
+ await fireOrderNotificationTrigger("order_completed", orderId, {
341
+ dataSource,
342
+ entityMap
343
+ });
344
+ } catch (err) {
345
+ console.warn("[completion-otp] failed to fire order_completed notification trigger", err);
346
+ }
347
+ return json({
348
+ ok: true,
349
+ message: "Order verified and completed successfully",
350
+ order: {
351
+ id: order.id,
352
+ status: "completed",
353
+ completedAt: now
354
+ }
355
+ });
356
+ }
357
+ };
358
+ }
359
+ __name(createOrderCompletionOtpHandlers, "createOrderCompletionOtpHandlers");
360
+
361
+ export { createOrderCompletionOtpHandlers };