Skip to content

ADR 019 Discretionary Funding

Frank Steiler edited this page Jun 19, 2026 · 2 revisions

ADR-019: Discretionary Funding Budget Source

Status

Accepted

Context

The budget system tracks financing sources (bank loans, credit lines, savings) and links them to budget lines via budget_source_id. However, two categories of spending fall outside this model:

  1. Invoice remainders -- when an invoice total exceeds the sum of its itemized budget line amounts, the difference is unaccounted for by any financing source.
  2. Unassigned budget lines -- budget lines with budget_source_id = NULL have no financing source attribution.

Without a way to represent these amounts, the budget overview cannot fully reconcile total project spending against financing sources. Users see a gap between total costs and the sum of source-attributed amounts.

Alternatives Considered

  1. Virtual computed row (no DB row): Generate a "Discretionary" entry at the API layer without storing it. This avoids schema changes but makes it impossible to treat the discretionary source consistently in queries, sorting, and UI -- it becomes a special case everywhere.

  2. Separate discretionary_amount field on budget overview: Add a dedicated field to the overview response. This is simpler but means the discretionary concept only exists in the overview, not in the budget sources list where users expect to see all funding sources.

  3. System-managed sentinel row (chosen): Seed a real budget_sources row with a discretionary source type and is_discretionary flag. This lets the discretionary source appear naturally in the budget sources list, use the same response shape, and participate in sorting and filtering without special-casing the API contract.

Decision

Introduce a system-managed "Discretionary Funding" sentinel row in the budget_sources table:

  • Migration 0021 adds an is_discretionary INTEGER NOT NULL DEFAULT 0 column and expands the source_type CHECK constraint to include 'discretionary'.
  • The migration seeds a row with id discretionary-system, source_type discretionary, total_amount 0, and is_discretionary = 1.
  • The discretionary source type is not available in create or update schemas -- only the system seed creates it.
  • The discretionary source cannot be deleted (returns 409 DISCRETIONARY_SOURCE) and its sourceType cannot be changed.
  • Its totalAmount is always 0. All meaningful amounts (usedAmount, paidAmount, projectedAmount, etc.) are computed at the API layer from invoice remainders and NULL-source budget lines.
  • In the budget sources list response, non-discretionary sources sort alphabetically first, with the discretionary source appearing last.

The BudgetSourceResponse type gains three new fields:

  • paidAmount: number -- sum of paid invoice amounts linked to this source's budget lines
  • projectedAmount: number -- blended projection using confidence margins
  • isDiscretionary: boolean -- identifies the system sentinel row

Consequences

Easier:

  • Budget overview can fully reconcile all spending against financing sources
  • The discretionary source uses the same API shape as all other sources -- no special-casing in the frontend
  • Sorting places the discretionary source last, giving it a clear "catch-all" visual position

Harder:

  • Delete and update operations must guard against modifying the discretionary source
  • The is_discretionary flag introduces a concept of "system-managed" rows, which is new to the schema
  • Future migrations must preserve the sentinel row (the WHERE NOT EXISTS guard in migration 0021 handles re-runs)

Clone this wiki locally