Kuma/src/modules/marketing/lib/personal-code.ts
2026-10-02 17:31:08 +02:00

65 lines
2.2 KiB
TypeScript

import { randomInt } from "crypto";
import { Prisma } from "@/src/generated/prisma/client";
import { prisma } from "@/src/lib/prisma";
import { berlinDayEnd } from "@/src/modules/courses/lib/course-days";
import type { Reward, TriggerKind } from "./node-config";
// Ohne 0/O und 1/I, damit niemand den Code beim Abtippen verwechselt.
const ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
const PREFIX: Record<TriggerKind, string> = {
birthday: "GEBURTSTAG",
milestone: "DANKE",
winback: "COMEBACK",
};
export function generateCodeString(kind: TriggerKind): string {
let suffix = "";
for (let i = 0; i < 6; i++) suffix += ALPHABET[randomInt(ALPHABET.length)];
return `${PREFIX[kind]}-${suffix}`;
}
/**
* Legt den persönlichen Rabattcode für eine Marketing-Mail an: einmal
* einlösbar, nur für dieses Konto, gültig bis einschließlich `validUntilDay`
* (Berliner Tag). Er erscheint nicht auf der Rabattcode-Seite (origin
* "marketing"), sondern im Verlauf des Marketings.
*/
export async function createPersonalDiscountCode(params: {
businessId: string;
userId: string;
triggerKind: TriggerKind;
reward: Reward;
validUntilDay: string;
}): Promise<{ id: string; code: string; expiresAt: Date }> {
const expiresAt = berlinDayEnd(params.validUntilDay);
// Kollisionen sind bei 32^6 Möglichkeiten pro Präfix selten - ein paar
// Versuche reichen.
for (let attempt = 1; ; attempt++) {
try {
return await prisma.discountCode
.create({
data: {
businessId: params.businessId,
code: generateCodeString(params.triggerKind),
type: params.reward.type,
value: params.reward.value,
appliesTo: params.reward.appliesTo,
scope: "all_courses",
courseIds: [],
maxRedemptions: 1,
expiresAt,
restrictedToUserId: params.userId,
origin: "marketing",
},
select: { id: true, code: true, expiresAt: true },
})
.then((code) => ({ ...code, expiresAt: code.expiresAt ?? expiresAt }));
} catch (error) {
const isCollision =
error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002";
if (!isCollision || attempt >= 5) throw error;
}
}
}