Phase 03: Operational Modules - 5 plans in 3 waves - Wave 1: 03-01 (zones), 03-03 (tickets) — parallel - Wave 2: 03-02 (collector collections), 03-04 (job orders) — parallel - Wave 3: 03-05 (technician compensation) - Ready for execution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
12 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 03-operational-modules | 05 | execute | 3 |
|
|
true |
|
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.
<execution_context> @C:\Users\KevinAsprec.claude/get-shit-done/workflows/execute-plan.md @C:\Users\KevinAsprec.claude/get-shit-done/templates/summary.md </execution_context>
@.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 }`-
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])
-
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])
-
Add reverse relations:
- User:
technicianProfile TechnicianProfile? - Zone:
technicianProfiles TechnicianProfile[]
- User:
-
Run
npx prisma migrate dev --name add-technician-profiles -
Add TechnicianProfile and JobTypeRate to TENANT_SCOPED_MODELS in prisma-tenant.ts with FULL 12-operation extension blocks.
npx prisma migrate devsucceedsnpx tsc --noEmitpasses- Grep prisma-tenant.ts confirms "technicianProfile" and "jobTypeRate" in TENANT_SCOPED_MODELS TechnicianProfile and JobTypeRate models exist, migration applied, tenant scoping configured.
-
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<string, Decimal>
- 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 }
-
-
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?)
-
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 passnpx tsc --noEmitpasses 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.
<success_criteria>
- 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 </success_criteria>