Files
Lageplan/src/app/api/upgrade-requests/route.ts

205 lines
9.5 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/db'
import { getSession } from '@/lib/auth'
import { sendEmail, getSmtpConfig } from '@/lib/email'
import { z } from 'zod'
const upgradeSchema = z.object({
requestedPlan: z.enum(['PRO']),
message: z.string().max(1000).optional(),
})
// GET: List upgrade requests for current tenant (TENANT_ADMIN) or all (SERVER_ADMIN)
export async function GET() {
try {
const user = await getSession()
if (!user) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
let where: any = {}
if (user.role === 'SERVER_ADMIN') {
// Server admin sees all
} else if (user.role === 'TENANT_ADMIN' && user.tenantId) {
where = { tenantId: user.tenantId }
} else {
return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 })
}
const requests = await (prisma as any).upgradeRequest.findMany({
where,
include: {
tenant: { select: { name: true, slug: true, plan: true, subscriptionStatus: true } },
requestedBy: { select: { name: true, email: true } },
},
orderBy: { createdAt: 'desc' },
})
return NextResponse.json({ requests })
} catch (error) {
console.error('Error fetching upgrade requests:', error)
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })
}
}
// POST: Create a new upgrade request (TENANT_ADMIN only)
export async function POST(req: NextRequest) {
try {
const user = await getSession()
if (!user) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
if (user.role !== 'TENANT_ADMIN' || !user.tenantId) {
return NextResponse.json({ error: 'Nur Mandanten-Administratoren können Upgrades anfordern' }, { status: 403 })
}
const body = await req.json()
const validated = upgradeSchema.safeParse(body)
if (!validated.success) {
return NextResponse.json({ error: 'Ungültige Eingabe', details: validated.error.flatten() }, { status: 400 })
}
// Get current tenant
const tenant = await (prisma as any).tenant.findUnique({
where: { id: user.tenantId },
select: { id: true, name: true, plan: true, contactEmail: true },
})
if (!tenant) {
return NextResponse.json({ error: 'Mandant nicht gefunden' }, { status: 404 })
}
// Check for existing pending request
const existingPending = await (prisma as any).upgradeRequest.findFirst({
where: { tenantId: user.tenantId, status: 'PENDING' },
})
if (existingPending) {
return NextResponse.json({
error: 'Es gibt bereits eine offene Upgrade-Anfrage. Bitte warten Sie auf die Bearbeitung.',
}, { status: 409 })
}
// Don't allow "downgrade" requests or same plan
const planOrder = { FREE: 0, PRO: 1 }
if ((planOrder[validated.data.requestedPlan as keyof typeof planOrder] || 0) <= (planOrder[tenant.plan as keyof typeof planOrder] || 0)) {
return NextResponse.json({ error: 'Der gewählte Plan ist kein Upgrade gegenüber dem aktuellen Plan.' }, { status: 400 })
}
// Create request
const request = await (prisma as any).upgradeRequest.create({
data: {
tenantId: user.tenantId,
requestedById: user.id,
requestedPlan: validated.data.requestedPlan,
currentPlan: tenant.plan,
message: validated.data.message || null,
},
include: {
tenant: { select: { name: true } },
requestedBy: { select: { name: true, email: true } },
},
})
// Send emails
const smtpConfig = await getSmtpConfig()
if (smtpConfig) {
const planLabels: Record<string, string> = {
FREE: 'Free', PRO: 'Pro',
}
// 1. Confirmation to tenant admin
try {
await sendEmail(
user.email,
`Upgrade-Anfrage bestätigt — ${planLabels[validated.data.requestedPlan]}`,
`
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 520px; margin: 0 auto;">
<div style="background: #2563eb; color: white; padding: 16px 24px; border-radius: 8px 8px 0 0;">
<h2 style="margin: 0; font-size: 18px;">Upgrade-Anfrage eingegangen</h2>
</div>
<div style="border: 1px solid #e5e7eb; border-top: none; padding: 24px; border-radius: 0 0 8px 8px;">
<p style="margin: 0 0 16px; line-height: 1.6; color: #374151;">
Ihre Upgrade-Anfrage für <strong>${tenant.name}</strong> wurde erfolgreich übermittelt.
</p>
<table style="width: 100%; border-collapse: collapse; margin: 16px 0;">
<tr>
<td style="padding: 8px 0; color: #6b7280; font-size: 14px;">Aktueller Plan</td>
<td style="padding: 8px 0; font-weight: 600; text-align: right;">${planLabels[tenant.plan] || tenant.plan}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #6b7280; font-size: 14px;">Gewünschter Plan</td>
<td style="padding: 8px 0; font-weight: 600; text-align: right; color: #2563eb;">${planLabels[validated.data.requestedPlan]}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #6b7280; font-size: 14px;">Status</td>
<td style="padding: 8px 0; font-weight: 600; text-align: right;">
<span style="background: #fef3c7; color: #92400e; padding: 2px 8px; border-radius: 4px; font-size: 12px;">Wird geprüft</span>
</td>
</tr>
</table>
${validated.data.message ? `<p style="margin: 16px 0 0; padding: 12px; background: #f9fafb; border-radius: 6px; font-size: 14px; color: #374151;"><strong>Ihre Nachricht:</strong><br/>${validated.data.message.replace(/\n/g, '<br/>')}</p>` : ''}
<p style="margin: 16px 0 0; font-size: 13px; color: #6b7280;">
Wir werden Ihre Anfrage so schnell wie möglich bearbeiten. Sie erhalten eine Benachrichtigung, sobald Ihr Plan aktiviert wurde.
</p>
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 20px 0;" />
<p style="margin: 0; font-size: 12px; color: #9ca3af;">Lageplan — Digitale Lagepläne für die Feuerwehr</p>
</div>
</div>
`
)
} catch (e) {
console.error('Failed to send upgrade confirmation email:', e)
}
// 2. Notification to all server admins
try {
const serverAdmins = await (prisma as any).user.findMany({
where: { role: 'SERVER_ADMIN' },
select: { email: true, name: true },
})
for (const admin of serverAdmins) {
await sendEmail(
admin.email,
`Neue Upgrade-Anfrage: ${tenant.name}${planLabels[validated.data.requestedPlan]}`,
`
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 520px; margin: 0 auto;">
<div style="background: #dc2626; color: white; padding: 16px 24px; border-radius: 8px 8px 0 0;">
<h2 style="margin: 0; font-size: 18px;">Neue Upgrade-Anfrage</h2>
</div>
<div style="border: 1px solid #e5e7eb; border-top: none; padding: 24px; border-radius: 0 0 8px 8px;">
<table style="width: 100%; border-collapse: collapse; margin: 0 0 16px;">
<tr>
<td style="padding: 8px 0; color: #6b7280; font-size: 14px;">Organisation</td>
<td style="padding: 8px 0; font-weight: 600; text-align: right;">${tenant.name}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #6b7280; font-size: 14px;">Angefragt von</td>
<td style="padding: 8px 0; font-weight: 600; text-align: right;">${user.name} (${user.email})</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #6b7280; font-size: 14px;">Aktueller Plan</td>
<td style="padding: 8px 0; text-align: right;">${planLabels[tenant.plan] || tenant.plan}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #6b7280; font-size: 14px;">Gewünschter Plan</td>
<td style="padding: 8px 0; font-weight: 600; text-align: right; color: #2563eb;">${planLabels[validated.data.requestedPlan]}</td>
</tr>
</table>
${validated.data.message ? `<p style="margin: 0 0 16px; padding: 12px; background: #f9fafb; border-radius: 6px; font-size: 14px; color: #374151;"><strong>Nachricht:</strong><br/>${validated.data.message.replace(/\n/g, '<br/>')}</p>` : ''}
<p style="margin: 0; font-size: 13px; color: #6b7280;">
Bitte prüfen und bestätigen Sie die Anfrage im Admin-Panel unter "Upgrade-Anfragen".
</p>
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 20px 0;" />
<p style="margin: 0; font-size: 12px; color: #9ca3af;">Lageplan — Automatische Benachrichtigung</p>
</div>
</div>
`
)
}
} catch (e) {
console.error('Failed to send admin notification email:', e)
}
}
return NextResponse.json({ request }, { status: 201 })
} catch (error) {
console.error('Error creating upgrade request:', error)
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })
}
}