Skip to content
Last updated: Sep 25, 2026

Collections — Worklist, Strategies & Disputes ​

OwnerClassificationVersionEffectiveNext reviewStatus
Platform EngineeringInternal1.02026-09-152026-12-15Draft

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:

ConcernLocation (relative to blitz/src)
HTTP controllerspackages/finance/cash/casha/controllers/cash/v1/ (collectionqueue, collectionstrategy, strategytask, collectionfollowuptask, followupaction, collectionslog, arcollectionstatus, disputestatus, collectionmonthlymetrics, finni)
Servicespackages/finance/cash/casha/services/cash/ (collectionqueue, collectionstrategy, collectionfollowuptask, collectionmonthlymetrics, collectionrollcure, collectionsfinni)
Elysia schemaspackages/finance/cash/casha/schemas/cash/ (collectionqueue.ts, collectionstrategy.ts, strategytask.ts, collectionfollowuptask.ts, arcollectionstatus.ts, collectionmonthlymetrics.ts, collectionslog.ts)
Prisma modelspackages/finance/cash/cashd/prisma/models/cashapp.prisma (all in the cash schema)
Workflowspackages/finance/cash/cashwf/src/workflows/ (runcollectionprocess orchestrator + steps)
Demo seedpackages/finance/cash/cashd/prisma/seed/collections_seed.ts
Finni agentpackages/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 ​

ModelRole
collectionqueueThe central worklist row: one per customer receivable, with aging buckets (current, b130, …), priority (P1–P5), riskcategory, agentemail assignment, collectionstatus
collectionstrategyA rule-selected playbook (selectioncriteria, querycondition JSONB, sequencenumber) that groups ordered strategytasks
strategytaskA template step in a strategy (tasktype, triggerworkday, isautomated, notification/output template refs)
collectionfollowuptaskAn instantiated task from a strategytask (actiontype, duedate, assigneduseremail, status, snooze fields)
followupactionA generic manual/escalation action with owner + assignee (actiontype, planneddate, status)
arcollectionstatusPer-invoice follow-up tracker: first/second/third follow-up date + mode, totalcollectedamount, collectionstatus
disputestatusDispute lifecycle for one disputed companyledger item (disputeid, status, disputereasons, collectorname)
collectionslog (+ _companyledger)Communication/audit log tying messages to the ledger items they reference
collectionmonthlymetricsPeriod 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.

StepWhat it does
arcollectionstatussetup / closearcollectionstatusMaintain the per-invoice AR collection-status ledger
collectionqueuesetupRebuild collectionqueue from cash.companyledger (the open AR ledger) — export → DuckDB razor → ImportTables
identifycollectionstrategy✅ Rule-based strategy selection via each strategy's selectioncriteria / querycondition
createcollectionfollowuptaskInstantiate collectionfollowuptasks from the selected strategy's strategytasks on their triggerworkday cadence
stopmonitoringcollectionqueueRetire queue entries that no longer qualify
calculatecollectionmetricsCompute the open period's collectionmonthlymetrics (DSO/CEI/aging/roll/cure) from the ledger
collectionfollowupnotificationEmail due follow-ups (runs after stop-monitoring so cancelled tasks aren't mailed)
assesscollectionintelligenceRuns 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:

  • collectionqueuesetup reads cash.companyledger — open items where paymentstatus = 'Open' and not Matched/Cleared/Partial Match, plus unapplied payments — and rebuilds the queue. It does not subscribe to ledger events.
  • runcollectionprocess is 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):

ToolAnswers
getCollectionsOverviewBook top-line
getMyCollectionsThe caller's own queue
getCollectionFocusThe ranked "who to chase first" worklist
getBrokenPromises / getOpenDisputesPromise defaults / disputed accounts
getExposureConcentrationHow lopsided the book is
getCustomerRisk / getCustomerHistoryOne account's verdict / payment history
getCollectionWhySubstantiation behind a verdict (firing signals + frozen provenance)
compareAccounts / getPortfolioSegmentsCompare accounts / segment the book
getCloseImpact / getCollectorWorkloadClose-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 × priority

with 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 ​

CapabilityStatusNote
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 ​

VersionDateAuthorChange
1.02026-09-15Platform EngineeringInitial draft — Collections as a Cash/AR sub-module: worklist, strategy/dunning flow, disputes, batch-nightly tie, Finni agent, rule-based/roadmap honesty.

Finaisse Internal — Confidential. Access-restricted; not for external distribution.