@forklaunch/implementation-messaging-twilio 1.0.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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/lib/domain/enum/index.d.ts +3 -0
  3. package/lib/domain/enum/index.d.ts.map +1 -0
  4. package/lib/domain/enum/index.js +50 -0
  5. package/lib/domain/enum/index.mjs +21 -0
  6. package/lib/domain/enum/twilioMessageStatus.enum.d.ts +16 -0
  7. package/lib/domain/enum/twilioMessageStatus.enum.d.ts.map +1 -0
  8. package/lib/domain/schemas/index.d.ts +2 -0
  9. package/lib/domain/schemas/index.d.ts.map +1 -0
  10. package/lib/domain/schemas/index.js +86 -0
  11. package/lib/domain/schemas/index.mjs +75 -0
  12. package/lib/domain/schemas/sms.schema.d.ts +44 -0
  13. package/lib/domain/schemas/sms.schema.d.ts.map +1 -0
  14. package/lib/domain/schemas/typebox/sms.schema.d.ts +39 -0
  15. package/lib/domain/schemas/typebox/sms.schema.d.ts.map +1 -0
  16. package/lib/domain/schemas/zod/sms.schema.d.ts +39 -0
  17. package/lib/domain/schemas/zod/sms.schema.d.ts.map +1 -0
  18. package/lib/domain/types/index.d.ts +2 -0
  19. package/lib/domain/types/index.d.ts.map +1 -0
  20. package/lib/domain/types/index.js +18 -0
  21. package/lib/domain/types/index.mjs +0 -0
  22. package/lib/domain/types/twilio.types.d.ts +15 -0
  23. package/lib/domain/types/twilio.types.d.ts.map +1 -0
  24. package/lib/eject/domain/enum/index.ts +3 -0
  25. package/lib/eject/domain/enum/twilioMessageStatus.enum.ts +18 -0
  26. package/lib/eject/domain/schemas/index.ts +1 -0
  27. package/lib/eject/domain/schemas/sms.schema.ts +33 -0
  28. package/lib/eject/domain/types/index.ts +1 -0
  29. package/lib/eject/domain/types/twilio.types.ts +24 -0
  30. package/lib/eject/services/index.ts +4 -0
  31. package/lib/eject/services/sms.service.ts +216 -0
  32. package/lib/persistence/entities/index.d.ts +92 -0
  33. package/lib/persistence/entities/index.d.ts.map +1 -0
  34. package/lib/persistence/entities/index.js +42 -0
  35. package/lib/persistence/entities/index.mjs +17 -0
  36. package/lib/services/index.d.ts +4 -0
  37. package/lib/services/index.d.ts.map +1 -0
  38. package/lib/services/index.js +193 -0
  39. package/lib/services/index.mjs +164 -0
  40. package/lib/services/sms.service.d.ts +38 -0
  41. package/lib/services/sms.service.d.ts.map +1 -0
  42. package/package.json +99 -0
@@ -0,0 +1,216 @@
1
+ import { IdDto } from '@forklaunch/common';
2
+ import {
3
+ evaluateTelemetryOptions,
4
+ MetricsDefinition,
5
+ OpenTelemetryCollector,
6
+ TelemetryOptions
7
+ } from '@forklaunch/core/http';
8
+ import { SmsStatusEnum } from '@forklaunch/implementation-messaging-base/enum';
9
+ import { SmsMappers } from '@forklaunch/implementation-messaging-base/types';
10
+ import { SmsService } from '@forklaunch/interfaces-messaging/interfaces';
11
+ import { SendSmsDto } from '@forklaunch/interfaces-messaging/types';
12
+ import { AnySchemaValidator } from '@forklaunch/validator';
13
+ import { EntityManager, InferEntity } from '@mikro-orm/core';
14
+ import { TwilioMessageStatusEnum } from '../domain/enum/twilioMessageStatus.enum';
15
+ import {
16
+ TwilioMessageResource,
17
+ TwilioSmsDtos,
18
+ TwilioSmsEntities,
19
+ TwilioStatusCallbackDto
20
+ } from '../domain/types/twilio.types';
21
+ import { SmsRecord } from '../persistence/entities';
22
+
23
+ /**
24
+ * Maps a Twilio message status (from the Messages REST API or a delivery
25
+ * status callback) onto the canonical SmsStatusEnum.
26
+ */
27
+ export const mapTwilioMessageStatus = (
28
+ messageStatus: string
29
+ ): SmsStatusEnum => {
30
+ switch (messageStatus) {
31
+ case TwilioMessageStatusEnum.SENT:
32
+ return SmsStatusEnum.SENT;
33
+ case TwilioMessageStatusEnum.DELIVERED:
34
+ case TwilioMessageStatusEnum.READ:
35
+ return SmsStatusEnum.DELIVERED;
36
+ case TwilioMessageStatusEnum.UNDELIVERED:
37
+ return SmsStatusEnum.UNDELIVERED;
38
+ case TwilioMessageStatusEnum.FAILED:
39
+ case TwilioMessageStatusEnum.CANCELED:
40
+ return SmsStatusEnum.FAILED;
41
+ case TwilioMessageStatusEnum.QUEUED:
42
+ case TwilioMessageStatusEnum.ACCEPTED:
43
+ case TwilioMessageStatusEnum.SCHEDULED:
44
+ case TwilioMessageStatusEnum.SENDING:
45
+ default:
46
+ return SmsStatusEnum.QUEUED;
47
+ }
48
+ };
49
+
50
+ const TWILIO_API_BASE_URL = 'https://api.twilio.com/2010-04-01';
51
+
52
+ export class TwilioSmsService<
53
+ SchemaValidator extends AnySchemaValidator,
54
+ Entities extends TwilioSmsEntities = TwilioSmsEntities,
55
+ Dto extends TwilioSmsDtos = TwilioSmsDtos
56
+ > implements SmsService<typeof SmsStatusEnum>
57
+ {
58
+ private evaluatedTelemetryOptions: {
59
+ logging?: boolean;
60
+ metrics?: boolean;
61
+ tracing?: boolean;
62
+ };
63
+ protected readonly accountSid: string;
64
+ protected readonly authToken: string;
65
+ protected readonly fromNumber: string;
66
+ public em: EntityManager;
67
+ protected readonly openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>;
68
+ protected readonly schemaValidator: SchemaValidator;
69
+ protected readonly mappers: SmsMappers<typeof SmsStatusEnum, Entities, Dto>;
70
+
71
+ constructor(
72
+ accountSid: string,
73
+ authToken: string,
74
+ fromNumber: string,
75
+ em: EntityManager,
76
+ openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>,
77
+ schemaValidator: SchemaValidator,
78
+ mappers: SmsMappers<typeof SmsStatusEnum, Entities, Dto>,
79
+ readonly options?: {
80
+ telemetry?: TelemetryOptions;
81
+ }
82
+ ) {
83
+ this.accountSid = accountSid;
84
+ this.authToken = authToken;
85
+ this.fromNumber = fromNumber;
86
+ this.em = em;
87
+ this.openTelemetryCollector = openTelemetryCollector;
88
+ this.schemaValidator = schemaValidator;
89
+ this.mappers = mappers;
90
+ this.evaluatedTelemetryOptions = options?.telemetry
91
+ ? evaluateTelemetryOptions(options.telemetry).enabled
92
+ : {
93
+ logging: false,
94
+ metrics: false,
95
+ tracing: false
96
+ };
97
+ }
98
+
99
+ async sendSms(
100
+ smsDto: SendSmsDto,
101
+ em?: EntityManager,
102
+ ...args: unknown[]
103
+ ): Promise<Dto['SmsRecordMapper']> {
104
+ if (this.evaluatedTelemetryOptions.logging) {
105
+ this.openTelemetryCollector.info('Sending sms via twilio', {
106
+ to: smsDto.to
107
+ });
108
+ }
109
+
110
+ const smsRecord = await this.mappers.SendSmsMapper.toEntity(
111
+ smsDto,
112
+ em ?? this.em,
113
+ ...(args[0] instanceof EntityManager ? args.slice(1) : args)
114
+ );
115
+ const record = smsRecord as {
116
+ status: SmsStatusEnum;
117
+ providerMessageId?: string | null;
118
+ error?: string | null;
119
+ };
120
+
121
+ // the twilio package is intentionally dependency-free — the message is
122
+ // dispatched via the Twilio Messages REST API with plain fetch
123
+ const response = await fetch(
124
+ `${TWILIO_API_BASE_URL}/Accounts/${this.accountSid}/Messages.json`,
125
+ {
126
+ method: 'POST',
127
+ headers: {
128
+ Authorization: `Basic ${Buffer.from(
129
+ `${this.accountSid}:${this.authToken}`
130
+ ).toString('base64')}`,
131
+ 'Content-Type': 'application/x-www-form-urlencoded'
132
+ },
133
+ body: new URLSearchParams({
134
+ To: smsDto.to,
135
+ From: this.fromNumber,
136
+ Body: smsDto.body
137
+ }).toString()
138
+ }
139
+ );
140
+
141
+ if (response.ok) {
142
+ const message = (await response.json()) as TwilioMessageResource;
143
+ record.providerMessageId = message.sid;
144
+ record.status = mapTwilioMessageStatus(message.status);
145
+ record.error = message.error_message ?? null;
146
+ } else {
147
+ const errorBody = await response.text();
148
+ record.status = SmsStatusEnum.FAILED;
149
+ record.error = `Twilio API error (${response.status}): ${errorBody}`;
150
+ this.openTelemetryCollector.error('Twilio message dispatch failed', {
151
+ status: response.status,
152
+ error: errorBody
153
+ });
154
+ }
155
+
156
+ await (em ?? this.em).transactional(async (innerEm) => {
157
+ await innerEm.persist(smsRecord);
158
+ });
159
+
160
+ return this.mappers.SmsRecordMapper.toDto(
161
+ smsRecord as InferEntity<Entities['SmsRecordMapper']>
162
+ );
163
+ }
164
+
165
+ async getSmsRecord(
166
+ { id }: IdDto,
167
+ em?: EntityManager
168
+ ): Promise<Dto['SmsRecordMapper']> {
169
+ if (this.evaluatedTelemetryOptions.logging) {
170
+ this.openTelemetryCollector.info('Getting sms record', { id });
171
+ }
172
+
173
+ const smsRecord = await (em ?? this.em).findOneOrFail(
174
+ this.mappers.SmsRecordMapper.entity as typeof SmsRecord,
175
+ { id }
176
+ );
177
+
178
+ return this.mappers.SmsRecordMapper.toDto(
179
+ smsRecord as InferEntity<Entities['SmsRecordMapper']>
180
+ );
181
+ }
182
+
183
+ /**
184
+ * Maps a Twilio delivery-status callback (MessageStatus, MessageSid,
185
+ * ErrorMessage) onto the persisted record's status.
186
+ */
187
+ async processStatusCallback(
188
+ payload: TwilioStatusCallbackDto,
189
+ em?: EntityManager
190
+ ): Promise<Dto['SmsRecordMapper']> {
191
+ if (this.evaluatedTelemetryOptions.logging) {
192
+ this.openTelemetryCollector.info('Processing twilio status callback', {
193
+ messageSid: payload.MessageSid,
194
+ messageStatus: payload.MessageStatus
195
+ });
196
+ }
197
+
198
+ const smsRecord = await (em ?? this.em).findOneOrFail(
199
+ this.mappers.SmsRecordMapper.entity as typeof SmsRecord,
200
+ { providerMessageId: payload.MessageSid }
201
+ );
202
+ const record = smsRecord as {
203
+ status: SmsStatusEnum;
204
+ error?: string | null;
205
+ };
206
+
207
+ record.status = mapTwilioMessageStatus(payload.MessageStatus);
208
+ record.error = payload.ErrorMessage ?? null;
209
+
210
+ await (em ?? this.em).flush();
211
+
212
+ return this.mappers.SmsRecordMapper.toDto(
213
+ smsRecord as InferEntity<Entities['SmsRecordMapper']>
214
+ );
215
+ }
216
+ }
@@ -0,0 +1,92 @@
1
+ export declare const SmsRecord: import("@mikro-orm/core").EntitySchemaWithMeta<"SmsRecord", string, import("@mikro-orm/core").InferEntityFromProperties<{
2
+ readonly id: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, Omit<import("@mikro-orm/core").EmptyOptions, "primary"> & {
3
+ primary: true;
4
+ } & {
5
+ readonly '~c': true;
6
+ }, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
7
+ readonly to: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, import("@mikro-orm/core").EmptyOptions & {
8
+ readonly '~c': true;
9
+ }, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
10
+ readonly body: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, import("@mikro-orm/core").EmptyOptions & {
11
+ readonly '~c': true;
12
+ }, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
13
+ readonly status: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<any, import("@mikro-orm/core").EmptyOptions & {
14
+ readonly '~c': true;
15
+ }, "accessor" | "array" | "autoincrement" | "check" | "collation" | "columnType" | "columnTypes" | "comment" | "compliance" | "concurrencyCheck" | "customOrder" | "default" | "defaultRaw" | "extra" | "fieldName" | "fieldNames" | "formula" | "generated" | "getter" | "getterName" | "groups" | "hidden" | "hydrate" | "ignoreSchemaChanges" | "index" | "lazy" | "length" | "name" | "nativeEnumName" | "nullable" | "onCreate" | "onUpdate" | "persist" | "precision" | "primary" | "ref" | "returning" | "runtimeType" | "scale" | "serializedName" | "serializedPrimaryKey" | "serializer" | "setter" | "type" | "unique" | "unsigned" | "version" | ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type")>, "accessor" | "array" | "autoincrement" | "check" | "collation" | "columnType" | "columnTypes" | "comment" | "compliance" | "concurrencyCheck" | "customOrder" | "default" | "defaultRaw" | "extra" | "fieldName" | "fieldNames" | "formula" | "generated" | "getter" | "getterName" | "groups" | "hidden" | "hydrate" | "ignoreSchemaChanges" | "index" | "lazy" | "length" | "name" | "nativeEnumName" | "nullable" | "onCreate" | "onUpdate" | "persist" | "precision" | "primary" | "ref" | "returning" | "runtimeType" | "scale" | "serializedName" | "serializedPrimaryKey" | "serializer" | "setter" | "type" | "unique" | "unsigned" | "version" | ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type")>;
16
+ readonly providerMessageId: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, Omit<import("@mikro-orm/core").EmptyOptions, "nullable"> & {
17
+ nullable: true;
18
+ } & {
19
+ readonly '~c': true;
20
+ }, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
21
+ readonly error: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, Omit<import("@mikro-orm/core").EmptyOptions, "nullable"> & {
22
+ nullable: true;
23
+ } & {
24
+ readonly '~c': true;
25
+ }, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
26
+ readonly metadata: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<unknown, Omit<import("@mikro-orm/core").EmptyOptions, "nullable"> & {
27
+ nullable: true;
28
+ } & {
29
+ readonly '~c': true;
30
+ }, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
31
+ }, undefined, never, never, false>, never, {
32
+ readonly id: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, Omit<import("@mikro-orm/core").EmptyOptions, "primary"> & {
33
+ primary: true;
34
+ } & {
35
+ readonly '~c': true;
36
+ }, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
37
+ readonly to: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, import("@mikro-orm/core").EmptyOptions & {
38
+ readonly '~c': true;
39
+ }, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
40
+ readonly body: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, import("@mikro-orm/core").EmptyOptions & {
41
+ readonly '~c': true;
42
+ }, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
43
+ readonly status: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<any, import("@mikro-orm/core").EmptyOptions & {
44
+ readonly '~c': true;
45
+ }, "accessor" | "array" | "autoincrement" | "check" | "collation" | "columnType" | "columnTypes" | "comment" | "compliance" | "concurrencyCheck" | "customOrder" | "default" | "defaultRaw" | "extra" | "fieldName" | "fieldNames" | "formula" | "generated" | "getter" | "getterName" | "groups" | "hidden" | "hydrate" | "ignoreSchemaChanges" | "index" | "lazy" | "length" | "name" | "nativeEnumName" | "nullable" | "onCreate" | "onUpdate" | "persist" | "precision" | "primary" | "ref" | "returning" | "runtimeType" | "scale" | "serializedName" | "serializedPrimaryKey" | "serializer" | "setter" | "type" | "unique" | "unsigned" | "version" | ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type")>, "accessor" | "array" | "autoincrement" | "check" | "collation" | "columnType" | "columnTypes" | "comment" | "compliance" | "concurrencyCheck" | "customOrder" | "default" | "defaultRaw" | "extra" | "fieldName" | "fieldNames" | "formula" | "generated" | "getter" | "getterName" | "groups" | "hidden" | "hydrate" | "ignoreSchemaChanges" | "index" | "lazy" | "length" | "name" | "nativeEnumName" | "nullable" | "onCreate" | "onUpdate" | "persist" | "precision" | "primary" | "ref" | "returning" | "runtimeType" | "scale" | "serializedName" | "serializedPrimaryKey" | "serializer" | "setter" | "type" | "unique" | "unsigned" | "version" | ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type")>;
46
+ readonly providerMessageId: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, Omit<import("@mikro-orm/core").EmptyOptions, "nullable"> & {
47
+ nullable: true;
48
+ } & {
49
+ readonly '~c': true;
50
+ }, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
51
+ readonly error: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, Omit<import("@mikro-orm/core").EmptyOptions, "nullable"> & {
52
+ nullable: true;
53
+ } & {
54
+ readonly '~c': true;
55
+ }, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
56
+ readonly metadata: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<unknown, Omit<import("@mikro-orm/core").EmptyOptions, "nullable"> & {
57
+ nullable: true;
58
+ } & {
59
+ readonly '~c': true;
60
+ }, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
61
+ }, import("@mikro-orm/core").EntityCtor<import("@mikro-orm/core").InferEntityFromProperties<{
62
+ readonly id: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, Omit<import("@mikro-orm/core").EmptyOptions, "primary"> & {
63
+ primary: true;
64
+ } & {
65
+ readonly '~c': true;
66
+ }, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
67
+ readonly to: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, import("@mikro-orm/core").EmptyOptions & {
68
+ readonly '~c': true;
69
+ }, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
70
+ readonly body: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, import("@mikro-orm/core").EmptyOptions & {
71
+ readonly '~c': true;
72
+ }, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
73
+ readonly status: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<any, import("@mikro-orm/core").EmptyOptions & {
74
+ readonly '~c': true;
75
+ }, "accessor" | "array" | "autoincrement" | "check" | "collation" | "columnType" | "columnTypes" | "comment" | "compliance" | "concurrencyCheck" | "customOrder" | "default" | "defaultRaw" | "extra" | "fieldName" | "fieldNames" | "formula" | "generated" | "getter" | "getterName" | "groups" | "hidden" | "hydrate" | "ignoreSchemaChanges" | "index" | "lazy" | "length" | "name" | "nativeEnumName" | "nullable" | "onCreate" | "onUpdate" | "persist" | "precision" | "primary" | "ref" | "returning" | "runtimeType" | "scale" | "serializedName" | "serializedPrimaryKey" | "serializer" | "setter" | "type" | "unique" | "unsigned" | "version" | ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type")>, "accessor" | "array" | "autoincrement" | "check" | "collation" | "columnType" | "columnTypes" | "comment" | "compliance" | "concurrencyCheck" | "customOrder" | "default" | "defaultRaw" | "extra" | "fieldName" | "fieldNames" | "formula" | "generated" | "getter" | "getterName" | "groups" | "hidden" | "hydrate" | "ignoreSchemaChanges" | "index" | "lazy" | "length" | "name" | "nativeEnumName" | "nullable" | "onCreate" | "onUpdate" | "persist" | "precision" | "primary" | "ref" | "returning" | "runtimeType" | "scale" | "serializedName" | "serializedPrimaryKey" | "serializer" | "setter" | "type" | "unique" | "unsigned" | "version" | ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type")>;
76
+ readonly providerMessageId: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, Omit<import("@mikro-orm/core").EmptyOptions, "nullable"> & {
77
+ nullable: true;
78
+ } & {
79
+ readonly '~c': true;
80
+ }, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
81
+ readonly error: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, Omit<import("@mikro-orm/core").EmptyOptions, "nullable"> & {
82
+ nullable: true;
83
+ } & {
84
+ readonly '~c': true;
85
+ }, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
86
+ readonly metadata: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<unknown, Omit<import("@mikro-orm/core").EmptyOptions, "nullable"> & {
87
+ nullable: true;
88
+ } & {
89
+ readonly '~c': true;
90
+ }, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
91
+ }, undefined, never, never, false>>, undefined>;
92
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../persistence/entities/index.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+CAWpB,CAAC"}
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // persistence/entities/index.ts
21
+ var entities_exports = {};
22
+ __export(entities_exports, {
23
+ SmsRecord: () => SmsRecord
24
+ });
25
+ module.exports = __toCommonJS(entities_exports);
26
+ var import_persistence = require("@forklaunch/core/persistence");
27
+ var SmsRecord = (0, import_persistence.defineComplianceEntity)({
28
+ name: "SmsRecord",
29
+ properties: {
30
+ id: import_persistence.fp.string().primary().compliance("none"),
31
+ to: import_persistence.fp.string().compliance("pii"),
32
+ body: import_persistence.fp.string().compliance("pii"),
33
+ status: import_persistence.fp.enum().compliance("none"),
34
+ providerMessageId: import_persistence.fp.string().nullable().compliance("none"),
35
+ error: import_persistence.fp.string().nullable().compliance("none"),
36
+ metadata: import_persistence.fp.json().nullable().compliance("none")
37
+ }
38
+ });
39
+ // Annotate the CommonJS export names for ESM import in node:
40
+ 0 && (module.exports = {
41
+ SmsRecord
42
+ });
@@ -0,0 +1,17 @@
1
+ // persistence/entities/index.ts
2
+ import { defineComplianceEntity, fp } from "@forklaunch/core/persistence";
3
+ var SmsRecord = defineComplianceEntity({
4
+ name: "SmsRecord",
5
+ properties: {
6
+ id: fp.string().primary().compliance("none"),
7
+ to: fp.string().compliance("pii"),
8
+ body: fp.string().compliance("pii"),
9
+ status: fp.enum().compliance("none"),
10
+ providerMessageId: fp.string().nullable().compliance("none"),
11
+ error: fp.string().nullable().compliance("none"),
12
+ metadata: fp.json().nullable().compliance("none")
13
+ }
14
+ });
15
+ export {
16
+ SmsRecord
17
+ };
@@ -0,0 +1,4 @@
1
+ export * from './sms.service';
2
+ export * from '@forklaunch/interfaces-messaging/interfaces';
3
+ export * from '@forklaunch/interfaces-messaging/types';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../services/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAE9B,cAAc,6CAA6C,CAAC;AAC5D,cAAc,wCAAwC,CAAC"}
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // services/index.ts
22
+ var services_exports = {};
23
+ __export(services_exports, {
24
+ TwilioSmsService: () => TwilioSmsService,
25
+ mapTwilioMessageStatus: () => mapTwilioMessageStatus
26
+ });
27
+ module.exports = __toCommonJS(services_exports);
28
+
29
+ // services/sms.service.ts
30
+ var import_http = require("@forklaunch/core/http");
31
+ var import_enum = require("@forklaunch/implementation-messaging-base/enum");
32
+ var import_core = require("@mikro-orm/core");
33
+
34
+ // domain/enum/twilioMessageStatus.enum.ts
35
+ var TwilioMessageStatusEnum = {
36
+ QUEUED: "queued",
37
+ ACCEPTED: "accepted",
38
+ SCHEDULED: "scheduled",
39
+ CANCELED: "canceled",
40
+ SENDING: "sending",
41
+ SENT: "sent",
42
+ DELIVERED: "delivered",
43
+ UNDELIVERED: "undelivered",
44
+ FAILED: "failed",
45
+ RECEIVING: "receiving",
46
+ RECEIVED: "received",
47
+ READ: "read"
48
+ };
49
+
50
+ // services/sms.service.ts
51
+ var mapTwilioMessageStatus = (messageStatus) => {
52
+ switch (messageStatus) {
53
+ case TwilioMessageStatusEnum.SENT:
54
+ return import_enum.SmsStatusEnum.SENT;
55
+ case TwilioMessageStatusEnum.DELIVERED:
56
+ case TwilioMessageStatusEnum.READ:
57
+ return import_enum.SmsStatusEnum.DELIVERED;
58
+ case TwilioMessageStatusEnum.UNDELIVERED:
59
+ return import_enum.SmsStatusEnum.UNDELIVERED;
60
+ case TwilioMessageStatusEnum.FAILED:
61
+ case TwilioMessageStatusEnum.CANCELED:
62
+ return import_enum.SmsStatusEnum.FAILED;
63
+ case TwilioMessageStatusEnum.QUEUED:
64
+ case TwilioMessageStatusEnum.ACCEPTED:
65
+ case TwilioMessageStatusEnum.SCHEDULED:
66
+ case TwilioMessageStatusEnum.SENDING:
67
+ default:
68
+ return import_enum.SmsStatusEnum.QUEUED;
69
+ }
70
+ };
71
+ var TWILIO_API_BASE_URL = "https://api.twilio.com/2010-04-01";
72
+ var TwilioSmsService = class {
73
+ constructor(accountSid, authToken, fromNumber, em, openTelemetryCollector, schemaValidator, mappers, options) {
74
+ this.options = options;
75
+ this.accountSid = accountSid;
76
+ this.authToken = authToken;
77
+ this.fromNumber = fromNumber;
78
+ this.em = em;
79
+ this.openTelemetryCollector = openTelemetryCollector;
80
+ this.schemaValidator = schemaValidator;
81
+ this.mappers = mappers;
82
+ this.evaluatedTelemetryOptions = options?.telemetry ? (0, import_http.evaluateTelemetryOptions)(options.telemetry).enabled : {
83
+ logging: false,
84
+ metrics: false,
85
+ tracing: false
86
+ };
87
+ }
88
+ options;
89
+ evaluatedTelemetryOptions;
90
+ accountSid;
91
+ authToken;
92
+ fromNumber;
93
+ em;
94
+ openTelemetryCollector;
95
+ schemaValidator;
96
+ mappers;
97
+ async sendSms(smsDto, em, ...args) {
98
+ if (this.evaluatedTelemetryOptions.logging) {
99
+ this.openTelemetryCollector.info("Sending sms via twilio", {
100
+ to: smsDto.to
101
+ });
102
+ }
103
+ const smsRecord = await this.mappers.SendSmsMapper.toEntity(
104
+ smsDto,
105
+ em ?? this.em,
106
+ ...args[0] instanceof import_core.EntityManager ? args.slice(1) : args
107
+ );
108
+ const record = smsRecord;
109
+ const response = await fetch(
110
+ `${TWILIO_API_BASE_URL}/Accounts/${this.accountSid}/Messages.json`,
111
+ {
112
+ method: "POST",
113
+ headers: {
114
+ Authorization: `Basic ${Buffer.from(
115
+ `${this.accountSid}:${this.authToken}`
116
+ ).toString("base64")}`,
117
+ "Content-Type": "application/x-www-form-urlencoded"
118
+ },
119
+ body: new URLSearchParams({
120
+ To: smsDto.to,
121
+ From: this.fromNumber,
122
+ Body: smsDto.body
123
+ }).toString()
124
+ }
125
+ );
126
+ if (response.ok) {
127
+ const message = await response.json();
128
+ record.providerMessageId = message.sid;
129
+ record.status = mapTwilioMessageStatus(message.status);
130
+ record.error = message.error_message ?? null;
131
+ } else {
132
+ const errorBody = await response.text();
133
+ record.status = import_enum.SmsStatusEnum.FAILED;
134
+ record.error = `Twilio API error (${response.status}): ${errorBody}`;
135
+ this.openTelemetryCollector.error("Twilio message dispatch failed", {
136
+ status: response.status,
137
+ error: errorBody
138
+ });
139
+ }
140
+ await (em ?? this.em).transactional(async (innerEm) => {
141
+ await innerEm.persist(smsRecord);
142
+ });
143
+ return this.mappers.SmsRecordMapper.toDto(
144
+ smsRecord
145
+ );
146
+ }
147
+ async getSmsRecord({ id }, em) {
148
+ if (this.evaluatedTelemetryOptions.logging) {
149
+ this.openTelemetryCollector.info("Getting sms record", { id });
150
+ }
151
+ const smsRecord = await (em ?? this.em).findOneOrFail(
152
+ this.mappers.SmsRecordMapper.entity,
153
+ { id }
154
+ );
155
+ return this.mappers.SmsRecordMapper.toDto(
156
+ smsRecord
157
+ );
158
+ }
159
+ /**
160
+ * Maps a Twilio delivery-status callback (MessageStatus, MessageSid,
161
+ * ErrorMessage) onto the persisted record's status.
162
+ */
163
+ async processStatusCallback(payload, em) {
164
+ if (this.evaluatedTelemetryOptions.logging) {
165
+ this.openTelemetryCollector.info("Processing twilio status callback", {
166
+ messageSid: payload.MessageSid,
167
+ messageStatus: payload.MessageStatus
168
+ });
169
+ }
170
+ const smsRecord = await (em ?? this.em).findOneOrFail(
171
+ this.mappers.SmsRecordMapper.entity,
172
+ { providerMessageId: payload.MessageSid }
173
+ );
174
+ const record = smsRecord;
175
+ record.status = mapTwilioMessageStatus(payload.MessageStatus);
176
+ record.error = payload.ErrorMessage ?? null;
177
+ await (em ?? this.em).flush();
178
+ return this.mappers.SmsRecordMapper.toDto(
179
+ smsRecord
180
+ );
181
+ }
182
+ };
183
+
184
+ // services/index.ts
185
+ __reExport(services_exports, require("@forklaunch/interfaces-messaging/interfaces"), module.exports);
186
+ __reExport(services_exports, require("@forklaunch/interfaces-messaging/types"), module.exports);
187
+ // Annotate the CommonJS export names for ESM import in node:
188
+ 0 && (module.exports = {
189
+ TwilioSmsService,
190
+ mapTwilioMessageStatus,
191
+ ...require("@forklaunch/interfaces-messaging/interfaces"),
192
+ ...require("@forklaunch/interfaces-messaging/types")
193
+ });