Skip to content

feat(reports): show feed cost per dozen - #1306

Merged
mforce merged 6 commits into
mainfrom
feat/1298-feed-cost-per-dozen
Oct 15, 2026
Merged

mforce merged 6 commits into
mainfrom
feat/1298-feed-cost-per-dozen

Conversation

@mforce

@mforce mforce commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner

Spec §19.3 defines feed cost per dozen as feed cost over dozens produced. The cost was already recorded: every FeedUsage row carries EstimatedCost (lot-cost costing, §12.4). This PR computes the figure server-side in ReportQueries.GetProfitAsync and shows it in the Reports page's admin Money section, between Expenses and profit.

Closes #1298

The formula

feed cost per dozen = Σ FeedUsage.EstimatedCost (Date in range) × 12 / official eggs

The denominator is the same eggs the production report counts: the OfficialStatuses filter ReportQueries already uses (Submitted, Locked, ManagerAdjusted). Drafts and voided entries contribute nothing. It reuses that filter rather than writing a second copy of the rule.

/reports/profit is already AdminOnly, so no new endpoint and no auth change. The response adds feedCostMinorUnits and feedCostPerDozenMinorUnits, both in minor units with the currency fields the report already carries.

Rounding

The per-dozen minor units are rounded to a whole minor unit with Math.Round's default midpoint rule, to even: 2,050 × 12 / 240 = 102.5 exactly answers 102, not 103. A range with no eggs returns null, shown as "—", never 0 or infinity. Both rules have their own integration tests.

Currency

The farm currency cannot change once feed usage rows exist, so no per-row currency filter is needed and no row can be left out:

  • FeedUsageRepository implements Farm's ICurrencyBoundRowSource with AnyAsync reading db.FeedUsages (refactor(arch): move repositories and EF configurations into their modules #1097).
  • UpdateFarmSettingsHandler probes every ICurrencyBoundRowSource inside its transaction and passes the result to Account.UpdateSettings, which refuses a currency change with Account.CurrencyLocked when any such row exists.

So a farm with feed rows is pinned to the currency those rows were stamped in, and summing all EstimatedCost values in the range never mixes currencies. The "feed in another currency is excluded" test from the issue does not apply, because the state cannot arise.

Zero-cost lots

Feed taken from a lot with no unit cost adds 0, so the figure can read low. The Help text says so plainly (en, es, tl) and the glossary repeats it; the UI does not hide it.

UI

One row in the Money section, after Expenses: the per-dozen figure via fmt.money, with the range total beneath it as the secondary line, like the expenses row. Strings in en, es and tl; specs/product/GLOSSARY.md and the in-app glossary define the term; the Reports Help section covers it.

Tests

Integration (real Postgres, tests/Cluckwork.Api.IntegrationTests/ReportsTests.cs):

  • Profit_FeedCostPerDozen_SumsInRangeFeed_OverOfficialEggs seeds 240 submitted eggs and feed worth 35,000 in range plus 7,500 at day −10; the report answers 1,750 per dozen.
  • Profit_FeedCostPerDozen_RoundsMidpointToEven seeds 2,050 over 240 eggs and expects 102.
  • Profit_ZeroEggRange_NullPerDozen records feed but no official eggs (a Draft's 50 eggs don't count) and expects null with the total still reported.
  • RangeGuards_And_RoleSplit pins the role boundary: a Worker gets 403 on the money reports and 200 on production. A Manager reads the money reports, because AdminOnly has meant Owner + Manager since F27: Roles + scoped permissions — role model, flock-scoped workers, RBAC UI (builds on #73) #103 (AuthPolicies.cs:50), and the SPA's isAdmin matches. The issue's draft criterion said a Manager cannot read the figure; that does not hold on this platform, so the test pins the real behavior instead. The figure is in the existing Money section, which has always been visible to Managers.

ReportsPage.test.tsx covers the row for an admin and the dash for a null figure.

Mutations

Each mutation was applied to ReportQueries.GetProfitAsync, rebuilt, and confirmed to turn the named test red, then reverted:

Mutation Red test
Drop the feed cost date filter Profit_FeedCostPerDozen_SumsInRangeFeed_OverOfficialEggs (35,000 becomes 42,500)
Divide by eggs instead of dozens Profit_FeedCostPerDozen_SumsInRangeFeed_OverOfficialEggs (1,750 becomes 146)
Return 0 instead of null Profit_ZeroEggRange_NullPerDozen
Use sellable eggs (total minus losses) as the denominator Profit_FeedCostPerDozen_SumsInRangeFeed_OverOfficialEggs (1,750 becomes 1,944; the fixture carries 24 discarded of 240)
Count Draft eggs (drop OfficialStatuses from the eggs query) Profit_ZeroEggRange_NullPerDozen
Drop the currency filter not applicable; see the currency section

Module ledger

ReportQueries now reads FeedUsages, so the Insights → GeneralInventory edge's symbol list gains ReportQueries (it already named ExportQueries for the same owner). ModuleLedgerRealTreeTests, SeamSurfaceRealAssemblyTests, AdapterReachRealTreeTests and CouplingMatrixRealTreeTests pass; the coupling matrix is regenerated.

Precache

PRECACHE_CEILING_KIB moves from 1,665 to 1,675 KiB in its own commit (chore(web): raise the precache ceiling to 1675 KiB for the report KPIs), per the maintainer's approval covering #1298–#1300 in one decision. This branch measures 1,665.81 KiB (npm run build + verify:sw).

Screenshots

Before/after at desktop 1280×800 and phone 390×844, same scenario, admin, default 7-day range, from two isolated stacks (before 9aad11a0 on cw1298b, after the merged head on cw1298), fixture priced at 42,500 minor units of feed in range: #1306 (comment)

Sum FeedUsage.EstimatedCost over the report range in GetProfitAsync and
divide by the official eggs the production report counts (× 12), returned
as feedCostMinorUnits and feedCostPerDozenMinorUnits — the latter null for
a range with no eggs and rounded to a whole minor unit, midpoint to even.
Every feed row is already in the account's current currency
(FeedUsageRepository is an ICurrencyBoundRowSource), so no per-row filter.

The Money section shows the figure between expenses and profit with the
range total beneath it, with en/es/tl strings, a glossary entry and Help
prose covering the zero-cost-lot caveat. Integration tests pin the range
filter, the official-only denominator, the to-even rounding and the null;
the Manager 403 joins the Worker's in the role-split test.

Closes #1298
@mforce
mforce force-pushed the feat/1298-feed-cost-per-dozen branch from 3ffbde6 to bc574aa Compare October 15, 2026 01:49
…403, tl typos

Four review findings on #1306:

- Reports Help (en/es/tl) described the KPI as feed over eggs collected
  with no factor of twelve; now "feed cost in the range divided by dozens
  collected (official eggs ÷ 12)", matching the glossary.
- The two positive-denominator fixtures had no cracked/dirty/discarded
  eggs, so a sellable-eggs denominator passed every rate assertion. Both
  now carry 24 discarded of 240 (grades reconciled to 216) and the rate
  is asserted against the production response's TotalEggs. The sellable
  mutation was run and goes red at 1,944 vs 1,750.
- RangeGuards_And_RoleSplit gains a Sales user asserting 403 on
  /reports/profit; the comment no longer calls the Manager a non-admin
  (AdminOnly is Owner + Manager since #103).
- Tagalog: dosezena → dosena (label, Help, glossary), italog → itlog.
@mforce

mforce commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner Author

Screenshots, same scenario on both stacks, admin, default 7-day range. After: the row sits between Expenses and Profit with the range total beneath it. Before: no feed row.

Desktop 1280×800 Phone 390×844
Before before desktop before phone
After after desktop after phone

Captured at before 9aad11a0, after d317486a (the merged head at capture time). Two isolated compose stacks, one per build: cw1298b from /home/mforce/.cluckwork-slices/1298-before and cw1298 from the PR worktree, both deploy/docker-compose.yml, run one at a time (single host, one :8080). The fixture has priced feed in the range: 17 kg of layer feed @ 2,500 minor units = 42,500 minor ($425.00) across 1,680 official eggs over 7 days, so the after frames read $3.04 per dozen.

@mforce
mforce merged commit acfc7fa into main Oct 15, 2026
22 checks passed
@mforce
mforce deleted the feat/1298-feed-cost-per-dozen branch October 15, 2026 03:05
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.

Reports: show feed cost per dozen (spec §19.3)

1 participant