Task — engineering-spec@1

"HR 5 contract types — indefinite + fixed_term + probation + part_time + contractor with per-type leave + benefit rules"

draftTASK-HR-002
module hr · class product · priority p0 · created 2026-05-17 · shipped null
depends on TASK-HR-001 · blocks none

§1 — Description (BCP-14 normative)

The HR service MUST extend member schema with contract types at services/hr/src/contract/ enforcing VN Labour Code constraints + per-type leave/SI rules, 3 memory audit kinds.

  1. MUST validate contract_type against closed enum per DEC-1811.
  1. MUST enforce probation duration per DEC-1812 — if contract_type='probation' and end_date - start_date > 60d (skilled) or 30d (unskilled), reject.
  1. MUST enforce fixed-term renewal limit per DEC-1812 — SELECT COUNT(*) FROM contract_history WHERE member_id=... AND type='fixed_term'; if ≥2 attempting → reject; must convert to indefinite.
  1. MUST override leave entitlement per DEC-1813:
  1. MUST override SI participation per DEC-1814:
  1. MUST define table extension at migration 0002: ```sql ALTER TABLE hr_members ADD COLUMN contract_type TEXT CHECK (contract_type IS NULL OR contract_type IN ('indefinite','fixed_term','probation','part_time','contractor')); ALTER TABLE hr_members ADD COLUMN contract_start_date DATE; ALTER TABLE hr_members ADD COLUMN contract_end_date DATE; ALTER TABLE hr_members ADD COLUMN hours_per_week INT; ALTER TABLE hr_members ADD COLUMN is_skilled BOOLEAN NOT NULL DEFAULT true; GRANT UPDATE (contract_type, contract_start_date, contract_end_date, hours_per_week, is_skilled) ON hr_members TO cyberos_app;

CREATE TABLE hr_contract_history ( history_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, member_id UUID NOT NULL, contract_type TEXT NOT NULL CHECK (contract_type IN ('indefinite','fixed_term','probation','part_time','contractor')), start_date DATE NOT NULL, end_date DATE, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX contract_hist_member_idx ON hr_contract_history(tenant_id, member_id, start_date DESC); ALTER TABLE hr_contract_history ENABLE ROW LEVEL SECURITY; CREATE POLICY contract_hist_rls ON hr_contract_history USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON hr_contract_history FROM cyberos_app; ```

  1. MUST emit 3 memory audit kinds per DEC-1815. PII per TASK-MEMORY-111: member_id (uuid) ok; type enum ok.
  1. MUST thread trace_id from set/renewal → enforcer → audit.
  1. MUST NOT allow probation > legal cap per DEC-1812.
  1. MUST NOT allow >1 fixed-term renewal per DEC-1812.

§2 — Why this design

Why 5 types (DEC-1810)? Covers VN Labour Code requirements + business need for contractor (non-employee). 5 types map to all current real-world scenarios at CyberSkill.

Why renewal limit (DEC-1812)? VN Labour Code Art. 20.3: max 1 fixed-term contract; second must be indefinite. Auto-enforcement prevents inspector findings.

Why per-type leave (DEC-1813)? Contractors don't get paid leave; part-timers pro-rated. Pre-computed at contract assignment avoids leave-calc errors.

Why per-type SI (DEC-1814)? Decree 152/2020 exempts contractors from SI; auto-enforcing avoids over-deduction.


§3 — API contract

PUT    /v1/hr/members/{id}/contract       body: {type, start_date, end_date?, hours_per_week?, is_skilled?}
GET    /v1/hr/members/{id}/contract-history

Sample contract assignment:

{
  "type": "probation",
  "start_date": "2026-06-01",
  "end_date": "2026-07-30",
  "is_skilled": true
}

§4 — Acceptance criteria

  1. 5-type enum + cardinality test. 2. Probation skilled ≤60d enforced. 3. Probation unskilled ≤30d enforced. 4. Fixed_term renewal limit 1 enforced. 5. 2nd fixed_term → reject; require indefinite conversion. 6. Contractor → 0 leave entitlement. 7. Part_time → pro-rated leave by hours_per_week/48. 8. Contractor → SI=false. 9. Other types → SI=true. 10. 3 memory audit kinds emitted. 11. PII: member_id (uuid) ok. 12. RLS denies cross-tenant. 13. CHRO role only for write. 14. Trace_id preserved. 15. History append-only. 16. fixed_term ≤36 months enforced. 17. Type change creates new history row (no in-place update). 18. End_date NULL allowed for indefinite/contractor. 19. Hours_per_week required for part_time (NULL rejected). 20. Violation detection sev-2 audit.

§5 — Verification

#[tokio::test]
async fn probation_max_60d_enforced() {
    let ctx = TestContext::with_member().await;
    let r = ctx.set_contract(ctx.member_id, "probation", "2026-06-01", Some("2026-09-01"), Some(true)).await;
    assert!(r.is_err());  // 92d > 60d cap
}

#[tokio::test]
async fn fixed_term_renewal_limit() {
    let ctx = TestContext::with_member_one_fixed_term().await;
    let r = ctx.try_renew_fixed_term(ctx.member_id).await;
    assert!(r.is_err());  // must convert to indefinite
}

#[tokio::test]
async fn contractor_zero_leave() {
    let ctx = TestContext::with_member().await;
    ctx.set_contract(ctx.member_id, "contractor", "2026-06-01", None, None).await;
    let m = ctx.fetch_member(ctx.member_id).await;
    assert_eq!(m.leave_entitlement_days, 0);
}

#[tokio::test]
async fn part_time_prorated_leave() {
    let ctx = TestContext::with_member().await;
    ctx.set_contract(ctx.member_id, "part_time", "2026-06-01", Some("2027-06-01"), Some(24)).await;
    let m = ctx.fetch_member(ctx.member_id).await;
    assert_eq!(m.leave_entitlement_days, 6);  // 12 × (24/48)
}

// 5.5..5.10

§7 — Dependencies

Upstream: TASK-HR-001. Cross-module: TASK-HR-004 (leave types), TASK-HR-005 (working hours), TASK-REW-004 (SI), TASK-MEMORY-111 (PII).

§10 — Failure modes

FailureDetectionOutcomeRecovery
Type invalidCHECK constraint400use valid
Probation length excessenforcer400 with rule citeshorten
Fixed_term 2nd renewalhistory count check400convert to indefinite
Part_time hours_per_week NULLvalidate400provide hours
Contractor with paid leave attemptoverrideleave_days=0 + sev-2 auditinherent
Contract end before startvalidate400fix dates
Concurrent contract changeoptimistic lock409retry
Cross-tenant memberRLS404inherent
History pruningappend-onlygrows unboundedpartition by year
Non-CHRO writerole check403request CHRO

§11 — Implementation notes


End of TASK-HR-002 spec.