Collapse entry ledger to a scalar count column #50
Labels
No labels
bug
duplicate
enhancement
good first issue
help wanted
question
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lvl0/incr#50
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The core data-model change. The counter's value stops being
SUM(quantity)over a ledger and becomes a plain integer.Decision
Scalar wins. Per-increment history is dropped permanently and deliberately — it was never displayed, and keeping both a
countand an audit log means two sources of truth that can drift.Scope
Schema: add
trackers.count(unsigned integer, default 0); drop theentriestable.PHP:
app/Models/Transactions/Entry.php— includingtotalQuantity(),totalCost(),averageCostPerUnit()app/Http/Controllers/Transactions/EntryController.php— including thequantity × unit_price ≈ total_costvalidation ruleTracker::entries();User::hasEntries()entries.*route group → replaced by:POST /increment—$tracker->increment('count')PATCH /count— set an absolute value, validatedinteger|min:0Frontend:
components/Transactions/AddEntryForm.tsx— the date + quantity + price form is replaced by a plain number dialogdashboard.tsx:totalShares→count; drophandlePurchaseSuccessData migration
There is real data in
entrieson the dev DB. The migration must seedtrackers.countfromSUM(quantity)before dropping the table, rounded to an integer — the counter is now whole numbers only.Acceptance criteria
POST /incrementbumps the count by exactly 1PATCH /countsets an absolute value; rejects negatives and non-integersentriesreferences remainKnown bug this ticket must fix
Found by
code-reviewerduring #51 (8ce8a20). Deliberately left unfixed there because this ticket deletes the code path.OnboardingFlow.tsxposts the starting value toentries.storewith rawfetch(). ButEntryController::storereturns Inertia-style redirects —back()->withErrors()on validation failure,back()->with('success')on success — because it was written foruseForm().post()(see the oldAddEntryForm.tsx).fetch()follows redirects by default, so a validation failure 302s to/dashboard, gets200 text/html, andresponse.okistrue. The error branch never fires: the user is told setup succeeded while no entry was saved. The "Could not save the starting value" message is currently unreachable via status code.Not currently exploitable in normal use — the input is
type="number" min="0" step="1"and guarded client-side byquantity > 0— but the error path is dead code.Note: the reviewer also flagged a timezone risk (
todayISO()is UTC vs Laraveltoday()). Checked —config/app.php:68sets'timezone' => 'UTC', so both sides agree and this does not apply unless the app timezone changes.Resolution here:
POST /incrementandPATCH /countreplaceentries.storeentirely. Make them return JSON with real status codes, not redirects, so the client's error handling works.Add to acceptance criteria:
POST /incrementandPATCH /countreturn JSON with appropriate status codes (notback()redirects)Done —
d482c6dThe counter is now
trackers.count, a scalar.SUM(entries.quantity)and the ledger are gone.Schema:
2026_08_15_000002_add_count_to_trackers_drop_entries.php— addscount(unsigned int, default 0), backfills each tracker fromSUM(quantity)rounded and clamped at 0, dropsentries.down()reverses the schema (lossy on per-entry history by design, noted in a comment).PHP: new
CounterControllerwithPOST /incrementandPATCH /count; deletedEntry,EntryController,Tracker::entries(),User::hasEntries().Frontend: clicking the LED increments with an optimistic update, rolled back on failure and guarded against overlapping requests. New
SetCountFormbehind[SET VALUE]. DeletedAddEntryFormandInlineForm.dashboard.tsxnow makes one fetch instead of two.todayISO()removed, which also retires the UTC/timezone risk flagged in the #51 review.The redirect bug is fixed
Both endpoints return JSON with real status codes. The bug logged on this ticket —
entries.storereturningback(), sofetch()followed the redirect and reported success on validation failure — is gone with the endpoint. #54 adds a regression test assertingcontent-type: application/jsonon both routes.From review
max:4294967295added toPATCH /count, so an oversized value returns 422 rather than a DB exception surfaced as a 500finally$tracker->refresh(): the response is what the client trusts as truth, and re-reading from the database is a stronger contract than trusting an in-memory value. One query on a single-user app is not worth weakening that.Backfill verification
Originally shipped unverified — the migration had already run via a silent
container_tinkercall (#58), and the dev volume wipe had likely destroyed the originalentriesrows.#54 closes that gap.
CountBackfillMigrationTestexercises the conversion against real ledger rows: summing, fractional rounding, negative clamping, empty-tracker default, table drop, and thedown()round-trip. Mutation-checked — replacingmax(0, (int) round($total))with(int) floor($total)fails the rounding test and surfacesSQLSTATE[22003] Out of range value for column 'count', i.e. without the clamp a negative total would crash the migration rather than store a wrong number.Gates
Pint PASS (50 files) · PHPUnit OK (25 tests, 62 assertions) · ESLint exit 0 · build PASS
"PHPStan clean" not checked — not installed yet (#55).