feat:gifts

This commit is contained in:
2026-03-03 16:26:37 +01:00
parent e7b3d7260f
commit b8cefd373d
10 changed files with 416 additions and 22 deletions

View File

@@ -4,7 +4,7 @@ import { adminRequest, registration } from "@kk/db/schema";
import { user } from "@kk/db/schema/auth";
import { env } from "@kk/env/server";
import type { RouterClient } from "@orpc/server";
import { and, count, desc, eq, gte, isNull, like, lte } from "drizzle-orm";
import { and, count, desc, eq, gte, isNull, like, lte, sum } from "drizzle-orm";
import { z } from "zod";
import {
sendCancellationEmail,
@@ -84,6 +84,7 @@ const coreRegistrationFields = {
isOver16: z.boolean().optional(),
guests: z.array(guestSchema).max(9).optional(),
extraQuestions: z.string().optional(),
giftAmount: z.number().int().min(0).default(0),
};
const submitRegistrationSchema = z.object(coreRegistrationFields);
@@ -135,6 +136,7 @@ export const appRouter = {
drinkCardValue: isPerformer ? 0 : drinkCardEuros(guests.length),
guests: guests.length > 0 ? JSON.stringify(guests) : null,
extraQuestions: input.extraQuestions || null,
giftAmount: input.giftAmount,
managementToken,
});
@@ -144,6 +146,8 @@ export const appRouter = {
managementToken,
wantsToPerform: isPerformer,
artForm: input.artForm,
giftAmount: input.giftAmount,
drinkCardValue: isPerformer ? 0 : drinkCardEuros(guests.length),
}).catch((err) =>
console.error("Failed to send confirmation email:", err),
);
@@ -175,6 +179,17 @@ export const appRouter = {
const isPerformer = input.registrationType === "performer";
const guests = isPerformer ? [] : (input.guests ?? []);
// Validate gift amount cannot be changed after payment
const hasPaidGift =
(row.paymentStatus === "paid" ||
row.paymentStatus === "extra_payment_pending") &&
(row.giftAmount ?? 0) > 0;
if (hasPaidGift && input.giftAmount !== row.giftAmount) {
throw new Error(
"Gift kan niet worden aangepast na betaling. Neem contact op met de organisatie.",
);
}
// Detect whether an already-paid watcher is adding extra guests so we
// can charge them the delta instead of the full amount.
const isPaid = row.paymentStatus === "paid";
@@ -208,6 +223,7 @@ export const appRouter = {
drinkCardValue: isPerformer ? 0 : drinkCardEuros(guests.length),
guests: guests.length > 0 ? JSON.stringify(guests) : null,
extraQuestions: input.extraQuestions || null,
giftAmount: input.giftAmount,
paymentStatus: newPaymentStatus,
paymentAmount: newPaymentAmount,
})
@@ -219,6 +235,8 @@ export const appRouter = {
managementToken: input.token,
wantsToPerform: isPerformer,
artForm: input.artForm,
giftAmount: input.giftAmount,
drinkCardValue: isPerformer ? 0 : drinkCardEuros(guests.length),
}).catch((err) => console.error("Failed to send update email:", err));
return { success: true };
@@ -300,7 +318,7 @@ export const appRouter = {
const today = new Date();
today.setHours(0, 0, 0, 0);
const [totalResult, todayResult, artFormResult, typeResult] =
const [totalResult, todayResult, artFormResult, typeResult, giftResult] =
await Promise.all([
db.select({ count: count() }).from(registration),
db
@@ -319,6 +337,7 @@ export const appRouter = {
})
.from(registration)
.groupBy(registration.registrationType),
db.select({ total: sum(registration.giftAmount) }).from(registration),
]);
return {
@@ -332,6 +351,7 @@ export const appRouter = {
registrationType: r.registrationType,
count: r.count,
})),
totalGiftRevenue: giftResult[0]?.total ?? 0,
};
}),
@@ -352,6 +372,7 @@ export const appRouter = {
"Experience",
"Is Over 16",
"Drink Card Value",
"Gift Amount",
"Guest Count",
"Guests",
"Payment Status",
@@ -379,6 +400,7 @@ export const appRouter = {
r.experience || "",
r.isOver16 ? "Yes" : "No",
String(r.drinkCardValue ?? 0),
String(r.giftAmount ?? 0),
String(guests.length),
guestSummary,
r.paymentStatus === "paid" ? "Paid" : "Pending",
@@ -561,22 +583,32 @@ export const appRouter = {
row.paymentStatus !== "extra_payment_pending"
)
throw new Error("Onverwachte betalingsstatus");
if (row.registrationType === "performer")
throw new Error("Artiesten hoeven niet te betalen");
const isPerformer = row.registrationType === "performer";
const guests = parseGuestsJson(row.guests);
const giftTotal = row.giftAmount ?? 0;
// Performers only pay if they have a gift; watchers pay drink card + gift
if (isPerformer && giftTotal === 0) {
throw new Error("Artiesten hoeven niet te betalen");
}
// For an extra_payment_pending registration, charge only the delta
// (stored in paymentAmount in cents). For a fresh pending registration
// charge the full drink card price.
// charge the full drink card price. Always add any gift amount.
const isExtraPayment = row.paymentStatus === "extra_payment_pending";
const amountInCents = isExtraPayment
const drinkCardTotal = isExtraPayment
? (row.paymentAmount ?? 0)
: drinkCardCents(guests.length);
: isPerformer
? 0
: drinkCardCents(guests.length);
const amountInCents = drinkCardTotal + giftTotal;
const productDescription = isExtraPayment
? `Extra bijdrage voor ${row.paymentAmount != null ? row.paymentAmount / 200 : 0} extra gast(en)`
: `Toegangskaart voor ${1 + guests.length} perso(o)n(en)`;
? `Extra bijdrage${giftTotal > 0 ? " + gift" : ""}`
: isPerformer
? `Vrijwillige gift${drinkCardTotal > 0 ? " + drinkkaart" : ""}`
: `Toegangskaart${giftTotal > 0 ? " + gift" : ""}`;
const response = await fetch(
"https://api.lemonsqueezy.com/v1/checkouts",