import { NextResponse } from 'next/server'; import { UserStatus } from '@prisma/client'; import { prisma } from '../../../../lib/prisma'; import { requireAuth } from '../../../../lib/jwt'; import { normalizeIban, normalizeNullableBoolean, normalizeOptionalString } from '../../../../lib/billing'; function pickTranslation(translations: { title: string; slug: string; locale: string }[]) { return translations.find((t) => t.locale === 'en') || translations.find((t) => t.locale === 'fi') || translations[0] || null; } async function loadState(userId: string) { const [user, listings] = await Promise.all([ prisma.user.findUnique({ where: { id: userId }, select: { id: true, status: true, billingEmailsEnabled: true, billingAccountName: true, billingIban: true, billingIncludeVatLine: true, }, }), prisma.listing.findMany({ where: { ownerId: userId, removedAt: null }, select: { id: true, billingAccountName: true, billingIban: true, billingIncludeVatLine: true, translations: { select: { title: true, slug: true, locale: true } }, }, orderBy: { createdAt: 'desc' }, }), ]); return { user, listings }; } function validateAccountName(name: string | null | undefined) { if (name && name.length > 120) { return 'Account owner name is too long'; } return null; } function validateIban(iban: string | null | undefined) { if (iban && !/^[A-Z0-9]{8,34}$/.test(iban)) { return 'IBAN must be 8-34 alphanumeric characters'; } return null; } function normalizeListingOverrides(body: any) { const overridesRaw = Array.isArray(body?.listings) ? body.listings : []; return overridesRaw .map((item: any) => ({ id: typeof item.id === 'string' ? item.id : null, accountName: normalizeOptionalString(item.accountName), iban: normalizeIban(item.iban), includeVatLine: normalizeNullableBoolean(item.includeVatLine), })) .filter((o) => o.id); } function buildResponsePayload(user: NonNullable>['user']>, listings: Awaited>['listings']) { return { settings: { enabled: user.billingEmailsEnabled, accountName: user.billingAccountName, iban: user.billingIban, includeVatLine: user.billingIncludeVatLine, }, listings: listings.map((listing) => { const translation = pickTranslation(listing.translations); return { id: listing.id, title: translation?.title ?? 'Listing', slug: translation?.slug ?? '', locale: translation?.locale ?? 'en', billingAccountName: listing.billingAccountName, billingIban: listing.billingIban, billingIncludeVatLine: listing.billingIncludeVatLine, }; }), }; } export async function GET(req: Request) { try { const session = await requireAuth(req); const { user, listings } = await loadState(session.userId); if (!user || user.status !== UserStatus.ACTIVE) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } return NextResponse.json(buildResponsePayload(user, listings)); } catch (error) { console.error('Billing settings fetch failed', error); return NextResponse.json({ error: 'Failed to load billing settings' }, { status: 500 }); } } export async function PATCH(req: Request) { let body: any; try { body = await req.json(); } catch { return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); } try { const session = await requireAuth(req); const { user, listings } = await loadState(session.userId); if (!user || user.status !== UserStatus.ACTIVE) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const enabled = body.enabled === undefined ? undefined : Boolean(body.enabled); const accountName = normalizeOptionalString(body.accountName); const iban = normalizeIban(body.iban); const includeVatLine = body.includeVatLine === undefined ? undefined : Boolean(body.includeVatLine); const listingOverrides = normalizeListingOverrides(body); const errors = [validateAccountName(accountName), validateIban(iban)].filter(Boolean) as string[]; for (const override of listingOverrides) { const nameError = validateAccountName(override.accountName); const ibanError = validateIban(override.iban); if (nameError) errors.push(`${nameError} for listing ${override.id}`); if (ibanError) errors.push(`${ibanError} for listing ${override.id}`); } if (errors.length) { return NextResponse.json({ error: errors.join('; ') }, { status: 400 }); } const userUpdates: any = {}; if (enabled !== undefined) userUpdates.billingEmailsEnabled = enabled; if (accountName !== undefined) userUpdates.billingAccountName = accountName; if (iban !== undefined) userUpdates.billingIban = iban; if (includeVatLine !== undefined) userUpdates.billingIncludeVatLine = includeVatLine; const listingMap = new Map(listings.map((l) => [l.id, l])); const listingUpdates: { id: string; data: Record }[] = []; listingOverrides.forEach((override) => { if (!listingMap.has(override.id!)) return; const data: Record = {}; if (override.accountName !== undefined) data.billingAccountName = override.accountName; if (override.iban !== undefined) data.billingIban = override.iban; if (override.includeVatLine !== undefined) data.billingIncludeVatLine = override.includeVatLine; if (Object.keys(data).length) { listingUpdates.push({ id: override.id!, data }); } }); const targetEnabled = enabled ?? user.billingEmailsEnabled; if (targetEnabled) { const targetAccountName = accountName !== undefined ? accountName : user.billingAccountName; const targetIban = iban !== undefined ? iban : user.billingIban; const hasGlobalDetails = Boolean(targetAccountName && targetIban); const missingFor: string[] = []; listings.forEach((listing) => { const override = listingUpdates.find((o) => o.id === listing.id); const effectiveAccountName = override?.data.billingAccountName !== undefined ? override.data.billingAccountName : listing.billingAccountName ?? targetAccountName; const effectiveIban = override?.data.billingIban !== undefined ? override.data.billingIban : listing.billingIban ?? targetIban; if (!effectiveAccountName || !effectiveIban) { const t = pickTranslation(listing.translations); missingFor.push(t?.slug || listing.id); } }); if (!hasGlobalDetails && missingFor.length) { return NextResponse.json( { error: `Provide billing account name and IBAN globally or per listing (missing for: ${missingFor.join(', ')})` }, { status: 400 }, ); } if (!hasGlobalDetails && listings.length === 0) { return NextResponse.json( { error: 'Add a billing account name and IBAN before enabling the billing assistant.' }, { status: 400 }, ); } } const tx: any[] = []; if (Object.keys(userUpdates).length) { tx.push(prisma.user.update({ where: { id: user.id }, data: userUpdates })); } listingUpdates.forEach((update) => { tx.push(prisma.listing.update({ where: { id: update.id }, data: update.data })); }); if (tx.length) { await prisma.$transaction(tx); } else { return NextResponse.json({ error: 'No updates provided' }, { status: 400 }); } const { user: updatedUser, listings: updatedListings } = await loadState(session.userId); if (!updatedUser) { return NextResponse.json({ error: 'User not found' }, { status: 404 }); } return NextResponse.json(buildResponsePayload(updatedUser, updatedListings)); } catch (error) { console.error('Billing settings update failed', error); const status = (error as any)?.message === 'Unauthorized' ? 401 : 500; return NextResponse.json({ error: 'Failed to update billing settings' }, { status }); } } export const dynamic = 'force-dynamic';