@wopr-network/platform-core 1.27.1 → 1.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/billing/crypto/charge-store.d.ts +10 -0
  2. package/dist/billing/crypto/charge-store.js +9 -1
  3. package/dist/billing/crypto/payment-method-store.d.ts +2 -0
  4. package/dist/billing/crypto/payment-method-store.js +6 -0
  5. package/dist/db/schema/crypto.d.ts +34 -0
  6. package/dist/db/schema/crypto.js +3 -1
  7. package/dist/db/schema/index.d.ts +1 -0
  8. package/dist/db/schema/index.js +1 -0
  9. package/dist/db/schema/notification-preferences.d.ts +17 -0
  10. package/dist/db/schema/notification-preferences.js +1 -0
  11. package/dist/db/schema/notification-templates.d.ts +165 -0
  12. package/dist/db/schema/notification-templates.js +18 -0
  13. package/dist/email/default-templates.d.ts +15 -0
  14. package/dist/email/default-templates.js +443 -0
  15. package/dist/email/drizzle-notification-template-repository.d.ts +21 -0
  16. package/dist/email/drizzle-notification-template-repository.js +90 -0
  17. package/dist/email/handlebars-renderer.d.ts +17 -0
  18. package/dist/email/handlebars-renderer.js +56 -0
  19. package/dist/email/index.d.ts +4 -0
  20. package/dist/email/index.js +3 -0
  21. package/dist/email/notification-preferences-store.js +5 -0
  22. package/dist/email/notification-preferences-store.test.js +1 -0
  23. package/dist/email/notification-repository-types.d.ts +30 -0
  24. package/dist/email/notification-service.d.ts +2 -0
  25. package/dist/email/notification-service.js +22 -0
  26. package/dist/email/notification-template-repository.d.ts +7 -0
  27. package/dist/email/notification-template-repository.js +7 -0
  28. package/dist/email/notification-templates.d.ts +1 -1
  29. package/dist/email/notification-templates.js +52 -0
  30. package/dist/trpc/index.d.ts +1 -0
  31. package/dist/trpc/index.js +1 -0
  32. package/dist/trpc/notification-template-router.d.ts +59 -0
  33. package/dist/trpc/notification-template-router.js +81 -0
  34. package/drizzle/migrations/0010_oracle_address.sql +11 -0
  35. package/drizzle/migrations/0011_notification_templates.sql +14 -0
  36. package/drizzle/migrations/meta/_journal.json +14 -0
  37. package/package.json +2 -1
  38. package/src/billing/crypto/charge-store.ts +14 -1
  39. package/src/billing/crypto/payment-method-store.ts +8 -0
  40. package/src/db/schema/crypto.ts +3 -1
  41. package/src/db/schema/index.ts +1 -0
  42. package/src/db/schema/notification-preferences.ts +1 -0
  43. package/src/db/schema/notification-templates.ts +19 -0
  44. package/src/email/default-templates.ts +680 -0
  45. package/src/email/drizzle-notification-template-repository.ts +108 -0
  46. package/src/email/handlebars-renderer.ts +64 -0
  47. package/src/email/index.ts +4 -0
  48. package/src/email/notification-preferences-store.test.ts +1 -0
  49. package/src/email/notification-preferences-store.ts +4 -0
  50. package/src/email/notification-repository-types.ts +41 -0
  51. package/src/email/notification-service.ts +31 -0
  52. package/src/email/notification-template-repository.ts +8 -0
  53. package/src/email/notification-templates.ts +61 -0
  54. package/src/trpc/index.ts +1 -0
  55. package/src/trpc/notification-template-router.ts +91 -0
@@ -0,0 +1,680 @@
1
+ /**
2
+ * Default email templates — Handlebars versions of every notification template.
3
+ *
4
+ * These are seeded into the notification_templates table on first run.
5
+ * Admin edits are preserved (seed uses INSERT OR IGNORE).
6
+ */
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Shared HTML wrapper (table-based, 600px centered, white card on gray bg)
10
+ // ---------------------------------------------------------------------------
11
+
12
+ const YEAR = "{{currentYear}}";
13
+
14
+ function layoutOpen(title: string): string {
15
+ return `<!DOCTYPE html>
16
+ <html>
17
+ <head>
18
+ <meta charset="utf-8">
19
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
20
+ <title>${title}</title>
21
+ </head>
22
+ <body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f4f4f4;">
23
+ <table role="presentation" style="width: 100%; border-collapse: collapse;">
24
+ <tr>
25
+ <td style="padding: 40px 0; text-align: center;">
26
+ <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);">`;
27
+ }
28
+
29
+ const LAYOUT_CLOSE = ` </table>
30
+ <p style="margin-top: 20px; color: #a0aec0; font-size: 12px;">&copy; ${YEAR} WOPR Network. All rights reserved.</p>
31
+ </td>
32
+ </tr>
33
+ </table>
34
+ </body>
35
+ </html>`;
36
+
37
+ function hd(text: string): string {
38
+ return `<tr>
39
+ <td style="padding: 40px 40px 20px 40px; text-align: center;">
40
+ <h1 style="margin: 0; font-size: 24px; font-weight: 600; color: #1a1a1a;">${text}</h1>
41
+ </td>
42
+ </tr>`;
43
+ }
44
+
45
+ function p(html: string): string {
46
+ return `<tr>
47
+ <td style="padding: 0 40px 20px 40px; color: #4a5568; font-size: 16px; line-height: 24px;">
48
+ ${html}
49
+ </td>
50
+ </tr>`;
51
+ }
52
+
53
+ function btn(urlExpr: string, label: string, color = "#2563eb"): string {
54
+ return `<tr>
55
+ <td style="padding: 0 40px 30px 40px; text-align: center;">
56
+ <a href="${urlExpr}" style="display: inline-block; padding: 12px 32px; background-color: ${color}; color: #ffffff; text-decoration: none; font-weight: 600; border-radius: 6px; font-size: 16px;">${label}</a>
57
+ </td>
58
+ </tr>`;
59
+ }
60
+
61
+ function ft(text: string): string {
62
+ return `<tr>
63
+ <td style="padding: 0 40px 40px 40px; color: #718096; font-size: 14px; line-height: 20px; border-top: 1px solid #e2e8f0;">
64
+ <p style="margin-top: 20px;">${text}</p>
65
+ </td>
66
+ </tr>`;
67
+ }
68
+
69
+ function html(title: string, ...rows: string[]): string {
70
+ return `${layoutOpen(title)}\n${rows.join("\n")}\n${LAYOUT_CLOSE}`;
71
+ }
72
+
73
+ const CR = `\n\n(c) ${YEAR} WOPR Network. All rights reserved.`;
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Template definitions
77
+ // ---------------------------------------------------------------------------
78
+
79
+ interface DefaultTemplate {
80
+ name: string;
81
+ description: string;
82
+ subject: string;
83
+ htmlBody: string;
84
+ textBody: string;
85
+ }
86
+
87
+ export const DEFAULT_TEMPLATES: DefaultTemplate[] = [
88
+ // -- Credits & Billing ---------------------------------------------------
89
+ {
90
+ name: "credits-depleted",
91
+ description: "Sent when tenant credit balance reaches zero",
92
+ subject: "Your WOPR credits are depleted \u2014 capabilities paused",
93
+ htmlBody: html(
94
+ "Credits Depleted",
95
+ hd("Your WOPR Credits Are Depleted"),
96
+ p(
97
+ "<p>Your WOPR credit balance has reached $0. All agent capabilities have been paused.</p><p>Add credits now to resume service immediately.</p>",
98
+ ),
99
+ `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Add Credits")}{{/if}}`,
100
+ ft("Your data is preserved. Add credits to reactivate."),
101
+ ),
102
+ textBody: `Your WOPR Credits Are Depleted
103
+
104
+ Your WOPR credit balance has reached $0. All agent capabilities have been paused.
105
+
106
+ Add credits now to resume service immediately.
107
+ {{#if creditsUrl}}
108
+ Add credits: {{creditsUrl}}
109
+ {{/if}}${CR}`,
110
+ },
111
+ {
112
+ name: "grace-period-start",
113
+ description: "Sent when a tenant enters the grace period after failed billing",
114
+ subject: "Action needed: top up to keep your WOPRs running",
115
+ htmlBody: html(
116
+ "Grace Period Started",
117
+ hd("Action Needed: Top Up to Keep Your WOPRs Running"),
118
+ p(
119
+ "<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>",
120
+ ),
121
+ `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Add Credits Now")}{{/if}}`,
122
+ ft("This is a critical notification about your account status."),
123
+ ),
124
+ textBody: `Action Needed: Top Up to Keep Your WOPRs Running
125
+
126
+ Your current balance is {{balanceDollars}} and the monthly deduction could not be processed.
127
+
128
+ You have a {{graceDays}}-day grace period to add credits before your account is suspended.
129
+ {{#if creditsUrl}}
130
+ Add credits: {{creditsUrl}}
131
+ {{/if}}${CR}`,
132
+ },
133
+ {
134
+ name: "grace-period-warning",
135
+ description: "Sent one day before grace period expires",
136
+ subject: "Last chance: your WOPRs will be suspended tomorrow",
137
+ htmlBody: html(
138
+ "Grace Period Warning",
139
+ hd("Last Chance: Your WOPRs Will Be Suspended Tomorrow"),
140
+ p(
141
+ "<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>",
142
+ ),
143
+ `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Add Credits Now", "#dc2626")}{{/if}}`,
144
+ ft("This is a critical notification about your account status."),
145
+ ),
146
+ textBody: `Last Chance: Your WOPRs Will Be Suspended Tomorrow
147
+
148
+ Your grace period expires tomorrow. If you do not add credits, your account will be suspended.
149
+ {{#if creditsUrl}}
150
+ Add credits: {{creditsUrl}}
151
+ {{/if}}${CR}`,
152
+ },
153
+ {
154
+ name: "auto-suspended",
155
+ description: "Sent when account is automatically suspended",
156
+ subject: "Your account has been suspended",
157
+ htmlBody: html(
158
+ "Account Suspended",
159
+ hd("Your Account Has Been Suspended"),
160
+ p(
161
+ "<p>Your WOPR account has been automatically suspended.</p><p><strong>Reason:</strong> {{reason}}</p><p>Add credits to reactivate your account immediately.</p>",
162
+ ),
163
+ `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Add Credits to Reactivate")}{{/if}}`,
164
+ ft("Your data is preserved for 30 days."),
165
+ ),
166
+ textBody: `Your Account Has Been Suspended
167
+
168
+ Reason: {{reason}}
169
+
170
+ Add credits to reactivate your account immediately.
171
+ {{#if creditsUrl}}
172
+ Add credits: {{creditsUrl}}
173
+ {{/if}}${CR}`,
174
+ },
175
+ {
176
+ name: "auto-topup-success",
177
+ description: "Sent after a successful auto top-up charge",
178
+ subject: "Auto top-up: {{amountDollars}} credits added",
179
+ htmlBody: html(
180
+ "Auto Top-Up Successful",
181
+ hd("Auto Top-Up: {{amountDollars}} Credits Added"),
182
+ p(
183
+ "<p>Your auto top-up was successful. <strong>{{amountDollars}}</strong> in credits has been added.</p><p>Your new balance is <strong>{{newBalanceDollars}}</strong>.</p>",
184
+ ),
185
+ `{{#if creditsUrl}}${btn("{{creditsUrl}}", "View Credits")}{{/if}}`,
186
+ ft("Auto top-up keeps your agents running without interruption."),
187
+ ),
188
+ textBody: `Auto Top-Up: {{amountDollars}} Credits Added
189
+
190
+ Your auto top-up was successful. {{amountDollars}} in credits has been added.
191
+
192
+ Your new balance is {{newBalanceDollars}}.
193
+ {{#if creditsUrl}}
194
+ View credits: {{creditsUrl}}
195
+ {{/if}}${CR}`,
196
+ },
197
+ {
198
+ name: "auto-topup-failed",
199
+ description: "Sent when auto top-up charge fails",
200
+ subject: "Auto top-up failed \u2014 update your payment method",
201
+ htmlBody: html(
202
+ "Auto Top-Up Failed",
203
+ hd("Auto Top-Up Failed"),
204
+ p(
205
+ "<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>",
206
+ ),
207
+ `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Add Credits")}{{/if}}`,
208
+ ft("If you need help, contact support@wopr.bot."),
209
+ ),
210
+ textBody: `Auto Top-Up Failed
211
+
212
+ Your auto top-up failed. We were unable to charge your payment method.
213
+
214
+ Please update your payment method or add credits manually to avoid service interruption.
215
+ {{#if creditsUrl}}
216
+ Add credits: {{creditsUrl}}
217
+ {{/if}}${CR}`,
218
+ },
219
+ {
220
+ name: "crypto-payment-confirmed",
221
+ description: "Sent when a crypto payment is confirmed on-chain",
222
+ subject: "Crypto payment confirmed: {{amountDollars}} credits added",
223
+ htmlBody: html(
224
+ "Crypto Payment Confirmed",
225
+ hd("Crypto Payment Confirmed: {{amountDollars}} Credits Added"),
226
+ p(
227
+ "<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>",
228
+ ),
229
+ ft("Thank you for supporting WOPR!"),
230
+ ),
231
+ textBody: `Crypto Payment Confirmed: {{amountDollars}} Credits Added
232
+
233
+ Your crypto payment has been confirmed. {{amountDollars}} in credits has been added.
234
+
235
+ Your new balance is {{newBalanceDollars}}.${CR}`,
236
+ },
237
+ // -- Account Administration -----------------------------------------------
238
+ {
239
+ name: "admin-suspended",
240
+ description: "Sent when an admin manually suspends an account",
241
+ subject: "Your account has been suspended",
242
+ htmlBody: html(
243
+ "Account Suspended",
244
+ hd("Your Account Has Been Suspended"),
245
+ p(
246
+ "<p>Your WOPR account has been suspended by an administrator.</p><p><strong>Reason:</strong> {{reason}}</p><p>If you believe this is an error, please contact support@wopr.bot.</p>",
247
+ ),
248
+ ft("Contact support@wopr.bot if you have questions."),
249
+ ),
250
+ textBody: `Your Account Has Been Suspended
251
+
252
+ Reason: {{reason}}
253
+
254
+ If you believe this is an error, please contact support@wopr.bot.${CR}`,
255
+ },
256
+ {
257
+ name: "admin-reactivated",
258
+ description: "Sent when an admin reactivates a suspended account",
259
+ subject: "Your account has been reactivated",
260
+ htmlBody: html(
261
+ "Account Reactivated",
262
+ hd("Your Account Has Been Reactivated"),
263
+ p(
264
+ "<p>Your WOPR account has been reactivated. You now have full access to all services.</p><p>Your agents and channels are ready to use.</p>",
265
+ ),
266
+ ft("Welcome back!"),
267
+ ),
268
+ textBody: `Your Account Has Been Reactivated
269
+
270
+ Your WOPR account has been reactivated. You now have full access to all services.${CR}`,
271
+ },
272
+ {
273
+ name: "credits-granted",
274
+ description: "Sent when credits are manually granted to a tenant",
275
+ subject: "You received {{amountDollars}} in credits",
276
+ htmlBody: html(
277
+ "Credits Granted",
278
+ hd("You Received {{amountDollars}} in Credits"),
279
+ p(
280
+ "<p><strong>{{amountDollars}}</strong> in credits has been added to your WOPR account.</p>{{#if reason}}<p><strong>Note:</strong> {{reason}}</p>{{/if}}",
281
+ ),
282
+ ft("Thank you for using WOPR!"),
283
+ ),
284
+ textBody: `You Received {{amountDollars}} in Credits
285
+
286
+ {{amountDollars}} has been added to your account.{{#if reason}}
287
+
288
+ Note: {{reason}}{{/if}}${CR}`,
289
+ },
290
+ {
291
+ name: "role-changed",
292
+ description: "Sent when a user role is changed",
293
+ subject: "Your role has been updated",
294
+ htmlBody: html(
295
+ "Role Changed",
296
+ hd("Your Role Has Been Updated"),
297
+ p(
298
+ "<p>Your role on the WOPR platform has been updated to <strong>{{newRole}}</strong>.</p><p>Your new permissions are now active.</p>",
299
+ ),
300
+ ft("If you did not expect this change, contact support@wopr.bot."),
301
+ ),
302
+ textBody: `Your Role Has Been Updated
303
+
304
+ Your role has been updated to {{newRole}}.${CR}`,
305
+ },
306
+ {
307
+ name: "team-invite",
308
+ description: "Sent when a user is invited to join a tenant",
309
+ subject: "You've been invited to join {{tenantName}}",
310
+ htmlBody: html(
311
+ "Team Invite",
312
+ hd("You've Been Invited to Join {{tenantName}}"),
313
+ p(
314
+ "<p>You've been invited to join <strong>{{tenantName}}</strong> on the WOPR platform.</p><p>Click below to accept the invitation.</p>",
315
+ ),
316
+ `{{#if inviteUrl}}${btn("{{inviteUrl}}", "Accept Invitation")}{{/if}}`,
317
+ ft("If you did not expect this invitation, you can ignore this email."),
318
+ ),
319
+ textBody: `You've Been Invited to Join {{tenantName}}
320
+ {{#if inviteUrl}}
321
+ Accept: {{inviteUrl}}
322
+ {{/if}}${CR}`,
323
+ },
324
+ // -- Agent & Channel ------------------------------------------------------
325
+ {
326
+ name: "agent-created",
327
+ description: "Sent when a new agent is created",
328
+ subject: "Your WOPR {{agentName}} is ready",
329
+ htmlBody: html(
330
+ "Agent Created",
331
+ hd("Your WOPR {{agentName}} Is Ready"),
332
+ p(
333
+ "<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>",
334
+ ),
335
+ ft("Happy building!"),
336
+ ),
337
+ textBody: `Your WOPR {{agentName}} Is Ready
338
+
339
+ Your new agent has been created and is ready to use.${CR}`,
340
+ },
341
+ {
342
+ name: "channel-connected",
343
+ description: "Sent when a channel is connected to an agent",
344
+ subject: "{{channelName}} connected to {{agentName}}",
345
+ htmlBody: html(
346
+ "Channel Connected",
347
+ hd("{{channelName}} Connected to {{agentName}}"),
348
+ p(
349
+ "<p><strong>{{channelName}}</strong> has been successfully connected to <strong>{{agentName}}</strong>.</p><p>Your agent is now active on this channel.</p>",
350
+ ),
351
+ ft("Your agent is live!"),
352
+ ),
353
+ textBody: `{{channelName}} Connected to {{agentName}}
354
+
355
+ {{channelName}} has been successfully connected to {{agentName}}.${CR}`,
356
+ },
357
+ {
358
+ name: "channel-disconnected",
359
+ description: "Sent when a channel is disconnected from an agent",
360
+ subject: "{{channelName}} disconnected from {{agentName}}",
361
+ htmlBody: html(
362
+ "Channel Disconnected",
363
+ hd("{{channelName}} Disconnected from {{agentName}}"),
364
+ p(
365
+ "<p><strong>{{channelName}}</strong> has been disconnected from <strong>{{agentName}}</strong>.</p>{{#if reason}}<p><strong>Reason:</strong> {{reason}}</p>{{/if}}<p>Reconnect the channel from your dashboard to restore service.</p>",
366
+ ),
367
+ ft("Your agent data is preserved."),
368
+ ),
369
+ textBody: `{{channelName}} Disconnected from {{agentName}}
370
+ {{#if reason}}
371
+ Reason: {{reason}}
372
+
373
+ {{/if}}Reconnect from your dashboard to restore service.${CR}`,
374
+ },
375
+ {
376
+ name: "agent-suspended",
377
+ description: "Sent when an agent is paused/suspended",
378
+ subject: "{{agentName}} has been paused",
379
+ htmlBody: html(
380
+ "Agent Paused",
381
+ hd("{{agentName}} Has Been Paused"),
382
+ p(
383
+ "<p>Your agent <strong>{{agentName}}</strong> has been paused.</p>{{#if reason}}<p><strong>Reason:</strong> {{reason}}</p>{{/if}}",
384
+ ),
385
+ ft("Contact support@wopr.bot if you have questions."),
386
+ ),
387
+ textBody: `{{agentName}} Has Been Paused
388
+ {{#if reason}}
389
+ Reason: {{reason}}
390
+ {{/if}}${CR}`,
391
+ },
392
+ // -- Account Deletion ------------------------------------------------------
393
+ {
394
+ name: "account-deletion-requested",
395
+ description: "Sent when a user requests account deletion",
396
+ subject: "Your WOPR account deletion request",
397
+ htmlBody: html(
398
+ "Account Deletion Requested",
399
+ hd("Account Deletion Requested"),
400
+ p(
401
+ "<p>Hi <strong>{{email}}</strong>,</p><p>We've received your request to delete your WOPR 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>",
402
+ ),
403
+ `{{#if cancelUrl}}${btn("{{cancelUrl}}", "Cancel Deletion", "#22c55e")}{{/if}}`,
404
+ ft("If you did not request this, please contact support@wopr.bot immediately."),
405
+ ),
406
+ textBody: `Account Deletion Requested
407
+
408
+ Hi {{email}},
409
+
410
+ We've received your request to delete your WOPR account and all associated data.
411
+
412
+ Your account will be permanently deleted on {{deleteAfterDate}}. Until then, you can cancel this request.
413
+
414
+ After that date, all your data will be permanently and irreversibly removed.
415
+ {{#if cancelUrl}}
416
+ Cancel deletion: {{cancelUrl}}
417
+ {{/if}}
418
+ If you did not request this, please contact support@wopr.bot immediately.${CR}`,
419
+ },
420
+ {
421
+ name: "account-deletion-cancelled",
422
+ description: "Sent when account deletion is cancelled",
423
+ subject: "Your WOPR account deletion has been cancelled",
424
+ htmlBody: html(
425
+ "Account Deletion Cancelled",
426
+ hd("Account Deletion Cancelled"),
427
+ p(
428
+ "<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>",
429
+ ),
430
+ ft("If you didn't cancel this, please contact support@wopr.bot."),
431
+ ),
432
+ textBody: `Account Deletion Cancelled
433
+
434
+ Hi {{email}},
435
+
436
+ Your account deletion request has been cancelled. Your account and all data remain intact.
437
+
438
+ No further action is needed.${CR}`,
439
+ },
440
+ {
441
+ name: "account-deletion-completed",
442
+ description: "Sent after account is permanently deleted",
443
+ subject: "Your WOPR account has been deleted",
444
+ htmlBody: html(
445
+ "Account Deleted",
446
+ hd("Your Account Has Been Deleted"),
447
+ p(
448
+ "<p>Hi <strong>{{email}}</strong>,</p><p>Your WOPR 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 WOPR again in the future, you're welcome to create a new account.</p>",
449
+ ),
450
+ ft("Thank you for using WOPR. We're sorry to see you go."),
451
+ ),
452
+ textBody: `Your Account Has Been Deleted
453
+
454
+ Hi {{email}},
455
+
456
+ Your WOPR account and all associated data have been permanently deleted as requested.
457
+
458
+ This includes all bots, conversation history, credit records, billing data, and plugin configurations.
459
+
460
+ If you'd like to use WOPR again in the future, you're welcome to create a new account.${CR}`,
461
+ },
462
+ // -- Dividend & Affiliate --------------------------------------------------
463
+ {
464
+ name: "dividend-weekly-digest",
465
+ description: "Weekly summary of dividend payouts",
466
+ subject: "WOPR paid you {{weeklyTotalDollars}} this week",
467
+ htmlBody: html(
468
+ "Weekly Dividend Digest",
469
+ hd("WOPR Paid You {{weeklyTotalDollars}} This Week"),
470
+ p(
471
+ `<p>Here's your weekly dividend summary for <strong>{{weekStartDate}} \u2013 {{weekEndDate}}</strong>.</p>` +
472
+ `<table style="width: 100%; border-collapse: collapse; margin: 16px 0;">` +
473
+ `<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>` +
474
+ `<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>` +
475
+ `<tr><td style="padding: 8px 0; color: #4a5568;">Avg. daily pool</td><td style="padding: 8px 0; text-align: right; font-weight: 600; color: #1a1a1a;">{{poolAvgDollars}}</td></tr>` +
476
+ `<tr><td style="padding: 8px 0; color: #4a5568;">Avg. active users</td><td style="padding: 8px 0; text-align: right; font-weight: 600; color: #1a1a1a;">{{activeUsersAvg}}</td></tr>` +
477
+ `<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>` +
478
+ `</table>` +
479
+ `{{#if nextDividendDate}}<p style="color: #718096; font-size: 14px;">Next dividend: <strong>{{nextDividendDate}}</strong></p>{{/if}}`,
480
+ ),
481
+ `{{#if creditsUrl}}${btn("{{creditsUrl}}", "View Your Credits")}{{/if}}`,
482
+ ft("Community dividends are distributed daily from platform revenue. Keep your credits active to stay eligible."),
483
+ '{{#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}}',
484
+ ),
485
+ textBody: `WOPR Paid You {{weeklyTotalDollars}} This Week
486
+
487
+ Weekly summary for {{weekStartDate}} \u2013 {{weekEndDate}}:
488
+
489
+ This week's dividends: {{weeklyTotalDollars}}
490
+ Days with distributions: {{distributionCount}} of 7
491
+ Avg. daily pool: {{poolAvgDollars}}
492
+ Avg. active users: {{activeUsersAvg}}
493
+ Lifetime total: {{lifetimeTotalDollars}}
494
+
495
+ Next dividend: {{nextDividendDate}}
496
+
497
+ Community dividends are distributed daily from platform revenue.{{#if unsubscribeUrl}}
498
+
499
+ Unsubscribe: {{unsubscribeUrl}}{{/if}}${CR}`,
500
+ },
501
+ {
502
+ name: "affiliate-credit-match",
503
+ description: "Sent when affiliate earns credits from a referral purchase",
504
+ subject: "You earned {{amountDollars}} in affiliate credits!",
505
+ htmlBody: html(
506
+ "Affiliate Credits Earned",
507
+ hd("You Earned Affiliate Credits!"),
508
+ p(
509
+ "<p>Great news! A user you referred just made their first purchase, and you've been credited <strong>{{amountDollars}}</strong>.</p>",
510
+ ),
511
+ `{{#if creditsUrl}}${btn("{{creditsUrl}}", "View Credit Balance")}{{/if}}`,
512
+ ft("Thank you for spreading the word about WOPR!"),
513
+ ),
514
+ textBody: `You Earned Affiliate Credits!
515
+
516
+ A user you referred just made their first purchase, and you've been credited {{amountDollars}}.
517
+ {{#if creditsUrl}}
518
+ View your balance: {{creditsUrl}}
519
+ {{/if}}${CR}`,
520
+ },
521
+ {
522
+ name: "spend-alert",
523
+ description: "Sent when monthly spend crosses the configured alert threshold",
524
+ subject: "Spending alert: you've reached your {{alertAtDollars}} threshold",
525
+ htmlBody: html(
526
+ "Spending Alert",
527
+ hd("Spending Alert: Threshold Reached"),
528
+ p(
529
+ "<p>Your monthly spend has reached <strong>{{currentSpendDollars}}</strong>, crossing your alert threshold of <strong>{{alertAtDollars}}</strong>.</p><p>Review your spending to stay within your budget.</p>",
530
+ ),
531
+ `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Review Spending")}{{/if}}`,
532
+ ft("This alert fires once per day when your spend exceeds your configured threshold."),
533
+ ),
534
+ textBody: `Spending Alert: Threshold Reached
535
+
536
+ Your monthly spend has reached {{currentSpendDollars}}, crossing your alert threshold of {{alertAtDollars}}.
537
+ {{#if creditsUrl}}
538
+ Review spending: {{creditsUrl}}
539
+ {{/if}}${CR}`,
540
+ },
541
+ {
542
+ name: "custom",
543
+ description: "Admin custom email with arbitrary body text",
544
+ subject: "{{subject}}",
545
+ htmlBody: html(
546
+ "{{subject}}",
547
+ hd("Message from WOPR"),
548
+ p("<p>{{{bodyTextHtml}}}</p>"),
549
+ ft("This is an administrative message from WOPR Network."),
550
+ ),
551
+ textBody: `{{bodyText}}${CR}`,
552
+ },
553
+ // -- Passthrough templates (billing/auth) ----------------------------------
554
+ {
555
+ name: "low-balance",
556
+ description: "Sent when credit balance drops below threshold",
557
+ subject: "Your WOPR credits are running low",
558
+ htmlBody: html(
559
+ "Low Balance",
560
+ hd("Your WOPR Credits Are Running Low"),
561
+ p("<p>Your balance is <strong>{{balanceDollars}}</strong>. Top up to keep your agents running.</p>"),
562
+ `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Buy Credits")}{{/if}}`,
563
+ ft("This is an automated billing notification."),
564
+ ),
565
+ textBody: `Your WOPR Credits Are Running Low
566
+
567
+ Balance: {{balanceDollars}}
568
+ {{#if creditsUrl}}
569
+ Buy credits: {{creditsUrl}}
570
+ {{/if}}${CR}`,
571
+ },
572
+ {
573
+ name: "credit-purchase-receipt",
574
+ description: "Sent after a credit purchase is completed",
575
+ subject: "Credits added to your account",
576
+ htmlBody: html(
577
+ "Credits Added",
578
+ hd("Credits Added to Your Account"),
579
+ p(
580
+ "<p><strong>{{amountDollars}}</strong> in credits has been added.</p>{{#if newBalanceDollars}}<p>New balance: <strong>{{newBalanceDollars}}</strong></p>{{/if}}",
581
+ ),
582
+ ft("Thank you for supporting WOPR!"),
583
+ ),
584
+ textBody: `Credits Added
585
+
586
+ {{amountDollars}} added.${CR}`,
587
+ },
588
+ {
589
+ name: "welcome",
590
+ description: "Sent to new users after registration",
591
+ subject: "Welcome to WOPR",
592
+ htmlBody: html(
593
+ "Welcome",
594
+ hd("Welcome to WOPR!"),
595
+ p("<p>Your account is now active. Start building!</p>"),
596
+ ft("Happy building!"),
597
+ ),
598
+ textBody: `Welcome to WOPR!
599
+
600
+ Your account is now active.${CR}`,
601
+ },
602
+ {
603
+ name: "password-reset",
604
+ description: "Sent when a user requests a password reset",
605
+ subject: "Reset your WOPR password",
606
+ htmlBody: html(
607
+ "Reset Password",
608
+ hd("Reset Your Password"),
609
+ p("<p>Click below to reset your password.</p>"),
610
+ `{{#if resetUrl}}${btn("{{resetUrl}}", "Reset Password")}{{/if}}`,
611
+ ft("If you did not request this, ignore this email."),
612
+ ),
613
+ textBody: `Reset Your Password
614
+ {{#if resetUrl}}
615
+ {{resetUrl}}
616
+ {{/if}}${CR}`,
617
+ },
618
+ // -- Fleet Updates (new) ---------------------------------------------------
619
+ {
620
+ name: "fleet-update-available",
621
+ description: "Sent when a new fleet update version is available",
622
+ subject: "Fleet update available: {{version}}",
623
+ htmlBody: html(
624
+ "Fleet Update Available",
625
+ hd("Fleet Update Available: {{version}}"),
626
+ p(
627
+ "<p>A new version <strong>{{version}}</strong> is available for your fleet.</p>" +
628
+ '{{#if changelogDate}}<p style="color: #718096; font-size: 14px;">Released: {{changelogDate}}</p>{{/if}}' +
629
+ '{{#if changelogSummary}}<div style="background: #f7fafc; border-left: 4px solid #2563eb; padding: 12px 16px; margin: 16px 0; color: #4a5568; font-size: 14px; line-height: 22px;">{{changelogSummary}}</div>{{/if}}',
630
+ ),
631
+ `{{#if fleetUrl}}${btn("{{fleetUrl}}", "View Fleet Dashboard")}{{/if}}`,
632
+ ft("Review the changelog and update when ready."),
633
+ ),
634
+ textBody: `Fleet Update Available: {{version}}
635
+
636
+ A new version {{version}} is available for your fleet.
637
+ {{#if changelogDate}}
638
+ Released: {{changelogDate}}
639
+ {{/if}}{{#if changelogSummary}}
640
+ Changelog: {{changelogSummary}}
641
+ {{/if}}{{#if fleetUrl}}
642
+ Fleet dashboard: {{fleetUrl}}
643
+ {{/if}}${CR}`,
644
+ },
645
+ {
646
+ name: "fleet-update-complete",
647
+ description: "Sent after a fleet update rollout completes",
648
+ subject:
649
+ "Fleet updated to {{version}} \u2014 {{#if (eq failed 0)}}all instances healthy{{else}}{{failed}} instance(s) failed{{/if}}",
650
+ htmlBody: html(
651
+ "Fleet Update Complete",
652
+ hd("Fleet Updated to {{version}}"),
653
+ p(
654
+ "<p>Your fleet update to <strong>{{version}}</strong> has completed.</p>" +
655
+ '<table style="width: 100%; border-collapse: collapse; margin: 16px 0;">' +
656
+ '<tr><td style="padding: 8px 0; color: #4a5568;">Succeeded</td><td style="padding: 8px 0; text-align: right; font-weight: 600; color: #22c55e;">{{succeeded}}</td></tr>' +
657
+ '<tr><td style="padding: 8px 0; color: #4a5568;">Failed</td><td style="padding: 8px 0; text-align: right; font-weight: 600; color: {{#if (gt failed 0)}}#dc2626{{else}}#22c55e{{/if}};">{{failed}}</td></tr>' +
658
+ '<tr style="border-top: 2px solid #e2e8f0;"><td style="padding: 12px 0; color: #4a5568; font-weight: 600;">Total</td><td style="padding: 12px 0; text-align: right; font-weight: 700; color: #1a1a1a;">{{total}}</td></tr>' +
659
+ "</table>" +
660
+ '{{#if (gt failed 0)}}<p style="color: #dc2626;">Some instances failed to update. Check the fleet dashboard for details.</p>{{/if}}',
661
+ ),
662
+ `{{#if fleetUrl}}${btn("{{fleetUrl}}", "View Fleet Dashboard")}{{/if}}`,
663
+ ft(
664
+ "{{#if (eq failed 0)}}All instances are running the latest version.{{else}}Review failed instances and retry if needed.{{/if}}",
665
+ ),
666
+ ),
667
+ textBody: `Fleet Updated to {{version}}
668
+
669
+ Your fleet update to {{version}} has completed.
670
+
671
+ Succeeded: {{succeeded}}
672
+ Failed: {{failed}}
673
+ Total: {{total}}
674
+ {{#if (gt failed 0)}}
675
+ Some instances failed to update. Check the fleet dashboard for details.
676
+ {{/if}}{{#if fleetUrl}}
677
+ Fleet dashboard: {{fleetUrl}}
678
+ {{/if}}${CR}`,
679
+ },
680
+ ];