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:
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* L14 Abrechnungsübersicht — Backfill der offenen Abrechnungseinträge für Bestandsdaten.
|
||||
*
|
||||
* Legt je Mandant fehlende `open`-Einträge an (idempotent, services/billing/candidates.ts):
|
||||
* Aufträge in „Zur Abrechnung freigegeben“, bestätigte Meilensteine ohne Eintrag und freigegebene
|
||||
* Tagesberichte von Aufträgen ohne Meilensteine. Bereits abgerechnete Aufträge (Status billed) bekommen
|
||||
* bewusst KEINEN offenen Eintrag.
|
||||
*
|
||||
* Lauf: npx tsx scripts/billing-backfill.ts (alle Mandanten)
|
||||
* npx tsx scripts/billing-backfill.ts --tenant demo (ein Mandant)
|
||||
*/
|
||||
import "dotenv/config";
|
||||
import { dbForTenant, prisma } from "../src/server/db";
|
||||
import type { ServiceCtx } from "../src/server/services/context";
|
||||
import { syncBillingCandidates } from "../src/server/services/billing/candidates";
|
||||
|
||||
async function main() {
|
||||
const idx = process.argv.indexOf("--tenant");
|
||||
const slug = idx > 0 ? process.argv[idx + 1] : undefined;
|
||||
const tenants = await prisma.tenant.findMany({ where: slug ? { slug } : {}, select: { id: true, slug: true } });
|
||||
if (slug && tenants.length === 0) throw new Error(`Mandant ${slug} nicht gefunden`);
|
||||
|
||||
let total = 0;
|
||||
for (const t of tenants) {
|
||||
const ctx: ServiceCtx = { db: dbForTenant(t.id), tenantId: t.id, userId: "system", permissions: new Set() };
|
||||
const orders = await ctx.db.workOrder.findMany({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
OR: [
|
||||
{ status: "released_for_billing" },
|
||||
{ milestones: { some: { status: "confirmed", billingRecordId: null, deletedAt: null } } },
|
||||
{ reports: { some: { type: "daily", status: "approved" } } },
|
||||
],
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
let created = 0;
|
||||
for (const o of orders) created += (await syncBillingCandidates(ctx, { workOrderId: o.id })).created;
|
||||
total += created;
|
||||
console.log(`✔ ${t.slug}: ${orders.length} Aufträge geprüft, ${created} Einträge angelegt`);
|
||||
}
|
||||
console.log(`\nFertig — ${total} Abrechnungseinträge angelegt.`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exitCode = 1;
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
try {
|
||||
const { closeJobQueues } = await import("../src/server/jobs/queues");
|
||||
await closeJobQueues();
|
||||
const { closeQueues } = await import("../src/server/mail/queue");
|
||||
await closeQueues();
|
||||
} catch {
|
||||
/* queues optional */
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user