Skip to content

F15 (2/2): Feed usage + stock corrections (#66) - #70

Merged
mforce merged 2 commits into
mainfrom
feat/f15-feed-usage
Jul 18, 2026
Merged

mforce merged 2 commits into
mainfrom
feat/f15-feed-usage

Conversation

@mforce

@mforce mforce commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Closes #66 (epic #14, Phase 1.1). Part 2 of the split — consumption + the correction path agreed on the issue. Builds on part 1 (PR #68).

What

  • FeedUsage — create-only feeding record (date, flock, item, quantity, unit snapshot, note) with a lot-cost estimate: Σ(consumed-per-lot × that lot's unit cost) — the §19 feed-cost KPI input. Repo seam rejects Update/Remove; corrections are compensating ledger rows, never edits.
  • POST /inventory/items/{id}/usage — drains lots FIFO under the canonical (ReceivedDate, Id) FOR UPDATE lock; one Usage movement per lot drained (lot-level provenance, the F14: Void confirmed sales order — return allocated stock #60 lesson); insufficient stock → 422 with nothing committed. Flock gate reuses CanRecordProductionOn (depleted backfill OK, archived never). Inactive items may still be used up — deactivation only stops new stock.
  • InventoryLot.Adjust — signed correction bounded to [0, QuantityReceived]: below-consumed refuses (InsufficientStock), beyond-received refuses (AdjustExceedsReceived — extra stock is a purchase, not an adjustment).
  • POST /inventory/items/{id}/adjustments — Adjustment (signed) or Discard (negative only) against a specific locked lot, reason mandatory (whitespace-guarded). A typo'd 5000-instead-of-500 purchase = one negative adjustment; both rows stay in the ledger.
  • GET /inventory/usage — newest first, flock/date filters, paged.
  • SPA — usage form (active-flock picker, date, quantity, note) + correction form (lot picker showing remaining/received, type, reason) on the item panel; ledger reflects both immediately.

Tests (11 new, 173 green)

  • Domain: Adjust bounds both directions, FeedUsage guards.
  • Integration: FIFO drain across two lots with per-lot movements and exact cost math (30×2400 + 20×2500 = 122 000); insufficient-stock 422 leaves zero side effects; archived-flock 422; typo-fix adjustment with both ledger rows; guard matrix (below-consumed 422, beyond-received 422, positive discard 400, whitespace reason 400, valid restore); parallel-usage race — two 60s against a 100 lot: one OK, one 422, lot at 40, Version delta 1, single usage row.

Runtime verified

API scenario (cost 122000 exact, on-hand 80→60 through usage+adjustment, all guard codes) + Playwright UI flow (purchase → usage → correction, headings and ledger updating) — zero console errors.

mforce added 2 commits July 17, 2026 22:07
Spec §12.4 consumption plus the correction path agreed on the issue.

- FeedUsage: create-only record (date, flock, item, quantity, unit snapshot,
  note) with a lot-cost estimate — Σ(consumed-per-lot × that lot's unit cost),
  the §19 feed-cost KPI input. Repo seam rejects Update/Remove: corrections
  are compensating ledger rows, never edits of the usage record.
- RecordFeedUsage: drains lots FIFO under the canonical (ReceivedDate, Id)
  FOR UPDATE lock; one Usage movement per lot drained (lot-level provenance,
  the #60 lesson); insufficient stock -> 422 with nothing committed. Flock
  gate reuses CanRecordProductionOn (depleted backfill allowed, archived
  never). Inactive items may still be used up — deactivation only stops new
  stock arriving.
- InventoryLot.Adjust: signed correction bounded to [0, QuantityReceived] —
  below-consumed and beyond-received both refuse; extra stock is a purchase,
  not an adjustment.
- RecordAdjustment: Adjustment (signed) or Discard (negative only) against a
  specific FOR UPDATE-locked lot, reason mandatory; a typo'd purchase is fixed
  by a compensating negative adjustment with both rows visible in the ledger.
- API: POST /inventory/items/{id}/usage + /adjustments, GET /inventory/usage
  (flock/date filters, paged).
- SPA: usage form (active-flock picker) + correction form (lot picker with
  remaining/received, type, mandatory reason) on the item panel.

Tests: 11 new (Adjust bounds, FeedUsage guards; integration FIFO drain with
per-lot movements + exact 122000 cost, insufficient 422 no-op, archived-flock
422, typo-fix adjustment, guard matrix, parallel-usage race — one winner,
lot never negative, Version delta 1). 173 green.

Runtime-verified: API scenario (FIFO cost math, 422/400 guards, corrections)
+ Playwright UI flow (usage + correction forms, ledger updates), zero console
errors.

Closes #66
codex P2s (no P1s), all applied:

- Usage now locks the item row FIRST (item -> lots, the purchase path's
  order) inside its transaction — a concurrent backdated purchase could
  previously insert an older lot after the FIFO read. Flock lifecycle gate
  moved inside the transaction too (remaining same-day deplete race is
  business-valid and documented).
- FIFO is as-of the usage date: a backdated feeding can no longer consume
  lots received after that day (stock that didn't exist yet). New test.
- Adjustments can't predate their lot's receipt (fabricated history). New test.
- Cost estimate accumulates in decimal with an explicit bound -> 422 instead
  of a silent long wrap/overflow 500; unit-cost minor units capped at 1e13 in
  the purchase/create/update validators so quantity x cost stays in range.
- Usage movements carry ReferenceType/ReferenceId = FeedUsage + id (spec
  §12.3): several same-day feedings of one flock now reconcile to their own
  records. Exposed on the movements endpoint. New test.
- Category gate: only Feed/Supplement/Additive items can be fed to a flock
  (API 422 + SPA hides the form) — packaging as feed would poison §19 KPIs.
  New test.
- Usage POST response includes CurrencyMinorUnit (matches GET; JPY/KWD-safe).
- FeedUsage.CreatedAtUtc for same-day list ordering; DailyEntryId gets a real
  FK; depleted flocks return to the SPA picker labeled backfill-only (the
  handler already allowed on-or-before-depletion dates).
- Adjustment validator range check rewritten without Math.Abs (hardening;
  decimal's symmetric range made the reported throw unreachable).

Declined as existing app-wide conventions (noted on the PR): offset
pagination and FluentValidation message format.

Second reviewer (local stand-in for rate-limited pi) reviewed the post-fix
tree: 0 findings; explicitly verified no deadlock cycle across the four
locking paths, tenant isolation in raw SQL, and SPA idempotency-key guards.

Migration regenerated (branch-only). 178 tests green. Runtime re-verified.
@mforce

mforce commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

Review round — applied in 3bb1a0d

Reviewers: codex + local stand-in for pi (weekly cap, resets ~tomorrow). codex: 0 P1, 12 accepted findings. Stand-in reviewed the post-fix tree: 0 findings.

# Sev Finding (codex) Resolution
1 P2 Usage skipped the item lock — backdated purchase could insert an older lot after the FIFO read. Item row locked first (item → lots, same order as purchases), inside the transaction.
2 P2 Flock gate checked before the transaction — raced concurrent deplete/archive. Moved inside; remaining same-day race documented as business-valid (birds ate before they left).
3 P2 FIFO included lots received after a backdated usage date. Lock query is as-of the usage date. Test.
4 P2 Adjustment/discard could predate its lot's receipt. 422 guard. Test.
5 P2 Cost math could overflow decimal→long (valid inputs). Decimal accumulation + explicit bound → 422; unit-cost minor units capped 1e13 across validators.
6 P2 Usage movements had no reference to their FeedUsage — same-day feedings unreconcilable. ReferenceType/ReferenceId (spec §12.3), exposed on the movements endpoint. Test.
7 P2 No category gate — packaging recordable as flock feed. Feed/Supplement/Additive only, API 422 + SPA hides the form. Test.
8 P2 Usage POST response omitted CurrencyMinorUnit. Added (matches GET).
9 P2 Math.Abs(decimal.MinValue) throws → 500. Not reproducible — decimal's range is symmetric, unlike int/long. Expression rewritten to InclusiveBetween anyway.
10 P3 FeedUsage lacked CreatedAt — same-day order undefined. CreatedAtUtc + list tiebreak.
11 P3 SPA hid depleted flocks — valid backfill unreachable. Back in the picker, labeled "depleted — backfill only".
12 P3 DailyEntryId had no FK. FK (Restrict) added.
13 P3 Offset pagination / FluentValidation message shape vs spec §3.2 cursor+codes. Declined here: both are the existing convention on every list/write endpoint in the app — changing them is a cross-cutting slice, not a PR #70 fix. Happy to file it.

Stand-in's independent verification of the post-fix tree: no deadlock cycle possible across the four locking paths (item-lockers never wait on lots; lot-lockers only touch the item via FK FOR KEY SHARE); per-lot rounding is the documented KPI tradeoff; raw-SQL tenant filtering is stronger than the check-after pattern; idempotency middleware only replays 2xx so failed-submit key reuse is safe.

Migration regenerated (branch-only). 178 tests green. Runtime re-verified: FIFO cost math exact, all guard codes, as-of-date behavior.

@mforce
mforce merged commit fd156fc into main Jul 18, 2026
3 checks passed
@mforce
mforce deleted the feat/f15-feed-usage branch July 18, 2026 18:15
mforce added a commit that referenced this pull request Jul 18, 2026
…e preservation, help/glossary corrections, a11y (#75)

* Fix: depleted flocks reachable for daily-entry backfill

Found by the PR #74 accuracy review (post-merge): the Help page, the Flocks
screen's own deplete dialog ("backfill for past dates still works"), the
glossary, and the domain (Flock.CanRecordProductionOn) all promise backfilled
entries for depleted flocks up to their depletion date — but the Daily entry
picker filtered to Active-only, so the capability was unreachable in the UI.

Picker now includes depleted flocks (archived stay hidden), labeled
"— depleted, backfill only", mirroring the feed-usage picker shipped in #70.
Default selection still prefers an Active flock; the API continues to gate
the exact dates.

Browser-verified: depleted flock listed + labeled, default selection Active,
and a post-depletion date surfaces the API's lifecycle rejection in the UI.

* Apply codex accuracy review of the help page (PR #74, post-merge)

codex P1: the help implied medication-withdrawal protection is operational,
but no production path sets EggLot.RestrictedUntil — a food-safety
overclaim. Stock section, in-app glossary, and GLOSSARY.md now state plainly
that nothing marks eggs restricted yet and withholding periods must be
managed outside Cluckwork until medication tracking (spec §13) lands.

App bug (codex P2): a draft referencing a since-deactivated grade rendered
without that line (SPA loaded active grades only) and the next save silently
dropped it via ReplaceGrades. Daily entry now loads grades includeInactive;
deactivated grades render (labeled) only while a prefilled quantity exists,
and the save payload includes them.

Doc corrections (codex P2s): deactivated-grade stock does NOT keep selling —
existing order lines confirm but new lines are rejected (help + GLOSSARY);
"typo in a feed purchase" scoped to quantities only (cost/date/lot number
not correctable); usage corrections fix stock, not the create-only usage
record or its cost estimate; "drafts freely editable" scoped (flock/date and
customer/date are fixed); legacy pre-allocation orders can't self-serve
void. GLOSSARY inventory-item entry: all categories support purchases/
corrections, only flock feeding is Feed/Supplement/Additive; deactivation
blocks new purchases, not using up stock.

UI (codex P2/P3): help tables wrap on phones instead of inheriting the
mobile nowrap scroller; glossary terms are th scope=row for screen-reader
term-definition association; --muted darkened to clear WCAG AA (4.6:1).

Browser-verified: help page sections/anchors/375px clean, depleted-backfill
flow intact, rowheader semantics in place.

---------

Co-authored-by: mforce <>
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.

F15: Feed inventory + feed usage

1 participant