Story-1278 functions lane new domain + migration + contract

Matter Types catalog

Every firm now has its own list of "what kind of matter is this" labels — Civil Suit, Commercial Dispute, Writ Petition, and so on. This Story builds that list as a real, firm-owned, archive-capable settings catalog with its own table, API, and per-firm numbering, and unblocks the app's matter-type picker.

The mental model: a firm already curates fine-grained catalogs — forums, filing types, proceeding types, participant labels. Matter Type is the coarse sibling: one label per matter, set at the top. We built it by cloning the proven proceeding_types catalog shape verbatim — same firm isolation, same archive lifecycle, same per-firm numbering — and gave it the TY- reference prefix. S1 freed the src/domains/matter_type/ package; this Story fills it with the real domain.

Why this exists

Spike-1274 split two ideas that had been conflated and ADR-115 ratified the result: a Matter Preset (a saved bundle of defaults) is not a Matter Type (the coarse classification of the matter itself). Matter Type is the new, real axis. It sits above the finer proceeding_type (which classifies each proceeding inside a matter) and supersedes the dormant, half-wired practice_area table. This Story (S2) ships the catalog itself; wiring it into the matter-create flow with inline create-on-the-fly is S3.

What changed

New table

business.matter_types — firm-owned, FORCE RLS with the five policies, firm-leading unique constraints and indexes, partial-unique live-name index, availability archive lifecycle. One hand-authored Alembic migration (0097), up and down.

Reference numbering

matter_type_number minted per firm via business.allocate_reference(firm_id, 'matter_type'). The migration extends the reference_counters kind CHECK with 'matter_type' and the downgrade cleans it back out.

API

Router /app/matter-types (tag matterTypes): list, create, update, delete, archive, unarchive. Separate *Request and *View/*ListItem DTOs; every route declares response_model and canonical error responses.

Formatter

TY-{number:03d} — TY-001, TY-002. Derived for display, never stored. MT- belongs to matter presets; TY- is the unused, unambiguous pick.

Archive lifecycle

Management lists return archived rows; pickers stay live-only; deleted rows are retained but hidden. Delete is guarded — allowed only when unused, else archive is the path.

Audit + seed

SUBJECTS["matter_type"] and five coverage.py route rows. New firms are seeded with nine canonical India-litigation types via FirmService.create_firm; the seed is idempotent.

First-run safety (the system-boundary gate)

A catalog that returns [] for every firm is a door welded shut. So the gate here is not "the unit test is green" — it is a real request against a freshly provisioned empty firm.

First-run stateBehaviour proven against a real DB
Empty firm, GET /app/matter-typesreturns [] (not an error, not a welded door)
POST a typecreates, numbered TY-001, usageCount 0, canDelete true
Archive → unarchiveround-trips; archived rows visible only with includeArchived
Delete an unused typesoft-deletes (availability='deleted'), then hidden even from the management list
Firm B reads/patches Firm A's typeRLS isolates it — invisible, and a cross-firm PATCH returns 404

Scope boundary — the delete guard

The delete guard is wired (MatterTypeService._in_use_references) but its reference set is empty in S2: no durable row points at a matter type yet. The matter_type_id FK columns on business.matters and business.matter_templates land in S3 (design note §5 steps 4–5). When they exist, S3 registers them in that one method and "delete-when-referenced rejects with a typed error" becomes live. Until then an unused type deletes cleanly, which is the only reachable state.

How it was verified

  1. ruff check on every changed/new file — clean.
  2. Migration round-trip against a real Postgres: alembic upgrade head → downgrade base → upgrade head → alembic check reports "No new upgrade operations detected" (entity model matches the schema, including the CHECK edit and the per-firm constraints).
  3. Integration tests against a real DB: empty-firm CRUD, archive round-trip, delete-when-unused, per-firm numbering, and cross-firm RLS isolation — all green.
  4. Full suite: 804 passing, including the RLS confidentiality classification (matter types are a support-readable settings catalog) and firm-provisioning seed.
  5. make openapi regenerated both snapshots; the change is purely additive — six new MatterType* schemas and six clean camelCase operation IDs, zero existing operations or schemas removed.

Engineering outcome: a firm-owned Matter Type catalog that behaves exactly like its proven proceeding_types sibling — isolated, archive-capable, per-firm numbered — proven to work from the very first request on an empty firm. The app's matter-type picker (#1281) is now unblocked, and S3 has a clean catalog to wire into matter creation.