a6c0f3de-eeb0-4d76-a57f-f600dd133c06
This commit is contained in:
1 parent
2aa3496a90
commit
762f3d7373
141 files changed
+4485
-69
No files matched your search
@@ -0,0 +1,90 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const schema = z.object({
|
||||
discountPct: z.number().int().min(0).max(20),
|
||||
reviewedBy: z.string().optional(),
|
||||
})
|
||||
|
||||
// PATCH /api/aos/agency/[id]/approve
|
||||
// Admin approves an agency application. Sets Company.agencyStatus = "approved",
|
||||
// Company.agencyDiscountPct = discountPct, and marks the application approved.
|
||||
// In production an email would be sent to the applicant with their discount.
|
||||
export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const parsed = schema.safeParse(await req.json())
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: 'discountPct (0-20) required', details: parsed.error.flatten() }, { status: 400 })
|
||||
}
|
||||
const { discountPct, reviewedBy } = parsed.data
|
||||
|
||||
const application = await db.agencyApplication.findUnique({
|
||||
where: { id },
|
||||
include: { company: true },
|
||||
})
|
||||
if (!application) {
|
||||
return NextResponse.json({ error: 'application not found' }, { status: 404 })
|
||||
}
|
||||
if (application.status === 'approved') {
|
||||
return NextResponse.json({ error: 'already approved' }, { status: 409 })
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
await db.$transaction([
|
||||
db.agencyApplication.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'approved',
|
||||
discountPct,
|
||||
reviewedBy: reviewedBy ?? 'admin:ui',
|
||||
reviewedAt: now,
|
||||
},
|
||||
}),
|
||||
db.company.update({
|
||||
where: { id: application.companyId },
|
||||
data: {
|
||||
isAgency: true,
|
||||
agencyStatus: 'approved',
|
||||
agencyApprovedAt: now,
|
||||
agencyDiscountPct: discountPct,
|
||||
agencyRejectedAt: null,
|
||||
agencyRejectReason: null,
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
await db.auditLog.create({
|
||||
data: {
|
||||
actor: reviewedBy ?? 'admin:ui',
|
||||
action: 'agency.approve',
|
||||
entity: 'AgencyApplication',
|
||||
entityId: id,
|
||||
after: JSON.stringify({
|
||||
company: application.company.name,
|
||||
discountPct,
|
||||
approvedAt: now,
|
||||
}),
|
||||
hash: 'sha256:' + Math.random().toString(36).slice(2),
|
||||
},
|
||||
})
|
||||
|
||||
// TODO (production): send email to company.email with subject
|
||||
// "Ihre Bayi-Bewerbung wurde genehmigt — {discountPct}% Rabatt"
|
||||
// containing the discount and a link to the agency portal.
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
status: 'approved',
|
||||
discountPct,
|
||||
email: application.company.email,
|
||||
message: `Genehmigt mit ${discountPct}% Rabatt. E-Mail an ${application.company.email ?? '(keine E-Mail hinterlegt)'} würde versendet.`,
|
||||
})
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'internal server error'
|
||||
return NextResponse.json({ error: msg }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const schema = z.object({
|
||||
reason: z.string().min(1).max(500),
|
||||
reviewedBy: z.string().optional(),
|
||||
})
|
||||
|
||||
// PATCH /api/aos/agency/[id]/reject
|
||||
// Admin rejects an agency application. Sets Company.agencyStatus = "rejected".
|
||||
export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const parsed = schema.safeParse(await req.json())
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: 'reason required', details: parsed.error.flatten() }, { status: 400 })
|
||||
}
|
||||
const { reason, reviewedBy } = parsed.data
|
||||
|
||||
const application = await db.agencyApplication.findUnique({
|
||||
where: { id },
|
||||
include: { company: true },
|
||||
})
|
||||
if (!application) {
|
||||
return NextResponse.json({ error: 'application not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
await db.$transaction([
|
||||
db.agencyApplication.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'rejected',
|
||||
rejectReason: reason,
|
||||
reviewedBy: reviewedBy ?? 'admin:ui',
|
||||
reviewedAt: now,
|
||||
},
|
||||
}),
|
||||
db.company.update({
|
||||
where: { id: application.companyId },
|
||||
data: {
|
||||
agencyStatus: 'rejected',
|
||||
agencyRejectedAt: now,
|
||||
agencyRejectReason: reason,
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
await db.auditLog.create({
|
||||
data: {
|
||||
actor: reviewedBy ?? 'admin:ui',
|
||||
action: 'agency.reject',
|
||||
entity: 'AgencyApplication',
|
||||
entityId: id,
|
||||
after: JSON.stringify({ company: application.company.name, reason }),
|
||||
hash: 'sha256:' + Math.random().toString(36).slice(2),
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ ok: true, status: 'rejected', reason })
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'internal server error'
|
||||
return NextResponse.json({ error: msg }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const schema = z.object({
|
||||
companyId: z.string().min(1),
|
||||
businessModel: z.string().max(200).optional(),
|
||||
expectedClientVolume: z.string().max(100).optional(),
|
||||
referredBy: z.string().max(200).optional(),
|
||||
})
|
||||
|
||||
// POST /api/aos/agency/apply
|
||||
// A company applies to become an agency (reseller). Sets Company.agencyStatus
|
||||
// to "pending" and creates an AgencyApplication record. Admin must approve.
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const parsed = schema.safeParse(await req.json())
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: 'validation failed', details: parsed.error.flatten() }, { status: 400 })
|
||||
}
|
||||
const { companyId, businessModel, expectedClientVolume, referredBy } = parsed.data
|
||||
|
||||
const company = await db.company.findUnique({ where: { id: companyId } })
|
||||
if (!company) {
|
||||
return NextResponse.json({ error: 'company not found' }, { status: 404 })
|
||||
}
|
||||
if (company.isAgency && company.agencyStatus === 'approved') {
|
||||
return NextResponse.json({ error: 'already an approved agency' }, { status: 409 })
|
||||
}
|
||||
|
||||
// Upsert application (one per company — @unique on companyId)
|
||||
const application = await db.agencyApplication.upsert({
|
||||
where: { companyId },
|
||||
create: {
|
||||
companyId,
|
||||
status: 'pending',
|
||||
businessModel,
|
||||
expectedClientVolume,
|
||||
referredBy,
|
||||
},
|
||||
update: {
|
||||
status: 'pending',
|
||||
businessModel,
|
||||
expectedClientVolume,
|
||||
referredBy,
|
||||
rejectReason: null,
|
||||
reviewedBy: null,
|
||||
reviewedAt: null,
|
||||
},
|
||||
})
|
||||
|
||||
// Mark company as pending-agency
|
||||
await db.company.update({
|
||||
where: { id: companyId },
|
||||
data: {
|
||||
isAgency: true,
|
||||
agencyStatus: 'pending',
|
||||
agencyAppliedAt: new Date(),
|
||||
},
|
||||
})
|
||||
|
||||
await db.auditLog.create({
|
||||
data: {
|
||||
actor: `company:${companyId}`,
|
||||
action: 'agency.apply',
|
||||
entity: 'AgencyApplication',
|
||||
entityId: application.id,
|
||||
after: JSON.stringify({ company: company.name, businessModel, expectedClientVolume }),
|
||||
hash: 'sha256:' + Math.random().toString(36).slice(2),
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ ok: true, applicationId: application.id, status: 'pending' }, { status: 201 })
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'internal server error'
|
||||
return NextResponse.json({ error: msg }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// GET /api/aos/agency
|
||||
// Returns all agency applications + approved agencies. Admin-only in practice
|
||||
// (no scope check here because AOS is single-tenant dev mode).
|
||||
export async function GET() {
|
||||
try {
|
||||
const [applications, agencies] = await Promise.all([
|
||||
db.agencyApplication.findMany({
|
||||
orderBy: { appliedAt: 'desc' },
|
||||
include: { company: true },
|
||||
}),
|
||||
db.company.findMany({
|
||||
where: { isAgency: true, agencyStatus: 'approved' },
|
||||
orderBy: { agencyApprovedAt: 'desc' },
|
||||
include: {
|
||||
agencyClients: { select: { id: true, name: true, email: true } },
|
||||
_count: { select: { agencyClients: true, licenses: true } },
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
const pending = applications.filter((a) => a.status === 'pending')
|
||||
const reviewed = applications.filter((a) => a.status !== 'pending')
|
||||
|
||||
return NextResponse.json({
|
||||
applications: { pending, reviewed },
|
||||
agencies,
|
||||
stats: {
|
||||
pending: pending.length,
|
||||
approved: agencies.length,
|
||||
totalApplications: applications.length,
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'internal server error'
|
||||
return NextResponse.json({ error: msg }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user