@voyantjs/notifications 0.1.1 → 0.3.0

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/dist/service.js CHANGED
@@ -1,775 +1,22 @@
1
- import { bookingParticipants, bookings } from "@voyantjs/bookings/schema";
2
- import { bookingPaymentSchedules, invoices, paymentSessions } from "@voyantjs/finance";
3
- import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
4
- import { notificationDeliveries, notificationReminderRules, notificationReminderRuns, notificationTemplates, } from "./schema.js";
5
- /**
6
- * Thrown when `send()` is called with a channel that has no registered
7
- * provider, or with a provider name that does not exist.
8
- */
9
- export class NotificationError extends Error {
10
- constructor(message) {
11
- super(message);
12
- this.name = "NotificationError";
13
- }
14
- }
15
- export function createNotificationService(providers) {
16
- const byChannel = new Map();
17
- const byName = new Map();
18
- for (const provider of providers) {
19
- byName.set(provider.name, provider);
20
- for (const channel of provider.channels) {
21
- byChannel.set(channel, provider);
22
- }
23
- }
24
- return {
25
- async send(payload) {
26
- const hintedProvider = payload.provider ? byName.get(payload.provider) : null;
27
- const provider = hintedProvider ?? byChannel.get(payload.channel);
28
- if (!provider) {
29
- throw new NotificationError(`No notification provider registered for channel "${payload.channel}"`);
30
- }
31
- return provider.send(payload);
32
- },
33
- async sendWith(providerName, payload) {
34
- const provider = byName.get(providerName);
35
- if (!provider) {
36
- throw new NotificationError(`No notification provider registered with name "${providerName}"`);
37
- }
38
- return provider.send(payload);
39
- },
40
- getProvider(channel) {
41
- return byChannel.get(channel);
42
- },
43
- };
44
- }
45
- function resolveMustachePath(path, scope) {
46
- const parts = path.match(/[^.[\]]+/g) ?? [];
47
- let current = scope;
48
- for (const part of parts) {
49
- if (current == null || typeof current !== "object")
50
- return undefined;
51
- current = current[part];
52
- }
53
- return current;
54
- }
55
- function stringifyRenderedValue(value) {
56
- if (value == null)
57
- return "";
58
- if (typeof value === "string")
59
- return value;
60
- if (typeof value === "number" || typeof value === "boolean")
61
- return String(value);
62
- return JSON.stringify(value);
63
- }
64
- export function renderNotificationTemplate(template, data) {
65
- if (!template)
66
- return null;
67
- return template.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_match, path) => {
68
- return stringifyRenderedValue(resolveMustachePath(path.trim(), data));
69
- });
70
- }
71
- function toTimestamp(value) {
72
- return value ? new Date(value) : null;
73
- }
74
- function startOfUtcDay(value) {
75
- return new Date(Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()));
76
- }
77
- function addUtcDays(value, days) {
78
- return new Date(value.getTime() + days * 24 * 60 * 60 * 1000);
79
- }
80
- function toDateString(value) {
81
- return value.toISOString().slice(0, 10);
82
- }
83
- function buildReminderDedupeKey(ruleId, targetId, runDate) {
84
- return `${ruleId}:${targetId}:${runDate}`;
85
- }
86
- function resolveReminderRecipient(participants) {
87
- const withEmail = participants.filter((participant) => participant.email);
88
- if (withEmail.length === 0) {
89
- return null;
90
- }
91
- const primary = withEmail.find((participant) => participant.isPrimary);
92
- if (primary) {
93
- return primary;
94
- }
95
- const preferredTypes = ["booker", "contact", "traveler", "occupant"];
96
- for (const type of preferredTypes) {
97
- const match = withEmail.find((participant) => participant.participantType === type);
98
- if (match) {
99
- return match;
100
- }
101
- }
102
- return withEmail[0] ?? null;
103
- }
104
- async function listBookingNotificationParticipants(db, bookingId) {
105
- return db
106
- .select({
107
- id: bookingParticipants.id,
108
- firstName: bookingParticipants.firstName,
109
- lastName: bookingParticipants.lastName,
110
- email: bookingParticipants.email,
111
- participantType: bookingParticipants.participantType,
112
- isPrimary: bookingParticipants.isPrimary,
113
- })
114
- .from(bookingParticipants)
115
- .where(eq(bookingParticipants.bookingId, bookingId))
116
- .orderBy(desc(bookingParticipants.isPrimary), bookingParticipants.createdAt);
117
- }
118
- async function paginate(rowsPromise, totalPromise, limit, offset) {
119
- const [data, totalRows] = await Promise.all([rowsPromise, totalPromise]);
120
- return {
121
- data,
122
- total: totalRows[0]?.total ?? 0,
123
- limit,
124
- offset,
125
- };
126
- }
127
- async function sendBookingPaymentScheduleReminder(db, dispatcher, rule, schedule, now) {
128
- const runDate = toDateString(startOfUtcDay(now));
129
- const dedupeKey = buildReminderDedupeKey(rule.id, schedule.id, runDate);
130
- const [existingRun] = await db
131
- .select({ id: notificationReminderRuns.id })
132
- .from(notificationReminderRuns)
133
- .where(eq(notificationReminderRuns.dedupeKey, dedupeKey))
134
- .limit(1);
135
- if (existingRun) {
136
- return null;
137
- }
138
- const [booking] = await db
139
- .select()
140
- .from(bookings)
141
- .where(eq(bookings.id, schedule.bookingId))
142
- .limit(1);
143
- if (!booking) {
144
- const [run] = await db
145
- .insert(notificationReminderRuns)
146
- .values({
147
- reminderRuleId: rule.id,
148
- targetType: "booking_payment_schedule",
149
- targetId: schedule.id,
150
- dedupeKey,
151
- bookingId: schedule.bookingId,
152
- personId: null,
153
- organizationId: null,
154
- paymentSessionId: null,
155
- notificationDeliveryId: null,
156
- status: "skipped",
157
- recipient: null,
158
- scheduledFor: now,
159
- processedAt: now,
160
- errorMessage: "Booking not found for payment schedule",
161
- metadata: {
162
- dueDate: schedule.dueDate,
163
- relativeDaysFromDueDate: rule.relativeDaysFromDueDate,
164
- },
165
- })
166
- .returning();
167
- return run ?? null;
168
- }
169
- const participants = await db
170
- .select({
171
- id: bookingParticipants.id,
172
- firstName: bookingParticipants.firstName,
173
- lastName: bookingParticipants.lastName,
174
- email: bookingParticipants.email,
175
- participantType: bookingParticipants.participantType,
176
- isPrimary: bookingParticipants.isPrimary,
177
- })
178
- .from(bookingParticipants)
179
- .where(eq(bookingParticipants.bookingId, booking.id))
180
- .orderBy(desc(bookingParticipants.isPrimary), bookingParticipants.createdAt);
181
- const recipient = resolveReminderRecipient(participants);
182
- const [processingRun] = await db
183
- .insert(notificationReminderRuns)
184
- .values({
185
- reminderRuleId: rule.id,
186
- targetType: "booking_payment_schedule",
187
- targetId: schedule.id,
188
- dedupeKey,
189
- bookingId: booking.id,
190
- personId: booking.personId ?? null,
191
- organizationId: booking.organizationId ?? null,
192
- paymentSessionId: null,
193
- notificationDeliveryId: null,
194
- status: "processing",
195
- recipient: recipient?.email ?? null,
196
- scheduledFor: now,
197
- processedAt: now,
198
- errorMessage: null,
199
- metadata: {
200
- dueDate: schedule.dueDate,
201
- relativeDaysFromDueDate: rule.relativeDaysFromDueDate,
202
- bookingNumber: booking.bookingNumber,
203
- },
204
- })
205
- .onConflictDoNothing({ target: notificationReminderRuns.dedupeKey })
206
- .returning();
207
- if (!processingRun) {
208
- return null;
209
- }
210
- if (!recipient?.email) {
211
- const [run] = await db
212
- .update(notificationReminderRuns)
213
- .set({
214
- status: "skipped",
215
- errorMessage: "No participant email available for booking payment reminder",
216
- processedAt: now,
217
- updatedAt: now,
218
- })
219
- .where(eq(notificationReminderRuns.id, processingRun.id))
220
- .returning();
221
- return run ?? null;
222
- }
223
- try {
224
- const delivery = await notificationsService.sendNotification(db, dispatcher, {
225
- templateId: rule.templateId ?? null,
226
- templateSlug: rule.templateSlug ?? null,
227
- channel: rule.channel,
228
- provider: rule.provider ?? null,
229
- to: recipient.email,
230
- data: {
231
- bookingId: booking.id,
232
- bookingNumber: booking.bookingNumber,
233
- dueDate: schedule.dueDate,
234
- amountCents: schedule.amountCents,
235
- currency: schedule.currency,
236
- scheduleType: schedule.scheduleType,
237
- reminderOffsetDays: rule.relativeDaysFromDueDate,
238
- participant: {
239
- firstName: recipient.firstName,
240
- lastName: recipient.lastName,
241
- email: recipient.email,
242
- },
243
- booking: {
244
- id: booking.id,
245
- bookingNumber: booking.bookingNumber,
246
- startDate: booking.startDate,
247
- endDate: booking.endDate,
248
- sellCurrency: booking.sellCurrency,
249
- sellAmountCents: booking.sellAmountCents,
250
- },
251
- paymentSchedule: {
252
- id: schedule.id,
253
- dueDate: schedule.dueDate,
254
- amountCents: schedule.amountCents,
255
- currency: schedule.currency,
256
- scheduleType: schedule.scheduleType,
257
- status: schedule.status,
258
- },
259
- },
260
- targetType: "booking_payment_schedule",
261
- targetId: schedule.id,
262
- bookingId: booking.id,
263
- personId: booking.personId ?? null,
264
- organizationId: booking.organizationId ?? null,
265
- metadata: {
266
- reminderRuleId: rule.id,
267
- reminderRunId: processingRun.id,
268
- },
269
- scheduledFor: now.toISOString(),
270
- });
271
- const [run] = await db
272
- .update(notificationReminderRuns)
273
- .set({
274
- notificationDeliveryId: delivery?.id ?? null,
275
- status: "sent",
276
- processedAt: new Date(),
277
- updatedAt: new Date(),
278
- errorMessage: null,
279
- })
280
- .where(eq(notificationReminderRuns.id, processingRun.id))
281
- .returning();
282
- return run ?? null;
283
- }
284
- catch (error) {
285
- const message = error instanceof Error ? error.message : "Notification reminder failed";
286
- const [run] = await db
287
- .update(notificationReminderRuns)
288
- .set({
289
- status: "failed",
290
- errorMessage: message,
291
- processedAt: new Date(),
292
- updatedAt: new Date(),
293
- })
294
- .where(eq(notificationReminderRuns.id, processingRun.id))
295
- .returning();
296
- return run ?? null;
297
- }
298
- }
1
+ export { createNotificationService, NotificationError, renderNotificationTemplate, } from "./service-shared.js";
2
+ import { getDeliveryById, listDeliveries, sendInvoiceNotification, sendNotification, sendPaymentSessionNotification, } from "./service-deliveries.js";
3
+ import { runDueReminders } from "./service-reminders.js";
4
+ import { createReminderRule, createTemplate, getReminderRuleById, getTemplateById, getTemplateBySlug, listReminderRules, listReminderRuns, listTemplates, updateReminderRule, updateTemplate, } from "./service-templates.js";
299
5
  export const notificationsService = {
300
- async listTemplates(db, query) {
301
- const conditions = [];
302
- if (query.channel)
303
- conditions.push(eq(notificationTemplates.channel, query.channel));
304
- if (query.provider)
305
- conditions.push(eq(notificationTemplates.provider, query.provider));
306
- if (query.status)
307
- conditions.push(eq(notificationTemplates.status, query.status));
308
- if (query.search) {
309
- const term = `%${query.search}%`;
310
- conditions.push(or(ilike(notificationTemplates.slug, term), ilike(notificationTemplates.name, term)));
311
- }
312
- const where = conditions.length > 0 ? and(...conditions) : undefined;
313
- return paginate(db
314
- .select()
315
- .from(notificationTemplates)
316
- .where(where)
317
- .limit(query.limit)
318
- .offset(query.offset)
319
- .orderBy(desc(notificationTemplates.updatedAt)), db.select({ total: sql `count(*)::int` }).from(notificationTemplates).where(where), query.limit, query.offset);
320
- },
321
- async getTemplateById(db, id) {
322
- const [row] = await db
323
- .select()
324
- .from(notificationTemplates)
325
- .where(eq(notificationTemplates.id, id))
326
- .limit(1);
327
- return row ?? null;
328
- },
329
- async getTemplateBySlug(db, slug) {
330
- const [row] = await db
331
- .select()
332
- .from(notificationTemplates)
333
- .where(eq(notificationTemplates.slug, slug))
334
- .limit(1);
335
- return row ?? null;
336
- },
337
- async createTemplate(db, data) {
338
- const [row] = await db.insert(notificationTemplates).values(data).returning();
339
- return row ?? null;
340
- },
341
- async updateTemplate(db, id, data) {
342
- const [row] = await db
343
- .update(notificationTemplates)
344
- .set({ ...data, updatedAt: new Date() })
345
- .where(eq(notificationTemplates.id, id))
346
- .returning();
347
- return row ?? null;
348
- },
349
- async listDeliveries(db, query) {
350
- const conditions = [];
351
- if (query.channel)
352
- conditions.push(eq(notificationDeliveries.channel, query.channel));
353
- if (query.provider)
354
- conditions.push(eq(notificationDeliveries.provider, query.provider));
355
- if (query.status)
356
- conditions.push(eq(notificationDeliveries.status, query.status));
357
- if (query.templateSlug)
358
- conditions.push(eq(notificationDeliveries.templateSlug, query.templateSlug));
359
- if (query.targetType)
360
- conditions.push(eq(notificationDeliveries.targetType, query.targetType));
361
- if (query.targetId)
362
- conditions.push(eq(notificationDeliveries.targetId, query.targetId));
363
- if (query.bookingId)
364
- conditions.push(eq(notificationDeliveries.bookingId, query.bookingId));
365
- if (query.invoiceId)
366
- conditions.push(eq(notificationDeliveries.invoiceId, query.invoiceId));
367
- if (query.paymentSessionId)
368
- conditions.push(eq(notificationDeliveries.paymentSessionId, query.paymentSessionId));
369
- if (query.personId)
370
- conditions.push(eq(notificationDeliveries.personId, query.personId));
371
- if (query.organizationId)
372
- conditions.push(eq(notificationDeliveries.organizationId, query.organizationId));
373
- const where = conditions.length > 0 ? and(...conditions) : undefined;
374
- return paginate(db
375
- .select()
376
- .from(notificationDeliveries)
377
- .where(where)
378
- .limit(query.limit)
379
- .offset(query.offset)
380
- .orderBy(desc(notificationDeliveries.createdAt)), db.select({ total: sql `count(*)::int` }).from(notificationDeliveries).where(where), query.limit, query.offset);
381
- },
382
- async getDeliveryById(db, id) {
383
- const [row] = await db
384
- .select()
385
- .from(notificationDeliveries)
386
- .where(eq(notificationDeliveries.id, id))
387
- .limit(1);
388
- return row ?? null;
389
- },
390
- async sendNotification(db, dispatcher, input) {
391
- let template = null;
392
- if (input.templateId) {
393
- template = await this.getTemplateById(db, input.templateId);
394
- }
395
- else if (input.templateSlug) {
396
- template = await this.getTemplateBySlug(db, input.templateSlug);
397
- }
398
- if ((input.templateId || input.templateSlug) && !template) {
399
- throw new NotificationError("Notification template not found");
400
- }
401
- const data = input.data ?? {};
402
- const channel = input.channel ?? template?.channel;
403
- if (!channel) {
404
- throw new NotificationError("Notification channel is required");
405
- }
406
- const provider = input.provider ?? template?.provider ?? dispatcher.getProvider(channel)?.name;
407
- if (!provider) {
408
- throw new NotificationError(`No notification provider available for channel "${channel}"`);
409
- }
410
- const subject = input.subject ?? renderNotificationTemplate(template?.subjectTemplate, data);
411
- const html = input.html ?? renderNotificationTemplate(template?.htmlTemplate, data);
412
- const text = input.text ?? renderNotificationTemplate(template?.textTemplate, data);
413
- const [pending] = await db
414
- .insert(notificationDeliveries)
415
- .values({
416
- templateId: template?.id ?? null,
417
- templateSlug: template?.slug ?? input.templateSlug ?? null,
418
- targetType: input.targetType,
419
- targetId: input.targetId ?? null,
420
- personId: input.personId ?? null,
421
- organizationId: input.organizationId ?? null,
422
- bookingId: input.bookingId ?? null,
423
- invoiceId: input.invoiceId ?? null,
424
- paymentSessionId: input.paymentSessionId ?? null,
425
- channel,
426
- provider,
427
- providerMessageId: null,
428
- status: "pending",
429
- toAddress: input.to,
430
- fromAddress: input.from ?? template?.fromAddress ?? null,
431
- subject: subject ?? null,
432
- htmlBody: html ?? null,
433
- textBody: text ?? null,
434
- payloadData: data,
435
- metadata: input.metadata ?? null,
436
- errorMessage: null,
437
- scheduledFor: toTimestamp(input.scheduledFor),
438
- sentAt: null,
439
- failedAt: null,
440
- })
441
- .returning();
442
- if (!pending) {
443
- throw new NotificationError("Failed to create notification delivery");
444
- }
445
- try {
446
- const result = provider === dispatcher.getProvider(channel)?.name
447
- ? await dispatcher.send({
448
- to: input.to,
449
- channel,
450
- provider,
451
- template: template?.slug ?? input.templateSlug ?? "direct",
452
- data,
453
- from: input.from ?? template?.fromAddress ?? undefined,
454
- subject: subject ?? undefined,
455
- html: html ?? undefined,
456
- text: text ?? undefined,
457
- })
458
- : await dispatcher.sendWith(provider, {
459
- to: input.to,
460
- channel,
461
- provider,
462
- template: template?.slug ?? input.templateSlug ?? "direct",
463
- data,
464
- from: input.from ?? template?.fromAddress ?? undefined,
465
- subject: subject ?? undefined,
466
- html: html ?? undefined,
467
- text: text ?? undefined,
468
- });
469
- const [sent] = await db
470
- .update(notificationDeliveries)
471
- .set({
472
- status: "sent",
473
- providerMessageId: result.id ?? null,
474
- sentAt: new Date(),
475
- errorMessage: null,
476
- updatedAt: new Date(),
477
- })
478
- .where(eq(notificationDeliveries.id, pending.id))
479
- .returning();
480
- return sent ?? null;
481
- }
482
- catch (error) {
483
- const message = error instanceof Error ? error.message : "Notification send failed";
484
- const [failed] = await db
485
- .update(notificationDeliveries)
486
- .set({
487
- status: "failed",
488
- failedAt: new Date(),
489
- errorMessage: message,
490
- updatedAt: new Date(),
491
- })
492
- .where(eq(notificationDeliveries.id, pending.id))
493
- .returning();
494
- throw new NotificationError(failed?.errorMessage ?? message);
495
- }
496
- },
497
- async listReminderRules(db, query) {
498
- const conditions = [];
499
- if (query.status)
500
- conditions.push(eq(notificationReminderRules.status, query.status));
501
- if (query.targetType)
502
- conditions.push(eq(notificationReminderRules.targetType, query.targetType));
503
- if (query.channel)
504
- conditions.push(eq(notificationReminderRules.channel, query.channel));
505
- if (query.search) {
506
- const term = `%${query.search}%`;
507
- conditions.push(or(ilike(notificationReminderRules.slug, term), ilike(notificationReminderRules.name, term)));
508
- }
509
- const where = conditions.length > 0 ? and(...conditions) : undefined;
510
- return paginate(db
511
- .select()
512
- .from(notificationReminderRules)
513
- .where(where)
514
- .limit(query.limit)
515
- .offset(query.offset)
516
- .orderBy(desc(notificationReminderRules.updatedAt)), db.select({ total: sql `count(*)::int` }).from(notificationReminderRules).where(where), query.limit, query.offset);
517
- },
518
- async getReminderRuleById(db, id) {
519
- const [row] = await db
520
- .select()
521
- .from(notificationReminderRules)
522
- .where(eq(notificationReminderRules.id, id))
523
- .limit(1);
524
- return row ?? null;
525
- },
526
- async createReminderRule(db, data) {
527
- const [row] = await db.insert(notificationReminderRules).values(data).returning();
528
- return row ?? null;
529
- },
530
- async updateReminderRule(db, id, data) {
531
- const [row] = await db
532
- .update(notificationReminderRules)
533
- .set({ ...data, updatedAt: new Date() })
534
- .where(eq(notificationReminderRules.id, id))
535
- .returning();
536
- return row ?? null;
537
- },
538
- async listReminderRuns(db, query) {
539
- const conditions = [];
540
- if (query.reminderRuleId) {
541
- conditions.push(eq(notificationReminderRuns.reminderRuleId, query.reminderRuleId));
542
- }
543
- if (query.targetType)
544
- conditions.push(eq(notificationReminderRuns.targetType, query.targetType));
545
- if (query.targetId)
546
- conditions.push(eq(notificationReminderRuns.targetId, query.targetId));
547
- if (query.bookingId)
548
- conditions.push(eq(notificationReminderRuns.bookingId, query.bookingId));
549
- if (query.status)
550
- conditions.push(eq(notificationReminderRuns.status, query.status));
551
- const where = conditions.length > 0 ? and(...conditions) : undefined;
552
- return paginate(db
553
- .select()
554
- .from(notificationReminderRuns)
555
- .where(where)
556
- .limit(query.limit)
557
- .offset(query.offset)
558
- .orderBy(desc(notificationReminderRuns.createdAt)), db.select({ total: sql `count(*)::int` }).from(notificationReminderRuns).where(where), query.limit, query.offset);
559
- },
560
- async runDueReminders(db, dispatcher, input = {}) {
561
- const now = toTimestamp(input.now) ?? new Date();
562
- const today = startOfUtcDay(now);
563
- const activeRules = await db
564
- .select()
565
- .from(notificationReminderRules)
566
- .where(eq(notificationReminderRules.status, "active"))
567
- .orderBy(notificationReminderRules.createdAt);
568
- const summary = {
569
- processed: 0,
570
- sent: 0,
571
- skipped: 0,
572
- failed: 0,
573
- };
574
- for (const rule of activeRules) {
575
- const matchingDueDate = toDateString(addUtcDays(today, -rule.relativeDaysFromDueDate));
576
- const schedules = await db
577
- .select()
578
- .from(bookingPaymentSchedules)
579
- .where(and(eq(bookingPaymentSchedules.dueDate, matchingDueDate), or(eq(bookingPaymentSchedules.status, "pending"), eq(bookingPaymentSchedules.status, "due"))))
580
- .orderBy(bookingPaymentSchedules.createdAt);
581
- for (const schedule of schedules) {
582
- const run = await sendBookingPaymentScheduleReminder(db, dispatcher, rule, schedule, now);
583
- if (!run) {
584
- continue;
585
- }
586
- summary.processed += 1;
587
- if (run.status === "sent")
588
- summary.sent += 1;
589
- if (run.status === "skipped")
590
- summary.skipped += 1;
591
- if (run.status === "failed")
592
- summary.failed += 1;
593
- }
594
- }
595
- return summary;
596
- },
597
- async sendPaymentSessionNotification(db, dispatcher, sessionId, input) {
598
- const [session] = await db
599
- .select()
600
- .from(paymentSessions)
601
- .where(eq(paymentSessions.id, sessionId))
602
- .limit(1);
603
- if (!session) {
604
- return null;
605
- }
606
- const booking = session.bookingId
607
- ? ((await db.select().from(bookings).where(eq(bookings.id, session.bookingId)).limit(1))[0] ??
608
- null)
609
- : null;
610
- const invoice = session.invoiceId
611
- ? ((await db.select().from(invoices).where(eq(invoices.id, session.invoiceId)).limit(1))[0] ??
612
- null)
613
- : null;
614
- const participants = booking ? await listBookingNotificationParticipants(db, booking.id) : [];
615
- const recipient = resolveReminderRecipient(participants);
616
- const to = input.to ?? session.payerEmail ?? recipient?.email ?? null;
617
- if (!to) {
618
- throw new NotificationError("No recipient available for payment session notification");
619
- }
620
- return this.sendNotification(db, dispatcher, {
621
- templateId: input.templateId ?? null,
622
- templateSlug: input.templateSlug ?? null,
623
- channel: input.channel,
624
- provider: input.provider ?? null,
625
- to,
626
- from: input.from ?? null,
627
- subject: input.subject ?? null,
628
- html: input.html ?? null,
629
- text: input.text ?? null,
630
- data: {
631
- paymentSession: {
632
- id: session.id,
633
- status: session.status,
634
- provider: session.provider,
635
- currency: session.currency,
636
- amountCents: session.amountCents,
637
- redirectUrl: session.redirectUrl,
638
- returnUrl: session.returnUrl,
639
- cancelUrl: session.cancelUrl,
640
- expiresAt: session.expiresAt,
641
- paymentMethod: session.paymentMethod,
642
- externalReference: session.externalReference,
643
- },
644
- booking: booking
645
- ? {
646
- id: booking.id,
647
- bookingNumber: booking.bookingNumber,
648
- startDate: booking.startDate,
649
- endDate: booking.endDate,
650
- sellCurrency: booking.sellCurrency,
651
- sellAmountCents: booking.sellAmountCents,
652
- }
653
- : null,
654
- invoice: invoice
655
- ? {
656
- id: invoice.id,
657
- invoiceNumber: invoice.invoiceNumber,
658
- invoiceType: invoice.invoiceType,
659
- status: invoice.status,
660
- currency: invoice.currency,
661
- totalCents: invoice.totalCents,
662
- balanceDueCents: invoice.balanceDueCents,
663
- issueDate: invoice.issueDate,
664
- dueDate: invoice.dueDate,
665
- }
666
- : null,
667
- participant: recipient
668
- ? {
669
- firstName: recipient.firstName,
670
- lastName: recipient.lastName,
671
- email: recipient.email,
672
- }
673
- : null,
674
- ...(input.data ?? {}),
675
- },
676
- targetType: "payment_session",
677
- targetId: session.id,
678
- bookingId: session.bookingId ?? null,
679
- invoiceId: session.invoiceId ?? null,
680
- paymentSessionId: session.id,
681
- personId: session.payerPersonId ?? booking?.personId ?? null,
682
- organizationId: session.payerOrganizationId ?? booking?.organizationId ?? null,
683
- metadata: input.metadata ?? null,
684
- scheduledFor: input.scheduledFor ?? null,
685
- });
686
- },
687
- async sendInvoiceNotification(db, dispatcher, invoiceId, input) {
688
- const [invoice] = await db.select().from(invoices).where(eq(invoices.id, invoiceId)).limit(1);
689
- if (!invoice) {
690
- return null;
691
- }
692
- const [booking] = await db
693
- .select()
694
- .from(bookings)
695
- .where(eq(bookings.id, invoice.bookingId))
696
- .limit(1);
697
- const participants = booking ? await listBookingNotificationParticipants(db, booking.id) : [];
698
- const recipient = resolveReminderRecipient(participants);
699
- const [latestSession] = await db
700
- .select()
701
- .from(paymentSessions)
702
- .where(eq(paymentSessions.invoiceId, invoice.id))
703
- .orderBy(desc(paymentSessions.createdAt))
704
- .limit(1);
705
- const to = input.to ?? latestSession?.payerEmail ?? recipient?.email ?? null;
706
- if (!to) {
707
- throw new NotificationError("No recipient available for invoice notification");
708
- }
709
- return this.sendNotification(db, dispatcher, {
710
- templateId: input.templateId ?? null,
711
- templateSlug: input.templateSlug ?? null,
712
- channel: input.channel,
713
- provider: input.provider ?? null,
714
- to,
715
- from: input.from ?? null,
716
- subject: input.subject ?? null,
717
- html: input.html ?? null,
718
- text: input.text ?? null,
719
- data: {
720
- invoice: {
721
- id: invoice.id,
722
- invoiceNumber: invoice.invoiceNumber,
723
- invoiceType: invoice.invoiceType,
724
- status: invoice.status,
725
- currency: invoice.currency,
726
- subtotalCents: invoice.subtotalCents,
727
- taxCents: invoice.taxCents,
728
- totalCents: invoice.totalCents,
729
- paidCents: invoice.paidCents,
730
- balanceDueCents: invoice.balanceDueCents,
731
- issueDate: invoice.issueDate,
732
- dueDate: invoice.dueDate,
733
- },
734
- booking: booking
735
- ? {
736
- id: booking.id,
737
- bookingNumber: booking.bookingNumber,
738
- startDate: booking.startDate,
739
- endDate: booking.endDate,
740
- sellCurrency: booking.sellCurrency,
741
- sellAmountCents: booking.sellAmountCents,
742
- }
743
- : null,
744
- paymentSession: latestSession
745
- ? {
746
- id: latestSession.id,
747
- status: latestSession.status,
748
- provider: latestSession.provider,
749
- redirectUrl: latestSession.redirectUrl,
750
- expiresAt: latestSession.expiresAt,
751
- amountCents: latestSession.amountCents,
752
- currency: latestSession.currency,
753
- }
754
- : null,
755
- participant: recipient
756
- ? {
757
- firstName: recipient.firstName,
758
- lastName: recipient.lastName,
759
- email: recipient.email,
760
- }
761
- : null,
762
- ...(input.data ?? {}),
763
- },
764
- targetType: "invoice",
765
- targetId: invoice.id,
766
- bookingId: invoice.bookingId,
767
- invoiceId: invoice.id,
768
- paymentSessionId: latestSession?.id ?? null,
769
- personId: invoice.personId ?? booking?.personId ?? null,
770
- organizationId: invoice.organizationId ?? booking?.organizationId ?? null,
771
- metadata: input.metadata ?? null,
772
- scheduledFor: input.scheduledFor ?? null,
773
- });
774
- },
6
+ listTemplates,
7
+ getTemplateById,
8
+ getTemplateBySlug,
9
+ createTemplate,
10
+ updateTemplate,
11
+ listDeliveries,
12
+ getDeliveryById,
13
+ sendNotification,
14
+ listReminderRules,
15
+ getReminderRuleById,
16
+ createReminderRule,
17
+ updateReminderRule,
18
+ listReminderRuns,
19
+ runDueReminders,
20
+ sendPaymentSessionNotification,
21
+ sendInvoiceNotification,
775
22
  };