phase-2: identity, from the landing hook to the profile screen

Flows A1 to A6 and E3 end to end. A visitor types a RUC on the landing page,
sees their real filing dates, registers, verifies a six digit code, grants
consent, completes a three step setup and lands on the first run screen, with
the profile, consent and audit rows to show for it.

API: public RUC lookup behind a token bucket (10/min/IP), the full /me surface
(profile, dependents, consents, notification prefs, data export, account
deletion), an append-only audit module that exports an insert and nothing else,
and a PII module that is the only thing allowed near those tables.

Deletion and consent revocation both freeze the account and drop every session,
reusing better-auth's ban flag rather than adding a second notion of disabled.
Nothing is destroyed yet: the purge is a job for phase 4. deadlineDigit is
always derived server side, never accepted from the client.

Web: landing with the RUC hook, registration, OTP verification, consent, the
setup wizard, the profile screen with "Tus datos", and legal pages that ship as
marked placeholders per COPY.md section 13. Money, Skeleton, Switch and
EmptyState components added.

The seed is now complete for identity: Maria at 4123456-1, filing digit 6 and
day 19, with a dependant, consents and prefs; Carlos as an IVA-only company.

Two real defects found by building the screens and fixed with tests:
the OTP boxes dropped a digit because the handler fired effects inside a
setState updater that React 19 invokes twice, and the switch knob rendered
outside its track because translate-x-5.5 does not resolve.

232 vitest tests, 26 Playwright tests across mobile and desktop, coverage still
100% on the rules, typecheck and lint clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Michilis
2026-09-03 23:33:10 +00:00
co-authored by Claude Opus 5
parent 80b10c958e
commit 0d7651b17c
61 changed files with 3776 additions and 82 deletions
+64
View File
@@ -209,3 +209,67 @@ the Playwright default base URL uses localhost.
This deviates from the literal example values in SPEC.md section 15, which use port 3000.
The compose stack still publishes 3000 and is unaffected.
---
## Phase 2
### The seed is now complete for identity
Maria is an individual whose RUC is her CI plus a check digit: base `4123456`, DV computed
by `computeRucDv`, so her filing digit is 6 and her day is the 19th, which is the example
FLOWS.md uses throughout. Carlos is a company registered for IVA only, so the IVA-only view
has real data behind it. Both have consents, notification preferences and the audit rows
CONTRACTS.md section 4 asks for. Documents arrive in phase 3.
### `GET /lookup/ruc/:number` reads a bare number as a CI
A number with a hyphen and a check digit is a RUC and the digit is verified. A bare number
is a CI, which is what individuals type when their RUC is their CI plus a digit; the filing
day is identical either way, which is the only thing the landing card shows. Neither form
touches the database: the endpoint reveals only what the number itself already encodes.
An unparseable number is a 200 with `valid: false`, per CONTRACTS.md section 3, so the
landing corrects the user inline rather than dead ending.
### Freezing an account reuses better-auth's ban flag
Both a deletion and a data processing consent revocation set `banned` with a distinct
`banReason` and drop every session. That reuses the check better-auth's sign in path
already performs, rather than introducing a second, parallel notion of a disabled account
that some code path would eventually forget to check. Nothing is destroyed yet: the purge
is a job, which lands with the jobs module in phase 4 (`softDeleteAccount` carries the TODO).
### `deadlineDigit` is derived, never accepted from the client
`PUT /me/profile` takes `ProfileInput`, which is `ProfileDto` minus `deadlineDigit`. The
digit decides real filing dates, so it is computed from the identity document server side.
The document itself is read only on the profile screen for the same reason: changing it is
a support conversation, not a text field.
### Deactivating a dependant rather than deleting it
A confirmed document may already be classified against a dependant, and that classification
has to keep making sense. `DELETE /me/dependents/:id` sets `active = 0`; the list filters on
it. Scoped by `user_id`, so one user cannot touch another's row (404, tested).
### Two real defects found by building the screens
**The OTP boxes dropped a digit.** The handler spread the pasted code inside a `setState`
updater and fired the focus move and the verify request from in there. React 19 invokes
updaters twice in development to check they are pure, so the effects ran twice and a digit
was lost. The spread is now computed outside the updater. Caught by golden path 1.
**The switch knob rendered outside its track.** `translate-x-5.5` does not resolve, so the
knob sat 44px along a 44px track, entirely outside it. Now `left-0.5` plus `translate-x-5`,
both on the spacing scale. `e2e/profile.spec.ts` asserts the knob stays within the track
bounds in both states, because this is invisible to every other kind of test.
### Deferred, deliberately
- **Motion.** FLOWS.md A2 and A6 call for GSAP (the card flip, the deadline stagger, the
first savings counter) and A1 for a canvas hero effect. The phase plan puts the motion
pass and the canvas touches in phase 7, so the screens here are built with the right
structure and states and no animation.
- **Push and Telegram.** Hidden until configured (FLOWS.md section 9). Setup step 3 offers
only email today; `POST /push/subscribe` needs the service worker and arrives in phase 7.
- **`/comprobantes`.** The first run buttons point at it. Ingestion is phase 3.
### The legal pages ship as marked placeholders
`/legal/privacidad` and `/legal/terminos` render "Documento en preparacion" and say a
document is being drafted, per COPY.md section 13. No legal text was generated.
**TODO: human written before launch.**