A running, dated journal of every meaningful step: what we did, why, and what's next. Newest entries at the top.
Diario fechado de cada paso importante: qué hicimos, por qué y qué viene después. Las entradas más recientes van arriba.
Done (P1 of PLAN-v2, backend PR)
WorkOrder+WorkOrderLineentities: customer, title, description, status (pendiente→en_curso→terminado→facturado), scheduled time, labour (hours × rate + VAT) and material lines in exact integer cents. New tables only — the invoice hash chain is untouched.WorkOrderService::convert()reusesQuoteService::convert()exactly: aconvertedInvoicelink makes it idempotent (convert twice ⇒ same invoice). Materials → invoice lines, labour → one "Mano de obra (N h)" line. Issued throughInvoiceService::create(), so it inherits gapless numbering + the chain record and respects the billing mode (ADR 0004).WorkOrderController: CRUD +/convert, all user-scoped; an invoiced order is immutable (409).- Tests:
WorkOrderServiceTest+ an integration test that converts twice and asserts a single invoice (total = materials + labour), isolation, immutability. 91 tests, 401 assertions.
Next
- P1 PR2 — mobile UI, photos and the client signature.
What happened
- Every invoice carried an AEAT test-host QR and a "Verifactu" legend, because
InvoiceServicealways writes a record and the PDF printed the QR whenever one existed. A real invoice for the pilot's real client would ship a pre-production QR. Wrong for real use.
Done (P0 of PLAN-v2)
- Per-user
User.billingMode(standarddefault /verifactudemo) + issuer fiscal profile (businessName,fiscalAddress) so a standard RD 1619/2012 invoice is complete. Migration backfills existing rows tostandard. InvoicePdf::build(..., bool $showVerifactu): QR + legend only in demo mode; the standard PDF is an ordinary invoice./qrand/xmlreturn 403 in standard mode.- The hash chain runs in both modes.
VerifactuHasher::fingerprint()does not include the mode, so switching mode never breaksGET /api/invoices/verifyover pre-existing invoices — asserted by a regression test. - Account UI: a plain-language mode selector + issuer form; invoices UI hides the QR/XML/chain in standard mode and shows a DEMO banner in Verifactu mode. Spanish, non-technical.
- Verified the obligation date against the BOE: autónomos from 1 July 2027 (RD-ley 15/2025); optional in 2026. See ADR 0004, guide 38.
- 85 tests, 379 assertions.
Next
- P1 — work orders (partes de trabajo).
What happened
- The designer computed the minimum points per room and then ignored them. You could draw a kitchen with three sockets, export it and generate the CIE. The core promise of the product was unfulfilled.
- Worse: the minimums themselves were an approximation someone invented. I downloaded the Ministry's GUÍA-BT-25, parsed the PDF and read tabla 2. Five discrepancies — see guide 37.
Done
roomRequirements()now is tabla 2: the kitchen's nine sockets across four circuits, the "3 sockets or one per 6 m², whichever is greater" floor, corridors ruled by length not surface, a terrace with no socket at all.- Fixed the grade: more than five circuits requires an extra differential, not electrificación elevada ("no supondrá el paso a electrificación elevada", 2.3.1). We were raising the contracted power from 5 750 W to 9 200 W for nothing — a real bill for the customer. The eight real triggers are implemented.
- Sockets declare the circuit that feeds them (C2/C3/C4/C5/C10). One that declares none is credited against whatever its room still lacks; one that declares the wrong circuit is not.
validateLayout()attributes devices by point-in-polygon, measures rooms by the shoelace formula (an L-shaped salón counts by its real surface), and names each shortfall. Create CIE is disabled while the plan falls short.panelSchedule()draws the main panel (CGMP) from the devices actually placed: circuits sized by the points connected, split at the maximum of tabla 1, IGA from the contracted power, one differential per five circuits, and DIN modules counted so the enclosure can be ordered.
Why it is built this way
- Both readers share one private
tally(). Counting twice would eventually make the check and the panel disagree about what is installed — a bug that is hell to chase. - An empty plan reports
checked: false: unchecked, not compliant. A validator that says "OK" to an empty input is worse than no validator. - The app states plainly that a plan can only prove the number of points of use. Sections, earthing and measurements are outside a drawing. No software can promise a certificate is never returned; what this closes is the commonest family of rejections.
Next
- Frontend tests (there are still none) — starting with the geometry and the homography.
2026-07-10 — Entry 046: The real plan underneath, de-skewed; rooms as polygons; the plan downloadable
Done
- The scanned plan sits under the 2D editor, its scale calibrated against a known dimension.
- Four-point rectification. A phone photo is rotated and keystoned, so a two-point calibration cannot save it: calibration recovers scale only, and a metre still measures differently across the sheet. The user marks the four corners; we solve the homography by Gaussian elimination and remap the image with bilinear sampling. Verified: corners land within 1e-13 px and straight lines stay straight.
- A self-intersecting or near-flat quad still yields a solvable system while collapsing the image, so convexity is checked separately. The first version of that guard let three collinear points through — a cross product of exactly zero is neither positive nor negative.
- Rooms are polygons: an L-shaped living room, a corridor. Drag an edge midpoint to spawn a vertex. Moving a room carries the devices inside it (members snapshotted at drag start).
- The finished plan downloads as a PNG with backdrop, areas, devices, legend and scale.
- Navigation regrouped: one Panel menu; Certificates and Installation became top-level routes.
- Production hardening: a rejected backdrop returned 200 with
background: null— the user watched their traced plan vanish. It now fails loudly with a stable code. And the container no longer swallows migration failures with|| true: serving traffic against a mismatched schema is how a login turns into a 500.
2026-07-09 — Entry 045: Production login broke — a missing migration, a CI guard, and better auth errors
What happened
- Login (and register) returned 500 in production. Not a password problem:
/api/healthwas fine, but a fake login returned 500 instead of 401. Cause: the BYOK feature added three encrypted columns toapp_user, but the migration was never generated —make:migrationhad failed silently because the local PostgreSQL was down, and I only checked the test output. Doctrine then queried columns that didn't exist in the production database. - Why the tests didn't catch it: the test schema is built from the entity metadata (SchemaTool on in-memory SQLite), never from the migrations. 59 green tests with a broken database. A real blind spot.
Fixed
- Confirmed the drift with
doctrine:schema:update --dump-sql, generated + applied the missing migration (Version20260709094713, three nullable columns — safe on existing rows), pushed; Render applied it on boot. Verified against production: login now answers 401 (invalid credentials), not 500.
The real fix — a CI guard
- New CI job "Migrations (schema in sync)": spins up a real PostgreSQL, runs every migration from
scratch, then
doctrine:schema:validate. If an entity changes without its migration, CI goes red before it can reach production. This is the class of bug the unit tests structurally cannot see.
Also: professional auth errors
- Failures now carry a stable machine-readable
codethe frontend translates:bad_credentials,email_taken,invalid_email,weak_password,too_many_attempts, plus server/network fallbacks. A customAuthenticationFailureHandlerreturns clean JSON. Login stays deliberately generic — the same 401 whether the account exists or the password is wrong, so nobody can enumerate registered emails; a test asserts the two responses are byte-identical. Minimum password raised to 8 characters (registration only, so existing accounts keep working). Suite now 60 tests, 273 assertions.
Done
- Users can now enable AI and open banking from Account → Integrations by pasting their own keys —
no env vars needed by the end user. Keys are encrypted at rest (
SecretCipher, AES-256-GCM from the app secret) and never returned to the client (status shows only configured + a masked hint).CredentialStoreresolves per-user keys with an env fallback; refactoredChatService,CategorizerService,GoCardlessClient(nowconfigure()d per request),OpenBankingServiceandBankControllerto read through it. New account endpoints (GET status, PUT/DELETE anthropic & gocardless)- an Account UI section. Migration adds three encrypted columns on
User. Suite now 58 tests, 250 assertions (cipher round-trip/tamper + an end-to-end BYOK test where saving GoCardless creds enables/api/bank/statusfor that user).
- an Account UI section. Migration adds three encrypted columns on
Why
- The features were gated behind server env vars an end user can't touch. BYOK makes them self-serve and honest (each user's own key), while staying secure — secrets encrypted, never echoed back.
Next
- Agentic AI + OCR could now build on a user's own Claude key.
Done
- Fixed the navbar overflowing/overlapping on phones. Each nav item now has an icon + a text label: desktop shows the label (unchanged look), mobile (≤820px) shows just the icon. The long user email collapses to a 👤 icon and logout to 🚪, so everything fits in a compact row. Tightened mobile paddings and the watermark size. Frontend/CSS only.
Why
- On a phone the five link names + the full email + three buttons wrapped and overlapped; icons keep it legible and small without touching the desktop layout.
Done
FloorPlan3D(react-three-fiber): extrudes the 2D layout into semi-transparent walls, places devices at realistic heights (socket 0.3 / switch 1.1 / light 2.4 emissive / panel 1.2 m) and the panel, with a floor + grid and OrbitControls (rotate/zoom/pan). Reads the samelayoutas the 2D editor. It's lazy-loaded behind a "View in 3D" toggle — the build confirms it splits into its own chunk (FloorPlan3D-*.js≈ 900 kB), so Three.js only downloads on demand and the main bundle is unchanged. Addedthree+@react-three/fiber@9+@react-three/drei@10(React 19 compatible). No backend change.
Why
- This is Phase 3 of the designer (enfoque B): a useful 2D model with an optional 3D presentation layer. The whole electrician flow now runs design → 2D plan → 3D view → CIE → quote → invoice.
Next
- Agentic AI + OCR (need an Anthropic key). Designer could later gain textured/first-person 3D, but that's a separate product.
Done
- Added
FloorPlanEditor: an SVG floor-plan canvas (metric grid) where rooms are draggable rectangles and the panel + devices (socket/switch/light) are placed by tool + click and dragged with Move (Delete removes). An Auto-place button seeds the plan from the design and drops each room's ITC-BT-25 points as devices.InstallationCalculator::layoutCable()measures cable as each device's Manhattan run to the panel (+drop, +10 % slack), so the UI shows "Cable from plan: N m" instead of the estimate once a layout exists. Layout persists as a JSON column onInstallation(migration);/computeaccepts alayout. Suite now 55 tests, 244 assertions (2 calculator + 1 integration).
Why
- Phase 1 gave a rough cable estimate from surface; a real layout gives real metres and a schematic plan the electrician can hand over. Same model will extrude into 3D in Phase 3.
Next
- Phase 3 (extruded walkable 3D). Agentic AI + OCR still need an Anthropic key.
Done
- Wired the installation designer into the rest of the billing module (frontend-only). From a computed
design, two buttons: "Create certificate (CIE)" prefills the Certificados form with the technical
data (power→kW, voltage, supply, earthing TT, circuits, IGA 25/40 A, differential 30 mA); "Materials →
quote" opens the Presupuestos form with a line per material. Added cross-tab prefill plumbing in
BillingPage(a tab hands data to another; manual tab clicks clear it). No placeholders: the user completes the customer/identity where it belongs.
Why
- This is the differentiator I kept pointing at: design → CIE → quote → invoice in one tool, each step feeding the next. The data the electrician already entered once now flows through the whole chain.
Next
- Designer Phase 2 (2D plan) / Phase 3 (3D). Agentic AI + OCR still need an Anthropic key.
2026-07-08 — Entry 039: Installation designer — Phase 1 (ITC-BT-25 calculator + single-line diagram)
Done
- New Instalación tab: a REBT ITC-BT-25 calculator.
InstallationCalculator(pure, unit-tested) turns rooms + expected loads into circuits (C1–C12 with section/PIA, splitting into C6/C7 when point limits are exceeded), minimum points per room, grade & power to contract, differentials, a bill of materials and an estimated cable length.InstallationControllerexposes a stateless/computeplus CRUD to save designs (Installationentity, JSON columns; result recomputed from stored input). The frontend recomputes live and renders a single-line diagram (SVG) + circuits/points/materials tables. Migration added. Suite now 52 tests, 233 assertions (6 calculator unit + integration). - Researched and grounded in ITC-BT-25 (5 mandatory circuits básico, 9+ elevado; one differential per 5 circuits; per-room point minima). Honest framing: a pre-dimensioning aid; cable is an estimate; not a signed project.
Why
- The electrician's design → CIE → quote → invoice chain starts here. This is Phase 1 of the designer we scoped (enfoque B); Phase 2 = 2D floor-plan editor, Phase 3 = extruded 3D view.
Next
- Phase 2 (2D plan), autofill the CIE from a design, materials → quote. Agentic AI + OCR still need a key.
Done
- Took the CIE from "draft" to sign-ready. Researched the GVA telematic procedure: documents must be PDF digitally signed (DNIe / FNMT / ACCV / Cl@ve-firma) and filed by the authorised installer at the sede — there is no third-party submission API. Chose the secure path (with the user): Cuentia never handles the installer's certificate.
- Upgraded
CiePdfto a complete CERTINS E-style document: full compliance declaration (RD 842/2002 + ITC-BT, favourable measurements), lugar y fecha, and reserved digital-signature areas for the company and the installer ("firma electrónica AutoFirma/ACCV"). Added an in-app "How to sign & file it" panel (download → sign with AutoFirma/ACCV → file at the GVA sede) with official links.
Why
- "Presentable de verdad" = a faithful document + a real signature + filing. The signature is done locally by the installer (private key never leaves their machine, as GVA recommends); filing has no public API, so it stays the installer's manual upload — the same honest boundary as Verifactu's real AEAT submission.
Next
- Agentic AI + OCR — both need an Anthropic API key.
Done
- Researched the Comunitat Valenciana CIE and confirmed an official model — CERTINS E (12/2012), filed
telematically with a digital signature via the GVA sede electrónica. Implemented a Certificate entity
CertificateControllerCRUD +CiePdf(a CERTINS E-style PDF via Dompdf) + a Certificados tab in Billing. Fields follow the REBT (RD 842/2002): installation, titular, installer company/installer, and technical characteristics (power, voltage, supply, earthing, IGA, differential, etc.). Migration added.
- Honest framing throughout (form note + PDF footer): it's a fill-in draft aid, not the official
telematic+signed submission. Tests: CRUD, validation, PDF, per-user isolation. Suite 45 tests, 192
assertions. Guide 32. (Fixed two small bugs the tests caught: an undefined-key read on an unset
installationType, and using null as an array offset in the PDF.)
Why
- A real plus for an electrician using the app: the CIE is the document they issue on every job, and reusing the same data they already keep for invoicing removes duplicate typing.
Next
- Agentic AI + OCR — both need an Anthropic API key.
Done
- Added quotes:
Quote/QuoteLineentities (non-fiscal, no hash chain),QuoteService,QuoteController(list/create/get/status/convert/pdf),QuotePdf, migration, and a Presupuestos tab (create with customer + catalog lines + valid until, status pill, per-row actions). The headline: convert copies a quote intoInvoiceService::create()to produce a fully sealed Verifactu invoice — idempotent (converts once, never duplicates), then marks the quoteconvertedand links it. - Tests:
QuoteServiceTest(totals/numbering/default series/draft/empty guard) + an integration test of create → status → convert → invoice sealed → idempotent → PDF. Suite now 44 tests, 179 assertions. Guide 31. Fixed a create bug where an unsetstatusre-read an undefined key and blanked the status.
Why
- Freelancers quote before they invoice; converting the accepted quote in one click (into a compliant invoice) is the natural workflow and keeps the fiscal chain separate from non-fiscal offers.
Next
- Agentic AI + OCR — both need an Anthropic API key.
Done
- Added a reusable services/products catalog:
Serviceentity (user-scoped name/unitPrice/vatRate),ServiceControllerCRUD (/api/services), migrationVersion20260708075806, and a Servicios tab. The new-invoice form gets an "Add from catalog…" dropdown that appends a line prefilled from the chosen service (still editable). Suite now 40 tests, 157 assertions (CRUD + validation + isolation).
Why
- Freelancers bill the same handful of services repeatedly; a catalog removes the retyping. Lines copy the service's values at creation, so deleting a catalog item never rewrites past invoices.
Next
- Quotes (presupuestos) — non-fiscal documents that convert into a real Verifactu invoice.
Done
- Turned the Invoices screen into a Billing section with sub-tabs (
BillingPage→Facturas,Clientes;Presupuestos/Servicioscoming). Added customer CRUD:CustomerController(GET/POST/PUT/DELETE /api/customers, all user-scoped) with a delete guard (409 if the customer has invoices) and requiredname/taxId. NewClientestab (list + create/edit/delete). Issuing an invoice can now reuse an existing customer bycustomerId(dropdown in the form);InvoiceServiceresolves id → get-or-create. - Tests: an integration test covering create/list/update, validation (400), the delete guard (409) and per-user isolation. Suite now 39 tests, 146 assertions. Guide 29.
Why
- This is the step from "issue a one-off invoice" toward an actual billing tool: customers you reuse, a section that can hold quotes and a services catalog next.
Next
- A services catalog to prefill lines, then quotes (presupuestos) with convert-to-invoice.
Done
- Each invoice can now be downloaded as a professional PDF (
InvoicePdfservice via Dompdf, pure PHP): issuer/customer, line table, totals, the Verifactu huella and the QR embedded as an SVGdata:URI.GET /api/invoices/{id}/pdf; the Invoices page shows a Download PDF link next to XML. Guide 28. Suite now 38 tests, 134 assertions (unit render + integration endpoint). - Also refreshed the README to showcase Verifactu invoicing and open banking, with an engineering highlight on the hash chain and an honestly rewritten scope section.
Why
- The QR/XML were developer artifacts; a PDF is the document a freelancer actually sends. Dompdf keeps it gd/imagick-free, and DejaVu Sans renders the € sign and accents.
Next
- Agentic AI + OCR — both need an Anthropic API key.
Done
- Built a real open-banking import via GoCardless Bank Account Data (PSD2):
GoCardlessClient(token → institutions → requisition → account transactions),OpenBankingService(connect + import with mapping), andBankController(/api/bank/status|institutions|connect|import). Movements now carry anexternalId(migrationVersion20260706103931) so re-imports skip duplicates. FrontendBankConnectcomponent on the Movements page: pick a bank → authorize (hosted GoCardless link) → import. - Feature flag: disabled unless
GOCARDLESS_SECRET_ID/_KEYare set; the UI then shows an honest "not configured" note and enabled-only endpoints return 503. - Tests:
GoCardlessClientTest(MockHttpClient),OpenBankingServiceTest(mapping, creditor-name fallback, dedup counts), integration test of the disabled path. Suite now 36 tests, 128 assertions.
Why
- Creating GoCardless app credentials asks for more personal data than makes sense for a portfolio, so the integration is built and tested against the documented API shape but not run live — shipping it behind a flag is the honest way to showcase the capability without pretending it's been exercised end-to-end.
Next
- Agentic AI + OCR — both need an Anthropic API key.
Done
- Each invoice now has its two Verifactu artifacts.
VerifactuQrbuilds the AEATValidarQRURL (nif, numserie, fecha, importe) and renders it as an SVG viaendroid/qr-code(SVG needs no gd/imagick);VerifactuXmlserializes aRegistroAltaXML withDOMDocument. EndpointsGET /api/invoices/{id}/qr(image/svg+xml) and/xml(download). The Invoices page shows the scannable QR + a Download XML link in the expanded detail, with a note that it's a faithful demonstration (test host), not a live submission. - Tests:
VerifactuDocumentsTest(QR url fields, SVG render, XML well-formed + carries the huella, first-vs -chained record) + integration coverage of both endpoints. Suite now 28 tests, 106 assertions.
Why
- SVG over PNG keeps the Docker image and CI free of the
gd/imagickextension. The QR and XML follow the AEAT format faithfully but point at the pre-production host — Cuentia isn't a registered issuer, so real SOAP submission stays Phase D (out of scope), as the ADR says.
Next
- Open banking (GoCardless Bank Account Data) — real bank movement imports.
Done
- New Invoices page (React): issue an invoice (customer + dynamic lines with a live client-side total
preview), see the list, and expand any row to reveal its Verifactu fingerprint (hash, the record it
chains to, sealed timestamp). A "Verify chain" button calls
/api/invoices/verifyand shows a green "🔒 Chain intact · N records verified" badge (or an amber "broken at …" one). Added the route, the navbar link and full ES/EN strings. Frontend build green (604 modules).
Why
- The Verifactu engine (phases A–B) was API-only and therefore invisible to anyone opening the live app. This page makes the tamper-evident chain something a visitor can see and operate — the point of the feature for a portfolio.
Next
- Phase C: the invoice QR (to the AEAT) and XML export, embedded in this same page.
Done
- Every issued invoice now generates an
InvoiceRecord(Verifactu registro de alta) carrying a SHA-256 fingerprint chained to the previous record — the core anti-fraud mechanism. AddedVerifactuHasher(canonical string + hash),VerifactuChain(integrity verifier), the record entity + repository, wired generation intoInvoiceService, and aGET /api/invoices/verifyendpoint; the invoice detail now returns itsverifactublock (hash, previousHash, generatedAt). Added ataxId(NIF) toUseras the fingerprint's issuer id. MigrationVersion20260706083619. - Tests:
VerifactuChainTest(determinism, chaining, tamper detection — both a mutated field and a resealed-but-unlinked record) + an end-to-end integration test (two invoices →/verifyok, count 2, chained, per-user isolation). Suite now 24 tests, 86 assertions.
Why
- A field-mutation test failed only on SQLite: a
NUMERIC1210.00read back as1210, so the fingerprint recomputed after a DB round-trip didn't match the sealed one (PostgreSQL returns1210.00, hiding it in the live check). Lesson banked: a cryptographic hash must never depend on how the database formats a value — amounts are now normalized to two decimals inside the canonical string.
Next
- Phase C: the invoice QR (to the AEAT) and the XML export.
Done
- Added the invoicing domain:
Customer,InvoiceandInvoiceLineentities (all scoped to aUser), their repositories, anInvoiceServiceand anInvoiceController(GET/POST /api/invoices,GET /api/invoices/{id}). MigrationVersion20260706075837creates the three tables. - Totals are computed in integer cents (never floats) and invoice numbers are correlative per
series (
nextNumber = MAX(number)+1) — both prerequisites for the Verifactu hash chain. - Wrote ADR 0003 (scope A→D + honest monetization note) and
guide 24. Added
InvoiceServiceTest(totals/VAT/numbering + empty-lines guard): suite now 18 tests, 59 assertions. Live check: one line @21 % + two @10 % → base 1100.00, VAT 220.00, total 1320.00, number 2026/1.
Why
- Verifactu (Orden HAC/1177/2024) becomes obligatory in 2026; getting ahead of it makes Cuentia a credible showcase. The domain model has to be right — cents-exact totals and gapless numbering — before the tamper-evident hash chain can sit on top of it.
Next
- Phase B: the
InvoiceRecordwith the chained SHA-256 hash and tamper-detection tests.
Done
- Switched the deploy target to free tiers: Render (Docker web service) for the backend and Neon
(PostgreSQL that doesn't pause) for the database; Vercel unchanged. Added
render.yaml(blueprint). - Fixed a Dockerfile bug: the Apache
$PORTwas substituted at build time (would be empty); now it's set at container start (${PORT:-8080}), so it works on Render/Koyeb/Fly/Railway. - Updated the deploy guide (Render + Neon, cold-start note, Koyeb alternative).
Why
- The Railway trial expired and paying isn't an option; the Dockerfile being host-agnostic means only the hosting instructions changed, not the app.
Next
- Deploy on Render + Neon, then add the URL + screenshots to the README and pin the repo.
Done
- Added
backend/Dockerfile(Apache + PHP 8.4, runs migrations on boot),public/.htaccess(apache-pack), awhen@prodframework config (trusted proxies + secure cookies), andfrontend/vercel.json(SPA routing/apiproxy to the backend — so no CORS and first-party session cookies).
- Wrote a full deploy guide (guide 23): PostgreSQL (Supabase/Railway), backend on
Railway with env vars,
app:create-userfor the admin, frontend on Vercel, verification & troubleshooting. - Tests still green (16), dev app unaffected.
Why
- The frontend→backend proxy avoids the classic cross-domain cookie/CORS pain and keeps the setup simple. Configs are prepared and documented; the actual cloud deploy is done from the user's accounts.
Next
- Go live (create the accounts, deploy), then add the URL + screenshots to the README and pin the repo.
Done
- Added
tests/Api/ApiIntegrationTest(WebTestCase): boots the real kernel/firewall/DB and tests register/ login/me/logout, 401 for unauthenticated, duplicate-email 409, per-user isolation (A imports 2, B sees 0), and clear/delete account. - Runs on SQLite in-memory (
.env.test) withdisableReboot()— no DB service needed; CI installspdo_sqlite. - Caught two real bugs:
transactionis a reserved word in SQLite (fixed by quoting the table name), and Windows SQLite file-locking (fixed with the in-memory DB).php bin/phpunit→ OK (16 tests, 52 assertions). Documented in guide 22.
Why
- Integration tests prove the whole stack is wired correctly — the strongest evidence, for an interviewer, that the app really works.
Next
- The live deployment.
Done
- Reworked
index.cssinto a "liquid glass" system (iOS/WhatsApp-inspired): translucent frosted cards and a floating rounded navbar (backdrop-filter), pill buttons/inputs, a tinted background, and WhatsApp-style chat bubbles (accent user bubble, asymmetric radii). Light + dark tokens. - Responsive: navbar wraps, charts stack, tables scroll horizontally (
.table-scroll) on mobile. - All via tokens/classes — components unchanged. Build passes. Documented in guide 21.
Why
- A cohesive, modern finish across light/dark and desktop/mobile is what makes the project feel professional at a glance.
Next
- API integration tests, then deploy.
Done
- Added
AccountController:POST /api/account/clear(delete the user's transactions) andDELETE /api/account(delete the account + data, invalidate the session — right to erasure). - Added an Account page (
/account, via the email in the navbar) with clear-data, delete-account (danger) and a privacy note;AuthContext.deleteAccount(). ES/EN. - Verified: clear 15→0; delete account → session invalidated. Documented in guide 20.
Why
- Real accounts need a way out (GDPR), and "clear my data" is a handy reset for the live demo — both cheap thanks to the per-user data isolation.
Next
- UX & responsive polish, API integration tests, then deploy.
Done
- Added
POST /api/demo/load: loads 2 months of realistic sample movements for the current user (only if empty) by reusing the real import + categorization pipeline, so a fresh account is never empty. - Added a "Load sample data" button to the Movements empty state (ES/EN).
- Added
app:create-userconsole command (with--admin) to create accounts/admins for a deployment. - Verified: created an admin; a fresh user loads 15 movements (2 months, 8 categories, VAT computed). Documented in guide 19.
Why
- Going public: the biggest demo-killer is an empty screen. Sample data (through the real pipeline) makes the app instantly explorable; the CLI safely seeds server accounts.
Next
- Account & GDPR (delete/clear), UX & responsive polish, API integration tests, then deploy.
Done
- Added a
Userentity + Symfony security-bundle: entity provider, sessionjson_login, logout, access control. Endpoints:POST /api/register,/api/login,/api/logout,GET /api/me. - Scoped everything to the current user:
Transactionnow belongs to aUser; imports set it;TransactionRepository::findForUser(); every service and the stats SQL filter by user (passed via#[CurrentUser]). - Frontend:
AuthContext+ a login/registerAuthPage;Appgates the whole app; navbar shows the email- logout.
- Updated all unit tests to the new signatures.
php bin/phpunit→ OK (11 tests, 37 assertions). Verified end to end with session cookies (register → login → scoped data → 401 when logged out). Documented in guide 18.
Why
- Multi-user data isolation is what turns this from a demo into a real SaaS — and enforcing it at the query layer is the correct, secure way.
Next
- Production hardening + the live deploy.
Done
- Added
ChatService: builds a factual context (balance, income/expenses, top categories, VAT, next IRPF payment) and asks Claude to answer using only it; deterministic data-summary fallback without a key. - Added
POST /api/chatand an Assistant page (/chat) with a chat UI; added the navbar link and ES/EN translations. Wired into the app. - Added
ChatServiceTest(fallback path).php bin/phpunit→ OK (11 tests, 37 assertions). Documented in guide 17.
Why
- Natural-language Q&A grounded on the user's own data is the flagship AI feature — and the fallback keeps it honest and always usable.
Next
- Phase 4: authentication/multi-user, then the live deploy.
Done
- Added
ForecastService(linear projection of the balance at +30/60/90 days from the average daily net) andGET /api/forecast. - Added a
Forecastline chart on the Dashboard (current balance + avg. monthly net tiles + an "estimate" note). Wired/api/forecastintouseFinanceData. - Added
ForecastServiceTest.php bin/phpunit→ OK (10 tests, 33 assertions). Documented in guide 16.
Why
- A forward-looking number is what a freelancer actually worries about ("will I make it?"), and a transparent, clearly-labeled model keeps it honest.
Next
- Natural-language questions over the transactions (AI), then auth + deploy.
Done
- Added
ImportService::importNorma43()— parses the fixed-width AEB Cuaderno 43 format (record 22 = movement, 23 = concept), and animport()dispatcher that auto-detects CSV vs Norma 43 by content. - The same upload endpoint/button now handles both; the file input accepts
.csvand.n43. - Added
ImportServiceTest(CSV + Norma 43, built from 80-char records).php bin/phpunit→ OK (8 tests, 25 assertions). Verified end to end with a sample.n43(4 movements imported). Documented in guide 15. Phase 2 is complete.
Why
- Norma 43 is what every Spanish bank exports; parsing it (and salaries/VAT correctly) is the domain depth that differentiates the project.
Next
- Phase 3 (cash-flow forecast, NL chat, open banking) or preparing the live deploy.
Done
- Added
.github/workflows/ci.ymlwith two parallel jobs: backend (PHP 8.4 →composer install→php bin/phpunit) and frontend (Node 24 →npm ci→npm run build). - Added a CI status badge (+ stack badges) to the README.
Why
- Running the tests and build on every push prevents regressions from being merged, and the green badge is instant credibility for anyone browsing the repo.
Next
- A live deployment with a link and screenshots in the README.
Done
- Installed PHPUnit (
symfony/test-pack). - Added
VatServiceTestandIrpfServiceTest: pure unit tests that build Transaction/Category objects in memory and feed them via a stub repository (no DB). Assert VAT (336/23/313), IRPF Q1 payment 230.64, salary excluded, and the next-deadline countdown. php bin/phpunit→ OK (6 tests, 14 assertions), no notices (usedcreateStubper PHPUnit 13). Documented in guide 13.
Why
- The tax logic is where a bug costs the most; testing it proves correctness, guards against regressions, and is a strong interview talking point.
Next
- CI (GitHub Actions running the tests) + a live deploy, or the Norma 43 importer.
Done
- Added a dependency-free i18n: a translations dictionary (en/es), a
LanguageContextwithuseTranslation()({ lang, setLang, t }), and an EN/ES toggle button in the navbar. Choice persists in localStorage. - Replaced hardcoded UI strings across navbar, pages, charts and tax panels with
t('key');t()supports{param}placeholders (e.g. the IRPF deadline alert, quarter prefix Q/T). - Default language Spanish; switches instantly. Documented in guide 12.
Why
- A bilingual UI signals international product thinking and showcases the author's ES/EN profile — a strong fit for remote/EU roles.
Next
- Norma 43 importer (finish Phase 2), then production concerns (tests, auth, deploy).
Done
- Replaced the template CSS (purple accent) with a design-token system in
index.css: surfaces, ink, one brand blue, semantic money colors, radii, shadows — plus a full dark theme. - Refactored every component/page to class names reading the tokens (
.card,.btn,.table,.tag,.navbar,.stat-value,.alert-warn…). Charts read--chart-*at runtime to follow the theme. - Sober, financial styling (hairline borders, subtle shadows, tabular figures) — deliberately not the flashy AI-template look. Documented in guide 11.
Why
- With all the functionality in place, a clean professional look is what turns "functional" into "impressive" for a recruiter.
Next
- Internationalization: an ES/EN language toggle.
Done
- Added React Router: a
Layout(navbar +<Outlet/>) that loads data once viauseFinanceData()and shares it with pages through the Outlet context. - Split the single page into Movements (
/), Dashboard (/dashboard) and Taxes (/taxes). - Extracted shared helpers (
lib/format,components/Stat). Verified all routes return 200 (SPA fallback) and the API proxy still works. Documented in guide 10.
Why
- A real multi-page SPA structure is cleaner, scalable and what a recruiter expects — and it sets the stage for the visual redesign.
Next
- Visual redesign: a clean, professional look that doesn't look AI-generated.
Done
- Added
IrpfService(reusesVatService::baseCents()): cumulative 20% of year-to-date net (self-employment income − deductible expenses, without VAT), salary excluded, per-quarter with official deadlines and a "next deadline" countdown. - Added
GET /api/irpfand an IRPF panel (quarter table + a warning banner when a deadline is < 30 days). - Verified: Q1 net 1153.21 → payment 230.64; next deadline Q2 2026-07-20 (18 days). Documented in guide 09.
Why
- IRPF + VAT together tell a complete tax story — the core value for a Spanish freelancer and the clearest demonstration of the finance moat.
Next
- Norma 43 import, then the frontend redesign.
Done
- Added
VatService(category → Spanish VAT-rate map, cents-based VAT extraction) andGET /api/vat. - Added a VAT summary panel in React: output VAT, input VAT and net (to pay / to reclaim).
- Verified on the sample data: output 336.00, input 23.00, net 313.00 to pay — salary and autónomo fee correctly carry no VAT. Documented in guide 08.
Why
- This is the finance moat: computing VAT correctly (right rates per category, salaries exempt, exact cents) is exactly the domain knowledge that differentiates this project.
Next
- IRPF estimate (modelo 130) and quarterly deadline alerts.
Done
- Added
GET /api/stats(raw SQL aggregation: totals, spending by category, income/expenses by month). - Added a
DashboardReact component (Recharts): a single-hue horizontal bar for category spending and a two-series bar for monthly income vs expenses. Colors chosen by the data-viz method and validated with the skill's script (CVD ΔE 74.6). Charts refresh on import/categorize. - Documented in guide 07.
Why
- This is the most "sellable" screen: at a glance you see where the money goes — the payoff of the import + categorization work.
Next
- Phase 2: the VAT panel (output vs input VAT, net due) — the finance moat.
Done
- Added
CategorizerService: a fixed category list, a deterministic Spanish keyword rule engine, and an optional Claude call (via HttpClient) that is validated against the allowed list. - Added
POST /api/transactions/categorizeand a "🧠 Categorize" button + Category column in React. - Verified with no API key: 8/8 categorized correctly by rules (Mercadona→Supermercado, Repsol→Combustible, Nómina→Nómina, etc.). Documented in guide 06.
Why
- Categorization is what turns a raw list into insight. Keeping AI optional (rule fallback) means the app always works and stays honest — a deliberate architecture principle.
Next
- Dashboard: spending by category and by month (Recharts), then the VAT panel (Phase 2).
Done
- Added
GET /api/transactions(maps entities to a plain DTO array, ordered by date). - Rebuilt the React
App.jsx: a transactions table (euro formatting, red/green by sign), an income/expenses/balance summary, and a file input that uploads a CSV (FormData) and refreshes. - Verified: the endpoint returns 8 rows through the Vite proxy and the frontend builds cleanly. Documented in guide 05.
Why
- First fully visible loop: upload a CSV → see your movements. It turns the backend work into something a person (or a recruiter) can actually see.
Next
- AI categorization: assign a category to each transaction with Claude + a rule-based fallback.
Done
- Added
ImportService(parses a bank CSV) andPOST /api/import/csv(thin controller). - The parser auto-detects the
,/;separator, maps bilingual columns, and handles the Spanish number format (1.234,56) — keeping money as an exact decimal string. - Added a sample file and verified end to end: imported 8 rows (balance 3316.21), 0 errors, accents preserved. Documented in guide 04.
Why
- This is the first tangible value: a real bank export becomes structured data. Keeping the finance parsing correct (Spanish decimals, no float) is the differentiator.
Next
- Expose the transactions via an API endpoint and render them in the React frontend.
Done
- Installed Doctrine (
symfony/orm-pack) and MakerBundle. - Started local PostgreSQL (
pg_ctl), configuredDATABASE_URLin.env.local, created thecuentiadatabase. - Wrote the
CategoryandTransactionentities (+ repositories); generated and ran the first migration. Tablescategoryandtransactionnow exist. - Documented it in guide 03. Decided to use auto-increment integer IDs for now (simpler than UUID); updated ARCHITECTURE accordingly.
Why
- The entities are the backbone of the app. Getting money right from day one (
decimal, not float) avoids a whole class of accounting bugs.
Next
- CSV import: an endpoint that parses a bank CSV and stores
Transactionrows (guide 04).
Done
- Scaffolded the React app (Vite) in
frontend/; added a dev proxy (/api→:8000) to avoid CORS. App.jsxcallsGET /api/healthon mount and shows "✅ API OK".- Verified end to end: with both servers running,
http://localhost:5173/api/healthis proxied to the backend and returns the JSON. Phase 0 is complete. - Documented it in guide 02, including the real
localhostvs127.0.0.1(IPv6) gotcha we hit.
Why
- Proving the frontend↔backend link now, on the simplest possible feature, means every later feature is built on a foundation we know works.
Next
- Phase 1: domain model (
Transaction,Category), PostgreSQL via Doctrine, and CSV import (guide 03).
Done
- Scaffolded the Symfony 8.1 API in
backend/(symfony new backend); removed its nested.gitto keep a single monorepo. - Added
HealthControllerwithGET /api/health; verified it returns{"status":"ok","service":"cuentia-api"}via a local server. - Documented the whole thing in the teaching
guide/(00-environment, 01-backend-scaffold).
Why
- A health endpoint is the smallest possible proof that the request→controller→response chain works — the right first milestone before adding a database or features.
Next
- Scaffold the React frontend (Vite) and have it call
/api/health(guide 02).
Done
- Created and pushed the public repo: github.com/R0b3r7DEV/cuentia (first commit = docs).
- Installed the backend toolchain via Scoop: PHP 8.5.7, Composer 2.10.2,
Symfony CLI 5.17.1, PostgreSQL 18.4 (cluster initialized; local
trustauth). - Created a
php.inienabling the extensions Symfony needs:pdo_pgsql,pgsql,intl,mbstring,openssl,curl,fileinfo,sodium,zip.
Why
- Building in public from commit 1 tells a strong story; a fully working local toolchain unblocks Phase 0.
Next
- Scaffold the Symfony API and the React app; first green
GET /api/health→ 200.
Done: made all docs bilingual (English first, Spanish below). Why: the author uses the docs in Spanish while learning, and recruiters read the English. Next: same as entry 001 — install tooling and scaffold.
Done
- Chose the project and stack (see ADR 0002): Cuentia, an AI cash-flow & tax copilot, built on Symfony + React + PostgreSQL + Claude.
- Created the documentation skeleton: README, ROADMAP, ARCHITECTURE, ADR 0001, ADR 0002, this dev log.
- Defined the initial domain model (
Transaction,Category) and the phased roadmap.
Why
- Starting with the why written down (ADRs) and a phased plan keeps the build focused and makes every decision defensible later — which is the whole point of this project.
Next
- Install backend tooling: PHP, Composer, Symfony CLI, PostgreSQL (Phase 0).
- Scaffold the Symfony API and the React app; get a
GET /api/healthreturning 200.
Hecho (P1 del PLAN-v2, PR de backend)
- Entidades
WorkOrder+WorkOrderLine: cliente, título, descripción, estado (pendiente→en_curso→terminado→facturado), fecha programada, mano de obra (horas × precio + IVA) y líneas de material en céntimos enteros. Solo tablas nuevas — la cadena de hash de facturas queda intacta. WorkOrderService::convert()reutilizaQuoteService::convert()igual: el enlaceconvertedInvoicelo hace idempotente (convertir dos veces ⇒ misma factura). Materiales → líneas; mano de obra → una línea "Mano de obra (N h)". Se emite porInvoiceService::create(), así que hereda numeración sin huecos + el registro de la cadena y respeta el modo de facturación (ADR 0004).WorkOrderController: CRUD +/convert, todo acotado al usuario; un parte facturado es inmutable (409).- Tests:
WorkOrderServiceTest+ un test de integración que convierte dos veces y comprueba una sola factura (total = materiales + mano de obra), aislamiento e inmutabilidad. 91 tests, 401 aserciones.
Siguiente
- P1 PR2 — UI móvil, fotos y firma del cliente.
2026-07-11 — Entrada 048: Modo dual de facturación — facturas reales por defecto, Verifactu como demo
Qué pasaba
- Toda factura llevaba un QR del host de pruebas de la AEAT y una leyenda «Verifactu», porque
InvoiceServicesiempre escribe un registro y el PDF imprimía el QR cuando existía. Una factura real del piloto para su cliente saldría con un QR de preproducción. Mal para uso real.
Hecho (P0 del PLAN-v2)
User.billingModepor usuario (standardpor defecto /verifactudemo) + perfil fiscal del emisor (businessName,fiscalAddress) para que una factura estándar RD 1619/2012 esté completa. La migración rellena las filas existentes astandard.InvoicePdf::build(..., bool $showVerifactu): QR + leyenda solo en demo; el PDF estándar es una factura ordinaria./qry/xmldevuelven 403 en modo estándar.- La cadena de hash vive en ambos modos. La huella no incluye el modo, así que cambiar de modo nunca
rompe
GET /api/invoices/verifysobre facturas preexistentes — comprobado con un test de regresión. - UI de cuenta: selector de modo en lenguaje claro + formulario de emisor; la UI de facturas oculta QR/XML/cadena en estándar y muestra un aviso DEMO en Verifactu. En español, no técnico.
- Fecha de obligación verificada contra el BOE: autónomos desde el 1 de julio de 2027 (RD-ley 15/2025); opcional en 2026. Ver ADR 0004, guía 38.
- 85 tests, 379 aserciones.
Siguiente
- P1 — partes de trabajo.
Qué pasaba
- El diseñador calculaba los puntos mínimos por estancia y luego los ignoraba. Podías dibujar una cocina con tres enchufes, exportarla y generar el CIE. La promesa central del producto estaba sin cumplir.
- Peor: los propios mínimos eran una aproximación inventada. Descargué la GUÍA-BT-25 del Ministerio, parseé el PDF y leí la tabla 2. Cinco desviaciones — ver guía 37.
Hecho
roomRequirements()es ya la tabla 2: las nueve tomas de la cocina repartidas en cuatro circuitos, el suelo de "3 tomas o una por cada 6 m², lo que sea mayor", los pasillos regidos por longitud y no por superficie, y una terraza que no lleva enchufe.- Corregido el grado: más de cinco circuitos exige un diferencial adicional, no electrificación elevada ("no supondrá el paso a electrificación elevada", 2.3.1). Subíamos la potencia contratada de 5 750 W a 9 200 W sin motivo — una factura real para el cliente. Implementados los ocho supuestos verdaderos.
- Cada toma declara el circuito que la alimenta (C2/C3/C4/C5/C10). La que no lo declara se imputa a lo que su estancia aún necesite; la que declara el circuito equivocado, no.
validateLayout()imputa dispositivos por punto en polígono, mide las estancias con el teorema del zapatero (un salón en L cuenta por su superficie real) y nombra cada carencia. El botón de crear el CIE se deshabilita mientras el plano no llegue.panelSchedule()dibuja el cuadro general (CGMP) con los dispositivos realmente colocados: circuitos dimensionados por los puntos conectados, desdoblados al máximo de la tabla 1, IGA según la potencia contratada, un diferencial por cada cinco circuitos y módulos DIN contados para poder pedir el envolvente.
Por qué así
- Los dos lectores comparten un único
tally()privado. Contar dos veces acabaría haciendo que la comprobación y el cuadro discrepasen sobre lo instalado — un fallo infernal de perseguir. - Un plano vacío informa
checked: false: sin comprobar, no conforme. Un validador que dice "OK" a una entrada vacía es peor que no tener validador. - La app dice sin rodeos que un plano solo puede demostrar el número de puntos de utilización. Secciones, tierras y mediciones quedan fuera de un dibujo. Ningún software puede prometer que un certificado no será devuelto; lo que se cierra es la familia de rechazos más común.
Siguiente
- Tests de frontend (no hay ninguno) — empezando por la geometría y la homografía.
2026-07-10 — Entrada 046: El plano real debajo, enderezado; estancias como polígonos; plano descargable
Hecho
- El plano escaneado va bajo el editor 2D, con su escala calibrada contra una cota conocida.
- Rectificación de cuatro puntos. Una foto de móvil sale girada y con perspectiva, y una calibración de dos puntos no la salva: solo recupera la escala, y un metro sigue midiendo distinto en cada zona de la hoja. El usuario marca las cuatro esquinas; resolvemos la homografía por eliminación gaussiana y remapeamos con muestreo bilineal. Verificado: las esquinas caen con error de 1e-13 px y las rectas siguen rectas.
- Un cuadrilátero cruzado o casi plano todavía da un sistema resoluble mientras colapsa la imagen, así que la convexidad se comprueba aparte. La primera versión de ese guardián dejaba pasar tres puntos colineales: un producto cruzado exactamente cero no es ni positivo ni negativo.
- Las estancias son polígonos: un salón en L, un pasillo. Arrastra el punto medio de un lado para crear un vértice. Mover una estancia arrastra los dispositivos que contiene.
- El plano acabado se descarga en PNG con fondo, áreas, dispositivos, leyenda y escala.
- Navegación reagrupada: un único menú Panel; Certificados e Instalación pasan a rutas propias.
- Endurecido para producción: un fondo rechazado devolvía 200 con
background: nully el usuario veía desaparecer su plano calcado. Ahora falla en voz alta con un código estable. Y el contenedor ya no se traga los errores de migración con|| true: servir tráfico contra un esquema que no cuadra es justo como un login acaba en 500.
2026-07-09 — Entrada 045: Se rompió el login en producción — migración ausente, guardián de CI y errores de auth mejores
Qué pasó
- El login (y el registro) devolvían 500 en producción. No era la contraseña:
/api/healthrespondía bien, pero un login con credenciales falsas daba 500 en vez de 401. Causa: la función BYOK añadió tres columnas cifradas aapp_user, pero la migración nunca se generó —make:migrationhabía fallado en silencio porque el PostgreSQL local estaba caído, y solo revisé la salida de los tests. Doctrine consultaba entonces columnas inexistentes en la base de datos de producción. - Por qué los tests no lo cazaron: el esquema de test se construye desde los metadatos de las entidades (SchemaTool sobre SQLite en memoria), nunca desde las migraciones. 59 tests en verde con la base de datos rota. Un punto ciego real.
Arreglado
- Confirmado el desajuste con
doctrine:schema:update --dump-sql, generada y aplicada la migración que faltaba (Version20260709094713, tres columnas nullable — segura sobre filas existentes), push; Render la aplicó al arrancar. Verificado contra producción: el login responde ya 401, no 500.
El arreglo de verdad — un guardián en CI
- Nuevo job «Migrations (schema in sync)»: levanta un PostgreSQL real, ejecuta todas las migraciones
desde cero y luego
doctrine:schema:validate. Si una entidad cambia sin su migración, el CI se pone en rojo antes de llegar a producción. Es justo la clase de fallo que los tests unitarios no pueden ver.
Además: errores de autenticación profesionales
- Los fallos llevan ahora un
codeestable que el frontend traduce:bad_credentials,email_taken,invalid_email,weak_password,too_many_attempts, más los casos de servidor/red. UnAuthenticationFailureHandlerpropio devuelve JSON limpio. El login sigue siendo genérico a propósito — el mismo 401 exista o no la cuenta, para que nadie pueda enumerar emails registrados; un test comprueba que ambas respuestas son idénticas. Contraseña mínima subida a 8 caracteres (solo en el registro, para no romper cuentas existentes). Suite: 60 tests, 273 aserciones.
Hecho
- Los usuarios pueden activar la IA y la banca abierta desde Cuenta → Integraciones pegando sus
propias claves — sin variables de entorno. Las claves van cifradas en reposo (
SecretCipher, AES-256-GCM desde el secreto de la app) y nunca se devuelven al cliente (el estado solo muestra configurada + pista enmascarada).CredentialStoreresuelve las claves por usuario con fallback a env; refactorizadosChatService,CategorizerService,GoCardlessClient(ahoraconfigure()por petición),OpenBankingServiceyBankControllerpara leer a través de él. Nuevos endpoints de cuenta + sección en la UI de Cuenta. Migración con tres columnas cifradas enUser. Suite: 58 tests, 250 aserciones (round-trip/manipulación del cifrado + test BYOK end-to-end donde guardar GoCardless activa/api/bank/statuspara ese usuario).
Por qué
- Las funciones dependían de variables de entorno que un usuario final no puede tocar. BYOK las hace autoservicio y honestas (la clave de cada uno), manteniendo la seguridad — secretos cifrados, nunca devueltos.
Siguiente
- La IA agéntica + OCR podrían construirse ya sobre la clave propia de Claude del usuario.
Hecho
- Arreglado el navbar que se salía/solapaba en el móvil. Cada elemento tiene ahora icono + etiqueta: en escritorio se ve la etiqueta (aspecto igual) y en móvil (≤820px) solo el icono. El email largo se reduce a un icono 👤 y salir a 🚪, así todo cabe en una fila compacta. Ajustados los paddings móviles y el tamaño de la marca de agua. Solo frontend/CSS.
Por qué
- En el móvil los cinco nombres + el email completo + tres botones se envolvían y se solapaban; los iconos lo mantienen legible y pequeño sin tocar el escritorio.
Hecho
FloorPlan3D(react-three-fiber): levanta la planta 2D a paredes semitransparentes, coloca los dispositivos a alturas realistas (enchufe 0.3 / interruptor 1.1 / luz 2.4 emisiva / cuadro 1.2 m) y el cuadro, con suelo + rejilla y OrbitControls (girar/zoom/mover). Lee el mismolayoutque el editor 2D. Va cargado en diferido tras un botón «Ver en 3D» — el build confirma que se separa en su propio chunk (FloorPlan3D-*.js≈ 900 kB), así que Three.js solo se descarga bajo demanda y el bundle principal no cambia. Añadidosthree+@react-three/fiber@9+@react-three/drei@10(compatibles con React 19). Sin cambios de backend.
Por qué
- Es la Fase 3 del diseñador (enfoque B): un modelo 2D útil con una capa de presentación 3D opcional. El flujo del electricista ya corre diseño → plano 2D → vista 3D → CIE → presupuesto → factura.
Siguiente
- IA agéntica + OCR (necesitan una key de Anthropic). El diseñador podría ganar 3D texturizado/primera persona, pero eso es otro producto.
Hecho
- Añadido
FloorPlanEditor: un lienzo SVG de planta (rejilla métrica) donde las estancias son rectángulos arrastrables y el cuadro + dispositivos (enchufe/interruptor/luz) se colocan con herramienta + clic y se arrastran con Mover (Borrar quita). Un botón Auto-colocar siembra el plano desde el diseño y coloca los puntos ITC-BT-25 de cada estancia como dispositivos.InstallationCalculator::layoutCable()mide el cable como el recorrido Manhattan de cada dispositivo al cuadro (+bajada, +10 % holgura), así que la UI muestra «Cable según plano: N m» en vez de la estimación cuando hay planta. La planta se persiste como columna JSON enInstallation(migración);/computeaceptalayout. Suite: 55 tests, 244 aserciones.
Por qué
- La Fase 1 daba una estimación por superficie; una planta real da metros reales y un croquis que el electricista puede entregar. El mismo modelo se extruirá a 3D en la Fase 3.
Siguiente
- Fase 3 (3D extruido paseable). La IA agéntica + OCR siguen necesitando una key.
Hecho
- Enganchado el diseñador de instalación con el resto del módulo de facturación (solo frontend). Desde un
diseño calculado, dos botones: «Crear certificado (CIE)» prellena el formulario de Certificados con
los datos técnicos (potencia→kW, tensión, suministro, tierra TT, circuitos, IGA 25/40 A, diferencial 30
mA); «Materiales → presupuesto» abre el formulario de Presupuestos con una línea por material.
Añadido el paso de datos entre pestañas en
BillingPage(una pestaña entrega datos a otra; al pulsar una pestaña manualmente se limpia). Sin placeholders: el usuario completa cliente/identidad donde toca.
Por qué
- Es el diferencial que venía señalando: diseño → CIE → presupuesto → factura en una sola herramienta, cada paso alimentando el siguiente. Los datos que el electricista introdujo una vez fluyen por toda la cadena.
Siguiente
- Fase 2 (plano 2D) / Fase 3 (3D) del diseñador. La IA agéntica + OCR siguen necesitando una key.
2026-07-08 — Entrada 039: Diseñador de instalación — Fase 1 (calculadora ITC-BT-25 + esquema unifilar)
Hecho
- Nueva pestaña Instalación: una calculadora del REBT ITC-BT-25.
InstallationCalculator(puro, con tests) convierte estancias + cargas previstas en circuitos (C1–C12 con sección/PIA, desdoblando en C6/C7 al superar los límites de puntos), puntos mínimos por estancia, grado y potencia a contratar, diferenciales, lista de materiales y estimación de cable.InstallationControllerexpone un/computesin estado más CRUD para guardar diseños (entidadInstallation, columnas JSON; el resultado se recalcula desde la entrada guardada). El frontend recalcula en vivo y dibuja un esquema unifilar (SVG) + tablas de circuitos/puntos/materiales. Migración añadida. Suite: 52 tests, 233 aserciones. - Basado en la ITC-BT-25 (5 circuitos básico, 9+ elevado; un diferencial por cada 5 circuitos; puntos mínimos por estancia). Enfoque honesto: ayuda de predimensionado; el cable es estimación; no es proyecto firmado.
Por qué
- Aquí empieza la cadena diseño → CIE → presupuesto → factura del electricista. Es la Fase 1 del diseñador (enfoque B); Fase 2 = editor 2D de planta, Fase 3 = vista 3D extruida.
Siguiente
- Fase 2 (plano 2D), autorrellenar el CIE desde un diseño, materiales → presupuesto. La IA agéntica + OCR siguen necesitando una key.
Hecho
- Llevado el CIE de "borrador" a listo para firmar. Investigada la tramitación telemática de la GVA: los documentos deben ir en PDF firmado digitalmente (DNIe / FNMT / ACCV / Cl@ve-firma) y los presenta el instalador habilitado en la sede — no hay API de terceros. Elegido el camino seguro (contigo): Cuentia nunca maneja el certificado del instalador.
- Mejorado
CiePdfa un documento CERTINS E completo: declaración de conformidad íntegra (RD 842/2002 + ITC-BT, mediciones favorables), lugar y fecha y áreas reservadas de firma electrónica para empresa e instalador ("firma electrónica AutoFirma/ACCV"). Añadido un panel in-app «Cómo firmarlo y presentarlo» (descargar → firmar con AutoFirma/ACCV → presentar en la sede de la GVA) con enlaces oficiales.
Por qué
- "Presentable de verdad" = documento fiel + firma real + presentación. La firma la hace el instalador en local (la clave privada nunca sale de su equipo, como recomienda la GVA); la presentación no tiene API pública, así que sigue siendo su subida manual — el mismo límite honesto que el envío real a la AEAT de Verifactu.
Siguiente
- IA agéntica + OCR — ambas necesitan una API key de Anthropic.
Hecho
- Investigado el CIE de la Comunitat Valenciana y confirmado un modelo oficial — CERTINS E (12/2012),
que se presenta telemáticamente con firma digital en la sede de la GVA. Implementada una entidad
Certificate + CRUD
CertificateController+CiePdf(PDF con estructura CERTINS E vía Dompdf) + una pestaña Certificados en Facturación. Los campos siguen el REBT (RD 842/2002): instalación, titular, empresa instaladora/instalador y características técnicas (potencia, tensión, suministro, tierra, IGA, diferencial, etc.). Migración añadida. - Enfoque honesto (nota del formulario + pie del PDF): es un borrador de ayuda, no la presentación
oficial telemática y firmada. Tests: CRUD, validación, PDF, aislamiento por usuario. Suite: 45 tests,
192 aserciones. Guía 32. (Corregidos dos bugs que cazaron los tests: lectura de clave indefinida en un
installationTypesin definir, y usar null como índice de array en el PDF.)
Por qué
- Un plus real para un electricista que use la app: el CIE es el documento que emite en cada trabajo, y reutilizar los datos que ya guarda para facturar elimina reescribir.
Siguiente
- IA agéntica + OCR — ambas necesitan una API key de Anthropic.
Hecho
- Añadidos los presupuestos: entidades
Quote/QuoteLine(no fiscales, sin cadena de hash),QuoteService,QuoteController(list/create/get/status/convert/pdf),QuotePdf, migración, y una pestaña Presupuestos (crear con cliente + líneas del catálogo + válido hasta, insignia de estado, acciones por fila). Lo principal: convertir copia el presupuesto aInvoiceService::create()para producir una factura Verifactu completamente sellada — idempotente (se convierte una vez, sin duplicar), y marca el presupuesto comoconvertedenlazándolo. - Tests:
QuoteServiceTest(totales/numeración/serie por defecto/borrador/guarda de líneas vacías) + un test de integración de crear → estado → convertir → factura sellada → idempotente → PDF. Suite: 44 tests, 179 aserciones. Guía 31. Corregido un bug de creación donde unstatussin definir releía una clave inexistente y dejaba el estado en blanco.
Por qué
- Los autónomos presupuestan antes de facturar; convertir el presupuesto aceptado en un clic (en una factura conforme) es el flujo natural y mantiene la cadena fiscal separada de las ofertas no fiscales.
Siguiente
- IA agéntica + OCR — ambas necesitan una API key de Anthropic.
Hecho
- Añadido un catálogo de servicios/productos reutilizable: entidad
Service(nombre/precio/IVA acotado al usuario), CRUDServiceController(/api/services), migraciónVersion20260708075806, y una pestaña Servicios. El formulario de factura gana un desplegable «Añadir del catálogo…» que añade una línea prellenada del servicio elegido (editable). Suite: 40 tests, 157 aserciones (CRUD + validación + aislamiento).
Por qué
- Los autónomos facturan los mismos servicios una y otra vez; un catálogo elimina el reescribir. Las líneas copian los valores del servicio al crearse, así que borrar un elemento nunca reescribe facturas pasadas.
Siguiente
- Presupuestos — documentos no fiscales que se convierten en factura Verifactu real.
Hecho
- Convertida la pantalla de Facturas en un apartado de Facturación con sub-pestañas (
BillingPage→Facturas,Clientes; lleganPresupuestos/Servicios). Añadido CRUD de clientes:CustomerController(GET/POST/PUT/DELETE /api/customers, acotado al usuario) con guarda de borrado (409 si el cliente tiene facturas) yname/taxIdobligatorios. Nueva pestañaClientes(listar + crear/editar/borrar). Emitir una factura puede reutilizar un cliente existente porcustomerId(desplegable en el formulario);InvoiceServiceresuelve id → busca-o-crea. - Tests: un test de integración que cubre crear/listar/actualizar, validación (400), la guarda de borrado (409) y el aislamiento por usuario. Suite: 39 tests, 146 aserciones. Guía 29.
Por qué
- Es el paso de "emitir una factura suelta" hacia una herramienta de facturación real: clientes que reutilizas, un apartado que albergará presupuestos y un catálogo de servicios a continuación.
Siguiente
- Un catálogo de servicios para rellenar líneas, y luego presupuestos con convertir-en-factura.
Hecho
- Cada factura se puede descargar ahora como PDF profesional (servicio
InvoicePdfcon Dompdf, PHP puro): emisor/cliente, tabla de líneas, totales, la huella Verifactu y el QR incrustado comodata:URI en SVG.GET /api/invoices/{id}/pdf; la página de facturas muestra un enlace Descargar PDF junto al de XML. Guía 28. Suite: 38 tests, 134 aserciones (render unitario + endpoint de integración). - Además, README actualizado para mostrar la facturación Verifactu y la banca abierta, con un recuadro de ingeniería sobre la cadena de hash y una sección de alcance reescrita con honestidad.
Por qué
- El QR/XML eran artefactos de desarrollador; un PDF es el documento que un autónomo envía de verdad. Dompdf lo mantiene libre de gd/imagick, y DejaVu Sans renderiza el € y los acentos.
Siguiente
- IA agéntica + OCR — ambas necesitan una API key de Anthropic.
Hecho
- Construida una importación real de banca abierta vía GoCardless Bank Account Data (PSD2):
GoCardlessClient(token → instituciones → requisition → transacciones de cuenta),OpenBankingService(conectar + importar con mapeo) yBankController(/api/bank/status|institutions|connect|import). Los movimientos llevan ahora unexternalId(migraciónVersion20260706103931) para que las reimportaciones salten duplicados. Componente frontendBankConnecten la página de Movimientos: elige banco → autoriza (enlace alojado de GoCardless) → importa. - Flag de función: deshabilitada salvo que estén
GOCARDLESS_SECRET_ID/_KEY; entonces la UI muestra un aviso honesto de "no configurada" y los endpoints que la requieren devuelven 503. - Tests:
GoCardlessClientTest(MockHttpClient),OpenBankingServiceTest(mapeo, fallback al nombre del acreedor, conteos de dedup), test de integración de la ruta deshabilitada. Suite: 36 tests, 128 aserciones.
Por qué
- Crear credenciales de aplicación de GoCardless pide más datos personales de los que tienen sentido para un portfolio, así que la integración está construida y testeada contra la forma documentada de la API pero no ejecutada en vivo — publicarla tras un flag es la forma honesta de mostrar la capacidad sin fingir que se ha probado de extremo a extremo.
Siguiente
- IA agéntica + OCR — ambas necesitan una API key de Anthropic.
Hecho
- Cada factura tiene ya sus dos artefactos Verifactu.
VerifactuQrconstruye la URLValidarQRde la AEAT (nif, numserie, fecha, importe) y la renderiza como SVG conendroid/qr-code(el SVG no necesita gd/imagick);VerifactuXmlserializa un XMLRegistroAltaconDOMDocument. EndpointsGET /api/invoices/{id}/qr(image/svg+xml) y/xml(descarga). La página de facturas muestra el QR escaneable + un enlace de descarga del XML en el detalle desplegado, con una nota de que es una demostración fiel (host de pruebas), no un envío real. - Tests:
VerifactuDocumentsTest(campos de la URL del QR, render SVG, XML bien formado + con la huella, primer registro vs. encadenado) + cobertura de integración de ambos endpoints. Suite: 28 tests, 106 aserciones.
Por qué
- SVG en vez de PNG mantiene la imagen Docker y el CI libres de la extensión
gd/imagick. El QR y el XML siguen el formato de la AEAT fielmente pero apuntan al host de pruebas — Cuentia no es un emisor registrado, así que el envío SOAP real queda en la Fase D (fuera de alcance), como dice el ADR.
Siguiente
- Banca abierta (GoCardless Bank Account Data) — importación real de movimientos bancarios.
Hecho
- Nueva página Facturas (React): emitir una factura (cliente + líneas dinámicas con previsualización
del total en cliente), ver la lista, y desplegar cualquier fila para revelar su huella Verifactu
(hash, el registro con el que encadena, sello temporal). Un botón «Verificar cadena» llama a
/api/invoices/verifyy muestra una insignia verde «🔒 Cadena íntegra · N registros verificados» (o una ámbar «rota en …»). Añadidos la ruta, el enlace del navbar y los textos ES/EN. Build del frontend en verde (604 módulos).
Por qué
- El motor Verifactu (fases A–B) vivía solo en la API y era invisible para quien abriera la app en vivo. Esta página convierte la cadena inalterable en algo que un visitante puede ver y operar — el sentido de la funcionalidad para un portfolio.
Siguiente
- Fase C: el QR de la factura (a la AEAT) y la exportación XML, integrados en esta misma página.
Hecho
- Cada factura emitida genera ahora un
InvoiceRecord(registro de alta Verifactu) con una huella SHA-256 encadenada al registro anterior — el mecanismo antifraude central. AñadidosVerifactuHasher(cadena canónica + hash),VerifactuChain(verificador de integridad), la entidad + repositorio del registro, la generación enganchada enInvoiceService, y un endpointGET /api/invoices/verify; el detalle de factura devuelve ahora su bloqueverifactu(hash, previousHash, generatedAt). Añadido untaxId(NIF) alUsercomo emisor de la huella. MigraciónVersion20260706083619. - Tests:
VerifactuChainTest(determinismo, encadenado, detección de manipulación — tanto un campo alterado como un registro reesellado pero desenganchado) + un test de integración de extremo a extremo (dos facturas →/verifyok, count 2, encadenado, aislamiento por usuario). Suite: 24 tests, 86 aserciones.
Por qué
- Un test de campo alterado falló solo en SQLite: un
NUMERIC1210.00se releía como1210, así que la huella recalculada tras el viaje a la BD no coincidía con la sellada (PostgreSQL devuelve1210.00y lo ocultaba en la prueba en vivo). Lección aprendida: un hash criptográfico nunca debe depender de cómo formatee un valor la base de datos — los importes se normalizan a dos decimales en la cadena canónica.
Siguiente
- Fase C: el QR de la factura (a la AEAT) y la exportación XML.
Hecho
- Añadido el dominio de facturación: entidades
Customer,InvoiceeInvoiceLine(todas ligadas a unUser), sus repositorios, unInvoiceServicey unInvoiceController(GET/POST /api/invoices,GET /api/invoices/{id}). La migraciónVersion20260706075837crea las tres tablas. - Los totales se calculan en céntimos enteros (nunca floats) y los números de factura son
correlativos por serie (
nextNumber = MAX(number)+1) — ambos requisitos previos a la cadena de hash Verifactu. - Escrito el ADR 0003 (alcance A→D + nota honesta de
monetización) y la guía 24. Añadido
InvoiceServiceTest(totales/IVA/numeración + guardia de líneas vacías): la suite ya tiene 18 tests, 59 aserciones. Prueba en vivo: una línea al 21 % + dos al 10 % → base 1100.00, IVA 220.00, total 1320.00, número 2026/1.
Por qué
- Verifactu (Orden HAC/1177/2024) será obligatorio en 2026; adelantarse lo convierte en un escaparate creíble. El modelo de dominio tiene que estar bien — totales exactos al céntimo y numeración sin huecos — antes de poner encima la cadena de hash inalterable.
Siguiente
- Fase B: el
InvoiceRecordcon el hash SHA-256 encadenado y tests de detección de manipulación.
Hecho
- Cambiado el objetivo de despliegue a planes gratuitos: Render (servicio web Docker) para el backend y
Neon (PostgreSQL que no se pausa) para la base de datos; Vercel igual. Añadido
render.yaml(blueprint). - Corregido un fallo del Dockerfile: el
$PORTde Apache se sustituía en build (quedaría vacío); ahora se fija al arrancar el contenedor (${PORT:-8080}), así funciona en Render/Koyeb/Fly/Railway. - Actualizada la guía de despliegue (Render + Neon, nota de arranque en frío, alternativa Koyeb).
Por qué
- Caducó el trial de Railway y pagar no es opción; como el Dockerfile es agnóstico del host, solo cambian las instrucciones de hosting, no la app.
Siguiente
- Desplegar en Render + Neon, y luego añadir la URL + capturas al README y fijar el repo.
Hecho
- Añadido
backend/Dockerfile(Apache + PHP 8.4, ejecuta migraciones al arrancar),public/.htaccess(apache-pack), una configwhen@prod(proxies de confianza + cookies seguras) yfrontend/vercel.json(rutas SPA + proxy de/apial backend — sin CORS y cookies de sesión de primera parte). - Escrita una guía de despliegue completa (guía 23): PostgreSQL (Supabase/Railway),
backend en Railway con variables,
app:create-userpara el admin, frontend en Vercel, verificación y resolución de problemas. - Tests siguen en verde (16), la app en dev sin cambios.
Por qué
- El proxy frontend→backend evita el clásico dolor de cookies cross-domain/CORS y mantiene todo simple. La config está lista y documentada; el despliegue real se hace desde las cuentas del usuario.
Siguiente
- Publicar (crear cuentas, desplegar), y luego añadir la URL + capturas al README y fijar el repo.
Hecho
- Añadido
tests/Api/ApiIntegrationTest(WebTestCase): arranca el kernel/firewall/BD reales y prueba register/login/me/logout, 401 sin auth, email duplicado 409, aislamiento por usuario (A importa 2, B ve 0), y limpiar/borrar cuenta. - Corre sobre SQLite en memoria (
.env.test) condisableReboot()— sin servicio de BD; CI instalapdo_sqlite. - Cazó dos errores reales:
transactiones palabra reservada en SQLite (resuelto entrecomillando el nombre de la tabla) y el bloqueo de fichero SQLite en Windows (resuelto con la BD en memoria).php bin/phpunit→ OK (16 tests, 52 assertions). Documentado en la guía 22.
Por qué
- Los tests de integración demuestran que toda la pila está bien cableada — la prueba más fuerte, para un entrevistador, de que la app funciona de verdad.
Siguiente
- El despliegue en vivo.
Hecho
- Rehecho
index.csscomo un sistema "liquid glass" (inspirado en iOS/WhatsApp): tarjetas translúcidas esmeriladas y una navbar flotante redondeada (backdrop-filter), botones/inputs de píldora, fondo con tinte, y burbujas de chat estilo WhatsApp (burbuja de usuario en color, radios asimétricos). Tokens claro- oscuro.
- Responsive: la navbar se envuelve, los gráficos se apilan, las tablas hacen scroll horizontal
(
.table-scroll) en móvil. - Todo vía tokens/clases — componentes sin cambios. Compila. Documentado en la guía 21.
Por qué
- Un acabado moderno y coherente en claro/oscuro y escritorio/móvil es lo que hace que el proyecto se sienta profesional de un vistazo.
Siguiente
- Tests de integración de la API, y luego deploy.
Hecho
- Añadido
AccountController:POST /api/account/clear(borra los movimientos del usuario) yDELETE /api/account(borra cuenta + datos, invalida la sesión — derecho al olvido). - Añadida una página de Cuenta (
/account, vía el email en la navbar) con limpiar datos, borrar cuenta (peligro) y una nota de privacidad;AuthContext.deleteAccount(). ES/EN. - Verificado: limpiar 15→0; borrar cuenta → sesión invalidada. Documentado en la guía 20.
Por qué
- Las cuentas reales necesitan una salida (RGPD), y "limpiar mis datos" es un reinicio cómodo para la demo en vivo — ambos baratos gracias al aislamiento de datos por usuario.
Siguiente
- Pulido UX y responsive, tests de integración de la API, y luego deploy.
Hecho
- Añadido
POST /api/demo/load: carga 2 meses de movimientos de ejemplo realistas para el usuario actual (solo si está vacío) reutilizando el pipeline real de import + categorización, para que una cuenta nueva nunca esté vacía. - Añadido un botón "Cargar datos de ejemplo" en el estado vacío de Movimientos (ES/EN).
- Añadido el comando de consola
app:create-user(con--admin) para crear cuentas/admins en un deploy. - Verificado: creado un admin; un usuario nuevo carga 15 movimientos (2 meses, 8 categorías, IVA calculado). Documentado en la guía 19.
Por qué
- Para publicar: lo que más mata una demo es una pantalla vacía. Los datos de ejemplo (por el pipeline real) hacen la app explorable al instante; el CLI siembra cuentas de servidor con seguridad.
Siguiente
- Cuenta y GDPR (borrar/limpiar), pulido UX y responsive, tests de integración de la API, y luego deploy.
Hecho
- Añadida entidad
User+ security-bundle de Symfony: proveedor de entidad,json_logincon sesión, logout, control de acceso. Endpoints:POST /api/register,/api/login,/api/logout,GET /api/me. - Todo acotado al usuario actual:
Transactionpertenece a unUser; las importaciones lo asignan;TransactionRepository::findForUser(); cada servicio y el SQL de stats filtran por usuario (vía#[CurrentUser]). - Frontend:
AuthContext+AuthPagede login/registro;Appprotege toda la app; la navbar muestra el email + salir. - Actualizados todos los tests unitarios a las nuevas firmas.
php bin/phpunit→ OK (11 tests, 37 assertions). Verificado end-to-end con cookies de sesión (registro → login → datos acotados → 401 sin sesión). Documentado en la guía 18.
Por qué
- El aislamiento de datos multiusuario es lo que convierte esto de una demo en un SaaS real — e imponerlo en la capa de consulta es la forma correcta y segura.
Siguiente
- Endurecimiento de producción y el deploy en vivo.
Hecho
- Añadido
ChatService: construye un contexto factual (balance, ingresos/gastos, categorías top, IVA, próximo pago de IRPF) y le pide a Claude que responda usando solo eso; fallback determinista de resumen sin clave. - Añadido
POST /api/chaty una página Asistente (/chat) con UI de chat; añadido el enlace en la navbar y traducciones ES/EN. - Añadido
ChatServiceTest(ruta de fallback).php bin/phpunit→ OK (11 tests, 37 assertions). Documentado en la guía 17.
Por qué
- El Q&A en lenguaje natural anclado en los datos del propio usuario es la función IA estrella — y el fallback la mantiene honesta y siempre utilizable.
Siguiente
- Fase 4: autenticación/multiusuario y luego el deploy en vivo.
Hecho
- Añadido
ForecastService(proyección lineal del saldo a +30/60/90 días según el neto diario medio) yGET /api/forecast. - Añadido un gráfico de líneas
Forecasten el Dashboard (saldo actual + neto mensual medio + nota de "estimación"). Conectado/api/forecastauseFinanceData. - Añadido
ForecastServiceTest.php bin/phpunit→ OK (10 tests, 33 assertions). Documentado en la guía 16.
Por qué
- Una cifra a futuro es lo que de verdad preocupa a un autónomo ("¿llegaré?"), y un modelo transparente y claramente etiquetado lo mantiene honesto.
Siguiente
- Preguntas en lenguaje natural sobre los movimientos (IA), y luego auth + deploy.
Hecho
- Añadido
ImportService::importNorma43()— parsea el formato de ancho fijo Cuaderno 43 de la AEB (registro 22 = movimiento, 23 = concepto), y un despachadorimport()que auto-detecta CSV vs Norma 43 por el contenido. - El mismo endpoint/botón de subida maneja ambos; el input acepta
.csvy.n43. - Añadido
ImportServiceTest(CSV + Norma 43, con registros de 80 caracteres).php bin/phpunit→ OK (8 tests, 25 assertions). Verificado end-to-end con un.n43de ejemplo (4 movimientos). Documentado en la guía 15. La Fase 2 está completa.
Por qué
- Norma 43 es lo que exporta cualquier banco español; parsearlo (y nóminas/IVA correctamente) es la profundidad de dominio que diferencia el proyecto.
Siguiente
- Fase 3 (previsión de tesorería, chat en lenguaje natural, open banking) o preparar el deploy en vivo.
Hecho
- Añadido
.github/workflows/ci.ymlcon dos jobs en paralelo: backend (PHP 8.4 →composer install→php bin/phpunit) y frontend (Node 24 →npm ci→npm run build). - Añadida una insignia de estado de CI (+ badges de stack) al README.
Por qué
- Ejecutar los tests y el build en cada push evita que se fusionen regresiones, y la insignia verde da credibilidad instantánea a quien navegue el repo.
Siguiente
- Un despliegue en vivo con enlace y capturas en el README.
Hecho
- Instalado PHPUnit (
symfony/test-pack). - Añadidos
VatServiceTesteIrpfServiceTest: tests unitarios puros que construyen objetos Transaction/Category en memoria y los pasan por un stub del repositorio (sin BD). Comprueban el IVA (336/23/313), el pago del T1 de IRPF 230,64, la nómina excluida y la cuenta atrás del vencimiento. php bin/phpunit→ OK (6 tests, 14 assertions), sin avisos (usécreateStub, como recomienda PHPUnit 13). Documentado en la guía 13.
Por qué
- La lógica fiscal es donde más cuesta un error; testearla demuestra corrección, protege de regresiones y es un gran argumento en una entrevista.
Siguiente
- CI (GitHub Actions ejecutando los tests) + un deploy en vivo, o el importador Norma 43.
Hecho
- Añadido un i18n sin dependencias: diccionario de traducciones (en/es), un
LanguageContextconuseTranslation()({ lang, setLang, t }), y un botón EN/ES en la navbar. La elección persiste en localStorage. - Sustituidos los textos fijos de la interfaz (navbar, páginas, gráficos, paneles fiscales) por
t('clave');t()admite marcadores{param}(p.ej. el aviso de vencimiento del IRPF, prefijo T/Q). - Idioma por defecto español; cambia al instante. Documentado en la guía 12.
Por qué
- Una interfaz bilingüe demuestra pensamiento de producto internacional y muestra el perfil ES/EN del autor — encaja con puestos remotos/UE.
Siguiente
- Importador Norma 43 (cerrar la Fase 2) y luego temas de producción (tests, auth, deploy).
Hecho
- Sustituido el CSS de plantilla (acento morado) por un sistema de tokens de diseño en
index.css: superficies, tinta, un azul de marca, colores semánticos del dinero, radios, sombras — más un tema oscuro completo. - Refactorizados todos los componentes/páginas a clases que leen los tokens (
.card,.btn,.table,.tag,.navbar,.stat-value,.alert-warn…). Los gráficos leen--chart-*en tiempo de ejecución para seguir el tema. - Estética sobria y financiera (bordes finos, sombras sutiles, cifras tabulares) — deliberadamente lejos del look de plantilla de IA. Documentado en la guía 11.
Por qué
- Con toda la funcionalidad ya lista, un aspecto limpio y profesional es lo que convierte "funcional" en "impresionante" para un reclutador.
Siguiente
- Internacionalización: un botón de idioma ES/EN.
Hecho
- Añadido React Router: un
Layout(navbar +<Outlet/>) que carga los datos una vez conuseFinanceData()y los comparte con las páginas por el contexto del Outlet. - Dividida la página única en Movements (
/), Dashboard (/dashboard) y Taxes (/taxes). - Extraídos helpers compartidos (
lib/format,components/Stat). Verificado que todas las rutas devuelven 200 (fallback SPA) y que el proxy de API sigue funcionando. Documentado en la guía 10.
Por qué
- Una estructura real de SPA multipágina es más limpia, escalable y lo que un reclutador espera — y prepara el terreno para el rediseño visual.
Siguiente
- Rediseño visual: un aspecto limpio y profesional que no parezca generado por IA.
Hecho
- Añadido
IrpfService(reutilizaVatService::baseCents()): 20% acumulado del neto del año hasta la fecha (ingresos de actividad − gastos deducibles, sin IVA), excluyendo la nómina, por trimestre con los vencimientos oficiales y una cuenta atrás del "próximo vencimiento". - Añadido
GET /api/irpfy un panel de IRPF (tabla de trimestres + banner de aviso cuando falta < 30 días). - Verificado: neto T1 1153,21 → pago 230,64; próximo vencimiento T2 2026-07-20 (18 días). Documentado en la guía 09.
Por qué
- IRPF + IVA juntos cuentan una historia fiscal completa — el valor central para un autónomo español y la demostración más clara del foso financiero.
Siguiente
- Importación Norma 43 y luego el rediseño del frontend.
Hecho
- Añadido
VatService(mapa categoría → tipo de IVA español, cálculo de IVA en céntimos) yGET /api/vat. - Añadido un panel de IVA en React: IVA repercutido, soportado y neto (a pagar / a compensar).
- Verificado con los datos de ejemplo: repercutido 336,00, soportado 23,00, neto 313,00 a pagar — la nómina y la cuota de autónomo correctamente sin IVA. Documentado en la guía 08.
Por qué
- Este es el foso financiero: calcular el IVA bien (tipos correctos por categoría, nóminas exentas, céntimos exactos) es justo el conocimiento del dominio que diferencia este proyecto.
Siguiente
- Estimación de IRPF (modelo 130) y avisos de trimestre.
Hecho
- Añadido
GET /api/stats(agregación en SQL: totales, gasto por categoría, ingresos/gastos por mes). - Añadido un componente
Dashboarden React (Recharts): barra horizontal de un tono para el gasto por categoría y barras de dos series para ingresos vs gastos por mes. Colores elegidos con el método de data-viz y validados con el script de la skill (CVD ΔE 74.6). Los gráficos se refrescan al importar/categorizar. - Documentado en la guía 07.
Por qué
- Es la pantalla más "vendible": de un vistazo ves a dónde va el dinero — la recompensa del trabajo de importación + categorización.
Siguiente
- Fase 2: el panel de IVA (repercutido vs soportado, neto a pagar) — el foso financiero.
Hecho
- Añadido
CategorizerService: lista fija de categorías, motor de reglas por palabras clave en español, y una llamada opcional a Claude (vía HttpClient) validada contra la lista permitida. - Añadido
POST /api/transactions/categorizey un botón "🧠 Categorize" + columna Categoría en React. - Verificado sin API key: 8/8 categorizadas correctamente por reglas (Mercadona→Supermercado, Repsol→Combustible, Nómina→Nómina, etc.). Documentado en la guía 06.
Por qué
- La categorización convierte una lista en información útil. Mantener la IA opcional (fallback por reglas) hace que la app siempre funcione y sea honesta — un principio de arquitectura deliberado.
Siguiente
- Panel: gasto por categoría y por mes (Recharts), y luego el panel de IVA (Fase 2).
Hecho
- Añadido
GET /api/transactions(mapea entidades a un array DTO plano, ordenado por fecha). - Reescrito el
App.jsxde React: tabla de movimientos (formato euro, rojo/verde según signo), un resumen de ingresos/gastos/balance, y un input de fichero que sube un CSV (FormData) y refresca. - Verificado: el endpoint devuelve 8 filas a través del proxy de Vite y el frontend compila sin errores. Documentado en la guía 05.
Por qué
- Primer bucle totalmente visible: subes un CSV → ves tus movimientos. Convierte el trabajo del backend en algo que una persona (o un reclutador) puede ver de verdad.
Siguiente
- Categorización con IA: asignar una categoría a cada movimiento con Claude + fallback por reglas.
Hecho
- Añadido
ImportService(parsea un CSV bancario) yPOST /api/import/csv(controlador fino). - El parser autodetecta el separador
,/;, mapea columnas bilingües y maneja el formato numérico español (1.234,56) — manteniendo el dinero como string decimal exacto. - Añadido un fichero de ejemplo y verificado de punta a punta: importadas 8 filas (balance 3316.21), 0 errores, acentos preservados. Documentado en la guía 04.
Por qué
- Es el primer valor tangible: un extracto real del banco se convierte en datos estructurados. Mantener correcto el parseo financiero (decimales españoles, sin float) es el diferenciador.
Siguiente
- Exponer los movimientos vía un endpoint de API y mostrarlos en el frontend React.
Hecho
- Instalado Doctrine (
symfony/orm-pack) y MakerBundle. - Arrancado PostgreSQL local (
pg_ctl), configuradoDATABASE_URLen.env.local, creada la base de datoscuentia. - Escritas las entidades
CategoryyTransaction(+ repositorios); generada y ejecutada la primera migración. Ya existen las tablascategoryytransaction. - Documentado en la guía 03. Decidido usar IDs enteros autoincrementales por ahora (más simple que UUID); actualizada la ARQUITECTURA en consecuencia.
Por qué
- Las entidades son la columna vertebral de la app. Hacer bien el dinero desde el primer día
(
decimal, no float) evita toda una familia de errores de contabilidad.
Siguiente
- Importación CSV: un endpoint que parsea un CSV bancario y guarda filas
Transaction(guía 04).
Hecho
- Generada la app React (Vite) en
frontend/; añadido un proxy de desarrollo (/api→:8000) para evitar CORS. App.jsxllama aGET /api/healthal montarse y muestra "✅ API OK".- Verificado de punta a punta: con ambos servidores en marcha,
http://localhost:5173/api/healthse reenvía por proxy al backend y devuelve el JSON. La Fase 0 está completa. - Documentado en la guía 02, incluyendo el detalle real de
localhostvs127.0.0.1(IPv6) que nos pasó.
Por qué
- Demostrar ya el enlace frontend↔backend, con la función más simple posible, hace que cada función posterior se construya sobre una base que sabemos que funciona.
Siguiente
- Fase 1: modelo de dominio (
Transaction,Category), PostgreSQL vía Doctrine e importación CSV (guía 03).
Hecho
- Generada la API Symfony 8.1 en
backend/(symfony new backend); eliminado su.gitanidado para mantener un único monorepo. - Añadido
HealthControllerconGET /api/health; verificado que devuelve{"status":"ok","service":"cuentia-api"}con un servidor local. - Documentado todo en la
guide/didáctica (00-entorno, 01-scaffold del backend).
Por qué
- Un endpoint de salud es la prueba más pequeña posible de que la cadena petición→controlador→respuesta funciona — el primer hito correcto antes de añadir base de datos o funciones.
Siguiente
- Generar el frontend React (Vite) y que llame a
/api/health(guía 02).
Hecho
- Creado y pusheado el repo público: github.com/R0b3r7DEV/cuentia (primer commit = docs).
- Instalado el entorno del backend con Scoop: PHP 8.5.7, Composer 2.10.2,
Symfony CLI 5.17.1, PostgreSQL 18.4 (clúster inicializado; auth
trusten local). - Creado un
php.inique habilita las extensiones que Symfony necesita:pdo_pgsql,pgsql,intl,mbstring,openssl,curl,fileinfo,sodium,zip.
Por qué
- Construir en público desde el commit 1 cuenta una buena historia; un entorno local funcionando desbloquea la Fase 0.
Siguiente
- Generar la API Symfony y la app React; primer "verde"
GET /api/health→ 200.
Hecho: toda la documentación es ahora bilingüe (inglés primero, español debajo). Por qué: el autor usa los docs en español mientras aprende, y el reclutador lee el inglés. Siguiente: lo mismo que la entrada 001 — instalar herramientas y generar el scaffold.
Hecho
- Elegido proyecto y stack (ver ADR 0002): Cuentia, un copiloto financiero con IA, sobre Symfony + React + PostgreSQL + Claude.
- Creado el esqueleto de documentación: README, ROADMAP, ARCHITECTURE, ADR 0001, ADR 0002 y este diario.
- Definido el modelo de dominio inicial (
Transaction,Category) y el roadmap por fases.
Por qué
- Empezar con el porqué escrito (ADRs) y un plan por fases mantiene el foco y hace que cada decisión sea defendible más adelante — que es justo el objetivo de este proyecto.
Siguiente
- Instalar el tooling del backend: PHP, Composer, Symfony CLI, PostgreSQL (Fase 0).
- Generar la API Symfony y la app React; conseguir un
GET /api/healthque devuelva 200.