@voyantjs/notifications 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/providers/local.d.ts.map +1 -1
- package/dist/providers/local.js +0 -1
- package/dist/service-deliveries.d.ts +153 -0
- package/dist/service-deliveries.d.ts.map +1 -0
- package/dist/service-deliveries.js +334 -0
- package/dist/service-reminders.d.ts +4 -0
- package/dist/service-reminders.d.ts.map +1 -0
- package/dist/service-reminders.js +215 -0
- package/dist/service-shared.d.ts +73 -0
- package/dist/service-shared.d.ts.map +1 -0
- package/dist/service-shared.js +124 -0
- package/dist/service-templates.d.ts +182 -0
- package/dist/service-templates.d.ts.map +1 -0
- package/dist/service-templates.js +115 -0
- package/dist/service.d.ts +21 -367
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +20 -773
- package/dist/tasks/send-due-reminders.d.ts +1 -6
- package/dist/tasks/send-due-reminders.d.ts.map +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { bookingParticipants, bookings } from "@voyantjs/bookings/schema";
|
|
2
|
+
import { bookingPaymentSchedules } from "@voyantjs/finance";
|
|
3
|
+
import { and, desc, eq, or } from "drizzle-orm";
|
|
4
|
+
import { notificationReminderRules, notificationReminderRuns } from "./schema.js";
|
|
5
|
+
import { sendNotification } from "./service-deliveries.js";
|
|
6
|
+
import { addUtcDays, buildReminderDedupeKey, resolveReminderRecipient, startOfUtcDay, toDateString, toTimestamp, } from "./service-shared.js";
|
|
7
|
+
async function sendBookingPaymentScheduleReminder(db, dispatcher, rule, schedule, now) {
|
|
8
|
+
const runDate = toDateString(startOfUtcDay(now));
|
|
9
|
+
const dedupeKey = buildReminderDedupeKey(rule.id, schedule.id, runDate);
|
|
10
|
+
const [existingRun] = await db
|
|
11
|
+
.select({ id: notificationReminderRuns.id })
|
|
12
|
+
.from(notificationReminderRuns)
|
|
13
|
+
.where(eq(notificationReminderRuns.dedupeKey, dedupeKey))
|
|
14
|
+
.limit(1);
|
|
15
|
+
if (existingRun) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const [booking] = await db
|
|
19
|
+
.select()
|
|
20
|
+
.from(bookings)
|
|
21
|
+
.where(eq(bookings.id, schedule.bookingId))
|
|
22
|
+
.limit(1);
|
|
23
|
+
if (!booking) {
|
|
24
|
+
const [run] = await db
|
|
25
|
+
.insert(notificationReminderRuns)
|
|
26
|
+
.values({
|
|
27
|
+
reminderRuleId: rule.id,
|
|
28
|
+
targetType: "booking_payment_schedule",
|
|
29
|
+
targetId: schedule.id,
|
|
30
|
+
dedupeKey,
|
|
31
|
+
bookingId: schedule.bookingId,
|
|
32
|
+
personId: null,
|
|
33
|
+
organizationId: null,
|
|
34
|
+
paymentSessionId: null,
|
|
35
|
+
notificationDeliveryId: null,
|
|
36
|
+
status: "skipped",
|
|
37
|
+
recipient: null,
|
|
38
|
+
scheduledFor: now,
|
|
39
|
+
processedAt: now,
|
|
40
|
+
errorMessage: "Booking not found for payment schedule",
|
|
41
|
+
metadata: {
|
|
42
|
+
dueDate: schedule.dueDate,
|
|
43
|
+
relativeDaysFromDueDate: rule.relativeDaysFromDueDate,
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
.returning();
|
|
47
|
+
return run ?? null;
|
|
48
|
+
}
|
|
49
|
+
const participants = await db
|
|
50
|
+
.select({
|
|
51
|
+
id: bookingParticipants.id,
|
|
52
|
+
firstName: bookingParticipants.firstName,
|
|
53
|
+
lastName: bookingParticipants.lastName,
|
|
54
|
+
email: bookingParticipants.email,
|
|
55
|
+
participantType: bookingParticipants.participantType,
|
|
56
|
+
isPrimary: bookingParticipants.isPrimary,
|
|
57
|
+
})
|
|
58
|
+
.from(bookingParticipants)
|
|
59
|
+
.where(eq(bookingParticipants.bookingId, booking.id))
|
|
60
|
+
.orderBy(desc(bookingParticipants.isPrimary), bookingParticipants.createdAt);
|
|
61
|
+
const recipient = resolveReminderRecipient(participants);
|
|
62
|
+
const [processingRun] = await db
|
|
63
|
+
.insert(notificationReminderRuns)
|
|
64
|
+
.values({
|
|
65
|
+
reminderRuleId: rule.id,
|
|
66
|
+
targetType: "booking_payment_schedule",
|
|
67
|
+
targetId: schedule.id,
|
|
68
|
+
dedupeKey,
|
|
69
|
+
bookingId: booking.id,
|
|
70
|
+
personId: booking.personId ?? null,
|
|
71
|
+
organizationId: booking.organizationId ?? null,
|
|
72
|
+
paymentSessionId: null,
|
|
73
|
+
notificationDeliveryId: null,
|
|
74
|
+
status: "processing",
|
|
75
|
+
recipient: recipient?.email ?? null,
|
|
76
|
+
scheduledFor: now,
|
|
77
|
+
processedAt: now,
|
|
78
|
+
errorMessage: null,
|
|
79
|
+
metadata: {
|
|
80
|
+
dueDate: schedule.dueDate,
|
|
81
|
+
relativeDaysFromDueDate: rule.relativeDaysFromDueDate,
|
|
82
|
+
bookingNumber: booking.bookingNumber,
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
.onConflictDoNothing({ target: notificationReminderRuns.dedupeKey })
|
|
86
|
+
.returning();
|
|
87
|
+
if (!processingRun) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (!recipient?.email) {
|
|
91
|
+
const [run] = await db
|
|
92
|
+
.update(notificationReminderRuns)
|
|
93
|
+
.set({
|
|
94
|
+
status: "skipped",
|
|
95
|
+
errorMessage: "No participant email available for booking payment reminder",
|
|
96
|
+
processedAt: now,
|
|
97
|
+
updatedAt: now,
|
|
98
|
+
})
|
|
99
|
+
.where(eq(notificationReminderRuns.id, processingRun.id))
|
|
100
|
+
.returning();
|
|
101
|
+
return run ?? null;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const delivery = await sendNotification(db, dispatcher, {
|
|
105
|
+
templateId: rule.templateId ?? null,
|
|
106
|
+
templateSlug: rule.templateSlug ?? null,
|
|
107
|
+
channel: rule.channel,
|
|
108
|
+
provider: rule.provider ?? null,
|
|
109
|
+
to: recipient.email,
|
|
110
|
+
data: {
|
|
111
|
+
bookingId: booking.id,
|
|
112
|
+
bookingNumber: booking.bookingNumber,
|
|
113
|
+
dueDate: schedule.dueDate,
|
|
114
|
+
amountCents: schedule.amountCents,
|
|
115
|
+
currency: schedule.currency,
|
|
116
|
+
scheduleType: schedule.scheduleType,
|
|
117
|
+
reminderOffsetDays: rule.relativeDaysFromDueDate,
|
|
118
|
+
participant: {
|
|
119
|
+
firstName: recipient.firstName,
|
|
120
|
+
lastName: recipient.lastName,
|
|
121
|
+
email: recipient.email,
|
|
122
|
+
},
|
|
123
|
+
booking: {
|
|
124
|
+
id: booking.id,
|
|
125
|
+
bookingNumber: booking.bookingNumber,
|
|
126
|
+
startDate: booking.startDate,
|
|
127
|
+
endDate: booking.endDate,
|
|
128
|
+
sellCurrency: booking.sellCurrency,
|
|
129
|
+
sellAmountCents: booking.sellAmountCents,
|
|
130
|
+
},
|
|
131
|
+
paymentSchedule: {
|
|
132
|
+
id: schedule.id,
|
|
133
|
+
dueDate: schedule.dueDate,
|
|
134
|
+
amountCents: schedule.amountCents,
|
|
135
|
+
currency: schedule.currency,
|
|
136
|
+
scheduleType: schedule.scheduleType,
|
|
137
|
+
status: schedule.status,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
targetType: "booking_payment_schedule",
|
|
141
|
+
targetId: schedule.id,
|
|
142
|
+
bookingId: booking.id,
|
|
143
|
+
personId: booking.personId ?? null,
|
|
144
|
+
organizationId: booking.organizationId ?? null,
|
|
145
|
+
metadata: {
|
|
146
|
+
reminderRuleId: rule.id,
|
|
147
|
+
reminderRunId: processingRun.id,
|
|
148
|
+
},
|
|
149
|
+
scheduledFor: now.toISOString(),
|
|
150
|
+
});
|
|
151
|
+
const [run] = await db
|
|
152
|
+
.update(notificationReminderRuns)
|
|
153
|
+
.set({
|
|
154
|
+
notificationDeliveryId: delivery?.id ?? null,
|
|
155
|
+
status: "sent",
|
|
156
|
+
processedAt: new Date(),
|
|
157
|
+
updatedAt: new Date(),
|
|
158
|
+
errorMessage: null,
|
|
159
|
+
})
|
|
160
|
+
.where(eq(notificationReminderRuns.id, processingRun.id))
|
|
161
|
+
.returning();
|
|
162
|
+
return run ?? null;
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
const message = error instanceof Error ? error.message : "Notification reminder failed";
|
|
166
|
+
const [run] = await db
|
|
167
|
+
.update(notificationReminderRuns)
|
|
168
|
+
.set({
|
|
169
|
+
status: "failed",
|
|
170
|
+
errorMessage: message,
|
|
171
|
+
processedAt: new Date(),
|
|
172
|
+
updatedAt: new Date(),
|
|
173
|
+
})
|
|
174
|
+
.where(eq(notificationReminderRuns.id, processingRun.id))
|
|
175
|
+
.returning();
|
|
176
|
+
return run ?? null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
export async function runDueReminders(db, dispatcher, input = {}) {
|
|
180
|
+
const now = toTimestamp(input.now) ?? new Date();
|
|
181
|
+
const today = startOfUtcDay(now);
|
|
182
|
+
const activeRules = await db
|
|
183
|
+
.select()
|
|
184
|
+
.from(notificationReminderRules)
|
|
185
|
+
.where(eq(notificationReminderRules.status, "active"))
|
|
186
|
+
.orderBy(notificationReminderRules.createdAt);
|
|
187
|
+
const summary = {
|
|
188
|
+
processed: 0,
|
|
189
|
+
sent: 0,
|
|
190
|
+
skipped: 0,
|
|
191
|
+
failed: 0,
|
|
192
|
+
};
|
|
193
|
+
for (const rule of activeRules) {
|
|
194
|
+
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;
|
|
204
|
+
}
|
|
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
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return summary;
|
|
215
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { bookingPaymentSchedules } from "@voyantjs/finance";
|
|
2
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
3
|
+
import type { SQLWrapper } from "drizzle-orm/sql";
|
|
4
|
+
import type { z } from "zod";
|
|
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";
|
|
8
|
+
export type NotificationTemplateListQuery = z.infer<typeof notificationTemplateListQuerySchema>;
|
|
9
|
+
export type NotificationDeliveryListQuery = z.infer<typeof notificationDeliveryListQuerySchema>;
|
|
10
|
+
export type CreateNotificationTemplateInput = z.infer<typeof insertNotificationTemplateSchema>;
|
|
11
|
+
export type UpdateNotificationTemplateInput = z.infer<typeof updateNotificationTemplateSchema>;
|
|
12
|
+
export type SendNotificationInput = z.infer<typeof sendNotificationSchema>;
|
|
13
|
+
export type NotificationReminderRuleListQuery = z.infer<typeof notificationReminderRuleListQuerySchema>;
|
|
14
|
+
export type NotificationReminderRunListQuery = z.infer<typeof notificationReminderRunListQuerySchema>;
|
|
15
|
+
export type CreateNotificationReminderRuleInput = z.infer<typeof insertNotificationReminderRuleSchema>;
|
|
16
|
+
export type UpdateNotificationReminderRuleInput = z.infer<typeof updateNotificationReminderRuleSchema>;
|
|
17
|
+
export type RunDueRemindersInput = z.infer<typeof runDueRemindersSchema>;
|
|
18
|
+
export type SendPaymentSessionNotificationInput = z.infer<typeof sendPaymentSessionNotificationSchema>;
|
|
19
|
+
export type SendInvoiceNotificationInput = z.infer<typeof sendInvoiceNotificationSchema>;
|
|
20
|
+
export type ReminderSweepResult = {
|
|
21
|
+
processed: number;
|
|
22
|
+
sent: number;
|
|
23
|
+
skipped: number;
|
|
24
|
+
failed: number;
|
|
25
|
+
};
|
|
26
|
+
export type NotificationReminderRuleRow = typeof notificationReminderRules.$inferSelect;
|
|
27
|
+
export type BookingPaymentScheduleRow = typeof bookingPaymentSchedules.$inferSelect;
|
|
28
|
+
export declare class NotificationError extends Error {
|
|
29
|
+
constructor(message: string);
|
|
30
|
+
}
|
|
31
|
+
export interface NotificationService {
|
|
32
|
+
send(payload: NotificationPayload): Promise<NotificationResult>;
|
|
33
|
+
sendWith(providerName: string, payload: NotificationPayload): Promise<NotificationResult>;
|
|
34
|
+
getProvider(channel: NotificationChannel): NotificationProvider | undefined;
|
|
35
|
+
}
|
|
36
|
+
export declare function createNotificationService(providers: ReadonlyArray<NotificationProvider>): NotificationService;
|
|
37
|
+
export declare function renderNotificationTemplate(template: string | null | undefined, data: Record<string, unknown>): string | null;
|
|
38
|
+
export declare function toTimestamp(value?: string | null): Date | null;
|
|
39
|
+
export declare function startOfUtcDay(value: Date): Date;
|
|
40
|
+
export declare function addUtcDays(value: Date, days: number): Date;
|
|
41
|
+
export declare function toDateString(value: Date): string;
|
|
42
|
+
export declare function buildReminderDedupeKey(ruleId: string, targetId: string, runDate: string): string;
|
|
43
|
+
export declare function resolveReminderRecipient(participants: Array<{
|
|
44
|
+
email: string | null;
|
|
45
|
+
isPrimary: boolean;
|
|
46
|
+
participantType: string;
|
|
47
|
+
firstName: string;
|
|
48
|
+
lastName: string;
|
|
49
|
+
}>): {
|
|
50
|
+
email: string | null;
|
|
51
|
+
isPrimary: boolean;
|
|
52
|
+
participantType: string;
|
|
53
|
+
firstName: string;
|
|
54
|
+
lastName: string;
|
|
55
|
+
} | null;
|
|
56
|
+
export declare function listBookingNotificationParticipants(db: PostgresJsDatabase, bookingId: string): Promise<{
|
|
57
|
+
id: string;
|
|
58
|
+
firstName: string;
|
|
59
|
+
lastName: string;
|
|
60
|
+
email: string | null;
|
|
61
|
+
participantType: "staff" | "other" | "traveler" | "booker" | "contact" | "occupant";
|
|
62
|
+
isPrimary: boolean;
|
|
63
|
+
}[]>;
|
|
64
|
+
export declare function paginate<T>(rowsPromise: Promise<T[]>, totalPromise: Promise<Array<{
|
|
65
|
+
total: number;
|
|
66
|
+
}>>, limit: number, offset: number): Promise<{
|
|
67
|
+
data: T[];
|
|
68
|
+
total: number;
|
|
69
|
+
limit: number;
|
|
70
|
+
offset: number;
|
|
71
|
+
}>;
|
|
72
|
+
export declare function buildWhereClause<T extends SQLWrapper>(conditions: Array<T | undefined>): import("drizzle-orm").SQL<unknown> | undefined;
|
|
73
|
+
//# sourceMappingURL=service-shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-shared.d.ts","sourceRoot":"","sources":["../src/service-shared.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { bookingParticipants } from "@voyantjs/bookings/schema";
|
|
2
|
+
import { and, desc, eq } from "drizzle-orm";
|
|
3
|
+
export class NotificationError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "NotificationError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function createNotificationService(providers) {
|
|
10
|
+
const byChannel = new Map();
|
|
11
|
+
const byName = new Map();
|
|
12
|
+
for (const provider of providers) {
|
|
13
|
+
byName.set(provider.name, provider);
|
|
14
|
+
for (const channel of provider.channels) {
|
|
15
|
+
byChannel.set(channel, provider);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
async send(payload) {
|
|
20
|
+
const hintedProvider = payload.provider ? byName.get(payload.provider) : null;
|
|
21
|
+
const provider = hintedProvider ?? byChannel.get(payload.channel);
|
|
22
|
+
if (!provider) {
|
|
23
|
+
throw new NotificationError(`No notification provider registered for channel "${payload.channel}"`);
|
|
24
|
+
}
|
|
25
|
+
return provider.send(payload);
|
|
26
|
+
},
|
|
27
|
+
async sendWith(providerName, payload) {
|
|
28
|
+
const provider = byName.get(providerName);
|
|
29
|
+
if (!provider) {
|
|
30
|
+
throw new NotificationError(`No notification provider registered with name "${providerName}"`);
|
|
31
|
+
}
|
|
32
|
+
return provider.send(payload);
|
|
33
|
+
},
|
|
34
|
+
getProvider(channel) {
|
|
35
|
+
return byChannel.get(channel);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function resolveMustachePath(path, scope) {
|
|
40
|
+
const parts = path.match(/[^.[\]]+/g) ?? [];
|
|
41
|
+
let current = scope;
|
|
42
|
+
for (const part of parts) {
|
|
43
|
+
if (current == null || typeof current !== "object")
|
|
44
|
+
return undefined;
|
|
45
|
+
current = current[part];
|
|
46
|
+
}
|
|
47
|
+
return current;
|
|
48
|
+
}
|
|
49
|
+
function stringifyRenderedValue(value) {
|
|
50
|
+
if (value == null)
|
|
51
|
+
return "";
|
|
52
|
+
if (typeof value === "string")
|
|
53
|
+
return value;
|
|
54
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
55
|
+
return String(value);
|
|
56
|
+
return JSON.stringify(value);
|
|
57
|
+
}
|
|
58
|
+
export function renderNotificationTemplate(template, data) {
|
|
59
|
+
if (!template)
|
|
60
|
+
return null;
|
|
61
|
+
return template.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_match, path) => {
|
|
62
|
+
return stringifyRenderedValue(resolveMustachePath(path.trim(), data));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
export function toTimestamp(value) {
|
|
66
|
+
return value ? new Date(value) : null;
|
|
67
|
+
}
|
|
68
|
+
export function startOfUtcDay(value) {
|
|
69
|
+
return new Date(Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()));
|
|
70
|
+
}
|
|
71
|
+
export function addUtcDays(value, days) {
|
|
72
|
+
return new Date(value.getTime() + days * 24 * 60 * 60 * 1000);
|
|
73
|
+
}
|
|
74
|
+
export function toDateString(value) {
|
|
75
|
+
return value.toISOString().slice(0, 10);
|
|
76
|
+
}
|
|
77
|
+
export function buildReminderDedupeKey(ruleId, targetId, runDate) {
|
|
78
|
+
return `${ruleId}:${targetId}:${runDate}`;
|
|
79
|
+
}
|
|
80
|
+
export function resolveReminderRecipient(participants) {
|
|
81
|
+
const withEmail = participants.filter((participant) => participant.email);
|
|
82
|
+
if (withEmail.length === 0) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const primary = withEmail.find((participant) => participant.isPrimary);
|
|
86
|
+
if (primary) {
|
|
87
|
+
return primary;
|
|
88
|
+
}
|
|
89
|
+
const preferredTypes = ["booker", "contact", "traveler", "occupant"];
|
|
90
|
+
for (const type of preferredTypes) {
|
|
91
|
+
const match = withEmail.find((participant) => participant.participantType === type);
|
|
92
|
+
if (match) {
|
|
93
|
+
return match;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return withEmail[0] ?? null;
|
|
97
|
+
}
|
|
98
|
+
export async function listBookingNotificationParticipants(db, bookingId) {
|
|
99
|
+
return db
|
|
100
|
+
.select({
|
|
101
|
+
id: bookingParticipants.id,
|
|
102
|
+
firstName: bookingParticipants.firstName,
|
|
103
|
+
lastName: bookingParticipants.lastName,
|
|
104
|
+
email: bookingParticipants.email,
|
|
105
|
+
participantType: bookingParticipants.participantType,
|
|
106
|
+
isPrimary: bookingParticipants.isPrimary,
|
|
107
|
+
})
|
|
108
|
+
.from(bookingParticipants)
|
|
109
|
+
.where(eq(bookingParticipants.bookingId, bookingId))
|
|
110
|
+
.orderBy(desc(bookingParticipants.isPrimary), bookingParticipants.createdAt);
|
|
111
|
+
}
|
|
112
|
+
export async function paginate(rowsPromise, totalPromise, limit, offset) {
|
|
113
|
+
const [data, totalRows] = await Promise.all([rowsPromise, totalPromise]);
|
|
114
|
+
return {
|
|
115
|
+
data,
|
|
116
|
+
total: totalRows[0]?.total ?? 0,
|
|
117
|
+
limit,
|
|
118
|
+
offset,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export function buildWhereClause(conditions) {
|
|
122
|
+
const filtered = conditions.filter((condition) => Boolean(condition));
|
|
123
|
+
return filtered.length > 0 ? and(...filtered) : undefined;
|
|
124
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
2
|
+
import type { CreateNotificationReminderRuleInput, CreateNotificationTemplateInput, NotificationReminderRuleListQuery, NotificationReminderRunListQuery, NotificationTemplateListQuery, UpdateNotificationReminderRuleInput, UpdateNotificationTemplateInput } from "./service-shared.js";
|
|
3
|
+
export declare function listTemplates(db: PostgresJsDatabase, query: NotificationTemplateListQuery): Promise<{
|
|
4
|
+
data: {
|
|
5
|
+
id: string;
|
|
6
|
+
slug: string;
|
|
7
|
+
name: string;
|
|
8
|
+
channel: "email" | "sms";
|
|
9
|
+
provider: string | null;
|
|
10
|
+
status: "draft" | "active" | "archived";
|
|
11
|
+
subjectTemplate: string | null;
|
|
12
|
+
htmlTemplate: string | null;
|
|
13
|
+
textTemplate: string | null;
|
|
14
|
+
fromAddress: string | null;
|
|
15
|
+
isSystem: boolean;
|
|
16
|
+
metadata: Record<string, unknown> | null;
|
|
17
|
+
createdAt: Date;
|
|
18
|
+
updatedAt: Date;
|
|
19
|
+
}[];
|
|
20
|
+
total: number;
|
|
21
|
+
limit: number;
|
|
22
|
+
offset: number;
|
|
23
|
+
}>;
|
|
24
|
+
export declare function getTemplateById(db: PostgresJsDatabase, id: string): Promise<{
|
|
25
|
+
id: string;
|
|
26
|
+
slug: string;
|
|
27
|
+
name: string;
|
|
28
|
+
channel: "email" | "sms";
|
|
29
|
+
provider: string | null;
|
|
30
|
+
status: "draft" | "active" | "archived";
|
|
31
|
+
subjectTemplate: string | null;
|
|
32
|
+
htmlTemplate: string | null;
|
|
33
|
+
textTemplate: string | null;
|
|
34
|
+
fromAddress: string | null;
|
|
35
|
+
isSystem: boolean;
|
|
36
|
+
metadata: Record<string, unknown> | null;
|
|
37
|
+
createdAt: Date;
|
|
38
|
+
updatedAt: Date;
|
|
39
|
+
} | null>;
|
|
40
|
+
export declare function getTemplateBySlug(db: PostgresJsDatabase, slug: string): Promise<{
|
|
41
|
+
id: string;
|
|
42
|
+
slug: string;
|
|
43
|
+
name: string;
|
|
44
|
+
channel: "email" | "sms";
|
|
45
|
+
provider: string | null;
|
|
46
|
+
status: "draft" | "active" | "archived";
|
|
47
|
+
subjectTemplate: string | null;
|
|
48
|
+
htmlTemplate: string | null;
|
|
49
|
+
textTemplate: string | null;
|
|
50
|
+
fromAddress: string | null;
|
|
51
|
+
isSystem: boolean;
|
|
52
|
+
metadata: Record<string, unknown> | null;
|
|
53
|
+
createdAt: Date;
|
|
54
|
+
updatedAt: Date;
|
|
55
|
+
} | null>;
|
|
56
|
+
export declare function createTemplate(db: PostgresJsDatabase, data: CreateNotificationTemplateInput): Promise<{
|
|
57
|
+
id: string;
|
|
58
|
+
name: string;
|
|
59
|
+
status: "draft" | "active" | "archived";
|
|
60
|
+
createdAt: Date;
|
|
61
|
+
updatedAt: Date;
|
|
62
|
+
metadata: Record<string, unknown> | null;
|
|
63
|
+
channel: "email" | "sms";
|
|
64
|
+
provider: string | null;
|
|
65
|
+
slug: string;
|
|
66
|
+
subjectTemplate: string | null;
|
|
67
|
+
htmlTemplate: string | null;
|
|
68
|
+
textTemplate: string | null;
|
|
69
|
+
fromAddress: string | null;
|
|
70
|
+
isSystem: boolean;
|
|
71
|
+
} | null>;
|
|
72
|
+
export declare function updateTemplate(db: PostgresJsDatabase, id: string, data: UpdateNotificationTemplateInput): Promise<{
|
|
73
|
+
id: string;
|
|
74
|
+
slug: string;
|
|
75
|
+
name: string;
|
|
76
|
+
channel: "email" | "sms";
|
|
77
|
+
provider: string | null;
|
|
78
|
+
status: "draft" | "active" | "archived";
|
|
79
|
+
subjectTemplate: string | null;
|
|
80
|
+
htmlTemplate: string | null;
|
|
81
|
+
textTemplate: string | null;
|
|
82
|
+
fromAddress: string | null;
|
|
83
|
+
isSystem: boolean;
|
|
84
|
+
metadata: Record<string, unknown> | null;
|
|
85
|
+
createdAt: Date;
|
|
86
|
+
updatedAt: Date;
|
|
87
|
+
} | null>;
|
|
88
|
+
export declare function listReminderRules(db: PostgresJsDatabase, query: NotificationReminderRuleListQuery): Promise<{
|
|
89
|
+
data: {
|
|
90
|
+
id: string;
|
|
91
|
+
slug: string;
|
|
92
|
+
name: string;
|
|
93
|
+
status: "draft" | "active" | "archived";
|
|
94
|
+
targetType: "booking_payment_schedule";
|
|
95
|
+
channel: "email" | "sms";
|
|
96
|
+
provider: string | null;
|
|
97
|
+
templateId: string | null;
|
|
98
|
+
templateSlug: string | null;
|
|
99
|
+
relativeDaysFromDueDate: number;
|
|
100
|
+
isSystem: boolean;
|
|
101
|
+
metadata: Record<string, unknown> | null;
|
|
102
|
+
createdAt: Date;
|
|
103
|
+
updatedAt: Date;
|
|
104
|
+
}[];
|
|
105
|
+
total: number;
|
|
106
|
+
limit: number;
|
|
107
|
+
offset: number;
|
|
108
|
+
}>;
|
|
109
|
+
export declare function getReminderRuleById(db: PostgresJsDatabase, id: string): Promise<{
|
|
110
|
+
id: string;
|
|
111
|
+
slug: string;
|
|
112
|
+
name: string;
|
|
113
|
+
status: "draft" | "active" | "archived";
|
|
114
|
+
targetType: "booking_payment_schedule";
|
|
115
|
+
channel: "email" | "sms";
|
|
116
|
+
provider: string | null;
|
|
117
|
+
templateId: string | null;
|
|
118
|
+
templateSlug: string | null;
|
|
119
|
+
relativeDaysFromDueDate: number;
|
|
120
|
+
isSystem: boolean;
|
|
121
|
+
metadata: Record<string, unknown> | null;
|
|
122
|
+
createdAt: Date;
|
|
123
|
+
updatedAt: Date;
|
|
124
|
+
} | null>;
|
|
125
|
+
export declare function createReminderRule(db: PostgresJsDatabase, data: CreateNotificationReminderRuleInput): Promise<{
|
|
126
|
+
id: string;
|
|
127
|
+
name: string;
|
|
128
|
+
status: "draft" | "active" | "archived";
|
|
129
|
+
createdAt: Date;
|
|
130
|
+
updatedAt: Date;
|
|
131
|
+
metadata: Record<string, unknown> | null;
|
|
132
|
+
channel: "email" | "sms";
|
|
133
|
+
provider: string | null;
|
|
134
|
+
targetType: "booking_payment_schedule";
|
|
135
|
+
templateId: string | null;
|
|
136
|
+
slug: string;
|
|
137
|
+
isSystem: boolean;
|
|
138
|
+
templateSlug: string | null;
|
|
139
|
+
relativeDaysFromDueDate: number;
|
|
140
|
+
} | null>;
|
|
141
|
+
export declare function updateReminderRule(db: PostgresJsDatabase, id: string, data: UpdateNotificationReminderRuleInput): Promise<{
|
|
142
|
+
id: string;
|
|
143
|
+
slug: string;
|
|
144
|
+
name: string;
|
|
145
|
+
status: "draft" | "active" | "archived";
|
|
146
|
+
targetType: "booking_payment_schedule";
|
|
147
|
+
channel: "email" | "sms";
|
|
148
|
+
provider: string | null;
|
|
149
|
+
templateId: string | null;
|
|
150
|
+
templateSlug: string | null;
|
|
151
|
+
relativeDaysFromDueDate: number;
|
|
152
|
+
isSystem: boolean;
|
|
153
|
+
metadata: Record<string, unknown> | null;
|
|
154
|
+
createdAt: Date;
|
|
155
|
+
updatedAt: Date;
|
|
156
|
+
} | null>;
|
|
157
|
+
export declare function listReminderRuns(db: PostgresJsDatabase, query: NotificationReminderRunListQuery): Promise<{
|
|
158
|
+
data: {
|
|
159
|
+
id: string;
|
|
160
|
+
reminderRuleId: string;
|
|
161
|
+
targetType: "booking_payment_schedule";
|
|
162
|
+
targetId: string;
|
|
163
|
+
dedupeKey: string;
|
|
164
|
+
bookingId: string | null;
|
|
165
|
+
personId: string | null;
|
|
166
|
+
organizationId: string | null;
|
|
167
|
+
paymentSessionId: string | null;
|
|
168
|
+
notificationDeliveryId: string | null;
|
|
169
|
+
status: "failed" | "sent" | "processing" | "skipped";
|
|
170
|
+
recipient: string | null;
|
|
171
|
+
scheduledFor: Date;
|
|
172
|
+
processedAt: Date;
|
|
173
|
+
errorMessage: string | null;
|
|
174
|
+
metadata: Record<string, unknown> | null;
|
|
175
|
+
createdAt: Date;
|
|
176
|
+
updatedAt: Date;
|
|
177
|
+
}[];
|
|
178
|
+
total: number;
|
|
179
|
+
limit: number;
|
|
180
|
+
offset: number;
|
|
181
|
+
}>;
|
|
182
|
+
//# sourceMappingURL=service-templates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-templates.d.ts","sourceRoot":"","sources":["../src/service-templates.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAOjE,OAAO,KAAK,EACV,mCAAmC,EACnC,+BAA+B,EAC/B,iCAAiC,EACjC,gCAAgC,EAChC,6BAA6B,EAC7B,mCAAmC,EACnC,+BAA+B,EAChC,MAAM,qBAAqB,CAAA;AAG5B,wBAAsB,aAAa,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,6BAA6B;;;;;;;;;;;;;;;;;;;;GAyB/F;AAED,wBAAsB,eAAe,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;UAOvE;AAED,wBAAsB,iBAAiB,CAAC,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM;;;;;;;;;;;;;;;UAO3E;AAED,wBAAsB,cAAc,CAClC,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,+BAA+B;;;;;;;;;;;;;;;UAItC;AAED,wBAAsB,cAAc,CAClC,EAAE,EAAE,kBAAkB,EACtB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,+BAA+B;;;;;;;;;;;;;;;UAQtC;AAED,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,iCAAiC;;;;;;;;;;;;;;;;;;;;GA0BzC;AAED,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;UAO3E;AAED,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,mCAAmC;;;;;;;;;;;;;;;UAI1C;AAED,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,kBAAkB,EACtB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,mCAAmC;;;;;;;;;;;;;;;UAQ1C;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,gCAAgC;;;;;;;;;;;;;;;;;;;;;;;;GAwBxC"}
|