Simulation for test-automation practice. Not a bank, not a financial service. All data is fictional. Do not enter real personal or financial data.
Leer en español: README.es.md
zeroFeeBank is a banking simulator built as a system under test (SUT): a realistic web application you can point Selenium, Playwright or Cypress at to practice test automation. Think of ParaBank, but current, deterministic and designed from day one to be automated.
What it is not. It is not a bank and not a financial service. No real money, no real accounts, no real bank and no real rates are involved. Every screen carries a notice strip saying so (see "Simulation notice" under Test seams). Never enter real personal or financial data.
- Backend: NestJS + PostgreSQL + Prisma (TypeScript).
- Frontend: Angular 21 (
web/). - The user interface is bilingual (Spanish by default, and English), chosen with the
language selector in the header; the choice lasts for the browser session. The documentation is in
English (and Spanish in README.es.md). The specs in
specs/are currently written in Spanish. - The E2E suite is deliberately not in this repository. A suite that lives inside the app ends up leaning on its code and stops being portable across frameworks. See ARQUITECTURA.md.
- What you can automate
- Requirements
- Quick start, step by step
- Environment variables
- Test seams
- Running the checks
- Security notes
- Troubleshooting
- Known limitations
- Repository layout
- Rigor
- License and disclaimer
| Screen / flow | What it does |
|---|---|
| Login / sign up | Login popover (the form is served from the API as an embedded frame, a cross-origin document, which is a good automation exercise) with account creation |
| Accounts | Account summary with balances, and opening a new account (checking or savings) |
| Transfers | Three-step transfer (source, destination, review) to one of your own accounts, to another account by id, or to another bank; replayed requests are flagged; PDF receipt |
| Movements | Searchable transaction history per account |
| Payments | Pay a third party (payee details, amount) and list past payments |
| Guarantee bonds (boletas de garantía) | Issue, list and return guarantee bonds; expiry depends on time, so the clock is controllable (see below); PDF receipts |
| Teller window (ventanilla) | Public screen where a bond is collected by id and the collector's national id number (RUT) |
| Contact | View and edit customer contact data |
The per-screen user stories live in specs/
(HU-01,
HU-02,
HU-03,
HU-04,
HU-05,
HU-06).
| Tool | What is needed | Why / where it comes from |
|---|---|---|
| Node.js | 22.18.0 or newer in the 22 line, or 24.11.0 or newer | The repository does not pin a Node version (there is no engines field and no .nvmrc). This range is derived: it is the intersection of the engines declared by the dependencies in package-lock.json (for example the @babel/* packages that Stryker pulls in: ^22.18.0 || >=24.11.0; Vitest 5: ^22.12.0 || ^24.0.0 || >=26.0.0; Prisma 7.10 and Angular CLI 21.2 in web/package-lock.json: ^20.19 || ^22.12 || >=24). The start script also uses node --env-file, and the Prisma config uses process.loadEnvFile. Node 25 is outside Vitest's declared range. |
| npm | The one that ships with those Node versions | web/package.json declares packageManager: npm@9.2.0; the Angular CLI declares npm >= 8 as its minimum. Both lockfiles are lockfileVersion 3. |
| Docker with Compose v2 | docker and the docker compose subcommand |
Runs PostgreSQL 18 (postgres:18-alpine, see docker-compose.yml). The npm scripts call docker compose, not docker-compose. |
| bash, curl, python3, openssl | Only for the scripts/ checks and the demo |
The scripts are bash; they use curl, python3 -m http.server (to serve the web build), openssl and GNU base64 -w0 (scripts/lib-app.sh). They were written for Linux; they are not verified on macOS or Windows (on Windows, use WSL or a similar bash environment). Not needed for npm start, the web app or the unit tests. |
| Chromium for Playwright | Only for npm run demo:m6 and every verificar:* script |
These scripts drive a real browser through playwright-core (pinned to 1.60.0 in package.json), which does not bundle one. Install it once with npx playwright-core install chromium (run from the repository root, after npm ci). If Chromium then fails to start because of missing system libraries on Linux, npx playwright-core install-deps chromium installs them (it asks for sudo). |
Ports in use (all on localhost):
| Port | Used by | If it is taken |
|---|---|---|
| 5432 | PostgreSQL in Docker. Published only on 127.0.0.1 (docker-compose.yml). |
Stop whatever uses it, or change the left side of the ports mapping in docker-compose.yml and the port inside both database URLs in your .env. |
| 3000 | The API (PORT, default 3000). The web app has this port written into its source (web/src/app/app.ts, web/src/app/app.html), so do not change PORT unless you also edit those files. |
Stop the other program. |
| 4200 | The Angular dev server, and the origin the API allows for CORS (ZFB_ORIGEN_APP, default http://localhost:4200). |
Stop the other program. If you serve the web app on another port, set ZFB_ORIGEN_APP to that exact origin in .env and restart the API. |
| 4201 | Only npm run verificar:s10-t4, which serves a production build there. |
The script aborts; free the port. |
Every command is run from the repository root unless it starts with cd. Do the steps in order.
1. Check your tools.
node --version
docker compose versionExpected: a Node version inside the range above, and a Compose v2 version line. Make sure the Docker daemon is running (Docker Desktop open on Windows and macOS).
2. Clone the repository and install the backend dependencies.
git clone https://github.com/Panxio/zeroFeeBank.git
cd zeroFeeBank
npm ciExpected: npm ci ends with an "added N packages" line and no ERR!.
3. Create your .env. The file .env is not in the repository: git ignores it
(.gitignore ignores .env and .env.*, and keeps only .env.example), so every person creates
their own. Start from the template:
cp .env.example .envOn Windows PowerShell use Copy-Item .env.example .env. Then generate a signing secret and paste it
after ZFB_AUTH_SECRET= in your .env. The secret must be at least 32 characters; the template
leaves it empty on purpose, and the API refuses to start while it is empty.
- Linux or macOS (with
openssl):openssl rand -hex 32 - Any system with Node (also Windows):
node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"
Both print 64 hexadecimal characters. Open .env in an editor and make the line read, for example,
ZFB_AUTH_SECRET=<the 64 characters you just generated>, with no spaces and no quotes.
(The Node one-liner was checked on Linux only.)
Alternative: write the whole .env in one command
The database values are the public credentials of the local container (see
Security notes). The Linux/macOS block was run and prints 64; the PowerShell
block is NOT TESTED (no PowerShell was available when this README was written).
Linux and macOS (bash or zsh, with openssl):
cat > .env <<EOF
DATABASE_URL=postgresql://zerofeebank_app:zerofeebank_app_local@localhost:5432/zerofeebank
DATABASE_URL_MIGRACION=postgresql://zerofeebank:zerofeebank_local@localhost:5432/zerofeebank
PORT=3000
ZFB_AUTH_SECRET=$(openssl rand -hex 32)
ZFB_COSTURAS_PRUEBA=1
ZFB_ORIGEN_APP=http://localhost:4200
EOF
# Check the secret length (must print 64)
awk -F= '/^ZFB_AUTH_SECRET/{print length($2)}' .envWindows (PowerShell), untested:
$bytes = New-Object byte[] 32
[System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
$secret = -join ($bytes | ForEach-Object { $_.ToString('x2') })
@"
DATABASE_URL=postgresql://zerofeebank_app:zerofeebank_app_local@localhost:5432/zerofeebank
DATABASE_URL_MIGRACION=postgresql://zerofeebank:zerofeebank_local@localhost:5432/zerofeebank
PORT=3000
ZFB_AUTH_SECRET=$secret
ZFB_COSTURAS_PRUEBA=1
ZFB_ORIGEN_APP=http://localhost:4200
"@ | Set-Content -Path .env -Encoding ascii
# Check the secret length (must print 64)
$secret.LengthThe -Encoding ascii matters on Windows PowerShell 5.1, where the default encoding can write a
byte-order mark that would corrupt the name of the first variable.
4. Start the database.
npm run db:upExpected: Docker pulls postgres:18-alpine the first time and the command returns once the
container zerofeebank-db is healthy (docker compose up -d --wait). Check with
docker ps --filter name=zerofeebank-db: the status column says (healthy).
5. Create the schema from the migrations. This wipes the local database (fine on a fresh one).
npx prisma migrate reset --forceIf Prisma refuses to run migrate reset (it has a guard that rejects it when an automated AI
agent launches it), on a freshly created database use npx prisma migrate deploy: it applies the
same migrations and deletes nothing.
Expected: the 8 migrations in prisma/migrations/ are applied, the last one being
20260914213749_s22_transferencia_otro_banco. Check with:
npx prisma migrate statusExpected: the output contains Database schema is up to date. The migration s04_ledger_append_only
also creates the unprivileged database role zerofeebank_app that your DATABASE_URL uses; that is
why migrations must run before the API.
6. Build and start the API.
npm run build
npm startExpected: the build ends without errors (it runs prisma generate and tsc, and creates
dist/main.js); npm start prints zeroFeeBank escuchando en http://localhost:3000 and keeps
running. Leave this terminal open.
7. Check the API in a second terminal.
curl -i http://localhost:3000/healthExpected: HTTP/1.1 200 and the body {"status":"ok","db":"up"}. If the database is unreachable it
answers 503 with {"status":"degraded","db":"down"}. Note that "db":"up" only proves the database
answers; it does not prove the migrations ran (use npx prisma migrate status for that).
8. Install and start the web app, in that second terminal.
cd web
npm ci
npm startExpected: Angular's dev server (ng serve) builds the app and serves it at http://localhost:4200/.
9. Open the app. Go to http://localhost:4200/. You should see the landing page with the
simulation notice strip. There are no built-in demo users: open the login popover and create an
account (the password needs at least 8 characters, src/modules/auth/auth.service.ts). To get a
ready-made user with data, use the seed seam below; two scenarios return working login credentials.
To stop everything: Ctrl+C in each terminal, then npm run db:down (stops the container; your data
stays in the Docker volume).
npm startinsideweb/serves the development build, which does not expose the browser clock hook (window.__zfb__). To get it, use thepruebasbuild (see "Injectable clock" below).
These are the variables the code reads. Their values go in your own .env (copied from
.env.example), which is never committed.
| Variable | Required | Default | Purpose and source |
|---|---|---|---|
DATABASE_URL |
Yes | none | Connection used by the app, as the unprivileged role zerofeebank_app, which cannot UPDATE or DELETE the ledger. The app refuses to start without it (src/infra/prisma.service.ts). |
DATABASE_URL_MIGRACION |
Yes | none | Connection used by migrations, as the database owner zerofeebank. Every Prisma command (prisma.config.ts) fails without it, and so does the API when the test seams are on (src/modules/costuras/). |
ZFB_AUTH_SECRET |
Yes | none (empty in .env.example) |
Token signing secret. The API refuses to start if it is missing or shorter than 32 characters (src/modules/auth/auth.service.ts). |
PORT |
No | 3000 |
API port (src/main.ts). The web app expects 3000. |
ZFB_COSTURAS_PRUEBA |
No | off | Turns the test seams on only when it is exactly 1; any other value or no value means off. .env.example sets it to 1. The demo and the verificar:*, invariantes and sonda:* scripts need it on. |
ZFB_ORIGEN_APP |
No | http://localhost:4200 |
Web origin the API allows for CORS. It must be an exact origin: no trailing slash and no path (src/infra/origen-app.ts). |
Script-only: ZFB_LOG sets where the demo and verificar:* scripts write their log (each script has
its own default).
The app exposes the hooks an automation suite needs, so it never has to touch the database or
patch the clock from inside. They are routes under /__test__/ plus a few conventions.
How to turn them on. Put ZFB_COSTURAS_PRUEBA=1 in .env and restart the API. How to keep
them off: remove the line, or set anything other than 1. With the flag off the routes are not
mounted at all, so they answer 404, not 403. You can confirm it:
curl -i -X POST http://localhost:3000/__test__/resetExpected with the flag off: HTTP/1.1 404. Expected with the flag on: HTTP/1.1 200 and the
body {"ok":true,"tablasVaciadas":[...]}, and the database has just been emptied. Do not run it
on data you want to keep.
-
Reset and seed by API, behind the flag.
POST /__test__/resetempties the tables (users, accounts, movements, bonds, idempotency keys) and un-fixes the clock (HTTP 200).POST /__test__/seedwith{"escenario": "<name>"}creates a named scenario and returns the ids it created (HTTP 201). Scenarios:cuenta-unica,dos-cuentas,cuenta-con-historial,movimientos-buscables,boletas-en-cada-estado. Only the last two return acredencialesobject (email,password) that works to log in from the UI; the other scenarios create a user nobody can log in as. An unknown scenario fails loudly withESCENARIO_DESCONOCIDO.POST /__test__/relojmoves the server clock:{"instante": "<ISO date>"}fixes it,{"instante": null}releases it,{"avanzarMs": <n>}advances a fixed clock.
curl -s -X POST http://localhost:3000/__test__/seed \ -H 'Content-Type: application/json' \ -d '{"escenario":"boletas-en-cada-estado"}'
Expected: HTTP 201 and a JSON body with
usuarioId,cuentas,boletasandcredenciales. Log in at http://localhost:4200/ with that email and password. -
Injectable clock. Time never comes from the wall clock inside the domain, so a bond expiry can be tested without waiting months. The server clock is moved with
POST /__test__/reloj. For the browser, build the web app with thepruebasconfiguration, which exposeswindow.__zfb__.reloj(fijar,avanzar,desfijar, in milliseconds), and serve it on 4200 as the scripts do:cd web npx ng build --configuration pruebas python3 -m http.server 4200 --directory dist/web/browserExpected: the build ends without errors and the app is served at http://localhost:4200/. Use this instead of
npm startinweb/(do not run both: they use the same port). -
Typed error codes. Every business error returns
{ "codigo": "FONDOS_INSUFICIENTES", "mensaje": "..." }. Assert on the code; the human-readable message may change. The catalogue is in AGENTS.md. -
data-testidas a versioned contract. Every element a business flow interacts with carries a stabledata-testid. The lists are versioned inspecs/(S-10-testids-T1.txttoT4.txt,S-17-testids-*.txt, and the language-selector list). Renaming one breaks every suite at once, so a change is a change to the contract, not an implementation detail. Loading, empty and error states have their own test ids (for examplecuentas-cargando,cuentas-error,boletas-cargando). Do not anchor locators on visible text or on formatted amounts. -
Language selector and
?lang=. The selector is a<select data-testid="idioma-selector">with optionsesanden; its accessible name (aria-label) tracks the active language.?lang=enor?lang=esin the URL sets the language at startup, so a suite can enter each route in a known language without clicking the selector. Priority at startup:?lang=parameter first, then thezfb.idiomakey insessionStorage, thenes.<html lang>tracks the active language. Business errors are shown by their typed code (for exampleFONDOS_INSUFICIENTES): a suite must assert on the code and not on the visible text, which changes with the language. Themensajefield of API responses remains in Spanish: the frontend replaces it with text from its catalogue according tocodigo. -
Idempotency. Every
POSTthat moves money requires anIdempotency-Keyheader; a replay returns the same response and the headerIdempotency-Replayed: true. -
Simulation notice. The web app has two
<main>regions (the landing page, which also hosts the signed-in screens, and the public ventanilla). Each carries a strip stating that this is a simulation, not a bank or a financial service, and that all data is fictitious; the strip tracks the chosen language (in Spanish it says «No es un banco…» and in English «Not a bank…»). It is a<p role="note" data-testid="aviso-simulacion">, exactly one per<main>, with nofixedorstickypositioning, so it does not cover a control.
Run them from the repository root, after steps 2 to 6 of the quick start (and with a .env). Step 6 builds the code and generates the Prisma client: without it typecheck fails with TS2339 errors and the test:integracion files fail with Cannot find module '.prisma/client/default'.
| Command | What it measures | Needs |
|---|---|---|
npm run typecheck |
TypeScript type check (tsc --noEmit). |
Nothing running. |
npm run test:domain |
Unit tests of the pure domain in src/domain/ (money, ledger, bond, transfer rules). No database, no network. |
Nothing running. |
npm run guante |
The "restriction gate": six checks by absence (domain imports no framework or database, no wall clock or randomness in the domain, no floating point for money, no ledger update or delete in the code, no wall clock in the bonds module, every monetary column is BIGINT). Each gate prints the terms it searched for. Ends with Guante: 6/6 compuertas OK when green. |
Nothing running. |
npm run test:mutation |
Mutation testing (Stryker) of src/domain/ only; fails below 70 % (stryker.config.json). It runs the domain tests once per mutant. |
Nothing running. |
npm run test:integracion |
All integration tests in test/*.int.spec.ts, one file at a time, against the real database. Individual suites: test:auth, test:cuentas, test:boletas, test:movimientos, test:contacto, test:billpay, test:prestamos, test:reloj, test:pdf, test:otros-bancos, test:concurrencia, test:idempotencia, test:costuras, test:costuras-credencial. |
Database up and migrated, .env. Some suites reset the database. |
npm run invariantes |
The ledger invariants I1 to I7 (every transaction sums to 0, the whole ledger sums to 0, balances are derived and not stored, no account below its limit, idempotency keys match transactions, the daily limit to other banks, the guarantee account equals the open bonds). It builds and starts the API itself if nothing answers on port 3000. INV_SEMBRAR=0 audits what is already in the database without resetting. The spec sets a goal of under 60 seconds (specs/S-11-invariantes.md). |
Database up and migrated, seams on. Resets the database unless INV_SEMBRAR=0. |
npm run invariantes:calibrar |
Calibration of the invariants: it injects one defect per invariant on purpose and checks that exactly the right invariant turns red and the others stay green. It resets the database when it ends. | Same as above. |
npm run verify:s00 |
Health check in two directions: /health is 200 with the database up, and 503 with it down while the app stays alive. It stops and restarts the database container. |
Docker; nothing else listening on port 3000. |
npm run verify:s04 |
The database itself rejects what the append-only rule forbids. It runs prisma migrate reset --force first, so it wipes the local database. |
Database up. |
npm run sonda:caja, npm run sonda:garantia |
Probabilistic race checks: the first concurrent operations when a system account does not exist yet (RONDAS and APERTURAS set the size; a pass does not prove absence of the race). |
Database up, seams on. |
npm run demo:m6 |
Full demo in a real browser, from reset: login, transfer, bond. Builds the backend and the pruebas web build, serves them on 3000 and 4200, drives Chromium. |
Chromium installed, python3, ports 3000 and 4200 free, seams on. Resets the database. |
npm run verificar:s10-t1 to t4, verificar:s17-boletas, -abrir-cuenta, -transferir, -movimientos, -pagos, -contacto, verificar:ux-a, ux-b1, ux-b2 |
One browser-driven check per screen or feature, each named after its spec in specs/. Same startup as the demo (verificar:s10-t4 also serves a production build on 4201). Each prints one line per check and a N/M summary; exit code 0 only when all pass. |
Same as demo:m6. |
Before running demo:m6 or any verificar:* script, stop your own npm start and web dev
server: the scripts start their own copies and abort with ABORTA: el puerto N ya está ocupado if a
port is taken. The invariantes and sonda:* scripts do the opposite: if an API already answers on
3000 they use it and leave it running.
Times: apart from the invariants goal above and a code comment saying the backend build takes about
15 seconds, this repository does not record how long these commands take, so none are promised here.
The calibrar:* scripts other than invariantes:calibrar are not included in this repository.
- The database password is public. The container created by
docker-compose.ymluses the passwordzerofeebank_local(and the app rolezerofeebank_appuseszerofeebank_app_local, created by a migration). They are written in this repository on purpose and are valid only for that local container. That is whydocker-compose.ymlpublishes port 5432 only on127.0.0.1: nothing outside your machine can reach the database. Do not change that mapping to5432:5432, and never reuse these passwords, or anything like them, anywhere else. - The test seams have no authentication. Anyone who can reach the API can call
POST /__test__/resetand wipe the database. Turn them on (ZFB_COSTURAS_PRUEBA=1) only on your own machine. Leave them off on any shared computer, server or network. - Do not expose the backend to the internet or to your network. The API does not restrict the
network interface it listens on (
app.listen(port)insrc/main.ts); on a shared network use your firewall so port 3000 stays local. This project is a simulation, not hardened software. - Do not use real data. No real names, ids (RUT), accounts, cards or passwords. Everything here is fictional by design.
.envis never uploaded. It is git-ignored; only.env.example(which holds no secret) is versioned. Generate your ownZFB_AUTH_SECRET; do not share it or paste it in public issues.
Each entry reads: symptom, cause, fix.
npm run db:upfails with an "address already in use" or "port is already allocated" error on 5432. Another PostgreSQL (or another container) already uses port 5432, and the compose file publishes a fixed port. Stop it, or change the left side of theportsmapping indocker-compose.ymland the port in both database URLs in.env.db:upfails because the container namezerofeebank-dbis already in use. The compose file fixescontainer_name: zerofeebank-db, so a container with that name from another checkout (or an older run) blocks it. Look withdocker ps -a --filter name=zerofeebank-db. If it is the same project,npm run db:upjust starts it. If it is stale and you do not need it,docker rm -f zerofeebank-db, thennpm run db:upagain.- "Cannot connect to the Docker daemon" or similar. Docker is not running. Start Docker (open Docker Desktop, or start the service) and repeat the command.
npm ciprintsEBADENGINEwarnings, or the toolchain fails strangely. Your Node is outside theenginesof some dependencies (see Requirements). Install Node 22.18.0 or newer (or 24.11.0 or newer) and runnpm ciagain.npm startstops withnode: .env: not found. There is no.envin the repository root. Do step 3 of the quick start.npx prisma ...ornpm run buildfail withFailed to load config fileandDATABASE_URL_MIGRACION no está definida.prisma.config.tsneeds that variable; your.envis missing or lacks it. Copy.env.exampleagain, or add the line.- The API stops at startup with
ZFB_AUTH_SECRET no está definida o mide menos de 32 caracteres. The secret is empty or too short..env.exampleleaves it empty on purpose: generate one (step 3) and restart. - The API stops with
DATABASE_URL no está definida, or withDATABASE_URL_MIGRACION no está definida. The variable is missing in.env(the second one is required whenZFB_COSTURAS_PRUEBA=1). - The API stops with
ZFB_ORIGEN_APP inválido. The value is not an exact origin. Usehttp://localhost:4200, without a trailing slash or path. /healthsays503and"db":"down". The API cannot reach the database: the container is not running, or the URL in.envhas the wrong port. Runnpm run db:upand checkdocker ps./healthis fine but requests fail, or the app cannot log in ("password authentication failed for user zerofeebank_app", or a missing-table error). The schema is not migrated: the migration creates the role and the tables. Runnpx prisma migrate reset --force, and check withnpx prisma migrate status.- The web page loads but login or data fail, and the browser console shows CORS errors, or the login
frame is blank. The web app talks to
http://localhost:3000(fixed in its source) and the API only allows the origin inZFB_ORIGEN_APP. Make sure the API runs on 3000 and that you open the web app on the origin set inZFB_ORIGEN_APP(defaulthttp://localhost:4200). - Any
__test__route answers 404. The seams are off. SetZFB_COSTURAS_PRUEBA=1in.envand restart the API. window.__zfb__is undefined in the browser. You are on the development build (npm startinweb/). Build with--configuration pruebas(see "Injectable clock").demo:m6or averificar:*script printsABORTA: el puerto 3000 ya está ocupado(or 4200 or 4201). Something is already listening there, often your ownnpm startorng serve. Stop it and run the script again.- A browser script fails saying the Chromium executable does not exist. Playwright's browser
is not installed. Run
npx playwright-core install chromium. npx playwright-core install chromiumsays "Playwright does not support chromium on ubuntu26.04-x64" (or on another Ubuntu newer than the ones Playwright knows). Playwright does not recognise your distribution. Point it at a supported one:PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=ubuntu24.04-x64 npx playwright-core install chromium(tested on Ubuntu 26.04: it fails without the variable and downloads the browser with it).npm ciends with "N vulnerabilities" warnings. That is the informational output ofnpm auditabout the dependencies. This repository is a local simulator that must not be deployed (see "Security notes"). Do not runnpm audit fix --forceblindly: it can change versions that are pinned on purpose, such asplaywright-core1.60.0.- A script says
ROJO: la BD no levantaorROJO: build del backend. The first meansdocker compose up -d --waitfailed (Docker off, port or name conflict, see above); the second meansnpm run buildfailed (for example a missing.env, see above). Read the log file the script names (ZFB_LOG).
These are deliberate scope decisions (the backend and the domain were not touched for translation), not hidden defects.
- The login frame (the iframe served by the backend,
src/modules/auth/marco.pagina.ts) remains in Spanish in both languages. - PDFs (receipts and guarantee bonds,
src/modules/comprobantes/plantillas/pdf.ts) are generated in Spanish in both languages. - The
mensajefield in API responses is in Spanish; the frontend uses the catalogue according tocodigo. If you test the API, assert oncodigo. - User data (names, descriptions) is not translated, nor are currency and date formats changed.
- There are only two languages (
es,en). There is no language per URL path or for SEO; only?lang=. - The specs in
specs/are in Spanish. - The Amount field of Bill Pay is a plain text input and does not filter what you type. This is
by design: validation belongs to the server. A malformed amount (for example
ujhuhjku) is rejected with400 MONTO_INVALIDO, the screen shows the error in the active language, and no money moves.
.
├── src/ backend (NestJS): domain/ (pure rules), modules/ (use cases), infra/
├── prisma/ schema and the SQL migrations
├── web/ Angular frontend
├── test/ integration tests (*.int.spec.ts)
├── scripts/ guante, invariants, demo and the per-screen browser checks
├── specs/ specs, user stories (HU-*) and the data-testid lists
├── docker-compose.yml PostgreSQL 18 for local use
├── .env.example template for your own .env
├── AGENTS.md domain rules, invariants, error codes, seam contract
├── ARQUITECTURA.md architecture and the reasons behind it
└── LICENSE MIT
Money is the one domain where a rounding error is invisible: the UI looks fine, the tests pass, and the books do not balance. So this project puts its checks on data invariants, and it calibrates them by breaking the system on purpose to confirm the check turns red. Every row below names the command that reproduces it; the one exception is noted under the table.
| Measure | Result | Measured on | Checked on publish day |
|---|---|---|---|
Mutation score on src/domain/ (npm run test:mutation, Stryker; threshold 70 %) |
83.20 % | 2026-10-06 | done |
Domain unit tests (npm run test:domain) |
144/144 | 2026-10-06 | done |
Integration tests (npm run test:integracion) |
446/446 | 2026-10-06 | done |
Ledger invariants I1 to I7 (npm run invariantes) |
7/7 | 2026-10-06 | done |
Invariant calibration (npm run invariantes:calibrar, defects injected on purpose) |
21/21 | 2026-10-06 | done |
Full demo (npm run demo:m6, login to transfer to bond, from reset) |
3/3 runs, 42/42 steps | 2026-10-06 | done |
Boletas screen check (npm run verificar:s17-boletas) |
43/43 | 2026-10-06 | done |
Boletas screen check calibration (npm run calibrar:s17-boletas, defects injected on purpose) |
52/54 (2 injected defects not caught) | 2026-10-06 | done |
Restriction gate (npm run guante) |
6/6 | 2026-10-06 | done |
Note: the script behind calibrar:s17-boletas is not included in this repository, so that one row
cannot be reproduced from here; the other rows can. The 2 injected defects that were not caught are
known blind spots of the screen check, not noise: they were measured before the bilingual front end
and again after it, and they are the same two.
A "blind" verification is one that could not turn red on the defect it was meant to catch. Finding those, and fixing them, is the point of calibrating. The numbers above are not a claim that the system has no defects: a green suite only shows that the suite found nothing.
MIT. The software is provided as is, without warranty of any kind. Nothing here is legal, financial or security advice. This is a simulation: no real money, no real accounts and no real bank are involved, and it must not be deployed as if it were one.