Repository navigation
F17 (part 1): Auto-lock + admin adjust/void of submitted entries with lot & ledger reconciliation (#69) - #80
Merged
Merged
Conversation
added 2 commits
July 18, 2026 21:05
… lot and ledger reconciliation (#69) Domain: - ManagerAdjust now accepts Submitted, Locked, and ManagerAdjusted entries (spec §8.1 MVP: one adjust path pre- and post-lock), requires a reason, and snapshots the values it replaces into AdjustedFromJson (the audit trail until the audit-log slice). Void(reason) preserves the entry as Voided. Lock(at) stamps LockedAtUtc. EggLot.AdjustProduction re-states a lot: the sold/allocated portion is untouchable, available absorbs the delta, 0 empties an unsold lot in place. - EggLot.DailyEntryId links every lot to the entry whose submit generated it. (flock, date) is NOT an entry surface — multiple houses mean sibling entries per flock and day, and live verification caught an adjust zeroing a sibling entry lot. Migration backfills the link where exactly one non-draft entry matches; unprovable lots stay null and their entries refuse adjust/void (DailyEntry.PredatesLotTracking) rather than guess. Application: - AdjustDailyEntryHandler: client base Version (409 on mismatch, EF token backstop), flock lifecycle gate, grade validation (active+saleable for new ids; ids already on the entry grandfathered), then in ONE transaction: FOR UPDATE on the entry lots in the canonical (ProductionDate, Id) order shared with confirm/void, lots grown/shrunk/added/emptied to match the new lines — shrinking below a lot sold quantity is a 422 naming the grade — and a compensating bird movement for any mortality delta (more deaths -> Mortality row; fewer -> negative Adjustment; originals never edited). - VoidDailyEntryHandler: same shape; empties every linked lot (422 if any eggs were sold), appends the reversing movement, preserves the entry. - DailyEntryLockSweep: submitted entries older than 7 farm-local days (account timezone) lock automatically; runs per-account inside the DurableJobWorker guarded poll; idempotent and Version-race safe. API: POST /daily-entries/{id}/adjust and /void, both AdminOnly (#73); entry responses now carry version, adjustReason, voidReason, lockedAtUtc, and the adjustedFrom snapshot. Tests (222 green): lot reconciliation matrix (grow/shrink/add/remove), shrink-below-sold 422 with full rollback, mortality compensation both directions with CurrentBirds assertions, void paths, sibling-entry isolation (the live-verify regression), parallel adjusts (exactly one wins, Version delta 1), draft/stale/blank-reason guards, lock sweep, and the worker-403 matrix extended with both new routes.
- Duplicate-grade lots: every lot keeps its sold floor and the first in canonical order carries the remainder — a later duplicate holding sold eggs no longer produces a spurious 422 when the aggregate target covers all sold eggs. Explicit aggregate-level 422 when it does not. - Ledger notes truncate to BirdMovement.MaxNoteLength: a max-length reason no longer throws (500) when prefixed into the compensating movement note. - Lock sweep uses a strict comparison — an entry exactly 7 farm-local days old keeps its final editable day; only strictly older entries lock. - Loss-count sum validated as long in BOTH the adjust and record validators: three ints could overflow past a positive total and slip through the domain guard when no grade lines were present. - AdjustedFromJson is plain text, not jsonb (provider-portability rule); the two migrations squashed into one clean AddDailyEntryAdjustments with the unambiguous lot backfill. - Docs-sync in THIS PR: Help page (submit bullet, 7-day lock bullet, two Fixing-mistakes rows, three glossary rows) + GLOSSARY.md full lifecycle rewrite (Locked/ManagerAdjusted/Voided, lot linkage, sold-untouchable). - New race test: parallel adjust vs void on the same base version — exactly one wins, Version delta 1, no blended lot state.
Owner
Author
|
codex round applied (4 correctness P2s + docs P2 + both P3s):
223 tests green. |
- Lock sweep isolates account failures: one account's bad timezone or transient DB error logs and moves on instead of starving the remaining accounts of their sweep. Shutdown cancellation still propagates. - The success log reports the actually-locked count, not the batch size.
Owner
Author
|
pi round (local llamacpp, scoped) applied in the latest commit:
Declined with rationale:
223 tests green. Part 2 (History-page adjust/void UI + badges) follows after this merges. |
This was referenced Jul 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part 1 of #69 (epic #14) — the backend. Part 2 (History-page UI + Help/GLOSSARY docs, per the F15 two-PR precedent) follows on top of this.
What ships
Auto-lock (spec §8.1 default) — submitted entries older than 7 farm-local days (account timezone; #35 refines) lock automatically. The sweep runs per-account inside the DurableJobWorker's guarded poll (#65 landed first for exactly this reason), stamps
lockedAtUtc, is idempotent, and loses Version races to concurrent admin work gracefully.Admin adjust (
POST /daily-entries/{id}/adjust, AdminOnly per #73) — one path for Submitted AND Locked (and re-adjusting ManagerAdjusted): totals + grade lines + required reason, client baseversion(stale → deterministic 409; EF token backstops). In ONE transaction:(ProductionDate, Id)order shared with confirm/void (no deadlock against sales): grown, shrunk, added (new grade line → new linked lot), or emptied in place (line removed).adjustedFrom) persisted and returned on the entry.Void (
POST /daily-entries/{id}/void, AdminOnly) — spec §8.1/§9.5: empties every lot the entry generated (422 if any eggs were sold), reverses the day's mortality, preserves the entry as Voided with its reason.The design find (live verification caught it)
First cut reconciled lots by
(flock, date)— and promptly zeroed a sibling entry's lot in the dev DB: multiple houses mean multiple entries per flock/day, so(flock, date)is not an entry surface. Fix:EggLot.DailyEntryId— every submit/adjust-created lot links to its entry; the migration backfills history where exactly one non-draft entry matches; unprovable lots stay null and their entries refuse adjust/void withDailyEntry.PredatesLotTracking(same honesty as pre-#60 orders refusing void). Regression-tested with sibling entries.Contracts
version,adjustReason,voidReason,lockedAtUtc,adjustedFrom— the PR Fix: end-to-end optimistic concurrency for water corrections (codex review of #76) #77 end-to-end optimistic-concurrency contract applied to entries.Tests — 222 green (was 202)
Reconciliation matrix (grow/shrink/add/remove + snapshot), shrink-below-sold 422 with rollback assertions, mortality compensation both directions (CurrentBirds verified), void + void-with-sold-stock, sibling-entry isolation, parallel adjusts (exactly one wins, Version delta 1 per AGENTS.md), draft/stale/blank-reason guards, lock-sweep (old locks, recent doesn't, locked entry still adjustable), worker-403 matrix extended with both routes. Domain: 11 new unit tests (adjust states, snapshots, void transitions, AdjustProduction).
Live-verified
8-day-old entry auto-locked by the sweep within one poll → adjust on the locked entry 200 (status ManagerAdjusted, reason +
adjustedFrom.totalEggspresent) → stale-version adjust 409 → void 200 (Voided + reason, stock returned). Migration + backfill applied cleanly against the dev DB with demo data.