@wopr-network/platform-core 1.54.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.
- package/dist/auth/better-auth.d.ts +2 -0
- package/dist/auth/better-auth.js +2 -2
- package/dist/email/default-templates.js +49 -49
- package/dist/email/drizzle-notification-template-repository.d.ts +11 -0
- package/dist/email/drizzle-notification-template-repository.js +18 -0
- package/dist/email/notification-service.d.ts +4 -1
- package/dist/email/notification-service.js +37 -31
- package/dist/email/notification-service.test.js +14 -0
- package/dist/email/notification-templates.js +154 -111
- package/dist/email/templates.d.ts +9 -9
- package/dist/email/templates.js +61 -55
- package/dist/email/templates.test.js +1 -1
- package/package.json +1 -1
- package/src/auth/better-auth.ts +6 -2
- package/src/email/default-templates.ts +56 -56
- package/src/email/drizzle-notification-template-repository.ts +27 -0
- package/src/email/notification-service.test.ts +14 -0
- package/src/email/notification-service.ts +36 -30
- package/src/email/notification-templates.ts +155 -107
- package/src/email/templates.test.ts +1 -1
- package/src/email/templates.ts +67 -47
|
@@ -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
|
}
|
package/dist/auth/better-auth.js
CHANGED
|
@@ -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,
|
|
@@ -23,7 +23,7 @@ function layoutOpen(title) {
|
|
|
23
23
|
<table role="presentation" style="width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">`;
|
|
24
24
|
}
|
|
25
25
|
const LAYOUT_CLOSE = ` </table>
|
|
26
|
-
<p style="margin-top: 20px; color: #a0aec0; font-size: 12px;">© ${YEAR}
|
|
26
|
+
<p style="margin-top: 20px; color: #a0aec0; font-size: 12px;">© ${YEAR} {{brandName}}. All rights reserved.</p>
|
|
27
27
|
</td>
|
|
28
28
|
</tr>
|
|
29
29
|
</table>
|
|
@@ -60,17 +60,17 @@ function ft(text) {
|
|
|
60
60
|
function html(title, ...rows) {
|
|
61
61
|
return `${layoutOpen(title)}\n${rows.join("\n")}\n${LAYOUT_CLOSE}`;
|
|
62
62
|
}
|
|
63
|
-
const CR = `\n\n(c) ${YEAR}
|
|
63
|
+
const CR = `\n\n(c) ${YEAR} {{brandName}}. All rights reserved.`;
|
|
64
64
|
export const DEFAULT_TEMPLATES = [
|
|
65
65
|
// -- Credits & Billing ---------------------------------------------------
|
|
66
66
|
{
|
|
67
67
|
name: "credits-depleted",
|
|
68
68
|
description: "Sent when tenant credit balance reaches zero",
|
|
69
|
-
subject: "Your
|
|
70
|
-
htmlBody: html("Credits Depleted", hd("Your
|
|
71
|
-
textBody: `Your
|
|
69
|
+
subject: "Your {{brandName}} credits are depleted \u2014 capabilities paused",
|
|
70
|
+
htmlBody: html("Credits Depleted", hd("Your {{brandName}} Credits Are Depleted"), p("<p>Your {{brandName}} credit balance has reached $0. All agent capabilities have been paused.</p><p>Add credits now to resume service immediately.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Add Credits")}{{/if}}`, ft("Your data is preserved. Add credits to reactivate.")),
|
|
71
|
+
textBody: `Your {{brandName}} Credits Are Depleted
|
|
72
72
|
|
|
73
|
-
Your
|
|
73
|
+
Your {{brandName}} credit balance has reached $0. All agent capabilities have been paused.
|
|
74
74
|
|
|
75
75
|
Add credits now to resume service immediately.
|
|
76
76
|
{{#if creditsUrl}}
|
|
@@ -80,9 +80,9 @@ Add credits: {{creditsUrl}}
|
|
|
80
80
|
{
|
|
81
81
|
name: "grace-period-start",
|
|
82
82
|
description: "Sent when a tenant enters the grace period after failed billing",
|
|
83
|
-
subject: "Action needed: top up to keep your
|
|
84
|
-
htmlBody: html("Grace Period Started", hd("Action Needed: Top Up to Keep Your
|
|
85
|
-
textBody: `Action Needed: Top Up to Keep Your
|
|
83
|
+
subject: "Action needed: top up to keep your {{brandName}} agents running",
|
|
84
|
+
htmlBody: html("Grace Period Started", hd("Action Needed: Top Up to Keep Your {{brandName}} Agents Running"), p("<p>Your current balance is <strong>{{balanceDollars}}</strong> and the monthly deduction could not be processed.</p><p>You have a <strong>{{graceDays}}-day grace period</strong> to add credits before your account is suspended.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Add Credits Now")}{{/if}}`, ft("This is a critical notification about your account status.")),
|
|
85
|
+
textBody: `Action Needed: Top Up to Keep Your {{brandName}} Agents Running
|
|
86
86
|
|
|
87
87
|
Your current balance is {{balanceDollars}} and the monthly deduction could not be processed.
|
|
88
88
|
|
|
@@ -94,9 +94,9 @@ Add credits: {{creditsUrl}}
|
|
|
94
94
|
{
|
|
95
95
|
name: "grace-period-warning",
|
|
96
96
|
description: "Sent one day before grace period expires",
|
|
97
|
-
subject: "Last chance: your
|
|
98
|
-
htmlBody: html("Grace Period Warning", hd("Last Chance: Your
|
|
99
|
-
textBody: `Last Chance: Your
|
|
97
|
+
subject: "Last chance: your agents will be suspended tomorrow",
|
|
98
|
+
htmlBody: html("Grace Period Warning", hd("Last Chance: Your Agents Will Be Suspended Tomorrow"), p("<p>Your grace period expires tomorrow. If you do not add credits, your account will be suspended.</p><p>Add credits now to keep your agents running.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Add Credits Now", "#dc2626")}{{/if}}`, ft("This is a critical notification about your account status.")),
|
|
99
|
+
textBody: `Last Chance: Your Agents Will Be Suspended Tomorrow
|
|
100
100
|
|
|
101
101
|
Your grace period expires tomorrow. If you do not add credits, your account will be suspended.
|
|
102
102
|
{{#if creditsUrl}}
|
|
@@ -107,7 +107,7 @@ Add credits: {{creditsUrl}}
|
|
|
107
107
|
name: "auto-suspended",
|
|
108
108
|
description: "Sent when account is automatically suspended",
|
|
109
109
|
subject: "Your account has been suspended",
|
|
110
|
-
htmlBody: html("Account Suspended", hd("Your Account Has Been Suspended"), p("<p>Your
|
|
110
|
+
htmlBody: html("Account Suspended", hd("Your Account Has Been Suspended"), p("<p>Your {{brandName}} account has been automatically suspended.</p><p><strong>Reason:</strong> {{reason}}</p><p>Add credits to reactivate your account immediately.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Add Credits to Reactivate")}{{/if}}`, ft("Your data is preserved for 30 days.")),
|
|
111
111
|
textBody: `Your Account Has Been Suspended
|
|
112
112
|
|
|
113
113
|
Reason: {{reason}}
|
|
@@ -135,7 +135,7 @@ View credits: {{creditsUrl}}
|
|
|
135
135
|
name: "auto-topup-failed",
|
|
136
136
|
description: "Sent when auto top-up charge fails",
|
|
137
137
|
subject: "Auto top-up failed \u2014 update your payment method",
|
|
138
|
-
htmlBody: html("Auto Top-Up Failed", hd("Auto Top-Up Failed"), p("<p>Your auto top-up failed. We were unable to charge your payment method.</p><p>Please update your payment method or add credits manually to avoid service interruption.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Add Credits")}{{/if}}`, ft("If you need help, contact
|
|
138
|
+
htmlBody: html("Auto Top-Up Failed", hd("Auto Top-Up Failed"), p("<p>Your auto top-up failed. We were unable to charge your payment method.</p><p>Please update your payment method or add credits manually to avoid service interruption.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Add Credits")}{{/if}}`, ft("If you need help, contact {{supportEmail}}.")),
|
|
139
139
|
textBody: `Auto Top-Up Failed
|
|
140
140
|
|
|
141
141
|
Your auto top-up failed. We were unable to charge your payment method.
|
|
@@ -149,7 +149,7 @@ Add credits: {{creditsUrl}}
|
|
|
149
149
|
name: "crypto-payment-confirmed",
|
|
150
150
|
description: "Sent when a crypto payment is confirmed on-chain",
|
|
151
151
|
subject: "Crypto payment confirmed: {{amountDollars}} credits added",
|
|
152
|
-
htmlBody: html("Crypto Payment Confirmed", hd("Crypto Payment Confirmed: {{amountDollars}} Credits Added"), p("<p>Your crypto payment has been confirmed. <strong>{{amountDollars}}</strong> in credits has been added to your account.</p><p>Your new balance is <strong>{{newBalanceDollars}}</strong>.</p>"), ft("Thank you for supporting
|
|
152
|
+
htmlBody: html("Crypto Payment Confirmed", hd("Crypto Payment Confirmed: {{amountDollars}} Credits Added"), p("<p>Your crypto payment has been confirmed. <strong>{{amountDollars}}</strong> in credits has been added to your account.</p><p>Your new balance is <strong>{{newBalanceDollars}}</strong>.</p>"), ft("Thank you for supporting {{brandName}}!")),
|
|
153
153
|
textBody: `Crypto Payment Confirmed: {{amountDollars}} Credits Added
|
|
154
154
|
|
|
155
155
|
Your crypto payment has been confirmed. {{amountDollars}} in credits has been added.
|
|
@@ -161,27 +161,27 @@ Your new balance is {{newBalanceDollars}}.${CR}`,
|
|
|
161
161
|
name: "admin-suspended",
|
|
162
162
|
description: "Sent when an admin manually suspends an account",
|
|
163
163
|
subject: "Your account has been suspended",
|
|
164
|
-
htmlBody: html("Account Suspended", hd("Your Account Has Been Suspended"), p("<p>Your
|
|
164
|
+
htmlBody: html("Account Suspended", hd("Your Account Has Been Suspended"), p("<p>Your {{brandName}} account has been suspended by an administrator.</p><p><strong>Reason:</strong> {{reason}}</p><p>If you believe this is an error, please contact {{supportEmail}}.</p>"), ft("Contact {{supportEmail}} if you have questions.")),
|
|
165
165
|
textBody: `Your Account Has Been Suspended
|
|
166
166
|
|
|
167
167
|
Reason: {{reason}}
|
|
168
168
|
|
|
169
|
-
If you believe this is an error, please contact
|
|
169
|
+
If you believe this is an error, please contact {{supportEmail}}.${CR}`,
|
|
170
170
|
},
|
|
171
171
|
{
|
|
172
172
|
name: "admin-reactivated",
|
|
173
173
|
description: "Sent when an admin reactivates a suspended account",
|
|
174
174
|
subject: "Your account has been reactivated",
|
|
175
|
-
htmlBody: html("Account Reactivated", hd("Your Account Has Been Reactivated"), p("<p>Your
|
|
175
|
+
htmlBody: html("Account Reactivated", hd("Your Account Has Been Reactivated"), p("<p>Your {{brandName}} account has been reactivated. You now have full access to all services.</p><p>Your agents and channels are ready to use.</p>"), ft("Welcome back!")),
|
|
176
176
|
textBody: `Your Account Has Been Reactivated
|
|
177
177
|
|
|
178
|
-
Your
|
|
178
|
+
Your {{brandName}} account has been reactivated. You now have full access to all services.${CR}`,
|
|
179
179
|
},
|
|
180
180
|
{
|
|
181
181
|
name: "credits-granted",
|
|
182
182
|
description: "Sent when credits are manually granted to a tenant",
|
|
183
183
|
subject: "You received {{amountDollars}} in credits",
|
|
184
|
-
htmlBody: html("Credits Granted", hd("You Received {{amountDollars}} in Credits"), p("<p><strong>{{amountDollars}}</strong> in credits has been added to your
|
|
184
|
+
htmlBody: html("Credits Granted", hd("You Received {{amountDollars}} in Credits"), p("<p><strong>{{amountDollars}}</strong> in credits has been added to your {{brandName}} account.</p>{{#if reason}}<p><strong>Note:</strong> {{reason}}</p>{{/if}}"), ft("Thank you for using {{brandName}}!")),
|
|
185
185
|
textBody: `You Received {{amountDollars}} in Credits
|
|
186
186
|
|
|
187
187
|
{{amountDollars}} has been added to your account.{{#if reason}}
|
|
@@ -192,7 +192,7 @@ Note: {{reason}}{{/if}}${CR}`,
|
|
|
192
192
|
name: "role-changed",
|
|
193
193
|
description: "Sent when a user role is changed",
|
|
194
194
|
subject: "Your role has been updated",
|
|
195
|
-
htmlBody: html("Role Changed", hd("Your Role Has Been Updated"), p("<p>Your role on the
|
|
195
|
+
htmlBody: html("Role Changed", hd("Your Role Has Been Updated"), p("<p>Your role on the {{brandName}} platform has been updated to <strong>{{newRole}}</strong>.</p><p>Your new permissions are now active.</p>"), ft("If you did not expect this change, contact {{supportEmail}}.")),
|
|
196
196
|
textBody: `Your Role Has Been Updated
|
|
197
197
|
|
|
198
198
|
Your role has been updated to {{newRole}}.${CR}`,
|
|
@@ -201,7 +201,7 @@ Your role has been updated to {{newRole}}.${CR}`,
|
|
|
201
201
|
name: "team-invite",
|
|
202
202
|
description: "Sent when a user is invited to join a tenant",
|
|
203
203
|
subject: "You've been invited to join {{tenantName}}",
|
|
204
|
-
htmlBody: html("Team Invite", hd("You've Been Invited to Join {{tenantName}}"), p("<p>You've been invited to join <strong>{{tenantName}}</strong> on the
|
|
204
|
+
htmlBody: html("Team Invite", hd("You've Been Invited to Join {{tenantName}}"), p("<p>You've been invited to join <strong>{{tenantName}}</strong> on the {{brandName}} platform.</p><p>Click below to accept the invitation.</p>"), `{{#if inviteUrl}}${btn("{{inviteUrl}}", "Accept Invitation")}{{/if}}`, ft("If you did not expect this invitation, you can ignore this email.")),
|
|
205
205
|
textBody: `You've Been Invited to Join {{tenantName}}
|
|
206
206
|
{{#if inviteUrl}}
|
|
207
207
|
Accept: {{inviteUrl}}
|
|
@@ -211,9 +211,9 @@ Accept: {{inviteUrl}}
|
|
|
211
211
|
{
|
|
212
212
|
name: "agent-created",
|
|
213
213
|
description: "Sent when a new agent is created",
|
|
214
|
-
subject: "Your
|
|
215
|
-
htmlBody: html("Agent Created", hd("Your
|
|
216
|
-
textBody: `Your
|
|
214
|
+
subject: "Your {{brandName}} {{agentName}} is ready",
|
|
215
|
+
htmlBody: html("Agent Created", hd("Your {{brandName}} {{agentName}} Is Ready"), p("<p>Your new agent <strong>{{agentName}}</strong> has been created and is ready to use.</p><p>Connect it to a channel to start receiving and sending messages.</p>"), ft("Happy building!")),
|
|
216
|
+
textBody: `Your {{brandName}} {{agentName}} Is Ready
|
|
217
217
|
|
|
218
218
|
Your new agent has been created and is ready to use.${CR}`,
|
|
219
219
|
},
|
|
@@ -241,7 +241,7 @@ Reason: {{reason}}
|
|
|
241
241
|
name: "agent-suspended",
|
|
242
242
|
description: "Sent when an agent is paused/suspended",
|
|
243
243
|
subject: "{{agentName}} has been paused",
|
|
244
|
-
htmlBody: html("Agent Paused", hd("{{agentName}} Has Been Paused"), p("<p>Your agent <strong>{{agentName}}</strong> has been paused.</p>{{#if reason}}<p><strong>Reason:</strong> {{reason}}</p>{{/if}}"), ft("Contact
|
|
244
|
+
htmlBody: html("Agent Paused", hd("{{agentName}} Has Been Paused"), p("<p>Your agent <strong>{{agentName}}</strong> has been paused.</p>{{#if reason}}<p><strong>Reason:</strong> {{reason}}</p>{{/if}}"), ft("Contact {{supportEmail}} if you have questions.")),
|
|
245
245
|
textBody: `{{agentName}} Has Been Paused
|
|
246
246
|
{{#if reason}}
|
|
247
247
|
Reason: {{reason}}
|
|
@@ -251,13 +251,13 @@ Reason: {{reason}}
|
|
|
251
251
|
{
|
|
252
252
|
name: "account-deletion-requested",
|
|
253
253
|
description: "Sent when a user requests account deletion",
|
|
254
|
-
subject: "Your
|
|
255
|
-
htmlBody: html("Account Deletion Requested", hd("Account Deletion Requested"), p("<p>Hi <strong>{{email}}</strong>,</p><p>We've received your request to delete your
|
|
254
|
+
subject: "Your {{brandName}} account deletion request",
|
|
255
|
+
htmlBody: html("Account Deletion Requested", hd("Account Deletion Requested"), p("<p>Hi <strong>{{email}}</strong>,</p><p>We've received your request to delete your {{brandName}} account and all associated data.</p><p>Your account will be permanently deleted on <strong>{{deleteAfterDate}}</strong>. Until then, you can cancel this request and keep your account.</p><p>After that date, all your data will be permanently and irreversibly removed, including bots, conversation history, credit records, and plugin configurations.</p>"), `{{#if cancelUrl}}${btn("{{cancelUrl}}", "Cancel Deletion", "#22c55e")}{{/if}}`, ft("If you did not request this, please contact {{supportEmail}} immediately.")),
|
|
256
256
|
textBody: `Account Deletion Requested
|
|
257
257
|
|
|
258
258
|
Hi {{email}},
|
|
259
259
|
|
|
260
|
-
We've received your request to delete your
|
|
260
|
+
We've received your request to delete your {{brandName}} account and all associated data.
|
|
261
261
|
|
|
262
262
|
Your account will be permanently deleted on {{deleteAfterDate}}. Until then, you can cancel this request.
|
|
263
263
|
|
|
@@ -265,13 +265,13 @@ After that date, all your data will be permanently and irreversibly removed.
|
|
|
265
265
|
{{#if cancelUrl}}
|
|
266
266
|
Cancel deletion: {{cancelUrl}}
|
|
267
267
|
{{/if}}
|
|
268
|
-
If you did not request this, please contact
|
|
268
|
+
If you did not request this, please contact {{supportEmail}} immediately.${CR}`,
|
|
269
269
|
},
|
|
270
270
|
{
|
|
271
271
|
name: "account-deletion-cancelled",
|
|
272
272
|
description: "Sent when account deletion is cancelled",
|
|
273
|
-
subject: "Your
|
|
274
|
-
htmlBody: html("Account Deletion Cancelled", hd("Account Deletion Cancelled"), p("<p>Hi <strong>{{email}}</strong>,</p><p>Your account deletion request has been cancelled. Your account and all data remain intact.</p><p>No further action is needed.</p>"), ft("If you didn't cancel this, please contact
|
|
273
|
+
subject: "Your {{brandName}} account deletion has been cancelled",
|
|
274
|
+
htmlBody: html("Account Deletion Cancelled", hd("Account Deletion Cancelled"), p("<p>Hi <strong>{{email}}</strong>,</p><p>Your account deletion request has been cancelled. Your account and all data remain intact.</p><p>No further action is needed.</p>"), ft("If you didn't cancel this, please contact {{supportEmail}}.")),
|
|
275
275
|
textBody: `Account Deletion Cancelled
|
|
276
276
|
|
|
277
277
|
Hi {{email}},
|
|
@@ -283,24 +283,24 @@ No further action is needed.${CR}`,
|
|
|
283
283
|
{
|
|
284
284
|
name: "account-deletion-completed",
|
|
285
285
|
description: "Sent after account is permanently deleted",
|
|
286
|
-
subject: "Your
|
|
287
|
-
htmlBody: html("Account Deleted", hd("Your Account Has Been Deleted"), p("<p>Hi <strong>{{email}}</strong>,</p><p>Your
|
|
286
|
+
subject: "Your {{brandName}} account has been deleted",
|
|
287
|
+
htmlBody: html("Account Deleted", hd("Your Account Has Been Deleted"), p("<p>Hi <strong>{{email}}</strong>,</p><p>Your {{brandName}} account and all associated data have been permanently deleted as requested.</p><p>This includes all bots, conversation history, credit records, billing data, and plugin configurations.</p><p>If you'd like to use {{brandName}} again in the future, you're welcome to create a new account.</p>"), ft("Thank you for using {{brandName}}. We're sorry to see you go.")),
|
|
288
288
|
textBody: `Your Account Has Been Deleted
|
|
289
289
|
|
|
290
290
|
Hi {{email}},
|
|
291
291
|
|
|
292
|
-
Your
|
|
292
|
+
Your {{brandName}} account and all associated data have been permanently deleted as requested.
|
|
293
293
|
|
|
294
294
|
This includes all bots, conversation history, credit records, billing data, and plugin configurations.
|
|
295
295
|
|
|
296
|
-
If you'd like to use
|
|
296
|
+
If you'd like to use {{brandName}} again in the future, you're welcome to create a new account.${CR}`,
|
|
297
297
|
},
|
|
298
298
|
// -- Dividend & Affiliate --------------------------------------------------
|
|
299
299
|
{
|
|
300
300
|
name: "dividend-weekly-digest",
|
|
301
301
|
description: "Weekly summary of dividend payouts",
|
|
302
|
-
subject: "
|
|
303
|
-
htmlBody: html("Weekly Dividend Digest", hd("
|
|
302
|
+
subject: "{{brandName}} paid you {{weeklyTotalDollars}} this week",
|
|
303
|
+
htmlBody: html("Weekly Dividend Digest", hd("{{brandName}} Paid You {{weeklyTotalDollars}} This Week"), p(`<p>Here's your weekly dividend summary for <strong>{{weekStartDate}} \u2013 {{weekEndDate}}</strong>.</p>` +
|
|
304
304
|
`<table style="width: 100%; border-collapse: collapse; margin: 16px 0;">` +
|
|
305
305
|
`<tr><td style="padding: 8px 0; color: #4a5568;">This week's dividends</td><td style="padding: 8px 0; text-align: right; font-weight: 600; color: #1a1a1a;">{{weeklyTotalDollars}}</td></tr>` +
|
|
306
306
|
`<tr><td style="padding: 8px 0; color: #4a5568;">Days with distributions</td><td style="padding: 8px 0; text-align: right; font-weight: 600; color: #1a1a1a;">{{distributionCount}} of 7</td></tr>` +
|
|
@@ -309,7 +309,7 @@ If you'd like to use WOPR again in the future, you're welcome to create a new ac
|
|
|
309
309
|
`<tr style="border-top: 2px solid #e2e8f0;"><td style="padding: 12px 0; color: #4a5568; font-weight: 600;">Lifetime total</td><td style="padding: 12px 0; text-align: right; font-weight: 700; color: #1a1a1a; font-size: 18px;">{{lifetimeTotalDollars}}</td></tr>` +
|
|
310
310
|
`</table>` +
|
|
311
311
|
`{{#if nextDividendDate}}<p style="color: #718096; font-size: 14px;">Next dividend: <strong>{{nextDividendDate}}</strong></p>{{/if}}`), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "View Your Credits")}{{/if}}`, ft("Community dividends are distributed daily from platform revenue. Keep your credits active to stay eligible."), '{{#if unsubscribeUrl}}<tr><td style="padding: 0 40px 20px 40px; text-align: center; color: #a0aec0; font-size: 12px;"><a href="{{unsubscribeUrl}}" style="color: #a0aec0; text-decoration: underline;">Unsubscribe from dividend digests</a></td></tr>{{/if}}'),
|
|
312
|
-
textBody: `
|
|
312
|
+
textBody: `{{brandName}} Paid You {{weeklyTotalDollars}} This Week
|
|
313
313
|
|
|
314
314
|
Weekly summary for {{weekStartDate}} \u2013 {{weekEndDate}}:
|
|
315
315
|
|
|
@@ -329,7 +329,7 @@ Unsubscribe: {{unsubscribeUrl}}{{/if}}${CR}`,
|
|
|
329
329
|
name: "affiliate-credit-match",
|
|
330
330
|
description: "Sent when affiliate earns credits from a referral purchase",
|
|
331
331
|
subject: "You earned {{amountDollars}} in affiliate credits!",
|
|
332
|
-
htmlBody: html("Affiliate Credits Earned", hd("You Earned Affiliate Credits!"), p("<p>Great news! A user you referred just made their first purchase, and you've been credited <strong>{{amountDollars}}</strong>.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "View Credit Balance")}{{/if}}`, ft("Thank you for spreading the word about
|
|
332
|
+
htmlBody: html("Affiliate Credits Earned", hd("You Earned Affiliate Credits!"), p("<p>Great news! A user you referred just made their first purchase, and you've been credited <strong>{{amountDollars}}</strong>.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "View Credit Balance")}{{/if}}`, ft("Thank you for spreading the word about {{brandName}}!")),
|
|
333
333
|
textBody: `You Earned Affiliate Credits!
|
|
334
334
|
|
|
335
335
|
A user you referred just made their first purchase, and you've been credited {{amountDollars}}.
|
|
@@ -353,16 +353,16 @@ Review spending: {{creditsUrl}}
|
|
|
353
353
|
name: "custom",
|
|
354
354
|
description: "Admin custom email with arbitrary body text",
|
|
355
355
|
subject: "{{subject}}",
|
|
356
|
-
htmlBody: html("{{subject}}", hd("Message from
|
|
356
|
+
htmlBody: html("{{subject}}", hd("Message from {{brandName}}"), p("<p>{{{bodyTextHtml}}}</p>"), ft("This is an administrative message from {{brandName}}.")),
|
|
357
357
|
textBody: `{{bodyText}}${CR}`,
|
|
358
358
|
},
|
|
359
359
|
// -- Passthrough templates (billing/auth) ----------------------------------
|
|
360
360
|
{
|
|
361
361
|
name: "low-balance",
|
|
362
362
|
description: "Sent when credit balance drops below threshold",
|
|
363
|
-
subject: "Your
|
|
364
|
-
htmlBody: html("Low Balance", hd("Your
|
|
365
|
-
textBody: `Your
|
|
363
|
+
subject: "Your {{brandName}} credits are running low",
|
|
364
|
+
htmlBody: html("Low Balance", hd("Your {{brandName}} Credits Are Running Low"), p("<p>Your balance is <strong>{{balanceDollars}}</strong>. Top up to keep your agents running.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Buy Credits")}{{/if}}`, ft("This is an automated billing notification.")),
|
|
365
|
+
textBody: `Your {{brandName}} Credits Are Running Low
|
|
366
366
|
|
|
367
367
|
Balance: {{balanceDollars}}
|
|
368
368
|
{{#if creditsUrl}}
|
|
@@ -373,7 +373,7 @@ Buy credits: {{creditsUrl}}
|
|
|
373
373
|
name: "credit-purchase-receipt",
|
|
374
374
|
description: "Sent after a credit purchase is completed",
|
|
375
375
|
subject: "Credits added to your account",
|
|
376
|
-
htmlBody: html("Credits Added", hd("Credits Added to Your Account"), p("<p><strong>{{amountDollars}}</strong> in credits has been added.</p>{{#if newBalanceDollars}}<p>New balance: <strong>{{newBalanceDollars}}</strong></p>{{/if}}"), ft("Thank you for supporting
|
|
376
|
+
htmlBody: html("Credits Added", hd("Credits Added to Your Account"), p("<p><strong>{{amountDollars}}</strong> in credits has been added.</p>{{#if newBalanceDollars}}<p>New balance: <strong>{{newBalanceDollars}}</strong></p>{{/if}}"), ft("Thank you for supporting {{brandName}}!")),
|
|
377
377
|
textBody: `Credits Added
|
|
378
378
|
|
|
379
379
|
{{amountDollars}} added.${CR}`,
|
|
@@ -381,16 +381,16 @@ Buy credits: {{creditsUrl}}
|
|
|
381
381
|
{
|
|
382
382
|
name: "welcome",
|
|
383
383
|
description: "Sent to new users after registration",
|
|
384
|
-
subject: "Welcome to
|
|
385
|
-
htmlBody: html("Welcome", hd("Welcome to
|
|
386
|
-
textBody: `Welcome to
|
|
384
|
+
subject: "Welcome to {{brandName}}",
|
|
385
|
+
htmlBody: html("Welcome", hd("Welcome to {{brandName}}!"), p("<p>Your account is now active. Start building!</p>"), ft("Happy building!")),
|
|
386
|
+
textBody: `Welcome to {{brandName}}!
|
|
387
387
|
|
|
388
388
|
Your account is now active.${CR}`,
|
|
389
389
|
},
|
|
390
390
|
{
|
|
391
391
|
name: "password-reset",
|
|
392
392
|
description: "Sent when a user requests a password reset",
|
|
393
|
-
subject: "Reset your
|
|
393
|
+
subject: "Reset your {{brandName}} password",
|
|
394
394
|
htmlBody: html("Reset Password", hd("Reset Your Password"), p("<p>Click below to reset your password.</p>"), `{{#if resetUrl}}${btn("{{resetUrl}}", "Reset Password")}{{/if}}`, ft("If you did not request this, ignore this email.")),
|
|
395
395
|
textBody: `Reset Your Password
|
|
396
396
|
{{#if resetUrl}}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
151
|
+
this.enqueue(tenantId, "admin-suspended", { email, reason });
|
|
146
152
|
}
|
|
147
153
|
notifyAdminReactivated(tenantId, email) {
|
|
148
|
-
this.
|
|
154
|
+
this.enqueue(tenantId, "admin-reactivated", { email });
|
|
149
155
|
}
|
|
150
156
|
notifyCreditsGranted(tenantId, email, amountDollars, reason) {
|
|
151
|
-
this.
|
|
157
|
+
this.enqueue(tenantId, "credits-granted", { email, amountDollars, reason });
|
|
152
158
|
}
|
|
153
159
|
notifyRoleChanged(tenantId, email, newRole) {
|
|
154
|
-
this.
|
|
160
|
+
this.enqueue(tenantId, "role-changed", { email, newRole });
|
|
155
161
|
}
|
|
156
162
|
notifyTeamInvite(tenantId, email, tenantName, inviteUrl) {
|
|
157
|
-
this.
|
|
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.
|
|
169
|
+
this.enqueue(tenantId, "agent-created", { email, agentName });
|
|
164
170
|
}
|
|
165
171
|
notifyChannelConnected(tenantId, email, channelName, agentName) {
|
|
166
|
-
this.
|
|
172
|
+
this.enqueue(tenantId, "channel-connected", { email, channelName, agentName });
|
|
167
173
|
}
|
|
168
174
|
notifyChannelDisconnected(tenantId, email, channelName, agentName, reason) {
|
|
169
|
-
this.
|
|
175
|
+
this.enqueue(tenantId, "channel-disconnected", { email, channelName, agentName, reason });
|
|
170
176
|
}
|
|
171
177
|
notifyAgentSuspended(tenantId, email, agentName, reason) {
|
|
172
|
-
this.
|
|
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.
|
|
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.
|
|
191
|
+
this.enqueue(tenantId, "account-deletion-cancelled", { email });
|
|
186
192
|
}
|
|
187
193
|
notifyAccountDeletionCompleted(tenantId, email) {
|
|
188
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
222
|
+
this.enqueue(tenantId, "custom", { email, subject, bodyText });
|
|
217
223
|
}
|
|
218
224
|
}
|