phase-4: the live tax position, deadlines and the notifications behind them
The dashboard, the deadline engine, the three sweeps and the notification channels. Confirm a comprobante and the IVA and IRP figures move; every headline number opens the documents behind it. The dashboard does no arithmetic of its own: it picks inputs and calls packages/rules. The acceptance test asserts the API's numbers equal what computeF120 and computeF515 produce over the same rows, before and after the bandeja is confirmed, so a drift in either direction fails. Deadlines come from the calendario perpetuo, skipping any period earlier than the date the taxpayer took the obligation on. Without that guard a brand new account opens on a red overdue card for a period that predates it. Sweeps are idempotent by dedupe key rather than bookkeeping: a restarted poller, a second replica and a crash mid-sweep all converge on one run, and one reminder per user per period per milestone. They queue notifications rather than sending them, so a channel being down retries on the job schedule. The T-10 test derives the date from dueDateFor rather than restating the calendar. Auto-confirm only touches what the rules were confident about and never a decision the user already made. An unconfigured channel is absent rather than broken: the fan-out skips it, the UI hides it, and a frozen account receives nothing. Web: the dashboard's three zones with swipeable position cards and traceable numbers, the deadlines timeline, and the app shell with the tab bar and the persistent scan button FLOWS.md asks for. Two additions to the specs, both marked: hasDocuments on DashboardDto, without which a position of all zeros is indistinguishable from a real one and the first-run state never shows; and an insight_dismissals table, which FLOWS.md requires and SPEC.md has nowhere to put. Also: pnpm db:reset, because the e2e suite changes the seed it runs against and a suite that is not repeatable is not a suite. 280 vitest tests, 51 Playwright tests, rules coverage still 100%, typecheck and lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b074456b70
commit
5dc284c5f2
@@ -367,3 +367,89 @@ as well as the name, because the name is deliberately not part of the dedupe key
|
||||
- **Seeded declarations.** CONTRACTS.md section 4 asks for a ready F120 and an approved one.
|
||||
The declarations module is phase 5 and seeds them then; phase 3 seeds the documents they
|
||||
will be computed from.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Phase 4
|
||||
|
||||
### The dashboard does no arithmetic
|
||||
`modules/dashboard` chooses inputs and calls `packages/rules`. Every figure on the screen
|
||||
comes out of `computeF120`, `projectIrp` or `savingsThisMonth`, and the acceptance test
|
||||
asserts the API's numbers equal what those functions return over the same rows, before and
|
||||
after the bandeja is confirmed (CONTRACTS.md 5.2). Confirmed documents only: nothing the
|
||||
user has not agreed to is ever counted.
|
||||
|
||||
The IVA carry-in comes from the last approved F120's summary and is zero when there is
|
||||
none, per RULES.md section 8.
|
||||
|
||||
### A deadline before the obligation existed is not overdue
|
||||
`upcomingDeadlines` skips any period earlier than the `since` date on the obligation.
|
||||
Without it a brand new account opens on a red "overdue" card for a period that predates it,
|
||||
which is both wrong and the worst possible first impression.
|
||||
|
||||
Maria still shows overdue, correctly: she has been registered since 2024 and has filed
|
||||
nothing, because declarations arrive in phase 5.
|
||||
|
||||
### Sweeps are idempotent by dedupe key, not by bookkeeping
|
||||
Each sweep is enqueued with a key naming the day (or the hour, for the digest) it belongs
|
||||
to, so a restarted poller, a second replica and a crash mid-sweep all converge on one run.
|
||||
The notifications they queue are keyed the same way, down to the milestone:
|
||||
`deadline:<user>:<obligation>:<period>:T-10` can only be queued once, whatever else
|
||||
happens. Sweeps queue notifications rather than sending them, so a channel being down
|
||||
retries on the job schedule instead of losing the message.
|
||||
|
||||
The time-travel test derives T-10 from `dueDateFor` rather than hardcoding a date, so it
|
||||
follows the calendario rather than restating it.
|
||||
|
||||
### Auto-confirm only touches what the rules were sure about
|
||||
The sweep confirms documents older than the user's window whose classification is still
|
||||
`decided_by = 'auto'` and at least 0.85 confident. A low confidence read is exactly the
|
||||
thing a person still needs to look at, and a decision the user already made is never
|
||||
revisited. Zero days means off.
|
||||
|
||||
### An unconfigured channel is absent, not broken
|
||||
`createChannels` returns `null` per channel when its configuration is missing. The fan-out
|
||||
skips those, the profile screen hides them, and a user with everything off receives nothing
|
||||
at all, which is the setting working rather than a failure. One dead push subscription does
|
||||
not stop the others; a frozen account is not a recipient.
|
||||
|
||||
### `hasDocuments` extends DashboardDto
|
||||
SPEC-GAP. FLOWS.md A6 wants the guided empty state until the first comprobante lands, and a
|
||||
position of all zeros is indistinguishable from a real one without this. It is an addition
|
||||
to CONTRACTS.md section 2, not a rename.
|
||||
|
||||
### `insight_dismissals` is a new table
|
||||
SPEC-GAP. FLOWS.md C3 requires dismissals to persist and SPEC.md section 5 lists nowhere to
|
||||
put them. One row per dismissal keeps the dashboard read free of per-user JSON to merge.
|
||||
|
||||
### The scan button is now global
|
||||
FLOWS.md B1 asks for a persistent scan affordance on every `(app)` screen. It arrives with
|
||||
the tab bar, and the per-screen scan link that stood in for it on `/comprobantes` is gone,
|
||||
since two of them on one screen is one too many.
|
||||
|
||||
### A copy bug the screenshot caught
|
||||
The overdue card filled a `{period}` slot with the formatted due date, so it read "sin
|
||||
presentar de 19 de marzo". The string now says what the data actually is: a form that was
|
||||
due on a date and never filed.
|
||||
|
||||
### Test isolation, again
|
||||
The read-only bandeja tests run against Carlos rather than Maria, because the mutating
|
||||
ingestion flows pull cards out of Maria's stack and two workers cannot share one queue.
|
||||
|
||||
The suite also erodes the seed it runs against: after a run, documents that started in the
|
||||
bandeja are confirmed and insights are dismissed, so the next run is not the same run.
|
||||
`pnpm db:reset` rebuilds the local database from the migrations and the seed, and the
|
||||
README says to use it before an e2e run. It refuses to touch anything but a local SQLite
|
||||
file, because it is destructive by definition.
|
||||
The dashboard's insight test self-skips once every insight for that account has been
|
||||
dismissed, since dismissals persist by design; the persistence guarantee itself is asserted
|
||||
against the API from a fresh database.
|
||||
|
||||
### Deferred, deliberately
|
||||
- **Push subscription registration.** `POST /push/subscribe` needs the service worker, which
|
||||
is phase 7. The channel itself is built and tested.
|
||||
- **Telegram linking.** The channel sends to a stored `telegram_chat_id`; the flow that
|
||||
obtains one is not in v1 scope.
|
||||
- **Declarations.** `declaration_ready` is in the next-action priority and the sweep reads
|
||||
from it, but nothing sets a declaration to `ready` until phase 5.
|
||||
|
||||
Reference in New Issue
Block a user