v1.2.0
Added
CHANGELOG.mdas the source of truth for release notes, automatically
published into each GitHub Release body by the release workflow.- Cash book — a new
/cashpage to hand-enter cash and other off-bank
spending (or income) so it lands in the same ledger as your imports: manual
entries categorise, recur-detect, and count toward your month exactly like
imported ones, because they flow through the canonical recording pipeline
(a synthetic per-user Cash account + a "manual" source). Manual rows are
user-owned and deletable (unlike immutable imported rows). The amount field
accepts plain and Dutch-grouped formats; the chosen category is validated as
your own; and two genuinely-distinct identical same-day entries both record
(the recorder bumps the booking time on a fingerprint collision rather than
dropping the second). - Net-worth roll-up — the dashboard leads with one figure across all
accounts (assets minus liabilities), with an expandable per-account breakdown.
Each account's balance is the same anchor the forecast uses as "today's
balance", already sign-correct (a credit card counts as what you owe). Sums
EUR-denominated accounts into the headline figure and flags when a non-EUR
account is left out (no balance FX conversion yet); the internal
paypal_fundingrouting account is excluded. Backed by a new
NetWorthQueryPublic service over the previously-internal balance resolver. - Month-over-month spending — the dashboard shows a "This month vs last"
card: the current period's total spend with the signed change against the
previous period, and the categories that moved the most (each with its delta).
Spending more reads in rose, less in emerald. Uses the same EUR-settled outflow
definition as the rest of the ledger, so the figures reconcile; renders nothing
until there is a prior period to compare. - Sidebar count badges — the nav items (Transactions, Recurring,
Counterparties, Drift alerts, Budgets, Subscriptions, Imports, Receipts) now
show how many items each holds. All counts come from a single per-user cached
payload (NavCountsService, short TTL, invalidated on demand) so the sidebar —
which renders on every page — never fans out a pile of COUNT queries per
render. Large counts are compact-formatted (e.g. "3.1k"), and a count of zero
hides the badge to keep the rail calm. - "You could save here" insights — the dashboard shows a "Ways to save"
card that pairs your recurring subscriptions with the most relevant official
link from the support-resource corpus: a cheaper / student / retention plan
where one exists, a cancellation page when a subscription's price has drifted
up, or a gentle review nudge for an ongoing charge. Each suggestion is
dismissible (and stays dismissed), and the whole set is cached per user so the
dashboard never re-runs the resolution on every render. Purely informational —
beatrax surfaces the official link, it never cancels or switches anything. - Recurring ↔ counterparty navigation — a counterparty profile's Recurring
card now lists that merchant's recurring series (each linking to the series
detail), and the recurring series detail page links back to the counterparty
profile. Resolved through the sharedtransactions.counterparty_id, so it is
exact rather than name-matched. - Settings redesign — the Settings page is reorganised from a narrow flat
list into wider, titled cards (Appearance · Preferences · Forecasting ·
Importing · Shared merchant list · Data & backup · Help · Developer), keeping
every existing control and behaviour, with proper padding from the nav. - Encrypted backups — Settings → Data & backup can now download a
passphrase-encrypted snapshot of your whole database (aVACUUM INTOcopy,
encrypted in place and streamed as a.sqlite.encfile), safe to keep on an
external drive or in cloud storage because it is unreadable without the
passphrase. Encryption is quantum-safe by construction: a purely symmetric
scheme (no public-key step, so no Shor exposure) using Argon2id key derivation
and XChaCha20-Poly1305 with a 256-bit key. The plaintext snapshot is locked to
0600 and deleted as soon as it is encrypted; the passphrase never persists.
Settings → Data & backup can also restore from an encrypted backup: upload
the.enc, enter the passphrase, and type the confirmation phrase. The file is
decrypted and integrity-checked first — a wrong passphrase or corrupt backup
throws before anything changes — then a pre-restore snapshot of the current
database is saved and the verified backup is swapped in (reload to see the
restored data). Available on the SQLite (desktop) build; on a server, use
php artisan db:restore(which gates on maintenance mode) instead. - Support-resource profiles — a counterparty profile now shows a "Support &
cancelling" card (merchants) or "Getting help" card (government) with the
official cancel / help / cheaper-plan links, atel:helpline, and — where a
service genuinely supports it — a one-click pre-filled cancellationmailto:.
Backed by a new bundledresources/corpus/support/<country>.yamlcorpus
(researched for ~25 common subscriptions and the main NL agencies; the
cancellation method, e.g. online form / phone / registered letter, is shown in
a note since none of these document email cancellation). Looked up by
word-level brand matching that tolerates legal-entity suffixes ("Netflix
International BV" → Netflix) without false-matching ("Apple" never matches
"Applebee's"; a plain "Albert Heijn" grocery charge never inherits the
"Albert Heijn Premium" cancel card). Link schemes are restricted to http(s)
and the mailto address is injection-guarded. - Subscription Drift Watch — a new
/drift/watchpage ("Subscriptions" in
the sidebar) that ranks your approved subscriptions by how much their price
has crept up since the first charge, each with a baseline → latest figure, a
signed €/% delta, an amount-history sparkline, and an "open alert" badge that
deep-links to the drift alert. The subscription-centric companion to the
alert-centric/driftpage; reuses the recurring occurrence history rather
than adding a new store, and reads the full history so the baseline is the
true first charge. - Category Budgets — a new
/budgetspage (Budgets module) to set a
monthly spending ceiling per expense category and track the current period's
spend against it, with a status-coloured progress bar (under / near / over),
remaining amount, an inline editor, and a period total. Budget writes are
validated against the user's own + global expense categories (a
client-supplied category id can never attach a budget to another user's
category), and the amount field accepts both plain and Dutch grouped formats.
An optional Budgets step is added to the first-run setup wizard so new
users can set a few category budgets during onboarding (or skip it); a step
added after a user already finished onboarding is seeded as skipped, so
finished users are not dropped back into the wizard.
Fixed
- The receipt-conflict resolver ("Use receipt") no longer 500s on a malformed
stored value: a non-JSONincoming_valueis skipped (not applied) while the
pending row is still cleared, matching the read side's tolerance. The demo
seeder now JSON-encodes the conflict values like the production producer. - Server-deployment support alongside the NativePHP desktop build.
config
now defines Postgres, MySQL, and MariaDB connections (SQLite stays the
default, so the desktop build is unchanged), selectable viaDB_CONNECTION- the
DB_*env vars. A new interactivephp artisan beatrax:setupcommand
walks a self-hoster through writing.env(app URL, application key,
database), verifies the connection, and hands off tobeatrax:installin a
fresh process. Adeploy/server/recipe ships a single-container FrankenPHP
image (code + assets baked in) with Postgres, a queue worker, and the
scheduler, and.docs/deployment.mddocuments both the Docker and the
bare-metal (clone-without-Docker) paths, including logging to stderr, the
database queue (no Redis/Horizon on a server), and keeping the Dev Console
off in production.
- the
- Import other European banks' CSV exports via a preset-driven generic CSV
importer. Bundled presets cover N26, Revolut, and ING (Netherlands), each
selectable under a new "Other bank" source in the import wizard. The engine
handles the cross-bank differences — signed vs separate debit/credit columns
vs an "Af/Bij"-style direction indicator, comma-or-dot decimals with thousands
separators, per-row or fixed currency, and varied date formats — matches
columns by a normalised header name (tolerant of minor spelling differences),
skips pending/reverted rows (e.g. RevolutState) instead of aborting, and
rounds sub-cent amounts rather than truncating. Adding another bank is a
data-only change (a new entry inCsvPresetRegistry).
Added
-
Government-agency and bank-fee classification now lives in a regex-capable,
per-country YAML corpus (resources/corpus/<type>/<country>.yaml) instead of
hardcoded Dutch keyword constants. Patterns may be literal substrings or a
regex:body, and the bundled set covers tax / social-security / broadcast-fee
agencies across all 27 EU member states plus the UK, US, Canada, and Ukraine
(e.g. Finanzamt, DGFiP/URSSAF, HMRC, IRS, Canada Revenue Agency, the German
Rundfunkbeitrag). Patterns are collision-safe: risky short acronyms use
regex:\b…\bword boundaries or the agency's full name, and agencies whose
statement descriptors carry only a payment reference (not the agency name)
are deliberately omitted to avoid false positives. -
The internal bank-statement parsers are no longer branded around a single
bank: the generic CAMT.053 and MT940 adapters, their helpers, and the shared
amount parser moved fromInternal/Adapters/Asnto a neutral
Internal/Adapters/Bankingnamespace (Camt053Adapter,Mt940Adapter,
BankAmountParser, …), and the matching Import-module payment-type hinters to
Internal/Parsers/Banking. ASN's own proprietary CSV adapter keeps its name
(it is one specific bank's format, like the new N26/Revolut/ING presets). No
behaviour change; format keys are unaffected. -
The bundled merchant corpus is reorganised into per-country files
(resources/corpus/merchants/<country>.yaml, region inferred from filename)
and expanded to ~600 merchants across all 27 EU member states plus the UK,
US, Canada, and Ukraine — supermarkets, fuel/energy, telecom, streaming,
transport, retail, insurance, and food delivery, with pan-European
subscriptions and payment-facilitator prefixes (PAYPAL *,GOOGLE *,
AMZN MKTP) inmerchants/eu.yaml. Merchant patterns support the same
regex:prefix as the rest of the corpus, used to give collision-prone short
brand tokens (DIA, ICA, NOS, TIM, …) word boundaries so they no longer match
inside ordinary words.
Changed
- User-facing copy and the README now describe beatrax as a generic European
personal-finance tool (any bank that exports CAMT.053, MT940, or CSV, plus
cards and PayPal) rather than an ASN-specific one. Format-specific names and
the genuine ASN-format parsers are unchanged. - Updated dependencies to clear outstanding Dependabot updates: guzzlehttp/guzzle,
google/apiclient-services, nativephp/desktop, larastan, and Pest on the PHP
side; fuse.js, vite, and concurrently on the front-end side. - Renamed the generic statement-format identifiers
asn-camt053→camt053
andasn-mt940→mt940, since CAMT.053 (ISO 20022) and MT940 (SWIFT) are
pan-European standards rather than ASN-specific formats. Existing imports are
migrated automatically; the ASN-specific CSV layout (asn-csv) keeps its name. - The personal-transfer counterparty heuristic now recognises any valid SEPA
IBAN, not just Dutch ones, using proper mod-97 + country-length validation
(viajschaedl/iban-validation). A German, French, or Belgian personal
transfer is now classified as a person rather than falling through to
"unknown". All test/demo fixtures use real, checksum-valid IBANs.
Fixed
- Resolve seven static-analysis findings surfaced by the larastan/phpstan
upgrade (the OAuth secrets store's per-inbox map is now typed with the
array-key it actually uses), and cap phpstan's worker count so the quality
gate runs deterministically in the Docker toolchain. - The Docker dev toolchain now points the Pest/PHPUnit suite at the isolated
sqlite_testingconnection instead of the WAL-configuredsqlite
connection, so the full test suite runs cleanly in the container instead of
failing thousands of tests on aRefreshDatabaseisolation clash.