@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.
- package/README.md +22 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/providers/resend.d.ts.map +1 -1
- package/dist/providers/resend.js +8 -0
- package/dist/routes.d.ts +128 -10
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +42 -1
- package/dist/schema.d.ts +6 -6
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +1 -0
- package/dist/service-booking-documents.d.ts +119 -0
- package/dist/service-booking-documents.d.ts.map +1 -0
- package/dist/service-booking-documents.js +261 -0
- package/dist/service-deliveries.d.ts +5 -5
- package/dist/service-deliveries.d.ts.map +1 -1
- package/dist/service-deliveries.js +25 -2
- package/dist/service-reminders.d.ts.map +1 -1
- package/dist/service-reminders.js +187 -19
- package/dist/service-shared.d.ts +11 -2
- package/dist/service-shared.d.ts.map +1 -1
- package/dist/service-shared.js +12 -0
- package/dist/service-templates.d.ts +5 -5
- package/dist/service.d.ts +111 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +6 -1
- package/dist/types.d.ts +22 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts +178 -6
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +64 -1
- package/package.json +7 -6
|
@@ -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" | "
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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;
|
|
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;
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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;
|
package/dist/service-shared.d.ts
CHANGED
|
@@ -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":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAA;AAEhE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAE5B,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAA;AAC5D,OAAO,KAAK,EACV,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EACnB,MAAM,YAAY,CAAA;AACnB,OAAO,KAAK,EACV,oCAAoC,EACpC,gCAAgC,EAChC,mCAAmC,EACnC,uCAAuC,EACvC,sCAAsC,EACtC,mCAAmC,EACnC,qBAAqB,EACrB,6BAA6B,EAC7B,sBAAsB,EACtB,oCAAoC,EACpC,oCAAoC,EACpC,gCAAgC,EACjC,MAAM,iBAAiB,CAAA;AAExB,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mCAAmC,CAAC,CAAA;AAC/F,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mCAAmC,CAAC,CAAA;AAC/F,MAAM,MAAM,+BAA+B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gCAAgC,CAAC,CAAA;AAC9F,MAAM,MAAM,+BAA+B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gCAAgC,CAAC,CAAA;AAC9F,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAA;AAC1E,MAAM,MAAM,iCAAiC,GAAG,CAAC,CAAC,KAAK,CACrD,OAAO,uCAAuC,CAC/C,CAAA;AACD,MAAM,MAAM,gCAAgC,GAAG,CAAC,CAAC,KAAK,CACpD,OAAO,sCAAsC,CAC9C,CAAA;AACD,MAAM,MAAM,mCAAmC,GAAG,CAAC,CAAC,KAAK,CACvD,OAAO,oCAAoC,CAC5C,CAAA;AACD,MAAM,MAAM,mCAAmC,GAAG,CAAC,CAAC,KAAK,CACvD,OAAO,oCAAoC,CAC5C,CAAA;AACD,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAA;AACxE,MAAM,MAAM,mCAAmC,GAAG,CAAC,CAAC,KAAK,CACvD,OAAO,oCAAoC,CAC5C,CAAA;AACD,MAAM,MAAM,4BAA4B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"service-shared.d.ts","sourceRoot":"","sources":["../src/service-shared.ts"],"names":[],"mappings":"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"}
|