Task — engineering-spec@1

"RES VN Labour Code Art. 107 OT cap hard-block — propose-time validation gate preventing weekly + annual OT overflow"

draftTASK-RES-005
module res · class product · priority p0 · created 2026-05-17 · shipped null
depends on TASK-HR-005 · blocks none

§1 — Description (BCP-14 normative)

The RES service MUST ship OT cap hard-block at services/res/src/ot_cap/ enforcing Art. 107 weekly + annual + consent, 4 memory audit kinds.

  1. MUST validate ot_cap_decision against closed enum per DEC-2061.
  1. MUST check at checker.rs::check(member, week, proposed_hours) per DEC-2060:
  1. MUST define consent table at migration 0005: ``sql CREATE TABLE res_ot_consent ( consent_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, member_id UUID NOT NULL, valid_from DATE NOT NULL, valid_to DATE NOT NULL, consented_at TIMESTAMPTZ NOT NULL DEFAULT now(), consented_by_member UUID NOT NULL, recorded_by_chro UUID NOT NULL, consent_doc_id UUID, -- TASK-DOC-001 reference UNIQUE (tenant_id, member_id, valid_from, valid_to) ); ALTER TABLE res_ot_consent ENABLE ROW LEVEL SECURITY; CREATE POLICY ot_consent_rls ON res_ot_consent USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON res_ot_consent FROM cyberos_app; ``
  1. MUST integrate with TASK-RES-002 validator — checker called pre-commit.
  1. MUST expose endpoints: ``text POST /v1/res/ot-consent (CHRO records consent) GET /v1/res/members/{id}/ot-status (current YTD + caps) ``
  1. MUST emit 4 memory audit kinds per DEC-2064. PII per TASK-MEMORY-111: hours SHA-256 hashed.
  1. MUST thread trace_id from TASK-RES-002 propose → checker → audit.
  1. MUST NOT bypass cap per DEC-2060.
  1. MUST NOT allow >regular_cap without active consent per DEC-2062.

§2 — Why this design

Why hard-block (DEC-2060)? Soft warnings get ignored; Labour Code is criminal liability for systemic violations.

Why consent gate (DEC-2062)? Art. 107 explicitly requires written consent for OT beyond 12h/wk; without it, allocation is per-se illegal.

Why policy lookup (DEC-2063)? Caps may change per VN gov updates; TASK-HR-005 versions handle this.


§3 — API contract

Sample check response:

{
  "decision": "blocked_weekly",
  "weekly_ot_proposed": 35,
  "weekly_ot_cap_with_consent": 30,
  "consent_active": true,
  "rejection_message": "Weekly OT 35h exceeds 30h cap per Decree 145 Art. 107"
}

Sample consent record:

POST /v1/res/ot-consent
{
  "member_id": "uuid",
  "valid_from": "2026-06-01",
  "valid_to": "2026-12-31",
  "consent_doc_id": "uuid-signed-consent-pdf"
}

§4 — Acceptance criteria

  1. ot_cap_decision enum cardinality 4. 2. Weekly OT cap enforced (12h no consent). 3. Consent cap enforced (30h with consent). 4. Annual OT cap enforced (200h). 5. Industry-specific 300h supported via policy. 6. Consent required for >12h/wk. 7. Caps read from TASK-HR-005. 8. YTD computed from TASK-TIME-007. 9. 4 memory audit kinds emitted. 10. PII scrubbed (hours SHA256). 11. RLS denies cross-tenant. 12. CHRO-only consent record. 13. Trace_id preserved. 14. Consent immutable (append-only). 15. Expired consent treated as missing. 16. Multiple consent rows per member allowed. 17. Cap exceeded → block with explanation. 18. rust_decimal precision. 19. Integration with TASK-RES-002 validator tested. 20. YTD aggregation handles partial year.

§5 — Verification

#[tokio::test]
async fn weekly_cap_no_consent_blocks_at_13h() {
    let ctx = TestContext::member_no_consent().await;
    let r = ot_cap::check(ctx.member_id, this_week(), dec!(53)).await;  // 40 reg + 13 OT
    assert_eq!(r.decision, "blocked_consent_missing");
}

#[tokio::test]
async fn weekly_cap_with_consent_blocks_at_31h() {
    let ctx = TestContext::member_with_consent().await;
    let r = ot_cap::check(ctx.member_id, this_week(), dec!(71)).await;  // 40 + 31 OT
    assert_eq!(r.decision, "blocked_weekly");
}

#[tokio::test]
async fn annual_cap_blocks_at_201h() {
    let ctx = TestContext::member_with_ytd_ot(190).await;
    let r = ot_cap::check(ctx.member_id, this_week(), dec!(52)).await;  // would push to 202
    assert_eq!(r.decision, "blocked_annual");
}

// 5.4..5.10

§7 — Dependencies

Upstream: TASK-HR-005. Cross-module: TASK-RES-002 (validator integration), TASK-TIME-007 (YTD), TASK-DOC-001 (consent doc), TASK-AUTH-101 (CHRO), TASK-MEMORY-111 (PII).

§10 — Failure modes

FailureDetectionOutcomeRecovery
Policy lookup failcatchsev-1; reject conservativelyretry
YTD compute failcatchsev-1; reject (safer)retry
Consent expired mid-weekexclude expiredblockrenew consent
Industry-specific 300h appliespolicy overrideinherenttenant config
Mid-week policy changeuse version at week startinherentinherent
Cross-tenant consentRLSnot found → blockedinherent
Decimal precisionrust_decimalinherentinherent
Partial-year hireYTD from hire dateinherentinherent
Consent doc missingwarnstill valid if recordeddoc upload
Duplicate consentUNIQUEsecond 409use existing

§11 — Implementation notes


End of TASK-RES-005 spec.