Task — engineering-spec@1

"DOC renewal proposal CUO draft — auto-generate renewal terms + price adjustment + send-to-customer flow with AM approval"

draftTASK-DOC-009
module doc · class product · priority p1 · created 2026-05-17 · shipped null
depends on TASK-DOC-007, TASK-CUO-101 · blocks none

§1 — Description (BCP-14 normative)

The DOC service MUST ship renewal proposal generation at services/doc/src/renewal/ triggered at TASK-DOC-008 d90 alert or manual, draft via TASK-AI-003, AM review required, creates child doc with parent link, 4 memory audit kinds.

  1. MUST trigger at d90 alert via TASK-DOC-008 hook OR manual POST /v1/doc/documents/{id}/draft-renewal per DEC-1730. Auto-trigger ONLY if renewal_terms.auto_renew = true.
  1. MUST validate renewal_recommendation against closed enum per DEC-1732.
  1. MUST generate draft via draft_generator.rs::generate(parent_doc):
  1. MUST queue for AM review per DEC-1733 — NEVER auto-send to customer.
  1. MUST on approval per DEC-1734:
  1. MUST define table at migration 0004: ``sql CREATE TABLE doc_renewal_drafts ( draft_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, parent_document_id UUID NOT NULL UNIQUE, -- one active draft per parent recommendation TEXT NOT NULL CHECK (recommendation IN ('auto_renew_as_is','renew_with_price_adj','renew_with_scope_change','do_not_renew')), draft_terms JSONB NOT NULL, ai_rationale TEXT, status TEXT NOT NULL DEFAULT 'pending_review' CHECK (status IN ('pending_review','approved','dismissed','sent','signed')), reviewed_by UUID, reviewed_at TIMESTAMPTZ, child_document_id UUID, trace_id CHAR(32), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ALTER TABLE doc_renewal_drafts ENABLE ROW LEVEL SECURITY; CREATE POLICY renewal_drafts_rls ON doc_renewal_drafts USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON doc_renewal_drafts FROM cyberos_app; GRANT UPDATE (status, reviewed_by, reviewed_at, child_document_id) ON doc_renewal_drafts TO cyberos_app; ``
  1. MUST expose endpoints: ``text POST /v1/doc/documents/{id}/draft-renewal (manual trigger, AM/CLO) GET /v1/doc/renewal-drafts (list pending review) POST /v1/doc/renewal-drafts/{id}/approve (creates child doc) POST /v1/doc/renewal-drafts/{id}/dismiss POST /v1/doc/renewal-drafts/{id}/send (TASK-EMAIL-009 send after approval) ``
  1. MUST emit 4 memory audit kinds per DEC-1735. PII per TASK-MEMORY-111: terms+rationale SHA-256 hashed; ids ok.
  1. MUST thread trace_id from trigger → AI → AM review → child create → send → audit.
  1. MUST NOT auto-send per DEC-1733.
  1. MUST NOT create child doc without parent_contract_id per DEC-1734.

§2 — Why this design

Why d90 trigger (DEC-1730)? Aligns with TASK-DOC-008 first alert; gives full quarter for negotiation.

Why manual approval (DEC-1733)? Price changes + scope changes are commercial decisions; AM owns relationship.

Why parent link (DEC-1734)? Lineage required for audit + legal traceability ("which contract supersedes which").

Why CPI adjustment default (DEC-1731)? Industry-standard escalation; AM overrides per relationship.


§3 — API contract

Sample draft:

{
  "draft_id": "uuid",
  "parent_document_id": "uuid",
  "recommendation": "renew_with_price_adj",
  "draft_terms": {
    "new_effective_date": "2028-01-01",
    "new_expiry_date": "2029-12-31",
    "new_monthly_fee_vnd": 11000000,
    "old_monthly_fee_vnd": 10000000,
    "cpi_adjustment_pct": 10.0,
    "scope_changes": []
  },
  "ai_rationale": "Standard CPI-indexed renewal; no scope changes detected; original account in good standing.",
  "status": "pending_review"
}

§4 — Acceptance criteria

  1. Auto-trigger at d90 with auto_renew=true. 2. Manual trigger via POST. 3. No trigger when auto_renew=false (manual only). 4. AM review required (no auto-send). 5. Approve creates child doc with parent link. 6. Status enum 4 + cardinality test. 7. CPI adjustment computed. 8. 4 memory audit kinds emitted. 9. PII scrubbed (terms+rationale SHA256). 10. RLS denies cross-tenant. 11. AM/CLO role only. 12. Trace_id preserved. 13. UNIQUE on parent_document_id (one active draft). 14. Send endpoint requires status=approved. 15. Append-only via REVOKE except 4 cols. 16. Dismiss → status=dismissed. 17. Recommendation enum 4 values. 18. AI failure → status=failed + sev-2 + retry. 19. Child doc inherits parties (with refresh prompt). 20. Send via TASK-EMAIL-009 with renewal template.

§5 — Verification

#[tokio::test]
async fn triggers_at_d90_with_auto_renew() {
    let ctx = TestContext::doc_expires_in_90d_with_auto_renew().await;
    ctx.run_d90_alert(ctx.doc_id).await;
    tokio::time::sleep(Duration::from_secs(1)).await;
    let drafts = ctx.fetch_renewal_drafts(ctx.doc_id).await;
    assert_eq!(drafts.len(), 1);
}

#[tokio::test]
async fn never_auto_sends() {
    let ctx = TestContext::with_renewal_draft().await;
    tokio::time::sleep(Duration::from_secs(2)).await;
    let sent_count = ctx.email_send_count().await;
    assert_eq!(sent_count, 0);
}

#[tokio::test]
async fn approve_creates_child_doc() {
    let ctx = TestContext::with_renewal_draft().await;
    ctx.approve_draft(ctx.draft_id).await;
    let row = ctx.fetch_draft(ctx.draft_id).await;
    assert!(row.child_document_id.is_some());
    let child = ctx.fetch_doc(row.child_document_id.unwrap()).await;
    assert_eq!(child.parent_contract_id, Some(ctx.parent_doc_id));
}

// 5.4..5.10

§7 — Dependencies

Upstream: TASK-DOC-007, TASK-CUO-101. Cross-module: TASK-DOC-008 (d90 trigger), TASK-EMAIL-009 (send), TASK-AI-003 (draft+rationale), TASK-AUTH-101 (AM/CLO role), TASK-MEMORY-111 (PII).

§10 — Failure modes

FailureDetectionOutcomeRecovery
AI timeoutretry 1xsev-2 fallback minimal draftAM completes manually
CPI source unavailableuse 5% defaultsev-2inherent
Duplicate draft raceUNIQUE on parent_doc_idsecond skippedinherent
Auto_renew=false unexpectedcheckno auto triggerinherent
Approve raceUPDATE WHERE pendingfirst winsinherent
Child doc create failsrollback approvalsev-1retry
Parent already renewedcheck existing chainrejectinherent
AM not assignedwarn + queueinherentreassign
Cross-tenant queryRLS0 rowsinherent
Send failsretrysev-2manual resend

§11 — Implementation notes


End of TASK-DOC-009 spec.