Collections — Worklist, Strategies & Disputes
| Owner | Classification | Version | Effective | Next review | Status |
|---|---|---|---|---|---|
| Platform Engineering | Internal | 1.0 | 2026-09-15 | 2026-12-15 | Draft |
Purpose. The reference for how Collections works — the receivables worklist, the strategy/dunning cadence, dispute tracking, and the Finni collections agent. Collections is not a standalone package: it is a sub-module of Cash/AR. Its controllers, services and workflows live inside the casha/cashwf packages, its Prisma models inside cashd, and its Finni tools inside financeagents. This document states — honestly — what is rule-based, what is batch (not real-time), and what is still roadmap. For the AR engine itself see Cash Application Intelligence; for the module inventory see Intelligence Inventory.
Honesty tags used inline: ✅ implemented · ⚠️ works, but with a caveat · ◯ not built (roadmap).
1. Where Collections lives
Collections has no package of its own. Every part is folded into Cash/AR:
| Concern | Location (relative to blitz/src) |
|---|---|
| HTTP controllers | packages/finance/cash/casha/controllers/cash/v1/ (collectionqueue, collectionstrategy, strategytask, collectionfollowuptask, followupaction, collectionslog, arcollectionstatus, disputestatus, collectionmonthlymetrics, finni) |
| Services | packages/finance/cash/casha/services/cash/ (collectionqueue, collectionstrategy, collectionfollowuptask, collectionmonthlymetrics, collectionrollcure, collectionsfinni) |
| Elysia schemas | packages/finance/cash/casha/schemas/cash/ (collectionqueue.ts, collectionstrategy.ts, strategytask.ts, collectionfollowuptask.ts, arcollectionstatus.ts, collectionmonthlymetrics.ts, collectionslog.ts) |
| Prisma models | packages/finance/cash/cashd/prisma/models/cashapp.prisma (all in the cash schema) |
| Workflows | packages/finance/cash/cashwf/src/workflows/ (runcollectionprocess orchestrator + steps) |
| Demo seed | packages/finance/cash/cashd/prisma/seed/collections_seed.ts |
| Finni agent | packages/finance/financeagents/src/agents/finni-agent/collection-{client,tools,systemprompt.md} |
The Prisma models all carry @@schema("cash") — Collections shares the AR schema, not a separate database.
2. The data model
| Model | Role |
|---|---|
collectionqueue | The central worklist row: one per customer receivable, with aging buckets (current, b130, …), priority (P1–P5), riskcategory, agentemail assignment, collectionstatus |
collectionstrategy | A rule-selected playbook (selectioncriteria, querycondition JSONB, sequencenumber) that groups ordered strategytasks |
strategytask | A template step in a strategy (tasktype, triggerworkday, isautomated, notification/output template refs) |
collectionfollowuptask | An instantiated task from a strategytask (actiontype, duedate, assigneduseremail, status, snooze fields) |
followupaction | A generic manual/escalation action with owner + assignee (actiontype, planneddate, status) |
arcollectionstatus | Per-invoice follow-up tracker: first/second/third follow-up date + mode, totalcollectedamount, collectionstatus |
disputestatus | Dispute lifecycle for one disputed companyledger item (disputeid, status, disputereasons, collectorname) |
collectionslog (+ _companyledger) | Communication/audit log tying messages to the ledger items they reference |
collectionmonthlymetrics | Period KPIs — DSO, CEI, aging, roll/cure — keyed to a period |
3. Worklist → strategy → dunning → notify flow
The worklist and everything downstream are built by the runcollectionprocess Temporal workflow (cashwf/src/workflows/runcollectionprocess/workflow.ts), an end-to-end orchestrator that creates a parent job plus a child job per step.
| Step | What it does |
|---|---|
arcollectionstatussetup / closearcollectionstatus | Maintain the per-invoice AR collection-status ledger |
collectionqueuesetup | Rebuild collectionqueue from cash.companyledger (the open AR ledger) — export → DuckDB razor → ImportTables |
identifycollectionstrategy | ✅ Rule-based strategy selection via each strategy's selectioncriteria / querycondition |
createcollectionfollowuptask | Instantiate collectionfollowuptasks from the selected strategy's strategytasks on their triggerworkday cadence |
stopmonitoringcollectionqueue | Retire queue entries that no longer qualify |
calculatecollectionmetrics | Compute the open period's collectionmonthlymetrics (DSO/CEI/aging/roll/cure) from the ledger |
collectionfollowupnotification | Email due follow-ups (runs after stop-monitoring so cancelled tasks aren't mailed) |
assesscollectionintelligence | Runs last — freezes materiality tier + behavioural anomaly + finbase.intelligencelog provenance |
The dunning cadence is rule-based, not ML (✅ but rules): strategies are chosen by criteria expressions, and task timing is a fixed triggerworkday offset chain. Manual trigger: POST …/cash/v1/runcollectionprocess (permission cashapp:workflow:execute).
4. Connected-to-cash tie ⚠️ batch nightly, not real-time
Collections is fed from the AR ledger, but on a batch cadence, not live:
collectionqueuesetupreadscash.companyledger— open items wherepaymentstatus = 'Open'and notMatched/Cleared/Partial Match, plus unapplied payments — and rebuilds the queue. It does not subscribe to ledger events.runcollectionprocessis designed to run daily (the metrics step header states it "runs as a step in runcollectionprocess (daily)"). ⚠️ So a payment applied in Cash App does not clear the worklist until the next batch pass.- ◯ There is no event-driven / real-time recompute path — the freshness of the queue equals the freshness of the last batch run.
⚠️ Unverified: whether a platform cron actually fires runcollectionprocess on a fixed nightly schedule in production (a taskschedule row) versus being triggered on demand. The workflow is built for a daily cadence; the trigger wiring was not confirmed in code during this review.
5. Disputes
A collector flags a companyledger item as disputed → a disputestatus row is created (one per ledger item, companyledgerid is @unique). It tracks disputereasons (pricing/delivery/quality/billing/documentation), status, startdate / closeddate / followupdate, and collectorname. Finni's guidance is explicit: an open dispute blocks collection — route/resolve it, don't dun the disputed balance. Disputes also surface cross-module as the reconciling "breaks" behind the AR reconciliation (see §6).
⚠️ Populated in demo only. disputestatus (and arcollectionstatus) are written by collections_seed.ts, not by a production populator observed in this review — outside the seeded demo they are expected to be at 0 rows until a collector raises a dispute.
6. The Finni collections agent
Finni is the embedded agent for the Collections app (collection-systemprompt.md). It is read-only and advisory — it ranks and explains but never auto-executes an action ("I recommend — the user decides").
It calls Cash-API endpoints under …/cash/v1/finni/ via collection-client.ts, surfaced as these tools (collection-tools.ts):
| Tool | Answers |
|---|---|
getCollectionsOverview | Book top-line |
getMyCollections | The caller's own queue |
getCollectionFocus | The ranked "who to chase first" worklist |
getBrokenPromises / getOpenDisputes | Promise defaults / disputed accounts |
getExposureConcentration | How lopsided the book is |
getCustomerRisk / getCustomerHistory | One account's verdict / payment history |
getCollectionWhy | Substantiation behind a verdict (firing signals + frozen provenance) |
compareAccounts / getPortfolioSegments | Compare accounts / segment the book |
getCloseImpact / getCollectorWorkload | Close-blocking disputes / team distribution |
Ranking is rule-based, not ML (✅ but rules). collectionsfinni.service.ts scores each queue row deterministically in SQL:
score = exposure × behavior × dispute × brokenPromise × ageCliff × slip × prioritywith hardcoded weights (WEIGHTS): behaviour Reliable 0.4 / Mixed 1.0 / Unreliable 1.6; open dispute ×1.5; broken-promise min(2, 1 + 0.3×count); aged-90+ ×1.4; behaviour-slip ×1.3; priority P1 1.3 → P5 0.8. The code notes these are "hardcoded weights. Future: move to rulesengine so tenants can tune."
Layer-2 judge. On a borderline single-account verdict the server consults an LLM judge and tags the result confidence: 'assisted' (vs verified for pure deterministic numbers, limited-history for thin records). ✅ The judge is a tie-breaker over the same verified numbers — it does not generate the ranking.
7. Honest status — rule-based, not predictive
| Capability | Status | Note |
|---|---|---|
| Worklist / queue build | ✅ | Deterministic rebuild from AR ledger |
| Strategy selection & dunning cadence | ✅ (but rules) | Criteria + fixed triggerworkday offsets, not learned |
| Dispute tracking | ✅ (demo-seeded) | ⚠️ no prod populator observed → 0 rows outside seed |
| Finni ranking | ✅ (but rules) | Deterministic score formula, hardcoded weights |
| Layer-2 judge on borderline verdicts | ✅ | Tie-breaker only; tagged assisted |
| Connected-to-cash | ⚠️ batch (daily) | Queue rebuilt from ledger on a batch pass, not real-time |
| Predictive / forward risk-ranking | ◯ | The prompt is explicit: promise-kept rate, short-pay frequency, and forward slip/default prediction are "not computed yet"; classic-ML is not wired |
The Finni prompt states the honest fallback verbatim: "STILL NOT AVAILABLE — be honest, never invent: promise-kept rate, short-pay frequency, and forward-looking slip/default prediction."
Revision history
| Version | Date | Author | Change |
|---|---|---|---|
| 1.0 | 2026-09-15 | Platform Engineering | Initial draft — Collections as a Cash/AR sub-module: worklist, strategy/dunning flow, disputes, batch-nightly tie, Finni agent, rule-based/roadmap honesty. |