Skip to content
Snippets Groups Projects
Commit 72194611 authored by Aaron Dötsch's avatar Aaron Dötsch
Browse files

Implement notification system

Many users requested to be able to get some kind of message when articles are bought. This commit
is the base for notifying users about several different actions, e.g. buying articles or
depositing money.
parent 63679a00
No related branches found
No related tags found
No related merge requests found
-- CreateTable
CREATE TABLE "NotificationChannel" (
"id" SERIAL NOT NULL,
"userId" INTEGER NOT NULL,
"channelType" INTEGER NOT NULL,
"notificationTypes" INTEGER NOT NULL,
"recipient" TEXT NOT NULL,
CONSTRAINT "NotificationChannel_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "NotificationChannel" ADD CONSTRAINT "NotificationChannel_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
......@@ -23,6 +23,7 @@ model User {
moneyTransfersSent MoneyTransfer[] @relation("from")
moneyTransfersReceived MoneyTransfer[] @relation("to")
vouchersUsed Voucher[] @relation("vouchersUsed")
notificationChannels NotificationChannel[]
updatedAt DateTime @updatedAt
comment String @default("")
}
......@@ -113,3 +114,12 @@ model Voucher {
usedById Int?
usedBy User? @relation("vouchersUsed", fields: [usedById], references: [id])
}
model NotificationChannel {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id])
channelType Int
notificationTypes Int
recipient String
}
import { handleWebhook } from "./webhookHandler";
import { handleEmail } from "./emailHandler";
/**
* @type {{
* WEBHOOK: import("./types").ChannelType<?>,
* EMAIL: import("./types").ChannelType<?>
* }}
*/
export const ChannelType = {
WEBHOOK: {key: 1<<0, handler: handleWebhook},
EMAIL: {key: 1<<1, handler: handleEmail},
};
import { formatMessage, getMessageTitle } from "./formatter";
import type { HandlerReturnType, NotificationData, NotificationType, PossibleNotificationType } from "./types";
export async function handleEmail<T extends PossibleNotificationType>(recipient: string, type: NotificationType<T>, data: NotificationData<T>): Promise<HandlerReturnType> {
const emailTitle = getMessageTitle(type);
const emailBody = formatMessage(type, data);
return Promise.resolve(); // TODO actually send email
};
import type { NotificationData, NotificationType, PossibleNotificationType } from "./types";
export function getMessageTitle<T extends PossibleNotificationType>(type: NotificationType<T>): string {
switch(type.name){
case "buy": {
return "Dein Einkauf";
}
case "refund": {
return "Geld erstattet";
}
case "deposit": {
return "Geld eingezahlt";
}
case "withdraw": {
return "Geld ausgezahlt";
}
case "use-voucher": {
return "Gutschein eingelöst";
}
case "send-transfer": {
return "Überweisung gesendet";
}
case "receive-transfer": {
return "Überweisung erhalten";
}
default: {
console.error(`Unknown notification type`, type);
return `Unknown notification type ${type}`;
}
}
}
export function formatMessage<T extends PossibleNotificationType>(type: NotificationType<T>, data: NotificationData<T>): string {
switch(type.name){
case "buy": {
const {total, items, balanceBefore, balanceAfter} = data as NotificationData<"buy">;
const premiums = items.filter(item => item.premium && item.premium > 0).map(item => item.premium).reduce((a,b) => a+b, 0);
return `Du hast folgende ${items.length} Artikel gekauft:
${items.map(item => `- ${item.name} (${item.code}) - ${(item.price/100).toFixed(2)}€` + (item.premium && item.premium > 0 ? ` (+${(item.premium/100).toFixed(2)}€)` : "")).join("\n")}
Gesamt: ${(total/100).toFixed(2)}€` + (premiums > 0 ? ` (davon ${(premiums/100).toFixed(2)}€ Negativaufschlag)` : "") + `\nDein neuer Kontostand beträgt ${(balanceAfter/100).toFixed(2)}€.`;
}
case "refund": {
const {refund, item, balanceBefore, balanceAfter, timeBought} = data as NotificationData<"refund">;
return `Dir wurden ${(refund/100).toFixed(2)}€ für ${item.name} (${item.code}) erstattet, gekauft am ${timeBought.toLocaleString()}.
Dein neuer Kontostand beträgt ${(balanceAfter/100).toFixed(2)}€.`;
}
case "deposit": {
const {amount, balanceBefore, balanceAfter} = data as NotificationData<"deposit">;
return `Du hast ${(amount/100).toFixed(2)}€ eingezahlt.
Dein neuer Kontostand beträgt ${(balanceAfter/100).toFixed(2)}€.`;
}
case "withdraw": {
const {amount, balanceBefore, balanceAfter} = data as NotificationData<"withdraw">;
return `Du hast ${(amount/100).toFixed(2)}€ ausgezahlt.
Dein neuer Kontostand beträgt ${(balanceAfter/100).toFixed(2)}€.`;
}
case "use-voucher": {
const {voucher, balanceBefore, balanceAfter} = data as NotificationData<"use-voucher">;
return `Du hast einen Gutschein im Wert von ${(voucher.value/100).toFixed(2)}€ eingelöst.
Dein neuer Kontostand beträgt ${(balanceAfter/100).toFixed(2)}€.`;
}
case "send-transfer": {
const {amount, balanceBefore, balanceAfter, receiver} = data as NotificationData<"send-transfer">;
return `Du hast ${(amount/100).toFixed(2)}€ an ${receiver.name} überwiesen.
Dein neuer Kontostand beträgt ${(balanceAfter/100).toFixed(2)}€.`;
}
case "receive-transfer": {
const {amount, balanceBefore, balanceAfter, sender} = data as NotificationData<"receive-transfer">;
return `Du hast ${(amount/100).toFixed(2)}€ von ${sender.name} erhalten.
Dein neuer Kontostand beträgt ${(balanceAfter/100).toFixed(2)}€.`;
}
default: {
console.error(`Unknown notification type`, type);
return `Unknown notification type ${type}`;
}
}
}
import { db } from "$lib/server/database";
import { ChannelType } from "./channelTypes";
import type { HandlerReturnType, NotificationChannel, NotificationData, NotificationType, PossibleNotificationType } from "./types";
export async function sendNotification<T extends PossibleNotificationType>(user: { id: number, notificationChannels: NotificationChannel[] | undefined } | number, type: NotificationType<T>, data: NotificationData<T>): Promise<HandlerReturnType[]>{
let notificationChannels: NotificationChannel[];
let userId: number;
if(typeof user === "number"){
userId = user;
}else{
userId = user.id;
notificationChannels = user.notificationChannels?.filter(channel => channel.notificationTypes & type.key);
}
if(!notificationChannels){
notificationChannels = await getNotificationChannels(userId, type.key);
}
return Promise.all(notificationChannels.map(channel => {
switch(channel.channelType){
case ChannelType.EMAIL.key: {
return ChannelType.EMAIL.handler(channel.recipient, type, data);
}
case ChannelType.WEBHOOK.key: {
return ChannelType.WEBHOOK.handler(channel.recipient, type, data);
}
default: {
console.error(`Unknown channel type`, channel.channelType);
return Promise.resolve();
}
};
}));
};
async function getNotificationChannels(userId: number, notificationType: number): Promise<NotificationChannel[]> {
const allChannels = await db.notificationChannel.findMany({
where: { userId }
});
return allChannels.filter(channel => channel.notificationTypes & notificationType);
}
/** @type {{
* BUY: import("./types").NotificationType<"buy">,
* REFUND: import("./types").NotificationType<"refund">,
* DEPOSIT: import("./types").NotificationType<"deposit">,
* WITHDRAW: import("./types").NotificationType<"withdraw">,
* USE_VOUCHER: import("./types").NotificationType<"use-voucher">,
* SEND_TRANSFER: import("./types").NotificationType<"send-transfer">,
* RECEIVE_TRANSFER: import("./types").NotificationType<"receive-transfer">,
* }}
*/
export const NotificationType = {
BUY: {key: 1<<0, name: "buy"},
REFUND: {key: 1<<1, name: "refund"},
DEPOSIT: {key: 1<<2, name: "deposit"},
WITHDRAW: {key: 1<<3, name: "withdraw"},
USE_VOUCHER: {key: 1<<4, name: "use-voucher"},
SEND_TRANSFER: {key: 1<<5, name: "send-transfer"},
RECEIVE_TRANSFER: {key: 1<<6, name: "receive-transfer"},
};
export type PossibleNotificationType = "buy" | "refund" | "deposit" | "withdraw" | "use-voucher" | "send-transfer" | "receive-transfer";
export type Item = {name: string, code: string, price: number, premium: number|undefined};
export type User = {name: string, id: number}
export type NotificationType<T extends PossibleNotificationType> = {key: number, name: T};
export type BuyNotificationData = {total: number, items: Item[], balanceBefore: number, balanceAfter: number};
export type RefundNotificationData = {refund: number, item: Item, balanceBefore: number, balanceAfter: number, timeBought: Date};
export type DepositNotificationData = {amount: number, balanceBefore: number, balanceAfter: number};
export type WithdrawNotificationData = {amount: number, balanceBefore: number, balanceAfter: number};
export type UseVoucherNotificationData = {voucher: {code: string, value: number}, balanceBefore: number, balanceAfter: number};
export type SendTransferNotificationData = {amount: number, receiver: User, balanceBefore: number, balanceAfter: number};
export type ReceiveTransferNotificationData = {amount: number, sender: User, balanceBefore: number, balanceAfter: number};
export type NotificationData<T extends PossibleNotificationType> =
T extends "buy" ? BuyNotificationData :
T extends "refund" ? RefundNotificationData :
T extends "deposit" ? DepositNotificationData :
T extends "withdraw" ? WithdrawNotificationData :
T extends "use-voucher" ? UseVoucherNotificationData :
T extends "send-transfer" ? SendTransferNotificationData :
T extends "receive-transfer" ? ReceiveTransferNotificationData :
never;
export type HandlerReturnType = any;
export type NotificationHandler<T extends PossibleNotificationType> = (recipient: string, type: NotificationType<T>, data: NotificationData<T>) => Promise<HandlerReturnType>;
export type ChannelType<T extends PossibleNotificationType> = {key: number, handler: NotificationHandler<T>};
export type NotificationChannel = {
id: number,
userId: number,
channelType: number,
notificationTypes: number,
recipient: string
};
export type GenericNotificationHandler<T extends PossibleNotificationType> = (user: {id:number,notificationChannels:NotificationChannel[]|undefined}|number, type: NotificationType<T>, data: NotificationData<T>) => Promise<any>;
import { formatMessage } from "./formatter";
import type { HandlerReturnType, NotificationData, NotificationType, PossibleNotificationType } from "./types";
export async function handleWebhook<T extends PossibleNotificationType>(recipient: string, type: NotificationType<T>, data: NotificationData<T>): Promise<HandlerReturnType> {
const body = JSON.stringify(Object.assign({}, data, {type: type.name, content: formatMessage(type, data)}));
const headers = {
"Content-Type": "application/json"
};
console.log({recipient, type, data, body});
return fetch(recipient, {
method: "POST",
body,
headers
});
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment