/** * Compensation Service Integration Tests * * Tests all three compensation models and edge cases: * - PER_JOB: sum of rates for completed jobs * - SALARY: monthlySalary only (no per-job bonuses) * - HYBRID: monthlySalary + sum of rates * - Missing job type rate defaults to 0 (not error) * - Only COMPLETED jobs count (PENDING, IN_PROGRESS, CANCELLED excluded) * - CANCELLED jobs excluded * - Date range filter (jobs outside period not counted) * - Summary returns all technicians with correct totals * - Detail returns job-by-job breakdown * - Technician with no completed jobs = 0 job bonus * - Cross-tenant isolation * * CLEANUP ORDER: * jobOrders -> tickets -> ticketCategories -> jobTypeRates -> technicianProfiles * -> zones -> subscribers -> servicePlans -> users -> tenant * * These tests require a live PostgreSQL database connection. */ import { prisma } from "@/lib/prisma"; import { withTenantContext } from "@/lib/prisma-tenant"; import { BillingType, CompensationModel, JobOrderStatus, TenantStatus } from "@prisma/client"; import { createTechnicianProfile, listTechnicians, updateTechnicianProfile, getTechnicianProfile, getTechnicianProfileByUserId, } from "@/lib/services/technician-service"; import { getCompensationSummary, getTechnicianCompensationDetail, } from "@/lib/services/compensation-service"; // --------------------------------------------------------------------------- // Shared state // --------------------------------------------------------------------------- const TS = Date.now(); let tenantId: string; let tenantBId: string; let adminUserId: string; // Technician users let techAUserId: string; // HYBRID model let techBUserId: string; // PER_JOB model let techCUserId: string; // SALARY model // Profiles let profileAId: string; let profileBId: string; let profileCId: string; // Job type rates let installRateId: string; let repairRateId: string; // Subscriber + ticket for creating job orders let subscriberId: string; let ticketId: string; let ticketCategoryId: string; let planId: string; let zoneId: string; // Counters for unique order numbers let joCounter = 0; let ticketCounter = 0; function tA() { return withTenantContext(tenantId); } function tB() { return withTenantContext(tenantBId); } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- async function createJobOrder( assignedToId: string, jobType: string, status: JobOrderStatus, completedAt?: Date ) { joCounter++; const orderNumber = `JO-COMP-${TS}-${joCounter}`; const jo = await prisma.jobOrder.create({ data: { tenantId, orderNumber, ticketId, jobType, description: `Test job ${joCounter}`, assignedToId, createdById: adminUserId, status, scheduledDate: null, startedAt: status !== JobOrderStatus.PENDING ? new Date() : null, completedAt: completedAt ?? (status === JobOrderStatus.COMPLETED ? new Date() : null), cancelledAt: status === JobOrderStatus.CANCELLED ? new Date() : null, outcomeNotes: status === JobOrderStatus.COMPLETED ? "Work complete" : null, }, }); return jo; } async function createTicket(): Promise { ticketCounter++; const t = await prisma.ticket.create({ data: { tenantId, ticketNumber: `TKT-COMP-${TS}-${ticketCounter}`, subject: `Test Ticket ${ticketCounter}`, description: "Test", categoryId: ticketCategoryId, priority: "MEDIUM", status: "OPEN", source: "STAFF", createdById: adminUserId, }, }); return t.id; } // --------------------------------------------------------------------------- // Setup // --------------------------------------------------------------------------- beforeAll(async () => { // Tenant A const tenant = await prisma.tenant.create({ data: { name: `Compensation Test Tenant ${TS}`, slug: `comp-${TS}`, ownerEmail: `comp-${TS}@test.example`, status: TenantStatus.ACTIVE, }, }); tenantId = tenant.id; // Tenant B (for isolation) const tenantB = await prisma.tenant.create({ data: { name: `Compensation Test Tenant B ${TS}`, slug: `comp-b-${TS}`, ownerEmail: `comp-b-${TS}@test.example`, status: TenantStatus.ACTIVE, }, }); tenantBId = tenantB.id; // Admin user const admin = await prisma.user.create({ data: { tenantId, email: `comp-admin-${TS}@test.example`, passwordHash: "hashed", firstName: "Comp", lastName: "Admin", roles: ["ADMIN"], isActive: true, }, }); adminUserId = admin.id; // Zone const zone = await prisma.zone.create({ data: { tenantId, name: `Zone Comp ${TS}`, isActive: true, }, }); zoneId = zone.id; // Service plan const plan = await prisma.servicePlan.create({ data: { tenantId, name: `Comp Plan ${TS}`, speed: "50 Mbps", monthlyPrice: 49.99, billingType: BillingType.POSTPAID, isActive: true, }, }); planId = plan.id; // Subscriber const sub = await prisma.subscriber.create({ data: { tenantId, accountNumber: `COMP-SUB-${TS}`, firstName: "Comp", lastName: "Subscriber", address: "123 Comp St", servicePlanId: planId, status: "ACTIVE", billingDay: 15, creditBalance: 0, }, }); subscriberId = sub.id; // Ticket category const cat = await prisma.ticketCategory.create({ data: { tenantId, name: `Comp Category ${TS}`, isActive: true, }, }); ticketCategoryId = cat.id; // Initial ticket ticketId = await createTicket(); // Technician users const techA = await prisma.user.create({ data: { tenantId, email: `comp-tech-a-${TS}@test.example`, passwordHash: "hashed", firstName: "TechA", lastName: "Hybrid", roles: ["TECHNICIAN"], isActive: true, }, }); techAUserId = techA.id; const techB = await prisma.user.create({ data: { tenantId, email: `comp-tech-b-${TS}@test.example`, passwordHash: "hashed", firstName: "TechB", lastName: "PerJob", roles: ["TECHNICIAN"], isActive: true, }, }); techBUserId = techB.id; const techC = await prisma.user.create({ data: { tenantId, email: `comp-tech-c-${TS}@test.example`, passwordHash: "hashed", firstName: "TechC", lastName: "Salary", roles: ["TECHNICIAN"], isActive: true, }, }); techCUserId = techC.id; // Create technician profiles const profileA = await createTechnicianProfile(tA(), tenantId, { userId: techAUserId, compensationModel: CompensationModel.HYBRID, monthlySalary: 2000, skills: ["fiber", "installation"], zoneId, }); profileAId = profileA.id; const profileB = await createTechnicianProfile(tA(), tenantId, { userId: techBUserId, compensationModel: CompensationModel.PER_JOB, skills: ["repair"], }); profileBId = profileB.id; const profileC = await createTechnicianProfile(tA(), tenantId, { userId: techCUserId, compensationModel: CompensationModel.SALARY, monthlySalary: 3500, }); profileCId = profileC.id; // Job type rates const installRate = await prisma.jobTypeRate.create({ data: { tenantId, jobType: "Installation", rate: 150, isActive: true, }, }); installRateId = installRate.id; const repairRate = await prisma.jobTypeRate.create({ data: { tenantId, jobType: "Repair", rate: 75, isActive: true, }, }); repairRateId = repairRate.id; }); // --------------------------------------------------------------------------- // Teardown // --------------------------------------------------------------------------- afterAll(async () => { for (const tid of [tenantId, tenantBId]) { if (!tid) continue; // 1. Job orders (FK to tickets and users) await prisma.jobOrder.deleteMany({ where: { tenantId: tid } }).catch(() => {}); // 2. Tickets await prisma.ticket.deleteMany({ where: { tenantId: tid } }).catch(() => {}); // 3. Ticket categories await prisma.ticketCategory.deleteMany({ where: { tenantId: tid } }).catch(() => {}); // 4. Job type rates await prisma.jobTypeRate.deleteMany({ where: { tenantId: tid } }).catch(() => {}); // 5. Technician profiles await prisma.technicianProfile.deleteMany({ where: { tenantId: tid } }).catch(() => {}); // 6. Zone assignments await prisma.zoneAssignment.deleteMany({ where: { tenantId: tid } }).catch(() => {}); // 7. Subscribers await prisma.subscriber.deleteMany({ where: { tenantId: tid } }).catch(() => {}); // 8. Zones await prisma.zone.deleteMany({ where: { tenantId: tid } }).catch(() => {}); // 9. Service plans await prisma.servicePlan.deleteMany({ where: { tenantId: tid } }).catch(() => {}); // 10. Users await prisma.user.deleteMany({ where: { tenantId: tid } }).catch(() => {}); // 11. Tenant await prisma.tenant.delete({ where: { id: tid } }).catch(() => {}); } await prisma.$disconnect(); }); // =========================================================================== // TECHNICIAN SERVICE TESTS // =========================================================================== describe("createTechnicianProfile", () => { it("creates a profile for a TECHNICIAN user", async () => { const profile = await getTechnicianProfile(tA(), profileAId); expect(profile).not.toBeNull(); expect(profile!.userId).toBe(techAUserId); expect(profile!.compensationModel).toBe(CompensationModel.HYBRID); expect(Number(profile!.monthlySalary)).toBe(2000); expect(profile!.skills).toContain("fiber"); expect(profile!.zoneId).toBe(zoneId); }); it("throws if user does not have TECHNICIAN role", async () => { await expect( createTechnicianProfile(tA(), tenantId, { userId: adminUserId, compensationModel: CompensationModel.PER_JOB, }) ).rejects.toThrow(/TECHNICIAN role/i); }); it("throws if profile already exists for user", async () => { await expect( createTechnicianProfile(tA(), tenantId, { userId: techAUserId, compensationModel: CompensationModel.PER_JOB, }) ).rejects.toThrow(/already exists/i); }); it("throws if SALARY model has no monthlySalary", async () => { // Create a separate technician user for this test const extraTech = await prisma.user.create({ data: { tenantId, email: `comp-extra-${TS}@test.example`, passwordHash: "hashed", firstName: "Extra", lastName: "Tech", roles: ["TECHNICIAN"], isActive: true, }, }); await expect( createTechnicianProfile(tA(), tenantId, { userId: extraTech.id, compensationModel: CompensationModel.SALARY, // No monthlySalary }) ).rejects.toThrow(/monthlySalary is required/i); // Cleanup await prisma.user.delete({ where: { id: extraTech.id } }).catch(() => {}); }); }); describe("listTechnicians", () => { it("returns all technicians", async () => { const result = await listTechnicians(tA()); // Should have at least our 3 technicians expect(result.total).toBeGreaterThanOrEqual(3); const profileIds = result.profiles.map((p: { id: string }) => p.id); expect(profileIds).toContain(profileAId); expect(profileIds).toContain(profileBId); expect(profileIds).toContain(profileCId); }); it("filters by activeOnly", async () => { // All profiles are active by default const result = await listTechnicians(tA(), { activeOnly: true }); expect(result.total).toBeGreaterThanOrEqual(3); }); it("filters by zoneId", async () => { const result = await listTechnicians(tA(), { zoneId }); const profileIds = result.profiles.map((p: { id: string }) => p.id); expect(profileIds).toContain(profileAId); // techA is in this zone expect(profileIds).not.toContain(profileBId); // techB has no zone }); }); describe("updateTechnicianProfile", () => { it("updates phone and skills", async () => { const updated = await updateTechnicianProfile(tA(), profileBId, { phone: "555-1234", skills: ["repair", "maintenance"], }); expect(updated.phone).toBe("555-1234"); expect(updated.skills).toContain("maintenance"); }); it("deactivates a profile", async () => { await updateTechnicianProfile(tA(), profileBId, { isActive: false }); const profile = await getTechnicianProfile(tA(), profileBId); expect(profile!.isActive).toBe(false); // Reactivate for other tests await updateTechnicianProfile(tA(), profileBId, { isActive: true }); }); }); describe("getTechnicianProfileByUserId", () => { it("finds profile by userId", async () => { const profile = await getTechnicianProfileByUserId(tA(), techCUserId); expect(profile).not.toBeNull(); expect(profile!.id).toBe(profileCId); expect(profile!.compensationModel).toBe(CompensationModel.SALARY); }); it("returns null for user without profile", async () => { const profile = await getTechnicianProfileByUserId(tA(), adminUserId); expect(profile).toBeNull(); }); }); // =========================================================================== // COMPENSATION SERVICE TESTS // =========================================================================== describe("PER_JOB compensation model", () => { it("sums rates for all completed jobs in period", async () => { // TechB (PER_JOB): 2x Installation ($150 each) + 1x Repair ($75) const periodStart = new Date("2026-02-01T00:00:00Z"); const periodEnd = new Date("2026-02-28T23:59:59Z"); await createJobOrder(techBUserId, "Installation", JobOrderStatus.COMPLETED, new Date("2026-02-10T10:00:00Z")); await createJobOrder(techBUserId, "Installation", JobOrderStatus.COMPLETED, new Date("2026-02-15T10:00:00Z")); await createJobOrder(techBUserId, "Repair", JobOrderStatus.COMPLETED, new Date("2026-02-20T10:00:00Z")); const summary = await getCompensationSummary(tA(), { periodStart, periodEnd, technicianProfileId: profileBId }); const techSummary = summary.technicians.find((t: { technicianProfileId: string }) => t.technicianProfileId === profileBId); expect(techSummary).toBeDefined(); expect(Number(techSummary!.baseSalary)).toBe(0); expect(Number(techSummary!.jobBonusTotal)).toBe(375); // 150 + 150 + 75 expect(Number(techSummary!.totalCompensation)).toBe(375); expect(techSummary!.completedJobCount).toBe(3); }); }); describe("SALARY compensation model", () => { it("returns monthlySalary only, regardless of completed jobs", async () => { const periodStart = new Date("2026-02-01T00:00:00Z"); const periodEnd = new Date("2026-02-28T23:59:59Z"); // Create a completed job for techC — but SALARY model should ignore it await createJobOrder(techCUserId, "Installation", JobOrderStatus.COMPLETED, new Date("2026-02-10T10:00:00Z")); const summary = await getCompensationSummary(tA(), { periodStart, periodEnd, technicianProfileId: profileCId }); const techSummary = summary.technicians.find((t: { technicianProfileId: string }) => t.technicianProfileId === profileCId); expect(techSummary).toBeDefined(); expect(Number(techSummary!.baseSalary)).toBe(3500); expect(Number(techSummary!.jobBonusTotal)).toBe(0); // SALARY model has no per-job bonuses expect(Number(techSummary!.totalCompensation)).toBe(3500); }); }); describe("HYBRID compensation model", () => { it("sums base salary + per-job rates", async () => { const periodStart = new Date("2026-02-01T00:00:00Z"); const periodEnd = new Date("2026-02-28T23:59:59Z"); // TechA (HYBRID, $2000 salary): 1x Installation ($150) await createJobOrder(techAUserId, "Installation", JobOrderStatus.COMPLETED, new Date("2026-02-12T10:00:00Z")); const summary = await getCompensationSummary(tA(), { periodStart, periodEnd, technicianProfileId: profileAId }); const techSummary = summary.technicians.find((t: { technicianProfileId: string }) => t.technicianProfileId === profileAId); expect(techSummary).toBeDefined(); expect(Number(techSummary!.baseSalary)).toBe(2000); expect(Number(techSummary!.jobBonusTotal)).toBeGreaterThanOrEqual(150); // at least 1 installation expect(Number(techSummary!.totalCompensation)).toBe( Number(techSummary!.baseSalary) + Number(techSummary!.jobBonusTotal) ); }); }); describe("Missing job type rate defaults to 0", () => { it("returns 0 bonus for unknown job type (not an error)", async () => { const periodStart = new Date("2026-03-01T00:00:00Z"); const periodEnd = new Date("2026-03-31T23:59:59Z"); // Create job with a type that has NO rate configured await createJobOrder(techBUserId, "UnknownJobType-no-rate", JobOrderStatus.COMPLETED, new Date("2026-03-05T10:00:00Z")); const summary = await getCompensationSummary(tA(), { periodStart, periodEnd, technicianProfileId: profileBId }); const techSummary = summary.technicians.find((t: { technicianProfileId: string }) => t.technicianProfileId === profileBId); // Should not throw; unknown job type = 0 bonus expect(techSummary).toBeDefined(); // The unknown job type contributes $0 const unknownJobDetail = await getTechnicianCompensationDetail(tA(), { technicianProfileId: profileBId, periodStart, periodEnd, }); const unknownJob = unknownJobDetail.jobs.find((j: { jobType: string }) => j.jobType === "UnknownJobType-no-rate"); expect(unknownJob).toBeDefined(); expect(Number(unknownJob!.rate)).toBe(0); }); }); describe("Only COMPLETED jobs count", () => { it("excludes PENDING, IN_PROGRESS, and CANCELLED jobs", async () => { const periodStart = new Date("2026-03-01T00:00:00Z"); const periodEnd = new Date("2026-03-31T23:59:59Z"); // Only the COMPLETED job at the start (from previous test) and explicitly add more here // Add PENDING, IN_PROGRESS, CANCELLED jobs — none should count await createJobOrder(techBUserId, "Installation", JobOrderStatus.PENDING); await createJobOrder(techBUserId, "Installation", JobOrderStatus.IN_PROGRESS); await createJobOrder(techBUserId, "Installation", JobOrderStatus.CANCELLED); const summary = await getCompensationSummary(tA(), { periodStart, periodEnd, technicianProfileId: profileBId }); const techSummary = summary.technicians.find((t: { technicianProfileId: string }) => t.technicianProfileId === profileBId); // completedJobCount should only include COMPLETED jobs (the unknown type one from previous test) // PENDING/IN_PROGRESS/CANCELLED are excluded expect(techSummary).toBeDefined(); // Verify the count equals the number of COMPLETED jobs only const allJobsInPeriod = await prisma.jobOrder.findMany({ where: { tenantId, assignedToId: techBUserId, completedAt: { gte: periodStart, lte: periodEnd }, status: JobOrderStatus.COMPLETED, }, }); expect(techSummary!.completedJobCount).toBe(allJobsInPeriod.length); }); it("CANCELLED jobs do not generate bonuses", async () => { const periodStart = new Date("2026-03-01T00:00:00Z"); const periodEnd = new Date("2026-03-31T23:59:59Z"); const detail = await getTechnicianCompensationDetail(tA(), { technicianProfileId: profileBId, periodStart, periodEnd, }); // All jobs in detail should be COMPLETED (by definition, as we only fetch COMPLETED) for (const job of detail.jobs) { expect(job.completedAt).not.toBeNull(); } }); }); describe("Date range filter", () => { it("excludes jobs outside the period", async () => { // Jobs from Feb 2026 (already created above) // Check March period only returns March jobs const marchStart = new Date("2026-03-01T00:00:00Z"); const marchEnd = new Date("2026-03-31T23:59:59Z"); const detail = await getTechnicianCompensationDetail(tA(), { technicianProfileId: profileBId, periodStart: marchStart, periodEnd: marchEnd, }); // All returned jobs should be within March for (const job of detail.jobs) { expect(job.completedAt.getTime()).toBeGreaterThanOrEqual(marchStart.getTime()); expect(job.completedAt.getTime()).toBeLessThanOrEqual(marchEnd.getTime()); } }); it("February jobs not included in March report", async () => { const marchStart = new Date("2026-03-01T00:00:00Z"); const marchEnd = new Date("2026-03-31T23:59:59Z"); const detail = await getTechnicianCompensationDetail(tA(), { technicianProfileId: profileBId, periodStart: marchStart, periodEnd: marchEnd, }); // Feb jobs (completedAt in Feb) should not appear const febJobs = detail.jobs.filter((j: { completedAt: Date }) => j.completedAt < marchStart); expect(febJobs).toHaveLength(0); }); }); describe("Summary report returns all technicians", () => { it("includes all technicians with correct totals", async () => { const periodStart = new Date("2026-02-01T00:00:00Z"); const periodEnd = new Date("2026-02-28T23:59:59Z"); const summary = await getCompensationSummary(tA(), { periodStart, periodEnd }); // Should include all 3 profiles const profileIds = summary.technicians.map((t: { technicianProfileId: string }) => t.technicianProfileId); expect(profileIds).toContain(profileAId); expect(profileIds).toContain(profileBId); expect(profileIds).toContain(profileCId); // grandTotal should be sum of all technician totals const sumOfTotals = summary.technicians.reduce( (sum: number, t: { totalCompensation: { toNumber: () => number } }) => sum + t.totalCompensation.toNumber(), 0 ); expect(Number(summary.grandTotal)).toBeCloseTo(sumOfTotals, 2); }); }); describe("Detail report returns job-by-job breakdown", () => { it("returns each completed job with rate", async () => { const periodStart = new Date("2026-02-01T00:00:00Z"); const periodEnd = new Date("2026-02-28T23:59:59Z"); const detail = await getTechnicianCompensationDetail(tA(), { technicianProfileId: profileBId, periodStart, periodEnd, }); // Should include job-by-job detail expect(detail.jobs.length).toBeGreaterThan(0); for (const job of detail.jobs) { expect(job.jobOrderId).toBeDefined(); expect(job.orderNumber).toBeDefined(); expect(job.jobType).toBeDefined(); expect(job.completedAt).toBeDefined(); expect(job.rate).toBeDefined(); expect(job.ticketId).toBeDefined(); expect(job.ticketNumber).toBeDefined(); } // Verify totals consistent const sumRates = detail.jobs.reduce( (sum: number, j: { rate: { toNumber: () => number } }) => sum + j.rate.toNumber(), 0 ); expect(Number(detail.jobBonusTotal)).toBeCloseTo(sumRates, 2); }); it("throws if technician profile not found", async () => { await expect( getTechnicianCompensationDetail(tA(), { technicianProfileId: "non-existent-profile-id", periodStart: new Date("2026-02-01"), periodEnd: new Date("2026-02-28"), }) ).rejects.toThrow(/not found/i); }); }); describe("Technician with no completed jobs", () => { it("returns 0 job bonus for technician with no completed jobs in period", async () => { // Create a brand new technician with no jobs const newTech = await prisma.user.create({ data: { tenantId, email: `comp-new-tech-${TS}@test.example`, passwordHash: "hashed", firstName: "NoJobs", lastName: "Tech", roles: ["TECHNICIAN"], isActive: true, }, }); const newProfile = await createTechnicianProfile(tA(), tenantId, { userId: newTech.id, compensationModel: CompensationModel.PER_JOB, }); const periodStart = new Date("2026-02-01T00:00:00Z"); const periodEnd = new Date("2026-02-28T23:59:59Z"); const summary = await getCompensationSummary(tA(), { periodStart, periodEnd, technicianProfileId: newProfile.id, }); const techSummary = summary.technicians.find( (t: { technicianProfileId: string }) => t.technicianProfileId === newProfile.id ); expect(techSummary).toBeDefined(); expect(Number(techSummary!.jobBonusTotal)).toBe(0); expect(Number(techSummary!.baseSalary)).toBe(0); expect(Number(techSummary!.totalCompensation)).toBe(0); expect(techSummary!.completedJobCount).toBe(0); // Cleanup await prisma.technicianProfile.delete({ where: { id: newProfile.id } }).catch(() => {}); await prisma.user.delete({ where: { id: newTech.id } }).catch(() => {}); }); }); describe("Cross-tenant isolation", () => { it("Tenant B technicians do not appear in Tenant A summary", async () => { // Create a technician in Tenant B const techB_user = await prisma.user.create({ data: { tenantId: tenantBId, email: `comp-tech-b-isolation-${TS}@test.example`, passwordHash: "hashed", firstName: "TenantB", lastName: "Tech", roles: ["TECHNICIAN"], isActive: true, }, }); const profileB = await createTechnicianProfile(tB(), tenantBId, { userId: techB_user.id, compensationModel: CompensationModel.PER_JOB, }); const periodStart = new Date("2026-02-01T00:00:00Z"); const periodEnd = new Date("2026-02-28T23:59:59Z"); const summaryA = await getCompensationSummary(tA(), { periodStart, periodEnd }); // Tenant B profile should not appear in Tenant A's report const profileIds = summaryA.technicians.map((t: { technicianProfileId: string }) => t.technicianProfileId); expect(profileIds).not.toContain(profileB.id); // Cleanup Tenant B technician await prisma.technicianProfile.delete({ where: { id: profileB.id } }).catch(() => {}); await prisma.user.delete({ where: { id: techB_user.id } }).catch(() => {}); }); it("job type rates are tenant-scoped", async () => { // Tenant B should not see Tenant A's rates const tenantBRates = await tB().jobTypeRate.findMany({}); const tenantAInstallRate = await tA().jobTypeRate.findFirst({ where: { id: installRateId } }); // Tenant B has no rates (we only created rates for Tenant A) const tenantBRateIds = tenantBRates.map((r: { id: string }) => r.id); expect(tenantBRateIds).not.toContain(installRateId); expect(tenantAInstallRate).not.toBeNull(); }); }); describe("Compensation detail baseSalary and jobBonusTotal", () => { it("HYBRID detail includes both baseSalary and job breakdown", async () => { const periodStart = new Date("2026-02-01T00:00:00Z"); const periodEnd = new Date("2026-02-28T23:59:59Z"); const detail = await getTechnicianCompensationDetail(tA(), { technicianProfileId: profileAId, periodStart, periodEnd, }); expect(detail.compensationModel).toBe(CompensationModel.HYBRID); expect(Number(detail.baseSalary)).toBe(2000); expect(detail.jobs.length).toBeGreaterThan(0); expect(Number(detail.totalCompensation)).toBe( Number(detail.baseSalary) + Number(detail.jobBonusTotal) ); }); it("SALARY detail shows salary with no jobs counted for bonus", async () => { const periodStart = new Date("2026-02-01T00:00:00Z"); const periodEnd = new Date("2026-02-28T23:59:59Z"); const detail = await getTechnicianCompensationDetail(tA(), { technicianProfileId: profileCId, periodStart, periodEnd, }); expect(detail.compensationModel).toBe(CompensationModel.SALARY); expect(Number(detail.baseSalary)).toBe(3500); expect(Number(detail.jobBonusTotal)).toBe(0); // Jobs array may have entries, but their rate should be 0 for SALARY model for (const job of detail.jobs) { expect(Number(job.rate)).toBe(0); } }); });