@voyant-travel/notifications 0.111.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +179 -0
  3. package/dist/index.d.ts +61 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +196 -0
  6. package/dist/liquid.d.ts +5 -0
  7. package/dist/liquid.d.ts.map +1 -0
  8. package/dist/liquid.js +156 -0
  9. package/dist/providers/local.d.ts +22 -0
  10. package/dist/providers/local.d.ts.map +1 -0
  11. package/dist/providers/local.js +23 -0
  12. package/dist/providers/voyant-cloud-email.d.ts +27 -0
  13. package/dist/providers/voyant-cloud-email.d.ts.map +1 -0
  14. package/dist/providers/voyant-cloud-email.js +73 -0
  15. package/dist/providers/voyant-cloud-sms.d.ts +26 -0
  16. package/dist/providers/voyant-cloud-sms.d.ts.map +1 -0
  17. package/dist/providers/voyant-cloud-sms.js +24 -0
  18. package/dist/routes.d.ts +1546 -0
  19. package/dist/routes.d.ts.map +1 -0
  20. package/dist/routes.js +337 -0
  21. package/dist/schema.d.ts +2119 -0
  22. package/dist/schema.d.ts.map +1 -0
  23. package/dist/schema.js +356 -0
  24. package/dist/service-booking-document-lifecycle.d.ts +99 -0
  25. package/dist/service-booking-document-lifecycle.d.ts.map +1 -0
  26. package/dist/service-booking-document-lifecycle.js +259 -0
  27. package/dist/service-booking-documents.d.ts +256 -0
  28. package/dist/service-booking-documents.d.ts.map +1 -0
  29. package/dist/service-booking-documents.js +323 -0
  30. package/dist/service-deliveries.d.ts +183 -0
  31. package/dist/service-deliveries.d.ts.map +1 -0
  32. package/dist/service-deliveries.js +413 -0
  33. package/dist/service-delivery-metadata.d.ts +42 -0
  34. package/dist/service-delivery-metadata.d.ts.map +1 -0
  35. package/dist/service-delivery-metadata.js +114 -0
  36. package/dist/service-reminder-authoring.d.ts +33 -0
  37. package/dist/service-reminder-authoring.d.ts.map +1 -0
  38. package/dist/service-reminder-authoring.js +247 -0
  39. package/dist/service-reminder-booking-context.d.ts +94 -0
  40. package/dist/service-reminder-booking-context.d.ts.map +1 -0
  41. package/dist/service-reminder-booking-context.js +164 -0
  42. package/dist/service-reminder-events.d.ts +33 -0
  43. package/dist/service-reminder-events.d.ts.map +1 -0
  44. package/dist/service-reminder-events.js +178 -0
  45. package/dist/service-reminder-run-state.d.ts +114 -0
  46. package/dist/service-reminder-run-state.d.ts.map +1 -0
  47. package/dist/service-reminder-run-state.js +100 -0
  48. package/dist/service-reminder-stage-runs.d.ts +6 -0
  49. package/dist/service-reminder-stage-runs.d.ts.map +1 -0
  50. package/dist/service-reminder-stage-runs.js +310 -0
  51. package/dist/service-reminders.d.ts +30 -0
  52. package/dist/service-reminders.d.ts.map +1 -0
  53. package/dist/service-reminders.js +189 -0
  54. package/dist/service-sequence-targets.d.ts +50 -0
  55. package/dist/service-sequence-targets.d.ts.map +1 -0
  56. package/dist/service-sequence-targets.js +136 -0
  57. package/dist/service-sequence.d.ts +68 -0
  58. package/dist/service-sequence.d.ts.map +1 -0
  59. package/dist/service-sequence.js +316 -0
  60. package/dist/service-shared.d.ts +107 -0
  61. package/dist/service-shared.d.ts.map +1 -0
  62. package/dist/service-shared.js +159 -0
  63. package/dist/service-stages.d.ts +23 -0
  64. package/dist/service-stages.d.ts.map +1 -0
  65. package/dist/service-stages.js +203 -0
  66. package/dist/service-template-data.d.ts +19 -0
  67. package/dist/service-template-data.d.ts.map +1 -0
  68. package/dist/service-template-data.js +278 -0
  69. package/dist/service-templates.d.ts +260 -0
  70. package/dist/service-templates.d.ts.map +1 -0
  71. package/dist/service-templates.js +293 -0
  72. package/dist/service.d.ts +273 -0
  73. package/dist/service.d.ts.map +1 -0
  74. package/dist/service.js +51 -0
  75. package/dist/task-runtime.d.ts +19 -0
  76. package/dist/task-runtime.d.ts.map +1 -0
  77. package/dist/task-runtime.js +11 -0
  78. package/dist/tasks/deliver-reminder.d.ts +9 -0
  79. package/dist/tasks/deliver-reminder.d.ts.map +1 -0
  80. package/dist/tasks/deliver-reminder.js +12 -0
  81. package/dist/tasks/index.d.ts +3 -0
  82. package/dist/tasks/index.d.ts.map +1 -0
  83. package/dist/tasks/index.js +2 -0
  84. package/dist/tasks/send-due-reminders.d.ts +7 -0
  85. package/dist/tasks/send-due-reminders.d.ts.map +1 -0
  86. package/dist/tasks/send-due-reminders.js +31 -0
  87. package/dist/template-authoring.d.ts +23 -0
  88. package/dist/template-authoring.d.ts.map +1 -0
  89. package/dist/template-authoring.js +386 -0
  90. package/dist/types.d.ts +82 -0
  91. package/dist/types.d.ts.map +1 -0
  92. package/dist/types.js +1 -0
  93. package/dist/validation.d.ts +1093 -0
  94. package/dist/validation.d.ts.map +1 -0
  95. package/dist/validation.js +451 -0
  96. package/package.json +102 -0
@@ -0,0 +1,247 @@
1
+ import { asc, eq, inArray, or, sql } from "drizzle-orm";
2
+ import { notificationReminderRuleAuthoringRequests, notificationReminderRuleStages, notificationReminderRules, notificationReminderStageChannels, notificationTemplates, } from "./schema.js";
3
+ function field(path) {
4
+ return path
5
+ .map((segment) => (typeof segment === "number" ? `[${segment}]` : segment))
6
+ .join(".")
7
+ .replaceAll(".[", "[");
8
+ }
9
+ function issue(issues, code, path, message, fix) {
10
+ issues.push({ code, field: field(path), message, fix });
11
+ }
12
+ async function resolveTemplateRefs(db, refs) {
13
+ const ids = [...new Set(refs.map((ref) => ref.templateId).filter((id) => !!id))];
14
+ const slugs = [
15
+ ...new Set(refs.map((ref) => ref.templateSlug).filter((slug) => !!slug)),
16
+ ];
17
+ if (ids.length === 0 && slugs.length === 0) {
18
+ return new Map();
19
+ }
20
+ const rows = await db
21
+ .select({
22
+ id: notificationTemplates.id,
23
+ slug: notificationTemplates.slug,
24
+ channel: notificationTemplates.channel,
25
+ })
26
+ .from(notificationTemplates)
27
+ .where(or(ids.length > 0 ? inArray(notificationTemplates.id, ids) : undefined, slugs.length > 0 ? inArray(notificationTemplates.slug, slugs) : undefined));
28
+ const resolved = new Map();
29
+ for (const row of rows) {
30
+ resolved.set(`id:${row.id}`, row);
31
+ resolved.set(`slug:${row.slug}`, row);
32
+ }
33
+ return resolved;
34
+ }
35
+ function normalizeTemplateRef(ref, path, resolved, issues) {
36
+ const idMatch = ref.templateId ? resolved.get(`id:${ref.templateId}`) : null;
37
+ const slugMatch = ref.templateSlug ? resolved.get(`slug:${ref.templateSlug}`) : null;
38
+ if (ref.templateId && !idMatch) {
39
+ issue(issues, "template_not_found", [...path, "templateId"], `Notification template "${ref.templateId}" was not found.`, "Use an existing templateId or omit it and provide a resolvable templateSlug.");
40
+ }
41
+ if (ref.templateSlug && !slugMatch) {
42
+ issue(issues, "template_not_found", [...path, "templateSlug"], `Notification template "${ref.templateSlug}" was not found.`, "Use an existing templateSlug or omit it and provide a resolvable templateId.");
43
+ }
44
+ if (idMatch && slugMatch && idMatch.id !== slugMatch.id) {
45
+ issue(issues, "template_ref_mismatch", path, "templateId and templateSlug resolve to different notification templates.", "Keep only one template reference or make both references point at the same template.");
46
+ }
47
+ const template = idMatch ?? slugMatch;
48
+ if (!template)
49
+ return null;
50
+ return {
51
+ templateId: template.id,
52
+ templateSlug: template.slug,
53
+ template,
54
+ };
55
+ }
56
+ async function validateAndNormalizeComposeInput(db, input) {
57
+ const issues = [];
58
+ const refs = [input.rule];
59
+ for (const stage of input.stages) {
60
+ refs.push(...stage.channels);
61
+ }
62
+ const resolved = await resolveTemplateRefs(db, refs);
63
+ const ruleTemplate = normalizeTemplateRef(input.rule, ["rule"], resolved, issues);
64
+ const stageOrderIndexes = new Set();
65
+ const normalizedChannels = [];
66
+ input.stages.forEach((stage, stageIndex) => {
67
+ const stagePath = ["stages", stageIndex];
68
+ if (stageOrderIndexes.has(stage.orderIndex)) {
69
+ issue(issues, "duplicate_stage_order", [...stagePath, "orderIndex"], `Stage orderIndex ${stage.orderIndex} is used more than once.`, "Give each stage a unique orderIndex.");
70
+ }
71
+ stageOrderIndexes.add(stage.orderIndex);
72
+ if (stage.windowEndDays < stage.windowStartDays) {
73
+ issue(issues, "invalid_stage_window", [...stagePath, "windowEndDays"], "windowEndDays must be greater than or equal to windowStartDays.", "Set windowEndDays to the same value as windowStartDays or a later day offset.");
74
+ }
75
+ if (stage.cadenceKind === "every_n_days" && !stage.cadenceEveryDays) {
76
+ issue(issues, "cadence_every_days_required", [...stagePath, "cadenceEveryDays"], "cadenceEveryDays is required when cadenceKind is every_n_days.", "Set cadenceEveryDays to a positive integer or change cadenceKind.");
77
+ }
78
+ if (stage.cadenceKind === "escalating" &&
79
+ (!stage.cadenceIntervals || stage.cadenceIntervals.length === 0)) {
80
+ issue(issues, "cadence_intervals_required", [...stagePath, "cadenceIntervals"], "cadenceIntervals is required when cadenceKind is escalating.", "Provide at least one escalation interval or change cadenceKind.");
81
+ }
82
+ const channelOrderIndexes = new Set();
83
+ normalizedChannels[stageIndex] = [];
84
+ stage.channels.forEach((channel, channelIndex) => {
85
+ const channelPath = [...stagePath, "channels", channelIndex];
86
+ if (channelOrderIndexes.has(channel.orderIndex)) {
87
+ issue(issues, "duplicate_channel_order", [...channelPath, "orderIndex"], `Channel orderIndex ${channel.orderIndex} is used more than once in this stage.`, "Give each channel in the stage a unique orderIndex.");
88
+ }
89
+ channelOrderIndexes.add(channel.orderIndex);
90
+ if (channel.channel !== input.rule.channel) {
91
+ issue(issues, "channel_mismatch", [...channelPath, "channel"], `Stage channel "${channel.channel}" does not match rule channel "${input.rule.channel}".`, "Use the same channel on the rule and all stage channels.");
92
+ }
93
+ const channelTemplate = normalizeTemplateRef(channel, channelPath, resolved, issues) ?? ruleTemplate;
94
+ if (!channelTemplate) {
95
+ issue(issues, "template_required", channelPath, "Each stage channel needs a resolvable template from the channel or rule default.", "Set templateId or templateSlug on this channel, or set a rule-level template.");
96
+ return;
97
+ }
98
+ if (channelTemplate.template.channel !== channel.channel) {
99
+ issue(issues, "template_channel_mismatch", channel.templateId || channel.templateSlug ? channelPath : ["rule"], `Template "${channelTemplate.template.slug}" is ${channelTemplate.template.channel}, but the channel is ${channel.channel}.`, "Use a template whose channel matches the stage channel.");
100
+ }
101
+ normalizedChannels[stageIndex][channelIndex] = channelTemplate;
102
+ });
103
+ });
104
+ if (ruleTemplate && ruleTemplate.template.channel !== input.rule.channel) {
105
+ issue(issues, "template_channel_mismatch", ["rule"], `Rule template "${ruleTemplate.template.slug}" is ${ruleTemplate.template.channel}, but the rule channel is ${input.rule.channel}.`, "Use a rule-level template whose channel matches the rule channel.");
106
+ }
107
+ if (issues.length > 0) {
108
+ return { ok: false, issues };
109
+ }
110
+ return { ok: true, input, ruleTemplate, channels: normalizedChannels };
111
+ }
112
+ async function loadComposeResult(db, ruleId) {
113
+ const stages = await db
114
+ .select({
115
+ id: notificationReminderRuleStages.id,
116
+ orderIndex: notificationReminderRuleStages.orderIndex,
117
+ })
118
+ .from(notificationReminderRuleStages)
119
+ .where(eq(notificationReminderRuleStages.reminderRuleId, ruleId))
120
+ .orderBy(asc(notificationReminderRuleStages.orderIndex));
121
+ const stageIds = stages.map((stage) => stage.id);
122
+ const channels = stageIds.length > 0
123
+ ? await db
124
+ .select({
125
+ id: notificationReminderStageChannels.id,
126
+ stageId: notificationReminderStageChannels.stageId,
127
+ orderIndex: notificationReminderStageChannels.orderIndex,
128
+ })
129
+ .from(notificationReminderStageChannels)
130
+ .where(inArray(notificationReminderStageChannels.stageId, stageIds))
131
+ .orderBy(asc(notificationReminderStageChannels.stageId), asc(notificationReminderStageChannels.orderIndex))
132
+ : [];
133
+ return {
134
+ ruleId,
135
+ stages: stages.map((stage) => ({
136
+ id: stage.id,
137
+ orderIndex: stage.orderIndex,
138
+ channels: channels
139
+ .filter((channel) => channel.stageId === stage.id)
140
+ .map((channel) => ({ id: channel.id, orderIndex: channel.orderIndex })),
141
+ })),
142
+ };
143
+ }
144
+ async function withIdempotency(tx, key, build) {
145
+ if (!key) {
146
+ return { result: await build(), reused: false };
147
+ }
148
+ // agent-quality: raw-sql reviewed -- owner: notifications; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
149
+ await tx.execute(sql `SELECT pg_advisory_xact_lock(hashtextextended(${key}, 0))`);
150
+ const [existing] = await tx
151
+ .select({ reminderRuleId: notificationReminderRuleAuthoringRequests.reminderRuleId })
152
+ .from(notificationReminderRuleAuthoringRequests)
153
+ .where(eq(notificationReminderRuleAuthoringRequests.idempotencyKey, key))
154
+ .limit(1);
155
+ if (existing) {
156
+ return { result: await loadComposeResult(tx, existing.reminderRuleId), reused: true };
157
+ }
158
+ const result = await build();
159
+ await tx.insert(notificationReminderRuleAuthoringRequests).values({
160
+ idempotencyKey: key,
161
+ reminderRuleId: result.ruleId,
162
+ operation: "compose",
163
+ });
164
+ return { result, reused: false };
165
+ }
166
+ async function buildReminderRuleGraph(tx, normalized) {
167
+ const [rule] = await tx
168
+ .insert(notificationReminderRules)
169
+ .values({
170
+ slug: normalized.input.rule.slug,
171
+ name: normalized.input.rule.name,
172
+ status: normalized.input.rule.status,
173
+ targetType: normalized.input.rule.targetType,
174
+ channel: normalized.input.rule.channel,
175
+ provider: normalized.input.rule.provider ?? null,
176
+ templateId: normalized.ruleTemplate?.templateId ?? null,
177
+ templateSlug: normalized.ruleTemplate?.templateSlug ?? null,
178
+ priority: normalized.input.rule.priority,
179
+ suppressionGroup: normalized.input.rule.suppressionGroup ?? null,
180
+ isSystem: normalized.input.rule.isSystem,
181
+ metadata: normalized.input.rule.metadata ?? null,
182
+ })
183
+ .returning({ id: notificationReminderRules.id });
184
+ if (!rule)
185
+ throw new Error("Failed to create notification reminder rule");
186
+ const result = { ruleId: rule.id, stages: [] };
187
+ for (let stageIndex = 0; stageIndex < normalized.input.stages.length; stageIndex += 1) {
188
+ const stage = normalized.input.stages[stageIndex];
189
+ const [stageRow] = await tx
190
+ .insert(notificationReminderRuleStages)
191
+ .values({
192
+ reminderRuleId: rule.id,
193
+ orderIndex: stage.orderIndex,
194
+ name: stage.name ?? null,
195
+ anchor: stage.anchor,
196
+ windowStartDays: stage.windowStartDays,
197
+ windowEndDays: stage.windowEndDays,
198
+ cadenceKind: stage.cadenceKind,
199
+ cadenceEveryDays: stage.cadenceEveryDays ?? null,
200
+ cadenceIntervals: stage.cadenceIntervals ?? null,
201
+ maxSendsInStage: stage.maxSendsInStage ?? null,
202
+ respectQuietHours: stage.respectQuietHours,
203
+ metadata: stage.metadata ?? null,
204
+ })
205
+ .returning({ id: notificationReminderRuleStages.id });
206
+ if (!stageRow)
207
+ throw new Error("Failed to create notification reminder stage");
208
+ const stageResult = {
209
+ id: stageRow.id,
210
+ orderIndex: stage.orderIndex,
211
+ channels: [],
212
+ };
213
+ for (let channelIndex = 0; channelIndex < stage.channels.length; channelIndex += 1) {
214
+ const channel = stage.channels[channelIndex];
215
+ const template = normalized.channels[stageIndex][channelIndex];
216
+ const [channelRow] = await tx
217
+ .insert(notificationReminderStageChannels)
218
+ .values({
219
+ stageId: stageRow.id,
220
+ orderIndex: channel.orderIndex,
221
+ channel: channel.channel,
222
+ provider: channel.provider ?? null,
223
+ templateId: template.templateId,
224
+ templateSlug: template.templateSlug,
225
+ recipientKind: channel.recipientKind,
226
+ recipientRole: channel.recipientRole ?? null,
227
+ metadata: channel.metadata ?? null,
228
+ })
229
+ .returning({
230
+ id: notificationReminderStageChannels.id,
231
+ orderIndex: notificationReminderStageChannels.orderIndex,
232
+ });
233
+ if (!channelRow)
234
+ throw new Error("Failed to create notification reminder stage channel");
235
+ stageResult.channels.push({ id: channelRow.id, orderIndex: channelRow.orderIndex });
236
+ }
237
+ result.stages.push(stageResult);
238
+ }
239
+ return result;
240
+ }
241
+ export async function composeNotificationReminderRule(db, input, options = {}) {
242
+ const normalized = await validateAndNormalizeComposeInput(db, input);
243
+ if (!normalized.ok)
244
+ return { status: "invalid", issues: normalized.issues };
245
+ const { result, reused } = await db.transaction((tx) => withIdempotency(tx, options.idempotencyKey ?? input.idempotencyKey, () => buildReminderRuleGraph(tx, normalized)));
246
+ return { status: "ok", result, reused };
247
+ }
@@ -0,0 +1,94 @@
1
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
+ import { type BookingDocumentAttachmentResolver } from "./service-booking-documents.js";
3
+ import type { BookingDocumentBundleItem, BookingPaymentScheduleRow } from "./service-shared.js";
4
+ import type { NotificationAttachment } from "./types.js";
5
+ export declare const PAYABLE_BOOKING_STATUSES: Set<string>;
6
+ export declare const OPEN_PAYMENT_SCHEDULE_STATUSES: Set<string>;
7
+ export interface BookingEventReminderRuntimeOptions {
8
+ documentAttachmentResolver?: BookingDocumentAttachmentResolver;
9
+ }
10
+ export declare function paymentScheduleStatusSkipReason(status: string): string;
11
+ export declare function bookingStatusSkipReason(status: string): string;
12
+ export declare function getPaymentReminderBookingStatusSkipReason(db: PostgresJsDatabase, bookingId: string): Promise<string | null>;
13
+ export declare function getBookingPaymentNotificationContext(db: PostgresJsDatabase, bookingId: string): Promise<{
14
+ paymentSchedule: {
15
+ id: string;
16
+ bookingId: string;
17
+ scheduleType: "other" | "deposit" | "installment" | "balance" | "hold";
18
+ status: "paid" | "pending" | "cancelled" | "due" | "waived" | "expired";
19
+ dueDate: string;
20
+ currency: string;
21
+ amountCents: number;
22
+ createdAt: Date;
23
+ } | null;
24
+ invoice: {
25
+ id: string;
26
+ invoiceNumber: string;
27
+ invoiceType: "invoice" | "proforma" | "credit_note";
28
+ status: "void" | "draft" | "paid" | "pending_external_allocation" | "issued" | "partially_paid" | "overdue";
29
+ currency: string;
30
+ subtotalCents: number;
31
+ taxCents: number;
32
+ totalCents: number;
33
+ paidCents: number;
34
+ balanceDueCents: number;
35
+ issueDate: string;
36
+ dueDate: string;
37
+ } | null;
38
+ paymentSession: {
39
+ id: string;
40
+ status: "paid" | "pending" | "failed" | "cancelled" | "processing" | "expired" | "authorized" | "requires_redirect";
41
+ provider: string | null;
42
+ currency: string;
43
+ amountCents: number;
44
+ redirectUrl: string | null;
45
+ returnUrl: string | null;
46
+ cancelUrl: string | null;
47
+ expiresAt: Date | null;
48
+ paymentMethod: "other" | "credit_card" | "bank_transfer" | "voucher" | "debit_card" | "wallet" | "direct_bill" | "cash" | "cheque" | null;
49
+ externalReference: string | null;
50
+ } | null;
51
+ }>;
52
+ export declare function getBookingEventDocumentContext(db: PostgresJsDatabase, bookingId: string, attachmentResolver?: BookingDocumentAttachmentResolver): Promise<{
53
+ documents: BookingDocumentBundleItem[];
54
+ attachments: NotificationAttachment[];
55
+ }>;
56
+ export declare function serializeBookingPaymentContext(context: Awaited<ReturnType<typeof getBookingPaymentNotificationContext>>, paymentScheduleOverride?: BookingPaymentScheduleRow | null): {
57
+ invoice: {
58
+ id: string;
59
+ invoiceNumber: string;
60
+ invoiceType: "invoice" | "proforma" | "credit_note";
61
+ status: "void" | "draft" | "paid" | "pending_external_allocation" | "issued" | "partially_paid" | "overdue";
62
+ currency: string;
63
+ subtotalCents: number;
64
+ taxCents: number;
65
+ totalCents: number;
66
+ paidCents: number;
67
+ balanceDueCents: number;
68
+ issueDate: string;
69
+ dueDate: string;
70
+ } | null;
71
+ paymentSession: {
72
+ id: string;
73
+ status: "paid" | "pending" | "failed" | "cancelled" | "processing" | "expired" | "authorized" | "requires_redirect";
74
+ provider: string | null;
75
+ currency: string;
76
+ amountCents: number;
77
+ redirectUrl: string | null;
78
+ returnUrl: string | null;
79
+ cancelUrl: string | null;
80
+ expiresAt: Date | null;
81
+ paymentMethod: "other" | "credit_card" | "bank_transfer" | "voucher" | "debit_card" | "wallet" | "direct_bill" | "cash" | "cheque" | null;
82
+ externalReference: string | null;
83
+ } | null;
84
+ paymentSchedule: {
85
+ id: string;
86
+ dueDate: string;
87
+ amountCents: number;
88
+ currency: string;
89
+ scheduleType: "other" | "deposit" | "installment" | "balance" | "hold";
90
+ status: "paid" | "pending" | "cancelled" | "due" | "waived" | "expired";
91
+ } | null;
92
+ };
93
+ export declare function hasOutstandingBookingBalance(db: PostgresJsDatabase, bookingId: string): Promise<boolean>;
94
+ //# sourceMappingURL=service-reminder-booking-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-reminder-booking-context.d.ts","sourceRoot":"","sources":["../src/service-reminder-booking-context.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,OAAO,EACL,KAAK,iCAAiC,EAGvC,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAA;AAC/F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAA;AAExD,eAAO,MAAM,wBAAwB,aAKnC,CAAA;AACF,eAAO,MAAM,8BAA8B,aAA8B,CAAA;AAEzE,MAAM,WAAW,kCAAkC;IACjD,0BAA0B,CAAC,EAAE,iCAAiC,CAAA;CAC/D;AAED,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,MAAM,UAE7D;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,UAErD;AAED,wBAAsB,yCAAyC,CAC7D,EAAE,EAAE,kBAAkB,EACtB,SAAS,EAAE,MAAM,0BAelB;AAED,wBAAsB,oCAAoC,CACxD,EAAE,EAAE,kBAAkB,EACtB,SAAS,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsElB;AAED,wBAAsB,8BAA8B,CAClD,EAAE,EAAE,kBAAkB,EACtB,SAAS,EAAE,MAAM,EACjB,kBAAkB,CAAC,EAAE,iCAAiC,GACrD,OAAO,CAAC;IACT,SAAS,EAAE,yBAAyB,EAAE,CAAA;IACtC,WAAW,EAAE,sBAAsB,EAAE,CAAA;CACtC,CAAC,CAgBD;AAED,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,oCAAoC,CAAC,CAAC,EACzE,uBAAuB,CAAC,EAAE,yBAAyB,GAAG,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+C3D;AAED,wBAAsB,4BAA4B,CAAC,EAAE,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,oBAoC3F"}
@@ -0,0 +1,164 @@
1
+ import { bookings } from "@voyant-travel/bookings/schema";
2
+ import { bookingPaymentSchedules, invoices, paymentSessions } from "@voyant-travel/finance";
3
+ import { and, asc, desc, eq, gt, or } from "drizzle-orm";
4
+ import { bookingDocumentNotificationsService, createDefaultBookingDocumentAttachment, } from "./service-booking-documents.js";
5
+ export const PAYABLE_BOOKING_STATUSES = new Set([
6
+ "on_hold",
7
+ "awaiting_payment",
8
+ "confirmed",
9
+ "in_progress",
10
+ ]);
11
+ export const OPEN_PAYMENT_SCHEDULE_STATUSES = new Set(["pending", "due"]);
12
+ export function paymentScheduleStatusSkipReason(status) {
13
+ return `payment_schedule_status_${status}`;
14
+ }
15
+ export function bookingStatusSkipReason(status) {
16
+ return `booking_status_${status}`;
17
+ }
18
+ export async function getPaymentReminderBookingStatusSkipReason(db, bookingId) {
19
+ const [booking] = await db
20
+ .select({ status: bookings.status })
21
+ .from(bookings)
22
+ .where(eq(bookings.id, bookingId))
23
+ .limit(1);
24
+ if (!booking) {
25
+ return "booking_not_found";
26
+ }
27
+ return PAYABLE_BOOKING_STATUSES.has(booking.status)
28
+ ? null
29
+ : bookingStatusSkipReason(booking.status);
30
+ }
31
+ export async function getBookingPaymentNotificationContext(db, bookingId) {
32
+ const [[paymentSchedule], [invoice], [paymentSession]] = await Promise.all([
33
+ db
34
+ .select({
35
+ id: bookingPaymentSchedules.id,
36
+ bookingId: bookingPaymentSchedules.bookingId,
37
+ scheduleType: bookingPaymentSchedules.scheduleType,
38
+ status: bookingPaymentSchedules.status,
39
+ dueDate: bookingPaymentSchedules.dueDate,
40
+ currency: bookingPaymentSchedules.currency,
41
+ amountCents: bookingPaymentSchedules.amountCents,
42
+ createdAt: bookingPaymentSchedules.createdAt,
43
+ })
44
+ .from(bookingPaymentSchedules)
45
+ .where(and(eq(bookingPaymentSchedules.bookingId, bookingId), or(eq(bookingPaymentSchedules.status, "pending"), eq(bookingPaymentSchedules.status, "due"))))
46
+ .orderBy(asc(bookingPaymentSchedules.dueDate), asc(bookingPaymentSchedules.createdAt))
47
+ .limit(1),
48
+ db
49
+ .select({
50
+ id: invoices.id,
51
+ invoiceNumber: invoices.invoiceNumber,
52
+ invoiceType: invoices.invoiceType,
53
+ status: invoices.status,
54
+ currency: invoices.currency,
55
+ subtotalCents: invoices.subtotalCents,
56
+ taxCents: invoices.taxCents,
57
+ totalCents: invoices.totalCents,
58
+ paidCents: invoices.paidCents,
59
+ balanceDueCents: invoices.balanceDueCents,
60
+ issueDate: invoices.issueDate,
61
+ dueDate: invoices.dueDate,
62
+ })
63
+ .from(invoices)
64
+ .where(eq(invoices.bookingId, bookingId))
65
+ .orderBy(desc(invoices.createdAt))
66
+ .limit(1),
67
+ db
68
+ .select({
69
+ id: paymentSessions.id,
70
+ status: paymentSessions.status,
71
+ provider: paymentSessions.provider,
72
+ currency: paymentSessions.currency,
73
+ amountCents: paymentSessions.amountCents,
74
+ redirectUrl: paymentSessions.redirectUrl,
75
+ returnUrl: paymentSessions.returnUrl,
76
+ cancelUrl: paymentSessions.cancelUrl,
77
+ expiresAt: paymentSessions.expiresAt,
78
+ paymentMethod: paymentSessions.paymentMethod,
79
+ externalReference: paymentSessions.externalReference,
80
+ })
81
+ .from(paymentSessions)
82
+ .where(eq(paymentSessions.bookingId, bookingId))
83
+ .orderBy(desc(paymentSessions.createdAt))
84
+ .limit(1),
85
+ ]);
86
+ return {
87
+ paymentSchedule: paymentSchedule ?? null,
88
+ invoice: invoice ?? null,
89
+ paymentSession: paymentSession ?? null,
90
+ };
91
+ }
92
+ export async function getBookingEventDocumentContext(db, bookingId, attachmentResolver) {
93
+ const bundle = await bookingDocumentNotificationsService.listBookingDocumentBundle(db, bookingId);
94
+ const documents = bundle?.documents ?? [];
95
+ if (documents.length === 0) {
96
+ return { documents, attachments: [] };
97
+ }
98
+ const resolver = attachmentResolver ??
99
+ (async (document) => createDefaultBookingDocumentAttachment(document));
100
+ const attachments = (await Promise.all(documents.map((document) => resolver(document)))).filter((attachment) => Boolean(attachment));
101
+ return { documents, attachments };
102
+ }
103
+ export function serializeBookingPaymentContext(context, paymentScheduleOverride) {
104
+ const schedule = paymentScheduleOverride ?? context.paymentSchedule;
105
+ return {
106
+ invoice: context.invoice
107
+ ? {
108
+ id: context.invoice.id,
109
+ invoiceNumber: context.invoice.invoiceNumber,
110
+ invoiceType: context.invoice.invoiceType,
111
+ status: context.invoice.status,
112
+ currency: context.invoice.currency,
113
+ subtotalCents: context.invoice.subtotalCents,
114
+ taxCents: context.invoice.taxCents,
115
+ totalCents: context.invoice.totalCents,
116
+ paidCents: context.invoice.paidCents,
117
+ balanceDueCents: context.invoice.balanceDueCents,
118
+ issueDate: context.invoice.issueDate,
119
+ dueDate: context.invoice.dueDate,
120
+ }
121
+ : null,
122
+ paymentSession: context.paymentSession
123
+ ? {
124
+ id: context.paymentSession.id,
125
+ status: context.paymentSession.status,
126
+ provider: context.paymentSession.provider,
127
+ currency: context.paymentSession.currency,
128
+ amountCents: context.paymentSession.amountCents,
129
+ redirectUrl: context.paymentSession.redirectUrl,
130
+ returnUrl: context.paymentSession.returnUrl,
131
+ cancelUrl: context.paymentSession.cancelUrl,
132
+ expiresAt: context.paymentSession.expiresAt,
133
+ paymentMethod: context.paymentSession.paymentMethod,
134
+ externalReference: context.paymentSession.externalReference,
135
+ }
136
+ : null,
137
+ paymentSchedule: schedule
138
+ ? {
139
+ id: schedule.id,
140
+ dueDate: schedule.dueDate,
141
+ amountCents: schedule.amountCents,
142
+ currency: schedule.currency,
143
+ scheduleType: schedule.scheduleType,
144
+ status: schedule.status,
145
+ }
146
+ : null,
147
+ };
148
+ }
149
+ export async function hasOutstandingBookingBalance(db, bookingId) {
150
+ const [openSchedule] = await db
151
+ .select({ id: bookingPaymentSchedules.id })
152
+ .from(bookingPaymentSchedules)
153
+ .where(and(eq(bookingPaymentSchedules.bookingId, bookingId), or(eq(bookingPaymentSchedules.status, "pending"), eq(bookingPaymentSchedules.status, "due"))))
154
+ .limit(1);
155
+ if (openSchedule) {
156
+ return true;
157
+ }
158
+ const [openInvoice] = await db
159
+ .select({ id: invoices.id })
160
+ .from(invoices)
161
+ .where(and(eq(invoices.bookingId, bookingId), gt(invoices.balanceDueCents, 0), or(eq(invoices.status, "issued"), eq(invoices.status, "partially_paid"), eq(invoices.status, "overdue"))))
162
+ .limit(1);
163
+ return Boolean(openInvoice);
164
+ }
@@ -0,0 +1,33 @@
1
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
+ import { type BookingEventReminderRuntimeOptions } from "./service-reminder-booking-context.js";
3
+ import type { NotificationReminderRuleRow, NotificationService } from "./service-shared.js";
4
+ type ReminderTargetType = NotificationReminderRuleRow["targetType"];
5
+ type BookingEventReminderTargetType = Extract<ReminderTargetType, "booking_confirmed" | "payment_complete" | "booking_cancelled_non_payment">;
6
+ export declare function dispatchReminderEventRules(db: PostgresJsDatabase, dispatcher: NotificationService, input: {
7
+ targetType: BookingEventReminderTargetType;
8
+ bookingId: string;
9
+ paymentSessionId?: string | null;
10
+ eventData?: Record<string, unknown>;
11
+ }, runtime?: BookingEventReminderRuntimeOptions): Promise<({
12
+ metadata: Record<string, unknown> | null;
13
+ scheduledFor: Date;
14
+ status: "sent" | "failed" | "queued" | "processing" | "skipped";
15
+ id: string;
16
+ createdAt: Date;
17
+ updatedAt: Date;
18
+ targetType: "invoice" | "booking_payment_schedule" | "booking_confirmed" | "payment_complete" | "booking_cancelled_non_payment";
19
+ targetId: string;
20
+ bookingId: string | null;
21
+ paymentSessionId: string | null;
22
+ personId: string | null;
23
+ organizationId: string | null;
24
+ errorMessage: string | null;
25
+ reminderRuleId: string;
26
+ notificationDeliveryId: string | null;
27
+ recipient: string | null;
28
+ dedupeKey: string;
29
+ processedAt: Date;
30
+ } | null)[]>;
31
+ export declare function bookingIsPaidInFullForNotification(db: PostgresJsDatabase, bookingId: string): Promise<boolean>;
32
+ export type { BookingEventReminderRuntimeOptions } from "./service-reminder-booking-context.js";
33
+ //# sourceMappingURL=service-reminder-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-reminder-events.d.ts","sourceRoot":"","sources":["../src/service-reminder-events.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAIjE,OAAO,EACL,KAAK,kCAAkC,EAKxC,MAAM,uCAAuC,CAAA;AAM9C,OAAO,KAAK,EAAE,2BAA2B,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAO3F,KAAK,kBAAkB,GAAG,2BAA2B,CAAC,YAAY,CAAC,CAAA;AACnE,KAAK,8BAA8B,GAAG,OAAO,CAC3C,kBAAkB,EAClB,mBAAmB,GAAG,kBAAkB,GAAG,+BAA+B,CAC3E,CAAA;AAwLD,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,kBAAkB,EACtB,UAAU,EAAE,mBAAmB,EAC/B,KAAK,EAAE;IACL,UAAU,EAAE,8BAA8B,CAAA;IAC1C,SAAS,EAAE,MAAM,CAAA;IACjB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC,EACD,OAAO,GAAE,kCAAuC;;;;;;;;;;;;;;;;;;;aAmBjD;AAED,wBAAsB,kCAAkC,CACtD,EAAE,EAAE,kBAAkB,EACtB,SAAS,EAAE,MAAM,oBAGlB;AAED,YAAY,EAAE,kCAAkC,EAAE,MAAM,uCAAuC,CAAA"}