@voyantjs/notifications 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import type { HonoModule } from "@voyantjs/hono/module";
2
2
  import { createNotificationsRoutes } from "./routes.js";
3
3
  export type { DefaultNotificationProviderOptions } from "./provider-resolution.js";
4
- export { createDefaultNotificationProviders, createResendProviderFromEnv, } from "./provider-resolution.js";
4
+ export { createDefaultNotificationProviders, createResendProviderFromEnv, createTwilioProviderFromEnv, } from "./provider-resolution.js";
5
5
  export type { LocalProviderOptions } from "./providers/local.js";
6
6
  export { createLocalProvider } from "./providers/local.js";
7
7
  export type { ResendFetch, ResendProviderOptions, ResendRenderedEmail, } from "./providers/resend.js";
8
8
  export { createResendProvider } from "./providers/resend.js";
9
+ export type { TwilioFetch, TwilioProviderOptions, TwilioRenderedSms } from "./providers/twilio.js";
10
+ export { createTwilioProvider } from "./providers/twilio.js";
9
11
  export { createNotificationsRoutes } from "./routes.js";
10
12
  export type { NewNotificationDelivery, NewNotificationReminderRule, NewNotificationReminderRun, NewNotificationTemplate, NotificationDelivery, NotificationReminderRule, NotificationReminderRun, NotificationsHonoModule, NotificationTemplate, } from "./schema.js";
11
13
  export { notificationChannelEnum, notificationDeliveries, notificationDeliveryStatusEnum, notificationReminderRules, notificationReminderRunStatusEnum, notificationReminderRuns, notificationReminderStatusEnum, notificationReminderTargetTypeEnum, notificationsModule, notificationTargetTypeEnum, notificationTemplateStatusEnum, notificationTemplates, } from "./schema.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAEvD,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAA;AAGvD,YAAY,EAAE,kCAAkC,EAAE,MAAM,0BAA0B,CAAA;AAClF,OAAO,EACL,kCAAkC,EAClC,2BAA2B,GAC5B,MAAM,0BAA0B,CAAA;AACjC,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC1D,YAAY,EACV,WAAW,EACX,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAC5D,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAA;AACvD,YAAY,EACV,uBAAuB,EACvB,2BAA2B,EAC3B,0BAA0B,EAC1B,uBAAuB,EACvB,oBAAoB,EACpB,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,8BAA8B,EAC9B,yBAAyB,EACzB,iCAAiC,EACjC,wBAAwB,EACxB,8BAA8B,EAC9B,kCAAkC,EAClC,mBAAmB,EACnB,0BAA0B,EAC1B,8BAA8B,EAC9B,qBAAqB,GACtB,MAAM,aAAa,CAAA;AACpB,YAAY,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AACvD,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EACjB,oBAAoB,EACpB,0BAA0B,GAC3B,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,4BAA4B,EAAE,MAAM,kBAAkB,CAAA;AAC/D,YAAY,EACV,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,oCAAoC,EACpC,gCAAgC,EAChC,yBAAyB,EACzB,mCAAmC,EACnC,gCAAgC,EAChC,uCAAuC,EACvC,sCAAsC,EACtC,mCAAmC,EACnC,gCAAgC,EAChC,oCAAoC,EACpC,4BAA4B,EAC5B,mCAAmC,EACnC,gCAAgC,EAChC,qBAAqB,EACrB,6BAA6B,EAC7B,sBAAsB,EACtB,oCAAoC,EACpC,oCAAoC,EACpC,gCAAgC,GACjC,MAAM,iBAAiB,CAAA;AAExB,wBAAgB,6BAA6B,CAC3C,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAC,CAAC,CAAC,GACxD,UAAU,CAKZ"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAEvD,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAA;AAGvD,YAAY,EAAE,kCAAkC,EAAE,MAAM,0BAA0B,CAAA;AAClF,OAAO,EACL,kCAAkC,EAClC,2BAA2B,EAC3B,2BAA2B,GAC5B,MAAM,0BAA0B,CAAA;AACjC,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC1D,YAAY,EACV,WAAW,EACX,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAC5D,YAAY,EAAE,WAAW,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAClG,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAC5D,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAA;AACvD,YAAY,EACV,uBAAuB,EACvB,2BAA2B,EAC3B,0BAA0B,EAC1B,uBAAuB,EACvB,oBAAoB,EACpB,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,8BAA8B,EAC9B,yBAAyB,EACzB,iCAAiC,EACjC,wBAAwB,EACxB,8BAA8B,EAC9B,kCAAkC,EAClC,mBAAmB,EACnB,0BAA0B,EAC1B,8BAA8B,EAC9B,qBAAqB,GACtB,MAAM,aAAa,CAAA;AACpB,YAAY,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AACvD,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EACjB,oBAAoB,EACpB,0BAA0B,GAC3B,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,4BAA4B,EAAE,MAAM,kBAAkB,CAAA;AAC/D,YAAY,EACV,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,oCAAoC,EACpC,gCAAgC,EAChC,yBAAyB,EACzB,mCAAmC,EACnC,gCAAgC,EAChC,uCAAuC,EACvC,sCAAsC,EACtC,mCAAmC,EACnC,gCAAgC,EAChC,oCAAoC,EACpC,4BAA4B,EAC5B,mCAAmC,EACnC,gCAAgC,EAChC,qBAAqB,EACrB,6BAA6B,EAC7B,sBAAsB,EACtB,oCAAoC,EACpC,oCAAoC,EACpC,gCAAgC,GACjC,MAAM,iBAAiB,CAAA;AAExB,wBAAgB,6BAA6B,CAC3C,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAC,CAAC,CAAC,GACxD,UAAU,CAKZ"}
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { createNotificationsRoutes } from "./routes.js";
2
2
  import { notificationsModule } from "./schema.js";
3
- export { createDefaultNotificationProviders, createResendProviderFromEnv, } from "./provider-resolution.js";
3
+ export { createDefaultNotificationProviders, createResendProviderFromEnv, createTwilioProviderFromEnv, } from "./provider-resolution.js";
4
4
  export { createLocalProvider } from "./providers/local.js";
5
5
  export { createResendProvider } from "./providers/resend.js";
6
+ export { createTwilioProvider } from "./providers/twilio.js";
6
7
  export { createNotificationsRoutes } from "./routes.js";
7
8
  export { notificationChannelEnum, notificationDeliveries, notificationDeliveryStatusEnum, notificationReminderRules, notificationReminderRunStatusEnum, notificationReminderRuns, notificationReminderStatusEnum, notificationReminderTargetTypeEnum, notificationsModule, notificationTargetTypeEnum, notificationTemplateStatusEnum, notificationTemplates, } from "./schema.js";
8
9
  export { createNotificationService, NotificationError, notificationsService, renderNotificationTemplate, } from "./service.js";
@@ -2,13 +2,18 @@ import type { NotificationProvider } from "./types.js";
2
2
  type NotificationProviderEnv = {
3
3
  RESEND_API_KEY?: unknown;
4
4
  EMAIL_FROM?: unknown;
5
+ TWILIO_ACCOUNT_SID?: unknown;
6
+ TWILIO_AUTH_TOKEN?: unknown;
7
+ TWILIO_SMS_FROM?: unknown;
5
8
  };
6
9
  export interface DefaultNotificationProviderOptions {
7
10
  includeLocal?: boolean;
8
11
  emailProvider?: "resend" | null;
12
+ smsProvider?: "twilio" | null;
9
13
  customProviders?: ReadonlyArray<NotificationProvider>;
10
14
  }
11
15
  export declare function createResendProviderFromEnv(env: NotificationProviderEnv): NotificationProvider | null;
16
+ export declare function createTwilioProviderFromEnv(env: NotificationProviderEnv): NotificationProvider | null;
12
17
  export declare function createDefaultNotificationProviders(env: NotificationProviderEnv, options?: DefaultNotificationProviderOptions): NotificationProvider[];
13
18
  export {};
14
19
  //# sourceMappingURL=provider-resolution.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"provider-resolution.d.ts","sourceRoot":"","sources":["../src/provider-resolution.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAEtD,KAAK,uBAAuB,GAAG;IAC7B,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB,CAAA;AAED,MAAM,WAAW,kCAAkC;IACjD,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,aAAa,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAA;IAC/B,eAAe,CAAC,EAAE,aAAa,CAAC,oBAAoB,CAAC,CAAA;CACtD;AAED,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,uBAAuB,+BAcvE;AAED,wBAAgB,kCAAkC,CAChD,GAAG,EAAE,uBAAuB,EAC5B,OAAO,GAAE,kCAAuC,0BAoBjD"}
1
+ {"version":3,"file":"provider-resolution.d.ts","sourceRoot":"","sources":["../src/provider-resolution.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAEtD,KAAK,uBAAuB,GAAG;IAC7B,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B,CAAA;AAED,MAAM,WAAW,kCAAkC;IACjD,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,aAAa,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAA;IAC/B,WAAW,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAA;IAC7B,eAAe,CAAC,EAAE,aAAa,CAAC,oBAAoB,CAAC,CAAA;CACtD;AAED,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,uBAAuB,+BAcvE;AAED,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,uBAAuB,+BAiBvE;AAED,wBAAgB,kCAAkC,CAChD,GAAG,EAAE,uBAAuB,EAC5B,OAAO,GAAE,kCAAuC,0BA2BjD"}
@@ -1,5 +1,6 @@
1
1
  import { createLocalProvider } from "./providers/local.js";
2
2
  import { createResendProvider } from "./providers/resend.js";
3
+ import { createTwilioProvider } from "./providers/twilio.js";
3
4
  export function createResendProviderFromEnv(env) {
4
5
  if (typeof env.RESEND_API_KEY === "string" &&
5
6
  env.RESEND_API_KEY &&
@@ -12,6 +13,21 @@ export function createResendProviderFromEnv(env) {
12
13
  }
13
14
  return null;
14
15
  }
16
+ export function createTwilioProviderFromEnv(env) {
17
+ if (typeof env.TWILIO_ACCOUNT_SID === "string" &&
18
+ env.TWILIO_ACCOUNT_SID &&
19
+ typeof env.TWILIO_AUTH_TOKEN === "string" &&
20
+ env.TWILIO_AUTH_TOKEN &&
21
+ typeof env.TWILIO_SMS_FROM === "string" &&
22
+ env.TWILIO_SMS_FROM) {
23
+ return createTwilioProvider({
24
+ accountSid: env.TWILIO_ACCOUNT_SID,
25
+ authToken: env.TWILIO_AUTH_TOKEN,
26
+ from: env.TWILIO_SMS_FROM,
27
+ });
28
+ }
29
+ return null;
30
+ }
15
31
  export function createDefaultNotificationProviders(env, options = {}) {
16
32
  const providers = [];
17
33
  if (options.includeLocal !== false) {
@@ -23,6 +39,12 @@ export function createDefaultNotificationProviders(env, options = {}) {
23
39
  providers.push(resend);
24
40
  }
25
41
  }
42
+ if (options.smsProvider === "twilio") {
43
+ const twilio = createTwilioProviderFromEnv(env);
44
+ if (twilio) {
45
+ providers.push(twilio);
46
+ }
47
+ }
26
48
  if (options.customProviders?.length) {
27
49
  providers.push(...options.customProviders);
28
50
  }
@@ -0,0 +1,24 @@
1
+ import type { NotificationProvider } from "../types.js";
2
+ export type TwilioFetch = (input: string, init: {
3
+ method: string;
4
+ headers: Record<string, string>;
5
+ body: string;
6
+ }) => Promise<{
7
+ ok: boolean;
8
+ status: number;
9
+ json: () => Promise<unknown>;
10
+ text: () => Promise<string>;
11
+ }>;
12
+ export interface TwilioRenderedSms {
13
+ text: string;
14
+ }
15
+ export interface TwilioProviderOptions {
16
+ accountSid: string;
17
+ authToken: string;
18
+ from: string;
19
+ baseUrl?: string;
20
+ fetch?: TwilioFetch;
21
+ renderTemplate?: (template: string, data: unknown) => Promise<TwilioRenderedSms> | TwilioRenderedSms;
22
+ }
23
+ export declare function createTwilioProvider(options: TwilioProviderOptions): NotificationProvider;
24
+ //# sourceMappingURL=twilio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"twilio.d.ts","sourceRoot":"","sources":["../../src/providers/twilio.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAsB,MAAM,aAAa,CAAA;AAE3E,MAAM,MAAM,WAAW,GAAG,CACxB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE;IACJ,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,IAAI,EAAE,MAAM,CAAA;CACb,KACE,OAAO,CAAC;IACX,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;IAC5B,IAAI,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;CAC5B,CAAC,CAAA;AAEF,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,cAAc,CAAC,EAAE,CACf,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,OAAO,KACV,OAAO,CAAC,iBAAiB,CAAC,GAAG,iBAAiB,CAAA;CACpD;AA0BD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,oBAAoB,CA4CzF"}
@@ -0,0 +1,48 @@
1
+ function toBase64(value) {
2
+ if (typeof btoa === "function") {
3
+ return btoa(value);
4
+ }
5
+ const globals = globalThis;
6
+ if (globals.Buffer) {
7
+ return globals.Buffer.from(value).toString("base64");
8
+ }
9
+ throw new Error("Twilio provider requires a base64 encoder");
10
+ }
11
+ export function createTwilioProvider(options) {
12
+ const baseUrl = options.baseUrl ?? "https://api.twilio.com/2010-04-01";
13
+ const fetchImpl = options.fetch ?? globalThis.fetch;
14
+ return {
15
+ name: "twilio",
16
+ channels: ["sms"],
17
+ async send(payload) {
18
+ if (payload.channel !== "sms") {
19
+ throw new Error(`Twilio provider only supports the "sms" channel, got "${payload.channel}"`);
20
+ }
21
+ if (!fetchImpl) {
22
+ throw new Error("Twilio provider requires a fetch implementation");
23
+ }
24
+ const rendered = options.renderTemplate
25
+ ? await options.renderTemplate(payload.template, payload.data)
26
+ : { text: payload.text ?? JSON.stringify(payload.data ?? {}) };
27
+ const body = new URLSearchParams({
28
+ To: payload.to,
29
+ From: payload.from ?? options.from,
30
+ Body: payload.text ?? rendered.text,
31
+ });
32
+ const response = await fetchImpl(`${baseUrl}/Accounts/${options.accountSid}/Messages.json`, {
33
+ method: "POST",
34
+ headers: {
35
+ Authorization: `Basic ${toBase64(`${options.accountSid}:${options.authToken}`)}`,
36
+ "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
37
+ },
38
+ body: body.toString(),
39
+ });
40
+ if (!response.ok) {
41
+ const text = await response.text().catch(() => "");
42
+ throw new Error(`Twilio send failed (${response.status}): ${text}`);
43
+ }
44
+ const data = (await response.json());
45
+ return { id: data.sid, provider: "twilio" };
46
+ },
47
+ };
48
+ }
@@ -0,0 +1,153 @@
1
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
+ import type { NotificationDeliveryListQuery, NotificationService, SendInvoiceNotificationInput, SendNotificationInput, SendPaymentSessionNotificationInput } from "./service-shared.js";
3
+ export declare function listDeliveries(db: PostgresJsDatabase, query: NotificationDeliveryListQuery): Promise<{
4
+ data: {
5
+ id: string;
6
+ templateId: string | null;
7
+ templateSlug: string | null;
8
+ targetType: "other" | "booking" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "payment_session" | "person" | "organization";
9
+ targetId: string | null;
10
+ personId: string | null;
11
+ organizationId: string | null;
12
+ bookingId: string | null;
13
+ invoiceId: string | null;
14
+ paymentSessionId: string | null;
15
+ channel: "email" | "sms";
16
+ provider: string;
17
+ providerMessageId: string | null;
18
+ status: "cancelled" | "pending" | "failed" | "sent";
19
+ toAddress: string;
20
+ fromAddress: string | null;
21
+ subject: string | null;
22
+ htmlBody: string | null;
23
+ textBody: string | null;
24
+ payloadData: Record<string, unknown> | null;
25
+ metadata: Record<string, unknown> | null;
26
+ errorMessage: string | null;
27
+ scheduledFor: Date | null;
28
+ sentAt: Date | null;
29
+ failedAt: Date | null;
30
+ createdAt: Date;
31
+ updatedAt: Date;
32
+ }[];
33
+ total: number;
34
+ limit: number;
35
+ offset: number;
36
+ }>;
37
+ export declare function getDeliveryById(db: PostgresJsDatabase, id: string): Promise<{
38
+ id: string;
39
+ templateId: string | null;
40
+ templateSlug: string | null;
41
+ targetType: "other" | "booking" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "payment_session" | "person" | "organization";
42
+ targetId: string | null;
43
+ personId: string | null;
44
+ organizationId: string | null;
45
+ bookingId: string | null;
46
+ invoiceId: string | null;
47
+ paymentSessionId: string | null;
48
+ channel: "email" | "sms";
49
+ provider: string;
50
+ providerMessageId: string | null;
51
+ status: "cancelled" | "pending" | "failed" | "sent";
52
+ toAddress: string;
53
+ fromAddress: string | null;
54
+ subject: string | null;
55
+ htmlBody: string | null;
56
+ textBody: string | null;
57
+ payloadData: Record<string, unknown> | null;
58
+ metadata: Record<string, unknown> | null;
59
+ errorMessage: string | null;
60
+ scheduledFor: Date | null;
61
+ sentAt: Date | null;
62
+ failedAt: Date | null;
63
+ createdAt: Date;
64
+ updatedAt: Date;
65
+ } | null>;
66
+ export declare function sendNotification(db: PostgresJsDatabase, dispatcher: NotificationService, input: SendNotificationInput): Promise<{
67
+ id: string;
68
+ templateId: string | null;
69
+ templateSlug: string | null;
70
+ targetType: "other" | "booking" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "payment_session" | "person" | "organization";
71
+ targetId: string | null;
72
+ personId: string | null;
73
+ organizationId: string | null;
74
+ bookingId: string | null;
75
+ invoiceId: string | null;
76
+ paymentSessionId: string | null;
77
+ channel: "email" | "sms";
78
+ provider: string;
79
+ providerMessageId: string | null;
80
+ status: "cancelled" | "pending" | "failed" | "sent";
81
+ toAddress: string;
82
+ fromAddress: string | null;
83
+ subject: string | null;
84
+ htmlBody: string | null;
85
+ textBody: string | null;
86
+ payloadData: Record<string, unknown> | null;
87
+ metadata: Record<string, unknown> | null;
88
+ errorMessage: string | null;
89
+ scheduledFor: Date | null;
90
+ sentAt: Date | null;
91
+ failedAt: Date | null;
92
+ createdAt: Date;
93
+ updatedAt: Date;
94
+ } | null>;
95
+ export declare function sendPaymentSessionNotification(db: PostgresJsDatabase, dispatcher: NotificationService, sessionId: string, input: SendPaymentSessionNotificationInput): Promise<{
96
+ id: string;
97
+ templateId: string | null;
98
+ templateSlug: string | null;
99
+ targetType: "other" | "booking" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "payment_session" | "person" | "organization";
100
+ targetId: string | null;
101
+ personId: string | null;
102
+ organizationId: string | null;
103
+ bookingId: string | null;
104
+ invoiceId: string | null;
105
+ paymentSessionId: string | null;
106
+ channel: "email" | "sms";
107
+ provider: string;
108
+ providerMessageId: string | null;
109
+ status: "cancelled" | "pending" | "failed" | "sent";
110
+ toAddress: string;
111
+ fromAddress: string | null;
112
+ subject: string | null;
113
+ htmlBody: string | null;
114
+ textBody: string | null;
115
+ payloadData: Record<string, unknown> | null;
116
+ metadata: Record<string, unknown> | null;
117
+ errorMessage: string | null;
118
+ scheduledFor: Date | null;
119
+ sentAt: Date | null;
120
+ failedAt: Date | null;
121
+ createdAt: Date;
122
+ updatedAt: Date;
123
+ } | null>;
124
+ export declare function sendInvoiceNotification(db: PostgresJsDatabase, dispatcher: NotificationService, invoiceId: string, input: SendInvoiceNotificationInput): Promise<{
125
+ id: string;
126
+ templateId: string | null;
127
+ templateSlug: string | null;
128
+ targetType: "other" | "booking" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "payment_session" | "person" | "organization";
129
+ targetId: string | null;
130
+ personId: string | null;
131
+ organizationId: string | null;
132
+ bookingId: string | null;
133
+ invoiceId: string | null;
134
+ paymentSessionId: string | null;
135
+ channel: "email" | "sms";
136
+ provider: string;
137
+ providerMessageId: string | null;
138
+ status: "cancelled" | "pending" | "failed" | "sent";
139
+ toAddress: string;
140
+ fromAddress: string | null;
141
+ subject: string | null;
142
+ htmlBody: string | null;
143
+ textBody: string | null;
144
+ payloadData: Record<string, unknown> | null;
145
+ metadata: Record<string, unknown> | null;
146
+ errorMessage: string | null;
147
+ scheduledFor: Date | null;
148
+ sentAt: Date | null;
149
+ failedAt: Date | null;
150
+ createdAt: Date;
151
+ updatedAt: Date;
152
+ } | null>;
153
+ //# sourceMappingURL=service-deliveries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-deliveries.d.ts","sourceRoot":"","sources":["../src/service-deliveries.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAGjE,OAAO,KAAK,EACV,6BAA6B,EAC7B,mBAAmB,EACnB,4BAA4B,EAC5B,qBAAqB,EACrB,mCAAmC,EACpC,MAAM,qBAAqB,CAAA;AAY5B,wBAAsB,cAAc,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgChG;AAED,wBAAsB,eAAe,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAOvE;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,kBAAkB,EACtB,UAAU,EAAE,mBAAmB,EAC/B,KAAK,EAAE,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAmH7B;AAED,wBAAsB,8BAA8B,CAClD,EAAE,EAAE,kBAAkB,EACtB,UAAU,EAAE,mBAAmB,EAC/B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA8F3C;AAED,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,kBAAkB,EACtB,UAAU,EAAE,mBAAmB,EAC/B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA4FpC"}
@@ -0,0 +1,334 @@
1
+ import { bookings } from "@voyantjs/bookings/schema";
2
+ import { invoices, paymentSessions } from "@voyantjs/finance";
3
+ import { desc, eq, sql } from "drizzle-orm";
4
+ import { notificationDeliveries } from "./schema.js";
5
+ import { buildWhereClause, listBookingNotificationParticipants, NotificationError, paginate, renderNotificationTemplate, resolveReminderRecipient, toTimestamp, } from "./service-shared.js";
6
+ import { getTemplateById, getTemplateBySlug } from "./service-templates.js";
7
+ export async function listDeliveries(db, query) {
8
+ const conditions = [];
9
+ if (query.channel)
10
+ conditions.push(eq(notificationDeliveries.channel, query.channel));
11
+ if (query.provider)
12
+ conditions.push(eq(notificationDeliveries.provider, query.provider));
13
+ if (query.status)
14
+ conditions.push(eq(notificationDeliveries.status, query.status));
15
+ if (query.templateSlug)
16
+ conditions.push(eq(notificationDeliveries.templateSlug, query.templateSlug));
17
+ if (query.targetType)
18
+ conditions.push(eq(notificationDeliveries.targetType, query.targetType));
19
+ if (query.targetId)
20
+ conditions.push(eq(notificationDeliveries.targetId, query.targetId));
21
+ if (query.bookingId)
22
+ conditions.push(eq(notificationDeliveries.bookingId, query.bookingId));
23
+ if (query.invoiceId)
24
+ conditions.push(eq(notificationDeliveries.invoiceId, query.invoiceId));
25
+ if (query.paymentSessionId) {
26
+ conditions.push(eq(notificationDeliveries.paymentSessionId, query.paymentSessionId));
27
+ }
28
+ if (query.personId)
29
+ conditions.push(eq(notificationDeliveries.personId, query.personId));
30
+ if (query.organizationId) {
31
+ conditions.push(eq(notificationDeliveries.organizationId, query.organizationId));
32
+ }
33
+ const where = buildWhereClause(conditions);
34
+ return paginate(db
35
+ .select()
36
+ .from(notificationDeliveries)
37
+ .where(where)
38
+ .limit(query.limit)
39
+ .offset(query.offset)
40
+ .orderBy(desc(notificationDeliveries.createdAt)), db.select({ total: sql `count(*)::int` }).from(notificationDeliveries).where(where), query.limit, query.offset);
41
+ }
42
+ export async function getDeliveryById(db, id) {
43
+ const [row] = await db
44
+ .select()
45
+ .from(notificationDeliveries)
46
+ .where(eq(notificationDeliveries.id, id))
47
+ .limit(1);
48
+ return row ?? null;
49
+ }
50
+ export async function sendNotification(db, dispatcher, input) {
51
+ let template = null;
52
+ if (input.templateId) {
53
+ template = await getTemplateById(db, input.templateId);
54
+ }
55
+ else if (input.templateSlug) {
56
+ template = await getTemplateBySlug(db, input.templateSlug);
57
+ }
58
+ if ((input.templateId || input.templateSlug) && !template) {
59
+ throw new NotificationError("Notification template not found");
60
+ }
61
+ const data = input.data ?? {};
62
+ const channel = input.channel ?? template?.channel;
63
+ if (!channel) {
64
+ throw new NotificationError("Notification channel is required");
65
+ }
66
+ const provider = input.provider ?? template?.provider ?? dispatcher.getProvider(channel)?.name;
67
+ if (!provider) {
68
+ throw new NotificationError(`No notification provider available for channel "${channel}"`);
69
+ }
70
+ const subject = input.subject ?? renderNotificationTemplate(template?.subjectTemplate, data);
71
+ const html = input.html ?? renderNotificationTemplate(template?.htmlTemplate, data);
72
+ const text = input.text ?? renderNotificationTemplate(template?.textTemplate, data);
73
+ const [pending] = await db
74
+ .insert(notificationDeliveries)
75
+ .values({
76
+ templateId: template?.id ?? null,
77
+ templateSlug: template?.slug ?? input.templateSlug ?? null,
78
+ targetType: input.targetType,
79
+ targetId: input.targetId ?? null,
80
+ personId: input.personId ?? null,
81
+ organizationId: input.organizationId ?? null,
82
+ bookingId: input.bookingId ?? null,
83
+ invoiceId: input.invoiceId ?? null,
84
+ paymentSessionId: input.paymentSessionId ?? null,
85
+ channel,
86
+ provider,
87
+ providerMessageId: null,
88
+ status: "pending",
89
+ toAddress: input.to,
90
+ fromAddress: input.from ?? template?.fromAddress ?? null,
91
+ subject: subject ?? null,
92
+ htmlBody: html ?? null,
93
+ textBody: text ?? null,
94
+ payloadData: data,
95
+ metadata: input.metadata ?? null,
96
+ errorMessage: null,
97
+ scheduledFor: toTimestamp(input.scheduledFor),
98
+ sentAt: null,
99
+ failedAt: null,
100
+ })
101
+ .returning();
102
+ if (!pending) {
103
+ throw new NotificationError("Failed to create notification delivery");
104
+ }
105
+ try {
106
+ const result = provider === dispatcher.getProvider(channel)?.name
107
+ ? await dispatcher.send({
108
+ to: input.to,
109
+ channel,
110
+ provider,
111
+ template: template?.slug ?? input.templateSlug ?? "direct",
112
+ data,
113
+ from: input.from ?? template?.fromAddress ?? undefined,
114
+ subject: subject ?? undefined,
115
+ html: html ?? undefined,
116
+ text: text ?? undefined,
117
+ })
118
+ : await dispatcher.sendWith(provider, {
119
+ to: input.to,
120
+ channel,
121
+ provider,
122
+ template: template?.slug ?? input.templateSlug ?? "direct",
123
+ data,
124
+ from: input.from ?? template?.fromAddress ?? undefined,
125
+ subject: subject ?? undefined,
126
+ html: html ?? undefined,
127
+ text: text ?? undefined,
128
+ });
129
+ const [sent] = await db
130
+ .update(notificationDeliveries)
131
+ .set({
132
+ status: "sent",
133
+ providerMessageId: result.id ?? null,
134
+ sentAt: new Date(),
135
+ errorMessage: null,
136
+ updatedAt: new Date(),
137
+ })
138
+ .where(eq(notificationDeliveries.id, pending.id))
139
+ .returning();
140
+ return sent ?? null;
141
+ }
142
+ catch (error) {
143
+ const message = error instanceof Error ? error.message : "Notification send failed";
144
+ const [failed] = await db
145
+ .update(notificationDeliveries)
146
+ .set({
147
+ status: "failed",
148
+ failedAt: new Date(),
149
+ errorMessage: message,
150
+ updatedAt: new Date(),
151
+ })
152
+ .where(eq(notificationDeliveries.id, pending.id))
153
+ .returning();
154
+ throw new NotificationError(failed?.errorMessage ?? message);
155
+ }
156
+ }
157
+ export async function sendPaymentSessionNotification(db, dispatcher, sessionId, input) {
158
+ const [session] = await db
159
+ .select()
160
+ .from(paymentSessions)
161
+ .where(eq(paymentSessions.id, sessionId))
162
+ .limit(1);
163
+ if (!session) {
164
+ return null;
165
+ }
166
+ const booking = session.bookingId
167
+ ? ((await db.select().from(bookings).where(eq(bookings.id, session.bookingId)).limit(1))[0] ??
168
+ null)
169
+ : null;
170
+ const invoice = session.invoiceId
171
+ ? ((await db.select().from(invoices).where(eq(invoices.id, session.invoiceId)).limit(1))[0] ??
172
+ null)
173
+ : null;
174
+ const participants = booking ? await listBookingNotificationParticipants(db, booking.id) : [];
175
+ const recipient = resolveReminderRecipient(participants);
176
+ const to = input.to ?? session.payerEmail ?? recipient?.email ?? null;
177
+ if (!to) {
178
+ throw new NotificationError("No recipient available for payment session notification");
179
+ }
180
+ return sendNotification(db, dispatcher, {
181
+ templateId: input.templateId ?? null,
182
+ templateSlug: input.templateSlug ?? null,
183
+ channel: input.channel,
184
+ provider: input.provider ?? null,
185
+ to,
186
+ from: input.from ?? null,
187
+ subject: input.subject ?? null,
188
+ html: input.html ?? null,
189
+ text: input.text ?? null,
190
+ data: {
191
+ paymentSession: {
192
+ id: session.id,
193
+ status: session.status,
194
+ provider: session.provider,
195
+ currency: session.currency,
196
+ amountCents: session.amountCents,
197
+ redirectUrl: session.redirectUrl,
198
+ returnUrl: session.returnUrl,
199
+ cancelUrl: session.cancelUrl,
200
+ expiresAt: session.expiresAt,
201
+ paymentMethod: session.paymentMethod,
202
+ externalReference: session.externalReference,
203
+ },
204
+ booking: booking
205
+ ? {
206
+ id: booking.id,
207
+ bookingNumber: booking.bookingNumber,
208
+ startDate: booking.startDate,
209
+ endDate: booking.endDate,
210
+ sellCurrency: booking.sellCurrency,
211
+ sellAmountCents: booking.sellAmountCents,
212
+ }
213
+ : null,
214
+ invoice: invoice
215
+ ? {
216
+ id: invoice.id,
217
+ invoiceNumber: invoice.invoiceNumber,
218
+ invoiceType: invoice.invoiceType,
219
+ status: invoice.status,
220
+ currency: invoice.currency,
221
+ totalCents: invoice.totalCents,
222
+ balanceDueCents: invoice.balanceDueCents,
223
+ issueDate: invoice.issueDate,
224
+ dueDate: invoice.dueDate,
225
+ }
226
+ : null,
227
+ participant: recipient
228
+ ? {
229
+ firstName: recipient.firstName,
230
+ lastName: recipient.lastName,
231
+ email: recipient.email,
232
+ }
233
+ : null,
234
+ ...(input.data ?? {}),
235
+ },
236
+ targetType: "payment_session",
237
+ targetId: session.id,
238
+ bookingId: session.bookingId ?? null,
239
+ invoiceId: session.invoiceId ?? null,
240
+ paymentSessionId: session.id,
241
+ personId: session.payerPersonId ?? booking?.personId ?? null,
242
+ organizationId: session.payerOrganizationId ?? booking?.organizationId ?? null,
243
+ metadata: input.metadata ?? null,
244
+ scheduledFor: input.scheduledFor ?? null,
245
+ });
246
+ }
247
+ export async function sendInvoiceNotification(db, dispatcher, invoiceId, input) {
248
+ const [invoice] = await db.select().from(invoices).where(eq(invoices.id, invoiceId)).limit(1);
249
+ if (!invoice) {
250
+ return null;
251
+ }
252
+ const [booking] = await db
253
+ .select()
254
+ .from(bookings)
255
+ .where(eq(bookings.id, invoice.bookingId))
256
+ .limit(1);
257
+ const participants = booking ? await listBookingNotificationParticipants(db, booking.id) : [];
258
+ const recipient = resolveReminderRecipient(participants);
259
+ const [latestSession] = await db
260
+ .select()
261
+ .from(paymentSessions)
262
+ .where(eq(paymentSessions.invoiceId, invoice.id))
263
+ .orderBy(desc(paymentSessions.createdAt))
264
+ .limit(1);
265
+ const to = input.to ?? latestSession?.payerEmail ?? recipient?.email ?? null;
266
+ if (!to) {
267
+ throw new NotificationError("No recipient available for invoice notification");
268
+ }
269
+ return sendNotification(db, dispatcher, {
270
+ templateId: input.templateId ?? null,
271
+ templateSlug: input.templateSlug ?? null,
272
+ channel: input.channel,
273
+ provider: input.provider ?? null,
274
+ to,
275
+ from: input.from ?? null,
276
+ subject: input.subject ?? null,
277
+ html: input.html ?? null,
278
+ text: input.text ?? null,
279
+ data: {
280
+ invoice: {
281
+ id: invoice.id,
282
+ invoiceNumber: invoice.invoiceNumber,
283
+ invoiceType: invoice.invoiceType,
284
+ status: invoice.status,
285
+ currency: invoice.currency,
286
+ subtotalCents: invoice.subtotalCents,
287
+ taxCents: invoice.taxCents,
288
+ totalCents: invoice.totalCents,
289
+ paidCents: invoice.paidCents,
290
+ balanceDueCents: invoice.balanceDueCents,
291
+ issueDate: invoice.issueDate,
292
+ dueDate: invoice.dueDate,
293
+ },
294
+ booking: booking
295
+ ? {
296
+ id: booking.id,
297
+ bookingNumber: booking.bookingNumber,
298
+ startDate: booking.startDate,
299
+ endDate: booking.endDate,
300
+ sellCurrency: booking.sellCurrency,
301
+ sellAmountCents: booking.sellAmountCents,
302
+ }
303
+ : null,
304
+ paymentSession: latestSession
305
+ ? {
306
+ id: latestSession.id,
307
+ status: latestSession.status,
308
+ provider: latestSession.provider,
309
+ redirectUrl: latestSession.redirectUrl,
310
+ expiresAt: latestSession.expiresAt,
311
+ amountCents: latestSession.amountCents,
312
+ currency: latestSession.currency,
313
+ }
314
+ : null,
315
+ participant: recipient
316
+ ? {
317
+ firstName: recipient.firstName,
318
+ lastName: recipient.lastName,
319
+ email: recipient.email,
320
+ }
321
+ : null,
322
+ ...(input.data ?? {}),
323
+ },
324
+ targetType: "invoice",
325
+ targetId: invoice.id,
326
+ bookingId: invoice.bookingId,
327
+ invoiceId: invoice.id,
328
+ paymentSessionId: latestSession?.id ?? null,
329
+ personId: invoice.personId ?? booking?.personId ?? null,
330
+ organizationId: invoice.organizationId ?? booking?.organizationId ?? null,
331
+ metadata: input.metadata ?? null,
332
+ scheduledFor: input.scheduledFor ?? null,
333
+ });
334
+ }