Skip to content

F17 follow-up: voiding a daily entry vacates its day for re-entry (#82) - #83

Merged
mforce merged 2 commits into
feat/f17-entry-adjust-uifrom
fix/f17-voided-day-reentry
Jul 19, 2026
Merged

mforce merged 2 commits into
feat/f17-entry-adjust-uifrom
fix/f17-voided-day-reentry

Conversation

@mforce

@mforce mforce commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Closes #82. Stacked on #81 (base = feat/f17-entry-adjust-ui) — the Help-page void wording this PR extends lives there; GitHub retargets to main automatically once #81 merges.

Problem

A voided entry kept occupying its (account, farm, house, flock, date) natural key. RecordDailyEntryHandler found it with no status filter and the Draft-only guard refused with 422 "Only draft entries can be edited." — so a voided day could never be recorded again, with an error that didn't explain why. Void means "this entry should never have existed"; the day's real production still needs a way in.

Fix — Voided vacates the key

  • FindByNaturalKeyAsync excludes Voided entries.
  • IX_DailyEntries_NaturalKey becomes a partial unique index (WHERE "Status" <> 'Voided'): at most one live entry per house/flock/day, any number of voided ones preserved in history (migration VoidedEntryVacatesNaturalKey).
  • SPA prefill skips voided entries and raises the lookup limit past 1 (a re-recorded day keeps its voided siblings on the same date; same-date ordering is by id, so limit: 1 could return a voided row and hide the live one).
  • Stale copy fixed while here: the submitted-day banner said corrections were "(coming later)" — they shipped in F17 (part 1): Auto-lock + admin adjust/void of submitted entries with lot & ledger reconciliation (#69) #80/F17 (part 2): History-page admin adjust/void with status badges (#69) #81; it now points at History → adjust/void (admin). Domain comment saying the adjust flow is "a later slice" updated likewise.
  • Docs sync: Help "Fixing mistakes" void row now states voiding frees the day (its old "Then record the correct entry." was aspirational — broken until this PR); GLOSSARY Voided bullet documents the vacated key.

Tests (224 green)

Void_VacatesDay_SameKeyCanBeReRecorded: live entry still blocks the key (422) → void → re-record succeeds as a fresh entry (new id; the voided one keeps its numbers) → full lifecycle on the replacement → void it too → a third entry still fits (two voided rows + one live on the same key) → History lists all three → bird ledger nets back to the seeded count.

Runtime verify (Playwright, alt-port stack)

Void from History → Daily entry for the same flock+day comes up fresh (no banner, no voided prefill) → save & submit succeeds → banner shows the corrected wording → History shows Voided + Submitted rows side by side.

Notes

  • Old code against the migrated DB stays correct: the partial index is strictly weaker, and the unfiltered lookup still finds live entries first-class. No backfill needed.
  • Two racing creates on a vacated key hit the unique index (500, not a friendly 409) — same pre-existing behavior as racing creates on any brand-new day; not widened by this PR.

mforce added 2 commits July 19, 2026 10:58
A voided entry kept occupying the (account, farm, house, flock, date)
natural key: RecordDailyEntry found it with no status filter and the
Draft-only guard rejected the save with a misleading "Only draft entries
can be edited." — the day could never be recorded again.

- FindByNaturalKeyAsync excludes Voided entries; the natural-key unique
  index becomes partial (WHERE "Status" <> 'Voided'): at most one live
  entry per key, any number of voided ones preserved in history.
- SPA prefill skips voided entries (limit raised past 1 — a re-recorded
  day keeps its voided siblings on the same date) and the submitted-day
  banner drops the stale "(coming later)" for the shipped History
  adjust/void controls.
- Help + GLOSSARY: voiding frees the day for a fresh entry.
- Regression test: live entry still blocks the key; void -> re-record ->
  void -> re-record leaves two voided rows and a live one on the same
  day, with the bird ledger netting back out.
The stored status values are enum member names (HasConversion<string>),
so nameof keeps the raw-SQL filter compile-checked against renames.
Generated SQL is unchanged — no model drift, no new migration.
@mforce
mforce merged commit a001815 into feat/f17-entry-adjust-ui Jul 19, 2026
1 check passed
@mforce
mforce deleted the fix/f17-voided-day-reentry branch July 19, 2026 18:37
mforce pushed a commit that referenced this pull request Jul 19, 2026
- FindByNaturalKeyAsync keeps the tenant query filter on: the explicit
  AccountId predicate becomes belt-and-suspenders instead of the only
  tenant guard (IgnoreQueryFilters was inherited, not needed here).
- Daily entry prefill fetches the server's default page (100) instead of
  10 so the live row can't fall off the set behind voided siblings.

Declined with rationale (see PR #81 comment): unique-violation race on
concurrent creates keeps the repo-wide precheck+index-backstop pattern
(same as grade names); migration integrity assertion would check a state
the old fully-unique index made impossible.
mforce pushed a commit that referenced this pull request Jul 19, 2026
entryFor picked the first same-flock row for today regardless of status;
after void + re-record the arbitrary (GUID) ordering could surface the
voided row's totals instead of the replacement — or mark a genuinely
vacated day as covered. Voided rows are now skipped, so a day with only
voided entries correctly reads as 'no entry yet'.
mforce added a commit that referenced this pull request Jul 19, 2026
…#81)

* F17 (part 2): History-page admin adjust/void with status badges (#69)

- Status column renders the full lifecycle: Locked (muted, lock time on
  hover), Adjusted (reason on hover), Voided (warn + reason, row dimmed).
- Admin-only actions on Submitted/Locked/ManagerAdjusted rows: adjust
  opens a panel prefilled with the entry (totals, losses, deaths, grade
  lines — the entry's own grades plus the active saleable catalog; a
  deactivated grade already on the entry stays correctable), requires a
  reason, and sends the loaded version as its base; void uses the F13
  prompt-as-confirmation with a required reason. Workers see no actions
  and the API enforces the role regardless.
- Previously adjusted entries show the audit snapshot summary in the
  panel. 409 conflicts reload the list with a human message; the 422s
  (sold stock, pre-link entries) surface the API's explanation as-is.
- DailyEntry client type gains version/adjustReason/voidReason/lockedAtUtc/
  adjustedFrom; new adjustDailyEntry/voidDailyEntry API functions.
- Help page: History section describes the badges and controls; the two
  Fixing-mistakes rows now point at History -> adjust/void.

Closes #69.

* Address codex review of PR #81

- Key lifecycle split by outcome: any server response (success or 4xx)
  rotates the idempotency key so an edited retry is a fresh request — the
  version base already guards double-apply; only transport failures keep
  the key for an exact replay. Fixes the write-succeeded/refresh-failed
  trap where an edited retry replayed the old success and reported the new
  values as saved.
- 409 on adjust re-binds the panel to the freshly fetched entry (typed
  values survive, new version becomes the base) instead of closing and
  discarding the correction; if the entry became uncorrectable meanwhile,
  the panel closes with an explanation. Reload failures no longer claim
  the list was reloaded.
- Voiding an entry that is open in the adjust panel closes the panel.
- Void success message no longer says "stock returned" — voiding
  production empties the lots; it now says so.
- Panel scrolls into view and focuses its first input on open; error and
  success banners carry role=alert/status for assistive tech.

* Address pi review of PR #81

- 409 rebind refills every field from the freshly fetched entry instead of
  keeping this admin's typed numbers — a kept grade map could silently
  clobber a line the race winner just added. Only the reason survives.
- Client-side sellable-cap check (grades <= total - losses) for an instant
  message; the API stays the authority.

* F17 follow-up: voiding a daily entry vacates its day for re-entry (#82) (#83)

* F17 follow-up: voiding a daily entry vacates its day for re-entry (#82)

A voided entry kept occupying the (account, farm, house, flock, date)
natural key: RecordDailyEntry found it with no status filter and the
Draft-only guard rejected the save with a misleading "Only draft entries
can be edited." — the day could never be recorded again.

- FindByNaturalKeyAsync excludes Voided entries; the natural-key unique
  index becomes partial (WHERE "Status" <> 'Voided'): at most one live
  entry per key, any number of voided ones preserved in history.
- SPA prefill skips voided entries (limit raised past 1 — a re-recorded
  day keeps its voided siblings on the same date) and the submitted-day
  banner drops the stale "(coming later)" for the shipped History
  adjust/void controls.
- Help + GLOSSARY: voiding frees the day for a fresh entry.
- Regression test: live entry still blocks the key; void -> re-record ->
  void -> re-record leaves two voided rows and a live one on the same
  day, with the bird ledger netting back out.

* Use nameof for the partial index filter's column and enum literals

The stored status values are enum member names (HasConversion<string>),
so nameof keeps the raw-SQL filter compile-checked against renames.
Generated SQL is unchanged — no model drift, no new migration.

---------

Co-authored-by: mforce <>

* Address pi review of PR #83

- FindByNaturalKeyAsync keeps the tenant query filter on: the explicit
  AccountId predicate becomes belt-and-suspenders instead of the only
  tenant guard (IgnoreQueryFilters was inherited, not needed here).
- Daily entry prefill fetches the server's default page (100) instead of
  10 so the live row can't fall off the set behind voided siblings.

Declined with rationale (see PR #81 comment): unique-violation race on
concurrent creates keeps the repo-wide precheck+index-backstop pattern
(same as grade names); migration integrity assertion would check a state
the old fully-unique index made impossible.

* Address codex review of PR #83: dashboard must not show a voided entry

entryFor picked the first same-flock row for today regardless of status;
after void + re-record the arbitrary (GUID) ordering could surface the
voided row's totals instead of the replacement — or mark a genuinely
vacated day as covered. Voided rows are now skipped, so a day with only
voided entries correctly reads as 'no entry yet'.

---------

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.

1 participant