@porulle/plugin-notifications 0.64.0 → 0.65.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/services/notification-service.d.ts +0 -2
- package/dist/services/notification-service.d.ts.map +1 -1
- package/dist/services/notification-service.js +0 -49
- package/dist/services/preference-service.d.ts +0 -10
- package/dist/services/preference-service.d.ts.map +1 -1
- package/dist/services/preference-service.js +1 -31
- package/package.json +15 -12
- package/src/services/notification-service.ts +0 -62
- package/src/services/preference-service.ts +1 -43
|
@@ -40,8 +40,6 @@ export declare class NotificationService {
|
|
|
40
40
|
data?: Record<string, unknown>;
|
|
41
41
|
metadata?: Record<string, unknown>;
|
|
42
42
|
}): Promise<Result<NotificationLogEntry>>;
|
|
43
|
-
sendSMS(orgId: string, to: string, body: string): Promise<Result<NotificationLogEntry>>;
|
|
44
|
-
sendPush(orgId: string, deviceToken: string, title: string, body: string, data?: Record<string, unknown>): Promise<Result<NotificationLogEntry>>;
|
|
45
43
|
listLog(orgId: string, filters?: {
|
|
46
44
|
channel?: string;
|
|
47
45
|
event?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notification-service.d.ts","sourceRoot":"","sources":["../../src/services/notification-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,EAAE,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,OAAO,EAAE,kBAAkB,EAC3E,MAAM,EACP,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAA2B,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE1F,qBAAa,mBAAmB;IAIlB,OAAO,CAAC,EAAE;IAHtB,OAAO,CAAC,UAAU,CAAyB;IAC3C,OAAO,CAAC,WAAW,CAA0B;gBAEzB,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,oBAAoB;IAOrD,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;QACzC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;KACzE,GAAG,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAkBnC,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAC;KACnC,GAAG,OAAO,CAAC,MAAM,CAAC,oBAAoB,EAAE,CAAC,CAAC;IAQrC,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAO7E,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;QACrD,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;KAC7D,GAAG,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAanC,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAatF;;;;OAIG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM;IAcvE;;;OAGG;IACG,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;QAC/B,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,OAAO,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACpC,GAAG,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAgFnC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"notification-service.d.ts","sourceRoot":"","sources":["../../src/services/notification-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,EAAE,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,OAAO,EAAE,kBAAkB,EAC3E,MAAM,EACP,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAA2B,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE1F,qBAAa,mBAAmB;IAIlB,OAAO,CAAC,EAAE;IAHtB,OAAO,CAAC,UAAU,CAAyB;IAC3C,OAAO,CAAC,WAAW,CAA0B;gBAEzB,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,oBAAoB;IAOrD,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;QACzC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;KACzE,GAAG,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAkBnC,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAC;KACnC,GAAG,OAAO,CAAC,MAAM,CAAC,oBAAoB,EAAE,CAAC,CAAC;IAQrC,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAO7E,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;QACrD,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;KAC7D,GAAG,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAanC,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAatF;;;;OAIG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM;IAcvE;;;OAGG;IACG,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;QAC/B,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,OAAO,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACpC,GAAG,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAgFnC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QACrC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,kBAAkB,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;KAC/E,GAAG,OAAO,CAAC,MAAM,CAAC,oBAAoB,EAAE,CAAC,CAAC;CAU5C"}
|
|
@@ -153,55 +153,6 @@ export class NotificationService {
|
|
|
153
153
|
}).returning();
|
|
154
154
|
return Ok(logRows[0]);
|
|
155
155
|
}
|
|
156
|
-
// ── Direct Channel Sends ───────────────────────────────────────────
|
|
157
|
-
async sendSMS(orgId, to, body) {
|
|
158
|
-
let adapterError;
|
|
159
|
-
let adapterMessageId;
|
|
160
|
-
if (this.smsAdapter) {
|
|
161
|
-
const result = await this.smsAdapter.send({ to, body });
|
|
162
|
-
if (!result.ok) {
|
|
163
|
-
adapterError = result.error;
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
adapterMessageId = result.value.messageId;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
const status = adapterError ? "failed" : "sent";
|
|
170
|
-
const rows = await this.db.insert(notificationLog).values({
|
|
171
|
-
organizationId: orgId,
|
|
172
|
-
channel: "sms",
|
|
173
|
-
event: "direct.sms",
|
|
174
|
-
recipient: to,
|
|
175
|
-
status,
|
|
176
|
-
error: adapterError,
|
|
177
|
-
metadata: adapterMessageId ? { adapterMessageId } : {},
|
|
178
|
-
}).returning();
|
|
179
|
-
return Ok(rows[0]);
|
|
180
|
-
}
|
|
181
|
-
async sendPush(orgId, deviceToken, title, body, data) {
|
|
182
|
-
let adapterError;
|
|
183
|
-
let adapterMessageId;
|
|
184
|
-
if (this.pushAdapter) {
|
|
185
|
-
const result = await this.pushAdapter.send({ deviceToken, title, body, ...(data != null ? { data } : {}) });
|
|
186
|
-
if (!result.ok) {
|
|
187
|
-
adapterError = result.error;
|
|
188
|
-
}
|
|
189
|
-
else {
|
|
190
|
-
adapterMessageId = result.value.messageId;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
const status = adapterError ? "failed" : "sent";
|
|
194
|
-
const rows = await this.db.insert(notificationLog).values({
|
|
195
|
-
organizationId: orgId,
|
|
196
|
-
channel: "push",
|
|
197
|
-
event: "direct.push",
|
|
198
|
-
recipient: deviceToken,
|
|
199
|
-
status,
|
|
200
|
-
error: adapterError,
|
|
201
|
-
metadata: adapterMessageId ? { adapterMessageId } : {},
|
|
202
|
-
}).returning();
|
|
203
|
-
return Ok(rows[0]);
|
|
204
|
-
}
|
|
205
156
|
// ── Log Queries ────────────────────────────────────────────────────
|
|
206
157
|
async listLog(orgId, filters) {
|
|
207
158
|
const conditions = [eq(notificationLog.organizationId, orgId)];
|
|
@@ -9,15 +9,5 @@ export declare class PreferenceService {
|
|
|
9
9
|
setPreference(orgId: string, customerId: string, channel: PrefChannel, isEnabled: boolean, destination?: string): Promise<Result<CustomerNotificationPref>>;
|
|
10
10
|
/** Get all notification preferences for a customer. */
|
|
11
11
|
getPreferences(orgId: string, customerId: string): Promise<Result<CustomerNotificationPref[]>>;
|
|
12
|
-
/**
|
|
13
|
-
* Determine which channels should fire for a given customer+event.
|
|
14
|
-
*
|
|
15
|
-
* Logic:
|
|
16
|
-
* 1. Find all active templates for the event.
|
|
17
|
-
* 2. For each template channel (email/sms/push), check customer preferences.
|
|
18
|
-
* 3. If no preference exists, the channel is considered enabled (opt-out model).
|
|
19
|
-
* 4. Returns channels that have an active template AND are not disabled by the customer.
|
|
20
|
-
*/
|
|
21
|
-
getActiveChannels(orgId: string, customerId: string, event: string): Promise<Result<PrefChannel[]>>;
|
|
22
12
|
}
|
|
23
13
|
//# sourceMappingURL=preference-service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preference-service.d.ts","sourceRoot":"","sources":["../../src/services/preference-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAE,EAAE,wBAAwB,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrF,qBAAa,iBAAiB;IAChB,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAE1B;;;OAGG;IACG,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,wBAAwB,CAAC,CAAC;IA2BjK,uDAAuD;IACjD,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,wBAAwB,EAAE,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"preference-service.d.ts","sourceRoot":"","sources":["../../src/services/preference-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAE,EAAE,wBAAwB,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrF,qBAAa,iBAAiB;IAChB,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAE1B;;;OAGG;IACG,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,wBAAwB,CAAC,CAAC;IA2BjK,uDAAuD;IACjD,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,wBAAwB,EAAE,CAAC,CAAC;CASrG"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { eq, and } from "@porulle/core/drizzle";
|
|
2
|
-
import { customerNotificationPrefs
|
|
2
|
+
import { customerNotificationPrefs } from "../schema.js";
|
|
3
3
|
import { Ok } from "../types.js";
|
|
4
4
|
export class PreferenceService {
|
|
5
5
|
db;
|
|
@@ -36,34 +36,4 @@ export class PreferenceService {
|
|
|
36
36
|
.where(and(eq(customerNotificationPrefs.organizationId, orgId), eq(customerNotificationPrefs.customerId, customerId)));
|
|
37
37
|
return Ok(rows);
|
|
38
38
|
}
|
|
39
|
-
/**
|
|
40
|
-
* Determine which channels should fire for a given customer+event.
|
|
41
|
-
*
|
|
42
|
-
* Logic:
|
|
43
|
-
* 1. Find all active templates for the event.
|
|
44
|
-
* 2. For each template channel (email/sms/push), check customer preferences.
|
|
45
|
-
* 3. If no preference exists, the channel is considered enabled (opt-out model).
|
|
46
|
-
* 4. Returns channels that have an active template AND are not disabled by the customer.
|
|
47
|
-
*/
|
|
48
|
-
async getActiveChannels(orgId, customerId, event) {
|
|
49
|
-
// Find active templates for this event (email, sms, push only — not print)
|
|
50
|
-
const templates = await this.db.select().from(notificationTemplates)
|
|
51
|
-
.where(and(eq(notificationTemplates.organizationId, orgId), eq(notificationTemplates.event, event), eq(notificationTemplates.isActive, true)));
|
|
52
|
-
const templateChannels = templates
|
|
53
|
-
.map((t) => t.channel)
|
|
54
|
-
.filter((ch) => ch === "email" || ch === "sms" || ch === "push");
|
|
55
|
-
if (templateChannels.length === 0)
|
|
56
|
-
return Ok([]);
|
|
57
|
-
// Get customer preferences
|
|
58
|
-
const prefs = await this.db.select().from(customerNotificationPrefs)
|
|
59
|
-
.where(and(eq(customerNotificationPrefs.organizationId, orgId), eq(customerNotificationPrefs.customerId, customerId)));
|
|
60
|
-
const prefMap = new Map(prefs.map((p) => [p.channel, p.isEnabled]));
|
|
61
|
-
// Filter: include channel if no preference (opt-out model) or explicitly enabled
|
|
62
|
-
const active = templateChannels.filter((ch) => {
|
|
63
|
-
const enabled = prefMap.get(ch);
|
|
64
|
-
return enabled !== false; // undefined (no pref) → allowed; true → allowed; false → blocked
|
|
65
|
-
});
|
|
66
|
-
// Deduplicate
|
|
67
|
-
return Ok([...new Set(active)]);
|
|
68
|
-
}
|
|
69
39
|
}
|
package/package.json
CHANGED
|
@@ -1,32 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@porulle/plugin-notifications",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.65.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
8
|
+
"@porulle/source": "./src/index.ts",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
11
|
},
|
|
12
12
|
"./schema": {
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
"@porulle/source": "./src/schema.ts",
|
|
14
|
+
"types": "./dist/schema.d.ts",
|
|
15
|
+
"default": "./dist/schema.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20.19.0"
|
|
18
21
|
},
|
|
19
22
|
"dependencies": {
|
|
20
23
|
"@hono/zod-openapi": "^1.2.2",
|
|
21
24
|
"hono": "^4.12.5",
|
|
22
|
-
"@porulle/core": "0.
|
|
25
|
+
"@porulle/core": "0.65.1"
|
|
23
26
|
},
|
|
24
27
|
"devDependencies": {
|
|
25
28
|
"@types/node": "^24.5.2",
|
|
26
29
|
"typescript": "5.9.2",
|
|
27
30
|
"vitest": "^3.2.4",
|
|
28
|
-
"@porulle/
|
|
29
|
-
"@porulle/
|
|
31
|
+
"@porulle/eslint-config": "0.1.0",
|
|
32
|
+
"@porulle/typescript-config": "0.1.0"
|
|
30
33
|
},
|
|
31
34
|
"publishConfig": {
|
|
32
35
|
"access": "public"
|
|
@@ -191,68 +191,6 @@ export class NotificationService {
|
|
|
191
191
|
return Ok(logRows[0]!);
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
// ── Direct Channel Sends ───────────────────────────────────────────
|
|
195
|
-
|
|
196
|
-
async sendSMS(orgId: string, to: string, body: string): Promise<Result<NotificationLogEntry>> {
|
|
197
|
-
let adapterError: string | undefined;
|
|
198
|
-
let adapterMessageId: string | undefined;
|
|
199
|
-
|
|
200
|
-
if (this.smsAdapter) {
|
|
201
|
-
const result = await this.smsAdapter.send({ to, body });
|
|
202
|
-
if (!result.ok) {
|
|
203
|
-
adapterError = result.error;
|
|
204
|
-
} else {
|
|
205
|
-
adapterMessageId = result.value.messageId;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const status: NotificationStatus = adapterError ? "failed" : "sent";
|
|
210
|
-
const rows = await this.db.insert(notificationLog).values({
|
|
211
|
-
organizationId: orgId,
|
|
212
|
-
channel: "sms",
|
|
213
|
-
event: "direct.sms",
|
|
214
|
-
recipient: to,
|
|
215
|
-
status,
|
|
216
|
-
error: adapterError,
|
|
217
|
-
metadata: adapterMessageId ? { adapterMessageId } : {},
|
|
218
|
-
}).returning();
|
|
219
|
-
|
|
220
|
-
return Ok(rows[0]!);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
async sendPush(
|
|
224
|
-
orgId: string,
|
|
225
|
-
deviceToken: string,
|
|
226
|
-
title: string,
|
|
227
|
-
body: string,
|
|
228
|
-
data?: Record<string, unknown>,
|
|
229
|
-
): Promise<Result<NotificationLogEntry>> {
|
|
230
|
-
let adapterError: string | undefined;
|
|
231
|
-
let adapterMessageId: string | undefined;
|
|
232
|
-
|
|
233
|
-
if (this.pushAdapter) {
|
|
234
|
-
const result = await this.pushAdapter.send({ deviceToken, title, body, ...(data != null ? { data } : {}) });
|
|
235
|
-
if (!result.ok) {
|
|
236
|
-
adapterError = result.error;
|
|
237
|
-
} else {
|
|
238
|
-
adapterMessageId = result.value.messageId;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const status: NotificationStatus = adapterError ? "failed" : "sent";
|
|
243
|
-
const rows = await this.db.insert(notificationLog).values({
|
|
244
|
-
organizationId: orgId,
|
|
245
|
-
channel: "push",
|
|
246
|
-
event: "direct.push",
|
|
247
|
-
recipient: deviceToken,
|
|
248
|
-
status,
|
|
249
|
-
error: adapterError,
|
|
250
|
-
metadata: adapterMessageId ? { adapterMessageId } : {},
|
|
251
|
-
}).returning();
|
|
252
|
-
|
|
253
|
-
return Ok(rows[0]!);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
194
|
// ── Log Queries ────────────────────────────────────────────────────
|
|
257
195
|
|
|
258
196
|
async listLog(orgId: string, filters?: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { eq, and } from "@porulle/core/drizzle";
|
|
2
|
-
import { customerNotificationPrefs
|
|
2
|
+
import { customerNotificationPrefs } from "../schema.js";
|
|
3
3
|
import type { Db, CustomerNotificationPref, PrefChannel, Result } from "../types.js";
|
|
4
4
|
import { Ok } from "../types.js";
|
|
5
5
|
|
|
@@ -47,46 +47,4 @@ export class PreferenceService {
|
|
|
47
47
|
return Ok(rows);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/**
|
|
51
|
-
* Determine which channels should fire for a given customer+event.
|
|
52
|
-
*
|
|
53
|
-
* Logic:
|
|
54
|
-
* 1. Find all active templates for the event.
|
|
55
|
-
* 2. For each template channel (email/sms/push), check customer preferences.
|
|
56
|
-
* 3. If no preference exists, the channel is considered enabled (opt-out model).
|
|
57
|
-
* 4. Returns channels that have an active template AND are not disabled by the customer.
|
|
58
|
-
*/
|
|
59
|
-
async getActiveChannels(orgId: string, customerId: string, event: string): Promise<Result<PrefChannel[]>> {
|
|
60
|
-
// Find active templates for this event (email, sms, push only — not print)
|
|
61
|
-
const templates = await this.db.select().from(notificationTemplates)
|
|
62
|
-
.where(and(
|
|
63
|
-
eq(notificationTemplates.organizationId, orgId),
|
|
64
|
-
eq(notificationTemplates.event, event),
|
|
65
|
-
eq(notificationTemplates.isActive, true),
|
|
66
|
-
));
|
|
67
|
-
|
|
68
|
-
const templateChannels = templates
|
|
69
|
-
.map((t) => t.channel)
|
|
70
|
-
.filter((ch): ch is PrefChannel => ch === "email" || ch === "sms" || ch === "push");
|
|
71
|
-
|
|
72
|
-
if (templateChannels.length === 0) return Ok([]);
|
|
73
|
-
|
|
74
|
-
// Get customer preferences
|
|
75
|
-
const prefs = await this.db.select().from(customerNotificationPrefs)
|
|
76
|
-
.where(and(
|
|
77
|
-
eq(customerNotificationPrefs.organizationId, orgId),
|
|
78
|
-
eq(customerNotificationPrefs.customerId, customerId),
|
|
79
|
-
));
|
|
80
|
-
|
|
81
|
-
const prefMap = new Map(prefs.map((p) => [p.channel, p.isEnabled]));
|
|
82
|
-
|
|
83
|
-
// Filter: include channel if no preference (opt-out model) or explicitly enabled
|
|
84
|
-
const active = templateChannels.filter((ch) => {
|
|
85
|
-
const enabled = prefMap.get(ch);
|
|
86
|
-
return enabled !== false; // undefined (no pref) → allowed; true → allowed; false → blocked
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
// Deduplicate
|
|
90
|
-
return Ok([...new Set(active)]);
|
|
91
|
-
}
|
|
92
50
|
}
|