@voyantjs/notifications 0.3.0 → 0.4.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.
Files changed (39) hide show
  1. package/README.md +22 -0
  2. package/dist/index.d.ts +8 -4
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +5 -3
  5. package/dist/provider-resolution.d.ts +5 -0
  6. package/dist/provider-resolution.d.ts.map +1 -1
  7. package/dist/provider-resolution.js +22 -0
  8. package/dist/providers/resend.d.ts.map +1 -1
  9. package/dist/providers/resend.js +8 -0
  10. package/dist/providers/twilio.d.ts +24 -0
  11. package/dist/providers/twilio.d.ts.map +1 -0
  12. package/dist/providers/twilio.js +48 -0
  13. package/dist/routes.d.ts +128 -10
  14. package/dist/routes.d.ts.map +1 -1
  15. package/dist/routes.js +42 -1
  16. package/dist/schema.d.ts +6 -6
  17. package/dist/schema.d.ts.map +1 -1
  18. package/dist/schema.js +1 -0
  19. package/dist/service-booking-documents.d.ts +119 -0
  20. package/dist/service-booking-documents.d.ts.map +1 -0
  21. package/dist/service-booking-documents.js +261 -0
  22. package/dist/service-deliveries.d.ts +5 -5
  23. package/dist/service-deliveries.d.ts.map +1 -1
  24. package/dist/service-deliveries.js +25 -2
  25. package/dist/service-reminders.d.ts.map +1 -1
  26. package/dist/service-reminders.js +187 -19
  27. package/dist/service-shared.d.ts +11 -2
  28. package/dist/service-shared.d.ts.map +1 -1
  29. package/dist/service-shared.js +12 -0
  30. package/dist/service-templates.d.ts +5 -5
  31. package/dist/service.d.ts +111 -1
  32. package/dist/service.d.ts.map +1 -1
  33. package/dist/service.js +6 -1
  34. package/dist/types.d.ts +22 -0
  35. package/dist/types.d.ts.map +1 -1
  36. package/dist/validation.d.ts +178 -6
  37. package/dist/validation.d.ts.map +1 -1
  38. package/dist/validation.js +64 -1
  39. package/package.json +7 -6
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
package/dist/schema.js CHANGED
@@ -30,6 +30,7 @@ export const notificationReminderStatusEnum = pgEnum("notification_reminder_stat
30
30
  ]);
31
31
  export const notificationReminderTargetTypeEnum = pgEnum("notification_reminder_target_type", [
32
32
  "booking_payment_schedule",
33
+ "invoice",
33
34
  ]);
34
35
  export const notificationReminderRunStatusEnum = pgEnum("notification_reminder_run_status", [
35
36
  "processing",
@@ -0,0 +1,119 @@
1
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
+ import type { BookingDocumentBundleItem, NotificationService, SendBookingDocumentsNotificationInput } from "./service-shared.js";
3
+ import type { NotificationAttachment } from "./types.js";
4
+ export type BookingDocumentAttachmentResolver = (document: BookingDocumentBundleItem) => Promise<NotificationAttachment | null>;
5
+ export interface SendBookingDocumentsRuntimeOptions {
6
+ attachmentResolver?: BookingDocumentAttachmentResolver;
7
+ }
8
+ declare function createDefaultAttachmentFromDocument(document: BookingDocumentBundleItem): NotificationAttachment | null;
9
+ export declare const bookingDocumentNotificationsService: {
10
+ listBookingDocumentBundle(db: PostgresJsDatabase, bookingId: string): Promise<{
11
+ bookingId: string;
12
+ documents: {
13
+ key: string;
14
+ source: "finance" | "legal";
15
+ documentType: "invoice" | "proforma" | "contract";
16
+ bookingId: string;
17
+ name: string;
18
+ createdAt: string;
19
+ contractId?: string | null | undefined;
20
+ invoiceId?: string | null | undefined;
21
+ attachmentId?: string | null | undefined;
22
+ renditionId?: string | null | undefined;
23
+ contractStatus?: string | null | undefined;
24
+ invoiceStatus?: string | null | undefined;
25
+ format?: string | null | undefined;
26
+ mimeType?: string | null | undefined;
27
+ storageKey?: string | null | undefined;
28
+ downloadUrl?: string | null | undefined;
29
+ language?: string | null | undefined;
30
+ metadata?: Record<string, unknown> | null | undefined;
31
+ }[];
32
+ } | null>;
33
+ sendBookingDocumentsNotification(db: PostgresJsDatabase, dispatcher: NotificationService, bookingId: string, input: SendBookingDocumentsNotificationInput, runtime?: SendBookingDocumentsRuntimeOptions): Promise<{
34
+ status: "not_found";
35
+ bookingId?: undefined;
36
+ recipient?: undefined;
37
+ documents?: undefined;
38
+ delivery?: undefined;
39
+ } | {
40
+ status: "no_documents";
41
+ bookingId?: undefined;
42
+ recipient?: undefined;
43
+ documents?: undefined;
44
+ delivery?: undefined;
45
+ } | {
46
+ status: "no_recipient";
47
+ bookingId?: undefined;
48
+ recipient?: undefined;
49
+ documents?: undefined;
50
+ delivery?: undefined;
51
+ } | {
52
+ status: "no_attachments";
53
+ bookingId?: undefined;
54
+ recipient?: undefined;
55
+ documents?: undefined;
56
+ delivery?: undefined;
57
+ } | {
58
+ status: "send_failed";
59
+ bookingId?: undefined;
60
+ recipient?: undefined;
61
+ documents?: undefined;
62
+ delivery?: undefined;
63
+ } | {
64
+ status: "sent";
65
+ bookingId: string;
66
+ recipient: string;
67
+ documents: {
68
+ key: string;
69
+ source: "finance" | "legal";
70
+ documentType: "invoice" | "proforma" | "contract";
71
+ bookingId: string;
72
+ name: string;
73
+ createdAt: string;
74
+ contractId?: string | null | undefined;
75
+ invoiceId?: string | null | undefined;
76
+ attachmentId?: string | null | undefined;
77
+ renditionId?: string | null | undefined;
78
+ contractStatus?: string | null | undefined;
79
+ invoiceStatus?: string | null | undefined;
80
+ format?: string | null | undefined;
81
+ mimeType?: string | null | undefined;
82
+ storageKey?: string | null | undefined;
83
+ downloadUrl?: string | null | undefined;
84
+ language?: string | null | undefined;
85
+ metadata?: Record<string, unknown> | null | undefined;
86
+ }[];
87
+ delivery: {
88
+ id: string;
89
+ templateId: string | null;
90
+ templateSlug: string | null;
91
+ targetType: "other" | "booking" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "organization" | "person" | "payment_session";
92
+ targetId: string | null;
93
+ personId: string | null;
94
+ organizationId: string | null;
95
+ bookingId: string | null;
96
+ invoiceId: string | null;
97
+ paymentSessionId: string | null;
98
+ channel: "email" | "sms";
99
+ provider: string;
100
+ providerMessageId: string | null;
101
+ status: "cancelled" | "pending" | "failed" | "sent";
102
+ toAddress: string;
103
+ fromAddress: string | null;
104
+ subject: string | null;
105
+ htmlBody: string | null;
106
+ textBody: string | null;
107
+ payloadData: Record<string, unknown> | null;
108
+ metadata: Record<string, unknown> | null;
109
+ errorMessage: string | null;
110
+ scheduledFor: Date | null;
111
+ sentAt: Date | null;
112
+ failedAt: Date | null;
113
+ createdAt: Date;
114
+ updatedAt: Date;
115
+ };
116
+ }>;
117
+ };
118
+ export { createDefaultAttachmentFromDocument as createDefaultBookingDocumentAttachment };
119
+ //# sourceMappingURL=service-booking-documents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-booking-documents.d.ts","sourceRoot":"","sources":["../src/service-booking-documents.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAGjE,OAAO,KAAK,EACV,yBAAyB,EACzB,mBAAmB,EACnB,qCAAqC,EACtC,MAAM,qBAAqB,CAAA;AAE5B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAA;AAExD,MAAM,MAAM,iCAAiC,GAAG,CAC9C,QAAQ,EAAE,yBAAyB,KAChC,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAAA;AAE3C,MAAM,WAAW,kCAAkC;IACjD,kBAAkB,CAAC,EAAE,iCAAiC,CAAA;CACvD;AA2BD,iBAAS,mCAAmC,CAC1C,QAAQ,EAAE,yBAAyB,GAClC,sBAAsB,GAAG,IAAI,CAU/B;AAwLD,eAAO,MAAM,mCAAmC;kCACV,kBAAkB,aAAa,MAAM;;;;;;;;;;;;;;;;;;;;;;;yCAkBnE,kBAAkB,cACV,mBAAmB,aACpB,MAAM,SACV,qCAAqC,YACnC,kCAAkC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6F9C,CAAA;AAED,OAAO,EAAE,mCAAmC,IAAI,sCAAsC,EAAE,CAAA"}
@@ -0,0 +1,261 @@
1
+ import { bookings } from "@voyantjs/bookings/schema";
2
+ import { invoiceRenditions, invoices } from "@voyantjs/finance/schema";
3
+ import { contractAttachments, contracts } from "@voyantjs/legal/contracts";
4
+ import { and, desc, eq, ne, or } from "drizzle-orm";
5
+ import { sendNotification } from "./service-deliveries.js";
6
+ import { listBookingNotificationParticipants, resolveReminderRecipient } from "./service-shared.js";
7
+ function getMetadataRecord(value) {
8
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
9
+ return null;
10
+ }
11
+ return value;
12
+ }
13
+ function getMetadataString(metadata, keys) {
14
+ if (!metadata) {
15
+ return null;
16
+ }
17
+ for (const key of keys) {
18
+ const value = metadata[key];
19
+ if (typeof value === "string" && value.length > 0) {
20
+ return value;
21
+ }
22
+ }
23
+ return null;
24
+ }
25
+ function createDefaultAttachmentFromDocument(document) {
26
+ if (!document.downloadUrl) {
27
+ return null;
28
+ }
29
+ return {
30
+ filename: document.name,
31
+ path: document.downloadUrl,
32
+ contentType: document.mimeType ?? undefined,
33
+ };
34
+ }
35
+ function buildDefaultDocumentMessage(booking, documents) {
36
+ const label = booking.bookingNumber || booking.id;
37
+ const listText = documents.map((document) => `- ${document.name}`).join("\n");
38
+ const listHtml = documents.map((document) => `<li>${document.name}</li>`).join("");
39
+ return {
40
+ subject: `Booking ${label} documents`,
41
+ text: `Your booking documents are attached.\n\nBooking: ${label}\n\n${listText}`,
42
+ html: `<p>Your booking documents are attached.</p><p><strong>Booking:</strong> ${label}</p><ul>${listHtml}</ul>`,
43
+ };
44
+ }
45
+ async function listLegalBookingDocuments(db, bookingId) {
46
+ const contractRows = await db
47
+ .select()
48
+ .from(contracts)
49
+ .where(and(eq(contracts.bookingId, bookingId), eq(contracts.scope, "customer"), ne(contracts.status, "void")))
50
+ .orderBy(desc(contracts.createdAt));
51
+ if (contractRows.length === 0) {
52
+ return [];
53
+ }
54
+ const attachmentRows = await db
55
+ .select()
56
+ .from(contractAttachments)
57
+ .where(and(eq(contractAttachments.kind, "document"), or(...contractRows.map((contract) => eq(contractAttachments.contractId, contract.id)))))
58
+ .orderBy(desc(contractAttachments.createdAt));
59
+ const bestAttachmentByContractId = new Map();
60
+ for (const attachment of attachmentRows) {
61
+ if (!bestAttachmentByContractId.has(attachment.contractId)) {
62
+ bestAttachmentByContractId.set(attachment.contractId, attachment);
63
+ }
64
+ }
65
+ return contractRows.flatMap((contract) => {
66
+ const attachment = bestAttachmentByContractId.get(contract.id);
67
+ if (!attachment) {
68
+ return [];
69
+ }
70
+ const metadata = getMetadataRecord(attachment.metadata);
71
+ return [
72
+ {
73
+ key: `legal:${attachment.id}`,
74
+ source: "legal",
75
+ documentType: "contract",
76
+ bookingId,
77
+ contractId: contract.id,
78
+ invoiceId: null,
79
+ attachmentId: attachment.id,
80
+ renditionId: null,
81
+ contractStatus: contract.status,
82
+ invoiceStatus: null,
83
+ name: attachment.name,
84
+ format: attachment.mimeType === "application/pdf" ? "pdf" : null,
85
+ mimeType: attachment.mimeType ?? null,
86
+ storageKey: attachment.storageKey ?? null,
87
+ downloadUrl: getMetadataString(metadata, ["url", "downloadUrl"]),
88
+ language: contract.language ?? null,
89
+ metadata,
90
+ createdAt: attachment.createdAt.toISOString(),
91
+ },
92
+ ];
93
+ });
94
+ }
95
+ function compareInvoiceRenditions(left, right) {
96
+ const formatRank = new Map([
97
+ ["pdf", 0],
98
+ ["html", 1],
99
+ ["json", 2],
100
+ ["xml", 3],
101
+ ]);
102
+ const leftRank = formatRank.get(left.format) ?? Number.MAX_SAFE_INTEGER;
103
+ const rightRank = formatRank.get(right.format) ?? Number.MAX_SAFE_INTEGER;
104
+ if (leftRank !== rightRank) {
105
+ return leftRank - rightRank;
106
+ }
107
+ return right.createdAt.getTime() - left.createdAt.getTime();
108
+ }
109
+ async function listFinanceBookingDocuments(db, bookingId) {
110
+ const invoiceRows = await db
111
+ .select()
112
+ .from(invoices)
113
+ .where(and(eq(invoices.bookingId, bookingId), ne(invoices.status, "void")))
114
+ .orderBy(desc(invoices.createdAt));
115
+ if (invoiceRows.length === 0) {
116
+ return [];
117
+ }
118
+ const renditionRows = await db
119
+ .select()
120
+ .from(invoiceRenditions)
121
+ .where(and(eq(invoiceRenditions.status, "ready"), or(...invoiceRows.map((invoice) => eq(invoiceRenditions.invoiceId, invoice.id)))))
122
+ .orderBy(desc(invoiceRenditions.createdAt));
123
+ const bestRenditionByInvoiceId = new Map();
124
+ for (const rendition of renditionRows) {
125
+ const existing = bestRenditionByInvoiceId.get(rendition.invoiceId);
126
+ if (!existing || compareInvoiceRenditions(rendition, existing) < 0) {
127
+ bestRenditionByInvoiceId.set(rendition.invoiceId, rendition);
128
+ }
129
+ }
130
+ return invoiceRows.flatMap((invoice) => {
131
+ const rendition = bestRenditionByInvoiceId.get(invoice.id);
132
+ if (!rendition) {
133
+ return [];
134
+ }
135
+ const metadata = getMetadataRecord(rendition.metadata);
136
+ const format = rendition.format;
137
+ const extension = format === "pdf" ? "pdf" : format;
138
+ return [
139
+ {
140
+ key: `finance:${rendition.id}`,
141
+ source: "finance",
142
+ documentType: invoice.invoiceType === "proforma" ? "proforma" : "invoice",
143
+ bookingId,
144
+ contractId: null,
145
+ invoiceId: invoice.id,
146
+ attachmentId: null,
147
+ renditionId: rendition.id,
148
+ contractStatus: null,
149
+ invoiceStatus: invoice.status,
150
+ name: `${invoice.invoiceNumber}.${extension}`,
151
+ format,
152
+ mimeType: format === "pdf"
153
+ ? "application/pdf"
154
+ : format === "html"
155
+ ? "text/html"
156
+ : format === "json"
157
+ ? "application/json"
158
+ : "application/xml",
159
+ storageKey: rendition.storageKey ?? null,
160
+ downloadUrl: getMetadataString(metadata, ["url", "downloadUrl"]),
161
+ language: rendition.language ?? invoice.language ?? null,
162
+ metadata,
163
+ createdAt: rendition.createdAt.toISOString(),
164
+ },
165
+ ];
166
+ });
167
+ }
168
+ export const bookingDocumentNotificationsService = {
169
+ async listBookingDocumentBundle(db, bookingId) {
170
+ const [booking] = await db.select().from(bookings).where(eq(bookings.id, bookingId)).limit(1);
171
+ if (!booking) {
172
+ return null;
173
+ }
174
+ const [legalDocuments, financeDocuments] = await Promise.all([
175
+ listLegalBookingDocuments(db, bookingId),
176
+ listFinanceBookingDocuments(db, bookingId),
177
+ ]);
178
+ return {
179
+ bookingId,
180
+ documents: [...legalDocuments, ...financeDocuments],
181
+ };
182
+ },
183
+ async sendBookingDocumentsNotification(db, dispatcher, bookingId, input, runtime = {}) {
184
+ const [booking] = await db.select().from(bookings).where(eq(bookings.id, bookingId)).limit(1);
185
+ if (!booking) {
186
+ return { status: "not_found" };
187
+ }
188
+ const bundle = await this.listBookingDocumentBundle(db, bookingId);
189
+ const requestedTypes = new Set(input.documentTypes ?? ["contract", "invoice", "proforma"]);
190
+ const documents = (bundle?.documents ?? []).filter((document) => requestedTypes.has(document.documentType));
191
+ if (documents.length === 0) {
192
+ return { status: "no_documents" };
193
+ }
194
+ const participants = await listBookingNotificationParticipants(db, bookingId);
195
+ const recipient = resolveReminderRecipient(participants);
196
+ const to = input.to ?? recipient?.email ?? null;
197
+ if (!to) {
198
+ return { status: "no_recipient" };
199
+ }
200
+ const attachmentResolver = runtime.attachmentResolver ??
201
+ (async (document) => createDefaultAttachmentFromDocument(document));
202
+ const attachments = (await Promise.all(documents.map((document) => attachmentResolver(document)))).filter((attachment) => Boolean(attachment));
203
+ if (attachments.length === 0) {
204
+ return { status: "no_attachments" };
205
+ }
206
+ const defaults = buildDefaultDocumentMessage(booking, documents);
207
+ const delivery = await sendNotification(db, dispatcher, {
208
+ templateId: input.templateId ?? null,
209
+ templateSlug: input.templateSlug ?? null,
210
+ channel: "email",
211
+ provider: input.provider ?? null,
212
+ to,
213
+ from: input.from ?? null,
214
+ subject: input.subject ?? defaults.subject,
215
+ html: input.html ?? defaults.html,
216
+ text: input.text ?? defaults.text,
217
+ attachments,
218
+ data: {
219
+ booking: {
220
+ id: booking.id,
221
+ bookingNumber: booking.bookingNumber,
222
+ status: booking.status,
223
+ sellCurrency: booking.sellCurrency,
224
+ sellAmountCents: booking.sellAmountCents,
225
+ startDate: booking.startDate,
226
+ endDate: booking.endDate,
227
+ },
228
+ participant: recipient
229
+ ? {
230
+ firstName: recipient.firstName,
231
+ lastName: recipient.lastName,
232
+ email: recipient.email,
233
+ }
234
+ : null,
235
+ documents,
236
+ ...(input.data ?? {}),
237
+ },
238
+ targetType: "booking",
239
+ targetId: booking.id,
240
+ bookingId: booking.id,
241
+ personId: booking.personId ?? null,
242
+ organizationId: booking.organizationId ?? null,
243
+ metadata: {
244
+ bookingDocumentKeys: documents.map((document) => document.key),
245
+ ...(input.metadata ?? {}),
246
+ },
247
+ scheduledFor: input.scheduledFor ?? null,
248
+ });
249
+ if (!delivery) {
250
+ return { status: "send_failed" };
251
+ }
252
+ return {
253
+ status: "sent",
254
+ bookingId: booking.id,
255
+ recipient: to,
256
+ documents,
257
+ delivery,
258
+ };
259
+ },
260
+ };
261
+ export { createDefaultAttachmentFromDocument as createDefaultBookingDocumentAttachment };
@@ -5,7 +5,7 @@ export declare function listDeliveries(db: PostgresJsDatabase, query: Notificati
5
5
  id: string;
6
6
  templateId: string | null;
7
7
  templateSlug: string | null;
8
- targetType: "other" | "booking" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "payment_session" | "person" | "organization";
8
+ targetType: "other" | "booking" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "organization" | "person" | "payment_session";
9
9
  targetId: string | null;
10
10
  personId: string | null;
11
11
  organizationId: string | null;
@@ -38,7 +38,7 @@ export declare function getDeliveryById(db: PostgresJsDatabase, id: string): Pro
38
38
  id: string;
39
39
  templateId: string | null;
40
40
  templateSlug: string | null;
41
- targetType: "other" | "booking" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "payment_session" | "person" | "organization";
41
+ targetType: "other" | "booking" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "organization" | "person" | "payment_session";
42
42
  targetId: string | null;
43
43
  personId: string | null;
44
44
  organizationId: string | null;
@@ -67,7 +67,7 @@ export declare function sendNotification(db: PostgresJsDatabase, dispatcher: Not
67
67
  id: string;
68
68
  templateId: string | null;
69
69
  templateSlug: string | null;
70
- targetType: "other" | "booking" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "payment_session" | "person" | "organization";
70
+ targetType: "other" | "booking" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "organization" | "person" | "payment_session";
71
71
  targetId: string | null;
72
72
  personId: string | null;
73
73
  organizationId: string | null;
@@ -96,7 +96,7 @@ export declare function sendPaymentSessionNotification(db: PostgresJsDatabase, d
96
96
  id: string;
97
97
  templateId: string | null;
98
98
  templateSlug: string | null;
99
- targetType: "other" | "booking" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "payment_session" | "person" | "organization";
99
+ targetType: "other" | "booking" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "organization" | "person" | "payment_session";
100
100
  targetId: string | null;
101
101
  personId: string | null;
102
102
  organizationId: string | null;
@@ -125,7 +125,7 @@ export declare function sendInvoiceNotification(db: PostgresJsDatabase, dispatch
125
125
  id: string;
126
126
  templateId: string | null;
127
127
  templateSlug: string | null;
128
- targetType: "other" | "booking" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "payment_session" | "person" | "organization";
128
+ targetType: "other" | "booking" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "organization" | "person" | "payment_session";
129
129
  targetId: string | null;
130
130
  personId: string | null;
131
131
  organizationId: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"service-deliveries.d.ts","sourceRoot":"","sources":["../src/service-deliveries.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAGjE,OAAO,KAAK,EACV,6BAA6B,EAC7B,mBAAmB,EACnB,4BAA4B,EAC5B,qBAAqB,EACrB,mCAAmC,EACpC,MAAM,qBAAqB,CAAA;AAY5B,wBAAsB,cAAc,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgChG;AAED,wBAAsB,eAAe,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAOvE;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,kBAAkB,EACtB,UAAU,EAAE,mBAAmB,EAC/B,KAAK,EAAE,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAmH7B;AAED,wBAAsB,8BAA8B,CAClD,EAAE,EAAE,kBAAkB,EACtB,UAAU,EAAE,mBAAmB,EAC/B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA8F3C;AAED,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,kBAAkB,EACtB,UAAU,EAAE,mBAAmB,EAC/B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA4FpC"}
1
+ {"version":3,"file":"service-deliveries.d.ts","sourceRoot":"","sources":["../src/service-deliveries.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAGjE,OAAO,KAAK,EACV,6BAA6B,EAC7B,mBAAmB,EACnB,4BAA4B,EAC5B,qBAAqB,EACrB,mCAAmC,EACpC,MAAM,qBAAqB,CAAA;AAyC5B,wBAAsB,cAAc,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgChG;AAED,wBAAsB,eAAe,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAOvE;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,kBAAkB,EACtB,UAAU,EAAE,mBAAmB,EAC/B,KAAK,EAAE,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA8H7B;AAED,wBAAsB,8BAA8B,CAClD,EAAE,EAAE,kBAAkB,EACtB,UAAU,EAAE,mBAAmB,EAC/B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA8F3C;AAED,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,kBAAkB,EACtB,UAAU,EAAE,mBAAmB,EAC/B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA4FpC"}
@@ -2,8 +2,21 @@ import { bookings } from "@voyantjs/bookings/schema";
2
2
  import { invoices, paymentSessions } from "@voyantjs/finance";
3
3
  import { desc, eq, sql } from "drizzle-orm";
4
4
  import { notificationDeliveries } from "./schema.js";
5
- import { buildWhereClause, listBookingNotificationParticipants, NotificationError, paginate, renderNotificationTemplate, resolveReminderRecipient, toTimestamp, } from "./service-shared.js";
5
+ import { buildWhereClause, listBookingNotificationParticipants, NotificationError, paginate, renderNotificationTemplate, resolveReminderRecipient, summarizeNotificationAttachments, toTimestamp, } from "./service-shared.js";
6
6
  import { getTemplateById, getTemplateBySlug } from "./service-templates.js";
7
+ function normalizeAttachments(attachments) {
8
+ if (!attachments || attachments.length === 0) {
9
+ return undefined;
10
+ }
11
+ return attachments.map((attachment) => ({
12
+ filename: attachment.filename,
13
+ ...(attachment.contentBase64 ? { contentBase64: attachment.contentBase64 } : {}),
14
+ ...(attachment.path ? { path: attachment.path } : {}),
15
+ ...(attachment.contentType ? { contentType: attachment.contentType } : {}),
16
+ ...(attachment.disposition ? { disposition: attachment.disposition } : {}),
17
+ ...(attachment.contentId ? { contentId: attachment.contentId } : {}),
18
+ }));
19
+ }
7
20
  export async function listDeliveries(db, query) {
8
21
  const conditions = [];
9
22
  if (query.channel)
@@ -70,6 +83,8 @@ export async function sendNotification(db, dispatcher, input) {
70
83
  const subject = input.subject ?? renderNotificationTemplate(template?.subjectTemplate, data);
71
84
  const html = input.html ?? renderNotificationTemplate(template?.htmlTemplate, data);
72
85
  const text = input.text ?? renderNotificationTemplate(template?.textTemplate, data);
86
+ const attachments = normalizeAttachments(input.attachments);
87
+ const attachmentSummary = summarizeNotificationAttachments(attachments);
73
88
  const [pending] = await db
74
89
  .insert(notificationDeliveries)
75
90
  .values({
@@ -92,7 +107,13 @@ export async function sendNotification(db, dispatcher, input) {
92
107
  htmlBody: html ?? null,
93
108
  textBody: text ?? null,
94
109
  payloadData: data,
95
- metadata: input.metadata ?? null,
110
+ metadata: (input.metadata ?? null) || attachmentSummary.length > 0
111
+ ? {
112
+ ...(input.metadata ?? {}),
113
+ attachmentCount: attachmentSummary.length,
114
+ attachments: attachmentSummary,
115
+ }
116
+ : null,
96
117
  errorMessage: null,
97
118
  scheduledFor: toTimestamp(input.scheduledFor),
98
119
  sentAt: null,
@@ -114,6 +135,7 @@ export async function sendNotification(db, dispatcher, input) {
114
135
  subject: subject ?? undefined,
115
136
  html: html ?? undefined,
116
137
  text: text ?? undefined,
138
+ attachments,
117
139
  })
118
140
  : await dispatcher.sendWith(provider, {
119
141
  to: input.to,
@@ -125,6 +147,7 @@ export async function sendNotification(db, dispatcher, input) {
125
147
  subject: subject ?? undefined,
126
148
  html: html ?? undefined,
127
149
  text: text ?? undefined,
150
+ attachments,
128
151
  });
129
152
  const [sent] = await db
130
153
  .update(notificationDeliveries)
@@ -1 +1 @@
1
- {"version":3,"file":"service-reminders.d.ts","sourceRoot":"","sources":["../src/service-reminders.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAIjE,OAAO,KAAK,EAGV,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACrB,MAAM,qBAAqB,CAAA;AAoM5B,wBAAsB,eAAe,CACnC,EAAE,EAAE,kBAAkB,EACtB,UAAU,EAAE,mBAAmB,EAC/B,KAAK,GAAE,oBAAyB,gCA+CjC"}
1
+ {"version":3,"file":"service-reminders.d.ts","sourceRoot":"","sources":["../src/service-reminders.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAIjE,OAAO,KAAK,EAGV,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACrB,MAAM,qBAAqB,CAAA;AAoW5B,wBAAsB,eAAe,CACnC,EAAE,EAAE,kBAAkB,EACtB,UAAU,EAAE,mBAAmB,EAC/B,KAAK,GAAE,oBAAyB,gCAiFjC"}