@wopr-network/platform-core 1.55.0 → 1.56.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.
@@ -61,6 +61,8 @@ export interface BetterAuthConfig {
61
61
  rateLimitRules?: Record<string, AuthRateLimitRule>;
62
62
  /** Trusted origins for CORS. Falls back to UI_ORIGIN env var. */
63
63
  trustedOrigins?: string[];
64
+ /** Brand name used in email templates. Default: "WOPR" */
65
+ brandName?: string;
64
66
  /** Called after a new user signs up (e.g., create personal tenant). */
65
67
  onUserCreated?: (userId: string, userName: string, email: string) => Promise<void>;
66
68
  }
@@ -160,7 +160,7 @@ function authOptions(cfg) {
160
160
  sendResetPassword: async ({ user, url }) => {
161
161
  try {
162
162
  const emailClient = getEmailClient();
163
- const template = passwordResetEmailTemplate(url, user.email);
163
+ const template = passwordResetEmailTemplate(url, user.email, cfg.brandName);
164
164
  await emailClient.send({
165
165
  to: user.email,
166
166
  ...template,
@@ -199,7 +199,7 @@ function authOptions(cfg) {
199
199
  const { token } = await generateVerificationToken(pool, user.id);
200
200
  const verifyUrl = `${baseURL}${basePath}/verify?token=${token}`;
201
201
  const emailClient = getEmailClient();
202
- const template = verifyEmailTemplate(verifyUrl, user.email);
202
+ const template = verifyEmailTemplate(verifyUrl, user.email, cfg.brandName);
203
203
  await emailClient.send({
204
204
  to: user.email,
205
205
  ...template,
@@ -17,5 +17,16 @@ export declare class DrizzleNotificationTemplateRepository implements INotificat
17
17
  htmlBody: string;
18
18
  textBody: string;
19
19
  }>): Promise<number>;
20
+ /**
21
+ * Re-seed all templates using upsert — overwrites existing content
22
+ * while preserving row IDs and createdAt timestamps.
23
+ */
24
+ reseedAll(templates: Array<{
25
+ name: string;
26
+ description: string;
27
+ subject: string;
28
+ htmlBody: string;
29
+ textBody: string;
30
+ }>): Promise<number>;
20
31
  private toRow;
21
32
  }
@@ -74,6 +74,24 @@ export class DrizzleNotificationTemplateRepository {
74
74
  .returning({ id: notificationTemplates.id });
75
75
  return result.length;
76
76
  }
77
+ /**
78
+ * Re-seed all templates using upsert — overwrites existing content
79
+ * while preserving row IDs and createdAt timestamps.
80
+ */
81
+ async reseedAll(templates) {
82
+ let count = 0;
83
+ for (const t of templates) {
84
+ await this.upsert(t.name, {
85
+ description: t.description,
86
+ subject: t.subject,
87
+ htmlBody: t.htmlBody,
88
+ textBody: t.textBody,
89
+ active: true,
90
+ });
91
+ count++;
92
+ }
93
+ return count;
94
+ }
77
95
  toRow(r) {
78
96
  return {
79
97
  id: r.id,
@@ -8,7 +8,10 @@ import type { INotificationQueueRepository } from "./notification-repository-typ
8
8
  export declare class NotificationService {
9
9
  private readonly queue;
10
10
  private readonly appBaseUrl;
11
- constructor(queue: INotificationQueueRepository, appBaseUrl: string);
11
+ private readonly brandName;
12
+ constructor(queue: INotificationQueueRepository, appBaseUrl: string, brandName?: string);
13
+ /** Enqueue with brandName injected into every data bag. */
14
+ private enqueue;
12
15
  private creditsUrl;
13
16
  notifyLowBalance(tenantId: string, email: string, balanceDollars: string, estimatedDays: number): void;
14
17
  notifyCreditsDepeleted(tenantId: string, email: string): void;
@@ -7,9 +7,15 @@
7
7
  export class NotificationService {
8
8
  queue;
9
9
  appBaseUrl;
10
- constructor(queue, appBaseUrl) {
10
+ brandName;
11
+ constructor(queue, appBaseUrl, brandName = "WOPR") {
11
12
  this.queue = queue;
12
13
  this.appBaseUrl = appBaseUrl;
14
+ this.brandName = brandName;
15
+ }
16
+ /** Enqueue with brandName injected into every data bag. */
17
+ enqueue(tenantId, template, data) {
18
+ this.queue.enqueue(tenantId, template, { ...data, brandName: this.brandName });
13
19
  }
14
20
  creditsUrl() {
15
21
  return `${this.appBaseUrl}/billing/credits`;
@@ -18,7 +24,7 @@ export class NotificationService {
18
24
  // Credit & Billing
19
25
  // ---------------------------------------------------------------------------
20
26
  notifyLowBalance(tenantId, email, balanceDollars, estimatedDays) {
21
- this.queue.enqueue(tenantId, "low-balance", {
27
+ this.enqueue(tenantId, "low-balance", {
22
28
  email,
23
29
  balanceDollars,
24
30
  estimatedDaysRemaining: estimatedDays,
@@ -26,13 +32,13 @@ export class NotificationService {
26
32
  });
27
33
  }
28
34
  notifyCreditsDepeleted(tenantId, email) {
29
- this.queue.enqueue(tenantId, "credits-depleted", {
35
+ this.enqueue(tenantId, "credits-depleted", {
30
36
  email,
31
37
  creditsUrl: this.creditsUrl(),
32
38
  });
33
39
  }
34
40
  notifyGracePeriodStart(tenantId, email, balanceDollars, graceDays) {
35
- this.queue.enqueue(tenantId, "grace-period-start", {
41
+ this.enqueue(tenantId, "grace-period-start", {
36
42
  email,
37
43
  balanceDollars,
38
44
  graceDays,
@@ -40,20 +46,20 @@ export class NotificationService {
40
46
  });
41
47
  }
42
48
  notifyGracePeriodWarning(tenantId, email) {
43
- this.queue.enqueue(tenantId, "grace-period-warning", {
49
+ this.enqueue(tenantId, "grace-period-warning", {
44
50
  email,
45
51
  creditsUrl: this.creditsUrl(),
46
52
  });
47
53
  }
48
54
  notifyAutoSuspended(tenantId, email, reason) {
49
- this.queue.enqueue(tenantId, "auto-suspended", {
55
+ this.enqueue(tenantId, "auto-suspended", {
50
56
  email,
51
57
  reason,
52
58
  creditsUrl: this.creditsUrl(),
53
59
  });
54
60
  }
55
61
  notifyAutoTopUpSuccess(tenantId, email, amountDollars, newBalanceDollars) {
56
- this.queue.enqueue(tenantId, "auto-topup-success", {
62
+ this.enqueue(tenantId, "auto-topup-success", {
57
63
  email,
58
64
  amountDollars,
59
65
  newBalanceDollars,
@@ -61,20 +67,20 @@ export class NotificationService {
61
67
  });
62
68
  }
63
69
  notifyAutoTopUpFailed(tenantId, email) {
64
- this.queue.enqueue(tenantId, "auto-topup-failed", {
70
+ this.enqueue(tenantId, "auto-topup-failed", {
65
71
  email,
66
72
  creditsUrl: this.creditsUrl(),
67
73
  });
68
74
  }
69
75
  notifyAffiliateCreditMatch(tenantId, email, amountDollars) {
70
- this.queue.enqueue(tenantId, "affiliate-credit-match", {
76
+ this.enqueue(tenantId, "affiliate-credit-match", {
71
77
  email,
72
78
  amountDollars,
73
79
  creditsUrl: this.creditsUrl(),
74
80
  });
75
81
  }
76
82
  notifyDisputeCreated(tenantId, email, disputeId, amountDollars, reason) {
77
- this.queue.enqueue(tenantId, "dispute-created", {
83
+ this.enqueue(tenantId, "dispute-created", {
78
84
  email,
79
85
  disputeId,
80
86
  amountDollars,
@@ -83,7 +89,7 @@ export class NotificationService {
83
89
  });
84
90
  }
85
91
  notifyDisputeWon(tenantId, email, disputeId, amountDollars) {
86
- this.queue.enqueue(tenantId, "dispute-won", {
92
+ this.enqueue(tenantId, "dispute-won", {
87
93
  email,
88
94
  disputeId,
89
95
  amountDollars,
@@ -91,7 +97,7 @@ export class NotificationService {
91
97
  });
92
98
  }
93
99
  notifyDisputeLost(tenantId, email, disputeId, amountDollars) {
94
- this.queue.enqueue(tenantId, "dispute-lost", {
100
+ this.enqueue(tenantId, "dispute-lost", {
95
101
  email,
96
102
  disputeId,
97
103
  amountDollars,
@@ -99,7 +105,7 @@ export class NotificationService {
99
105
  });
100
106
  }
101
107
  notifySpendThresholdAlert(tenantId, email, currentSpendDollars, alertAtDollars) {
102
- this.queue.enqueue(tenantId, "spend-alert", {
108
+ this.enqueue(tenantId, "spend-alert", {
103
109
  email,
104
110
  currentSpendDollars,
105
111
  alertAtDollars,
@@ -107,7 +113,7 @@ export class NotificationService {
107
113
  });
108
114
  }
109
115
  notifyCreditPurchaseReceipt(tenantId, email, amountDollars, newBalanceDollars) {
110
- this.queue.enqueue(tenantId, "credit-purchase-receipt", {
116
+ this.enqueue(tenantId, "credit-purchase-receipt", {
111
117
  email,
112
118
  amountDollars,
113
119
  newBalanceDollars,
@@ -116,7 +122,7 @@ export class NotificationService {
116
122
  }
117
123
  notifyDividendWeeklyDigest(tenantId, email, weeklyTotalDollars, weeklyTotalCents, lifetimeTotalDollars, distributionCount, poolAvgCents, activeUsersAvg, nextDividendDate, weekStartDate, weekEndDate) {
118
124
  const unsubscribeUrl = `${this.appBaseUrl}/settings/notifications`;
119
- this.queue.enqueue(tenantId, "dividend-weekly-digest", {
125
+ this.enqueue(tenantId, "dividend-weekly-digest", {
120
126
  email,
121
127
  weeklyTotalDollars,
122
128
  weeklyTotalCents,
@@ -132,7 +138,7 @@ export class NotificationService {
132
138
  });
133
139
  }
134
140
  notifyCryptoPaymentConfirmed(tenantId, email, amountDollars, newBalanceDollars) {
135
- this.queue.enqueue(tenantId, "crypto-payment-confirmed", {
141
+ this.enqueue(tenantId, "crypto-payment-confirmed", {
136
142
  email,
137
143
  amountDollars,
138
144
  newBalanceDollars,
@@ -142,56 +148,56 @@ export class NotificationService {
142
148
  // Account
143
149
  // ---------------------------------------------------------------------------
144
150
  notifyAdminSuspended(tenantId, email, reason) {
145
- this.queue.enqueue(tenantId, "admin-suspended", { email, reason });
151
+ this.enqueue(tenantId, "admin-suspended", { email, reason });
146
152
  }
147
153
  notifyAdminReactivated(tenantId, email) {
148
- this.queue.enqueue(tenantId, "admin-reactivated", { email });
154
+ this.enqueue(tenantId, "admin-reactivated", { email });
149
155
  }
150
156
  notifyCreditsGranted(tenantId, email, amountDollars, reason) {
151
- this.queue.enqueue(tenantId, "credits-granted", { email, amountDollars, reason });
157
+ this.enqueue(tenantId, "credits-granted", { email, amountDollars, reason });
152
158
  }
153
159
  notifyRoleChanged(tenantId, email, newRole) {
154
- this.queue.enqueue(tenantId, "role-changed", { email, newRole });
160
+ this.enqueue(tenantId, "role-changed", { email, newRole });
155
161
  }
156
162
  notifyTeamInvite(tenantId, email, tenantName, inviteUrl) {
157
- this.queue.enqueue(tenantId, "team-invite", { email, tenantName, inviteUrl });
163
+ this.enqueue(tenantId, "team-invite", { email, tenantName, inviteUrl });
158
164
  }
159
165
  // ---------------------------------------------------------------------------
160
166
  // Agent & Channel
161
167
  // ---------------------------------------------------------------------------
162
168
  notifyAgentCreated(tenantId, email, agentName) {
163
- this.queue.enqueue(tenantId, "agent-created", { email, agentName });
169
+ this.enqueue(tenantId, "agent-created", { email, agentName });
164
170
  }
165
171
  notifyChannelConnected(tenantId, email, channelName, agentName) {
166
- this.queue.enqueue(tenantId, "channel-connected", { email, channelName, agentName });
172
+ this.enqueue(tenantId, "channel-connected", { email, channelName, agentName });
167
173
  }
168
174
  notifyChannelDisconnected(tenantId, email, channelName, agentName, reason) {
169
- this.queue.enqueue(tenantId, "channel-disconnected", { email, channelName, agentName, reason });
175
+ this.enqueue(tenantId, "channel-disconnected", { email, channelName, agentName, reason });
170
176
  }
171
177
  notifyAgentSuspended(tenantId, email, agentName, reason) {
172
- this.queue.enqueue(tenantId, "agent-suspended", { email, agentName, reason });
178
+ this.enqueue(tenantId, "agent-suspended", { email, agentName, reason });
173
179
  }
174
180
  // ---------------------------------------------------------------------------
175
181
  // Account Deletion
176
182
  // ---------------------------------------------------------------------------
177
183
  notifyAccountDeletionRequested(tenantId, email, deleteAfterDate) {
178
- this.queue.enqueue(tenantId, "account-deletion-requested", {
184
+ this.enqueue(tenantId, "account-deletion-requested", {
179
185
  email,
180
186
  deleteAfterDate,
181
187
  cancelUrl: `${this.appBaseUrl}/settings/account`,
182
188
  });
183
189
  }
184
190
  notifyAccountDeletionCancelled(tenantId, email) {
185
- this.queue.enqueue(tenantId, "account-deletion-cancelled", { email });
191
+ this.enqueue(tenantId, "account-deletion-cancelled", { email });
186
192
  }
187
193
  notifyAccountDeletionCompleted(tenantId, email) {
188
- this.queue.enqueue(tenantId, "account-deletion-completed", { email });
194
+ this.enqueue(tenantId, "account-deletion-completed", { email });
189
195
  }
190
196
  // ---------------------------------------------------------------------------
191
197
  // Fleet Updates
192
198
  // ---------------------------------------------------------------------------
193
199
  notifyFleetUpdateAvailable(tenantId, email, version, changelogDate, changelogSummary) {
194
- this.queue.enqueue(tenantId, "fleet-update-available", {
200
+ this.enqueue(tenantId, "fleet-update-available", {
195
201
  email,
196
202
  version,
197
203
  changelogDate,
@@ -200,7 +206,7 @@ export class NotificationService {
200
206
  });
201
207
  }
202
208
  notifyFleetUpdateComplete(tenantId, email, version, succeeded, failed) {
203
- this.queue.enqueue(tenantId, "fleet-update-complete", {
209
+ this.enqueue(tenantId, "fleet-update-complete", {
204
210
  email,
205
211
  version,
206
212
  succeeded,
@@ -213,6 +219,6 @@ export class NotificationService {
213
219
  // Admin custom email
214
220
  // ---------------------------------------------------------------------------
215
221
  sendCustomEmail(tenantId, email, subject, bodyText) {
216
- this.queue.enqueue(tenantId, "custom", { email, subject, bodyText });
222
+ this.enqueue(tenantId, "custom", { email, subject, bodyText });
217
223
  }
218
224
  }
@@ -25,6 +25,7 @@ describe("NotificationService", () => {
25
25
  balanceDollars: "$2.50",
26
26
  estimatedDaysRemaining: 5,
27
27
  creditsUrl: `${BASE_URL}/billing/credits`,
28
+ brandName: "WOPR",
28
29
  });
29
30
  });
30
31
  });
@@ -34,6 +35,7 @@ describe("NotificationService", () => {
34
35
  expect(queue.enqueue).toHaveBeenCalledWith("tenant-1", "credits-depleted", {
35
36
  email: "user@example.com",
36
37
  creditsUrl: `${BASE_URL}/billing/credits`,
38
+ brandName: "WOPR",
37
39
  });
38
40
  });
39
41
  });
@@ -45,6 +47,7 @@ describe("NotificationService", () => {
45
47
  balanceDollars: "$0.50",
46
48
  graceDays: 7,
47
49
  creditsUrl: `${BASE_URL}/billing/credits`,
50
+ brandName: "WOPR",
48
51
  });
49
52
  });
50
53
  });
@@ -54,6 +57,7 @@ describe("NotificationService", () => {
54
57
  expect(queue.enqueue).toHaveBeenCalledWith("tenant-1", "grace-period-warning", {
55
58
  email: "user@example.com",
56
59
  creditsUrl: `${BASE_URL}/billing/credits`,
60
+ brandName: "WOPR",
57
61
  });
58
62
  });
59
63
  });
@@ -64,6 +68,7 @@ describe("NotificationService", () => {
64
68
  email: "user@example.com",
65
69
  reason: "Grace period expired",
66
70
  creditsUrl: `${BASE_URL}/billing/credits`,
71
+ brandName: "WOPR",
67
72
  });
68
73
  });
69
74
  });
@@ -73,6 +78,7 @@ describe("NotificationService", () => {
73
78
  expect(queue.enqueue).toHaveBeenCalledWith("tenant-1", "admin-suspended", {
74
79
  email: "user@example.com",
75
80
  reason: "ToS violation",
81
+ brandName: "WOPR",
76
82
  });
77
83
  });
78
84
  });
@@ -81,6 +87,7 @@ describe("NotificationService", () => {
81
87
  service.notifyAdminReactivated("tenant-1", "user@example.com");
82
88
  expect(queue.enqueue).toHaveBeenCalledWith("tenant-1", "admin-reactivated", {
83
89
  email: "user@example.com",
90
+ brandName: "WOPR",
84
91
  });
85
92
  });
86
93
  });
@@ -91,6 +98,7 @@ describe("NotificationService", () => {
91
98
  email: "user@example.com",
92
99
  amountDollars: "$5.00",
93
100
  reason: "Support credit",
101
+ brandName: "WOPR",
94
102
  });
95
103
  });
96
104
  });
@@ -100,6 +108,7 @@ describe("NotificationService", () => {
100
108
  expect(queue.enqueue).toHaveBeenCalledWith("tenant-1", "role-changed", {
101
109
  email: "user@example.com",
102
110
  newRole: "tenant_admin",
111
+ brandName: "WOPR",
103
112
  });
104
113
  });
105
114
  });
@@ -110,6 +119,7 @@ describe("NotificationService", () => {
110
119
  email: "user@example.com",
111
120
  tenantName: "Acme Corp",
112
121
  inviteUrl: "https://app.wopr.bot/invite/abc",
122
+ brandName: "WOPR",
113
123
  });
114
124
  });
115
125
  });
@@ -121,6 +131,7 @@ describe("NotificationService", () => {
121
131
  channelName: "Discord",
122
132
  agentName: "MyBot",
123
133
  reason: "Token expired",
134
+ brandName: "WOPR",
124
135
  });
125
136
  });
126
137
  });
@@ -131,6 +142,7 @@ describe("NotificationService", () => {
131
142
  email: "user@example.com",
132
143
  subject: "Hello there",
133
144
  bodyText: "This is the body.",
145
+ brandName: "WOPR",
134
146
  });
135
147
  });
136
148
  });
@@ -143,6 +155,7 @@ describe("NotificationService", () => {
143
155
  amountDollars: "$50.00",
144
156
  reason: "fraudulent",
145
157
  creditsUrl: `${BASE_URL}/billing/credits`,
158
+ brandName: "WOPR",
146
159
  });
147
160
  });
148
161
  });
@@ -154,6 +167,7 @@ describe("NotificationService", () => {
154
167
  disputeId: "dp_123",
155
168
  amountDollars: "$50.00",
156
169
  creditsUrl: `${BASE_URL}/billing/credits`,
170
+ brandName: "WOPR",
157
171
  });
158
172
  });
159
173
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-core",
3
- "version": "1.55.0",
3
+ "version": "1.56.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -80,6 +80,10 @@ export interface BetterAuthConfig {
80
80
  /** Trusted origins for CORS. Falls back to UI_ORIGIN env var. */
81
81
  trustedOrigins?: string[];
82
82
 
83
+ // --- Branding ---
84
+ /** Brand name used in email templates. Default: "WOPR" */
85
+ brandName?: string;
86
+
83
87
  // --- Lifecycle hooks ---
84
88
  /** Called after a new user signs up (e.g., create personal tenant). */
85
89
  onUserCreated?: (userId: string, userName: string, email: string) => Promise<void>;
@@ -228,7 +232,7 @@ function authOptions(cfg: BetterAuthConfig): BetterAuthOptions {
228
232
  sendResetPassword: async ({ user, url }) => {
229
233
  try {
230
234
  const emailClient = getEmailClient();
231
- const template = passwordResetEmailTemplate(url, user.email);
235
+ const template = passwordResetEmailTemplate(url, user.email, cfg.brandName);
232
236
  await emailClient.send({
233
237
  to: user.email,
234
238
  ...template,
@@ -266,7 +270,7 @@ function authOptions(cfg: BetterAuthConfig): BetterAuthOptions {
266
270
  const { token } = await generateVerificationToken(pool, user.id);
267
271
  const verifyUrl = `${baseURL}${basePath}/verify?token=${token}`;
268
272
  const emailClient = getEmailClient();
269
- const template = verifyEmailTemplate(verifyUrl, user.email);
273
+ const template = verifyEmailTemplate(verifyUrl, user.email, cfg.brandName);
270
274
  await emailClient.send({
271
275
  to: user.email,
272
276
  ...template,
@@ -92,6 +92,33 @@ export class DrizzleNotificationTemplateRepository implements INotificationTempl
92
92
  return result.length;
93
93
  }
94
94
 
95
+ /**
96
+ * Re-seed all templates using upsert — overwrites existing content
97
+ * while preserving row IDs and createdAt timestamps.
98
+ */
99
+ async reseedAll(
100
+ templates: Array<{
101
+ name: string;
102
+ description: string;
103
+ subject: string;
104
+ htmlBody: string;
105
+ textBody: string;
106
+ }>,
107
+ ): Promise<number> {
108
+ let count = 0;
109
+ for (const t of templates) {
110
+ await this.upsert(t.name, {
111
+ description: t.description,
112
+ subject: t.subject,
113
+ htmlBody: t.htmlBody,
114
+ textBody: t.textBody,
115
+ active: true,
116
+ });
117
+ count++;
118
+ }
119
+ return count;
120
+ }
121
+
95
122
  private toRow(r: typeof notificationTemplates.$inferSelect): NotificationTemplateRow {
96
123
  return {
97
124
  id: r.id,
@@ -30,6 +30,7 @@ describe("NotificationService", () => {
30
30
  balanceDollars: "$2.50",
31
31
  estimatedDaysRemaining: 5,
32
32
  creditsUrl: `${BASE_URL}/billing/credits`,
33
+ brandName: "WOPR",
33
34
  });
34
35
  });
35
36
  });
@@ -40,6 +41,7 @@ describe("NotificationService", () => {
40
41
  expect(queue.enqueue).toHaveBeenCalledWith("tenant-1", "credits-depleted", {
41
42
  email: "user@example.com",
42
43
  creditsUrl: `${BASE_URL}/billing/credits`,
44
+ brandName: "WOPR",
43
45
  });
44
46
  });
45
47
  });
@@ -52,6 +54,7 @@ describe("NotificationService", () => {
52
54
  balanceDollars: "$0.50",
53
55
  graceDays: 7,
54
56
  creditsUrl: `${BASE_URL}/billing/credits`,
57
+ brandName: "WOPR",
55
58
  });
56
59
  });
57
60
  });
@@ -62,6 +65,7 @@ describe("NotificationService", () => {
62
65
  expect(queue.enqueue).toHaveBeenCalledWith("tenant-1", "grace-period-warning", {
63
66
  email: "user@example.com",
64
67
  creditsUrl: `${BASE_URL}/billing/credits`,
68
+ brandName: "WOPR",
65
69
  });
66
70
  });
67
71
  });
@@ -73,6 +77,7 @@ describe("NotificationService", () => {
73
77
  email: "user@example.com",
74
78
  reason: "Grace period expired",
75
79
  creditsUrl: `${BASE_URL}/billing/credits`,
80
+ brandName: "WOPR",
76
81
  });
77
82
  });
78
83
  });
@@ -83,6 +88,7 @@ describe("NotificationService", () => {
83
88
  expect(queue.enqueue).toHaveBeenCalledWith("tenant-1", "admin-suspended", {
84
89
  email: "user@example.com",
85
90
  reason: "ToS violation",
91
+ brandName: "WOPR",
86
92
  });
87
93
  });
88
94
  });
@@ -92,6 +98,7 @@ describe("NotificationService", () => {
92
98
  service.notifyAdminReactivated("tenant-1", "user@example.com");
93
99
  expect(queue.enqueue).toHaveBeenCalledWith("tenant-1", "admin-reactivated", {
94
100
  email: "user@example.com",
101
+ brandName: "WOPR",
95
102
  });
96
103
  });
97
104
  });
@@ -103,6 +110,7 @@ describe("NotificationService", () => {
103
110
  email: "user@example.com",
104
111
  amountDollars: "$5.00",
105
112
  reason: "Support credit",
113
+ brandName: "WOPR",
106
114
  });
107
115
  });
108
116
  });
@@ -113,6 +121,7 @@ describe("NotificationService", () => {
113
121
  expect(queue.enqueue).toHaveBeenCalledWith("tenant-1", "role-changed", {
114
122
  email: "user@example.com",
115
123
  newRole: "tenant_admin",
124
+ brandName: "WOPR",
116
125
  });
117
126
  });
118
127
  });
@@ -124,6 +133,7 @@ describe("NotificationService", () => {
124
133
  email: "user@example.com",
125
134
  tenantName: "Acme Corp",
126
135
  inviteUrl: "https://app.wopr.bot/invite/abc",
136
+ brandName: "WOPR",
127
137
  });
128
138
  });
129
139
  });
@@ -136,6 +146,7 @@ describe("NotificationService", () => {
136
146
  channelName: "Discord",
137
147
  agentName: "MyBot",
138
148
  reason: "Token expired",
149
+ brandName: "WOPR",
139
150
  });
140
151
  });
141
152
  });
@@ -147,6 +158,7 @@ describe("NotificationService", () => {
147
158
  email: "user@example.com",
148
159
  subject: "Hello there",
149
160
  bodyText: "This is the body.",
161
+ brandName: "WOPR",
150
162
  });
151
163
  });
152
164
  });
@@ -160,6 +172,7 @@ describe("NotificationService", () => {
160
172
  amountDollars: "$50.00",
161
173
  reason: "fraudulent",
162
174
  creditsUrl: `${BASE_URL}/billing/credits`,
175
+ brandName: "WOPR",
163
176
  });
164
177
  });
165
178
  });
@@ -172,6 +185,7 @@ describe("NotificationService", () => {
172
185
  disputeId: "dp_123",
173
186
  amountDollars: "$50.00",
174
187
  creditsUrl: `${BASE_URL}/billing/credits`,
188
+ brandName: "WOPR",
175
189
  });
176
190
  });
177
191
  });
@@ -11,8 +11,14 @@ export class NotificationService {
11
11
  constructor(
12
12
  private readonly queue: INotificationQueueRepository,
13
13
  private readonly appBaseUrl: string,
14
+ private readonly brandName: string = "WOPR",
14
15
  ) {}
15
16
 
17
+ /** Enqueue with brandName injected into every data bag. */
18
+ private enqueue(tenantId: string, template: string, data: Record<string, unknown>): void {
19
+ this.queue.enqueue(tenantId, template, { ...data, brandName: this.brandName });
20
+ }
21
+
16
22
  private creditsUrl(): string {
17
23
  return `${this.appBaseUrl}/billing/credits`;
18
24
  }
@@ -22,7 +28,7 @@ export class NotificationService {
22
28
  // ---------------------------------------------------------------------------
23
29
 
24
30
  notifyLowBalance(tenantId: string, email: string, balanceDollars: string, estimatedDays: number): void {
25
- this.queue.enqueue(tenantId, "low-balance", {
31
+ this.enqueue(tenantId, "low-balance", {
26
32
  email,
27
33
  balanceDollars,
28
34
  estimatedDaysRemaining: estimatedDays,
@@ -31,14 +37,14 @@ export class NotificationService {
31
37
  }
32
38
 
33
39
  notifyCreditsDepeleted(tenantId: string, email: string): void {
34
- this.queue.enqueue(tenantId, "credits-depleted", {
40
+ this.enqueue(tenantId, "credits-depleted", {
35
41
  email,
36
42
  creditsUrl: this.creditsUrl(),
37
43
  });
38
44
  }
39
45
 
40
46
  notifyGracePeriodStart(tenantId: string, email: string, balanceDollars: string, graceDays: number): void {
41
- this.queue.enqueue(tenantId, "grace-period-start", {
47
+ this.enqueue(tenantId, "grace-period-start", {
42
48
  email,
43
49
  balanceDollars,
44
50
  graceDays,
@@ -47,14 +53,14 @@ export class NotificationService {
47
53
  }
48
54
 
49
55
  notifyGracePeriodWarning(tenantId: string, email: string): void {
50
- this.queue.enqueue(tenantId, "grace-period-warning", {
56
+ this.enqueue(tenantId, "grace-period-warning", {
51
57
  email,
52
58
  creditsUrl: this.creditsUrl(),
53
59
  });
54
60
  }
55
61
 
56
62
  notifyAutoSuspended(tenantId: string, email: string, reason: string): void {
57
- this.queue.enqueue(tenantId, "auto-suspended", {
63
+ this.enqueue(tenantId, "auto-suspended", {
58
64
  email,
59
65
  reason,
60
66
  creditsUrl: this.creditsUrl(),
@@ -62,7 +68,7 @@ export class NotificationService {
62
68
  }
63
69
 
64
70
  notifyAutoTopUpSuccess(tenantId: string, email: string, amountDollars: string, newBalanceDollars: string): void {
65
- this.queue.enqueue(tenantId, "auto-topup-success", {
71
+ this.enqueue(tenantId, "auto-topup-success", {
66
72
  email,
67
73
  amountDollars,
68
74
  newBalanceDollars,
@@ -71,14 +77,14 @@ export class NotificationService {
71
77
  }
72
78
 
73
79
  notifyAutoTopUpFailed(tenantId: string, email: string): void {
74
- this.queue.enqueue(tenantId, "auto-topup-failed", {
80
+ this.enqueue(tenantId, "auto-topup-failed", {
75
81
  email,
76
82
  creditsUrl: this.creditsUrl(),
77
83
  });
78
84
  }
79
85
 
80
86
  notifyAffiliateCreditMatch(tenantId: string, email: string, amountDollars: string): void {
81
- this.queue.enqueue(tenantId, "affiliate-credit-match", {
87
+ this.enqueue(tenantId, "affiliate-credit-match", {
82
88
  email,
83
89
  amountDollars,
84
90
  creditsUrl: this.creditsUrl(),
@@ -92,7 +98,7 @@ export class NotificationService {
92
98
  amountDollars: string,
93
99
  reason: string,
94
100
  ): void {
95
- this.queue.enqueue(tenantId, "dispute-created", {
101
+ this.enqueue(tenantId, "dispute-created", {
96
102
  email,
97
103
  disputeId,
98
104
  amountDollars,
@@ -102,7 +108,7 @@ export class NotificationService {
102
108
  }
103
109
 
104
110
  notifyDisputeWon(tenantId: string, email: string, disputeId: string, amountDollars: string): void {
105
- this.queue.enqueue(tenantId, "dispute-won", {
111
+ this.enqueue(tenantId, "dispute-won", {
106
112
  email,
107
113
  disputeId,
108
114
  amountDollars,
@@ -111,7 +117,7 @@ export class NotificationService {
111
117
  }
112
118
 
113
119
  notifyDisputeLost(tenantId: string, email: string, disputeId: string, amountDollars: string): void {
114
- this.queue.enqueue(tenantId, "dispute-lost", {
120
+ this.enqueue(tenantId, "dispute-lost", {
115
121
  email,
116
122
  disputeId,
117
123
  amountDollars,
@@ -125,7 +131,7 @@ export class NotificationService {
125
131
  currentSpendDollars: string,
126
132
  alertAtDollars: string,
127
133
  ): void {
128
- this.queue.enqueue(tenantId, "spend-alert", {
134
+ this.enqueue(tenantId, "spend-alert", {
129
135
  email,
130
136
  currentSpendDollars,
131
137
  alertAtDollars,
@@ -134,7 +140,7 @@ export class NotificationService {
134
140
  }
135
141
 
136
142
  notifyCreditPurchaseReceipt(tenantId: string, email: string, amountDollars: string, newBalanceDollars: string): void {
137
- this.queue.enqueue(tenantId, "credit-purchase-receipt", {
143
+ this.enqueue(tenantId, "credit-purchase-receipt", {
138
144
  email,
139
145
  amountDollars,
140
146
  newBalanceDollars,
@@ -156,7 +162,7 @@ export class NotificationService {
156
162
  weekEndDate: string,
157
163
  ): void {
158
164
  const unsubscribeUrl = `${this.appBaseUrl}/settings/notifications`;
159
- this.queue.enqueue(tenantId, "dividend-weekly-digest", {
165
+ this.enqueue(tenantId, "dividend-weekly-digest", {
160
166
  email,
161
167
  weeklyTotalDollars,
162
168
  weeklyTotalCents,
@@ -178,7 +184,7 @@ export class NotificationService {
178
184
  amountDollars: string,
179
185
  newBalanceDollars: string,
180
186
  ): void {
181
- this.queue.enqueue(tenantId, "crypto-payment-confirmed", {
187
+ this.enqueue(tenantId, "crypto-payment-confirmed", {
182
188
  email,
183
189
  amountDollars,
184
190
  newBalanceDollars,
@@ -190,23 +196,23 @@ export class NotificationService {
190
196
  // ---------------------------------------------------------------------------
191
197
 
192
198
  notifyAdminSuspended(tenantId: string, email: string, reason: string): void {
193
- this.queue.enqueue(tenantId, "admin-suspended", { email, reason });
199
+ this.enqueue(tenantId, "admin-suspended", { email, reason });
194
200
  }
195
201
 
196
202
  notifyAdminReactivated(tenantId: string, email: string): void {
197
- this.queue.enqueue(tenantId, "admin-reactivated", { email });
203
+ this.enqueue(tenantId, "admin-reactivated", { email });
198
204
  }
199
205
 
200
206
  notifyCreditsGranted(tenantId: string, email: string, amountDollars: string, reason: string): void {
201
- this.queue.enqueue(tenantId, "credits-granted", { email, amountDollars, reason });
207
+ this.enqueue(tenantId, "credits-granted", { email, amountDollars, reason });
202
208
  }
203
209
 
204
210
  notifyRoleChanged(tenantId: string, email: string, newRole: string): void {
205
- this.queue.enqueue(tenantId, "role-changed", { email, newRole });
211
+ this.enqueue(tenantId, "role-changed", { email, newRole });
206
212
  }
207
213
 
208
214
  notifyTeamInvite(tenantId: string, email: string, tenantName: string, inviteUrl: string): void {
209
- this.queue.enqueue(tenantId, "team-invite", { email, tenantName, inviteUrl });
215
+ this.enqueue(tenantId, "team-invite", { email, tenantName, inviteUrl });
210
216
  }
211
217
 
212
218
  // ---------------------------------------------------------------------------
@@ -214,11 +220,11 @@ export class NotificationService {
214
220
  // ---------------------------------------------------------------------------
215
221
 
216
222
  notifyAgentCreated(tenantId: string, email: string, agentName: string): void {
217
- this.queue.enqueue(tenantId, "agent-created", { email, agentName });
223
+ this.enqueue(tenantId, "agent-created", { email, agentName });
218
224
  }
219
225
 
220
226
  notifyChannelConnected(tenantId: string, email: string, channelName: string, agentName: string): void {
221
- this.queue.enqueue(tenantId, "channel-connected", { email, channelName, agentName });
227
+ this.enqueue(tenantId, "channel-connected", { email, channelName, agentName });
222
228
  }
223
229
 
224
230
  notifyChannelDisconnected(
@@ -228,11 +234,11 @@ export class NotificationService {
228
234
  agentName: string,
229
235
  reason: string,
230
236
  ): void {
231
- this.queue.enqueue(tenantId, "channel-disconnected", { email, channelName, agentName, reason });
237
+ this.enqueue(tenantId, "channel-disconnected", { email, channelName, agentName, reason });
232
238
  }
233
239
 
234
240
  notifyAgentSuspended(tenantId: string, email: string, agentName: string, reason: string): void {
235
- this.queue.enqueue(tenantId, "agent-suspended", { email, agentName, reason });
241
+ this.enqueue(tenantId, "agent-suspended", { email, agentName, reason });
236
242
  }
237
243
 
238
244
  // ---------------------------------------------------------------------------
@@ -240,7 +246,7 @@ export class NotificationService {
240
246
  // ---------------------------------------------------------------------------
241
247
 
242
248
  notifyAccountDeletionRequested(tenantId: string, email: string, deleteAfterDate: string): void {
243
- this.queue.enqueue(tenantId, "account-deletion-requested", {
249
+ this.enqueue(tenantId, "account-deletion-requested", {
244
250
  email,
245
251
  deleteAfterDate,
246
252
  cancelUrl: `${this.appBaseUrl}/settings/account`,
@@ -248,11 +254,11 @@ export class NotificationService {
248
254
  }
249
255
 
250
256
  notifyAccountDeletionCancelled(tenantId: string, email: string): void {
251
- this.queue.enqueue(tenantId, "account-deletion-cancelled", { email });
257
+ this.enqueue(tenantId, "account-deletion-cancelled", { email });
252
258
  }
253
259
 
254
260
  notifyAccountDeletionCompleted(tenantId: string, email: string): void {
255
- this.queue.enqueue(tenantId, "account-deletion-completed", { email });
261
+ this.enqueue(tenantId, "account-deletion-completed", { email });
256
262
  }
257
263
 
258
264
  // ---------------------------------------------------------------------------
@@ -266,7 +272,7 @@ export class NotificationService {
266
272
  changelogDate: string,
267
273
  changelogSummary: string,
268
274
  ): void {
269
- this.queue.enqueue(tenantId, "fleet-update-available", {
275
+ this.enqueue(tenantId, "fleet-update-available", {
270
276
  email,
271
277
  version,
272
278
  changelogDate,
@@ -276,7 +282,7 @@ export class NotificationService {
276
282
  }
277
283
 
278
284
  notifyFleetUpdateComplete(tenantId: string, email: string, version: string, succeeded: number, failed: number): void {
279
- this.queue.enqueue(tenantId, "fleet-update-complete", {
285
+ this.enqueue(tenantId, "fleet-update-complete", {
280
286
  email,
281
287
  version,
282
288
  succeeded,
@@ -291,6 +297,6 @@ export class NotificationService {
291
297
  // ---------------------------------------------------------------------------
292
298
 
293
299
  sendCustomEmail(tenantId: string, email: string, subject: string, bodyText: string): void {
294
- this.queue.enqueue(tenantId, "custom", { email, subject, bodyText });
300
+ this.enqueue(tenantId, "custom", { email, subject, bodyText });
295
301
  }
296
302
  }