L14 Abrechnungsübersicht: Tests, Backfill, Smoke und Lane-Bericht

test-abrechnung-service (118 Prüfungen inkl. Mandantentrennung und PDF-Render-Smoke), test-abrechnung-sync (21), Fixture-Zeilen für die neuen Tenant-Modelle, Rollen-Matrix billing:*, smoke-auth um /billing, Detail, Druckansicht, Auftrags-Tab und mobile Meilensteine erweitert, scripts/billing-backfill.ts, docs/craftvia/lanes/abrechnung.md, ABNAHME §3 „Abrechnungsübersicht“ (keine Buchhaltung). Gate 69/69, RLS 69/69.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 10:36:10 +02:00
co-authored by Claude Opus 5
parent b76c1bdbfd
commit 2df785dd14
8 changed files with 662 additions and 2 deletions
+31 -2
View File
@@ -39,13 +39,32 @@ async function ids() {
const inReview = d16 ? await prisma.report.findFirst({ where: { workOrderId: d16.id } }) : null;
const imp = await prisma.importJob.findFirst({ where: { tenantId: demo.id, status: "review_required" }, orderBy: { createdAt: "desc" } });
const photo = await prisma.photo.findFirst({ where: { workOrderId: d07.id }, select: { documentId: true } });
return { d07, d08, d14, d15, d16, d17, d18, emergency, customer, foreignCustomer, site, approved, inReview, imp, photo };
// L14: open billing record (DEMO-18 released for billing → event hook / scripts/billing-backfill.ts) + smoke milestone on DEMO-07
const billing = await prisma.billingRecord.findFirst({ where: { tenantId: demo.id, workOrderId: d18.id, status: "open" }, select: { id: true } });
const milestone =
(await prisma.workOrderMilestone.findFirst({ where: { tenantId: demo.id, workOrderId: d07.id, title: "Smoke-Meilenstein", deletedAt: null }, select: { id: true } })) ??
(await prisma.workOrderMilestone.create({ data: { tenantId: demo.id, workOrderId: d07.id, title: "Smoke-Meilenstein", sortOrder: 10 }, select: { id: true } }));
return { d07, d08, d14, d15, d16, d17, d18, emergency, customer, foreignCustomer, site, approved, inReview, imp, photo, billing, milestone };
}
function plans(x: Awaited<ReturnType<typeof ids>>): Plan[] {
const wo = (o: { id: string } | null, suffix = "") => (o ? `/work-orders/${o.id}${suffix}` : "/work-orders");
const m = (o: { id: string } | null, suffix = "") => (o ? `/m/orders/${o.id}${suffix}` : "/m/orders");
const tabs = ["overview", "checklist", "material", "times", "photos", "notes", "reports", "documents", "history"];
const tabs = ["overview", "checklist", "material", "times", "photos", "notes", "reports", "billing", "documents", "history"];
// L14 Abrechnungsübersicht
const billingBackoffice: Check[] = [
{ path: "/billing", mustContain: ["Abrechnungsübersicht", "Offen"] },
{ path: "/billing?status=billed", mustContain: "Abgerechnet" },
{ path: "/dashboard", mustContain: "Bereit zur Abrechnung" },
{ path: wo(x.d07, "?tab=billing"), mustContain: ["Smoke-Meilenstein", "Abrechnungseinträge"] },
...(x.billing
? [
{ path: `/billing?q=${x.d18.number}`, mustContain: x.d18.number },
{ path: `/billing/${x.billing.id}`, mustContain: ["Abrechnungsblatt", x.d18.number, "Anfahrten", "Als abgerechnet markieren"] },
{ path: `/billing/print?ids=${x.billing.id}`, mustContain: ["Abrechnungsblatt", x.d18.number] },
]
: []),
];
return [
{
email: "admin@demo.example",
@@ -81,6 +100,7 @@ function plans(x: Awaited<ReturnType<typeof ids>>): Plan[] {
...tabs.map((t) => ({ path: wo(x.d07, `?tab=${t}`), mustContain: x.d07.number })),
{ path: wo(x.d18, "?tab=reports"), mustContain: x.d18.number },
{ path: "/work-orders/conflicts" },
...billingBackoffice,
// L12 Zeiterfassung
{ path: "/work-orders/time-approvals", mustContain: "Zeiten zur Freigabe" },
{ path: "/dashboard", mustContain: "Zeiten zur Freigabe" },
@@ -151,6 +171,11 @@ function plans(x: Awaited<ReturnType<typeof ids>>): Plan[] {
{ path: "/m/time/new", mustContain: ["Zeit nachtragen", "Begründung"] },
{ path: "/m/approvals", mustContain: "nicht freigeschaltet" },
{ path: m(x.d07, "/time"), mustContain: x.d07.number },
// L14 Abrechnungsübersicht: Meilensteine mobil, Backoffice-Übersicht ohne Recht
{ path: m(x.d07), mustContain: ["Meilensteine", "Smoke-Meilenstein", "Erreicht melden"] },
// order numbers appear in the monteur's own notifications and all UI texts in the serialized messages → check the record link
{ path: "/billing", expect: [200, 307], mustContain: "nicht freigeschaltet", mustNotContain: x.billing ? [`/billing/${x.billing.id}`] : [] },
...(x.billing ? [{ path: `/billing/${x.billing.id}`, expect: [404, 307] }] : []),
...(x.photo ? [{ path: `/files/${x.photo.documentId}`, expect: [200] }] : []),
{ path: m(x.d08), expect: [404] },
{ path: `/customers/${x.foreignCustomer.id}`, expect: [404, 307] },
@@ -181,6 +206,10 @@ function plans(x: Awaited<ReturnType<typeof ids>>): Plan[] {
...(x.emergency ? [{ path: `/work-orders/emergency-review/${x.emergency.id}`, expect: [404] }] : []),
...(x.imp ? [{ path: `/imports/${x.imp.id}`, expect: [404] }] : []),
...(x.photo ? [{ path: `/files/${x.photo.documentId}`, expect: [404] }] : []),
// L14 Abrechnungsübersicht: keine Einträge von demo
{ path: "/billing", mustContain: "Abrechnungsübersicht", mustNotContain: [x.d18.number, "Elbblick"] },
...(x.billing ? [{ path: `/billing/${x.billing.id}`, expect: [404] }, { path: `/billing/print?ids=${x.billing.id}`, mustNotContain: [x.d18.number] }] : []),
{ path: wo(x.d07, "?tab=billing"), expect: [404] },
],
},
];