Functional design · October 2026
Contabilidad DEMO
This functional design was created in collaboration with Marie Obelleiro and Claude in a human/AI-driven way.

1. Purpose & scope

The product

An accounting app for freelancers (autónomos) in Spain: invoices, expenses, quarterly VAT (IVA, Modelo 303 / 390) and income-tax instalments (IRPF, Modelo 130). It is published as an online example at marieobelleiro.com/demo/app_DEMO.html: a single HTML file loaded with sample data, where visitors can add invoices and expenses and see how every figure reacts. Nothing is stored — reloading the page brings back the example data.

Two assistants sit on top of the app: Acceso fácil, a button-driven window built for keyboard and screen-reader users, and the Asistente Inteligente, a conversational assistant whose understanding is provided by Jev (TypeSafe).

A sandbox, not a ledger

The online example is a demo sandbox: it opens with sample data for 2025 and 2026, and every change a visitor makes — new invoices or expenses, deletions, paid/pending, 303 checkboxes, payments, notes, settings, conversations with the Asistente Inteligente — lives only in the browser's memory. Nothing is stored, and no accounting data leaves the page (only the text of a question typed to the Asistente Inteligente is sent, for Jev to understand it). Reloading the page restores the sample data, which is also the way to start again. The welcome dialog and the DEMO tooltip present it this way, and both assistants remind the user after saving.

Audience

Whoever builds the API — you, a hired freelancer, or a future team — plus anyone reviewing or approving the scope beforehand. No prior knowledge of this codebase is assumed.

It also serves as durable context for an AI coding agent asked to implement or modify the API later: since the app is a single HTML file with no separate spec, an agent working from the code alone would have to infer intent from function names and DOM logic. This document states that intent explicitly, so a change made months from now — by a person or an agent — stays consistent with the design instead of drifting from it.

What it's for

A build-ready specification. It replaces the need to reverse-engineer requirements from the HTML/JS, so an implementer knows exactly which actions to build, what each one takes in and returns, and what's explicitly excluded — without having to ask.

In scope

  • Every capability reachable through the UI — add/update/delete invoice, add/update/delete expense, mark an invoice as paid, record tax payments, notes and settings, manage years — expressed as typed, callable operations.
  • The read/query surface: dashboard totals, quarterly and annual IVA/IRPF figures, invoice and expense lists.
  • Identity and permission rules for those operations.
  • Structured success/failure reporting back to the caller.
  • The two assistants (Sections 7 and 8). They are described because they are the first internal callers of the read and action layers, and their needs shaped several requirements below.
  • The behaviour of the interface on desktop and mobile, as far as it defines rules the API must respect (dates, quarters, read-only views, warnings).

Out of scope

  • Visual design: styles, components and design tokens.
  • Multi-user or multi-account support — the product is single-user, and this design reflects that as-is.

Why this matters

The application keeps all data in a JSON object (__appData) inside the page, with no server and no callable actions — everything happens through direct DOM manipulation in the browser, and the data lives only in memory. Defining a proper action and read API is the foundation for reliable, testable and maintainable operation of the product, whatever triggers a given action: a screen, an assistant, a script or a test.

Terminology — Spanish ↔ English

This document uses English terms for readability. Below is the mapping to the exact names shown in the app. The app itself keeps the official Spanish names on purpose: they are the names on the Hacienda forms freelancers have to file.

App label (Spanish)This document (English)
FacturaciónInvoices screen
GastosExpenses screen
IVAVAT screen
IRPFIncome tax screen
Resumen AnualAnnual view
1T·Ene–Mar / 2T·Abr–Jun / 3T·Jul–Sep / 4T·Oct–DicQ1 / Q2 / Q3 / Q4
Factura / GastoInvoice / Expense
Factura nacional / Factura exenta de impuestosDomestic / Tax-exempt invoice
Cobrada / PendientePaid / Pending
Acceso fácilAccessibility assistant
Asistente InteligenteConversational assistant

2. API layers overview

The API is organized into four functional layers.

LayerRoleApplied to this product
Action APIExposes each user capability as a discrete, callable operation with typed parameterscreate_invoice, update_invoice, delete_invoice, set_invoice_paid, create_expense, update_expense, delete_expense, set_expense_in_303, set_iva_paid, set_irpf_paid, add_year, delete_year
Search / Retrieval APIQueries app state in a form a caller can consume directlyDashboard totals, invoice/expense lists filtered by year or quarter, IVA/IRPF quarterly and annual figures
Identity & Permissions APIConfirms who is calling and what they're allowed to doTies every action to the ledger the caller is allowed to use; blocks actions the caller isn't allowed to run
Notification / State-Change APIReports events back to the callerConfirms an action succeeded, carries warnings (e.g. date outside the quarter) and errors — replacing the demo's in-page messages and the remaining native prompt()

Design rules that apply across all four layers: actions must be atomic and composable, permission checks must live server-side (not only in UI logic), every action returns a structured success/failure result, and every state-changing action is logged consistently regardless of what triggered it.

3. Functional requirements per layer

Action API

  • Every capability triggered in the demo by addFact(), deleteFact(), updateFact(), toggleCobrada(), addGasto(), deleteGasto(), updateGasto(), the mobile forms (mSaveInv(), mSaveGasto(), mDelete()) and the assistants must have a corresponding action with a name, a typed parameter list and a defined return shape.
  • Actions validate input server-side rather than relying on the form to catch it, and apply the same rules on every caller: date normalisation, quarter assignment and the Seguridad Social rule (Section 7).
  • State changes are expressed as set, never toggle: set_invoice_paid(invoice_id, paid) replaces toggleCobrada() and set_expense_in_303(expense_id, included) replaces the 303 checkbox toggle, so repeating a call (a retry, a double tap, an undo) can't flip the status the wrong way.
  • Settings saved straight from an input also need actions: set_salary_clients(year, clients), set_iva_compensation(quarter, on, amount), set_iva_note, set_irpf_note.
  • Deleting must be reversible for a short time, because the mobile lists offer "Deshacer": either a restore_invoice / restore_expense action, or a soft delete with an expiry.
  • Every action returns status (success / error), a machine-readable code on error, a short human-readable message, and a list of warnings (e.g. date_outside_quarter) that never block the action.

Search / Retrieval API

  • Supports the same filters the UI offers: by year, by quarter, by invoice status, by IRPF category, by IVA rate.
  • Returns the same computed totals the screens show, rather than raw rows the caller has to recompute. This is what lets both assistants answer with exactly the figures on screen.
  • Supports a single-item lookup by ID in addition to list queries.
  • Provides the monthly series used by the dashboard chart and by the assistants (income and expenses per month, invoiced per month, net salary per month).

Identity & Permissions API

  • Confirms that a request is operating on the ledger the caller is allowed to use, and rejects any action targeting a year or record outside it.
  • Session mode. The online example runs in demo mode: every action is allowed, changes live in memory only and disappear on reload. A real build adds a persisted mode where actions are stored. Every action result states which mode it ran in, so a caller can tell the user whether a change will be kept.

Notification / State-Change API

  • Carries the feedback the UI shows — confirmation messages, the "Atención" date warning, inline form errors, the "Deshacer" message after a delete — as structured events any caller can render its own way.
  • Warnings are a separate channel from errors: the date-outside-quarter warning is shown after the data is saved, never instead of saving it.

4. Example action schema

The invoice functions map to one action family. Below is create_invoice — any caller (a screen, an assistant, a script) uses this shape.

{
  "name": "create_invoice",
  "description": "Creates an invoice (factura) in the given ledger year.",
  "parameters": {
    "type": "object",
    "properties": {
      "year": { "type": "integer", "description": "The year selected in the app. The date's year is always set to it." },
      "number": { "type": "string", "description": "Invoice number (Núm.)" },
      "type": { "type": "string", "enum": ["esp", "ext"], "description": "esp = domestic with IVA; ext = tax-exempt, foreign client" },
      "client_name": { "type": "string" },
      "description": { "type": "string", "description": "Concepto" },
      "date": { "type": "string", "format": "date", "description": "An impossible day is moved to the month's last day (31/06 → 30/06)" },
      "quarter": { "type": "integer", "enum": [1, 2, 3, 4], "description": "Quarter it is declared in. Defaults to the date's quarter; may differ (warning only)" },
      "amount": { "type": "number", "description": "Taxable base (base imponible), before VAT and withholding" },
      "vat_rate": { "type": "number", "enum": [0, 4, 5, 10, 21], "description": "Forced to 0 when type is ext" },
      "withholding": { "type": "boolean", "default": false, "description": "Flat 15% IRPF retention; only for type esp" },
      "paid": { "type": "boolean", "default": false }
    },
    "required": ["year", "number", "type", "client_name", "description", "date", "amount"]
  },
  "returns": {
    "type": "object",
    "properties": {
      "status": { "type": "string", "enum": ["success", "error"] },
      "mode": { "type": "string", "enum": ["demo", "persisted"] },
      "invoice_id": { "type": "string" },
      "warnings": { "type": "array", "items": { "type": "string" } },
      "code": { "type": "string" },
      "message": { "type": "string" }
    }
  }
}

The corresponding read action, get_dashboard_summary, returns the same totals renderDashboard() computes — as structured data instead of rendered HTML.

5. Demo app mapping

Mapping from app_DEMO.html's in-browser functions to the API actions this design proposes.

ScreenFunction(s) in the demoProposed API action
FacturaciónaddFact(), updateFact(), deleteFact(), toggleCobrada(), saveSalarioClientes(); mobile: mSaveInv(), mDelete('f')create_invoice, update_invoice, delete_invoice (+ restore), set_invoice_paid, set_salary_clients
GastosaddGasto(), addGastoIva(), addIvaRate(), updateGasto(), deleteGasto(); mobile: mSaveGasto(), mDelete('g')create_expense, update_expense, delete_expense (+ restore), set_expense_in_303
IVArenderIva(), renderIvaAnual(), saveIvaNota(), saveIvaPagado(), saveIvaCompensarOn(), saveIvaCompensarAmt()get_iva_summary, set_iva_note, set_iva_paid, set_iva_compensation
IRPFrenderIrpf(), renderIrpfAnual(), saveIrpfNota(), saveIrpfPagado()get_irpf_summary, set_irpf_note, set_irpf_paid
DashboardrenderDashboard(), getIngresosGastosPorMes(), getFacturadoPorMes(), getSalarioMainClientMonthly()get_dashboard_summary, get_monthly_series
Year managementsetYear(); addYear(), deleteYearConfirm() (not wired to the UI)list_years, add_year, delete_year
Acceso fácilReads by rendering the screens and reading the figures back (window.AF.read); writes through getState()/saveState() and the save functions aboveThe same read actions as each screen plus create_invoice, create_expense, set_invoice_paid, set_salary_clients, set_iva_paid, set_irpf_paid
Asistente InteligenteAnswers through window.AF.answer() and the monthly helpers; writes through cfSave() (forms) and saveFlow() (step flows)Same as Acceso fácil, plus classify_question — the call to Jev (Section 8)

The demo has no backend: it reads and writes the __appData object in memory. Every function above needs to move behind the typed action interface before any external caller can use it.

6. End-to-end usage flow

Example: a caller marks an invoice as paid. This shows how the four layers from Section 2 work together for a real action.

sequenceDiagram
  participant Caller
  participant IdentityAPI as Identity & Permissions API
  participant ActionAPI as Action API
  participant StateAPI as Notification / State-Change API

  Caller->>IdentityAPI: Confirm caller can use this ledger
  IdentityAPI-->>Caller: Allowed (mode: demo)
  Caller->>ActionAPI: set_invoice_paid(invoice_id, true)
  ActionAPI->>ActionAPI: Validate invoice exists in the ledger
  ActionAPI-->>StateAPI: Emit state-change event
  StateAPI-->>Caller: status: success, mode: demo

This flow works identically for any caller — a screen, an assistant, a script or a test harness — because the API layers don't change based on who is calling them.

7. Domain & calculation logic (tax rules)

Organized the way the app itself is laid out — top to bottom, screen by screen — so it reads alongside the UI.

Layout & breakpoints

WidthLayout
Up to 1023 pxMobile layout: one column, burger navigation, period dropdowns, summary boxes and full-screen lists and forms. From 768 px the column is limited to 768 px and centred, so boxes and charts keep their proportions instead of stretching.
1024–1231 pxDesktop layout that adapts to the available width; wide tables scroll inside their box.
1232 px and widerDesktop layout at its full width (1108 px content).

The welcome dialog and the two assistant windows switch to their small-screen form (full screen / bottom panel) below 768 px. Text is kept at 14 px or larger on mobile, except the uppercase mono labels (12 px, as on desktop) and the chart axes.

Assistant buttons. On desktop they sit at the bottom of the left navigation bar. In the mobile layout both sit bottom-right inside a semi-transparent white box (12 px padding, 27 px gap, 12 px radius, 1 px #E9E9E9 border), each with a light shadow. The button of an open window gets an orange ring.

Header

User info area. Account name and a "DEMO" badge with an (i) tooltip: the demo is meant to organise the accounting of freelancers in Spain; visitors are invited to try it by adding expenses or invoices; the two assistants show how far a design can be taken in collaboration with AI; and a contact email for anyone who wants to use it for their own accounting.

Mobile header. First row: the green "CONTABILIDAD" chip and the user info. Second row: the burger button and a chip with the current section in its colour.

Year strip special logic

On desktop it always renders exactly 6 year slots from the earliest year with data. States: active (selected), has-data (grey, turns green on hover), future (disabled, greyed), or plain/empty. In the mobile layout only the years with data are shown. Clicking a year calls setYear(yr).

dead code  addYear(), deleteYearConfirm() and pedirAnoInicial() exist in the code but aren't wired to any button: there is no way to add or delete a year through the UI. If rebuilt, this is a gap to close deliberately.

Dialogues & messages

In-app modals

  • Welcome dialog (on page load) — ESP/ENG switch, title "Bienvenidos a Contabilidad Demo", the same four paragraphs as the DEMO tooltip (the email is a link) and a "Continuar" button. Below 768 px it fills the screen. The keyboard focus ring only appears once the user presses Tab.
  • Date warning — "Atención!" with a single "Entendido" button. A warning, never a blocker: the data is already saved when it appears. Shown when the date's month falls outside the quarter the item is filed in. Escape closes it. See Dates & quarters.
  • Mobile full-screen dialogs — the new invoice and new expense forms and the "Ver todas" lists (Facturación, Gastos). Each has its title with CANCELAR or CERRAR at the top right, keeps the keyboard focus inside, closes with Escape and returns focus to the button that opened it.

Messages

  • Confirmation message (mobile) — a short black message at the bottom after saving, e.g. "Factura 411 guardada en el 3T 2026 como pendiente de cobro."
  • Delete message with "Deshacer" (mobile quarter lists) — shown for 7 seconds; "Deshacer" puts the item back in its place. Keyboard users can reach it.
  • Inline form errors — under each field, linked to it for screen readers; focus moves to the first invalid field and the error clears as soon as it is corrected.

Native browser dialogue

  • Custom VAT rate prompt (desktop Gastos, "Por % IVA" → "+ Añadir bloque IVA") — a raw prompt() asking for the rate. The new expense starts in category 8 (Otros consumos de explotación) and can be changed in its row. The only native dialogue reachable from the UI; a rebuilt version should route it through a form.

Info tooltips

Every (i) button behaves the same way: hover on desktop (the pointer can move into the tooltip, e.g. to click a link), tap on touch screens (tap again or tap outside to close), and focus / Enter with the keyboard (Escape closes). The tooltip is light blue with a blue left border.

Salario neto tooltip ends: "El salario neto es calculado con esas facturas, restando los gastos del mes anterior, independientemente de si los pagos se hayan realizado."

Dates & quarters non-obvious rule

Every invoice and expense stores its quarter (trim) separately from its date. IVA, IRPF and the Facturación/Gastos boxes group by the stored quarter; the dashboard charts and the salary estimate group by the date. The same rules apply in every place where data is entered — desktop tables, mobile forms, Acceso fácil and the Asistente Inteligente:

  • Year. The date always takes the year selected in the year strip: with 2026 selected, 15/09/2025 or 15/09/2027 becomes 15/09/2026. Forms show it in the label ("Fecha · 2026").
  • Impossible day. Moved to the last day of the month: 31/06 → 30/06, 30/02 → 28/02. Something that isn't a date (45/06, 10/13) is rejected with "DD/MM/AAAA".
  • Quarter. In a quarter tab or quarter list, the item is filed in that quarter whatever its month, because declaring a forgotten expense in a later quarter is legitimate. If the month is outside the quarter, the date warning appears after saving. In annual views and in both assistants (which have no quarter selected) the quarter comes from the date, so no warning is needed.
  • Input. Touch screens use the native date picker (limited to the selected year). Elsewhere the date is typed as DD/MM/AAAA and short forms are accepted (31/6, 15-3-26), normalised when leaving the field.

Navigation

Dashboard Facturación Gastos IVA IRPF

Desktop: fixed left bar, always visible. Mobile: the burger opens a small white menu with the same five sections (12 px mono, as on desktop); it closes on selection, on tapping outside or with Escape, and supports the arrow keys. The current section is marked for screen readers.

Either way, a section is opened with goTo(sectionId) — the app's single routing function — which swaps the visible section, updates the active highlight and re-renders from __appData.

Period selection. Desktop uses tabs (Resumen Anual · 1T–4T), announced as tabs to screen readers. The mobile layout uses a grey dropdown button with a chevron; both stay in sync.

Mapping to the API: each section should call its read action on entry — Dashboard → get_dashboard_summary, Facturación → an invoice-list read — and every button inside should call the matching action instead of changing __appData in place.

Dashboard

Header: today's date ("1 OCT 2026"); for past years, the year's last day.

  • "IVA a pagar" / "IRPF a pagar" boxes. Clickable, three states: quarter in progress (forecast), filing window open (shows the deadline), year closed ("Año cerrado"). Always shows the first quarter whose deadline hasn't passed. The IVA figure follows exactly the same rule as the Modelo 303 screen, including any compensation applied, so both always match.
  • "Salario neto del mes." Estimated net salary from the client(s) set in "Cálculo del salario", using the previous month's invoices of those clients minus that month's expenses — whether or not the invoices are paid (getSalarioMainClientMonthly()).
  • "Previsión de ahorro mensual." 12% of the salary figure above, floored at 0.
  • "Comparativa Ingresos · Gastos" chart — paid invoices against all expenses, month by month.
  • "Comparativa salario neto" chart — the year against the previous one (desktop only).
  • "Distribución del gasto" — all 10 categories, even at zero (desktop only on the dashboard; the mobile Gastos summary shows it for the selected period).
  • "Últimas facturas" — the 5 most recent invoices, with a paid/pending dot that can be tapped (8 px dot, 32 px invisible tap area) and an (i) explaining it. The mobile list leaves out the date.

Mobile order: date, Comparativa Ingresos · Gastos, Últimas facturas, Salario neto, Previsión de ahorro, IVA a pagar, IRPF a pagar.

Facturación

Summary boxes

  • Total facturado — base amount, domestic + foreign, for the period in view.
  • Cobrado — base amount and number of paid invoices in the period.
  • Pendiente de cobro non-obvious rule — every unpaid invoice from Q1 through the selected quarter, plus every unpaid invoice from every previous year. A running "money owed to me" figure, not a quarter snapshot — which is why its title carries no period on mobile. Orange when above zero.
  • IRPF retenido — sum of 15% retentions in the period.

Invoice types

TypeIVARetentionTotal
ext (tax-exempt, foreign client)NoneNonebase
esp (domestic)0/4/5/10/21%Optional flat 15%base + IVA − retention
  • Paid / pending dot flips the status and feeds the dashboard chart and the Pendiente de cobro box. Pending is the default for every new invoice.
  • "Cálculo del salario (Dashboard)" — names the client(s) that drive the salary estimate, separated by "+", or "Todas"; stored per year.
  • Resumen Anual is read-only: invoices are added and edited in their quarter.

Mobile

  • Period dropdown and "VER TODAS LAS FACTURAS" at the top right.
  • Boxes with the period in the title ("Total facturado 2026" / "Facturado (1T)"): Total facturado, Cobrado, Pendiente de cobro, IRPF retenido, Facturas nacionales (net total with IRPF and IVA lines) and Facturas exentas de impuestos — the last two with "+ AÑADIR FACTURA". At the end, the "Cálculo del salario" box.
  • Nueva factura nacional / exenta (full screen): número, fecha, cliente, concepto, base imponible; for domestic invoices also % de IVA and "Aplicar retención IRPF del 15 %". CANCELAR top right, Guardar bottom right.
  • Ver todas las facturas (full screen, grey): one box per type; each invoice shows client, number, date — concepto, base, IVA, IRPF and total with its status dot; newest first; net total per box and overall; Cobrada/Pendiente legend; an (i) next to the title.
  • Annual list: read-only — no add buttons, dots show the status but can't be tapped. Quarter lists: tapping a dot changes the status, and a right-hand column holds a trash button on the Total line (with "Deshacer").

Gastos

Summary boxes

  • Total gastos — base amount of all expenses in view.
  • IVA soportado — total IVA, regardless of 303 status.
  • IVA incluido 303 — only expenses checked for the quarterly return ("todo el año" in the annual view).
  • Total gastado — base + IVA.

IRPF categories — numbered 1 to 10

1 Seguridad Social / RETA 2 Arrendamientos y cánones 3 Suministros (30% cap) 4 Otros suministros 5 Otros servicios exteriores 6 Transporte y viajes 7 Gastos de manutención (€26.67 / €48.08 día) 8 Otros consumos de explotación 9 Otros gastos deducibles (5% / €2,000) 10 Otros desgravables (with "Objeto")

The number appears everywhere a category is named (box titles, lists, forms, both assistants) except the distribution chart. In the "Por % IVA" table the category column shows only the number; it opens a dropdown with the full numbered names, and the full name is available on hover and to screen readers.

Two views, same data

Por cat. IRPF Por % IVA

Expenses can be grouped by the deductible categories the IRPF return defines, or by the VAT rate applied. The expenses don't change, only how they're grouped. Both views show a Total € column per expense and BASE · IVA · TOTAL per box.

  • Seguridad Social never carries IVA. An expense in category 1 always has 0% IVA and can't be included in the 303 — enforced on every save, whatever the caller.
  • informative only The caps and rates shown against the categories (30% suministros, €26.67/€48.08 manutención, 5%/€2,000, 25% bienes amortizables, 15% retention, 20% pago fraccionado…) are reference text: the app doesn't calculate or enforce them. Both the list of categories and these rules should be checked every year in case the regulation changes.

The 303 checkbox — three states

  • Disabled — IVA rate is 0%, or the expense is Seguridad Social.
  • Enabled, flagged — no domestic invoices this quarter (warning only, still usable).
  • Enabled, normal — otherwise.

Mobile

  • Period dropdown and "VER TODOS LOS GASTOS" at the top right; boxes with the period in the title; "+ AÑADIR GASTO" in the Total gastado box; Distribución del gasto for the selected period.
  • Nuevo gasto (full screen, orange): número de factura (optional), fecha, proveedor, concepto, base imponible, % de IVA, a live "TOTAL GASTO", categoría IRPF (starts empty), "Objeto" for category 10 and "Incluir en el 303". Choosing category 1 locks IVA at 0% with a note and disables the 303.
  • Ver todos los gastos (full screen): POR CAT. IRPF / POR % IVA switch; only boxes with expenses are shown; each expense shows supplier, number, date — concepto, base, IVA and total; a 303 column on the right; totals per box and TOTAL GASTADO · BASE · IVA at the end; an (i) next to the title.
  • Annual list: read-only (303 shown but not tappable, no add or delete). Quarter lists: 303 tappable at the top of the right column, trash button at the bottom on the Total line (with "Deshacer"), "+ AÑADIR GASTO" per box.

IVA (Modelo 303)

Deadlines: 20 Apr · 20 Jul · 20 Oct · 30 Jan (year+1).

BoxMeaningFormula (that quarter)
c.01Base imponible nacionalΣ base of esp invoices
c.03IVA repercutidoΣ base × iva% (esp invoices)
c.28Base gastos deduciblesΣ base of qualifying expenses
c.29IVA soportadoΣ base × iva% (qualifying expenses)
c.120Servicios extranjeroΣ base of ext invoices

Result: subtotal = c.03 − c.29; resultado = subtotal − compensar (if "Aplicar compensación" is on). Positive → "A ingresar"; negative → "A compensar próximo trimestre"; zero → "Sin actividad". The payment block only appears when positive. The 303 is disabled for RETA and 0% expenses (see Gastos).

One rule on every screen. The quarterly 303, the annual summary (390) and the dashboard box use the same result: IVA repercutido − IVA soportado − IVA compensado. With no domestic invoices the 303 shows a warning, but IVA soportado is still deducted.

Annual summary (Modelo 390): IVA repercutido · IVA deducido · Operaciones no sujetas · IVA compensado · Resultado Mod. 303, per quarter; "—" for quarters not yet due. The footer "Resultado anual → Mod. 390" adds repercutido − deducido for the quarters already due; offsets are left out because they only move amounts between quarters and would otherwise be counted twice.

Mobile: the annual table becomes one block per quarter (label on the left, amount on the right, result in bold); quarters not yet due read "Sin resultado hasta que termine el plazo de declaración".

IRPF (Modelo 130)

Same deadlines as IVA. Figures are cumulative from Q1, not quarter-only.

BoxMeaningFormula (cumulative)
c.01Ingresos íntegrosΣ base, all invoices, Q1→now
c.02Gastos deduciblesΣ base, all expenses, Q1→now
c.03Rendimiento netoc.01 − c.02
c.0520% s/ rendimientoc.03 × 20%
c.07RetencionesΣ base × 15% (esp, ret=true)
c.08Pagos anterioresΣ paid in prior quarters

Result: cuota = max(0, c.05 − c.07 − c.08). Floored — no negative state, unlike IVA.

Quarter boxes: "IRPF abonado (1T)", "IRPF acumulado (2T)", "IRPF acumulado (3T)", "IRPF acumulado (4T)" — the amount paid through each quarter. In the mobile layout only the selected quarter and the one before it are shown, since what matters there is what has already been paid. The annual summary follows the same one-column rule as IVA on mobile.

Acceso fácil — accessibility assistant

A round accessibility button opens a window, "ACCESO FÁCIL", that lets the user consult and enter data through a short sequence of buttons instead of navigating the screens. Its goal is accessibility: a linear, button-only path that is fast with a keyboard and with VoiceOver or any other screen reader. It runs entirely inside the page — no external service, no network calls.

Why buttons only

The data is structured and the vocabulary is fixed by the Spanish tax forms, so every question maps to a finite set of choices. Buttons are the most reliable input for screen-reader users: each one is a labelled, reachable stop, with no ambiguity about whether the input was understood. Free-text questions are the job of the Asistente Inteligente (Section 8).

Window behaviour

  • Launch button: label "Acceso fácil: asistente accesible"; toggles the window and announces its state with aria-expanded. Closing the window (close button or Escape) returns focus to it.
  • Header: title, ESP/ENG switch, full-screen toggle, close.
  • Floating by default: docked at the bottom without covering the launch button; it can be dragged by the header and resized from the corner.
  • Full screen covers the whole app and becomes modal (focus is trapped inside).
  • Below 768 px it becomes a bottom panel, with drag and resize disabled.
  • Opening Acceso fácil closes the Asistente Inteligente and vice versa: the two are separate experiences.

Flow

Three numbered steps, then the answer. After the answer, a "Cambiar periodo" row (Resumen anual · 1T–4T · year) and a "Ver en la app" link that opens the matching screen, year and quarter. By default every answer shows the current quarter of the active year; past years default to the annual summary.

1. Action2. Category3. Options
Consultar datosDatos clavePending invoices, Salario neto del mes, Previsión de ahorro mensual, IVA a pagar, IRPF a pagar. Follow-ups: income/expenses per month (last 12 months), salary vs last month and same month last year, annual distribution of expenses.
Consultar datosFacturaciónThe four summary boxes. Follow-ups: table totals (domestic / exempt / net), list of invoices.
Consultar datosGastosThe four summary boxes. Follow-ups: by IRPF category (numbered), by % IVA.
Consultar datosImpuestosIVA · Modelo 303 or IRPF · Modelo 130: quarterly boxes and result, or the annual summary (Modelo 390 / Mod. 100).
Introducir datosFacturaciónNew invoice (form), mark an invoice as paid (list of pending invoices, with undo), salary clients.
Introducir datosGastosNew expense form, with the numbered IRPF category chosen from buttons and "Incluir en 303".
Introducir datosImpuestosConfirm a payment (IVA, IRPF or both) for a chosen quarter, prefilled with the quarter's result. IVA can only be recorded when the result is "A ingresar".
Consultar definiciones18 termsBase imponible, IVA repercutido/soportado, Modelo 303/390/130, c.120, IRPF, rendimiento neto, retención 15 %, RETA, gastos deducibles, amortización, IVA a compensar, cobrada/pendiente, salario neto, previsión de ahorro, trimestre.

Bottom of the window: "Volver" (undo the last step) and "Empezar de nuevo" (reset).

Data rules non-obvious rule

  • No new calculations. The assistant never computes its own totals, so its numbers are always identical to what the screens show.
  • Demo implementation temporary  With no API, the assistant sets the year/tab, calls the screen's own render function, reads the figures back from the page and restores the app. This is the opposite of what the Search / Retrieval API requires and is fragile: renaming an element id on a screen breaks the assistant silently. Once the layers in Section 3 exist, the assistant simply calls them.
  • Writes use the same data shape as the screens and follow the date rules (year of the year strip, impossible days corrected). The quarter comes from the date.
  • After saving, the window shows "Ejemplo online: los cambios se ven en la app; al recargar la página vuelven los datos de ejemplo."

Languages

The ESP/ENG switch translates the whole window. English keeps the official Spanish terms in brackets — "VAT payable (IVA a pagar)", "Form 130 (Modelo 130)", "Q3 (3T)" — so a freelancer can match them against the real forms. The window's lang attribute switches too, so screen readers change voice. Amounts keep the Spanish format (1.234,56 €).

Accessibility behaviour

  • Each step is a role="group" labelled by its question; option buttons use aria-pressed.
  • After choosing an option, focus moves to the next step or to the RESPUESTA heading. Changing the period keeps focus on the pressed button and announces the new answer through a polite live region.
  • Forms use visible labels and inline errors linked with aria-describedby / aria-invalid; focus moves to the first invalid field. Confirmations use role="status".
  • Escape closes the window; every control is reachable and operable with the keyboard.

Known limitations

  • to review later  New invoices and expenses are always filed in the quarter of their date; filing them in a later quarter is only possible from the screens.

8. Asistente Inteligente — conversational assistant (Jev)

What it does

A chat window, opened from the robot button, where the user writes questions in their own words — in Spanish or English — about their figures, the tax terms, or what they want to record. Jev understands what is being asked; the app produces the answer from its own data and functions. Answers are never invented: every figure comes from the same code as the screens.

How Jev is used

  • Only for understanding. Each question is sent to Jev (model jev-latest, TypeSafe) as a single "choice" question: which of the app's 46 requests does this text make? Jev returns the chosen request, the probabilities and a confidence value.
  • Only the typed question is sent — never the accounting data. Jev doesn't see or compute any figure.
  • Confidence threshold 0.45. Below it, the assistant doesn't guess: it replies "¿Te refieres a…?" with the two or three most likely options as buttons.
  • Period words ("el trimestre pasado", "en agosto", "2025") are read by the app, not by Jev, and follow-up questions ("¿y el año pasado?") keep the context of the previous question.
GroupRequests Jev chooses from
FiguresKey figures, net salary, month by month, distribution of expenses, invoicing, table totals, list of invoices, expenses, expenses by IRPF category, expenses by % IVA, IVA, IRPF, profit for a month/quarter/year, next month's forecast
Entering dataNew invoice, mark an invoice as paid, salary clients, new expense, IVA paid, IRPF paid
Definitions19 terms (the 18 of Acceso fácil plus "beneficio")
ConversationGreeting, thanks, compliment, complaint, goodbye, help, and "outside the app" (weather, jokes, legal advice…), which gets a polite explanation of what the assistant can do

Connection

The page never holds the TypeSafe key. It sends the question to a small connector on the same server, jev.php, which adds the key, calls Jev and returns the result.

  • Files on the server (same folder as the app): jev.php (holds the key; needs PHP with cURL) and jev-intents.json (the 46 requests and their descriptions — updating the assistant's vocabulary only means replacing this file).
  • Protection: only requests from marieobelleiro.com are accepted; questions are cut at 300 characters; each visitor (IP address) can ask 20 questions every 24 hours.
  • When Jev can't be reached — the connector is missing, the key isn't set, the network fails or there's no answer within 4 seconds — the message below the chat input says so: "Comprensión: reconocimiento local · Jev no conectado". When connected it reads "Comprensión: Jev (TypeSafe)", and when the visitor's limit is reached, "límite de Jev de esta demo alcanzado". In those cases the assistant falls back to a basic keyword recognition so the conversation doesn't stop, with lower understanding.

Answers

  • Figures and definitions reuse the answers of Acceso fácil (window.AF.answer()) for the period understood, so both assistants always say the same thing.
  • Profit ("¿qué beneficio llevo este mes?") — income minus expenses for a month, quarter or year, both as invoiced and as collected.
  • Forecast ("¿cuánto cobraré el mes que viene?") — next month's net salary, from this month's invoices of the salary clients minus this month's expenses.
  • Each answer ends with related suggestions as buttons, and a note of how the question was understood.

Data entry in the conversation

  • New invoice / new expense — the form appears inside the conversation, with the same fields, colours and checks as the mobile forms (blue for invoices, orange for expenses), CANCELAR top right and Guardar bottom right. The invoice form starts with the type (domestic / tax-exempt); it starts on tax-exempt if the question mentions "exenta" or "extranjero". After saving, the form stays as a summary of what was saved, followed by the confirmation and how that quarter looks.
  • Mark an invoice as paid, record an IVA or IRPF payment, salary clients — short step-by-step questions with buttons, a summary to check, and Guardar / Cancelar. Marking an invoice as paid offers "Deshacer".
  • While a form is open, typed text gets "Rellena el formulario de arriba y pulsa Guardar, o escribe «cancelar»."
  • Dates follow the rules in Section 7: the year of the year strip, impossible days corrected, the quarter taken from the date.

Window & accessibility

  • Same window behaviour as Acceso fácil: floating and resizable, full screen, bottom panel below 768 px; opening one assistant closes the other.
  • ESP/ENG translates the assistant's messages — the whole conversation is re-rendered in the other language, forms included, without losing what was typed. The user's own messages stay as written.
  • User messages in light grey bubbles, assistant messages in white; the input gets a green focus outline.
  • Closing the window keeps the conversation; "Borrar conversación" clears it on purpose. Reloading the page starts empty.
  • Messages are announced through a polite live region; forms have labels, inline errors and focus management; everything works with the keyboard.

Limitations

  • Needs a server with PHP + cURL and a TypeSafe key; without them the assistant only has its basic keyword recognition.
  • 20 questions per visitor every 24 hours; a 4-second wait at most per question.
  • Understands one request per message: "IVA e IRPF del 3T" is answered as one of them.
  • Can only answer about this app's data and the tax terms it knows; it gives no legal or tax advice.
  • No memory beyond the open conversation; nothing is stored.
  • Each question costs one call to TypeSafe.

9. Considerations for a real hosted API

Sections 1–8 describe an API that could sit behind the single-file app. Hosting it for real — a backend other devices could call — introduces problems that don't exist in the online example.

Storage

The online example keeps everything in memory and resets on reload. A real product needs persistent storage per user, which turns the "demo / persisted" mode of Section 3 into a real distinction.

Identity & auth model

"The ledger the caller is allowed to use" needs a real credential — an account, a session or an API key. Whether the product stays single-user is a decision to state explicitly, not leave implied.

Concurrent sessions

Two open sessions editing the same data would overwrite each other. A host needs either optimistic concurrency control (reject a write if the data changed since it was read) or real-time sync — or at minimum, a way to tell a caller "this changed elsewhere" instead of losing data silently.

Complete action schema catalog

Only create_invoice has a full JSON schema here. Every other action in the Demo Mapping table needs the same treatment — parameters, types, required fields, return shape — before this is truly build-ready.

Error and warning code catalog

Actions return a machine-readable code on failure and warnings on success, but no enumerated list exists yet. The messages catalogued in Section 7 are the starting list (e.g. the date warning → date_outside_quarter, a date that can't be read → invalid_date).

Data migration & backups

The ledger is one JSON object (__appData), structured by year — the natural input for a hosted store. A host holding real tax records needs automatic backups and a version history, plus a user-controlled export.

The Jev connector

In a hosted product classify_question becomes a server action like any other: the key stays server-side, limits are applied per account rather than per IP address, and the request list (jev-intents.json) grows together with the actions it points to.

Keeping the descriptions in sync

This functional design lives apart from the app, with nothing keeping the two in sync automatically. Worth deciding which one is canonical before a real build starts, and updating this document whenever scope actually changes.