"DOC renewal proposal CUO draft — auto-generate renewal terms + price adjustment + send-to-customer flow with AM approval"
§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.
- MUST trigger at d90 alert via TASK-DOC-008 hook OR manual
POST /v1/doc/documents/{id}/draft-renewalper DEC-1730. Auto-trigger ONLY ifrenewal_terms.auto_renew = true.
- MUST validate
renewal_recommendationagainst closed enum per DEC-1732.
- MUST generate draft via
draft_generator.rs::generate(parent_doc):
- Pull parent terms (effective_date, expiry_date, renewal_terms.term_months).
- Compute new dates: new_effective = old_expiry + 1d; new_expiry = new_effective + term_months.
- CPI adjust price per
cpi_adjuster.rs::adjust(old_price, residency, since_date). - AI summarize scope-change recommendation.
- MUST queue for AM review per DEC-1733 — NEVER auto-send to customer.
- MUST on approval per DEC-1734:
- Create new doc row,
parent_contract_id = original.document_id. - Status='draft'.
- Lifecycle status compute via TASK-DOC-007 (will be 'draft' if effective > now).
- 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;``
- 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)``
- MUST emit 4 memory audit kinds per DEC-1735. PII per TASK-MEMORY-111: terms+rationale SHA-256 hashed; ids ok.
- MUST thread trace_id from trigger → AI → AM review → child create → send → audit.
- MUST NOT auto-send per DEC-1733.
- 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
- 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
| Failure | Detection | Outcome | Recovery |
|---|---|---|---|
| AI timeout | retry 1x | sev-2 fallback minimal draft | AM completes manually |
| CPI source unavailable | use 5% default | sev-2 | inherent |
| Duplicate draft race | UNIQUE on parent_doc_id | second skipped | inherent |
| Auto_renew=false unexpected | check | no auto trigger | inherent |
| Approve race | UPDATE WHERE pending | first wins | inherent |
| Child doc create fails | rollback approval | sev-1 | retry |
| Parent already renewed | check existing chain | reject | inherent |
| AM not assigned | warn + queue | inherent | reassign |
| Cross-tenant query | RLS | 0 rows | inherent |
| Send fails | retry | sev-2 | manual resend |
§11 — Implementation notes
- §11.1 CPI: per-residency lookup table (vn-1: VN CPI, sg-1: SG CPI, eu-1: EU HICP, us-1: US CPI-U).
- §11.2 AI rationale prompt: "Summarize this renewal proposal (scope/price changes) in 2-3 sentences."
- §11.3 Child doc inherits parties array; AM prompted to confirm/update before send.
- §11.4 memory audit body: parent_doc_id, recommendation; draft_terms SHA256.
- §11.5 Send templates: branded per tenant (TASK-PORTAL-002 brand pack).
End of TASK-DOC-009 spec.