feat: full CRUD support — companies, deals, projects, expenses, tickets, licenses
API routes (new + modified): - POST /api/aos/crm/companies (create company) - PATCH/DELETE /api/aos/crm/companies/[id] (edit/delete with dependency check) - PATCH/DELETE /api/aos/crm/deals/[id] (edit/delete deal) - POST /api/aos/projects (create project) - PATCH/DELETE /api/aos/projects/[id] (edit/delete with dependency check) - POST /api/aos/expenses (create expense) - PATCH/DELETE /api/aos/expenses/[id] (edit/delete with GoBD lock) - PATCH/DELETE /api/aos/tickets/[id] (edit/delete with message check) - DELETE /api/aos/licenses/[id] (delete only if cancelled + no deps) - NO invoice delete endpoint (GoBD retention — storno only) UI components (new): - DeleteDialog: reusable AlertDialog with dependency list - CompanyFormDialog: create + edit (12 fields) - DealFormDialog: create + edit (replaces CreateDealDialog) - ProjectFormDialog: create + edit - ExpenseFormDialog: create + edit - TicketEditDialog: edit status/priority/category/assignee UI views (modified): - CRM: 'Neue Firma' + 'Neuer Deal' buttons, edit/delete per row - Projects: 'Projekt' button, edit/delete on cards - Expenses: 'Ausgabe' button, edit/delete per row (hidden if DATEV-locked) - Support: edit/delete in inbox header - Licenses: delete button (only if cancelled/revoked/expired) Cascade policy: - Every DELETE counts FK relations, returns 409 with dependency list - Hard delete only when no dependencies exist - GoBD: expenses locked if DATEV-exported or invoiced - Audit log on every create/update/delete
This commit is contained in:
1 parent
27669adc21
commit
aa075762b2
21 files changed
+2618
-40
No files matched your search
@@ -0,0 +1,84 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// DELETE /api/aos/licenses/[id] — hard-delete a license.
|
||||
//
|
||||
// Cascade policy: a license may only be hard-deleted when:
|
||||
// - it has NO activations, AND
|
||||
// - it has NO linked instance (License 1:1 Instance), AND
|
||||
// - it has NO child module licenses (parentId === this id), AND
|
||||
// - it is in status 'cancelled' (cancelled licenses that have reached their
|
||||
// effective date are safe to remove from active bookkeeping).
|
||||
//
|
||||
// Otherwise the call returns 409 with the dependency counts.
|
||||
//
|
||||
// GoBD: if the license has been issued, activated, or used to bill a
|
||||
// customer, do NOT delete — cancel (cancel/route.ts) keeps the audit record.
|
||||
// This endpoint exists for genuine cleanup (e.g. demo licenses that never
|
||||
// started a subscription, or test fixtures).
|
||||
export async function DELETE(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const existing = await db.license.findUnique({ where: { id } })
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'license not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const [activations, instances, modules] = await Promise.all([
|
||||
db.licenseActivation.count({ where: { licenseId: id } }),
|
||||
db.instance.count({ where: { licenseId: id } }),
|
||||
db.license.count({ where: { parentId: id } }),
|
||||
])
|
||||
|
||||
const dependencies: Record<string, number> = { activations, instances, modules }
|
||||
const totalDeps = Object.values(dependencies).reduce((s, n) => s + n, 0)
|
||||
|
||||
if (totalDeps > 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'license has related records — cancel the license instead of deleting it',
|
||||
dependencies,
|
||||
total: totalDeps,
|
||||
},
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
|
||||
// Only allow deletion when the license is already cancelled or never started.
|
||||
// Active/trial/suspended licenses must go through the cancel flow.
|
||||
if (
|
||||
existing.status !== 'cancelled' &&
|
||||
existing.status !== 'revoked' &&
|
||||
existing.status !== 'expired'
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ error: `license is ${existing.status} — cancel before deleting (GoBD retention)` },
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
|
||||
await db.license.delete({ where: { id } })
|
||||
|
||||
await db.auditLog.create({
|
||||
data: {
|
||||
actor: 'admin:ui',
|
||||
action: 'license:delete',
|
||||
entity: 'License',
|
||||
entityId: id,
|
||||
before: JSON.stringify({ key: existing.key, status: existing.status, product: existing.product }),
|
||||
after: JSON.stringify({ deleted: true }),
|
||||
hash: 'sha256:' + Math.random().toString(36).slice(2),
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ ok: true, deleted: true, id })
|
||||
} 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