Files
craftvia/scripts/billing-backfill.ts
msolarczekandClaude Opus 5 2df785dd14 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>
2026-09-15 10:36:10 +02:00

61 lines
2.4 KiB
TypeScript

/**
* 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 */
}
});