Skip to content

F10: Mortality/culling — bird movement ledger + current flock counts (#54) - #55

Merged
mforce merged 2 commits into
mainfrom
feat/f10-bird-movements
Jul 16, 2026
Merged

mforce merged 2 commits into
mainfrom
feat/f10-bird-movements

Conversation

@mforce

@mforce mforce commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Closes #54. Phase 1.1 slice (epic #14): "Mortality/culling + bird movement generation".

What

  • BirdMovement — append-only ledger row: flock, date, type (Mortality/Cull/Adjustment), quantity (birds removed; Adjustment may be negative), note. Immutable — corrections are new rows, so no Version token.
  • Submit generates mortality: SubmitDailyEntry writes a Mortality movement alongside egg lots in the same transaction — the existing submit Version race covers it (losing submit rolls back movement + lots together). Draft mortality is not a movement.
  • Manual movements: POST /flocks/{id}/movements for Cull/Adjustment; manual Mortality rejected (would double-count at submit). Lifecycle gate reuses CanRecordProductionOn from F7: Flock management — edit/deplete/archive + Flocks screen #47 — depleted flocks accept backfill dated on/before depletion, archived accept nothing.
  • Current counts: flock responses gain CurrentBirds = InitialCount − Σ quantities (one grouped query for lists). Flocks screen shows current count with initial alongside once they differ, plus a per-flock ledger panel with movement history and a cull/adjustment form.

Out of scope (per issue)

Transfers (houses phantom until Phase 2), auto-zero on deplete, feed/water.

Tests (+4 → 119)

Submit generates exactly one movement / zero-mortality days none / draft mortality not counted; cull + negative-adjustment math on get and list; validation (manual Mortality 400, zero qty, negative cull) and lifecycle gating (backfill OK, post-depletion 422, archived 422, unknown flock 404); parallel-submit race leaves exactly one movement.

Verification (chromium, real stack)

Submitted an entry with mortality 6 → Flocks screen 400→394; recorded a 10-bird cull → 384; −2 adjustment → 386. Ledger panel shows all three row types with signed quantities.

mforce added 2 commits July 16, 2026 14:54
…54)

Daily entry captured a mortality count that went nowhere and flock
counts never moved from InitialCount.

Domain
- BirdMovement: append-only ledger row (flock, date, type
  Mortality/Cull/Adjustment, quantity = birds removed, note). Rows are
  immutable — corrections are new Adjustment rows (which may be
  negative to add birds back) — so no Version token per the
  aggregate-mutation rule's rationale.

Application
- SubmitDailyEntry also writes a Mortality movement for the entry's
  date when MortalityCount > 0, in the same transaction as egg lots:
  the existing submit Version race covers it, so a losing submit
  rolls back its movement with its lots. Draft mortality is not a
  movement (submit freezes the day).
- RecordBirdMovement handler for manual Cull/Adjustment; manual
  Mortality is rejected (would double-count at submit). Flock
  lifecycle gate reuses CanRecordProductionOn: depleted flocks accept
  backfill dated on/before depletion, archived accept nothing.

API
- POST/GET /flocks/{id}/movements (paged, newest first; 404 for
  unknown/foreign flocks).
- Flock responses gain CurrentBirds = InitialCount − Σ quantities;
  list uses one grouped query.

Web
- Flocks screen: Birds column shows current count (initial alongside
  once they differ); per-flock "birds" ledger panel with movement
  history and a cull/adjustment form. Scoped idempotency keys,
  refresh-before-rotation.

Tests (+4 -> 119): submit generates exactly one movement and zero-
mortality days none, draft mortality not counted, cull + negative
adjustment math on get and list, validation and lifecycle gating,
parallel-submit race leaves exactly one movement.

Verified in chromium against the real stack: submit with mortality
dropped House 2 400->394 on the Flocks screen; cull -> 384;
negative adjustment -> 386; ledger shows all three row types.

Closes #54
- Backfill migration: entries submitted before this feature carried
  mortality that the new ledger-derived CurrentBirds would silently
  forget. The EntryRef migration inserts Mortality rows for existing
  non-Draft entries; the NOT EXISTS guard covers both the new
  back-reference and same-flock/date rows created between the two
  migrations. (codex P1)
- BirdMovement.DailyEntryId back-reference on generated rows (FK,
  nullable) — the reconciliation hook the future manager-adjust flow
  needs to find the ledger side of a corrected entry. (pi Medium)
- Submit now applies the same lifecycle gate as recording: an
  archived flock's leftover draft can no longer mint lots + a
  mortality row; depleted-flock backfill drafts still submit when
  dated on/before DepletedOn. (pi Low)
- Repository Update/Remove throw NotSupportedException — the
  append-only invariant is enforced at the seam, not just by
  convention. (pi Low)
- FlocksPage: ledger fetches are guarded by the currently-open flock
  id, so a slow response for flock A can't render under flock B's
  heading or clobber a post-write refresh. Cull quantity input gets
  min=1. (codex P2, pi Low)

120 tests green; backfill verified against the dev DB: a pre-feature
submitted entry gained a back-referenced Mortality row and an
existing movement was not duplicated.
@mforce

mforce commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

Review round: codex + pi

# Source Severity Finding Action
1 codex P1 Upgrading an existing DB silently loses historical mortality — pre-feature submitted entries have no ledger rows, so CurrentBirds forgets past deaths permanently Fixed — backfill in the follow-up migration inserts Mortality rows for existing non-Draft entries; verified against the dev DB (pre-feature entry gained a row, existing movement not duplicated) (fc46fdd)
2 pi Medium Generated Mortality rows carried no DailyEntryId — future manager-adjust/void flows can't find the ledger side of a corrected entry Fixed — nullable FK back-reference set at submit; backfilled rows carry it too (fc46fdd)
3 codex + pi P2/Low Ledger fetch race: slow response for flock A renders under flock B's heading / clobbers post-write refresh Fixed — responses apply only if their flock is still the open ledger (fc46fdd)
4 pi Low Submit path skipped the lifecycle gate — archived flock's leftover draft could still mint lots + mortality Fixed — submit reuses CanRecordProductionOn(entry.Date); depleted-flock backfill drafts still submit (fc46fdd)
5 pi Low Append-only invariant unenforced at the repository seam Fixed — Update/Remove throw NotSupportedException (fc46fdd)
6 pi Low Cull quantity input allowed negatives client-side Fixed — min=1 when type is Cull; server validator was already the guarantee (fc46fdd)
7 pi Low No unique index on (Flock, Date, Mortality) Deferred — the daily-entry natural-key unique index upstream is the invariant; revisit if backdated corrections ever loosen it
8 pi Low No bound on CurrentBirds going negative / above initial via adjustments Deferred — display is honest about ledger contents; bounds enforcement belongs with the manager-adjust slice
9 pi Low RemovedByFlockAsync sums the whole tenant on every list Deferred — flock counts are tiny; restrict to the page when flock lists actually page
10 pi Low Redundant standalone FlockId index (EF FK convention) Skipped — EF-generated FK support index; harmless
11 pi Low Enum.Parse after validator is an implicit boundary Skipped — same validated-parse pattern as grade endpoints

120 tests green after fixes (+1: archived-draft submit rejected / depleted backfill draft still submits; back-reference asserted in the submit test).

@mforce
mforce merged commit 3ff2850 into main Jul 16, 2026
3 checks passed
@mforce
mforce deleted the feat/f10-bird-movements branch July 16, 2026 22:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

F10: Mortality/culling — bird movement ledger + current flock counts

1 participant