@xcelsior/support-api 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xcelsior/support-api",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"packages/**",
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
"@tsconfig/node18": "^18.2.2",
|
|
27
27
|
"@types/node": "^20.11.16",
|
|
28
28
|
"@xcelsior/aws": "1.0.3",
|
|
29
|
+
"@xcelsior/support-client": "1.0.1",
|
|
29
30
|
"@xcelsior/lambda-http": "1.0.5",
|
|
30
31
|
"@xcelsior/monitoring": "1.0.4",
|
|
31
|
-
"@xcelsior/email": "1.0.2"
|
|
32
|
-
"@xcelsior/support-client": "1.0.0"
|
|
32
|
+
"@xcelsior/email": "1.0.2"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"dev": "sst dev --stage dev --mode basic",
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import type { SQSEvent } from 'aws-lambda';
|
|
2
|
+
import { DynamoDBService } from '@xcelsior/aws/src/dynamodb';
|
|
3
|
+
import { SqsService } from '@xcelsior/aws/src/sqs';
|
|
4
|
+
import {
|
|
5
|
+
logger,
|
|
6
|
+
TICKETS_TABLE,
|
|
7
|
+
TICKET_EVENTS_TABLE,
|
|
8
|
+
SUPPORT_CONFIG_TABLE,
|
|
9
|
+
REMINDER_QUEUE,
|
|
10
|
+
} from '../config';
|
|
11
|
+
import {
|
|
12
|
+
sendReminderEmail,
|
|
13
|
+
sendAssignmentEmail,
|
|
14
|
+
isEmailConfigured,
|
|
15
|
+
} from '../services/emailService';
|
|
16
|
+
import { randomUUID } from 'node:crypto';
|
|
17
|
+
|
|
18
|
+
interface AssignmentMessage {
|
|
19
|
+
type: 'assignment';
|
|
20
|
+
ticketId: string;
|
|
21
|
+
assigneeEmail: string;
|
|
22
|
+
ticketNumber: number;
|
|
23
|
+
subject: string;
|
|
24
|
+
priority?: string | null;
|
|
25
|
+
requesterName: string;
|
|
26
|
+
requesterEmail: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ReminderMessage {
|
|
30
|
+
type: 'reminder';
|
|
31
|
+
ticketId: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type NotificationMessage = AssignmentMessage | ReminderMessage;
|
|
35
|
+
|
|
36
|
+
interface Ticket {
|
|
37
|
+
id: string;
|
|
38
|
+
ticketNumber: number;
|
|
39
|
+
subject: string;
|
|
40
|
+
status: string;
|
|
41
|
+
priority?: string | null;
|
|
42
|
+
assigneeUserId?: string | null;
|
|
43
|
+
assigneeEmail?: string | null;
|
|
44
|
+
requesterName: string;
|
|
45
|
+
requesterEmail: string;
|
|
46
|
+
reminderAt?: string | null;
|
|
47
|
+
reminderSentAt?: string | null;
|
|
48
|
+
createdAt: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const processAssignment = async (message: AssignmentMessage): Promise<void> => {
|
|
52
|
+
// Check if assignment notifications are enabled
|
|
53
|
+
const config = await DynamoDBService.getItem<{
|
|
54
|
+
id: string;
|
|
55
|
+
notification?: { emailOnAssignment?: boolean };
|
|
56
|
+
}>(
|
|
57
|
+
{
|
|
58
|
+
tableName: SUPPORT_CONFIG_TABLE,
|
|
59
|
+
key: 'support-config',
|
|
60
|
+
},
|
|
61
|
+
logger
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (config?.notification?.emailOnAssignment === false) {
|
|
65
|
+
logger.info('Assignment emails disabled in config, skipping', {
|
|
66
|
+
ticketId: message.ticketId,
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await sendAssignmentEmail({
|
|
72
|
+
to: message.assigneeEmail,
|
|
73
|
+
ticketNumber: message.ticketNumber,
|
|
74
|
+
ticketId: message.ticketId,
|
|
75
|
+
subject: message.subject,
|
|
76
|
+
priority: message.priority,
|
|
77
|
+
requesterName: message.requesterName,
|
|
78
|
+
requesterEmail: message.requesterEmail,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Log the assignment notification event
|
|
82
|
+
await DynamoDBService.putItem(
|
|
83
|
+
{
|
|
84
|
+
tableName: TICKET_EVENTS_TABLE,
|
|
85
|
+
item: {
|
|
86
|
+
id: randomUUID(),
|
|
87
|
+
ticketId: message.ticketId,
|
|
88
|
+
type: 'assignment_email_sent',
|
|
89
|
+
payload: {
|
|
90
|
+
assigneeEmail: message.assigneeEmail,
|
|
91
|
+
},
|
|
92
|
+
createdAt: new Date().toISOString(),
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
logger
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
logger.info('Assignment notification sent', {
|
|
99
|
+
ticketId: message.ticketId,
|
|
100
|
+
assigneeEmail: message.assigneeEmail,
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const processReminder = async (message: ReminderMessage): Promise<void> => {
|
|
105
|
+
const ticket = await DynamoDBService.getItem<Ticket>(
|
|
106
|
+
{
|
|
107
|
+
tableName: TICKETS_TABLE,
|
|
108
|
+
key: message.ticketId,
|
|
109
|
+
},
|
|
110
|
+
logger
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (!ticket) {
|
|
114
|
+
logger.warn('Ticket not found for reminder', { ticketId: message.ticketId });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Skip if ticket is already resolved or archived
|
|
119
|
+
if (['Resolved', 'Archived'].includes(ticket.status)) {
|
|
120
|
+
logger.info('Ticket already resolved, skipping reminder', {
|
|
121
|
+
ticketId: ticket.id,
|
|
122
|
+
status: ticket.status,
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Skip if no assignee email
|
|
128
|
+
if (!ticket.assigneeEmail) {
|
|
129
|
+
logger.info('No assignee email, skipping reminder', { ticketId: ticket.id });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Skip if reminder already sent for this reminderAt
|
|
134
|
+
if (ticket.reminderSentAt && ticket.reminderAt) {
|
|
135
|
+
const sentAt = new Date(ticket.reminderSentAt);
|
|
136
|
+
const reminderAt = new Date(ticket.reminderAt);
|
|
137
|
+
if (sentAt >= reminderAt) {
|
|
138
|
+
logger.info('Reminder already sent for this period', {
|
|
139
|
+
ticketId: ticket.id,
|
|
140
|
+
reminderSentAt: ticket.reminderSentAt,
|
|
141
|
+
reminderAt: ticket.reminderAt,
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check if reminder notifications are enabled
|
|
148
|
+
const config = await DynamoDBService.getItem<{
|
|
149
|
+
id: string;
|
|
150
|
+
notification?: { emailOnReminder?: boolean };
|
|
151
|
+
}>(
|
|
152
|
+
{
|
|
153
|
+
tableName: SUPPORT_CONFIG_TABLE,
|
|
154
|
+
key: 'support-config',
|
|
155
|
+
},
|
|
156
|
+
logger
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (config?.notification?.emailOnReminder === false) {
|
|
160
|
+
logger.info('Reminder emails disabled in config, skipping', { ticketId: ticket.id });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Send the reminder email
|
|
165
|
+
await sendReminderEmail({
|
|
166
|
+
to: ticket.assigneeEmail,
|
|
167
|
+
ticketNumber: ticket.ticketNumber,
|
|
168
|
+
ticketId: ticket.id,
|
|
169
|
+
subject: ticket.subject,
|
|
170
|
+
priority: ticket.priority,
|
|
171
|
+
status: ticket.status,
|
|
172
|
+
requesterName: ticket.requesterName,
|
|
173
|
+
requesterEmail: ticket.requesterEmail,
|
|
174
|
+
reminderAt: ticket.reminderAt!,
|
|
175
|
+
createdAt: ticket.createdAt,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Update ticket with reminderSentAt
|
|
179
|
+
const now = new Date().toISOString();
|
|
180
|
+
await DynamoDBService.updateItem(
|
|
181
|
+
{
|
|
182
|
+
tableName: TICKETS_TABLE,
|
|
183
|
+
key: { id: ticket.id },
|
|
184
|
+
updateExpression: 'SET reminderSentAt = :reminderSentAt',
|
|
185
|
+
expressionAttributeValues: {
|
|
186
|
+
':reminderSentAt': now,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
logger
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Log the reminder event
|
|
193
|
+
await DynamoDBService.putItem(
|
|
194
|
+
{
|
|
195
|
+
tableName: TICKET_EVENTS_TABLE,
|
|
196
|
+
item: {
|
|
197
|
+
id: randomUUID(),
|
|
198
|
+
ticketId: ticket.id,
|
|
199
|
+
type: 'reminder_sent',
|
|
200
|
+
payload: {
|
|
201
|
+
assigneeEmail: ticket.assigneeEmail,
|
|
202
|
+
reminderAt: ticket.reminderAt,
|
|
203
|
+
},
|
|
204
|
+
createdAt: now,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
logger
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
logger.info('Reminder processed successfully', {
|
|
211
|
+
ticketId: ticket.id,
|
|
212
|
+
ticketNumber: ticket.ticketNumber,
|
|
213
|
+
assigneeEmail: ticket.assigneeEmail,
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const processMessage = async (message: NotificationMessage): Promise<void> => {
|
|
218
|
+
switch (message.type) {
|
|
219
|
+
case 'assignment':
|
|
220
|
+
await processAssignment(message);
|
|
221
|
+
break;
|
|
222
|
+
case 'reminder':
|
|
223
|
+
await processReminder(message);
|
|
224
|
+
break;
|
|
225
|
+
default:
|
|
226
|
+
logger.warn('Unknown message type', { message });
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
export const handler = async (event: SQSEvent) => {
|
|
231
|
+
if (!isEmailConfigured()) {
|
|
232
|
+
logger.info('Email not configured, skipping notification processing', {
|
|
233
|
+
recordCount: event.Records.length,
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
logger.info('Processing notification messages', {
|
|
239
|
+
recordCount: event.Records.length,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await SqsService.handleMessages(event, REMINDER_QUEUE, processMessage);
|
|
243
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { DynamoDBService } from '@xcelsior/aws/src/dynamodb';
|
|
2
|
+
import { SqsService } from '@xcelsior/aws/src/sqs';
|
|
3
|
+
import { logger, TICKETS_TABLE, REMINDER_QUEUE, SUPPORT_CONFIG_TABLE } from '../config';
|
|
4
|
+
import { isEmailConfigured } from '../services/emailService';
|
|
5
|
+
|
|
6
|
+
interface Ticket {
|
|
7
|
+
id: string;
|
|
8
|
+
status: string;
|
|
9
|
+
priority?: string | null;
|
|
10
|
+
assigneeEmail?: string | null;
|
|
11
|
+
reminderAt?: string | null;
|
|
12
|
+
reminderSentAt?: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ReminderMessage {
|
|
16
|
+
ticketId: string;
|
|
17
|
+
type: 'reminder';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const PRIORITIES = ['low', 'medium', 'high', 'critical'] as const;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Scans for tickets that need reminders and queues them for processing.
|
|
24
|
+
* Runs on a schedule (cron) to find overdue tickets.
|
|
25
|
+
*/
|
|
26
|
+
export const handler = async () => {
|
|
27
|
+
// Check if email is configured - skip if not
|
|
28
|
+
if (!isEmailConfigured()) {
|
|
29
|
+
logger.info('Email not configured, skipping reminder scan');
|
|
30
|
+
return { processed: 0, queued: 0 };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check if reminder notifications are enabled in config
|
|
34
|
+
const config = await DynamoDBService.getItem<{
|
|
35
|
+
id: string;
|
|
36
|
+
notification?: { emailOnReminder?: boolean };
|
|
37
|
+
}>(
|
|
38
|
+
{
|
|
39
|
+
tableName: SUPPORT_CONFIG_TABLE,
|
|
40
|
+
key: 'support-config',
|
|
41
|
+
},
|
|
42
|
+
logger
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
if (config?.notification?.emailOnReminder === false) {
|
|
46
|
+
logger.info('Reminder notifications disabled in config');
|
|
47
|
+
return { processed: 0, queued: 0 };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const now = new Date().toISOString();
|
|
51
|
+
let totalProcessed = 0;
|
|
52
|
+
const messagesToQueue: ReminderMessage[] = [];
|
|
53
|
+
|
|
54
|
+
// Query each priority using the ReminderIndex GSI
|
|
55
|
+
// The GSI has priority as partition key and reminderAt as sort key
|
|
56
|
+
for (const priority of PRIORITIES) {
|
|
57
|
+
try {
|
|
58
|
+
// Query tickets where reminderAt <= now for this priority
|
|
59
|
+
const result = await DynamoDBService.queryItems<Ticket>(
|
|
60
|
+
{
|
|
61
|
+
tableName: TICKETS_TABLE,
|
|
62
|
+
indexName: 'ReminderIndex',
|
|
63
|
+
keyConditionExpression: 'priority = :priority AND reminderAt <= :now',
|
|
64
|
+
expressionAttributeValues: {
|
|
65
|
+
':priority': priority,
|
|
66
|
+
':now': now,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
logger
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const tickets = result.items ?? [];
|
|
73
|
+
totalProcessed += tickets.length;
|
|
74
|
+
|
|
75
|
+
for (const ticket of tickets) {
|
|
76
|
+
// Skip if no assignee email
|
|
77
|
+
if (!ticket.assigneeEmail) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Skip if ticket is resolved or archived
|
|
82
|
+
if (['Resolved', 'Archived'].includes(ticket.status)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Skip if reminder was already sent for this reminderAt
|
|
87
|
+
if (ticket.reminderSentAt && ticket.reminderAt) {
|
|
88
|
+
const sentAt = new Date(ticket.reminderSentAt);
|
|
89
|
+
const reminderAt = new Date(ticket.reminderAt);
|
|
90
|
+
if (sentAt >= reminderAt) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
messagesToQueue.push({
|
|
96
|
+
ticketId: ticket.id,
|
|
97
|
+
type: 'reminder',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
logger.error('Failed to scan tickets for priority', {
|
|
102
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
103
|
+
priority,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Send all messages in batches
|
|
109
|
+
if (messagesToQueue.length > 0) {
|
|
110
|
+
try {
|
|
111
|
+
await SqsService.sendMessageBatch({
|
|
112
|
+
url: REMINDER_QUEUE,
|
|
113
|
+
messages: messagesToQueue,
|
|
114
|
+
id: (msg) => `reminder-${msg.ticketId}`,
|
|
115
|
+
});
|
|
116
|
+
logger.info('Queued reminder messages', { count: messagesToQueue.length });
|
|
117
|
+
} catch (error) {
|
|
118
|
+
logger.error('Failed to queue reminder messages', {
|
|
119
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
logger.info('Reminder scan complete', {
|
|
125
|
+
totalProcessed,
|
|
126
|
+
totalQueued: messagesToQueue.length,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return { processed: totalProcessed, queued: messagesToQueue.length };
|
|
130
|
+
};
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { SQSEvent } from 'aws-lambda';
|
|
2
|
-
import { logger, middlewareConfig } from '../config';
|
|
3
|
-
import { middyfy } from '@xcelsior/lambda-http';
|
|
4
|
-
|
|
5
|
-
const reminderSweep = async (event: SQSEvent) => {
|
|
6
|
-
logger.info('Reminder sweep - stub', {
|
|
7
|
-
recordCount: event.Records.length,
|
|
8
|
-
});
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export const handler = middyfy(reminderSweep, middlewareConfig);
|