@voyantjs/notifications 0.6.8 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.d.ts +27 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -4
- package/dist/liquid.d.ts +5 -0
- package/dist/liquid.d.ts.map +1 -0
- package/dist/liquid.js +156 -0
- package/dist/routes.d.ts +88 -2
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +54 -1
- package/dist/schema.d.ts +1 -1
- package/dist/service-booking-documents.d.ts +128 -0
- package/dist/service-booking-documents.d.ts.map +1 -1
- package/dist/service-booking-documents.js +56 -4
- package/dist/service-deliveries.d.ts.map +1 -1
- package/dist/service-deliveries.js +25 -7
- package/dist/service-reminders.d.ts +1 -1
- package/dist/service-reminders.d.ts.map +1 -1
- package/dist/service-reminders.js +98 -78
- package/dist/service-shared.d.ts +48 -3
- package/dist/service-shared.d.ts.map +1 -1
- package/dist/service-shared.js +300 -40
- package/dist/service-templates.d.ts +2 -2
- package/dist/service.d.ts +119 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +4 -1
- package/dist/tasks/deliver-reminder.d.ts +1 -1
- package/dist/template-authoring.d.ts +23 -0
- package/dist/template-authoring.d.ts.map +1 -0
- package/dist/template-authoring.js +386 -0
- package/dist/validation.d.ts +102 -4
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +57 -0
- package/package.json +8 -7
|
@@ -123,6 +123,134 @@ export declare const bookingDocumentNotificationsService: {
|
|
|
123
123
|
updatedAt: Date;
|
|
124
124
|
};
|
|
125
125
|
}>;
|
|
126
|
+
/**
|
|
127
|
+
* Confirm-and-dispatch — single orchestrated call for the operator flow
|
|
128
|
+
* that wants "list the booking's documents, then send them to the client"
|
|
129
|
+
* to be one action instead of two round-trips.
|
|
130
|
+
*
|
|
131
|
+
* With `sendNotification: false`, the caller gets the bundle back without
|
|
132
|
+
* attempting a send — useful for rendering a preview/checkbox list before
|
|
133
|
+
* the operator confirms. With `sendNotification: true`, the same send
|
|
134
|
+
* guards as `sendBookingDocumentsNotification` apply (no_documents,
|
|
135
|
+
* no_recipient, no_attachments, send_failed); the result keeps the bundle
|
|
136
|
+
* regardless so the UI always has something to show.
|
|
137
|
+
*/
|
|
138
|
+
confirmAndDispatchBooking(db: PostgresJsDatabase, dispatcher: NotificationService, bookingId: string, input: {
|
|
139
|
+
sendNotification?: boolean;
|
|
140
|
+
} & SendBookingDocumentsNotificationInput, runtime?: SendBookingDocumentsRuntimeOptions): Promise<{
|
|
141
|
+
status: "not_found";
|
|
142
|
+
bookingId?: undefined;
|
|
143
|
+
documents?: undefined;
|
|
144
|
+
skipReason?: undefined;
|
|
145
|
+
recipient?: undefined;
|
|
146
|
+
delivery?: undefined;
|
|
147
|
+
} | {
|
|
148
|
+
status: "preview";
|
|
149
|
+
bookingId: string;
|
|
150
|
+
documents: {
|
|
151
|
+
key: string;
|
|
152
|
+
source: "finance" | "legal";
|
|
153
|
+
documentType: "invoice" | "proforma" | "contract";
|
|
154
|
+
bookingId: string;
|
|
155
|
+
name: string;
|
|
156
|
+
createdAt: string;
|
|
157
|
+
contractId?: string | null | undefined;
|
|
158
|
+
invoiceId?: string | null | undefined;
|
|
159
|
+
attachmentId?: string | null | undefined;
|
|
160
|
+
renditionId?: string | null | undefined;
|
|
161
|
+
contractStatus?: string | null | undefined;
|
|
162
|
+
invoiceStatus?: string | null | undefined;
|
|
163
|
+
format?: string | null | undefined;
|
|
164
|
+
mimeType?: string | null | undefined;
|
|
165
|
+
storageKey?: string | null | undefined;
|
|
166
|
+
downloadUrl?: string | null | undefined;
|
|
167
|
+
language?: string | null | undefined;
|
|
168
|
+
metadata?: Record<string, unknown> | null | undefined;
|
|
169
|
+
}[];
|
|
170
|
+
skipReason?: undefined;
|
|
171
|
+
recipient?: undefined;
|
|
172
|
+
delivery?: undefined;
|
|
173
|
+
} | {
|
|
174
|
+
status: "skipped";
|
|
175
|
+
bookingId: string;
|
|
176
|
+
documents: {
|
|
177
|
+
key: string;
|
|
178
|
+
source: "finance" | "legal";
|
|
179
|
+
documentType: "invoice" | "proforma" | "contract";
|
|
180
|
+
bookingId: string;
|
|
181
|
+
name: string;
|
|
182
|
+
createdAt: string;
|
|
183
|
+
contractId?: string | null | undefined;
|
|
184
|
+
invoiceId?: string | null | undefined;
|
|
185
|
+
attachmentId?: string | null | undefined;
|
|
186
|
+
renditionId?: string | null | undefined;
|
|
187
|
+
contractStatus?: string | null | undefined;
|
|
188
|
+
invoiceStatus?: string | null | undefined;
|
|
189
|
+
format?: string | null | undefined;
|
|
190
|
+
mimeType?: string | null | undefined;
|
|
191
|
+
storageKey?: string | null | undefined;
|
|
192
|
+
downloadUrl?: string | null | undefined;
|
|
193
|
+
language?: string | null | undefined;
|
|
194
|
+
metadata?: Record<string, unknown> | null | undefined;
|
|
195
|
+
}[];
|
|
196
|
+
skipReason: "no_documents" | "no_recipient" | "no_attachments" | "send_failed";
|
|
197
|
+
recipient?: undefined;
|
|
198
|
+
delivery?: undefined;
|
|
199
|
+
} | {
|
|
200
|
+
status: "dispatched";
|
|
201
|
+
bookingId: string;
|
|
202
|
+
documents: {
|
|
203
|
+
key: string;
|
|
204
|
+
source: "finance" | "legal";
|
|
205
|
+
documentType: "invoice" | "proforma" | "contract";
|
|
206
|
+
bookingId: string;
|
|
207
|
+
name: string;
|
|
208
|
+
createdAt: string;
|
|
209
|
+
contractId?: string | null | undefined;
|
|
210
|
+
invoiceId?: string | null | undefined;
|
|
211
|
+
attachmentId?: string | null | undefined;
|
|
212
|
+
renditionId?: string | null | undefined;
|
|
213
|
+
contractStatus?: string | null | undefined;
|
|
214
|
+
invoiceStatus?: string | null | undefined;
|
|
215
|
+
format?: string | null | undefined;
|
|
216
|
+
mimeType?: string | null | undefined;
|
|
217
|
+
storageKey?: string | null | undefined;
|
|
218
|
+
downloadUrl?: string | null | undefined;
|
|
219
|
+
language?: string | null | undefined;
|
|
220
|
+
metadata?: Record<string, unknown> | null | undefined;
|
|
221
|
+
}[];
|
|
222
|
+
recipient: string;
|
|
223
|
+
delivery: {
|
|
224
|
+
id: string;
|
|
225
|
+
templateId: string | null;
|
|
226
|
+
templateSlug: string | null;
|
|
227
|
+
targetType: "organization" | "other" | "booking" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "person" | "payment_session";
|
|
228
|
+
targetId: string | null;
|
|
229
|
+
personId: string | null;
|
|
230
|
+
organizationId: string | null;
|
|
231
|
+
bookingId: string | null;
|
|
232
|
+
invoiceId: string | null;
|
|
233
|
+
paymentSessionId: string | null;
|
|
234
|
+
channel: "email" | "sms";
|
|
235
|
+
provider: string;
|
|
236
|
+
providerMessageId: string | null;
|
|
237
|
+
status: "pending" | "cancelled" | "failed" | "sent";
|
|
238
|
+
toAddress: string;
|
|
239
|
+
fromAddress: string | null;
|
|
240
|
+
subject: string | null;
|
|
241
|
+
htmlBody: string | null;
|
|
242
|
+
textBody: string | null;
|
|
243
|
+
payloadData: Record<string, unknown> | null;
|
|
244
|
+
metadata: Record<string, unknown> | null;
|
|
245
|
+
errorMessage: string | null;
|
|
246
|
+
scheduledFor: Date | null;
|
|
247
|
+
sentAt: Date | null;
|
|
248
|
+
failedAt: Date | null;
|
|
249
|
+
createdAt: Date;
|
|
250
|
+
updatedAt: Date;
|
|
251
|
+
};
|
|
252
|
+
skipReason?: undefined;
|
|
253
|
+
}>;
|
|
126
254
|
};
|
|
127
255
|
export { createDefaultAttachmentFromDocument as createDefaultBookingDocumentAttachment };
|
|
128
256
|
//# sourceMappingURL=service-booking-documents.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-booking-documents.d.ts","sourceRoot":"","sources":["../src/service-booking-documents.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAI9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAGjE,OAAO,KAAK,EACV,yBAAyB,EACzB,mBAAmB,EACnB,qCAAqC,EACtC,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"service-booking-documents.d.ts","sourceRoot":"","sources":["../src/service-booking-documents.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAI9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAGjE,OAAO,KAAK,EACV,yBAAyB,EACzB,mBAAmB,EACnB,qCAAqC,EACtC,MAAM,qBAAqB,CAAA;AAM5B,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;IACtD,QAAQ,CAAC,EAAE,QAAQ,CAAA;CACpB;AAED,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,YAAY,EAAE,MAAM,EAAE,CAAA;CACvB;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAoH7C;;;;;;;;;;;OAWG;kCAEG,kBAAkB,cACV,mBAAmB,aACpB,MAAM,SACV;QAAE,gBAAgB,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,qCAAqC,YACpE,kCAAkC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4C9C,CAAA;AAED,OAAO,EAAE,mCAAmC,IAAI,sCAAsC,EAAE,CAAA"}
|
|
@@ -3,7 +3,7 @@ import { invoiceRenditions, invoices } from "@voyantjs/finance/schema";
|
|
|
3
3
|
import { contractAttachments, contracts } from "@voyantjs/legal/contracts";
|
|
4
4
|
import { and, desc, eq, ne, or } from "drizzle-orm";
|
|
5
5
|
import { sendNotification } from "./service-deliveries.js";
|
|
6
|
-
import { listBookingNotificationParticipants, resolveReminderRecipient } from "./service-shared.js";
|
|
6
|
+
import { listBookingNotificationItems, listBookingNotificationParticipants, resolveReminderRecipient, } from "./service-shared.js";
|
|
7
7
|
function getMetadataRecord(value) {
|
|
8
8
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
9
9
|
return null;
|
|
@@ -191,8 +191,11 @@ export const bookingDocumentNotificationsService = {
|
|
|
191
191
|
if (documents.length === 0) {
|
|
192
192
|
return { status: "no_documents" };
|
|
193
193
|
}
|
|
194
|
-
const participants = await
|
|
195
|
-
|
|
194
|
+
const [participants, items] = await Promise.all([
|
|
195
|
+
listBookingNotificationParticipants(db, bookingId),
|
|
196
|
+
listBookingNotificationItems(db, bookingId),
|
|
197
|
+
]);
|
|
198
|
+
const recipient = resolveReminderRecipient(booking, participants);
|
|
196
199
|
const to = input.to ?? recipient?.email ?? null;
|
|
197
200
|
if (!to) {
|
|
198
201
|
return { status: "no_recipient" };
|
|
@@ -225,14 +228,18 @@ export const bookingDocumentNotificationsService = {
|
|
|
225
228
|
startDate: booking.startDate,
|
|
226
229
|
endDate: booking.endDate,
|
|
227
230
|
},
|
|
228
|
-
|
|
231
|
+
traveler: recipient
|
|
229
232
|
? {
|
|
230
233
|
firstName: recipient.firstName,
|
|
231
234
|
lastName: recipient.lastName,
|
|
232
235
|
email: recipient.email,
|
|
236
|
+
participantType: recipient.participantType,
|
|
237
|
+
isPrimary: recipient.isPrimary,
|
|
233
238
|
}
|
|
234
239
|
: null,
|
|
240
|
+
travelers: participants,
|
|
235
241
|
documents,
|
|
242
|
+
items,
|
|
236
243
|
...(input.data ?? {}),
|
|
237
244
|
},
|
|
238
245
|
targetType: "booking",
|
|
@@ -267,5 +274,50 @@ export const bookingDocumentNotificationsService = {
|
|
|
267
274
|
delivery,
|
|
268
275
|
};
|
|
269
276
|
},
|
|
277
|
+
/**
|
|
278
|
+
* Confirm-and-dispatch — single orchestrated call for the operator flow
|
|
279
|
+
* that wants "list the booking's documents, then send them to the client"
|
|
280
|
+
* to be one action instead of two round-trips.
|
|
281
|
+
*
|
|
282
|
+
* With `sendNotification: false`, the caller gets the bundle back without
|
|
283
|
+
* attempting a send — useful for rendering a preview/checkbox list before
|
|
284
|
+
* the operator confirms. With `sendNotification: true`, the same send
|
|
285
|
+
* guards as `sendBookingDocumentsNotification` apply (no_documents,
|
|
286
|
+
* no_recipient, no_attachments, send_failed); the result keeps the bundle
|
|
287
|
+
* regardless so the UI always has something to show.
|
|
288
|
+
*/
|
|
289
|
+
async confirmAndDispatchBooking(db, dispatcher, bookingId, input, runtime = {}) {
|
|
290
|
+
const bundle = await this.listBookingDocumentBundle(db, bookingId);
|
|
291
|
+
if (!bundle)
|
|
292
|
+
return { status: "not_found" };
|
|
293
|
+
const documents = bundle.documents;
|
|
294
|
+
const sendNotification = input.sendNotification ?? true;
|
|
295
|
+
if (!sendNotification) {
|
|
296
|
+
return {
|
|
297
|
+
status: "preview",
|
|
298
|
+
bookingId,
|
|
299
|
+
documents,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
const result = await this.sendBookingDocumentsNotification(db, dispatcher, bookingId, input, runtime);
|
|
303
|
+
if (result.status === "not_found") {
|
|
304
|
+
return { status: "not_found" };
|
|
305
|
+
}
|
|
306
|
+
if (result.status !== "sent") {
|
|
307
|
+
return {
|
|
308
|
+
status: "skipped",
|
|
309
|
+
bookingId,
|
|
310
|
+
documents,
|
|
311
|
+
skipReason: result.status,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
status: "dispatched",
|
|
316
|
+
bookingId: result.bookingId,
|
|
317
|
+
documents: result.documents,
|
|
318
|
+
recipient: result.recipient,
|
|
319
|
+
delivery: result.delivery,
|
|
320
|
+
};
|
|
321
|
+
},
|
|
270
322
|
};
|
|
271
323
|
export { createDefaultAttachmentFromDocument as createDefaultBookingDocumentAttachment };
|
|
@@ -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;AA0C5B,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAuG3C;AAED,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,kBAAkB,EACtB,UAAU,EAAE,mBAAmB,EAC/B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAqGpC"}
|
|
@@ -2,7 +2,7 @@ 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, summarizeNotificationAttachments, toTimestamp, } from "./service-shared.js";
|
|
5
|
+
import { buildWhereClause, listBookingNotificationItems, listBookingNotificationParticipants, NotificationError, paginate, renderNotificationTemplate, resolveReminderRecipient, summarizeNotificationAttachments, toTimestamp, } from "./service-shared.js";
|
|
6
6
|
import { getTemplateById, getTemplateBySlug } from "./service-templates.js";
|
|
7
7
|
function normalizeAttachments(attachments) {
|
|
8
8
|
if (!attachments || attachments.length === 0) {
|
|
@@ -194,8 +194,13 @@ export async function sendPaymentSessionNotification(db, dispatcher, sessionId,
|
|
|
194
194
|
? ((await db.select().from(invoices).where(eq(invoices.id, session.invoiceId)).limit(1))[0] ??
|
|
195
195
|
null)
|
|
196
196
|
: null;
|
|
197
|
-
const participants = booking
|
|
198
|
-
|
|
197
|
+
const [participants, items] = booking
|
|
198
|
+
? await Promise.all([
|
|
199
|
+
listBookingNotificationParticipants(db, booking.id),
|
|
200
|
+
listBookingNotificationItems(db, booking.id),
|
|
201
|
+
])
|
|
202
|
+
: [[], []];
|
|
203
|
+
const recipient = resolveReminderRecipient(booking ?? null, participants);
|
|
199
204
|
const to = input.to ?? session.payerEmail ?? recipient?.email ?? null;
|
|
200
205
|
if (!to) {
|
|
201
206
|
throw new NotificationError("No recipient available for payment session notification");
|
|
@@ -247,13 +252,17 @@ export async function sendPaymentSessionNotification(db, dispatcher, sessionId,
|
|
|
247
252
|
dueDate: invoice.dueDate,
|
|
248
253
|
}
|
|
249
254
|
: null,
|
|
250
|
-
|
|
255
|
+
traveler: recipient
|
|
251
256
|
? {
|
|
252
257
|
firstName: recipient.firstName,
|
|
253
258
|
lastName: recipient.lastName,
|
|
254
259
|
email: recipient.email,
|
|
260
|
+
participantType: recipient.participantType,
|
|
261
|
+
isPrimary: recipient.isPrimary,
|
|
255
262
|
}
|
|
256
263
|
: null,
|
|
264
|
+
travelers: participants,
|
|
265
|
+
items,
|
|
257
266
|
...(input.data ?? {}),
|
|
258
267
|
},
|
|
259
268
|
targetType: "payment_session",
|
|
@@ -277,8 +286,13 @@ export async function sendInvoiceNotification(db, dispatcher, invoiceId, input)
|
|
|
277
286
|
.from(bookings)
|
|
278
287
|
.where(eq(bookings.id, invoice.bookingId))
|
|
279
288
|
.limit(1);
|
|
280
|
-
const participants = booking
|
|
281
|
-
|
|
289
|
+
const [participants, items] = booking
|
|
290
|
+
? await Promise.all([
|
|
291
|
+
listBookingNotificationParticipants(db, booking.id),
|
|
292
|
+
listBookingNotificationItems(db, booking.id),
|
|
293
|
+
])
|
|
294
|
+
: [[], []];
|
|
295
|
+
const recipient = resolveReminderRecipient(booking ?? null, participants);
|
|
282
296
|
const [latestSession] = await db
|
|
283
297
|
.select()
|
|
284
298
|
.from(paymentSessions)
|
|
@@ -335,13 +349,17 @@ export async function sendInvoiceNotification(db, dispatcher, invoiceId, input)
|
|
|
335
349
|
currency: latestSession.currency,
|
|
336
350
|
}
|
|
337
351
|
: null,
|
|
338
|
-
|
|
352
|
+
traveler: recipient
|
|
339
353
|
? {
|
|
340
354
|
firstName: recipient.firstName,
|
|
341
355
|
lastName: recipient.lastName,
|
|
342
356
|
email: recipient.email,
|
|
357
|
+
participantType: recipient.participantType,
|
|
358
|
+
isPrimary: recipient.isPrimary,
|
|
343
359
|
}
|
|
344
360
|
: null,
|
|
361
|
+
travelers: participants,
|
|
362
|
+
items,
|
|
345
363
|
...(input.data ?? {}),
|
|
346
364
|
},
|
|
347
365
|
targetType: "invoice",
|
|
@@ -17,7 +17,7 @@ export declare function deliverReminderRun(db: PostgresJsDatabase, dispatcher: N
|
|
|
17
17
|
organizationId: string | null;
|
|
18
18
|
paymentSessionId: string | null;
|
|
19
19
|
notificationDeliveryId: string | null;
|
|
20
|
-
status: "failed" | "sent" | "processing" | "
|
|
20
|
+
status: "failed" | "sent" | "processing" | "skipped" | "queued";
|
|
21
21
|
recipient: string | null;
|
|
22
22
|
scheduledFor: Date;
|
|
23
23
|
processedAt: Date;
|
|
@@ -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,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,mBAAmB,EACnB,oBAAoB,EACrB,MAAM,qBAAqB,CAAA;AAW5B,KAAK,wBAAwB,GAAG,CAAC,KAAK,EAAE;IAAE,aAAa,EAAE,MAAM,CAAA;CAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AAoyBnF,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,oBAAoB,YAAK,EAChC,eAAe,EAAE,wBAAwB,gCAoF1C;AAED,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,kBAAkB,EACtB,UAAU,EAAE,mBAAmB,EAC/B,KAAK,EAAE;IAAE,aAAa,EAAE,MAAM,CAAA;CAAE;;;;;;;;;;;;;;;;;;;UAkDjC;AAED,wBAAsB,eAAe,CACnC,EAAE,EAAE,kBAAkB,EACtB,UAAU,EAAE,mBAAmB,EAC/B,KAAK,GAAE,oBAAyB,gCA4EjC"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { bookings, bookingTravelers } from "@voyantjs/bookings/schema";
|
|
2
2
|
import { bookingPaymentSchedules, invoices } from "@voyantjs/finance";
|
|
3
3
|
import { and, desc, eq, gt, or } from "drizzle-orm";
|
|
4
4
|
import { notificationReminderRules, notificationReminderRuns } from "./schema.js";
|
|
5
5
|
import { sendInvoiceNotification, sendNotification } from "./service-deliveries.js";
|
|
6
|
-
import { addUtcDays, buildReminderDedupeKey, resolveReminderRecipient, startOfUtcDay, toDateString, toTimestamp, } from "./service-shared.js";
|
|
6
|
+
import { addUtcDays, buildReminderDedupeKey, listBookingNotificationItems, resolveReminderRecipient, startOfUtcDay, toDateString, toTimestamp, } from "./service-shared.js";
|
|
7
7
|
function buildReminderSweepSummary() {
|
|
8
8
|
return {
|
|
9
9
|
processed: 0,
|
|
@@ -156,21 +156,23 @@ async function queueBookingPaymentScheduleReminder(db, enqueueDelivery, rule, sc
|
|
|
156
156
|
if (!booking) {
|
|
157
157
|
return markReminderRunSkipped(db, reminderRun.id, now, "Booking not found for payment schedule");
|
|
158
158
|
}
|
|
159
|
-
const participants = await
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
159
|
+
const [participants] = await Promise.all([
|
|
160
|
+
db
|
|
161
|
+
.select({
|
|
162
|
+
id: bookingTravelers.id,
|
|
163
|
+
firstName: bookingTravelers.firstName,
|
|
164
|
+
lastName: bookingTravelers.lastName,
|
|
165
|
+
email: bookingTravelers.email,
|
|
166
|
+
participantType: bookingTravelers.participantType,
|
|
167
|
+
isPrimary: bookingTravelers.isPrimary,
|
|
168
|
+
})
|
|
169
|
+
.from(bookingTravelers)
|
|
170
|
+
.where(eq(bookingTravelers.bookingId, booking.id))
|
|
171
|
+
.orderBy(desc(bookingTravelers.isPrimary), bookingTravelers.createdAt),
|
|
172
|
+
]);
|
|
173
|
+
const recipient = resolveReminderRecipient(booking, participants);
|
|
172
174
|
if (!recipient?.email) {
|
|
173
|
-
return markReminderRunSkipped(db, reminderRun.id, now, "No
|
|
175
|
+
return markReminderRunSkipped(db, reminderRun.id, now, "No traveler email available for booking payment reminder");
|
|
174
176
|
}
|
|
175
177
|
return enqueueReminderRun(db, enqueueDelivery, { ...reminderRun, recipient: recipient.email }, now);
|
|
176
178
|
}
|
|
@@ -225,21 +227,23 @@ async function queueInvoiceReminder(db, enqueueDelivery, rule, invoice, now) {
|
|
|
225
227
|
if (!booking) {
|
|
226
228
|
return markReminderRunSkipped(db, reminderRun.id, now, "Booking not found for invoice reminder");
|
|
227
229
|
}
|
|
228
|
-
const participants = await
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
230
|
+
const [participants] = await Promise.all([
|
|
231
|
+
db
|
|
232
|
+
.select({
|
|
233
|
+
id: bookingTravelers.id,
|
|
234
|
+
firstName: bookingTravelers.firstName,
|
|
235
|
+
lastName: bookingTravelers.lastName,
|
|
236
|
+
email: bookingTravelers.email,
|
|
237
|
+
participantType: bookingTravelers.participantType,
|
|
238
|
+
isPrimary: bookingTravelers.isPrimary,
|
|
239
|
+
})
|
|
240
|
+
.from(bookingTravelers)
|
|
241
|
+
.where(eq(bookingTravelers.bookingId, booking.id))
|
|
242
|
+
.orderBy(desc(bookingTravelers.isPrimary), bookingTravelers.createdAt),
|
|
243
|
+
]);
|
|
244
|
+
const recipient = resolveReminderRecipient(booking, participants);
|
|
241
245
|
if (!recipient?.email) {
|
|
242
|
-
return markReminderRunSkipped(db, reminderRun.id, now, "No
|
|
246
|
+
return markReminderRunSkipped(db, reminderRun.id, now, "No traveler email available for invoice reminder");
|
|
243
247
|
}
|
|
244
248
|
return enqueueReminderRun(db, enqueueDelivery, { ...reminderRun, recipient: recipient.email }, now);
|
|
245
249
|
}
|
|
@@ -285,19 +289,22 @@ async function sendBookingPaymentScheduleReminder(db, dispatcher, rule, schedule
|
|
|
285
289
|
.returning();
|
|
286
290
|
return run ?? null;
|
|
287
291
|
}
|
|
288
|
-
const participants = await
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
292
|
+
const [participants, items] = await Promise.all([
|
|
293
|
+
db
|
|
294
|
+
.select({
|
|
295
|
+
id: bookingTravelers.id,
|
|
296
|
+
firstName: bookingTravelers.firstName,
|
|
297
|
+
lastName: bookingTravelers.lastName,
|
|
298
|
+
email: bookingTravelers.email,
|
|
299
|
+
participantType: bookingTravelers.participantType,
|
|
300
|
+
isPrimary: bookingTravelers.isPrimary,
|
|
301
|
+
})
|
|
302
|
+
.from(bookingTravelers)
|
|
303
|
+
.where(eq(bookingTravelers.bookingId, booking.id))
|
|
304
|
+
.orderBy(desc(bookingTravelers.isPrimary), bookingTravelers.createdAt),
|
|
305
|
+
listBookingNotificationItems(db, booking.id),
|
|
306
|
+
]);
|
|
307
|
+
const recipient = resolveReminderRecipient(booking, participants);
|
|
301
308
|
const [processingRun] = await db
|
|
302
309
|
.insert(notificationReminderRuns)
|
|
303
310
|
.values({
|
|
@@ -327,7 +334,7 @@ async function sendBookingPaymentScheduleReminder(db, dispatcher, rule, schedule
|
|
|
327
334
|
return null;
|
|
328
335
|
}
|
|
329
336
|
if (!recipient?.email) {
|
|
330
|
-
return markReminderRunSkipped(db, processingRun.id, now, "No
|
|
337
|
+
return markReminderRunSkipped(db, processingRun.id, now, "No traveler email available for booking payment reminder");
|
|
331
338
|
}
|
|
332
339
|
try {
|
|
333
340
|
const delivery = await sendNotification(db, dispatcher, {
|
|
@@ -344,11 +351,14 @@ async function sendBookingPaymentScheduleReminder(db, dispatcher, rule, schedule
|
|
|
344
351
|
currency: schedule.currency,
|
|
345
352
|
scheduleType: schedule.scheduleType,
|
|
346
353
|
reminderOffsetDays: rule.relativeDaysFromDueDate,
|
|
347
|
-
|
|
354
|
+
traveler: {
|
|
348
355
|
firstName: recipient.firstName,
|
|
349
356
|
lastName: recipient.lastName,
|
|
350
357
|
email: recipient.email,
|
|
358
|
+
participantType: recipient.participantType,
|
|
359
|
+
isPrimary: recipient.isPrimary,
|
|
351
360
|
},
|
|
361
|
+
travelers: participants,
|
|
352
362
|
booking: {
|
|
353
363
|
id: booking.id,
|
|
354
364
|
bookingNumber: booking.bookingNumber,
|
|
@@ -365,6 +375,7 @@ async function sendBookingPaymentScheduleReminder(db, dispatcher, rule, schedule
|
|
|
365
375
|
scheduleType: schedule.scheduleType,
|
|
366
376
|
status: schedule.status,
|
|
367
377
|
},
|
|
378
|
+
items,
|
|
368
379
|
},
|
|
369
380
|
targetType: "booking_payment_schedule",
|
|
370
381
|
targetId: schedule.id,
|
|
@@ -428,19 +439,21 @@ async function sendInvoiceReminder(db, dispatcher, rule, invoice, now) {
|
|
|
428
439
|
.returning();
|
|
429
440
|
return run ?? null;
|
|
430
441
|
}
|
|
431
|
-
const participants = await
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
442
|
+
const [participants] = await Promise.all([
|
|
443
|
+
db
|
|
444
|
+
.select({
|
|
445
|
+
id: bookingTravelers.id,
|
|
446
|
+
firstName: bookingTravelers.firstName,
|
|
447
|
+
lastName: bookingTravelers.lastName,
|
|
448
|
+
email: bookingTravelers.email,
|
|
449
|
+
participantType: bookingTravelers.participantType,
|
|
450
|
+
isPrimary: bookingTravelers.isPrimary,
|
|
451
|
+
})
|
|
452
|
+
.from(bookingTravelers)
|
|
453
|
+
.where(eq(bookingTravelers.bookingId, booking.id))
|
|
454
|
+
.orderBy(desc(bookingTravelers.isPrimary), bookingTravelers.createdAt),
|
|
455
|
+
]);
|
|
456
|
+
const recipient = resolveReminderRecipient(booking, participants);
|
|
444
457
|
const [processingRun] = await db
|
|
445
458
|
.insert(notificationReminderRuns)
|
|
446
459
|
.values({
|
|
@@ -472,7 +485,7 @@ async function sendInvoiceReminder(db, dispatcher, rule, invoice, now) {
|
|
|
472
485
|
return null;
|
|
473
486
|
}
|
|
474
487
|
if (!recipient?.email) {
|
|
475
|
-
return markReminderRunSkipped(db, processingRun.id, now, "No
|
|
488
|
+
return markReminderRunSkipped(db, processingRun.id, now, "No traveler email available for invoice reminder");
|
|
476
489
|
}
|
|
477
490
|
try {
|
|
478
491
|
const delivery = await sendInvoiceNotification(db, dispatcher, invoice.id, {
|
|
@@ -515,23 +528,26 @@ async function sendQueuedBookingPaymentScheduleReminder(db, dispatcher, run, rul
|
|
|
515
528
|
if (!booking) {
|
|
516
529
|
return markReminderRunSkipped(db, run.id, now, "Booking not found for payment schedule");
|
|
517
530
|
}
|
|
518
|
-
const participants = await
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
531
|
+
const [participants, items] = await Promise.all([
|
|
532
|
+
db
|
|
533
|
+
.select({
|
|
534
|
+
id: bookingTravelers.id,
|
|
535
|
+
firstName: bookingTravelers.firstName,
|
|
536
|
+
lastName: bookingTravelers.lastName,
|
|
537
|
+
email: bookingTravelers.email,
|
|
538
|
+
participantType: bookingTravelers.participantType,
|
|
539
|
+
isPrimary: bookingTravelers.isPrimary,
|
|
540
|
+
})
|
|
541
|
+
.from(bookingTravelers)
|
|
542
|
+
.where(eq(bookingTravelers.bookingId, booking.id))
|
|
543
|
+
.orderBy(desc(bookingTravelers.isPrimary), bookingTravelers.createdAt),
|
|
544
|
+
listBookingNotificationItems(db, booking.id),
|
|
545
|
+
]);
|
|
546
|
+
const fallbackRecipient = resolveReminderRecipient(booking, participants);
|
|
547
|
+
const traveler = participants.find((entry) => entry.email === run.recipient) ?? fallbackRecipient ?? null;
|
|
548
|
+
const recipientEmail = run.recipient ?? traveler?.email ?? null;
|
|
533
549
|
if (!recipientEmail) {
|
|
534
|
-
return markReminderRunSkipped(db, run.id, now, "No
|
|
550
|
+
return markReminderRunSkipped(db, run.id, now, "No traveler email available for booking payment reminder");
|
|
535
551
|
}
|
|
536
552
|
try {
|
|
537
553
|
const delivery = await sendNotification(db, dispatcher, {
|
|
@@ -548,13 +564,16 @@ async function sendQueuedBookingPaymentScheduleReminder(db, dispatcher, run, rul
|
|
|
548
564
|
currency: schedule.currency,
|
|
549
565
|
scheduleType: schedule.scheduleType,
|
|
550
566
|
reminderOffsetDays: rule.relativeDaysFromDueDate,
|
|
551
|
-
|
|
567
|
+
traveler: traveler
|
|
552
568
|
? {
|
|
553
|
-
firstName:
|
|
554
|
-
lastName:
|
|
569
|
+
firstName: traveler.firstName,
|
|
570
|
+
lastName: traveler.lastName,
|
|
555
571
|
email: recipientEmail,
|
|
572
|
+
participantType: traveler.participantType,
|
|
573
|
+
isPrimary: traveler.isPrimary,
|
|
556
574
|
}
|
|
557
575
|
: null,
|
|
576
|
+
travelers: participants,
|
|
558
577
|
booking: {
|
|
559
578
|
id: booking.id,
|
|
560
579
|
bookingNumber: booking.bookingNumber,
|
|
@@ -571,6 +590,7 @@ async function sendQueuedBookingPaymentScheduleReminder(db, dispatcher, run, rul
|
|
|
571
590
|
scheduleType: schedule.scheduleType,
|
|
572
591
|
status: schedule.status,
|
|
573
592
|
},
|
|
593
|
+
items,
|
|
574
594
|
},
|
|
575
595
|
targetType: "booking_payment_schedule",
|
|
576
596
|
targetId: schedule.id,
|