diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 1ca7a8a..71c7543 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -19,7 +19,7 @@ import { Route as IndexRouteImport } from './routes/index' import { Route as AdminIndexRouteImport } from './routes/admin/index' import { Route as ManageTokenRouteImport } from './routes/manage.$token' import { Route as AdminDrinkkaartRouteImport } from './routes/admin/drinkkaart' -import { Route as ApiWebhookMollieRouteImport } from './routes/api/webhook/mollie' +import { Route as ApiWebhookLemonsqueezyRouteImport } from './routes/api/webhook/lemonsqueezy' import { Route as ApiRpcSplatRouteImport } from './routes/api/rpc/$' import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$' @@ -73,9 +73,9 @@ const AdminDrinkkaartRoute = AdminDrinkkaartRouteImport.update({ path: '/admin/drinkkaart', getParentRoute: () => rootRouteImport, } as any) -const ApiWebhookMollieRoute = ApiWebhookMollieRouteImport.update({ - id: '/api/webhook/mollie', - path: '/api/webhook/mollie', +const ApiWebhookLemonsqueezyRoute = ApiWebhookLemonsqueezyRouteImport.update({ + id: '/api/webhook/lemonsqueezy', + path: '/api/webhook/lemonsqueezy', getParentRoute: () => rootRouteImport, } as any) const ApiRpcSplatRoute = ApiRpcSplatRouteImport.update({ @@ -102,7 +102,7 @@ export interface FileRoutesByFullPath { '/admin/': typeof AdminIndexRoute '/api/auth/$': typeof ApiAuthSplatRoute '/api/rpc/$': typeof ApiRpcSplatRoute - '/api/webhook/mollie': typeof ApiWebhookMollieRoute + '/api/webhook/lemonsqueezy': typeof ApiWebhookLemonsqueezyRoute } export interface FileRoutesByTo { '/': typeof IndexRoute @@ -117,7 +117,7 @@ export interface FileRoutesByTo { '/admin': typeof AdminIndexRoute '/api/auth/$': typeof ApiAuthSplatRoute '/api/rpc/$': typeof ApiRpcSplatRoute - '/api/webhook/mollie': typeof ApiWebhookMollieRoute + '/api/webhook/lemonsqueezy': typeof ApiWebhookLemonsqueezyRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -133,7 +133,7 @@ export interface FileRoutesById { '/admin/': typeof AdminIndexRoute '/api/auth/$': typeof ApiAuthSplatRoute '/api/rpc/$': typeof ApiRpcSplatRoute - '/api/webhook/mollie': typeof ApiWebhookMollieRoute + '/api/webhook/lemonsqueezy': typeof ApiWebhookLemonsqueezyRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -150,7 +150,7 @@ export interface FileRouteTypes { | '/admin/' | '/api/auth/$' | '/api/rpc/$' - | '/api/webhook/mollie' + | '/api/webhook/lemonsqueezy' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -165,7 +165,7 @@ export interface FileRouteTypes { | '/admin' | '/api/auth/$' | '/api/rpc/$' - | '/api/webhook/mollie' + | '/api/webhook/lemonsqueezy' id: | '__root__' | '/' @@ -180,7 +180,7 @@ export interface FileRouteTypes { | '/admin/' | '/api/auth/$' | '/api/rpc/$' - | '/api/webhook/mollie' + | '/api/webhook/lemonsqueezy' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -196,7 +196,7 @@ export interface RootRouteChildren { AdminIndexRoute: typeof AdminIndexRoute ApiAuthSplatRoute: typeof ApiAuthSplatRoute ApiRpcSplatRoute: typeof ApiRpcSplatRoute - ApiWebhookMollieRoute: typeof ApiWebhookMollieRoute + ApiWebhookLemonsqueezyRoute: typeof ApiWebhookLemonsqueezyRoute } declare module '@tanstack/react-router' { @@ -271,11 +271,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AdminDrinkkaartRouteImport parentRoute: typeof rootRouteImport } - '/api/webhook/mollie': { - id: '/api/webhook/mollie' - path: '/api/webhook/mollie' - fullPath: '/api/webhook/mollie' - preLoaderRoute: typeof ApiWebhookMollieRouteImport + '/api/webhook/lemonsqueezy': { + id: '/api/webhook/lemonsqueezy' + path: '/api/webhook/lemonsqueezy' + fullPath: '/api/webhook/lemonsqueezy' + preLoaderRoute: typeof ApiWebhookLemonsqueezyRouteImport parentRoute: typeof rootRouteImport } '/api/rpc/$': { @@ -308,7 +308,7 @@ const rootRouteChildren: RootRouteChildren = { AdminIndexRoute: AdminIndexRoute, ApiAuthSplatRoute: ApiAuthSplatRoute, ApiRpcSplatRoute: ApiRpcSplatRoute, - ApiWebhookMollieRoute: ApiWebhookMollieRoute, + ApiWebhookLemonsqueezyRoute: ApiWebhookLemonsqueezyRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/apps/web/src/routes/api/webhook/mollie.ts b/apps/web/src/routes/api/webhook/lemonsqueezy.ts similarity index 63% rename from apps/web/src/routes/api/webhook/mollie.ts rename to apps/web/src/routes/api/webhook/lemonsqueezy.ts index 2054e09..f6ef665 100644 --- a/apps/web/src/routes/api/webhook/mollie.ts +++ b/apps/web/src/routes/api/webhook/lemonsqueezy.ts @@ -1,4 +1,4 @@ -import { randomUUID } from "node:crypto"; +import { createHmac, randomUUID } from "node:crypto"; import { creditRegistrationToAccount } from "@kk/api/routers/index"; import { db } from "@kk/db"; import { drinkkaart, drinkkaartTopup, registration } from "@kk/db/schema"; @@ -7,106 +7,101 @@ import { env } from "@kk/env/server"; import { createFileRoute } from "@tanstack/react-router"; import { and, eq } from "drizzle-orm"; -// Mollie payment object (relevant fields only) -interface MolliePayment { - id: string; - status: string; - amount: { value: string; currency: string }; - customerId?: string; - metadata?: { - registration_token?: string; - type?: string; - drinkkaartId?: string; - userId?: string; +// LemonSqueezy webhook payload types (order_created event) +interface LemonSqueezyOrderCreatedPayload { + meta: { + event_name: string; + custom_data?: { + type?: string; + registration_token?: string; + drinkkaartId?: string; + userId?: string; + }; + }; + data: { + id: string; + attributes: { + status: string; + customer_id: number; + total: number; // amount in cents + }; }; } -async function fetchMolliePayment(paymentId: string): Promise { - const response = await fetch( - `https://api.mollie.com/v2/payments/${paymentId}`, - { - headers: { - Authorization: `Bearer ${env.MOLLIE_API_KEY}`, - }, - }, - ); - - if (!response.ok) { - throw new Error( - `Failed to fetch Mollie payment ${paymentId}: ${response.status}`, - ); - } - - return response.json() as Promise; +function verifyWebhookSignature( + payload: string, + signature: string, + secret: string, +): boolean { + const hmac = createHmac("sha256", secret); + hmac.update(payload); + const digest = hmac.digest("hex"); + return signature === digest; } async function handleWebhook({ request }: { request: Request }) { - if (!env.MOLLIE_API_KEY) { - console.error("MOLLIE_API_KEY not configured"); + if (!env.LEMON_SQUEEZY_WEBHOOK_SECRET) { + console.error("LEMON_SQUEEZY_WEBHOOK_SECRET not configured"); return new Response("Payment provider not configured", { status: 500 }); } - // Mollie sends application/x-www-form-urlencoded with a single "id" field - let paymentId: string | null = null; + const payload = await request.text(); + const signature = request.headers.get("X-Signature"); + + if (!signature) { + return new Response("Missing signature", { status: 401 }); + } + + if ( + !verifyWebhookSignature( + payload, + signature, + env.LEMON_SQUEEZY_WEBHOOK_SECRET, + ) + ) { + return new Response("Invalid signature", { status: 401 }); + } + + let event: LemonSqueezyOrderCreatedPayload; try { - const body = await request.text(); - const params = new URLSearchParams(body); - paymentId = params.get("id"); + event = JSON.parse(payload) as LemonSqueezyOrderCreatedPayload; } catch { - return new Response("Invalid request body", { status: 400 }); + return new Response("Invalid JSON", { status: 400 }); } - if (!paymentId) { - return new Response("Missing payment id", { status: 400 }); + // Only handle order_created events + if (event.meta.event_name !== "order_created") { + return new Response("Event ignored", { status: 200 }); } - // Fetch-to-verify: retrieve the actual payment from Mollie to confirm its - // status. A malicious webhook cannot fake a paid status this way. - let payment: MolliePayment; - try { - payment = await fetchMolliePayment(paymentId); - } catch (err) { - console.error("Failed to fetch Mollie payment:", err); - return new Response("Failed to fetch payment", { status: 500 }); - } - - // Only process paid payments - if (payment.status !== "paid") { - return new Response("Payment status ignored", { status: 200 }); - } - - const metadata = payment.metadata; + const orderId = event.data.id; + const customerId = String(event.data.attributes.customer_id); + const amountCents = event.data.attributes.total; + const customData = event.meta.custom_data; try { // ------------------------------------------------------------------------- // Branch: Drinkkaart top-up // ------------------------------------------------------------------------- - if (metadata?.type === "drinkkaart_topup") { - const { drinkkaartId, userId } = metadata; + if (customData?.type === "drinkkaart_topup") { + const { drinkkaartId, userId } = customData; if (!drinkkaartId || !userId) { console.error( - "Missing drinkkaartId or userId in drinkkaart_topup payment metadata", + "Missing drinkkaartId or userId in drinkkaart_topup custom_data", ); return new Response("Missing drinkkaart data", { status: 400 }); } - // Amount in cents — Mollie returns e.g. "10.00"; parse to integer cents - const amountCents = Math.round( - Number.parseFloat(payment.amount.value) * 100, - ); - // Idempotency: skip if already processed const existing = await db .select({ id: drinkkaartTopup.id }) .from(drinkkaartTopup) - .where(eq(drinkkaartTopup.molliePaymentId, payment.id)) + .where(eq(drinkkaartTopup.lemonsqueezyOrderId, orderId)) .limit(1) .then((r) => r[0]); if (existing) { - console.log( - `Drinkkaart topup already processed for payment ${payment.id}`, - ); + console.log(`Drinkkaart topup already processed for order ${orderId}`); return new Response("OK", { status: 200 }); } @@ -141,7 +136,7 @@ async function handleWebhook({ request }: { request: Request }) { ); if (result.rowsAffected === 0) { - // Return 500 so Mollie retries; idempotency check prevents double-credit + // Return 500 so LemonSqueezy retries; idempotency check prevents double-credit console.error( `Drinkkaart optimistic lock conflict for ${drinkkaartId}`, ); @@ -156,14 +151,14 @@ async function handleWebhook({ request }: { request: Request }) { balanceBefore, balanceAfter, type: "payment", - molliePaymentId: payment.id, + lemonsqueezyOrderId: orderId, adminId: null, reason: null, paidAt: new Date(), }); console.log( - `Drinkkaart topup successful: drinkkaart=${drinkkaartId}, amount=${amountCents}c, payment=${payment.id}`, + `Drinkkaart topup successful: drinkkaart=${drinkkaartId}, amount=${amountCents}c, order=${orderId}`, ); return new Response("OK", { status: 200 }); @@ -172,9 +167,9 @@ async function handleWebhook({ request }: { request: Request }) { // ------------------------------------------------------------------------- // Branch: Registration payment // ------------------------------------------------------------------------- - const registrationToken = metadata?.registration_token; + const registrationToken = customData?.registration_token; if (!registrationToken) { - console.error("No registration token in payment metadata"); + console.error("No registration token in order custom_data"); return new Response("Missing registration token", { status: 400 }); } @@ -197,13 +192,14 @@ async function handleWebhook({ request }: { request: Request }) { .set({ paymentStatus: "paid", paymentAmount: 0, - molliePaymentId: payment.id, + lemonsqueezyOrderId: orderId, + lemonsqueezyCustomerId: customerId, paidAt: new Date(), }) .where(eq(registration.managementToken, registrationToken)); console.log( - `Payment successful for registration ${registrationToken}, payment ${payment.id}`, + `Payment successful for registration ${registrationToken}, order ${orderId}`, ); // If this is a watcher with a drink card value, try to credit their @@ -254,7 +250,7 @@ async function handleWebhook({ request }: { request: Request }) { } } -export const Route = createFileRoute("/api/webhook/mollie")({ +export const Route = createFileRoute("/api/webhook/lemonsqueezy")({ server: { handlers: { POST: handleWebhook, diff --git a/packages/api/src/routers/drinkkaart.ts b/packages/api/src/routers/drinkkaart.ts index ed74e24..66a625c 100644 --- a/packages/api/src/routers/drinkkaart.ts +++ b/packages/api/src/routers/drinkkaart.ts @@ -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", diff --git a/packages/api/src/routers/index.ts b/packages/api/src/routers/index.ts index 777f895..94d1579 100644 --- a/packages/api/src/routers/index.ts +++ b/packages/api/src/routers/index.ts @@ -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"); diff --git a/packages/db/src/migrations/0007_lemonsqueezy_migration.sql b/packages/db/src/migrations/0007_lemonsqueezy_migration.sql new file mode 100644 index 0000000..dcf4edc --- /dev/null +++ b/packages/db/src/migrations/0007_lemonsqueezy_migration.sql @@ -0,0 +1,15 @@ +-- Migrate from Mollie back to LemonSqueezy +-- Renames payment provider columns in registration and drinkkaart_topup tables, +-- and restores the lemonsqueezy_customer_id column on registration. + +-- registration table: +-- mollie_payment_id -> lemonsqueezy_order_id +-- (re-add) lemonsqueezy_customer_id + +ALTER TABLE registration RENAME COLUMN mollie_payment_id TO lemonsqueezy_order_id; +ALTER TABLE registration ADD COLUMN lemonsqueezy_customer_id text; + +-- drinkkaart_topup table: +-- mollie_payment_id -> lemonsqueezy_order_id + +ALTER TABLE drinkkaart_topup RENAME COLUMN mollie_payment_id TO lemonsqueezy_order_id; diff --git a/packages/db/src/schema/drinkkaart.ts b/packages/db/src/schema/drinkkaart.ts index f8c2b04..7d6d4eb 100644 --- a/packages/db/src/schema/drinkkaart.ts +++ b/packages/db/src/schema/drinkkaart.ts @@ -46,7 +46,7 @@ export const drinkkaartTopup = sqliteTable("drinkkaart_topup", { balanceBefore: integer("balance_before").notNull(), balanceAfter: integer("balance_after").notNull(), type: text("type", { enum: ["payment", "admin_credit"] }).notNull(), - molliePaymentId: text("mollie_payment_id").unique(), // nullable; only for type="payment" + lemonsqueezyOrderId: text("lemonsqueezy_order_id").unique(), // nullable; only for type="payment" adminId: text("admin_id"), // nullable; only for type="admin_credit" reason: text("reason"), paidAt: integer("paid_at", { mode: "timestamp_ms" }).notNull(), diff --git a/packages/db/src/schema/registrations.ts b/packages/db/src/schema/registrations.ts index b6d27bf..45d5df7 100644 --- a/packages/db/src/schema/registrations.ts +++ b/packages/db/src/schema/registrations.ts @@ -29,7 +29,8 @@ export const registration = sqliteTable( paymentStatus: text("payment_status").notNull().default("pending"), paymentAmount: integer("payment_amount").default(0), giftAmount: integer("gift_amount").default(0), - molliePaymentId: text("mollie_payment_id"), + lemonsqueezyOrderId: text("lemonsqueezy_order_id"), + lemonsqueezyCustomerId: text("lemonsqueezy_customer_id"), paidAt: integer("paid_at", { mode: "timestamp_ms" }), // Set when the drinkCardValue has been credited to the user's drinkkaart. // Null means not yet credited (either unpaid, account doesn't exist yet, or @@ -49,6 +50,6 @@ export const registration = sqliteTable( index("registration_managementToken_idx").on(table.managementToken), index("registration_paymentStatus_idx").on(table.paymentStatus), index("registration_giftAmount_idx").on(table.giftAmount), - index("registration_molliePaymentId_idx").on(table.molliePaymentId), + index("registration_lemonsqueezyOrderId_idx").on(table.lemonsqueezyOrderId), ], ); diff --git a/packages/env/src/server.ts b/packages/env/src/server.ts index 41a3d43..ecf94a7 100644 --- a/packages/env/src/server.ts +++ b/packages/env/src/server.ts @@ -25,7 +25,10 @@ export const env = createEnv({ NODE_ENV: z .enum(["development", "production", "test"]) .default("development"), - MOLLIE_API_KEY: z.string().min(1).optional(), + LEMON_SQUEEZY_API_KEY: z.string().min(1).optional(), + LEMON_SQUEEZY_STORE_ID: z.string().min(1).optional(), + LEMON_SQUEEZY_VARIANT_ID: z.string().min(1).optional(), + LEMON_SQUEEZY_WEBHOOK_SECRET: z.string().min(1).optional(), }, runtimeEnv: process.env, emptyStringAsUndefined: true, diff --git a/packages/infra/alchemy.run.ts b/packages/infra/alchemy.run.ts index 87bab0c..9584695 100644 --- a/packages/infra/alchemy.run.ts +++ b/packages/infra/alchemy.run.ts @@ -30,8 +30,11 @@ export const web = await TanStackStart("web", { SMTP_USER: getEnvVar("SMTP_USER"), SMTP_PASS: getEnvVar("SMTP_PASS"), SMTP_FROM: getEnvVar("SMTP_FROM"), - // Payments (Mollie) - MOLLIE_API_KEY: getEnvVar("MOLLIE_API_KEY"), + // Payments (LemonSqueezy) + 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"], });