Skip to content
Merged
45 changes: 39 additions & 6 deletions specs/product/GLOSSARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ does not count (it vacated its day, #82); a Draft does (captured, not yet
submitted). Depleted and archived flocks appear only when they do have an
entry today. Beside it the dashboard shows the last 14 days from the
**Production report** (submitted days only, yesterday back — a fixed window,
unlike Reports) with the report's own **Hen-day %** for the last 7 complete
unlike Reports) with the report's own **Hen-day %** for the last 7 finished
days against the 7 before, and stock as one stacked bar by grade whose total
is the Stock screen's.

Expand Down Expand Up @@ -700,11 +700,44 @@ the admin set on this screen, so clearing it would silently widen the page from
one record to the whole farm. The empty state stays a plain sentence with no
action (#655).

**Hen-day % (#91)** — eggs collected ÷ hen-days × 100 (spec §19.3). A
hen-day is one bird alive for one day; the day's bird count comes from the
bird ledger (placements + movements). The production report shows it per
day and for the whole period (period eggs ÷ period hen-days — not an
average of daily percentages).
**Hen-day % (#91, #780)** — eggs collected ÷ **recorded** hen-days × 100
(spec §19.3, amended by #780). A hen-day is one bird alive for one day; the
day's bird count comes from the bird ledger (placements + movements). The
production report shows it per day and for the whole period (period eggs ÷
period recorded hen-days — not an average of daily percentages).

The denominator counts only the houses that **recorded** the day (#780). A
house that filed nothing produced no eggs the report can see, so counting
its birds as exposure states a lay rate nothing supports: a day nobody
recorded read as 0%, and a day one house of three missed read as a collapse
in output. The **Hen-days** column keeps its full meaning — every bird
alive, recorded or not — and the gap between it and the recorded exposure
is exactly what is missing. A day nobody recorded has **no** percentage,
not 0.

A flock files against a date, and the bird ledger decides separately whether
that flock was live on it and how many birds it held. The two disagree at a
lifecycle boundary — an entry filed on the day after depletion, a placement
date corrected forward past entries that already exist, a mistyped mortality
that removes more birds than were placed. **The rate's numerator is
restricted to exactly the flocks whose birds are in its denominator**, so a
flock in any of those states keeps its eggs in the day's egg total and out
of the rate. Without that restriction the two exclusions compounded and a
farm laying 80% was reported at 160%.

The report therefore carries **Rated eggs** beside **Recorded hen-days** —
the percentage's own numerator and denominator — so the figure can be
reproduced rather than taken on trust. They equal the day's eggs and its
hen-days on any ordinary day; a gap means a filing and the bird ledger
disagree about that date.

**Partly recorded day (#780)** — a date where at least one flock filed an
official entry and at least one did not. Its egg total is a **floor**, not
the day's output, so the Dashboard's 14-day strip draws it hatched and
leaves it out of that panel's Peak and Avg; hen-day % still rates it on the
flocks that did file, so a missing flock lowers what the figure is measured
over rather than the figure itself. Distinct from a day nobody recorded,
which has no figure at all.

**Production report (#91)** — per-day official production over a range
(Draft entries aren't submitted, Voided ones vacated their day — neither
Expand Down
24 changes: 20 additions & 4 deletions specs/product/specs.md
Original file line number Diff line number Diff line change
Expand Up @@ -2230,12 +2230,28 @@ sum(bird_inventory_movements.quantity_delta)

```text
Hen-day production % (single day) =
eggs collected on date / hen-days on date × 100
eggs collected on date / RECORDED hen-days on date × 100

Hen-day production % (period) =
total eggs collected in period / sum(daily hen-days in period) × 100
total eggs collected in period / sum(daily RECORDED hen-days in period) × 100
```

**Recorded hen-days (#780).** The denominator counts only the flocks that filed
an official entry for that date. The numerator can only ever contain eggs from
flocks that filed, so counting a silent flock's birds as exposure reports a lay
rate nothing supports: before #780 a day nobody recorded read as 0% production,
and a day one flock of three missed read as a collapse in output. A date with no
official entry has **no** hen-day % at all — not 0.

The numerator is restricted to the same flocks. A flock outside its lifecycle
window, or one whose removals have over-run its placement, has eggs the report
shows and birds it does not count; admitting its eggs against a denominator it
is absent from reported 160% on a farm laying 80%. Both halves of the fraction
are carried in the payload so it can be reproduced. `hen-days on date` below
keeps its full meaning (every live laying bird, recorded or not) and is what the
report's own Hen-days column shows; the gap between the two is exactly what is
missing. → [`docs/decisions/`](../../docs/decisions/) and issue #780.

`hen-days on date` is the live laying-bird count **on that operational date**, reconstructed
from the bird ledger, not the current count:

Expand All @@ -2259,8 +2275,8 @@ Rules:
- Flag hen-day % as **approximate** for `mixed` flocks, and reserve a future
`female_bird_count` (or sex-split placement movements) to make it exact.
Do not silently report an exact-looking hen-day % for a mixed flock.
- Period rate divides total eggs by the sum of daily hen-days, not by an end-of-period
headcount.
- Period rate divides total eggs by the sum of daily **recorded** hen-days, not by an
end-of-period headcount and not by the calendar's full exposure (#780).
- The benchmark-meaningful period anchors to **start of lay** (`flock.expected_start_lay_date`,
or first laying production date), not an arbitrary calendar range. Comparing week 1 of
lay against week 20 as one blended rate produces a number that matches no industry
Expand Down
46 changes: 44 additions & 2 deletions src/Cluckwork.Application/Features/Reports/IReportQueries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,58 @@ public interface IReportQueries
// many did we grade" vs "how many can we sell"), they were only ever equal
// because conditions used to be losses, and merging them would silently move
// the number the capture screen is validated against.
// #780 — three fields exist so a consumer can tell what the farm KNOWS from
// what the farm PRODUCED. Every other figure here is 0 both for a day nobody
// recorded and for a day that genuinely produced no eggs, so without them the
// two are indistinguishable, which is what left the Dashboard's 14-day strip
// asserting a zero it had no evidence for.
//
// `RecordedFlocks` — flocks that filed an OFFICIAL entry (Submitted, Locked,
// ManagerAdjusted). 0 means nobody recorded the day; it does not mean the
// farm produced nothing. Counted from the entries, so a flock that filed
// counts whether or not the bird ledger agrees it was live that day.
// `ExpectedFlocks` — flocks that owed one: placed, not yet depleted or
// archived. It and `RecordedFlocks` can legitimately disagree EITHER WAY at a
// lifecycle boundary — a flock files on a day the ledger says it had ended, or
// a placement date is corrected forward past entries that already exist — so
// never assume one bounds the other.
// `MissingFlocks` — expected flocks with no filing, and the ONLY sound test for
// a partly recorded day. Comparing the two counts above is not: they are
// counts over different sets, so expected {A, B} against filings {A, C} gives
// 2 and 2 and hides B entirely, presenting a day short one flock's eggs as
// complete and feeding it into the Dashboard's Peak and Avg. `MissingFlocks`
// is computed from identities. `ExpectedFlocks - MissingFlocks` is how many
// of the flocks that owed a count actually filed one.
// `RecordedHenDays` — the exposure behind the rate. `HenDays` keeps the
// glossary's meaning (one bird alive for one day, over every flock) so the
// Reports column still says what it always said; the rate divides by the
// subset with evidence behind it. Dividing by `HenDays` is what made an
// unrecorded day read as a day of zero production, and did the same to a day
// one flock of three missed.
// `RatedEggs` — `HenDayPct`'s NUMERATOR: the eggs of exactly the flocks whose
// birds are in `RecordedHenDays`. Equal to `TotalEggs` on any day whose
// filings all come from live flocks with birds, which is every ordinary day.
// It is carried rather than left implicit so the rate is reproducible from
// the payload; without it, `TotalEggs / RecordedHenDays` silently disagrees
// with the percentage beside it on exactly the contradictory days.
public sealed record ProductionDay(
DateOnly Date, int TotalEggs, int Cracked, int Dirty, int Discarded,
int Sellable, int FromCounts, int Deaths, long HenDays, decimal? HenDayPct);
int Sellable, int FromCounts, int Deaths,
int RecordedFlocks, int ExpectedFlocks, int MissingFlocks,
long HenDays, long RecordedHenDays, int RatedEggs, decimal? HenDayPct);

public sealed record GradeTotal(Guid EggGradeId, string Name, int Quantity);

// `TotalRecordedHenDays` and `TotalRatedEggs` are `PeriodHenDayPct`'s exact
// denominator and numerator (#780), carried so the period figure is
// reproducible from the payload rather than being a number nobody can check.
// `TotalRecordedHenDays` equals `TotalHenDays` on a period every flock
// recorded, and the gap between them is exactly what is missing.
public sealed record ProductionReport(
IReadOnlyList<ProductionDay> Days,
int TotalEggs, int TotalSellable, int TotalFromCounts, int TotalDeaths,
long TotalHenDays, decimal? PeriodHenDayPct,
long TotalHenDays, long TotalRecordedHenDays, int TotalRatedEggs,
decimal? PeriodHenDayPct,
IReadOnlyList<GradeTotal> GradeTotals);

// About the period's ORDERS (order date in range): revenue is their confirmed
Expand Down
Loading
Loading