From dab65564fc61a5d1894a4b377ee2a8258cadf461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Pe=C3=B1a?= Date: Tue, 21 Jul 2026 01:43:41 -0400 Subject: [PATCH 1/3] docs: add OSS scaffolding (LICENSE, CONTRIBUTING, CoC, SECURITY, issue templates) --- .github/ISSUE_TEMPLATE/add-a-bank.yml | 60 ++++++++++++++++++ .github/ISSUE_TEMPLATE/bug_report.yml | 44 +++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 ++ .github/ISSUE_TEMPLATE/feature_request.yml | 21 +++++++ .github/PULL_REQUEST_TEMPLATE.md | 9 +++ CODE_OF_CONDUCT.md | 73 ++++++++++++++++++++++ CONTRIBUTING.md | 61 ++++++++++++++++++ LICENSE | 21 +++++++ SECURITY.md | 21 +++++++ pyproject.toml | 1 + 10 files changed, 316 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/add-a-bank.yml create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 SECURITY.md diff --git a/.github/ISSUE_TEMPLATE/add-a-bank.yml b/.github/ISSUE_TEMPLATE/add-a-bank.yml new file mode 100644 index 0000000..79c49fa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/add-a-bank.yml @@ -0,0 +1,60 @@ +name: Add a bank +description: Request support for a new bank, or start contributing a parser yourself +title: "[bank] " +labels: ["bank-parser"] +body: + - type: markdown + attributes: + value: | + Thanks for helping Finitum support more banks! Parsers are data-driven -- you can add one yourself + with no engine code by following [docs/adding-a-bank.md](../blob/main/docs/adding-a-bank.md). + If you'd rather not, fill this out with sanitized samples and someone can pick it up. + + **Important: sanitize every sample.** Replace real names, account numbers, national ids, and card + digits with fake values of the same shape (same length/format). + - type: input + id: bank + attributes: + label: Bank name and country + placeholder: "e.g. Banco Estado (Chile)" + validations: + required: true + - type: input + id: senders + attributes: + label: Sender addresses + description: The From addresses the bank uses for notification emails. + placeholder: "e.g. notifications@bank.com, alerts@bank.com" + validations: + required: true + - type: checkboxes + id: types + attributes: + label: Notification types the bank sends + options: + - label: Purchases (card payments) + - label: ATM withdrawals + - label: Transfers + - type: textarea + id: samples + attributes: + label: Sanitized sample emails + description: | + For each type, paste the subject and the body (use "Show Original" in Gmail, then sanitize). + Keep the exact spacing/encoding artifacts -- they matter for the regexes. + render: text + validations: + required: true + - type: checkboxes + id: intent + attributes: + label: Contribution + options: + - label: I plan to open a PR for this parser myself + - type: checkboxes + id: sanitized + attributes: + label: Confirmation + options: + - label: I confirm the samples above contain no real personal or account data + required: true diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..af40372 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,44 @@ +name: Bug report +description: Something isn't working +labels: ["bug"] +body: + - type: textarea + id: what + attributes: + label: What happened? + description: What did you do, what did you expect, and what happened instead? + validations: + required: true + - type: textarea + id: repro + attributes: + label: Steps to reproduce + placeholder: | + 1. ... + 2. ... + validations: + required: true + - type: dropdown + id: area + attributes: + label: Area + options: + - Email ingestion / forwarding + - Bank parser (wrong amount, merchant, category...) + - Web app / dashboard + - Authentication + - Self-hosting / Docker + - Other + validations: + required: true + - type: textarea + id: env + attributes: + label: Environment + description: Self-hosted or finitum.app? If self-hosted, how are you running it? Include relevant logs (redact secrets). + - type: markdown + attributes: + value: | + If a bank email was parsed wrongly, please include a **sanitized** copy of the email + (replace names, account numbers, and card digits with fake values of the same shape). + Never post real personal or financial data. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..2bae454 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Security vulnerability + url: https://github.com/richardhapb/finitum/blob/main/SECURITY.md + about: Please report security issues privately -- not as public issues. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..b166cbc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,21 @@ +name: Feature request +description: Suggest an idea or improvement +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: What problem would this solve? + validations: + required: true + - type: textarea + id: solution + attributes: + label: Proposed solution + description: How do you imagine it working? Alternatives you considered are welcome too. + validations: + required: true + - type: textarea + id: context + attributes: + label: Additional context diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..08c02ad --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ +## What + + + +## Checklist + +- [ ] Tests added/updated for behavior changes (`uv run pytest`) +- [ ] Lint passes (`uv run ruff check`) +- [ ] For bank parsers: fixtures are sanitized (no real names, accounts, or card digits) and parametrized cases added in `tests/test_parse.py` diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..d8d1760 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,73 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances + of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +finitumapp@gmail.com. All complaints will be reviewed and investigated promptly +and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e4fb2b9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,61 @@ +# Contributing to Finitum + +Thanks for your interest in Finitum! Contributions of all kinds are welcome, but the one we want most is simple: **add your bank**. Parsers are data-driven (JSON + regex + fixtures, no engine code), and every new bank makes Finitum useful to more people. The complete guide is [docs/adding-a-bank.md](docs/adding-a-bank.md). + +## Ways to contribute + +- **Add a bank parser** -- see [docs/adding-a-bank.md](docs/adding-a-bank.md). No Python required. +- **Improve category detection** -- add merchant keywords to `categories.json`. +- **Report bugs / propose features** -- use the [issue templates](https://github.com/richardhapb/finitum/issues/new/choose). +- **Improve the core** -- backend (FastAPI), frontend (React Router), the ingest pipeline, or the docs. + +## Development setup + +The backend uses Python 3.12+ managed with [uv](https://docs.astral.sh/uv/); the frontend uses [Bun](https://bun.sh/). + +**Full stack via Docker (recommended for running the app):** + +```bash +cp .env.example .env # fill in the values +docker compose up --build +``` + +**Backend only (for tests and linting):** + +```bash +uv sync +uv run pytest # run the test suite +uv run ruff check # lint +``` + +Parser tests need no services at all -- they run against text fixtures in `tests/banks/`: + +```bash +uv run pytest tests/test_parse.py -v +``` + +**Frontend:** + +```bash +cd web +bun install +bun run dev # dev server +bun run typecheck # typegen + tsc +bun run lint # eslint +``` + +## Pull requests + +- Branch from `main`; keep PRs focused on one change. +- **Include tests** for anything that changes behavior. For parsers this means fixtures + parametrized cases (see the guide); CI runs `ruff` and `pytest` on every PR. +- **Sanitize fixtures**: sample emails must not contain real names, account numbers, national ids, or card digits. Replace them with fake values of the same shape. +- Keep the parser engine generic -- bank-specific logic belongs in `regex.json`, not in Python. +- New user-facing text should be in English; localized labels are handled via the category catalog overrides. + +## Questions + +Open a [discussion or issue](https://github.com/richardhapb/finitum/issues), or email finitumapp@gmail.com. Security issues: please follow [SECURITY.md](SECURITY.md) instead of opening a public issue. + +## Code of Conduct + +By participating you agree to the [Code of Conduct](CODE_OF_CONDUCT.md). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ebfca96 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Richard Peña and Finitum contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..3ee99ca --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Reporting a vulnerability + +Please **do not open a public issue** for security problems. + +Report vulnerabilities privately to **finitumapp@gmail.com** (or via [GitHub private vulnerability reporting](https://github.com/richardhapb/finitum/security/advisories/new) if enabled). Include steps to reproduce and, if possible, an assessment of impact. + +You should receive an acknowledgement within a few days. Please give us a reasonable window to ship a fix before public disclosure. + +## Scope + +Especially interesting areas: + +- The inbound email webhook (`POST /ingest/email`): HMAC signature verification, ingest-token handling, deduplication. +- Authentication: JWT handling, Google sign-in flow. +- Anything that could expose one user's transactions or ingest address to another user. + +## Supported versions + +Finitum is pre-1.0: only the latest `main` is supported with security fixes. diff --git a/pyproject.toml b/pyproject.toml index e8799bc..3988bf8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [project] version = "0.0.1" name = "finitum" +license = { text = "MIT" } requires-python = ">=3.12" dependencies = [ "alembic>=1.16.5", From 03be4ed0a88fffcf4b62a630193e302acf98d687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Pe=C3=B1a?= Date: Tue, 21 Jul 2026 01:43:41 -0400 Subject: [PATCH 2/3] docs: rewrite README around forwarding and community parsers, add docs/ --- CLAUDE.md | 41 +++++ README.md | 394 ++++++++---------------------------------- docs/adding-a-bank.md | 148 ++++++++++++++++ docs/deployment.md | 31 ++++ web/README.md | 69 +++----- 5 files changed, 322 insertions(+), 361 deletions(-) create mode 100644 CLAUDE.md create mode 100644 docs/adding-a-bank.md create mode 100644 docs/deployment.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..58b65f8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,41 @@ +# Finitum + +Open-source, dev-first personal finance manager: users forward their bank notification emails to a per-user ingest address, and Finitum parses them into transactions with community-written bank parsers. + +## Project direction (read this first) + +Finitum pivoted in July 2026 from a private, Chile-only SaaS to a **worldwide, open-source, dev-first project**. The goal is to become a world-class OSS standard for email-based transaction parsing, not primarily to monetize. Implications: + +- **Parsers are community-contributed.** Anyone should be able to add their bank by editing data files and fixtures, without touching engine code. Optimize every change for that contributor experience. +- **Email ingestion is via forwarding rules, not the Gmail API.** Users add a Gmail forwarding rule (or any provider's equivalent) pointing to their per-user ingest address; a Cloudflare Email Routing worker POSTs the raw MIME to `POST /ingest/email`. The old Gmail-API polling path (Celery `src/tasks/email_fetch.py`, `EmailManager` in `src/email_service/manager.py`) is legacy, gated behind `GMAIL_POLLING_ENABLED=false`, and slated for removal. +- **Google OAuth is login-only** (`openid` + `userinfo.email` scopes, `src/oauth_service/google_oauth.py`). Never reintroduce Gmail scopes. +- **Worldwide, not Chile-only.** Hardcoded Chilean assumptions (CLP defaults, `America/Santiago`, Spanish-only labels, Chilean merchant keywords) are legacy to be generalized, not patterns to extend. +- A hosted cloud offering for non-technical users may exist later, but the repo is the OSS product; keep private-deployment specifics (the VPS, GHCR namespace, `finitum.app` domains) out of contributor-facing docs and templated in infra configs. +- **Planned: outbound webhooks.** Devs should be able to subscribe to events (new transaction parsed, transfer detected, etc.) and trigger their own automations from the data. Not implemented yet; design APIs with this event surface in mind. + +The reference deployment is live at https://finitum.app (Lightsail VPS, deployed via the CI pipeline). Richard dogfoods it daily -- treat `main` as production. + +## Architecture + +- **Backend**: FastAPI (`src/api/server.py`), PostgreSQL + Alembic (`src/db/`, `alembic/`), Redis (dedupe + Gmail-confirmation capture). +- **Ingestion**: `src/email_service/ingest.py` -- resolves user by `ingest_token` from the `u-@` recipient, HMAC-verifies `X-Finitum-Signature` (`INGEST_WEBHOOK_SECRET`), dedupes on `Message-ID`, auto-captures Gmail forwarding-confirmation links/codes into Redis for one-click setup. Worker lives in `infra/email-worker/`. +- **Parsers**: fully data-driven. All bank logic lives in `src/parsers/regex.json` (per-bank: `remitents` sender allowlist, `subject` classification patterns, `body` extraction regexes). Engine: `src/parsers/parser.py` (`EmailParser`, `BankPatterns.from_json`). The bank is a per-user setting (`User.bank`); there is no content-based bank auto-detection. `GET /banks` derives the bank list from `regex.json` keys. +- **Categories**: keyword matching in `src/parsers/base.py` from root `categories.json`; slugs/labels registry in `src/category_catalog.py`; Spanish overrides in `category_labels.es.json`; per-user custom categories via `POST /categories` + `src/db/categories.py`. +- **Frontend**: React Router v7 + TypeScript + Tailwind + Bun in `web/` (file routes under `web/app/routes/`). `profile.tsx` holds the up-to-date forwarding-setup UX; `home.tsx` and `guide.tsx` still carry stale OAuth-era messaging. + +## Adding a bank (the core contributor flow) + +1. Add a bank block to `src/parsers/regex.json` (all body regex keys are required by `BankPatterns.from_json` validation). +2. Add sanitized sample fixtures under `tests/banks//` (plain text body/subject files, e.g. `purchase_clp.txt`, `purchase_subject.txt`). +3. Add parametrized cases in `tests/test_parse.py`. Tests build `Message(...)` by hand and run `EmailParser(bank)` directly -- no network or Gmail needed. + +## Conventions + +- Python managed with `uv` (`uv.lock`, `uv sync --frozen`); lint with `ruff`; tests with `pytest`. CI in `.github/workflows/ci.yml` runs lint + tests on PRs. +- Run everything via Docker Compose (`docker compose up --build`); API on 9090. +- Write tests for behavior changes, especially parser changes (fixture-driven). +- English is the canonical language for docs, code, and labels; other locales are overrides. + +## Known drift (do not trust these blindly) + +- Root `README.md` still describes the OAuth/Gmail-API model and private EC2/GHCR deploy; `web/README.md` describes a layout that no longer exists; `terms/` texts assume the Gmail-access SaaS. The roadmap lives in `plans/` (phases 1-3) and `todo.md` -- `plans/phase-3-open-source-i18n.md` is the source of truth for the OSS/i18n work (LICENSE decision, CONTRIBUTING, docs/, issue templates, SECURITY.md are still missing). diff --git a/README.md b/README.md index dab4499..ee0e717 100644 --- a/README.md +++ b/README.md @@ -1,362 +1,116 @@ -# Finitum – Personal Finance Manager +# Finitum -Finitum is a modern, extensible personal finance manager that automatically parses your bank and email notifications to track expenses, transfers, and financial activity. It features a web dashboard, category detection, and secure OAuth integration with Google. +**Turn your bank's notification emails into structured financial data -- automatically.** ---- +Finitum is an open-source personal finance tracker. Instead of scraping your bank or asking for your credentials, it uses the emails your bank already sends you: you set up a one-time forwarding rule, and every purchase, withdrawal, and transfer notification is parsed into a transaction (amount, merchant, date, category) and shown on your dashboard. -## Features +- **No inbox access.** Finitum never reads your email account. You forward only the bank notifications you choose; Google sign-in is optional and used for login only. +- **Community bank parsers.** Every bank's email format is described in a single JSON file -- no engine code needed. Adding your bank is a JSON block plus a couple of test fixtures. See [Adding a bank](docs/adding-a-bank.md). +- **Self-hostable.** One `docker compose up` runs the whole stack. A hosted instance lives at [finitum.app](https://finitum.app). +- **Privacy first.** Raw email content is processed in real time and never stored -- only the extracted transaction data. -- **Automatic Email Parsing**: Connect your Gmail account and Finitum will extract expenses and transfers from bank notifications. -- **Category Detection**: Uses robust keyword matching and normalization to classify transactions into categories (Food, Transport, Online, etc.). -- **Dashboard**: Interactive Dash/Plotly dashboard for visualizing spending trends, top merchants, category breakdowns, and more. -- **User Authentication**: Secure signup/signin with JWT-based authentication. -- **Google OAuth2 Integration**: Securely authorize Gmail access using OAuth2. -- **Celery Task Queue**: Asynchronous background tasks for fetching and parsing emails. -- **PostgreSQL Database**: Stores users, transactions, and credentials securely. -- **Extensible**: Modular parser and category system for easy adaptation to new banks or notification formats. +License: [MIT](LICENSE). ---- +## How it works -## Quickstart (Docker Compose) - -**The recommended way to run Finitum is via Docker Compose.** -No local Python or database setup required. - -### 1. Requirements +``` +Your bank ──notification──▶ Your inbox ──forwarding rule──▶ u-@your-domain + │ + Cloudflare Email Routing worker + │ (HMAC-signed POST) + ▼ + Finitum API /ingest/email + │ + bank parser (regex.json) + │ + ▼ + transaction ──▶ dashboard 📊 +``` -- [Docker](https://docs.docker.com/get-docker/) -- [Docker Compose](https://docs.docker.com/compose/) +1. Each user gets a unique ingest address (`u-@`). +2. In Gmail (or any provider), you add that address as a forwarding target and create a filter for your bank's sender addresses. Finitum even captures Gmail's forwarding-confirmation email automatically so setup is one click. +3. Incoming mail is relayed by a small [Cloudflare Email Routing worker](infra/email-worker/) to the API, verified with an HMAC signature, deduplicated, parsed, and saved. -### 2. Environment Variables +## Supported banks -Copy `.env.example` to `.env` and fill in the required values: +| Bank | Country | Parser id | +|------|---------|-----------| +| Banco de Chile | 🇨🇱 Chile | `banco_chile` | +| Santander | 🇨🇱 Chile | `santander` | -```env -CONN_STR=postgresql+psycopg://finitum:yourpassword@db:5432/finitum -PGUSER=finitum -PGPASSWORD=yourpassword -PGDATABASE=finitum -REDIS_URL=redis://redis:6379/0 -GOOGLE_CLIENT=your-google-client-id -GOOGLE_SECRET=your-google-client-secret -GOOGLE_REDIRECT_URI=http://localhost:9090/google_oauth2callback -SECRET_KEY=your-jwt-secret -TZ=America/Santiago -DEBUG=true -``` +Your bank not here? **You can add it without writing engine code** -- parsers are data-driven regex definitions with fixture-based tests. Follow [docs/adding-a-bank.md](docs/adding-a-bank.md) and open a PR, or open an [Add a bank](https://github.com/richardhapb/finitum/issues/new?template=add-a-bank.yml) issue with sanitized samples. -- You must create Google OAuth credentials for Gmail API access: - [Google Cloud Console – Credentials](https://console.cloud.google.com/apis/credentials) +## Quickstart (self-host) -### 3. Build and Start All Services +Requirements: [Docker + Docker Compose](https://docs.docker.com/get-docker/). ```bash +git clone https://github.com/richardhapb/finitum.git +cd finitum +cp .env.example .env # fill in the values docker compose up --build ``` -This will start: - -- **API server** (FastAPI, port 9090) -- **Dashboard** (Dash/Plotly, port 5050) -- **Celery worker** (background email parsing) -- **Celery beat** (periodic tasks) -- **PostgreSQL** (database, port 5444) -- **Redis** (cache, port 6379) -- **Alembic** (runs DB migrations on first up) - -### 4. Access the App - -- **Web Dashboard & API**: [http://localhost:9090](http://localhost:9090) -- **API Docs**: [http://localhost:9090/docs](http://localhost:9090/docs) - ---- - -## Usage +This starts the FastAPI server (port 9090), the web app, PostgreSQL, Redis, and background workers; database migrations run automatically on first start. -1. **Sign Up**: Register a user via `/signup` endpoint or UI. -2. **Google OAuth**: Visit `/google-authorize` to link your Gmail account. -3. **Fetch Emails**: Celery tasks will periodically fetch and parse new emails. -4. **Dashboard**: Open the dashboard at [http://localhost:5050](http://localhost:5050) to view your financial analytics. +- **App / API**: http://localhost:9090 (interactive API docs at `/docs`) ---- +Key environment variables (see `.env.example` for the full list): -## Deployment (CI/CD) +| Variable | Purpose | +|----------|---------| +| `CONN_STR`, `PGUSER`, `PGPASSWORD`, `PGDATABASE` | PostgreSQL connection | +| `REDIS_URL` | Redis (dedupe, forwarding-confirmation capture) | +| `SECRET_KEY` | JWT signing secret | +| `INGEST_WEBHOOK_SECRET` | Shared secret between the email worker and the API | +| `INGEST_DOMAIN` | Domain of the per-user ingest addresses | +| `GOOGLE_CLIENT`, `GOOGLE_SECRET`, `GOOGLE_REDIRECT_URI` | Optional -- Google sign-in (login only, no Gmail scopes) | +| `TZ` | Timezone for parsed email dates | -Every push to `main` runs `.github/workflows/ci.yml`: +To receive real emails you also deploy the email worker on your own domain -- see [infra/email-worker/](infra/email-worker/) for a 5-minute setup with Cloudflare Email Routing. -1. **lint-test** — `ruff check` + `pytest` (also runs on pull requests, as a gate). -2. **build-push** — builds a locked image (`uv.lock` + `uv sync --frozen`) and - pushes `ghcr.io/richardhapb/finitum-api` tagged `latest` and `sha-`. -3. **deploy** — SSHes into the EC2 host and runs: +## Adding your bank in 3 steps - ```bash - cd ~/finitum - git pull --ff-only - docker compose -f docker-compose.yml -f docker-compose.prod.yml pull - docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d - docker image prune -f - ``` +1. **Describe the emails**: add a block for your bank to [`src/parsers/regex.json`](src/parsers/regex.json) -- sender addresses, subject patterns per transaction type, and body regexes that capture amount, merchant, and date. +2. **Add fixtures**: drop sanitized sample email bodies/subjects into `tests/banks//`. +3. **Add a test case**: extend the parametrized cases in [`tests/test_parse.py`](tests/test_parse.py) and run `pytest tests/test_parse.py -v`. -The image is self-contained (code + deps + alembic migrations baked in); `git -pull` on the server only refreshes the compose/env files. Prod uses -`volumes: !reset []` so no host source is mounted — the server runs exactly what -CI built. +That's it -- the API and the web UI pick up new banks automatically from `regex.json`. The full guide, including regex tips, encoding pitfalls, and a CLI for testing against a raw email file, is in [docs/adding-a-bank.md](docs/adding-a-bank.md). -### One-time setup - -- **Make the package public**: GitHub → Packages → `finitum-api` → Package - settings → Change visibility → **Public**. The server then pulls with no auth. -- **Add repository secrets** (Settings → Secrets and variables → Actions): - `EC2_HOST`, `EC2_USER` (e.g. `ubuntu`), and `EC2_SSH_KEY` (a private key whose - public half is in the box's `~/.ssh/authorized_keys`). -- **On the server**: clone the repo to `~/finitum` on `main`, populate - `.env.prod` (including `CREDENTIALS_ENCRYPTION_KEY` — back it up, losing it - makes stored Google tokens unrecoverable), and confirm - `docker compose version` ≥ 2.24 (required for `!reset`). - ---- - -## Project Structure +## Project structure ``` src/ - api/ # FastAPI server, JWT auth, endpoints - db/ # SQLModel models, DB service - email_service/ # Gmail API integration, message parsing - oauth_service/ # Google OAuth2 logic - parsers/ # Expense/transference parsing, category logic - tasks/ # Celery tasks for background processing - ui/ # Dash/Plotly dashboard + api/ # FastAPI server, JWT auth, ingest webhook, endpoints + db/ # SQLModel models, DB services + email_service/ # Inbound email processing (MIME parsing, ingest pipeline) + oauth_service/ # Google sign-in (login only) + parsers/ # Bank parsers engine + regex.json definitions utils/ # Config, logging, helpers +web/ # React Router v7 + Tailwind frontend +infra/email-worker/ # Cloudflare Email Routing worker (forwarding relay) alembic/ # Database migrations -tests/ # Unit and integration tests -``` - ---- - -## Extending Finitum - -### Adding Support for a New Bank - -Finitum uses regex patterns to parse bank notification emails. Each bank has its own patterns defined in `src/parsers/regex.json`. - -#### 1. Collect Sample Emails - -Get raw email samples from your bank for: -- Purchase notifications -- ATM withdrawals -- Transfer notifications - -Use "Show Original" in Gmail to get the actual email source, including encoding artifacts. Clean the text—the parser needs to handle the clean format. - -Use this function to clean the html from email text. - -```python -from bs4 import BeautifulSoup -def remove_html_tags(html_doc: str) -> str: - soup = BeautifulSoup(html_doc, "html.parser") - return soup.get_text() -``` - - -#### 2. Create Test Files - -Create a directory under `tests/banks/` for your bank: - -```bash -mkdir -p tests/banks/your_bank_name -``` - -Add test files for each transaction type: -- `purchase_clp.txt` - Purchase notification body -- `purchase_subject.txt` - Purchase email subject -- `withdrawal.txt` - ATM withdrawal body -- `withdrawal_subject.txt` - ATM withdrawal subject -- `transference.txt` - Transfer notification body -- `transference_subject.txt` - Transfer email subject - -#### 3. Write Regex Patterns - -Add your bank's patterns to `src/parsers/regex.json`: - -```json -{ - "your_bank_name": { - "subject": { - "exclusions": ["internal transfer", "between accounts"], - "purchase": "purchase confirmation", - "withdrawal": "atm withdrawal", - "transference": "transfer notification" - }, - "body": { - "amountPurchase": "Amount\\s+\\$([\\d.,]+)", - "amountTransference": "Transfer\\s+amount\\s+\\$([\\d.,]+)", - "amountWithdrawal": "Withdrawal\\s+\\$([\\d.,]+)", - "date": "\\d{1,2}[-/]\\d{1,2}[-/]\\d{4}", - "commerce": "Merchant:\\s+(.+?)\\s+Date", - "transferenceRecipient": "Recipient:\\s+(.+?)\\s+Account" - } - } -} -``` - -**Pattern Guidelines**: -- **Subject patterns**: Lowercase text fragments to match in email subjects -- **Exclusions**: Subject patterns to ignore (e.g., internal transfers) -- **Body patterns**: Regex to capture specific fields from email body -- Use `\\s+` for flexible whitespace matching -- Capture groups `(...)` extract the actual value -- Handle encoded characters if present (e.g., `=C3=B1` for `ñ`) -- Account for separators like `>`, tabs (`=09`), or HTML artifacts - -**Common Pitfalls**: -- Emails often have quoted-printable encoding (`=C3=A9` for accented chars) -- HTML emails get stripped but may have odd spacing artifacts -- Line breaks might be `> ` or `\n` depending on email format -- Currency amounts may use `.` or `,` as thousands separators - -#### 4. Write Tests - -Add parametrized test cases in `tests/test_parse.py`: - -```python -@pytest.mark.parametrize( - ("bank", "amount", "commerce", "cat"), - [ - ("banco_chile", 38844, "STA ISABEL JM CAR", ExpenseCategory.FOOD), - ("santander", 68885, "Entel pcs", ExpenseCategory.SERVICES), - ("your_bank_name", 15000, "COFFEE SHOP", ExpenseCategory.FOOD), - ], -) -def test_amount_data_clp(bank, amount, commerce, cat): - # Test implementation - ... -``` - -#### 5. Run Tests - -```bash -pytest tests/test_parse.py -v -``` - -Fix regex patterns until all tests pass. Common issues: -- **Commerce not captured**: Adjust the pattern to match the exact structure -- **Amount wrong format**: Handle thousands separators (`.` vs `,`) -- **Date format mismatch**: Update date regex for DD/MM/YYYY vs DD-MM-YYYY -- **Recipient name issues**: Account for ALL CAPS vs Mixed Case - -#### 6. Debug Failed Matches - -If regex doesn't match, dump the actual string being parsed: - -```python -with open(f"tests/banks/{bank}/purchase_clp.txt", "r") as f: - content = f.read() - print(repr(content)) # Shows exact bytes, whitespace, encoding -``` - -Compare the `repr()` output against your regex pattern character by character. - -### Adding New Transaction Categories - -Categories are defined in `src/parsers/categories.json` and mapped in `src/parsers/base.py`. - -#### 1. Edit Category Keywords - -Add or modify categories in `src/parsers/categories.json`: - -```json -{ - "food": ["grocery", "restaurant", "cafe", "market"], - "transport": ["uber", "taxi", "metro", "gas station"], - "your_new_category": ["keyword1", "keyword2", "keyword3"] -} -``` - -#### 2. Update ExpenseCategory Enum - -Add your category to `src/parsers/base.py`: - -```python -class ExpenseCategory(str, Enum): - FOOD = "food" - TRANSPORT = "transport" - YOUR_NEW_CATEGORY = "your_new_category" -``` - -#### 3. Test Category Detection - -Categories are detected by matching merchant names against keywords. Test with: - -```python -from parsers.expense import Expense - -expense = Expense("100", "COFFEE SHOP", datetime.now()) -assert expense.category == ExpenseCategory.FOOD +tests/ # Test suite + sample bank emails (tests/banks/) +docs/ # Contributor and operator documentation ``` -### Debugging Email Parsing Issues - -Common problems and solutions: - -**Emails not being fetched**: -- Check Celery logs: `docker compose logs celery_worker` -- Verify Gmail API scope includes read access -- Check Google OAuth token hasn't expired - -**Regex not matching**: -- Email body might have HTML artifacts—check `remove_html_tags()` function -- Use `repr()` to see exact string format including hidden characters -- Test regex patterns at [regex101.com](https://regex101.com) - -**Wrong amounts extracted**: -- Check for multiple amounts in email (bill amount vs. paid amount) -- Verify thousands separator handling (`.` vs `,`) -- Some banks show amounts in multiple places—ensure you're capturing the right one - -**Categories wrong**: -- Add more specific keywords to `categories.json` -- Keywords are matched case-insensitive and after normalization -- Check keyword priority—more specific terms should come first - -### Development Workflow - -1. **Get raw email sample** (Show Original in Gmail) -2. **Create test files** with exact email content -3. **Write regex patterns** in `regex.json` -4. **Run tests** and iterate on patterns -5. **Debug with `repr()`** to see exact format -6. **Validate in production** with small date range first - -### Performance Considerations - -- Celery tasks fetch emails in batches (configurable in `tasks/email.py`) -- Rate limits apply to Gmail API (quota: 1 billion requests/day) -- Database queries are optimized with proper indexes -- Dashboard caches data for 5 minutes to reduce DB load - ---- - -## Security +## Roadmap -- No email content is saved—only extracted transaction data -- All sensitive tokens and credentials are stored securely in the database -- OAuth2 state is managed with Redis for CSRF protection -- Passwords are hashed using strong algorithms -- JWT tokens are used for authentication +- **Outbound webhooks**: subscribe to events (`transaction.created`, transfers, ...) and trigger your own automations. +- **Internationalization**: English-canonical labels with per-locale overrides; per-user timezone and currency handling. +- **Multi-bank per user** with sender-based parser dispatch. ---- +See [plans/](plans/) for the detailed phase documents. -## License +## Contributing -MIT License +Contributions are very welcome -- bank parsers most of all. Start with [CONTRIBUTING.md](CONTRIBUTING.md). Please also read the [Code of Conduct](CODE_OF_CONDUCT.md). Security issues: see [SECURITY.md](SECURITY.md). ---- +Deployment/CI notes for operators of the reference instance are in [docs/deployment.md](docs/deployment.md). ## Acknowledgements -- [Dash](https://plotly.com/dash/) - [FastAPI](https://fastapi.tiangolo.com/) - [SQLModel](https://sqlmodel.tiangolo.com/) -- [Celery](https://docs.celeryq.dev/) -- [Google API Python Client](https://github.com/googleapis/google-api-python-client) +- [React Router](https://reactrouter.com/) +- [Cloudflare Email Routing](https://developers.cloudflare.com/email-routing/) diff --git a/docs/adding-a-bank.md b/docs/adding-a-bank.md new file mode 100644 index 0000000..4e394e6 --- /dev/null +++ b/docs/adding-a-bank.md @@ -0,0 +1,148 @@ +# Adding a bank + +Bank parsers in Finitum are **data-driven**: every bank is a JSON block in [`src/parsers/regex.json`](../src/parsers/regex.json), interpreted by the shared engine in [`src/parsers/parser.py`](../src/parsers/parser.py). Adding a bank requires no engine code -- just regexes and test fixtures. The API (`GET /banks`) and the web UI discover new banks automatically from `regex.json`. + +## 1. Collect sample emails + +Get raw samples of your bank's notification emails for each type Finitum understands: + +- **Purchase** (card payment) +- **Withdrawal** (ATM) +- **Transference** (money transfer) + +In Gmail use "Show Original" to see the actual source, including encoding artifacts. The parser receives the body after HTML stripping, so to see exactly what your regexes will run against, strip it the same way: + +```python +from bs4 import BeautifulSoup + +def remove_html_tags(html_doc: str) -> str: + soup = BeautifulSoup(html_doc, "html.parser") + return soup.get_text() +``` + +**Sanitize your samples before committing them**: replace real names, account numbers, national ids, and card digits with fake values that keep the same shape (same length/format), so the regexes still match. + +## 2. Create test fixtures + +Create a directory under `tests/banks/` for your bank: + +```bash +mkdir -p tests/banks/your_bank_name +``` + +Add plain-text files per transaction type (body + subject): + +- `purchase_clp.txt` / `purchase_subject.txt` +- `withdrawal.txt` / `withdrawal_subject.txt` +- `transference.txt` / `transference_subject.txt` + +(The `_clp` suffix is historical -- use it for your local-currency purchase sample. Look at `tests/banks/banco_chile/` for a complete example.) + +## 3. Write the patterns + +Add your bank to `src/parsers/regex.json`: + +```json +{ + "your_bank_name": { + "remitents": ["notifications@yourbank.com"], + "subject": { + "exclusions": ["between my accounts"], + "purchase": "purchase confirmation", + "withdrawal": "atm withdrawal", + "transference": "transfer notification" + }, + "body": { + "amountPurchase": "Amount\\s+\\$([\\d.,]+)", + "amountTransference": "Transfer\\s+amount\\s+\\$([\\d.,]+)", + "amountWithdrawal": "Withdrawal\\s+\\$([\\d.,]+)", + "date": "\\d{1,2}[-/]\\d{1,2}[-/]\\d{4}", + "commerce": "Merchant:\\s+(.+?)\\s+Date", + "transferenceRecipient": "Recipient:\\s+(.+?)\\s+Account", + "transferenceMatches": "" + } + } +} +``` + +Field reference: + +| Field | Meaning | +|-------|---------| +| `remitents` | Sender addresses your bank uses. Emails from other senders are ignored, and the web UI builds the Gmail forwarding filter (`from:(...)`) from this list. | +| `subject.purchase` / `withdrawal` / `transference` | Lowercase regex fragments matched against the subject to classify the email. | +| `subject.exclusions` | Subjects to skip entirely (e.g. transfers between your own accounts). | +| `body.amount*` | Regex with one capture group extracting the amount for each type. | +| `body.commerce` | Capture group for the merchant name (purchases). | +| `body.date` | Regex matching the transaction date inside the body. | +| `body.transferenceRecipient` | Capture group for the transfer recipient's name. | +| `body.transferenceMatches` | Extra pattern a transfer body must contain (use `""` if not needed). | + +All `body` keys are required -- `BankPatterns.from_json` validates them and raises if one is missing. Use `""` for patterns your bank doesn't need. + +Pattern tips: + +- Use `\\s+` for flexible whitespace; capture groups `(...)` extract the value. +- Emails often carry quoted-printable artifacts (`=C3=B1` for `ñ`, `=09` for tabs) and odd spacing left over from HTML stripping -- look at the fixture with `repr()` to see the exact characters. +- Amounts may use `.` or `,` as thousands separators depending on locale. +- Test candidate regexes at [regex101.com](https://regex101.com) against your fixture content. + +## 4. Write tests + +Add your bank to the parametrized cases in [`tests/test_parse.py`](../tests/test_parse.py): + +```python +@pytest.mark.parametrize( + ("bank", "amount", "commerce", "cat", "test_remitent"), + [ + ("banco_chile", 38844, "STA ISABEL JM CAR", ExpenseCategory.FOOD, banco_chile_remitent), + ("santander", 68885, "Entel pcs", ExpenseCategory.SERVICES, santander_remitent), + ("your_bank_name", 15000, "COFFEE SHOP", ExpenseCategory.FOOD, "notifications@yourbank.com"), + ], +) +``` + +Then run: + +```bash +pytest tests/test_parse.py -v +``` + +Tests build a `Message` directly from your fixture files and run the parser -- no network, no email account, no running services needed. Add equivalent cases for withdrawals and transfers. + +## 5. Debug failed matches + +Dump the exact string being parsed and compare it character by character against your regex: + +```python +with open("tests/banks/your_bank_name/purchase_clp.txt", encoding="utf-8") as f: + print(repr(f.read())) # shows exact bytes, whitespace, encoding +``` + +There is also a small CLI to run the parser against a raw email file without the test suite (run from the `src/` directory): + +```bash +cd src +uv run python -m parsers.test_parse your_bank_name path/to/email.txt --raw +``` + +Common issues: + +- **Commerce not captured**: adjust for the exact separators around the merchant name. +- **Wrong amount**: check thousands separators and whether the email contains multiple amounts (billed vs. paid). +- **Date mismatch**: `DD/MM/YYYY` vs `DD-MM-YYYY`, optional time component. +- **Nothing classified**: the subject patterns are matched lowercase -- check `subject.*` against the actual subject fixture. + +## Categories + +Merchant names are matched against keyword lists to auto-assign a category: + +- Keywords live in [`categories.json`](../categories.json) (repo root), keyed by parser category. +- The category registry (slugs, English/Spanish labels) is in [`src/category_catalog.py`](../src/category_catalog.py); Spanish label overrides live in [`category_labels.es.json`](../category_labels.es.json). +- The closed set of built-in categories is the `ExpenseCategory` enum in [`src/parsers/base.py`](../src/parsers/base.py). + +To improve detection for your bank's merchants, add keywords to `categories.json`. Matching is case-insensitive and accent-normalized, longest match first; unmatched merchants fall back to `GENERAL`. (Users can also define custom categories at runtime, so built-in keywords only need to cover the common cases.) + +## Submitting your parser + +Open a PR with the `regex.json` block, the sanitized fixtures, and the test cases. If you can't finish the parser yourself, open an [Add a bank issue](https://github.com/richardhapb/finitum/issues/new?template=add-a-bank.yml) with sanitized samples and someone can pick it up. diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..81bcee1 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,31 @@ +# Deployment (reference instance) + +Operator notes for the CI/CD pipeline and the reference deployment ([finitum.app](https://finitum.app), a VPS). Self-hosters don't need any of this -- `docker compose up --build` per the [README](../README.md) is enough; this doc describes how the hosted instance ships. + +## Pipeline + +Every push to `main` runs [.github/workflows/ci.yml](../.github/workflows/ci.yml): + +1. **lint-test** -- `ruff check` + `pytest` (also runs on pull requests, as a gate). +2. **build-push** -- builds a locked image (`uv.lock` + `uv sync --frozen`) and pushes `ghcr.io/richardhapb/finitum-api` tagged `latest` and `sha-`. +3. **deploy** -- SSHes into the VPS and runs: + + ```bash + cd ~/finitum + git pull --ff-only + docker compose -f docker-compose.yml -f docker-compose.prod.yml pull + docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d + docker image prune -f + ``` + +The image is self-contained (code + deps + alembic migrations baked in); `git pull` on the server only refreshes the compose/env files. Prod uses `volumes: !reset []` so no host source is mounted -- the server runs exactly what CI built. + +## One-time setup + +- **Make the package public**: GitHub → Packages → `finitum-api` → Package settings → Change visibility → **Public**. The server then pulls with no auth. +- **Add repository secrets** (Settings → Secrets and variables → Actions): `HOST_ADDRESS`, `HOST_USER`, and `HOST_SSH_KEY` (a private key whose public half is in the server's `~/.ssh/authorized_keys`). +- **On the server**: clone the repo to `~/finitum` on `main`, populate `.env.prod` (including `CREDENTIALS_ENCRYPTION_KEY` -- back it up, losing it makes stored Google tokens unrecoverable), and confirm `docker compose version` ≥ 2.24 (required for `!reset`). + +## Email worker + +Inbound email relies on the Cloudflare Email Routing worker in [infra/email-worker/](../infra/email-worker/) deployed on the ingest domain, sharing `INGEST_WEBHOOK_SECRET` with the API. See that directory's README for setup. diff --git a/web/README.md b/web/README.md index 166687f..af38853 100644 --- a/web/README.md +++ b/web/README.md @@ -1,62 +1,49 @@ # Finitum Web Frontend -Finance tracker frontend built with React + TypeScript + Tailwind. +Frontend for [Finitum](../README.md), built with React Router v7 (framework mode) + TypeScript + Tailwind, running on Bun. ## Setup ```bash -# Install dependencies bun install +bun run dev # dev server (expects the API at VITE_API_URL, default http://localhost:9090) +``` -# Create .env file -cp .env.example .env +Other scripts: -# Run dev server -bun run dev +```bash +bun run build # production build +bun run start # serve the production build +bun run typecheck # react-router typegen + tsc +bun run lint # eslint ``` ## Stack -- **React 19** - UI framework -- **TypeScript** - Type safety -- **Vite** - Build tool (Bun runtime) -- **Tailwind CSS** - Styling -- **React Router** - Routing -- **TanStack Query** - Server state management -- **React Hook Form + Zod** - Form validation -- **Axios** - HTTP client +- **React 19** + **React Router v7** (framework mode, file routes) +- **TypeScript**, **Tailwind CSS** +- **TanStack Query** for server state +- **React Hook Form + Zod** for forms +- **Axios** API client with JWT handling -## Project Structure +## Structure ``` -src/ -├── components/ -│ ├── auth/ # Login/Signup forms -│ ├── expenses/ # Expense list and form -│ └── layout/ # Navbar, ProtectedRoute -├── pages/ # Page components -├── lib/ # API client, query config -└── types/ # TypeScript types +app/ +├── routes/ # File routes: home, guide, login, signup, dashboard, profile, privacy, terms +├── components/ # Shared components (auth forms, layout, expenses) +├── lib/ # API client (api.ts), query client, category helpers +├── types/ # TypeScript types +└── root.tsx # App shell ``` -## Key Features - -- JWT authentication with refresh token support -- Protected routes -- Expense CRUD operations -- Form validation with Zod -- Automatic API error handling -- Google OAuth integration +Key routes: -## Backend Integration +- `home.tsx` -- public landing page +- `guide.tsx` -- how-it-works onboarding (forwarding setup overview) +- `profile.tsx` -- the email-forwarding setup flow (ingest address, Gmail confirmation capture, filter generator) and account settings +- `dashboard.tsx` -- transactions and analytics -The frontend expects the FastAPI backend running at `http://localhost:9090` (configurable via `VITE_API_URL`). +## Backend integration -Required backend endpoints: -- `POST /signup` - User registration -- `POST /signin` - User login -- `POST /refresh` - Token refresh (YOU NEED TO ADD THIS) -- `GET /me` - Get current user -- `GET /expenses` - List expenses -- `POST /expenses` - Create expense -- `GET /google-authorize` - Google OAuth +The frontend talks to the FastAPI backend (`VITE_API_URL`). Auth uses JWT (signup/signin) with optional Google sign-in (login only -- no Gmail access). The bank list and forwarding-filter senders come from `GET /banks`, which is derived from the parser definitions in `../src/parsers/regex.json`. From 05a16b495ccffd6a24541ee23e06a62362697a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Pe=C3=B1a?= Date: Tue, 21 Jul 2026 01:43:41 -0400 Subject: [PATCH 3/3] feat(web): reposition landing, guide, and policies around forwarding + OSS --- terms/privacy.txt | 36 ++++--- terms/terms.txt | 4 + web/app/root.tsx | 14 +-- web/app/routes/guide.tsx | 130 ++++++++++++++++++------- web/app/routes/home.tsx | 191 ++++++++++++++++++++++++++----------- web/app/routes/privacy.tsx | 51 +++++++--- web/app/routes/terms.tsx | 13 +++ 7 files changed, 311 insertions(+), 128 deletions(-) diff --git a/terms/privacy.txt b/terms/privacy.txt index 9341a9f..0c5bc3b 100644 --- a/terms/privacy.txt +++ b/terms/privacy.txt @@ -2,21 +2,25 @@ Privacy Policy Finitum respects your privacy. -Finitum only accesses Gmail data with the user's explicit consent, and -only for the purpose of extracting financial transaction information -from bank notification emails. +Finitum does not access your email account or your bank. It only +processes the emails you explicitly forward to your personal Finitum +address, for the sole purpose of extracting financial transaction +information from bank notification emails. You control what is +forwarded and can stop at any time by removing the forwarding rule. No raw email content is permanently stored. Only extracted transaction data (amount, merchant, date, category) is saved to provide financial analytics. +Google sign-in is optional and used for authentication only. Finitum +does not request access to Gmail or to any email content. + Data Retention and Deletion -Google user data, including Gmail message content accessed during -transaction extraction, is processed in real time and is not stored -beyond the immediate processing session. Only the derived transaction -data (amount, merchant, date, category) is retained in your account -for as long as your account remains active. +Forwarded emails are processed in real time and are not stored beyond +the immediate processing session. Only the derived transaction data +(amount, merchant, date, category) is retained in your account for as +long as your account remains active. You may request deletion of all your data at any time by contacting finitumapp@gmail.com. Upon receiving a deletion request, all stored @@ -24,15 +28,19 @@ transaction data and associated account information will be permanently deleted within 30 days. You may also delete your account directly within the application, which will immediately remove all stored data. -OAuth tokens granted by Google are stored securely and are revoked -and deleted when you disconnect your Google account or delete your -Finitum account. +Tokens granted by Google for sign-in are stored securely and are +revoked and deleted when you disconnect your Google account or delete +your Finitum account. Finitum does not sell, share, or use user data for advertising purposes. -Authentication is handled securely using Google OAuth2 and JWT tokens. -Sensitive credentials and tokens are stored securely. +Authentication is handled securely using JWT tokens, with optional +Google OAuth2 sign-in. Sensitive credentials and tokens are stored +securely. + +Finitum is open-source software (MIT license); you can inspect exactly +how your data is handled, or self-host your own instance, at +https://github.com/richardhapb/finitum. If you have questions about this policy, contact: finitumapp@gmail.com - diff --git a/terms/terms.txt b/terms/terms.txt index a9a372d..a3e8e0a 100644 --- a/terms/terms.txt +++ b/terms/terms.txt @@ -11,4 +11,8 @@ the information provided by the application. By using the service, you agree to the collection and use of data as described in the Privacy Policy. +Finitum is open-source software released under the MIT license: +https://github.com/richardhapb/finitum. You are free to self-host +your own instance. + Contact: finitumapp@gmail.com diff --git a/web/app/root.tsx b/web/app/root.tsx index d0b6274..18473e6 100644 --- a/web/app/root.tsx +++ b/web/app/root.tsx @@ -19,11 +19,11 @@ export const links: LinksFunction = () => [ export const meta: MetaFunction = () => [ { charSet: "utf-8" }, { name: "viewport", content: "width=device-width, initial-scale=1.0" }, - { title: "Finitum - Personal Finance Manager | Automatic Expense Tracking" }, + { title: "Finitum - Open-Source Finance Tracker | Automatic Expense Tracking" }, { name: "description", content: - "Finitum is a personal finance manager that automatically tracks your expenses by parsing bank notification emails from your Gmail. View spending analytics, categorize transactions, and take control of your finances with privacy-first design.", + "Finitum is an open-source personal finance tracker that automatically parses the notification emails you forward from your bank. No bank credentials, no inbox access. View spending analytics, categorize transactions, and self-host if you want.", }, { name: "author", content: "Finitum" }, { name: "application-name", content: "Finitum" }, @@ -31,24 +31,24 @@ export const meta: MetaFunction = () => [ { property: "og:url", content: "https://finitum.app/" }, { property: "og:title", - content: "Finitum - Personal Finance Manager | Automatic Expense Tracking", + content: "Finitum - Open-Source Finance Tracker | Automatic Expense Tracking", }, { property: "og:description", content: - "Finitum is a personal finance manager that automatically tracks your expenses by parsing bank notification emails from your Gmail. View spending analytics and take control of your finances.", + "Open-source personal finance tracker: forward your bank's notification emails and get automatic expense tracking with spending analytics. No bank credentials, no inbox access.", }, { property: "og:site_name", content: "Finitum" }, { property: "twitter:card", content: "summary_large_image" }, { property: "twitter:url", content: "https://finitum.app/" }, { property: "twitter:title", - content: "Finitum - Personal Finance Manager | Automatic Expense Tracking", + content: "Finitum - Open-Source Finance Tracker | Automatic Expense Tracking", }, { property: "twitter:description", content: - "Finitum is a personal finance manager that automatically tracks your expenses by parsing bank notification emails from your Gmail. View spending analytics and take control of your finances.", + "Open-source personal finance tracker: forward your bank's notification emails and get automatic expense tracking with spending analytics. No bank credentials, no inbox access.", }, { name: "google-site-verification", @@ -58,7 +58,7 @@ export const meta: MetaFunction = () => [ export function Layout({ children }: { children: React.ReactNode }) { return ( - + diff --git a/web/app/routes/guide.tsx b/web/app/routes/guide.tsx index 960c135..4643f28 100644 --- a/web/app/routes/guide.tsx +++ b/web/app/routes/guide.tsx @@ -1,5 +1,7 @@ import { Link } from "react-router"; +const GITHUB_URL = "https://github.com/richardhapb/finitum"; + export default function GuidePage() { return (
@@ -24,9 +26,11 @@ export default function GuidePage() { {/* Header */}
-

Getting Started

+

How Finitum Works

- Follow these steps to start tracking your expenses automatically. + Your bank already emails you every transaction. Set up a one-time + forwarding rule and Finitum does the rest -- no bank credentials, no + inbox access.

@@ -42,7 +46,9 @@ export default function GuidePage() {

Create an Account

- Sign up with your email and password, or use Google to create an account instantly. + Sign up with your email and password, or use Google sign-in. + Either way, Finitum never accesses your email account -- + Google is used for login only.

-

Connect Your Gmail

+

+ Get Your Forwarding Address +

- Link your Google account to allow Finitum to read your bank notification emails. - We only access emails from financial institutions to extract transaction data. + In your profile, select your bank and copy your personal + Finitum address (it looks like{" "} + u-a1b2c3@finitum.app). + This address is unique to you -- emails sent to it land in + your account and nowhere else. +

+
+
+
+ + {/* Step 3 */} +
+
+
+ 3 +
+
+

+ Set Up the Forwarding Rule +

+

+ In Gmail, add your Finitum address as a forwarding address and + create a filter that forwards only your bank's notification + emails. Your profile page walks you through it: Gmail's + confirmation request is captured automatically, and the filter + for your bank's senders is generated for you -- just copy and + paste.

- What we access: + You stay in control:
    -
  • Bank transaction notification emails
  • -
  • Payment confirmations from supported banks
  • +
  • Only emails matching your filter are forwarded
  • +
  • Finitum never connects to your inbox or your bank
  • +
  • Remove the rule at any time to stop instantly
- {/* Step 3 */} + {/* Step 4 */}
- 3 + 4
-

Automatic Import

+

+ Transactions Appear Automatically +

- Once connected, Finitum automatically scans your inbox for transaction emails - and extracts the relevant data: amount, merchant, date, and category. + From then on, every bank notification is parsed the moment it + arrives. The raw email is processed in real time and never + stored -- only the extracted transaction data.

Extracted data: @@ -102,17 +139,19 @@ export default function GuidePage() {
- {/* Step 4 */} + {/* Step 5 */}
- 4 + 5
-

View Your Dashboard

+

+ Explore Your Dashboard +

- Head to your dashboard to see all your expenses in one place. View charts, - filter by category, and understand your spending patterns. + See all your expenses in one place, and add manual entries for + cash or anything not covered by email.

  • @@ -132,20 +171,34 @@ export default function GuidePage() {
- {/* Step 5 */} -
-
-
- 5 -
-
-

Add Manual Expenses

-

- For cash transactions or expenses not captured by email, you can manually - add entries directly from your dashboard. -

-
-
+ {/* Bank not supported */} +
+

+ Bank not supported yet? +

+

+ Finitum is open source and bank parsers are community-built -- + adding a bank takes a JSON definition and a few sample emails, no + core code.{" "} + + Read the guide + {" "} + or{" "} + + request your bank on GitHub + + . +

@@ -166,9 +219,14 @@ export default function GuidePage() {
Finitum
- - Guide - + + GitHub + Privacy Policy diff --git a/web/app/routes/home.tsx b/web/app/routes/home.tsx index 3b47ed0..5b22cff 100644 --- a/web/app/routes/home.tsx +++ b/web/app/routes/home.tsx @@ -1,5 +1,8 @@ import { Link } from "react-router"; -import { authApi } from "../lib/api"; +import { useQuery } from "@tanstack/react-query"; +import { authApi, banksApi } from "../lib/api"; + +const GITHUB_URL = "https://github.com/richardhapb/finitum"; const GoogleIcon = () => ( @@ -22,11 +25,22 @@ const GoogleIcon = () => ( ); +const GitHubIcon = () => ( + + + +); + export default function LandingPage() { const handleGoogleLogin = () => { window.location.href = authApi.getGoogleAuthUrl(); }; + const { data: banks = [] } = useQuery({ + queryKey: ["banks"], + queryFn: banksApi.getAll, + }); + return (
{/* Navigation */} @@ -34,10 +48,19 @@ export default function LandingPage() {
Finitum - Personal Finance Manager + Open-Source Finance Tracker
-
+
+ + + GitHub +

Finitum

- Personal Finance Manager - Automatic Expense Tracking + Your bank already emails you every transaction. Finitum turns those + emails into your finance dashboard.

- {/* Google OAuth Compliance Summary (CRITICAL) */} -
-

- Finitum is a personal finance application that uses - Google OAuth to read bank transaction notification emails from a - user's Gmail account. -

-

- Email access is strictly limited to messages that match predefined - sender domains and subject patterns associated with financial - institutions. The application extracts transaction data such as - amount, date, and merchant. -

-

- Finitum does not store email bodies, attachments, or personal - messages. Only extracted transaction metadata is stored securely. -

-
- -

- Track Your Expenses Automatically -

-

- Finitum is a personal finance application that connects to your Gmail - and reads bank transaction notification emails to extract structured - transaction data. No manual entry required. + Forward your bank's notification emails to your personal Finitum + address and every purchase, withdrawal, and transfer is parsed + automatically -- amount, merchant, date, and category. No bank + credentials, no inbox access, no manual entry.

@@ -95,13 +97,13 @@ export default function LandingPage() { to="/guide" className="px-8 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg text-lg font-medium transition-colors" > - Get Started + How It Works - Login + Get Started
@@ -112,6 +114,20 @@ export default function LandingPage() { Continue with Google +

+ Google sign-in is optional and used for login only. Finitum never + accesses your inbox. +

+
+ + + {/* How it works, in one line */} +
+
+ + Bank email → your forwarding rule → u-…@finitum.app → parser → + dashboard 📊 +
@@ -120,52 +136,106 @@ export default function LandingPage() {
📧
-

Gmail Integration

+

Email Forwarding

- Reads bank transaction notification emails using secure Google - OAuth access. + A one-time forwarding rule sends your bank notifications to your + personal Finitum address. You choose exactly what gets forwarded + -- Finitum never reads your inbox.

-
📊
-

Visual Analytics

+
🌍
+

Open Source

- Analyze spending patterns with charts and categorized summaries. + MIT-licensed and self-hostable. Bank parsers are community-built: + adding your bank is a JSON file and a couple of tests -- no core + code required.

-
🔒
-

Privacy First

+
📊
+

Visual Analytics

- No raw emails stored. Only extracted transaction metadata is saved. + Automatic categories, spending trends, and a full transaction + history -- built from data extracted the moment each email + arrives.

+ {/* Supported banks */} +
+
+

Supported Banks

+ {banks.length > 0 ? ( +
+ {banks.map((b) => ( + + {b.name} + + ))} +
+ ) : ( +

Loading bank list…

+ )} +

+ Missing yours? Parsers are data-driven and community-contributed --{" "} + + add your bank + {" "} + with a JSON block and a few sample emails, or{" "} + + request it on GitHub + + . +

+
+
+ {/* About Section */} -
+
-

About Finitum

+

+ Privacy by Design +

- Finitum is a personal finance - management application designed to automatically track expenses - without manual input. + Finitum works only with the emails you explicitly forward to it. + There is no connection to your bank and no access to your email + account.

- The application uses Google OAuth 2.0 to read bank transaction - notification emails sent by recognized financial institutions. It - extracts structured transaction data including amount, date, and - merchant, and presents it through an analytics dashboard. + Forwarded emails are processed in real time: transaction data + (amount, merchant, date, category) is extracted and stored; the + raw email content is not.

- Privacy: Email access is - limited to predefined sender domains and subject patterns. Finitum - does not store email bodies, attachments, or personal - communications. + Don't want to trust a hosted service? Run it yourself -- the{" "} + + entire stack is open source + {" "} + and ships with a Docker Compose setup.

@@ -177,9 +247,17 @@ export default function LandingPage() {
Finitum | - Personal Finance Manager + Open-Source Finance Tracker
+ + GitHub + Guide @@ -195,4 +273,3 @@ export default function LandingPage() {
); } - diff --git a/web/app/routes/privacy.tsx b/web/app/routes/privacy.tsx index ad98f2e..3a53036 100644 --- a/web/app/routes/privacy.tsx +++ b/web/app/routes/privacy.tsx @@ -2,7 +2,7 @@ import type { MetaFunction } from "react-router"; export const meta: MetaFunction = () => [ { title: "Privacy Policy - Finitum" }, - { name: "description", content: "Finitum privacy policy. Learn how Finitum handles your Gmail data and protects your privacy." }, + { name: "description", content: "Finitum privacy policy. Learn how Finitum handles the bank emails you forward and protects your privacy." }, ]; export default function Privacy() { @@ -14,9 +14,12 @@ export default function Privacy() {

Finitum respects your privacy.

- Finitum only accesses Gmail data with the user's explicit consent, and - only for the purpose of extracting financial transaction information - from bank notification emails. + Finitum does not access your email account or your bank. It only + processes the emails you explicitly forward to your personal + Finitum address, for the sole purpose of extracting financial + transaction information from bank notification emails. You control + what is forwarded and can stop at any time by removing the + forwarding rule.

@@ -25,14 +28,18 @@ export default function Privacy() { is saved to provide financial analytics.

+

+ Google sign-in is optional and used for authentication only. + Finitum does not request access to Gmail or to any email content. +

+

Data Retention and Deletion

- Google user data, including Gmail message content accessed during - transaction extraction, is processed in real time and is not stored - beyond the immediate processing session. Only the derived transaction - data (amount, merchant, date, category) is retained in your account - for as long as your account remains active. + Forwarded emails are processed in real time and are not stored + beyond the immediate processing session. Only the derived + transaction data (amount, merchant, date, category) is retained in + your account for as long as your account remains active.

@@ -47,16 +54,32 @@ export default function Privacy() {

- OAuth tokens granted by Google are stored securely and are revoked - and deleted when you disconnect your Google account or delete your - Finitum account. + Tokens granted by Google for sign-in are stored securely and are + revoked and deleted when you disconnect your Google account or + delete your Finitum account.

Finitum does not sell, share, or use user data for advertising purposes.

- Authentication is handled securely using Google OAuth2 and JWT tokens. - Sensitive credentials and tokens are stored securely. + Authentication is handled securely using JWT tokens, with optional + Google OAuth2 sign-in. Sensitive credentials and tokens are stored + securely. +

+ +

+ Finitum is open-source software (MIT license); you can inspect + exactly how your data is handled, or self-host your own instance, + at{" "} + + github.com/richardhapb/finitum + + .

diff --git a/web/app/routes/terms.tsx b/web/app/routes/terms.tsx index ed30502..6e51341 100644 --- a/web/app/routes/terms.tsx +++ b/web/app/routes/terms.tsx @@ -28,6 +28,19 @@ export default function Terms() { as described in the Privacy Policy.

+

+ Finitum is open-source software released under the MIT license:{" "} + + github.com/richardhapb/finitum + + . You are free to self-host your own instance. +

+

Contact:{" "}