@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.
- package/dist/billing/crypto/charge-store.d.ts +10 -0
- package/dist/billing/crypto/charge-store.js +9 -1
- package/dist/billing/crypto/payment-method-store.d.ts +2 -0
- package/dist/billing/crypto/payment-method-store.js +6 -0
- package/dist/db/schema/crypto.d.ts +34 -0
- package/dist/db/schema/crypto.js +3 -1
- package/dist/db/schema/index.d.ts +1 -0
- package/dist/db/schema/index.js +1 -0
- package/dist/db/schema/notification-preferences.d.ts +17 -0
- package/dist/db/schema/notification-preferences.js +1 -0
- package/dist/db/schema/notification-templates.d.ts +165 -0
- package/dist/db/schema/notification-templates.js +18 -0
- package/dist/email/default-templates.d.ts +15 -0
- package/dist/email/default-templates.js +443 -0
- package/dist/email/drizzle-notification-template-repository.d.ts +21 -0
- package/dist/email/drizzle-notification-template-repository.js +90 -0
- package/dist/email/handlebars-renderer.d.ts +17 -0
- package/dist/email/handlebars-renderer.js +56 -0
- package/dist/email/index.d.ts +4 -0
- package/dist/email/index.js +3 -0
- package/dist/email/notification-preferences-store.js +5 -0
- package/dist/email/notification-preferences-store.test.js +1 -0
- package/dist/email/notification-repository-types.d.ts +30 -0
- package/dist/email/notification-service.d.ts +2 -0
- package/dist/email/notification-service.js +22 -0
- package/dist/email/notification-template-repository.d.ts +7 -0
- package/dist/email/notification-template-repository.js +7 -0
- package/dist/email/notification-templates.d.ts +1 -1
- package/dist/email/notification-templates.js +52 -0
- package/dist/trpc/index.d.ts +1 -0
- package/dist/trpc/index.js +1 -0
- package/dist/trpc/notification-template-router.d.ts +59 -0
- package/dist/trpc/notification-template-router.js +81 -0
- package/drizzle/migrations/0010_oracle_address.sql +11 -0
- package/drizzle/migrations/0011_notification_templates.sql +14 -0
- package/drizzle/migrations/meta/_journal.json +14 -0
- package/package.json +2 -1
- package/src/billing/crypto/charge-store.ts +14 -1
- package/src/billing/crypto/payment-method-store.ts +8 -0
- package/src/db/schema/crypto.ts +3 -1
- package/src/db/schema/index.ts +1 -0
- package/src/db/schema/notification-preferences.ts +1 -0
- package/src/db/schema/notification-templates.ts +19 -0
- package/src/email/default-templates.ts +680 -0
- package/src/email/drizzle-notification-template-repository.ts +108 -0
- package/src/email/handlebars-renderer.ts +64 -0
- package/src/email/index.ts +4 -0
- package/src/email/notification-preferences-store.test.ts +1 -0
- package/src/email/notification-preferences-store.ts +4 -0
- package/src/email/notification-repository-types.ts +41 -0
- package/src/email/notification-service.ts +31 -0
- package/src/email/notification-template-repository.ts +8 -0
- package/src/email/notification-templates.ts +61 -0
- package/src/trpc/index.ts +1 -0
- package/src/trpc/notification-template-router.ts +91 -0
package/dist/email/index.js
CHANGED
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export { BillingEmailService } from "./billing-emails.js";
|
|
12
12
|
export { EmailClient, getEmailClient, resetEmailClient, setEmailClient } from "./client.js";
|
|
13
|
+
export { DEFAULT_TEMPLATES } from "./default-templates.js";
|
|
13
14
|
export { DrizzleBillingEmailRepository } from "./drizzle-billing-email-repository.js";
|
|
15
|
+
export { DrizzleNotificationTemplateRepository } from "./drizzle-notification-template-repository.js";
|
|
16
|
+
export { HandlebarsRenderer } from "./handlebars-renderer.js";
|
|
14
17
|
export { DrizzleNotificationPreferencesStore } from "./notification-preferences-store.js";
|
|
15
18
|
export { DrizzleNotificationQueueStore } from "./notification-queue-store.js";
|
|
16
19
|
export { NotificationService } from "./notification-service.js";
|
|
@@ -11,6 +11,7 @@ const DEFAULTS = {
|
|
|
11
11
|
agent_status_changes: false,
|
|
12
12
|
account_role_changes: true,
|
|
13
13
|
account_team_invites: true,
|
|
14
|
+
fleet_updates: true,
|
|
14
15
|
};
|
|
15
16
|
export class DrizzleNotificationPreferencesStore {
|
|
16
17
|
db;
|
|
@@ -34,6 +35,7 @@ export class DrizzleNotificationPreferencesStore {
|
|
|
34
35
|
agent_status_changes: row.agentStatusChanges,
|
|
35
36
|
account_role_changes: row.accountRoleChanges,
|
|
36
37
|
account_team_invites: row.accountTeamInvites,
|
|
38
|
+
fleet_updates: row.fleetUpdates,
|
|
37
39
|
};
|
|
38
40
|
}
|
|
39
41
|
/** Update preferences for a tenant. Upserts — creates row if missing. */
|
|
@@ -53,6 +55,8 @@ export class DrizzleNotificationPreferencesStore {
|
|
|
53
55
|
values.accountRoleChanges = prefs.account_role_changes;
|
|
54
56
|
if (prefs.account_team_invites !== undefined)
|
|
55
57
|
values.accountTeamInvites = prefs.account_team_invites;
|
|
58
|
+
if (prefs.fleet_updates !== undefined)
|
|
59
|
+
values.fleetUpdates = prefs.fleet_updates;
|
|
56
60
|
// Get existing row to merge with defaults
|
|
57
61
|
const existing = await this.db
|
|
58
62
|
.select()
|
|
@@ -73,6 +77,7 @@ export class DrizzleNotificationPreferencesStore {
|
|
|
73
77
|
agentStatusChanges: DEFAULTS.agent_status_changes,
|
|
74
78
|
accountRoleChanges: DEFAULTS.account_role_changes,
|
|
75
79
|
accountTeamInvites: DEFAULTS.account_team_invites,
|
|
80
|
+
fleetUpdates: DEFAULTS.fleet_updates,
|
|
76
81
|
updatedAt: Math.floor(Date.now() / 1000),
|
|
77
82
|
...values,
|
|
78
83
|
};
|
|
@@ -20,6 +20,7 @@ export interface NotificationPrefs {
|
|
|
20
20
|
agent_status_changes: boolean;
|
|
21
21
|
account_role_changes: boolean;
|
|
22
22
|
account_team_invites: boolean;
|
|
23
|
+
fleet_updates: boolean;
|
|
23
24
|
}
|
|
24
25
|
export type NotificationEmailType = "low_balance" | "grace_entered" | "suspended" | "receipt" | "welcome" | "reactivated";
|
|
25
26
|
export interface NotificationInput {
|
|
@@ -48,6 +49,35 @@ export interface NotificationRow {
|
|
|
48
49
|
/** Unix epoch ms */
|
|
49
50
|
sentAt: number | null;
|
|
50
51
|
}
|
|
52
|
+
/** Row shape returned by the template repository. */
|
|
53
|
+
export interface NotificationTemplateRow {
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
description: string | null;
|
|
57
|
+
subject: string;
|
|
58
|
+
htmlBody: string;
|
|
59
|
+
textBody: string;
|
|
60
|
+
active: boolean;
|
|
61
|
+
createdAt: number;
|
|
62
|
+
updatedAt: number;
|
|
63
|
+
}
|
|
64
|
+
/** Repository contract for notification templates. */
|
|
65
|
+
export interface INotificationTemplateRepository {
|
|
66
|
+
getByName(name: string): Promise<NotificationTemplateRow | null>;
|
|
67
|
+
list(): Promise<NotificationTemplateRow[]>;
|
|
68
|
+
upsert(name: string, template: Omit<NotificationTemplateRow, "id" | "name" | "createdAt" | "updatedAt">): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Seed default templates — INSERT OR IGNORE so admin edits are not overwritten.
|
|
71
|
+
* @returns number of templates inserted (not already present).
|
|
72
|
+
*/
|
|
73
|
+
seed(templates: Array<{
|
|
74
|
+
name: string;
|
|
75
|
+
description: string;
|
|
76
|
+
subject: string;
|
|
77
|
+
htmlBody: string;
|
|
78
|
+
textBody: string;
|
|
79
|
+
}>): Promise<number>;
|
|
80
|
+
}
|
|
51
81
|
/** Repository interface for notification preferences. */
|
|
52
82
|
export interface INotificationPreferencesRepository {
|
|
53
83
|
get(tenantId: string): Promise<NotificationPrefs>;
|
|
@@ -37,5 +37,7 @@ export declare class NotificationService {
|
|
|
37
37
|
notifyAccountDeletionRequested(tenantId: string, email: string, deleteAfterDate: string): void;
|
|
38
38
|
notifyAccountDeletionCancelled(tenantId: string, email: string): void;
|
|
39
39
|
notifyAccountDeletionCompleted(tenantId: string, email: string): void;
|
|
40
|
+
notifyFleetUpdateAvailable(tenantId: string, email: string, version: string, changelogDate: string, changelogSummary: string): void;
|
|
41
|
+
notifyFleetUpdateComplete(tenantId: string, email: string, version: string, succeeded: number, failed: number): void;
|
|
40
42
|
sendCustomEmail(tenantId: string, email: string, subject: string, bodyText: string): void;
|
|
41
43
|
}
|
|
@@ -188,6 +188,28 @@ export class NotificationService {
|
|
|
188
188
|
this.queue.enqueue(tenantId, "account-deletion-completed", { email });
|
|
189
189
|
}
|
|
190
190
|
// ---------------------------------------------------------------------------
|
|
191
|
+
// Fleet Updates
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
notifyFleetUpdateAvailable(tenantId, email, version, changelogDate, changelogSummary) {
|
|
194
|
+
this.queue.enqueue(tenantId, "fleet-update-available", {
|
|
195
|
+
email,
|
|
196
|
+
version,
|
|
197
|
+
changelogDate,
|
|
198
|
+
changelogSummary,
|
|
199
|
+
fleetUrl: `${this.appBaseUrl}/fleet`,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
notifyFleetUpdateComplete(tenantId, email, version, succeeded, failed) {
|
|
203
|
+
this.queue.enqueue(tenantId, "fleet-update-complete", {
|
|
204
|
+
email,
|
|
205
|
+
version,
|
|
206
|
+
succeeded,
|
|
207
|
+
failed,
|
|
208
|
+
total: succeeded + failed,
|
|
209
|
+
fleetUrl: `${this.appBaseUrl}/fleet`,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
191
213
|
// Admin custom email
|
|
192
214
|
// ---------------------------------------------------------------------------
|
|
193
215
|
sendCustomEmail(tenantId, email, subject, bodyText) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* INotificationTemplateRepository — interface for DB-driven email templates.
|
|
3
|
+
*
|
|
4
|
+
* Types live in notification-repository-types.ts; this file re-exports them
|
|
5
|
+
* so existing imports continue to work.
|
|
6
|
+
*/
|
|
7
|
+
export type { INotificationTemplateRepository, NotificationTemplateRow } from "./notification-repository-types.js";
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Export: renderNotificationTemplate(template, data) → TemplateResult
|
|
8
8
|
*/
|
|
9
9
|
import type { TemplateResult } from "./templates.js";
|
|
10
|
-
export type NotificationTemplateName = "credits-depleted" | "grace-period-start" | "grace-period-warning" | "auto-suspended" | "auto-topup-success" | "auto-topup-failed" | "crypto-payment-confirmed" | "admin-suspended" | "admin-reactivated" | "credits-granted" | "role-changed" | "team-invite" | "agent-created" | "channel-connected" | "channel-disconnected" | "agent-suspended" | "custom" | "account-deletion-requested" | "account-deletion-cancelled" | "account-deletion-completed" | "dividend-weekly-digest" | "affiliate-credit-match" | "spend-alert" | "low-balance" | "credit-purchase-receipt" | "welcome" | "password-reset";
|
|
10
|
+
export type NotificationTemplateName = "credits-depleted" | "grace-period-start" | "grace-period-warning" | "auto-suspended" | "auto-topup-success" | "auto-topup-failed" | "crypto-payment-confirmed" | "admin-suspended" | "admin-reactivated" | "credits-granted" | "role-changed" | "team-invite" | "agent-created" | "channel-connected" | "channel-disconnected" | "agent-suspended" | "custom" | "account-deletion-requested" | "account-deletion-cancelled" | "account-deletion-completed" | "dividend-weekly-digest" | "affiliate-credit-match" | "spend-alert" | "fleet-update-available" | "fleet-update-complete" | "low-balance" | "credit-purchase-receipt" | "welcome" | "password-reset";
|
|
11
11
|
export type TemplateName = NotificationTemplateName;
|
|
12
12
|
/**
|
|
13
13
|
* Render a notification template by name.
|
|
@@ -520,6 +520,58 @@ export function renderNotificationTemplate(template, data) {
|
|
|
520
520
|
return accountDeletionCancelledTemplate(data);
|
|
521
521
|
case "account-deletion-completed":
|
|
522
522
|
return accountDeletionCompletedTemplate(data);
|
|
523
|
+
case "fleet-update-available": {
|
|
524
|
+
const version = escapeHtml(data.version || "");
|
|
525
|
+
const changelogDate = escapeHtml(data.changelogDate || "");
|
|
526
|
+
const changelogSummary = escapeHtml(data.changelogSummary || "");
|
|
527
|
+
const fleetUrl = data.fleetUrl || "";
|
|
528
|
+
const faParts = [
|
|
529
|
+
heading(`Fleet Update Available: ${version}`),
|
|
530
|
+
paragraph(`<p>A new version <strong>${version}</strong> is available for your fleet.</p>` +
|
|
531
|
+
(changelogDate ? `<p style="color: #718096; font-size: 14px;">Released: ${changelogDate}</p>` : "") +
|
|
532
|
+
(changelogSummary
|
|
533
|
+
? `<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>`
|
|
534
|
+
: "")),
|
|
535
|
+
];
|
|
536
|
+
if (fleetUrl)
|
|
537
|
+
faParts.push(button(fleetUrl, "View Fleet Dashboard"));
|
|
538
|
+
faParts.push(footer("Review the changelog and update when ready."));
|
|
539
|
+
return {
|
|
540
|
+
subject: `Fleet update available: ${data.version}`,
|
|
541
|
+
html: wrapHtml("Fleet Update Available", faParts.join("\n")),
|
|
542
|
+
text: `Fleet Update Available: ${data.version}\n\nA new version ${data.version} is available for your fleet.\n${changelogDate ? `Released: ${data.changelogDate}\n` : ""}${changelogSummary ? `Changelog: ${data.changelogSummary}\n` : ""}${fleetUrl ? `\nFleet dashboard: ${fleetUrl}\n` : ""}${copyright()}`,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
case "fleet-update-complete": {
|
|
546
|
+
const fcVersion = escapeHtml(data.version || "");
|
|
547
|
+
const succeeded = Number(data.succeeded) || 0;
|
|
548
|
+
const failed = Number(data.failed) || 0;
|
|
549
|
+
const total = succeeded + failed;
|
|
550
|
+
const fleetUrl2 = data.fleetUrl || "";
|
|
551
|
+
const statusText = failed === 0 ? "all instances healthy" : `${failed} instance(s) failed`;
|
|
552
|
+
const fcParts = [
|
|
553
|
+
heading(`Fleet Updated to ${fcVersion}`),
|
|
554
|
+
paragraph(`<p>Your fleet update to <strong>${fcVersion}</strong> has completed.</p>` +
|
|
555
|
+
`<table style="width: 100%; border-collapse: collapse; margin: 16px 0;">` +
|
|
556
|
+
`<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>` +
|
|
557
|
+
`<tr><td style="padding: 8px 0; color: #4a5568;">Failed</td><td style="padding: 8px 0; text-align: right; font-weight: 600; color: ${failed > 0 ? "#dc2626" : "#22c55e"};">${failed}</td></tr>` +
|
|
558
|
+
`<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>` +
|
|
559
|
+
`</table>` +
|
|
560
|
+
(failed > 0
|
|
561
|
+
? `<p style="color: #dc2626;">Some instances failed to update. Check the fleet dashboard for details.</p>`
|
|
562
|
+
: "")),
|
|
563
|
+
];
|
|
564
|
+
if (fleetUrl2)
|
|
565
|
+
fcParts.push(button(fleetUrl2, "View Fleet Dashboard"));
|
|
566
|
+
fcParts.push(footer(failed === 0
|
|
567
|
+
? "All instances are running the latest version."
|
|
568
|
+
: "Review failed instances and retry if needed."));
|
|
569
|
+
return {
|
|
570
|
+
subject: `Fleet updated to ${data.version} \u2014 ${statusText}`,
|
|
571
|
+
html: wrapHtml("Fleet Update Complete", fcParts.join("\n")),
|
|
572
|
+
text: `Fleet Updated to ${data.version}\n\nSucceeded: ${succeeded}\nFailed: ${failed}\nTotal: ${total}\n${failed > 0 ? "\nSome instances failed to update.\n" : ""}${fleetUrl2 ? `\nFleet dashboard: ${fleetUrl2}\n` : ""}${copyright()}`,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
523
575
|
case "low-balance":
|
|
524
576
|
return {
|
|
525
577
|
subject: "Your WOPR credits are running low",
|
package/dist/trpc/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { createFleetUpdateConfigRouter } from "./fleet-update-config-router.js";
|
|
2
2
|
export { adminProcedure, createCallerFactory, orgAdminProcedure, orgMemberProcedure, protectedProcedure, publicProcedure, router, setTrpcOrgMemberRepo, type TRPCContext, tenantProcedure, } from "./init.js";
|
|
3
|
+
export { createNotificationTemplateRouter } from "./notification-template-router.js";
|
package/dist/trpc/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { createFleetUpdateConfigRouter } from "./fleet-update-config-router.js";
|
|
2
2
|
export { adminProcedure, createCallerFactory, orgAdminProcedure, orgMemberProcedure, protectedProcedure, publicProcedure, router, setTrpcOrgMemberRepo, tenantProcedure, } from "./init.js";
|
|
3
|
+
export { createNotificationTemplateRouter } from "./notification-template-router.js";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin tRPC router for managing DB-driven notification templates.
|
|
3
|
+
*
|
|
4
|
+
* All procedures require platform_admin role via adminProcedure.
|
|
5
|
+
*/
|
|
6
|
+
import type { INotificationTemplateRepository } from "../email/notification-template-repository.js";
|
|
7
|
+
export declare function createNotificationTemplateRouter(getRepo: () => INotificationTemplateRepository): import("@trpc/server").TRPCBuiltRouter<{
|
|
8
|
+
ctx: import("./init.js").TRPCContext;
|
|
9
|
+
meta: object;
|
|
10
|
+
errorShape: import("@trpc/server").TRPCDefaultErrorShape;
|
|
11
|
+
transformer: false;
|
|
12
|
+
}, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
|
|
13
|
+
listTemplates: import("@trpc/server").TRPCQueryProcedure<{
|
|
14
|
+
input: void;
|
|
15
|
+
output: import("../email/notification-repository-types.js").NotificationTemplateRow[];
|
|
16
|
+
meta: object;
|
|
17
|
+
}>;
|
|
18
|
+
getTemplate: import("@trpc/server").TRPCQueryProcedure<{
|
|
19
|
+
input: {
|
|
20
|
+
name: string;
|
|
21
|
+
};
|
|
22
|
+
output: import("../email/notification-repository-types.js").NotificationTemplateRow | null;
|
|
23
|
+
meta: object;
|
|
24
|
+
}>;
|
|
25
|
+
updateTemplate: import("@trpc/server").TRPCMutationProcedure<{
|
|
26
|
+
input: {
|
|
27
|
+
name: string;
|
|
28
|
+
subject?: string | undefined;
|
|
29
|
+
htmlBody?: string | undefined;
|
|
30
|
+
textBody?: string | undefined;
|
|
31
|
+
description?: string | undefined;
|
|
32
|
+
active?: boolean | undefined;
|
|
33
|
+
};
|
|
34
|
+
output: void;
|
|
35
|
+
meta: object;
|
|
36
|
+
}>;
|
|
37
|
+
previewTemplate: import("@trpc/server").TRPCMutationProcedure<{
|
|
38
|
+
input: {
|
|
39
|
+
subject: string;
|
|
40
|
+
htmlBody: string;
|
|
41
|
+
textBody: string;
|
|
42
|
+
data: Record<string, unknown>;
|
|
43
|
+
};
|
|
44
|
+
output: {
|
|
45
|
+
subject: string;
|
|
46
|
+
html: string;
|
|
47
|
+
text: string;
|
|
48
|
+
};
|
|
49
|
+
meta: object;
|
|
50
|
+
}>;
|
|
51
|
+
seedDefaults: import("@trpc/server").TRPCMutationProcedure<{
|
|
52
|
+
input: void;
|
|
53
|
+
output: {
|
|
54
|
+
inserted: number;
|
|
55
|
+
total: number;
|
|
56
|
+
};
|
|
57
|
+
meta: object;
|
|
58
|
+
}>;
|
|
59
|
+
}>>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin tRPC router for managing DB-driven notification templates.
|
|
3
|
+
*
|
|
4
|
+
* All procedures require platform_admin role via adminProcedure.
|
|
5
|
+
*/
|
|
6
|
+
import { TRPCError } from "@trpc/server";
|
|
7
|
+
import Handlebars from "handlebars";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { logger } from "../config/logger.js";
|
|
10
|
+
import { DEFAULT_TEMPLATES } from "../email/default-templates.js";
|
|
11
|
+
import { adminProcedure, router } from "./init.js";
|
|
12
|
+
export function createNotificationTemplateRouter(getRepo) {
|
|
13
|
+
return router({
|
|
14
|
+
listTemplates: adminProcedure.query(async () => {
|
|
15
|
+
const repo = getRepo();
|
|
16
|
+
return repo.list();
|
|
17
|
+
}),
|
|
18
|
+
getTemplate: adminProcedure.input(z.object({ name: z.string() })).query(async ({ input }) => {
|
|
19
|
+
const repo = getRepo();
|
|
20
|
+
return repo.getByName(input.name);
|
|
21
|
+
}),
|
|
22
|
+
updateTemplate: adminProcedure
|
|
23
|
+
.input(z.object({
|
|
24
|
+
name: z.string(),
|
|
25
|
+
subject: z.string().optional(),
|
|
26
|
+
htmlBody: z.string().optional(),
|
|
27
|
+
textBody: z.string().optional(),
|
|
28
|
+
description: z.string().optional(),
|
|
29
|
+
active: z.boolean().optional(),
|
|
30
|
+
}))
|
|
31
|
+
.mutation(async ({ input }) => {
|
|
32
|
+
const repo = getRepo();
|
|
33
|
+
const existing = await repo.getByName(input.name);
|
|
34
|
+
if (!existing) {
|
|
35
|
+
throw new TRPCError({ code: "NOT_FOUND", message: `Template "${input.name}" not found` });
|
|
36
|
+
}
|
|
37
|
+
await repo.upsert(input.name, {
|
|
38
|
+
subject: input.subject ?? existing.subject,
|
|
39
|
+
htmlBody: input.htmlBody ?? existing.htmlBody,
|
|
40
|
+
textBody: input.textBody ?? existing.textBody,
|
|
41
|
+
description: input.description ?? existing.description,
|
|
42
|
+
active: input.active ?? existing.active,
|
|
43
|
+
});
|
|
44
|
+
logger.info("Notification template updated", {
|
|
45
|
+
action: "notification_template.update",
|
|
46
|
+
templateName: input.name,
|
|
47
|
+
});
|
|
48
|
+
}),
|
|
49
|
+
previewTemplate: adminProcedure
|
|
50
|
+
.input(z.object({
|
|
51
|
+
subject: z.string(),
|
|
52
|
+
htmlBody: z.string(),
|
|
53
|
+
textBody: z.string(),
|
|
54
|
+
data: z.record(z.string(), z.unknown()),
|
|
55
|
+
}))
|
|
56
|
+
.mutation(({ input }) => {
|
|
57
|
+
try {
|
|
58
|
+
const subject = Handlebars.compile(input.subject)(input.data);
|
|
59
|
+
const html = Handlebars.compile(input.htmlBody)(input.data);
|
|
60
|
+
const text = Handlebars.compile(input.textBody)(input.data);
|
|
61
|
+
return { subject, html, text };
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
throw new TRPCError({
|
|
65
|
+
code: "BAD_REQUEST",
|
|
66
|
+
message: `Template render error: ${err instanceof Error ? err.message : String(err)}`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}),
|
|
70
|
+
seedDefaults: adminProcedure.mutation(async () => {
|
|
71
|
+
const repo = getRepo();
|
|
72
|
+
const inserted = await repo.seed(DEFAULT_TEMPLATES);
|
|
73
|
+
logger.info("Notification template defaults seeded", {
|
|
74
|
+
action: "notification_template.seed",
|
|
75
|
+
inserted,
|
|
76
|
+
total: DEFAULT_TEMPLATES.length,
|
|
77
|
+
});
|
|
78
|
+
return { inserted, total: DEFAULT_TEMPLATES.length };
|
|
79
|
+
}),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
ALTER TABLE "payment_methods" ADD COLUMN "oracle_address" text;
|
|
2
|
+
--> statement-breakpoint
|
|
3
|
+
ALTER TABLE "payment_methods" ADD COLUMN "xpub" text;
|
|
4
|
+
--> statement-breakpoint
|
|
5
|
+
UPDATE "payment_methods" SET "oracle_address" = '0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70' WHERE "id" = 'ETH:base';
|
|
6
|
+
--> statement-breakpoint
|
|
7
|
+
UPDATE "payment_methods" SET "oracle_address" = '0x64c911996D3c6aC71f9b455B1E8E7266BcbD848F' WHERE "id" = 'BTC:mainnet';
|
|
8
|
+
--> statement-breakpoint
|
|
9
|
+
UPDATE "payment_methods" SET "xpub" = 'xpub6DSVkV7mgEZrnBEmZEq412Cx9sYYZtFvGSb6W9bRDDSikYdpmUiJoNeuechuir63ZjdHQuWBLwchQQnh2GD6DJP6bPKUa1bey1X6XvH9jvM' WHERE "chain" = 'base';
|
|
10
|
+
--> statement-breakpoint
|
|
11
|
+
UPDATE "payment_methods" SET "xpub" = 'xpub6BuGg4sQuvoA7q545ZoStxU7QP24qmZNMo39FxRjLwbBCQ77sjsHGcpxeNVboGZQNdbeANHVK1GJx7ECMfjohkpLqoGLVP9SCQM4bR1F5vh' WHERE "chain" = 'bitcoin';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS "notification_templates" (
|
|
2
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
3
|
+
"name" text NOT NULL,
|
|
4
|
+
"description" text,
|
|
5
|
+
"subject" text NOT NULL,
|
|
6
|
+
"html_body" text NOT NULL,
|
|
7
|
+
"text_body" text NOT NULL,
|
|
8
|
+
"active" boolean DEFAULT true NOT NULL,
|
|
9
|
+
"created_at" bigint DEFAULT (extract(epoch from now()))::bigint NOT NULL,
|
|
10
|
+
"updated_at" bigint DEFAULT (extract(epoch from now()))::bigint NOT NULL,
|
|
11
|
+
CONSTRAINT "notification_templates_name_unique" UNIQUE("name")
|
|
12
|
+
);
|
|
13
|
+
--> statement-breakpoint
|
|
14
|
+
ALTER TABLE "notification_preferences" ADD COLUMN "fleet_updates" boolean DEFAULT true NOT NULL;
|
|
@@ -71,6 +71,20 @@
|
|
|
71
71
|
"when": 1742400000000,
|
|
72
72
|
"tag": "0009_tenant_update_configs",
|
|
73
73
|
"breakpoints": true
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"idx": 10,
|
|
77
|
+
"version": "7",
|
|
78
|
+
"when": 1742486400000,
|
|
79
|
+
"tag": "0010_oracle_address",
|
|
80
|
+
"breakpoints": true
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"idx": 11,
|
|
84
|
+
"version": "7",
|
|
85
|
+
"when": 1742572800000,
|
|
86
|
+
"tag": "0011_notification_templates",
|
|
87
|
+
"breakpoints": true
|
|
74
88
|
}
|
|
75
89
|
]
|
|
76
90
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wopr-network/platform-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -132,6 +132,7 @@
|
|
|
132
132
|
"@scure/base": "^2.0.0",
|
|
133
133
|
"@scure/bip32": "^2.0.1",
|
|
134
134
|
"@scure/bip39": "^2.0.1",
|
|
135
|
+
"handlebars": "^4.7.8",
|
|
135
136
|
"js-yaml": "^4.1.1",
|
|
136
137
|
"viem": "^2.47.4",
|
|
137
138
|
"yaml": "^2.8.2"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { eq, sql } from "drizzle-orm";
|
|
1
|
+
import { and, eq, isNotNull, isNull, sql } from "drizzle-orm";
|
|
2
2
|
import type { PlatformDb } from "../../db/index.js";
|
|
3
3
|
import { cryptoCharges } from "../../db/schema/crypto.js";
|
|
4
4
|
import type { CryptoPaymentState } from "./types.js";
|
|
@@ -43,6 +43,8 @@ export interface ICryptoChargeRepository {
|
|
|
43
43
|
createStablecoinCharge(input: CryptoDepositChargeInput): Promise<void>;
|
|
44
44
|
getByDepositAddress(address: string): Promise<CryptoChargeRecord | null>;
|
|
45
45
|
getNextDerivationIndex(): Promise<number>;
|
|
46
|
+
/** List deposit addresses with pending (uncredited) charges, grouped by chain. */
|
|
47
|
+
listActiveDepositAddresses(): Promise<{ chain: string; address: string }[]>;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
/**
|
|
@@ -154,6 +156,17 @@ export class DrizzleCryptoChargeRepository implements ICryptoChargeRepository {
|
|
|
154
156
|
return this.toRecord(row);
|
|
155
157
|
}
|
|
156
158
|
|
|
159
|
+
/** List deposit addresses with pending (uncredited) charges. */
|
|
160
|
+
async listActiveDepositAddresses(): Promise<{ chain: string; address: string }[]> {
|
|
161
|
+
const rows = await this.db
|
|
162
|
+
.select({ chain: cryptoCharges.chain, address: cryptoCharges.depositAddress })
|
|
163
|
+
.from(cryptoCharges)
|
|
164
|
+
.where(
|
|
165
|
+
and(isNull(cryptoCharges.creditedAt), isNotNull(cryptoCharges.depositAddress), isNotNull(cryptoCharges.chain)),
|
|
166
|
+
);
|
|
167
|
+
return rows.filter((r): r is { chain: string; address: string } => r.chain !== null && r.address !== null);
|
|
168
|
+
}
|
|
169
|
+
|
|
157
170
|
/** Get the next available HD derivation index (max + 1, or 0 if empty). */
|
|
158
171
|
async getNextDerivationIndex(): Promise<number> {
|
|
159
172
|
const result = await this.db
|
|
@@ -13,6 +13,8 @@ export interface PaymentMethodRecord {
|
|
|
13
13
|
enabled: boolean;
|
|
14
14
|
displayOrder: number;
|
|
15
15
|
rpcUrl: string | null;
|
|
16
|
+
oracleAddress: string | null;
|
|
17
|
+
xpub: string | null;
|
|
16
18
|
confirmations: number;
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -76,6 +78,8 @@ export class DrizzlePaymentMethodStore implements IPaymentMethodStore {
|
|
|
76
78
|
enabled: method.enabled,
|
|
77
79
|
displayOrder: method.displayOrder,
|
|
78
80
|
rpcUrl: method.rpcUrl,
|
|
81
|
+
oracleAddress: method.oracleAddress,
|
|
82
|
+
xpub: method.xpub,
|
|
79
83
|
confirmations: method.confirmations,
|
|
80
84
|
})
|
|
81
85
|
.onConflictDoUpdate({
|
|
@@ -90,6 +94,8 @@ export class DrizzlePaymentMethodStore implements IPaymentMethodStore {
|
|
|
90
94
|
enabled: method.enabled,
|
|
91
95
|
displayOrder: method.displayOrder,
|
|
92
96
|
rpcUrl: method.rpcUrl,
|
|
97
|
+
oracleAddress: method.oracleAddress,
|
|
98
|
+
xpub: method.xpub,
|
|
93
99
|
confirmations: method.confirmations,
|
|
94
100
|
},
|
|
95
101
|
});
|
|
@@ -112,6 +118,8 @@ function toRecord(row: typeof paymentMethods.$inferSelect): PaymentMethodRecord
|
|
|
112
118
|
enabled: row.enabled,
|
|
113
119
|
displayOrder: row.displayOrder,
|
|
114
120
|
rpcUrl: row.rpcUrl,
|
|
121
|
+
oracleAddress: row.oracleAddress,
|
|
122
|
+
xpub: row.xpub,
|
|
115
123
|
confirmations: row.confirmations,
|
|
116
124
|
};
|
|
117
125
|
}
|
package/src/db/schema/crypto.ts
CHANGED
|
@@ -61,7 +61,9 @@ export const paymentMethods = pgTable("payment_methods", {
|
|
|
61
61
|
displayName: text("display_name").notNull(),
|
|
62
62
|
enabled: boolean("enabled").notNull().default(true),
|
|
63
63
|
displayOrder: integer("display_order").notNull().default(0),
|
|
64
|
-
rpcUrl: text("rpc_url"), //
|
|
64
|
+
rpcUrl: text("rpc_url"), // chain node RPC endpoint
|
|
65
|
+
oracleAddress: text("oracle_address"), // Chainlink feed address for price (null = 1:1 stablecoin)
|
|
66
|
+
xpub: text("xpub"), // HD wallet extended public key for deposit address derivation
|
|
65
67
|
confirmations: integer("confirmations").notNull().default(1),
|
|
66
68
|
createdAt: text("created_at").notNull().default(sql`(now())`),
|
|
67
69
|
});
|
package/src/db/schema/index.ts
CHANGED
|
@@ -34,6 +34,7 @@ export * from "./node-transitions.js";
|
|
|
34
34
|
export * from "./nodes.js";
|
|
35
35
|
export * from "./notification-preferences.js";
|
|
36
36
|
export * from "./notification-queue.js";
|
|
37
|
+
export * from "./notification-templates.js";
|
|
37
38
|
export * from "./oauth-states.js";
|
|
38
39
|
export * from "./onboarding-scripts.js";
|
|
39
40
|
export * from "./onboarding-sessions.js";
|
|
@@ -15,5 +15,6 @@ export const notificationPreferences = pgTable("notification_preferences", {
|
|
|
15
15
|
agentStatusChanges: boolean("agent_status_changes").notNull().default(false),
|
|
16
16
|
accountRoleChanges: boolean("account_role_changes").notNull().default(true),
|
|
17
17
|
accountTeamInvites: boolean("account_team_invites").notNull().default(true),
|
|
18
|
+
fleetUpdates: boolean("fleet_updates").notNull().default(true),
|
|
18
19
|
updatedAt: bigint("updated_at", { mode: "number" }).notNull().default(sql`(extract(epoch from now()))::bigint`),
|
|
19
20
|
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { bigint, boolean, pgTable, text } from "drizzle-orm/pg-core";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* DB-driven email templates with Handlebars syntax.
|
|
6
|
+
* Each row stores a named template (subject + HTML + text bodies).
|
|
7
|
+
* Admins can edit these at runtime without code deploys.
|
|
8
|
+
*/
|
|
9
|
+
export const notificationTemplates = pgTable("notification_templates", {
|
|
10
|
+
id: text("id").primaryKey(),
|
|
11
|
+
name: text("name").notNull().unique(),
|
|
12
|
+
description: text("description"),
|
|
13
|
+
subject: text("subject").notNull(),
|
|
14
|
+
htmlBody: text("html_body").notNull(),
|
|
15
|
+
textBody: text("text_body").notNull(),
|
|
16
|
+
active: boolean("active").notNull().default(true),
|
|
17
|
+
createdAt: bigint("created_at", { mode: "number" }).notNull().default(sql`(extract(epoch from now()))::bigint`),
|
|
18
|
+
updatedAt: bigint("updated_at", { mode: "number" }).notNull().default(sql`(extract(epoch from now()))::bigint`),
|
|
19
|
+
});
|