- Add notification router (sendEmail, sendPush, sendSMS, device mgmt, prefs) - Create provider clients: Resend, Firebase Admin (FCM), Twilio - Add notification_preferences table to Drizzle schema - Create branded email templates (welcome, alert, password reset, family invite, billing) - Implement notification service with error handling and E.164 validation - Wire router into app root - Write unit tests with mocked providers (25 tests passing) - Add resend, firebase-admin, twilio dependencies
101 lines
2.4 KiB
TypeScript
101 lines
2.4 KiB
TypeScript
import { wrap } from "@typeschema/valibot";
|
|
import {
|
|
object,
|
|
string,
|
|
optional,
|
|
record,
|
|
boolean,
|
|
picklist,
|
|
} from "valibot";
|
|
import { createTRPCRouter, protectedProcedure, adminProcedure } from "../utils";
|
|
import {
|
|
sendEmail,
|
|
sendPush,
|
|
sendSMS,
|
|
registerDevice,
|
|
unregisterDevice,
|
|
listDevices,
|
|
getPreferences,
|
|
updatePreferences,
|
|
} from "~/server/services/notification.service";
|
|
|
|
const SendEmailSchema = object({
|
|
to: string(),
|
|
subject: string(),
|
|
html: string(),
|
|
text: optional(string()),
|
|
});
|
|
|
|
const SendPushSchema = object({
|
|
title: string(),
|
|
body: string(),
|
|
data: optional(record(string(), string())),
|
|
});
|
|
|
|
const SendSMSSchema = object({
|
|
phoneNumber: string(),
|
|
message: string(),
|
|
});
|
|
|
|
const RegisterDeviceSchema = object({
|
|
token: string(),
|
|
platform: picklist(["ios", "android", "web"]),
|
|
deviceType: picklist(["mobile", "web", "desktop"]),
|
|
});
|
|
|
|
const UnregisterDeviceSchema = object({
|
|
token: string(),
|
|
});
|
|
|
|
const UpdatePreferencesSchema = object({
|
|
emailEnabled: optional(boolean()),
|
|
pushEnabled: optional(boolean()),
|
|
smsEnabled: optional(boolean()),
|
|
});
|
|
|
|
export const notificationRouter = createTRPCRouter({
|
|
sendEmail: adminProcedure
|
|
.input(wrap(SendEmailSchema))
|
|
.mutation(async ({ input }) => {
|
|
return sendEmail(input.to, input.subject, input.html, input.text);
|
|
}),
|
|
|
|
sendPush: protectedProcedure
|
|
.input(wrap(SendPushSchema))
|
|
.mutation(async ({ ctx, input }) => {
|
|
return sendPush(ctx.user.id, input.title, input.body, input.data);
|
|
}),
|
|
|
|
sendSMS: protectedProcedure
|
|
.input(wrap(SendSMSSchema))
|
|
.mutation(async ({ input }) => {
|
|
return sendSMS(input.phoneNumber, input.message);
|
|
}),
|
|
|
|
registerDevice: protectedProcedure
|
|
.input(wrap(RegisterDeviceSchema))
|
|
.mutation(async ({ ctx, input }) => {
|
|
return registerDevice(ctx.user.id, input.token, input.platform, input.deviceType);
|
|
}),
|
|
|
|
unregisterDevice: protectedProcedure
|
|
.input(wrap(UnregisterDeviceSchema))
|
|
.mutation(async ({ ctx, input }) => {
|
|
return unregisterDevice(ctx.user.id, input.token);
|
|
}),
|
|
|
|
listDevices: protectedProcedure.query(async ({ ctx }) => {
|
|
return listDevices(ctx.user.id);
|
|
}),
|
|
|
|
getPreferences: protectedProcedure.query(async ({ ctx }) => {
|
|
return getPreferences(ctx.user.id);
|
|
}),
|
|
|
|
updatePreferences: protectedProcedure
|
|
.input(wrap(UpdatePreferencesSchema))
|
|
.mutation(async ({ ctx, input }) => {
|
|
return updatePreferences(ctx.user.id, input);
|
|
}),
|
|
});
|