feat:switch back to lemonsqueezy

This commit is contained in:
2026-03-07 02:28:03 +01:00
parent 2f0dc46469
commit ac466a7f0e
9 changed files with 237 additions and 153 deletions

View File

@@ -134,49 +134,82 @@ export const drinkkaartRouter = {
.handler(async ({ input, context }) => {
const { env: serverEnv, session } = context;
if (!serverEnv.MOLLIE_API_KEY) {
if (
!serverEnv.LEMON_SQUEEZY_API_KEY ||
!serverEnv.LEMON_SQUEEZY_STORE_ID ||
!serverEnv.LEMON_SQUEEZY_VARIANT_ID
) {
throw new ORPCError("INTERNAL_SERVER_ERROR", {
message: "Mollie is niet geconfigureerd",
message: "LemonSqueezy is niet geconfigureerd",
});
}
const card = await getOrCreateDrinkkaart(session.user.id);
// Mollie amounts must be formatted as "10.00" (string, 2 decimal places)
const amountValue = (input.amountCents / 100).toFixed(2);
const response = await fetch("https://api.mollie.com/v2/payments", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${serverEnv.MOLLIE_API_KEY}`,
},
body: JSON.stringify({
amount: { value: amountValue, currency: "EUR" },
description: `Drinkkaart Opladen — ${formatCents(input.amountCents)}`,
redirectUrl: `${serverEnv.BETTER_AUTH_URL}/account?topup=success`,
webhookUrl: `${serverEnv.BETTER_AUTH_URL}/api/webhook/mollie`,
locale: "nl_NL",
metadata: {
type: "drinkkaart_topup",
drinkkaartId: card.id,
userId: session.user.id,
const response = await fetch(
"https://api.lemonsqueezy.com/v1/checkouts",
{
method: "POST",
headers: {
Accept: "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
Authorization: `Bearer ${serverEnv.LEMON_SQUEEZY_API_KEY}`,
},
}),
});
body: JSON.stringify({
data: {
type: "checkouts",
attributes: {
custom_price: input.amountCents,
product_options: {
name: "Drinkkaart Opladen",
description: `Opwaardering van ${formatCents(input.amountCents)}`,
redirect_url: `${serverEnv.BETTER_AUTH_URL}/account?topup=success`,
},
checkout_data: {
email: session.user.email,
name: session.user.name,
custom: {
type: "drinkkaart_topup",
drinkkaartId: card.id,
userId: session.user.id,
},
},
checkout_options: {
embed: false,
locale: "nl",
},
},
relationships: {
store: {
data: {
type: "stores",
id: serverEnv.LEMON_SQUEEZY_STORE_ID,
},
},
variant: {
data: {
type: "variants",
id: serverEnv.LEMON_SQUEEZY_VARIANT_ID,
},
},
},
},
}),
},
);
if (!response.ok) {
const errorData = await response.json();
console.error("Mollie Drinkkaart checkout error:", errorData);
console.error("LemonSqueezy Drinkkaart checkout error:", errorData);
throw new ORPCError("INTERNAL_SERVER_ERROR", {
message: "Kon checkout niet aanmaken",
});
}
const data = (await response.json()) as {
_links?: { checkout?: { href?: string } };
data?: { attributes?: { url?: string } };
};
const checkoutUrl = data._links?.checkout?.href;
const checkoutUrl = data.data?.attributes?.url;
if (!checkoutUrl) {
throw new ORPCError("INTERNAL_SERVER_ERROR", {
message: "Geen checkout URL ontvangen",

View File

@@ -50,7 +50,7 @@ function parseGuestsJson(raw: string | null): Array<{
* Credits a watcher's drinkCardValue to their drinkkaart account.
*
* Safe to call multiple times — the `drinkkaartCreditedAt IS NULL` guard and the
* `mollie_payment_id` UNIQUE constraint together prevent double-crediting even
* `lemonsqueezy_order_id` UNIQUE constraint together prevent double-crediting even
* if called concurrently (webhook + signup racing each other).
*
* Returns a status string describing the outcome.
@@ -160,11 +160,11 @@ export async function creditRegistrationToAccount(
return { credited: false, amountCents: 0, status: "already_credited" };
}
// Record the topup. Re-use the registration's molliePaymentId so the
// Record the topup. Re-use the registration's lemonsqueezyOrderId so the
// topup row is clearly linked to the original payment. We prefix with
// "reg_" to distinguish from direct drinkkaart top-up orders if needed.
const topupPaymentId = reg.molliePaymentId
? `reg_${reg.molliePaymentId}`
const topupPaymentId = reg.lemonsqueezyOrderId
? `reg_${reg.lemonsqueezyOrderId}`
: null;
await db.insert(drinkkaartTopup).values({
@@ -175,7 +175,7 @@ export async function creditRegistrationToAccount(
balanceBefore,
balanceAfter,
type: "payment",
molliePaymentId: topupPaymentId,
lemonsqueezyOrderId: topupPaymentId,
adminId: null,
reason: "Drinkkaart bij registratie",
paidAt: reg.paidAt ?? new Date(),
@@ -741,8 +741,12 @@ export const appRouter = {
}),
)
.handler(async ({ input }) => {
if (!env.MOLLIE_API_KEY) {
throw new Error("Mollie is niet geconfigureerd");
if (
!env.LEMON_SQUEEZY_API_KEY ||
!env.LEMON_SQUEEZY_STORE_ID ||
!env.LEMON_SQUEEZY_VARIANT_ID
) {
throw new Error("LemonSqueezy is niet geconfigureerd");
}
const rows = await db
@@ -795,37 +799,66 @@ export const appRouter = {
const redirectUrl =
input.redirectUrl ?? `${env.CORS_ORIGIN}/manage/${input.token}`;
// Mollie amounts must be formatted as "10.00" (string, 2 decimal places)
const amountValue = (amountInCents / 100).toFixed(2);
const webhookUrl = `${env.CORS_ORIGIN}/api/webhook/mollie`;
const response = await fetch("https://api.mollie.com/v2/payments", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${env.MOLLIE_API_KEY}`,
const response = await fetch(
"https://api.lemonsqueezy.com/v1/checkouts",
{
method: "POST",
headers: {
Accept: "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
Authorization: `Bearer ${env.LEMON_SQUEEZY_API_KEY}`,
},
body: JSON.stringify({
data: {
type: "checkouts",
attributes: {
custom_price: amountInCents,
product_options: {
name: "Kunstenkamp Evenement",
description: productDescription,
redirect_url: redirectUrl,
},
checkout_data: {
email: row.email,
name: `${row.firstName} ${row.lastName}`,
custom: {
registration_token: input.token,
},
},
checkout_options: {
embed: false,
locale: "nl",
},
},
relationships: {
store: {
data: {
type: "stores",
id: env.LEMON_SQUEEZY_STORE_ID,
},
},
variant: {
data: {
type: "variants",
id: env.LEMON_SQUEEZY_VARIANT_ID,
},
},
},
},
}),
},
body: JSON.stringify({
amount: { value: amountValue, currency: "EUR" },
description: `Kunstenkamp Evenement — ${productDescription}`,
redirectUrl,
webhookUrl,
locale: "nl_NL",
metadata: { registration_token: input.token },
}),
});
);
if (!response.ok) {
const errorData = await response.json();
console.error("Mollie checkout error:", errorData);
console.error("LemonSqueezy checkout error:", errorData);
throw new Error("Kon checkout niet aanmaken");
}
const checkoutData = (await response.json()) as {
_links?: { checkout?: { href?: string } };
data?: { attributes?: { url?: string } };
};
const checkoutUrl = checkoutData._links?.checkout?.href;
const checkoutUrl = checkoutData.data?.attributes?.url;
if (!checkoutUrl) throw new Error("Geen checkout URL ontvangen");