- 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>
134 lines
5.7 KiB
TypeScript
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);
|
|
},
|
|
};
|
|
}
|