Files
NetForge/.planning/phases/03-operational-modules/03-05-PLAN.md
kevin-asprec d54b517e2e docs(03): create phase plan
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>
2026-03-05 07:11:44 +08:00

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
03-04
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
true
truths artifacts key_links
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
path provides contains
prisma/schema.prisma TechnicianProfile and JobTypeRate models with compensation enums model TechnicianProfile
path provides exports
src/lib/services/technician-service.ts Technician profile CRUD
createTechnicianProfile
updateTechnicianProfile
getTechnicianProfile
listTechnicians
path provides exports
src/lib/services/compensation-service.ts Compensation calculation and summary report
getCompensationSummary
getTechnicianCompensationDetail
path provides min_lines
src/lib/__tests__/compensation-service.test.ts Tests for all compensation models (per-job, salary, hybrid) and edge cases 120
from to via pattern
src/lib/services/compensation-service.ts prisma/schema.prisma queries JobOrder (COMPLETED, in period) and JobTypeRate for rate lookup jobOrder.findMany|jobTypeRate.findMany
from to via pattern
src/lib/services/compensation-service.ts src/lib/services/technician-service.ts reads TechnicianProfile for compensation model and base salary 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.

<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 }`
  1. 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])
  2. 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])
  3. Add reverse relations:

    • User: technicianProfile TechnicianProfile?
    • Zone: technicianProfiles TechnicianProfile[]
  4. Run npx prisma migrate dev --name add-technician-profiles

  5. 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
  1. 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 }

  2. 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?)
  3. 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)

<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>
After completion, create `.planning/phases/03-operational-modules/03-05-SUMMARY.md`