Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a9ef9e1
feat(eggs): bind Daily Entry condition counters to stable grade ident…
Aug 3, 2026
455b48f
feat(eggs): persist condition-grade bindings and entry snapshots (#396)
Aug 3, 2026
9b11616
feat(export): carry the condition snapshots and grade binding into ex…
Aug 3, 2026
87ec4c7
feat(daily-entry): refuse a condition grade named as a manual line (#…
Aug 3, 2026
37ed412
feat(daily-entry): resolve condition grades at submit and mint their …
Aug 3, 2026
79163c5
fix(daily-entry): reconcile the quality lots on adjust, not just the …
Aug 3, 2026
388abd0
feat(reports): report eggs that became stock without being hand-grade…
Aug 3, 2026
b6ecdc7
feat(web): show condition stock on Reports and History (#396)
Aug 3, 2026
1b307ad
feat(web): keep counter-fed grades out of the grading panes (#396)
Aug 3, 2026
f8a3b40
feat(seed): mint and sell condition lots in the demo and simulation f…
Aug 3, 2026
082aa15
fix(reports): include condition stock in the By-grade breakdown (#396)
Aug 3, 2026
6c84403
docs(migrations): make #407 the cutover to one migration per change (…
Aug 3, 2026
1c81df1
test(migrations): freeze InitialCreate's operations, not just its ide…
Aug 3, 2026
4889567
test(migrations): fingerprint every operation property, not a hand-pi…
Aug 3, 2026
6b3ee09
test(migrations): reach annotations via GetAnnotations(), which a pro…
Aug 3, 2026
0367bf7
fix(test): stop the migration digest depending on the machine it runs…
Aug 3, 2026
a0726fc
docs(test): record why the Type leaf uses FullName, not AssemblyQuali…
Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion AGENTS.md

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,38 @@ carries the pre-squash migration history: the 34 migrations were replaced by a
single `InitialCreate`, which EF then sees as pending and tries to apply over
tables that already exist. Such a database cannot migrate forward — recreate it.

### Changing the database schema

**Add a migration. Never edit `InitialCreate`.**

```bash
# Against the local dev Postgres. The connection is fail-closed (#318): there is
# no default, and every target is held to the same TLS floor as a Production
# boot — the loopback opt-out below is what permits plaintext, and only for
# localhost/127.0.0.1/::1.
CLUCKWORK_MIGRATIONS_CONNECTION='Host=localhost;Port=5432;Database=cluckwork;Username=…;Password=…' \
CLUCKWORK_MIGRATIONS_ALLOW_INSECURE_LOOPBACK=true \
dotnet ef migrations add <Name> \
-p src/Cluckwork.Infrastructure -s src/Cluckwork.Api
```

Before PR #407 the repo held exactly one migration and schema changes were
hand-folded into it. That was only safe while the app had never been deployed —
no database had applied the file yet, so rewriting it was free. **#407 was the
cutover.** Now that a database exists which has already applied `InitialCreate`,
EF will never re-run it, so a hand-folded column is a column that silently does
not exist. It surfaces as broken behaviour (a failing login, a missing field),
not as a migration error — which is precisely what makes it worth a rule.

`InitialCreate` is also **not regenerable**: it carries four `lower("Name")`
expression indexes EF cannot model and 21 rows of guarded reference-data SQL,
and re-adding it would mint a new timestamp that desynchronises
`__EFMigrationsHistory` everywhere. `MigrationSecurityReviewTests` fails if it
stops being the first migration or loses its recorded id.

A dev database created **before #407 merged** predates those folded-in columns —
wipe and recreate it as above.

### Backup &amp; restore (self-hosted)

Two complementary layers (spec §17.5):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Daily Entry Reconciliation and Saleable Quality Eggs Implementation Plan

Tracks #394 and #396. Implementation must follow the non-CI writer rule in
PR #395.

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Require official Daily Entry reconciliation while allowing independently saleable Cracked and Dirty stock with farm-defined product prices.

**Architecture:** Store a stable Daily Entry kind on egg grades and snapshot the resolved Cracked/Dirty grade ids on the official Daily Entry. Manual grades always reconcile against the non-condition remainder; submission and adjustment add or reconcile all snapshot-backed lots atomically. Product prices remain product-owned and are never derived from another grade.

**Tech Stack:** .NET 10/C#, EF Core/Postgres migrations, React 19/Vite/TypeScript, FluentValidation, Vitest, xUnit/Testcontainers, Playwright simulation harness.

## Global Constraints

- Preserve `DailyEntry.Version++` on every mutation and add parallel-race coverage for new aggregate mutation paths.
- Drafts accept incomplete manual grade lines; submit and adjustment require exact reconciliation.
- `Discarded` stays non-saleable; Cracked and Dirty are saleable only through their snapshotted configured grade.
- **Add no second migration.** `Persistence/Migrations/` holds exactly one, `InitialCreate` (#245), and this application has never been deployed — a virgin database is the only starting state anywhere. Hand-carry the columns, the partial unique index, and the seed changes into `InitialCreate` itself, and hand-edit rather than regenerate: it carries four un-regenerable `lower("Name")` expression indexes and 21 rows of `WHERE NOT EXISTS`-guarded reference data that `dotnet ef migrations add` would silently drop. There is correspondingly **no existing farm whose saleability could be modified**, so the "leave existing farms untouched" half of this constraint describes data that cannot exist — dropping it avoids shipping the dead-backfill pattern the #245 squash was performed to remove.
- Update every non-CI writer and fixture per PR #395, including simulation manifest verification.
- Update `HelpPage.tsx` and `specs/product/GLOSSARY.md` with the behavior change.

---

### Task 1: Persist quality-condition identity and official-entry snapshots

**Files:**
- Modify: `src/Cluckwork.Domain/Eggs/EggGrade.cs`, `src/Cluckwork.Domain/Eggs/DailyEntry.cs`
- Modify: EF configurations and the hand-maintained `InitialCreate` (hand-edited, never regenerated) — **no second migration**, see Global Constraints
- Modify: base-reference seed SQL and migration security/base-data tests
- Modify: `src/Cluckwork.Infrastructure/Repositories/ExportQueries.cs` — add the two quality snapshot ids to the `daily-entries` projection and `DailyEntryKind` to the `egg-grades` projection, so a full account export can still reconstruct which grade a condition counter represented
- Test: Egg-grade and DailyEntry domain tests; migration tests; `ExportTests.cs` regression coverage for both changed projections

- [ ] Write failing tests for immutable `DailyEntryKind`, one Cracked/Dirty grade per farm, null/non-null quality snapshots on official entries, and an account export missing both additions.
- [ ] Run the focused tests and verify the model cannot yet distinguish a renamed Cracked/Dirty grade from any other quality grade, and that a full account export cannot yet reconstruct which grade a condition counter represented.
- [ ] Add the enum, partial uniqueness constraint, and official-entry snapshots, carried into `InitialCreate` by hand — columns, index, and the seed defaults together. Confirm afterwards that the four `lower("Name")` expression indexes and all 21 guarded reference-data rows survive, and that `MigrationSecurityReviewTests` and `BaseReferenceDataMigrationTests` still pass; treat a failure there as having destroyed something, not as a test to adjust.
- [ ] Add the quality snapshot ids to the `daily-entries` export projection and `DailyEntryKind` to the `egg-grades` export projection.
- [ ] Verify new-base defaults are saleable, existing official records remain non-reclassified, migration security tests still pass, and both export regression tests are green.

### Task 2: Enforce exact reconciliation and atomically produce quality lots

**Files:**
- Modify: `DailyEntry`, `RecordDailyEntryHandler`, `SubmitDailyEntryHandler`, `AdjustDailyEntryHandler`, grade repositories/commands as required
- Test: Domain, record, submit, adjustment, stock, report, and concurrency integration tests

- [ ] Write failing domain/API tests for ungraded and partially graded official entries, zero remainder, each saleable quality counter, each non-saleable quality counter, an **inactive-but-saleable** condition grade (must snapshot null — `EggGrade.Deactivate()` leaves `IsSaleable` set, so this is the case a saleability-only rule wrongly accepts), a zero-count day for an otherwise-saleable quality counter (snapshot recorded, no lot — `EggLot.Create` rejects zero), and an adjustment that preserves its original snapshot.
- [ ] Write failing API tests submitting a **condition-kind grade id as a manual line** — on both record and adjust — and assert it is refused. Excluding condition grades from the manual pane is a UI affordance, not the enforcement: `RecordDailyEntryHandler` gates manual-line eligibility on `IsSaleable` **alone**, with no `GradeType`/kind restriction, and this feature makes Cracked and Dirty saleable by default. So a direct or stale API client can name a Cracked id as a manual line, the exact-total check passes (it is only a sum), and submission then creates **both** that manual lot and the counter-backed quality lot for the same grade — double-counting the day's stock and breaking the one-lot-per-grade assumption reconciliation depends on. The rejection belongs at the handler/aggregate boundary where the UI cannot be bypassed.
- [ ] Run them red; assert status/version/lots remain unchanged on refusal.
- [ ] Implement `manualGrades == total - cracked - dirty - discarded` at the aggregate boundary, reject any manual line naming a condition-kind grade, resolve snapshots only on first official submission and only for a grade that is both `Active` and `IsSaleable`, and create/reconcile quality lots — skipping lot creation for a zero counter while still recording its snapshot — in the existing transactions.
- [ ] Update report and stock projections to count only snapshot-backed quality stock as marketable; verify no historical row changes its meaning.
- [ ] Run targeted integration suites plus parallel submit/adjust tests.

### Task 3: Extend the API contract and align Daily Entry and adjustment UI

**Files:**
- Modify: `src/Cluckwork.Api/Endpoints/DailyEntries/DailyEntryEndpoints.cs` — `DailyEntryResponse` gains the two quality snapshot ids
- Modify: `src/Cluckwork.Api/Endpoints/EggGrades/EggGradeEndpoints.cs` — `EggGradeResponse` gains `DailyEntryKind`
- Modify: `web/src/api/cluckwork.ts` — mirror both DTO changes in the `DailyEntry` and `EggGrade` TypeScript contracts
- Modify: `web/src/routes/DailyEntryPage.tsx`, `web/src/routes/HistoryPage.tsx`, shared dialog/styles if needed
- Modify: `web/src/i18n/en.ts`, `web/src/i18n/es.ts`, `web/src/i18n/tl.ts`
- Test: API integration coverage in `SubmitDailyEntryTests.cs`/`DailyEntryAdjustTests.cs` and `EggGradeManagementTests.cs` for the two new response fields; `DailyEntryPage.test.tsx`, `HistoryPage.test.tsx`, catalog parity/typecheck

- [ ] Write failing API integration tests asserting `DailyEntryResponse` carries both quality snapshot ids and `EggGradeResponse` carries `DailyEntryKind`, plus failing UI tests that exclude condition grades from manual entry, show quality counts in the marketable summary, disable official saves for an under/over grade total, and render the adjustment dialog as the two-pane Daily Entry layout.
- [ ] Run focused API and Vitest tests red.
- [ ] Add both response fields and their TypeScript contract mirrors, then implement shared calculations and the adjustment layout without changing draft-save behavior, conflict rebind, reason requirement, or accessible live feedback. The History UI must derive its reconciliation feedback from the entry's own snapshots, never the current egg-grade catalog, so a later saleability change cannot retroactively rewrite how a past entry reads.
- [ ] Run API integration tests, page tests, i18n catalog parity, and typecheck.

### Task 4: Preserve sales configuration and update non-CI writers

**Files:**
- Modify: product/grade UI guidance and relevant API tests
- Modify: `DemoDataSeeder`, `SimulationDataSeeder`, `tools/simulation/k6/bundles.js`, Manager/Worker Playwright specs, simulation manifest/verification scripts
- Test: seeder/manifest tests and simulation-harness self-check

- [ ] Write failing coverage proving a Cracked/Dirty product maps only to its saleable quality lot and can retain an independently configured default price.
- [ ] Update all seed and browser writers to satisfy exact reconciliation and include quality lots where intended.
- [ ] Recompute the simulation fixture manifest and add/update self-checks so a stale writer or fixture fails closed.
- [ ] Execute targeted seed, k6 bundle, Playwright, and harness verification commands documented by PR #395.

### Task 5: Document the operating rule

**Files:**
- Modify: `web/src/routes/HelpPage.tsx`, i18n help catalogs, `specs/product/GLOSSARY.md`, `specs/product/specs.md`
- Test: Help-page tests and catalog parity

- [ ] Explain that drafts are flexible but official saves reconcile exactly, and that Cracked/Dirty quality stock is farm-configurable and separately priced through products.
- [ ] Document the no-retroactive-reclassification rule.
- [ ] Run Help, i18n, backend, and SPA verification suites before review.
Loading