Files
craftvia/scripts/lib/einsatz-fixture.ts
T
msolarczekandClaude Opus 5 952e74cddd L4 Einsatz mobil: Field-Services, Sync-API, Uploads und Tests
- services/field: Einsatz-Sessions (Anfahrt/Arbeit/Pause als TimeEntry-Segmente,
  eine aktive Session je User+Auftrag), Zeitkorrektur mit Recht + Grund + Audit,
  Checkliste, Material (Abweichung nur mit Begründung, Zusatzmaterial), Notizen,
  Fotos, Sprachnotizen (ohne Transkriptions-Processor Status disabled), Uploads
  (idempotent je Mandant), autorisierte Dokument-Auslieferung, Lesemodelle + Bundle
- services/sync: applyOperations mit Idempotenz, baseVersion-Konfliktprüfung,
  Registry für Ops anderer Lanes, lane-lokaler requireApiContext
- /api/v1/sync, /api/v1/uploads, /api/v1/field/bundle, /api/v1/field/documents/[id]
- lib/sync/ops.ts (Zod-Payloads je opType), lib/field/material-rules.ts
- Stubs mit Vertragssignatur: transitionWorkOrder (L2), storeFile (§4.3),
  getSiteHistory (L1)
- Processor image-derivatives + Registrierung, Audit-Entity-Labels
- Tests: test-einsatz-field (48 Prüfungen), test-einsatz-sync (38 Prüfungen)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:29:48 +02:00

134 lines
5.7 KiB
TypeScript

// Shared fixture of the lane L4 tests (scripts/test-einsatz-*.ts): two zz test tenants with
// users, customer, site, team and work orders. Not a test itself (the runner only picks up
// scripts/test-*.ts at top level).
import { Prisma } from "@prisma/client";
import { prisma, dbForTenant } from "../../src/server/db";
import { ROLE_DEFS, type RoleKey } from "../../src/server/rbac";
import type { ServiceCtx } from "../../src/server/services/context";
export let failures = 0;
export const ok = (cond: boolean, msg: string) => {
console.log(`${cond ? "✓" : "✗ FEHLER"} ${msg}`);
if (!cond) failures++;
};
/** Expects `fn` to throw a ServiceError with the given code. */
export async function expectCode(fn: () => Promise<unknown>, code: string, msg: string) {
try {
await fn();
ok(false, `${msg} — kein Fehler (erwartet ${code})`);
} catch (err) {
const actual = (err as { code?: string }).code;
ok(actual === code, `${msg}${actual === code ? "" : ` — Code ${actual ?? (err as Error).message}`}`);
}
}
export function ctxFor(tenantId: string, userId: string, role: RoleKey): ServiceCtx {
return { db: dbForTenant(tenantId), tenantId, userId, permissions: new Set<string>(ROLE_DEFS[role].permissions) };
}
async function cleanupSlug(slug: string, emailDomain: string) {
const tenant = await prisma.tenant.findUnique({ where: { slug }, select: { id: true } });
if (tenant) {
const where = { tenantId: tenant.id };
await prisma.auditLog.deleteMany({ where });
await prisma.syncOperation.deleteMany({ where });
await prisma.photo.deleteMany({ where });
await prisma.voiceNote.deleteMany({ where });
await prisma.document.deleteMany({ where });
await prisma.workOrder.deleteMany({ where });
await prisma.teamMember.deleteMany({ where });
await prisma.team.deleteMany({ where });
await prisma.site.deleteMany({ where });
await prisma.contact.deleteMany({ where });
await prisma.customer.deleteMany({ where });
await prisma.numberSequence.deleteMany({ where });
await prisma.userRole.deleteMany({ where: { user: { tenantId: tenant.id } } });
await prisma.user.deleteMany({ where });
await prisma.role.deleteMany({ where });
await prisma.tenant.delete({ where: { id: tenant.id } });
}
await prisma.identity.deleteMany({ where: { email: { endsWith: emailDomain }, memberships: { none: {} } } });
}
async function user(tenantId: string, email: string, name: string) {
const identity = await prisma.identity.upsert({ where: { email }, update: {}, create: { email, passwordHash: "x" } });
return prisma.user.create({ data: { tenantId, identityId: identity.id, email, name } });
}
export type Fixture = Awaited<ReturnType<typeof createFixture>>;
export async function createFixture(prefix: string) {
const slugA = `zz-${prefix}-a`;
const slugB = `zz-${prefix}-b`;
const domain = `@zz-${prefix}.test`;
await cleanupSlug(slugA, domain);
await cleanupSlug(slugB, domain);
const tenantA = await prisma.tenant.create({ data: { name: `L4 ${prefix} A`, slug: slugA } });
const tenantB = await prisma.tenant.create({ data: { name: `L4 ${prefix} B`, slug: slugB } });
const tech = await user(tenantA.id, `tech${domain}`, "Tech A");
const lead = await user(tenantA.id, `lead${domain}`, "Lead A");
const outsider = await user(tenantA.id, `outsider${domain}`, "Outsider A");
const techB = await user(tenantB.id, `techb${domain}`, "Tech B");
const customerA = await prisma.customer.create({ data: { tenantId: tenantA.id, companyName: "Kunde A GmbH", city: "Hamburg" } });
const siteA = await prisma.site.create({ data: { tenantId: tenantA.id, customerId: customerA.id, name: "Halle 1", street: "Hafenstraße", houseNumber: "1", postalCode: "20457", city: "Hamburg", accessNotes: "Schlüssel beim Pförtner" } });
const team = await prisma.team.create({ data: { tenantId: tenantA.id, name: `Team ${prefix}`, leaderUserId: lead.id } });
const now = new Date();
const orderA = await prisma.workOrder.create({
data: {
tenantId: tenantA.id,
number: `A-${prefix}-1`,
customerId: customerA.id,
siteId: siteA.id,
title: "Heizung montieren",
status: "assigned",
plannedStart: new Date(now.getTime() - 60 * 60 * 1000),
plannedEnd: new Date(now.getTime() + 60 * 60 * 1000),
teamLeadUserId: lead.id,
assignees: { create: [{ tenantId: tenantA.id, userId: tech.id }] },
checklistItems: { create: [{ tenantId: tenantA.id, key: "safe", label: "Arbeitsbereich abgesichert", required: true }] },
photoRequirements: { create: [{ tenantId: tenantA.id, key: "typenschild", label: "Typenschild" }] },
materialPlans: { create: [{ tenantId: tenantA.id, name: "Kupferrohr 15 mm", plannedQuantity: new Prisma.Decimal(10), unit: "m" }] },
},
include: { checklistItems: true, photoRequirements: true, materialPlans: true },
});
const customerB = await prisma.customer.create({ data: { tenantId: tenantB.id, companyName: "Kunde B AG" } });
const orderB = await prisma.workOrder.create({
data: {
tenantId: tenantB.id,
number: `A-${prefix}-B1`,
customerId: customerB.id,
title: "Wartung B",
status: "assigned",
assignees: { create: [{ tenantId: tenantB.id, userId: techB.id }] },
},
});
return {
tenantA,
tenantB,
tech,
lead,
outsider,
techB,
team,
siteA,
orderA,
orderB,
ctxTech: ctxFor(tenantA.id, tech.id, "technician"),
ctxLead: ctxFor(tenantA.id, lead.id, "team-lead"),
ctxOutsider: ctxFor(tenantA.id, outsider.id, "technician"),
ctxB: ctxFor(tenantB.id, techB.id, "technician"),
cleanup: async () => {
await cleanupSlug(slugA, domain);
await cleanupSlug(slugB, domain);
},
};
}