Files
Epicnabbo-Catalogus-Updated…/src/app/api/paypal/create/route.ts
T
openhands 17847545dd Improvements: remove dead config, fix ESM, add URL validation, unify types, add missing logging
- Remove .prettierrc (dead config, Biome replaces Prettier)
- Rename lighthouserc.json to lighthouserc.cjs with module.exports for ESM compat
- Add logger.warn to empty catch blocks in auth, register, site-settings, prisma-cache, redis, security, rate-limit
- Unify ActionResult type: action-helper.ts uses 'ok' consistent with safe-action-shared.ts
- Add noUnusedLocals + noUnusedParameters to tsconfig + fix 25 pre-existing unused vars
- Replace barrel export src/types/index.ts with direct @/types/common imports
- Make trustHost conditional (development only) in auth.ts
- Add pre-flight URL validation to update-Nitrov3.sh to catch image.library.url misconfigurations
- Improve NITRO_IMAGE_LIBRARY_URL content validation in pre-flight & post-compute checks
2026-07-26 20:28:11 +02:00

111 lines
3.0 KiB
TypeScript

import { NextResponse } from "next/server";
import { env } from "@/env";
import { auth } from "@/lib/auth";
import { sessionUserId } from "@/lib/auth/session-user";
import { resolveHotelName } from "@/lib/hotel-name";
import { logger } from "@/lib/logger";
import { prisma } from "@/lib/prisma";
import {
createOrder,
creditsPerUnit,
isPayPalConfigured,
PAYPAL_CURRENCY,
} from "@/lib/services/paypal";
import { recordCreatedTopup } from "@/lib/services/paypal-topup";
export const dynamic = "force-dynamic";
const MIN_AMOUNT = 1;
const MAX_AMOUNT = 500;
/**
* POST /api/paypal/create — create a PayPal CAPTURE order for the signed-in user.
* Body: { amount: number } (in the configured currency, default USD).
* Returns { id, approveUrl } on success; a clear JSON error otherwise.
*
* Auth-gated via auth(): the order is tied to the session, never to a body field.
*/
export async function POST(req: Request): Promise<Response> {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json(
{ error: "You must be signed in to top up." },
{ status: 401 },
);
}
const userId = sessionUserId(session.user.id);
if (!userId) {
return NextResponse.json({ error: "Invalid session." }, { status: 401 });
}
// Fail fast (and clearly) when the sandbox/live keys aren't set.
if (!isPayPalConfigured()) {
return NextResponse.json(
{
error:
"PayPal is not configured. Set PAYPAL_CLIENT_ID and PAYPAL_SECRET.",
},
{ status: 503 },
);
}
let body: unknown;
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "Invalid JSON body." }, { status: 400 });
}
const raw = (body as { amount?: unknown })?.amount;
const amount = Math.round(Number(raw) * 100) / 100;
if (!Number.isFinite(amount) || amount < MIN_AMOUNT || amount > MAX_AMOUNT) {
return NextResponse.json(
{
error: `Enter an amount between ${MIN_AMOUNT} and ${MAX_AMOUNT} ${PAYPAL_CURRENCY}.`,
},
{ status: 422 },
);
}
const credits = Math.floor(amount * creditsPerUnit());
const base = env.APP_URL.replace(/\/+$/, "");
const hotelName = await resolveHotelName();
try {
const order = await createOrder(amount, {
description: `${hotelName} top-up: ${credits} credits`,
returnUrl: `${base}/shop/topup?status=success`,
cancelUrl: `${base}/shop/topup?status=cancel`,
});
if (!order.approveUrl) {
return NextResponse.json(
{ error: "PayPal did not return an approval link. Try again." },
{ status: 502 },
);
}
await recordCreatedTopup(
{ userId, orderId: order.id, amount, currency: PAYPAL_CURRENCY, credits },
(data) => prisma.websitePaypalTransactions.create({ data }),
);
return NextResponse.json({
id: order.id,
approveUrl: order.approveUrl,
amount,
currency: PAYPAL_CURRENCY,
credits,
});
} catch (e) {
logger.error("PayPal create order failed", {
module: "paypal/create",
error: (e as Error).message,
});
return NextResponse.json(
{ error: "Could not start the PayPal checkout. Please try again." },
{ status: 502 },
);
}
}