@voyantjs/notifications 0.3.1 → 0.4.1

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.
@@ -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"}
@@ -1,8 +1,8 @@
1
1
  import { bookingParticipants, bookings } from "@voyantjs/bookings/schema";
2
- import { bookingPaymentSchedules } from "@voyantjs/finance";
3
- import { and, desc, eq, or } from "drizzle-orm";
2
+ import { bookingPaymentSchedules, invoices } from "@voyantjs/finance";
3
+ import { and, desc, eq, gt, or } from "drizzle-orm";
4
4
  import { notificationReminderRules, notificationReminderRuns } from "./schema.js";
5
- import { sendNotification } from "./service-deliveries.js";
5
+ import { sendInvoiceNotification, sendNotification } from "./service-deliveries.js";
6
6
  import { addUtcDays, buildReminderDedupeKey, resolveReminderRecipient, startOfUtcDay, toDateString, toTimestamp, } from "./service-shared.js";
7
7
  async function sendBookingPaymentScheduleReminder(db, dispatcher, rule, schedule, now) {
8
8
  const runDate = toDateString(startOfUtcDay(now));
@@ -176,6 +176,151 @@ async function sendBookingPaymentScheduleReminder(db, dispatcher, rule, schedule
176
176
  return run ?? null;
177
177
  }
178
178
  }
179
+ async function sendInvoiceReminder(db, dispatcher, rule, invoice, now) {
180
+ const runDate = toDateString(startOfUtcDay(now));
181
+ const dedupeKey = buildReminderDedupeKey(rule.id, invoice.id, runDate);
182
+ const [existingRun] = await db
183
+ .select({ id: notificationReminderRuns.id })
184
+ .from(notificationReminderRuns)
185
+ .where(eq(notificationReminderRuns.dedupeKey, dedupeKey))
186
+ .limit(1);
187
+ if (existingRun) {
188
+ return null;
189
+ }
190
+ const [booking] = await db
191
+ .select()
192
+ .from(bookings)
193
+ .where(eq(bookings.id, invoice.bookingId))
194
+ .limit(1);
195
+ if (!booking) {
196
+ const [run] = await db
197
+ .insert(notificationReminderRuns)
198
+ .values({
199
+ reminderRuleId: rule.id,
200
+ targetType: "invoice",
201
+ targetId: invoice.id,
202
+ dedupeKey,
203
+ bookingId: invoice.bookingId,
204
+ personId: invoice.personId ?? null,
205
+ organizationId: invoice.organizationId ?? null,
206
+ paymentSessionId: null,
207
+ notificationDeliveryId: null,
208
+ status: "skipped",
209
+ recipient: null,
210
+ scheduledFor: now,
211
+ processedAt: now,
212
+ errorMessage: "Booking not found for invoice reminder",
213
+ metadata: {
214
+ dueDate: invoice.dueDate,
215
+ relativeDaysFromDueDate: rule.relativeDaysFromDueDate,
216
+ invoiceNumber: invoice.invoiceNumber,
217
+ invoiceType: invoice.invoiceType,
218
+ },
219
+ })
220
+ .returning();
221
+ return run ?? null;
222
+ }
223
+ const participants = await db
224
+ .select({
225
+ id: bookingParticipants.id,
226
+ firstName: bookingParticipants.firstName,
227
+ lastName: bookingParticipants.lastName,
228
+ email: bookingParticipants.email,
229
+ participantType: bookingParticipants.participantType,
230
+ isPrimary: bookingParticipants.isPrimary,
231
+ })
232
+ .from(bookingParticipants)
233
+ .where(eq(bookingParticipants.bookingId, booking.id))
234
+ .orderBy(desc(bookingParticipants.isPrimary), bookingParticipants.createdAt);
235
+ const recipient = resolveReminderRecipient(participants);
236
+ const [processingRun] = await db
237
+ .insert(notificationReminderRuns)
238
+ .values({
239
+ reminderRuleId: rule.id,
240
+ targetType: "invoice",
241
+ targetId: invoice.id,
242
+ dedupeKey,
243
+ bookingId: booking.id,
244
+ personId: invoice.personId ?? booking.personId ?? null,
245
+ organizationId: invoice.organizationId ?? booking.organizationId ?? null,
246
+ paymentSessionId: null,
247
+ notificationDeliveryId: null,
248
+ status: "processing",
249
+ recipient: recipient?.email ?? null,
250
+ scheduledFor: now,
251
+ processedAt: now,
252
+ errorMessage: null,
253
+ metadata: {
254
+ dueDate: invoice.dueDate,
255
+ relativeDaysFromDueDate: rule.relativeDaysFromDueDate,
256
+ bookingNumber: booking.bookingNumber,
257
+ invoiceNumber: invoice.invoiceNumber,
258
+ invoiceType: invoice.invoiceType,
259
+ },
260
+ })
261
+ .onConflictDoNothing({ target: notificationReminderRuns.dedupeKey })
262
+ .returning();
263
+ if (!processingRun) {
264
+ return null;
265
+ }
266
+ if (!recipient?.email) {
267
+ const [run] = await db
268
+ .update(notificationReminderRuns)
269
+ .set({
270
+ status: "skipped",
271
+ errorMessage: "No participant email available for invoice reminder",
272
+ processedAt: now,
273
+ updatedAt: now,
274
+ })
275
+ .where(eq(notificationReminderRuns.id, processingRun.id))
276
+ .returning();
277
+ return run ?? null;
278
+ }
279
+ try {
280
+ const delivery = await sendInvoiceNotification(db, dispatcher, invoice.id, {
281
+ templateId: rule.templateId ?? null,
282
+ templateSlug: rule.templateSlug ?? null,
283
+ channel: rule.channel,
284
+ provider: rule.provider ?? null,
285
+ to: recipient.email,
286
+ data: {
287
+ reminderOffsetDays: rule.relativeDaysFromDueDate,
288
+ reminderRunId: processingRun.id,
289
+ },
290
+ metadata: {
291
+ reminderRuleId: rule.id,
292
+ reminderRunId: processingRun.id,
293
+ },
294
+ scheduledFor: now.toISOString(),
295
+ });
296
+ const [run] = await db
297
+ .update(notificationReminderRuns)
298
+ .set({
299
+ notificationDeliveryId: delivery?.id ?? null,
300
+ status: "sent",
301
+ processedAt: new Date(),
302
+ updatedAt: new Date(),
303
+ errorMessage: null,
304
+ })
305
+ .where(eq(notificationReminderRuns.id, processingRun.id))
306
+ .returning();
307
+ return run ?? null;
308
+ }
309
+ catch (error) {
310
+ const message = error instanceof Error ? error.message : "Invoice reminder failed";
311
+ const [run] = await db
312
+ .update(notificationReminderRuns)
313
+ .set({
314
+ status: "failed",
315
+ errorMessage: message,
316
+ processedAt: new Date(),
317
+ updatedAt: new Date(),
318
+ })
319
+ .where(eq(notificationReminderRuns.id, processingRun.id))
320
+ .returning();
321
+ return run ?? null;
322
+ }
323
+ }
179
324
  export async function runDueReminders(db, dispatcher, input = {}) {
180
325
  const now = toTimestamp(input.now) ?? new Date();
181
326
  const today = startOfUtcDay(now);
@@ -192,23 +337,46 @@ export async function runDueReminders(db, dispatcher, input = {}) {
192
337
  };
193
338
  for (const rule of activeRules) {
194
339
  const matchingDueDate = toDateString(addUtcDays(today, -rule.relativeDaysFromDueDate));
195
- const schedules = await db
196
- .select()
197
- .from(bookingPaymentSchedules)
198
- .where(and(eq(bookingPaymentSchedules.dueDate, matchingDueDate), or(eq(bookingPaymentSchedules.status, "pending"), eq(bookingPaymentSchedules.status, "due"))))
199
- .orderBy(bookingPaymentSchedules.createdAt);
200
- for (const schedule of schedules) {
201
- const run = await sendBookingPaymentScheduleReminder(db, dispatcher, rule, schedule, now);
202
- if (!run) {
203
- continue;
340
+ if (rule.targetType === "booking_payment_schedule") {
341
+ const schedules = await db
342
+ .select()
343
+ .from(bookingPaymentSchedules)
344
+ .where(and(eq(bookingPaymentSchedules.dueDate, matchingDueDate), or(eq(bookingPaymentSchedules.status, "pending"), eq(bookingPaymentSchedules.status, "due"))))
345
+ .orderBy(bookingPaymentSchedules.createdAt);
346
+ for (const schedule of schedules) {
347
+ const run = await sendBookingPaymentScheduleReminder(db, dispatcher, rule, schedule, now);
348
+ if (!run) {
349
+ continue;
350
+ }
351
+ summary.processed += 1;
352
+ if (run.status === "sent")
353
+ summary.sent += 1;
354
+ if (run.status === "skipped")
355
+ summary.skipped += 1;
356
+ if (run.status === "failed")
357
+ summary.failed += 1;
358
+ }
359
+ continue;
360
+ }
361
+ if (rule.targetType === "invoice") {
362
+ const dueInvoices = await db
363
+ .select()
364
+ .from(invoices)
365
+ .where(and(eq(invoices.dueDate, matchingDueDate), gt(invoices.balanceDueCents, 0), or(eq(invoices.invoiceType, "invoice"), eq(invoices.invoiceType, "proforma")), or(eq(invoices.status, "sent"), eq(invoices.status, "partially_paid"), eq(invoices.status, "overdue"))))
366
+ .orderBy(invoices.createdAt);
367
+ for (const invoice of dueInvoices) {
368
+ const run = await sendInvoiceReminder(db, dispatcher, rule, invoice, now);
369
+ if (!run) {
370
+ continue;
371
+ }
372
+ summary.processed += 1;
373
+ if (run.status === "sent")
374
+ summary.sent += 1;
375
+ if (run.status === "skipped")
376
+ summary.skipped += 1;
377
+ if (run.status === "failed")
378
+ summary.failed += 1;
204
379
  }
205
- summary.processed += 1;
206
- if (run.status === "sent")
207
- summary.sent += 1;
208
- if (run.status === "skipped")
209
- summary.skipped += 1;
210
- if (run.status === "failed")
211
- summary.failed += 1;
212
380
  }
213
381
  }
214
382
  return summary;
@@ -3,8 +3,8 @@ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
3
3
  import type { SQLWrapper } from "drizzle-orm/sql";
4
4
  import type { z } from "zod";
5
5
  import type { notificationReminderRules } from "./schema.js";
6
- import type { NotificationChannel, NotificationPayload, NotificationProvider, NotificationResult } from "./types.js";
7
- import type { insertNotificationReminderRuleSchema, insertNotificationTemplateSchema, notificationDeliveryListQuerySchema, notificationReminderRuleListQuerySchema, notificationReminderRunListQuerySchema, notificationTemplateListQuerySchema, runDueRemindersSchema, sendInvoiceNotificationSchema, sendNotificationSchema, sendPaymentSessionNotificationSchema, updateNotificationReminderRuleSchema, updateNotificationTemplateSchema } from "./validation.js";
6
+ import type { NotificationAttachment, NotificationChannel, NotificationPayload, NotificationProvider, NotificationResult } from "./types.js";
7
+ import type { bookingDocumentBundleItemSchema, insertNotificationReminderRuleSchema, insertNotificationTemplateSchema, notificationDeliveryListQuerySchema, notificationReminderRuleListQuerySchema, notificationReminderRunListQuerySchema, notificationTemplateListQuerySchema, runDueRemindersSchema, sendBookingDocumentsNotificationSchema, sendInvoiceNotificationSchema, sendNotificationSchema, sendPaymentSessionNotificationSchema, updateNotificationReminderRuleSchema, updateNotificationTemplateSchema } from "./validation.js";
8
8
  export type NotificationTemplateListQuery = z.infer<typeof notificationTemplateListQuerySchema>;
9
9
  export type NotificationDeliveryListQuery = z.infer<typeof notificationDeliveryListQuerySchema>;
10
10
  export type CreateNotificationTemplateInput = z.infer<typeof insertNotificationTemplateSchema>;
@@ -17,6 +17,8 @@ export type UpdateNotificationReminderRuleInput = z.infer<typeof updateNotificat
17
17
  export type RunDueRemindersInput = z.infer<typeof runDueRemindersSchema>;
18
18
  export type SendPaymentSessionNotificationInput = z.infer<typeof sendPaymentSessionNotificationSchema>;
19
19
  export type SendInvoiceNotificationInput = z.infer<typeof sendInvoiceNotificationSchema>;
20
+ export type SendBookingDocumentsNotificationInput = z.infer<typeof sendBookingDocumentsNotificationSchema>;
21
+ export type BookingDocumentBundleItem = z.infer<typeof bookingDocumentBundleItemSchema>;
20
22
  export type ReminderSweepResult = {
21
23
  processed: number;
22
24
  sent: number;
@@ -34,6 +36,13 @@ export interface NotificationService {
34
36
  getProvider(channel: NotificationChannel): NotificationProvider | undefined;
35
37
  }
36
38
  export declare function createNotificationService(providers: ReadonlyArray<NotificationProvider>): NotificationService;
39
+ export declare function summarizeNotificationAttachments(attachments: ReadonlyArray<NotificationAttachment> | null | undefined): {
40
+ filename: string;
41
+ path: string | null;
42
+ contentType: string | null;
43
+ disposition: "attachment" | "inline" | null;
44
+ contentId: string | null;
45
+ }[];
37
46
  export declare function renderNotificationTemplate(template: string | null | undefined, data: Record<string, unknown>): string | null;
38
47
  export declare function toTimestamp(value?: string | null): Date | null;
39
48
  export declare function startOfUtcDay(value: Date): Date;
@@ -1 +1 @@
1
- {"version":3,"file":"service-shared.d.ts","sourceRoot":"","sources":["../src/service-shared.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"service-shared.d.ts","sourceRoot":"","sources":["../src/service-shared.ts"],"names":[],"mappings":"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"}