fix:manage edit for extra bezoekers payments
This commit is contained in:
@@ -1,11 +1,18 @@
|
||||
import { env } from "@kk/env/server";
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
function createTransport() {
|
||||
// Singleton transport — created once per module, reused across all email sends.
|
||||
// Re-creating it on every call causes EventEmitter listener accumulation in
|
||||
// long-lived Cloudflare Worker processes, triggering memory leak warnings.
|
||||
let _transport: nodemailer.Transporter | null | undefined;
|
||||
|
||||
function getTransport(): nodemailer.Transporter | null {
|
||||
if (_transport !== undefined) return _transport;
|
||||
if (!env.SMTP_HOST || !env.SMTP_USER || !env.SMTP_PASS) {
|
||||
_transport = null;
|
||||
return null;
|
||||
}
|
||||
return nodemailer.createTransport({
|
||||
_transport = nodemailer.createTransport({
|
||||
host: env.SMTP_HOST,
|
||||
port: env.SMTP_PORT,
|
||||
secure: env.SMTP_PORT === 465,
|
||||
@@ -14,6 +21,7 @@ function createTransport() {
|
||||
pass: env.SMTP_PASS,
|
||||
},
|
||||
});
|
||||
return _transport;
|
||||
}
|
||||
|
||||
const from = env.SMTP_FROM ?? "Kunstenkamp <info@kunstenkamp.be>";
|
||||
@@ -223,7 +231,7 @@ export async function sendConfirmationEmail(params: {
|
||||
wantsToPerform: boolean;
|
||||
artForm?: string | null;
|
||||
}) {
|
||||
const transport = createTransport();
|
||||
const transport = getTransport();
|
||||
if (!transport) {
|
||||
console.warn("SMTP not configured — skipping confirmation email");
|
||||
return;
|
||||
@@ -249,7 +257,7 @@ export async function sendUpdateEmail(params: {
|
||||
wantsToPerform: boolean;
|
||||
artForm?: string | null;
|
||||
}) {
|
||||
const transport = createTransport();
|
||||
const transport = getTransport();
|
||||
if (!transport) {
|
||||
console.warn("SMTP not configured — skipping update email");
|
||||
return;
|
||||
@@ -272,7 +280,7 @@ export async function sendCancellationEmail(params: {
|
||||
to: string;
|
||||
firstName: string;
|
||||
}) {
|
||||
const transport = createTransport();
|
||||
const transport = getTransport();
|
||||
if (!transport) {
|
||||
console.warn("SMTP not configured — skipping cancellation email");
|
||||
return;
|
||||
|
||||
@@ -172,11 +172,28 @@ export const appRouter = {
|
||||
.handler(async ({ input }) => {
|
||||
const row = await getActiveRegistration(input.token);
|
||||
|
||||
// If already paid and switching to a watcher with fewer guests, we
|
||||
// still allow the update — payment adjustments are handled manually.
|
||||
const isPerformer = input.registrationType === "performer";
|
||||
const guests = isPerformer ? [] : (input.guests ?? []);
|
||||
|
||||
// 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";
|
||||
const oldGuestCount = isPerformer
|
||||
? 0
|
||||
: parseGuestsJson(row.guests).length;
|
||||
const newGuestCount = guests.length;
|
||||
const extraGuests =
|
||||
!isPerformer && isPaid ? newGuestCount - oldGuestCount : 0;
|
||||
|
||||
// Determine the new paymentStatus and paymentAmount (delta in cents).
|
||||
// Only flag extra_payment_pending when the watcher genuinely owes more.
|
||||
const newPaymentStatus =
|
||||
isPaid && extraGuests > 0 ? "extra_payment_pending" : row.paymentStatus;
|
||||
const newPaymentAmount =
|
||||
newPaymentStatus === "extra_payment_pending"
|
||||
? extraGuests * 2 * 100 // €2 per extra guest, in cents
|
||||
: row.paymentAmount;
|
||||
|
||||
await db
|
||||
.update(registration)
|
||||
.set({
|
||||
@@ -191,6 +208,8 @@ export const appRouter = {
|
||||
drinkCardValue: isPerformer ? 0 : drinkCardEuros(guests.length),
|
||||
guests: guests.length > 0 ? JSON.stringify(guests) : null,
|
||||
extraQuestions: input.extraQuestions || null,
|
||||
paymentStatus: newPaymentStatus,
|
||||
paymentAmount: newPaymentAmount,
|
||||
})
|
||||
.where(eq(registration.managementToken, input.token));
|
||||
|
||||
@@ -202,9 +221,6 @@ export const appRouter = {
|
||||
artForm: input.artForm,
|
||||
}).catch((err) => console.error("Failed to send update email:", err));
|
||||
|
||||
// Satisfy the linter — row is used to confirm the record existed.
|
||||
void row;
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
@@ -540,11 +556,27 @@ export const appRouter = {
|
||||
if (!row) throw new Error("Inschrijving niet gevonden");
|
||||
if (row.paymentStatus === "paid")
|
||||
throw new Error("Betaling is al voltooid");
|
||||
if (
|
||||
row.paymentStatus !== "pending" &&
|
||||
row.paymentStatus !== "extra_payment_pending"
|
||||
)
|
||||
throw new Error("Onverwachte betalingsstatus");
|
||||
if (row.registrationType === "performer")
|
||||
throw new Error("Artiesten hoeven niet te betalen");
|
||||
|
||||
const guests = parseGuestsJson(row.guests);
|
||||
const amountInCents = drinkCardCents(guests.length);
|
||||
|
||||
// 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.
|
||||
const isExtraPayment = row.paymentStatus === "extra_payment_pending";
|
||||
const amountInCents = isExtraPayment
|
||||
? (row.paymentAmount ?? 0)
|
||||
: drinkCardCents(guests.length);
|
||||
|
||||
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)`;
|
||||
|
||||
const response = await fetch(
|
||||
"https://api.lemonsqueezy.com/v1/checkouts",
|
||||
@@ -562,7 +594,7 @@ export const appRouter = {
|
||||
custom_price: amountInCents,
|
||||
product_options: {
|
||||
name: "Kunstenkamp Evenement",
|
||||
description: `Toegangskaart voor ${1 + guests.length} perso(o)n(en)`,
|
||||
description: productDescription,
|
||||
redirect_url: `${env.CORS_ORIGIN}/manage/${input.token}`,
|
||||
},
|
||||
checkout_data: {
|
||||
|
||||
@@ -7,7 +7,7 @@ config({ path: "../env/.env" });
|
||||
|
||||
const app = await alchemy("kk");
|
||||
|
||||
// Helper function to get required env var
|
||||
/** Throws at deploy time if a required variable is missing from the environment. */
|
||||
function getEnvVar(name: string): string {
|
||||
const value = process.env[name];
|
||||
if (!value) {
|
||||
@@ -19,15 +19,22 @@ function getEnvVar(name: string): string {
|
||||
export const web = await TanStackStart("web", {
|
||||
cwd: "../../apps/web",
|
||||
bindings: {
|
||||
// Core
|
||||
DATABASE_URL: getEnvVar("DATABASE_URL"),
|
||||
CORS_ORIGIN: getEnvVar("CORS_ORIGIN"),
|
||||
BETTER_AUTH_SECRET: getEnvVar("BETTER_AUTH_SECRET"),
|
||||
BETTER_AUTH_URL: getEnvVar("BETTER_AUTH_URL"),
|
||||
// Email (SMTP)
|
||||
SMTP_HOST: getEnvVar("SMTP_HOST"),
|
||||
SMTP_PORT: getEnvVar("SMTP_PORT"),
|
||||
SMTP_USER: getEnvVar("SMTP_USER"),
|
||||
SMTP_PASS: getEnvVar("SMTP_PASS"),
|
||||
SMTP_FROM: getEnvVar("SMTP_FROM"),
|
||||
// Payments (Lemon Squeezy)
|
||||
LEMON_SQUEEZY_API_KEY: getEnvVar("LEMON_SQUEEZY_API_KEY"),
|
||||
LEMON_SQUEEZY_STORE_ID: getEnvVar("LEMON_SQUEEZY_STORE_ID"),
|
||||
LEMON_SQUEEZY_VARIANT_ID: getEnvVar("LEMON_SQUEEZY_VARIANT_ID"),
|
||||
LEMON_SQUEEZY_WEBHOOK_SECRET: getEnvVar("LEMON_SQUEEZY_WEBHOOK_SECRET"),
|
||||
},
|
||||
domains: ["kunstenkamp.be", "www.kunstenkamp.be"],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user