--- phase: 03-operational-modules plan: "05" type: execute wave: 3 depends_on: ["03-04"] files_modified: - prisma/schema.prisma - src/lib/prisma-tenant.ts - src/lib/services/technician-service.ts - src/lib/services/compensation-service.ts - src/app/api/technicians/route.ts - src/app/api/technicians/[id]/route.ts - src/app/api/technicians/[id]/compensation/route.ts - src/app/api/job-type-rates/route.ts - src/app/api/job-type-rates/[id]/route.ts - src/app/api/reports/compensation/route.ts - src/lib/__tests__/compensation-service.test.ts autonomous: true must_haves: truths: - "Admin can create technician profiles with contact info, skills, zone, and compensation model" - "Admin can configure flat per-job compensation rates by job type at tenant level" - "System supports hybrid compensation: base salary PLUS per-job bonuses (both optional)" - "Only COMPLETED job orders count toward per-job compensation" - "Missing job type rate defaults to 0 bonus (not error)" - "Compensation summary shows per-technician totals: base salary, job bonuses, total" - "Compensation summary supports drill-down to job-by-job detail" artifacts: - path: "prisma/schema.prisma" provides: "TechnicianProfile and JobTypeRate models with compensation enums" contains: "model TechnicianProfile" - path: "src/lib/services/technician-service.ts" provides: "Technician profile CRUD" exports: ["createTechnicianProfile", "updateTechnicianProfile", "getTechnicianProfile", "listTechnicians"] - path: "src/lib/services/compensation-service.ts" provides: "Compensation calculation and summary report" exports: ["getCompensationSummary", "getTechnicianCompensationDetail"] - path: "src/lib/__tests__/compensation-service.test.ts" provides: "Tests for all compensation models (per-job, salary, hybrid) and edge cases" min_lines: 120 key_links: - from: "src/lib/services/compensation-service.ts" to: "prisma/schema.prisma" via: "queries JobOrder (COMPLETED, in period) and JobTypeRate for rate lookup" pattern: "jobOrder\\.findMany|jobTypeRate\\.findMany" - from: "src/lib/services/compensation-service.ts" to: "src/lib/services/technician-service.ts" via: "reads TechnicianProfile for compensation model and base salary" pattern: "technicianProfile" --- Create the technician management system: TechnicianProfile model (skills, zone, compensation model), JobTypeRate model (per-job rates by type), CompensationService (hybrid salary + per-job calculation), and compensation summary report with drill-down. Purpose: Enables admin to track technician compensation across per-job, salary, and hybrid models. Only completed jobs count, missing rates default to zero, and the summary provides both overview and detail views. Output: TechnicianProfile/JobTypeRate models, technician-service.ts, compensation-service.ts, 6 API routes, integration tests. @C:\Users\KevinAsprec\.claude/get-shit-done/workflows/execute-plan.md @C:\Users\KevinAsprec\.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/03-operational-modules/03-CONTEXT.md @.planning/phases/03-operational-modules/03-RESEARCH.md @.planning/phases/03-operational-modules/03-04-SUMMARY.md @prisma/schema.prisma @src/lib/prisma-tenant.ts @src/lib/services/job-order-service.ts @src/lib/__tests__/payment.test.ts (test pattern reference) Task 1: TechnicianProfile/JobTypeRate schema, migration, tenant scoping prisma/schema.prisma src/lib/prisma-tenant.ts 1. Add enum to schema.prisma: - `enum CompensationModel { PER_JOB SALARY HYBRID }` 2. Add TechnicianProfile model: - id (uuid), tenantId - userId (String, FK to User — the technician user, @@unique with tenantId) - phone (String?) - skills (String[]) — PostgreSQL array, e.g., ["Installation", "Repair", "Fiber Splicing"] - zoneId (String?, FK to Zone — primary assigned zone) - compensationModel (CompensationModel, default PER_JOB) - monthlySalary (Decimal? @db.Decimal(10,2)) — null for PER_JOB model, set for SALARY/HYBRID - isActive (Boolean, default true) - createdAt, updatedAt - Relations: user -> User, zone -> Zone - @@unique([tenantId, userId]) — one profile per user per tenant - @@index([tenantId]) 3. Add JobTypeRate model (tenant-level rates, not per-technician): - id (uuid), tenantId - jobType (String) — e.g., "Installation", "Repair" — matches JobOrder.jobType - rate (Decimal @db.Decimal(10,2)) — flat rate per completed job of this type - description (String?) - isActive (Boolean, default true) - createdAt, updatedAt - @@unique([tenantId, jobType]) - @@index([tenantId]) 4. Add reverse relations: - User: `technicianProfile TechnicianProfile?` - Zone: `technicianProfiles TechnicianProfile[]` 5. Run `npx prisma migrate dev --name add-technician-profiles` 6. Add TechnicianProfile and JobTypeRate to TENANT_SCOPED_MODELS in prisma-tenant.ts with FULL 12-operation extension blocks. - `npx prisma migrate dev` succeeds - `npx tsc --noEmit` passes - Grep prisma-tenant.ts confirms "technicianProfile" and "jobTypeRate" in TENANT_SCOPED_MODELS TechnicianProfile and JobTypeRate models exist, migration applied, tenant scoping configured. Task 2: Technician service, compensation service, APIs, and integration tests src/lib/services/technician-service.ts src/lib/services/compensation-service.ts src/app/api/technicians/route.ts src/app/api/technicians/[id]/route.ts src/app/api/technicians/[id]/compensation/route.ts src/app/api/job-type-rates/route.ts src/app/api/job-type-rates/[id]/route.ts src/app/api/reports/compensation/route.ts src/lib/__tests__/compensation-service.test.ts 1. Create src/lib/services/technician-service.ts: - `createTechnicianProfile(tenantPrisma, tenantId, { userId, phone?, skills?, zoneId?, compensationModel?, monthlySalary? })`: - Validate user exists and has TECHNICIAN role - Create TechnicianProfile - `updateTechnicianProfile(tenantPrisma, profileId, { phone?, skills?, zoneId?, compensationModel?, monthlySalary?, isActive? })`: - If compensationModel changes to PER_JOB, set monthlySalary to null - If compensationModel is SALARY or HYBRID and monthlySalary is not provided, throw - `getTechnicianProfile(tenantPrisma, profileId)` — include user, zone - `getTechnicianProfileByUserId(tenantPrisma, userId)` — lookup by user - `listTechnicians(tenantPrisma, { activeOnly?, zoneId? })` — list with optional filters, include user name, zone, compensation model 2. Create src/lib/services/compensation-service.ts: - `getCompensationSummary(tenantPrisma, { periodStart: Date, periodEnd: Date, technicianProfileId? })`: a. Load all active technician profiles (or specific one if filtered) b. For each technician: - Load COMPLETED job orders where completedAt is between periodStart and periodEnd AND assignedToId = profile.userId - Load all JobTypeRates for the tenant. Build rateMap: Map - Calculate jobBonusTotal: for each completed job, look up rateMap.get(job.jobType) ?? 0 (missing rate = 0, not error per RESEARCH pitfall 6) - Calculate baseSalary: if compensationModel is SALARY or HYBRID, use profile.monthlySalary ?? 0. If PER_JOB, baseSalary = 0. - totalCompensation = baseSalary + jobBonusTotal - completedJobCount = number of completed jobs c. Return array of { technicianProfileId, technicianName, compensationModel, baseSalary, jobBonusTotal, totalCompensation, completedJobCount } - `getTechnicianCompensationDetail(tenantPrisma, { technicianProfileId, periodStart: Date, periodEnd: Date })`: a. Load technician profile with user b. Load completed job orders in period for this technician c. Load rate map d. Return { profile info, baseSalary, jobs: [{ orderNumber, jobType, completedAt, rate (from map, 0 if missing), ticketNumber }], jobBonusTotal, totalCompensation } 3. Create API routes: - GET /api/technicians: withPermission("read", "User") -> listTechnicians (admin/staff) - POST /api/technicians: withPermission("manage", "User") -> createTechnicianProfile (admin only) - GET /api/technicians/[id]: withPermission("read", "User") -> getTechnicianProfile - PUT /api/technicians/[id]: withPermission("manage", "User") -> updateTechnicianProfile - GET /api/technicians/[id]/compensation: withPermission("read", "Report") -> getTechnicianCompensationDetail (query: periodStart, periodEnd) - GET /api/job-type-rates: withPermission("read", "Report") -> list all rates - POST /api/job-type-rates: withPermission("manage", "User") -> create rate (admin) - PUT /api/job-type-rates/[id]: withPermission("manage", "User") -> update rate - GET /api/reports/compensation: withPermission("read", "Report") -> getCompensationSummary (query: periodStart, periodEnd, technicianProfileId?) 4. Create src/lib/__tests__/compensation-service.test.ts: - Setup: create tenant, admin user, 2 technician users (tech1: HYBRID model with monthlySalary=10000, tech2: PER_JOB model), create technician profiles, create job type rates (Installation=500, Repair=300), create subscriber, create ticket, create job orders assigned to technicians, complete some job orders with different job types - Test: PER_JOB technician — compensation = sum of rates for completed jobs only - Test: SALARY technician — compensation = monthlySalary only (no job bonus) - Test: HYBRID technician — compensation = monthlySalary + sum of rates - Test: missing job type rate defaults to 0 (not error) — create completed job with job type "Custom" that has no rate entry - Test: only COMPLETED jobs count — PENDING and IN_PROGRESS jobs excluded - Test: CANCELLED jobs excluded from compensation - Test: date range filter — only jobs completed within period - Test: getCompensationSummary returns all technicians with correct totals - Test: getTechnicianCompensationDetail returns job-by-job breakdown - Test: technician with no completed jobs in period shows 0 job bonus - Cleanup: jobOrders -> tickets -> ticketCategories -> jobTypeRates -> technicianProfiles -> zones -> subscribers -> servicePlans -> users -> tenant - `npx vitest run src/lib/__tests__/compensation-service.test.ts` — all tests pass - `npx tsc --noEmit` passes Technician profiles created with compensation model config, job type rates configurable at tenant level, compensation calculation correct for PER_JOB/SALARY/HYBRID models, missing rates default to 0, only completed jobs count, summary and detail reports work, all tests pass. - `npx prisma migrate dev` succeeds - `npx tsc --noEmit` passes - `npx vitest run src/lib/__tests__/compensation-service.test.ts` — all green - All three compensation models produce correct results - Missing rate edge case handled (0, not error) - TechnicianProfile with compensation model (PER_JOB, SALARY, HYBRID) - JobTypeRate for tenant-level per-job rates - CompensationService correctly calculates all three models - Missing job type rate = 0 bonus (not error) - Only COMPLETED jobs in date range count - Summary and detail endpoints work - Cross-tenant isolation - All integration tests pass After completion, create `.planning/phases/03-operational-modules/03-05-SUMMARY.md`