github John710/finapp v1.4.0

2 hours ago

[1.4.0] - 2026-10-29

Bug Fixes

  • Ship the maintained shoutrrr fork and stop leaking tokens by @John710
    containrrr/shoutrrr 0.8.0 rejects Gotify 3.x application tokens (gtfya.…),
    so every Gotify notification failed. Switch the image to
    nicholas-fedor/shoutrrr 0.22.1 with per-arch sha256 pinned in the
    Dockerfile, and point the settings docs links at its documentation.
    Error messages and logs carried the full target URL, including tokens in
    the path; they now show only scheme://host and a redacted stderr.
  • Keep installment schedules, receipts and paid status consistent by @John710
    Code-review fixes for the debt-installments backend:
    • PUT /debts/:id took a new amount or currency without touching the
      schedule, so the installments stopped adding up to the debt. It now
      accepts installments: { rows } and replaces the schedule in the same
      transaction under the debt row lock; an amount/currency change on a debt
      with a schedule and no new rows is 409 installments_need_update, and any
      paid or skipped installment makes it 409 installments_locked.
    • The "untouched transaction" test ignored attachments, so undoing a
      payment or deleting a debt deleted a transaction with receipts (they
      cascade). Such transactions, and creation bookings with receipts, are now
      kept like hand-edited ones.
    • Undoing an installment payment whose transaction was deleted or moved by
      hand answered 409 payment_edited forever; the payment row is now removed
      without reversing anything. 409 stays for a transaction that still exists
      but was edited.
    • DELETE /debts reports booked payments without a matching transaction in
      unmatched_payments instead of dropping them silently.
    • Undo reopened a debt the owner had closed by hand. Migration 39 adds
      debts.auto_paid (set when payments close the debt, cleared when the owner
      changes the status), and undo only reopens automatically closed debts.
    • Cross-currency payments could leave dust (99.999999999999 of 100), so the
      debt never closed: coverage now allows half a minor unit of the debt
      currency.
    • Calendar rows carry can_edit: owner or an 'edit' share for installments;
      the owner only for subscriptions, whose pay/skip routes are owner-only.
    • iCal escapeText left a lone CR raw; all line breaks become \n.
  • Review fixes for installments UI (single PUT, safe undo, row actions) by @John710
    Save the debt and its schedule in one PUT /debts/:id so a refused schedule
    cannot leave a debt whose total no longer matches it. Offer undo after a
    delete only when the debt had no payments and the server kept or failed to
    match nothing, since recreating it books a second creation transaction and
    cannot restore payments. Use the backend's currency digit rule (ISO minor
    units, else 8) for crypto schedules. Add per-installment pay/skip/undo on
    the debt card so a paid last installment can be cancelled after the debt
    auto-closed. Keep debt amounts as the API's decimal strings in the editor.
    Calendar: honour can_edit and mark debt installment chips with a violet edge
    so the legend means something.
  • Label form controls, name icon buttons and make clickable cards keyboard-reachable by @John710
    A static scan found that no input/select/textarea was tied to its label,
    about 15 icon-only buttons had no accessible name, and several card-like
    elements could not be reached from the keyboard. - Tie every control to its label (for/id via useId, or aria-label when there is no visible label); colour/icon pickers and type toggles became labelled groups with aria-pressed - Add aria-labels to icon-only buttons (toast close, undo dismiss, back, quick-add, help close); recurring active toggle is now a role=switch - Account, tag and (admin) category cards get role/tabindex and Enter/Space handlers; the SVG upload drop area is a real button - Icon.vue renders aria-hidden so decorative icons stay out of the tree - Add a static test that fails when a form control has no accessible name - New i18n keys: common.back, sharing.permission, sharing.permission_for, recurring.toggle_active, recurring.sorts.label
  • Round money totals once from Decimal, never through floats by @John710
    Report and dashboard totals were already summed with decimal.js but left
    through parseFloat(decimal.toNumber().toFixed(2)): the float detour rounds
    1.005 down to 1.00, the trend net subtracted two floats, and a crypto base
    currency collapsed 0.00000001 BTC to 0. services/currency.js now has
    toDisplayNumber(), which rounds the exact Decimal once (half-up) at the
    serialisation boundary: 2 decimals for fiat codes, 8 for anything else.
    Field names and JSON types are unchanged; by-category rows are ordered by
    the exact totals before rounding.
    The cron reminders printed amounts via Number(), which shows 0.00000001 as
    "1e-8" and cuts digits past ~17; they now use Decimal#toFixed(). Demo data
    wrote Math.random() floats and float balance deltas to numeric columns; the
    amounts are now decimal strings (cents for fiat, 8 decimals for crypto
    rewards) and the crypto purchase leg converts with convertAmountDecimal.
  • Render calendar days in any timezone and parse grouped amounts by @John710
    formatDate passed 'YYYY-MM-DD' to new Date(), which reads it as UTC
    midnight, so every browser west of UTC showed due dates, transaction
    dates and next payments one day early. Date-only strings are now built
    from their parts at local midnight; ISO timestamps keep the Date parser.
    parseAmount replaced only the first comma, so "1,234.56" became 1.234.
    It now follows the backend CSV import rule: spaces, NBSP and apostrophes
    group digits, the last of ','/'.' is the decimal separator unless a lone
    separator repeats, and grouping has to come in threes; anything else is
    NaN instead of a silently truncated number.
    ReportsView formatted debts' due dates and recurring next dates with
    new Date(), and filtered upcoming payments by comparing that UTC instant
    with the current time, which dropped today's payments east of UTC. It
    now uses formatDate and compares the day strings.
  • Return DATE columns as YYYY-MM-DD text and harden the pool by @John710
    pg has no override for DATE (oid 1082), so it built a Date at local
    midnight of the Node process. With the container on Asia/Vladivostok,
    DATE '2026-10-29' reached clients as "2026-10-28T14:00:00.000Z"; the
    transaction edit form sliced that to 2026-10-28 and saving moved the
    transaction back a day. Every DATE column in every response (transactions,
    budgets, debts, recurring rules/exceptions/history) had the same shift,
    and budget edits sent the timestamp back where the schema expects a date.
    The db plugin now keeps DATE as the text PostgreSQL sends; TIMESTAMP is
    unchanged. The pool also logs idle-client errors instead of crashing the
    process on an unhandled 'error' event, sets a server-side
    statement_timeout next to the client query_timeout, and closes idle
    clients.
    Calendar days still taken from the Node clock now come from
    CURRENT_DATE: the net-worth report's month list, the demo data dates
    (local-midnight Dates through toISOString() landed a day early) and the
    CSV export filename. shiftMonth moves to the shared calendar helpers and
    replaces the reports-only nextMonthKey.
  • Take a rule's currency from its account in the form by @John710
    The form defaulted the currency to the base currency and let the user pick
    any other, but a rule is booked in its account's currency and the API now
    rejects a mismatch. The currency is shown read-only and follows the selected
    account; opening an older rule whose currency differed relabels it with the
    account currency it was always booked in.
  • Close sharing, undo, currency and feed-limit holes by @John710
    • DELETE /recurring/:id removed entity_shares for the id before checking
      ownership and without owner_id, so any user could strip another user's
      shares. The owned rule delete now runs first, and only the owner's shares
      go with it, in one transaction.
    • Undoing a payment read the transaction outside the transaction and deleted
      it without checking the row count, so two concurrent undos reverted the
      balance twice. DELETE ... RETURNING inside the transaction is now the check.
    • Every booking path moves the account balance by the rule amount as-is,
      while the plan report converts by the rule currency: a 10 USD rule on a RUB
      account debited 10 RUB. Create now stores the account currency and rejects
      any other (400 errors.rule_currency_mismatch); PATCH/PUT that touch the
      account or the currency are held to the same match. Existing mismatched
      rules keep booking as before and are relabelled on their next edit.
    • Shared rules exposed the owner's account name even when the account itself
      was not shared; account_name is now null unless the account is visible.
    • The calendar feed rate limit was keyed by the token, so every guessed
      token got a fresh bucket; it is now keyed by client address.
  • Honour CSV options and validate every row before writing by @John710
    Multipart is registered without attachFieldsToBody, so request.body was
    undefined and the options were never read: "Preview" ran a real import. The
    handler now iterates request.parts(), accepts the options field before or
    after the file, and only writes when the options say previewOnly: false.
    Rows are validated before the insert transaction opens, and each bad row is
    reported in stats.errors as { row, error } instead of aborting or vanishing:
    • amounts are parsed locale-aware into decimal.js (spaces/NBSP/apostrophes
      are grouping, the last '.' or ',' is the decimal point, groups of three),
      garbage is a row error;
    • dates honour dateFormat (MM/DD/YYYY was unreachable), impossible days are
      row errors, an unsupported dateFormat is a 400;
    • a currency column that differs from the account currency, a transfer row
      (no target account) and a named account the user does not have are row
      errors;
    • categories are only matched to existing ones by name and type; unknown
      names stay uncategorized (stats.uncategorized) instead of creating a global
      category every user would see;
    • balance deltas are summed in decimal.js.
      Also adds the backend error strings for the recurring currency check.
  • Clean up recurring links before migration 32's constraints by @John710
    v1.3.2 deleted rules without clearing transactions.recurring_rule_id and
    could book an occurrence twice, so upgrading a real 1.3.x database failed on
    the validated FK and on the unique (recurring_rule_id, date) index, and the
    container crash-looped (docker-entrypoint runs with set -e).
    up() now first unlinks transactions that point at deleted rules and, for
    duplicate (rule, date) pairs, keeps the lowest id linked and unlinks the rest.
    Nothing is deleted and no amount or balance changes; migration 35 recounts
    occurrences_paid afterwards. Editing this applied migration is a deliberate
    exception: databases where it already ran never re-run it, and for them the
    clean-up would match no rows anyway.
  • Book due rules by the database clock and isolate failures by @John710
    The hourly job selected rules with SQL next_date <= CURRENT_DATE (UTC in
    PostgreSQL) but booked them up to a Node-side today (Asia/Vladivostok in the
    container). When the two calendars disagreed a rule was selected, owed
    nothing, kept its cursor and was selected again forever; the running guard
    then skipped every later hourly run. One failing rule also rethrew out of the
    per-user loop and stopped the job for every user, every hour.
    The per-user loop now lives in services/recurring-run.js (bookDueRules): it
    reads today from SELECT CURRENT_DATE::text in the same transaction as the
    selection, sets aside a rule whose state did not advance, and catches,
    logs and sets aside a rule whose booking failed so the user's other rules and
    the other users still run.
  • Ignore protocol-relative redirect targets by @John710
    A ?redirect=//host value passed the leading-slash check.
  • Address audit findings in saving, undo, charts, auth and modals by @John710
    Users could create duplicate transactions by double-clicking or pressing
    Ctrl+Enter, and API validation errors were silently swallowed. Several
    other paths failed quietly or showed wrong data.
    • guard save/delete handlers with a busy flag and toast API errors in
      Transactions, Budgets, Debts and Accounts; add a global Vue errorHandler
    • undo bar follows the current route; drop undo for account delete (it only
      recreated an empty account); transaction undo keeps tags and splits and is
      replaced by a stronger confirm when attachments exist
    • escape user strings in ECharts tooltips; give sankey nodes unique ids so a
      shared income/expense category name no longer breaks the DAG
    • unsubscribe web push on logout; sync base currency in checkAuth
    • ConfirmModal: localized defaults, focus Cancel, danger on delete confirms
    • CSV export goes through api() (401 refresh, blob support) with error toast
    • ImportCsvModal: options before file, real tab delimiter, stats.errors,
      emit imported only on success, handle profile load failure
    • command palette: unique keys, open the add modal via ?new=1 after routing
    • focus trap: no scroll unlock from closed modals, stacked modals, cancel
      pending frame
    • sanitizeSvg forbids style and a; service worker caches only ok shells and
      tolerates non-JSON push payloads
    • api(): keep ?redirect= on session loss, localized messages
    • AccountDetail uses its own list, refetches on :id change, load more
    • notifications rows are buttons, store calls are caught
    • rates: show unconvertible amounts in their own currency
    • i18n: totp_required, KB, TOTP QR alt and the new strings above
  • Keep personal API tokens away from password and 2FA routes by @John710
    A leaked API token could change the password, disable 2FA or sign out
    every session, locking the owner out of their own account.
  • Close token, session, 2FA and SSRF gaps found in the audit by @John710- Repair dashboard, reports, budgets, debts, export and rate refresh by @John710- Convert USD into crypto currencies by @John710
    Rates are stored as USD->fiat and crypto->USD. A USD source has no
    USD->USD row, so convertAmountDecimal found neither the via-USD nor the
    inverse path and USD->BTC returned null while EUR->BTC worked. The USD
    leg of a USD source is now the identity.
  • Repair category binds and lock down global catalog writes by @John710
    Creating or updating a transaction with a category, splits and category
    merge all sent more bind values than placeholders, which PostgreSQL rejects
    with 08P01, so they answered 500 in production. The budget alert joined the
    categories.user_id column dropped by migration 29 and never fired.
    Categories and tags are global since migration 29, yet any user could
    rename, delete or merge them, rewriting every user's transactions, rules
    and budgets (a category delete cascades into everyone's budgets). Update,
    delete and merge are now admin-only (403 catalog_admin_only); creating
    stays open. Tag usage counts only cover the caller's visible accounts.
    Editing or deleting a transfer rewrote the paired leg and the counter
    account's balance with access checked on the source account only; write
    access to the counter account is now required (403 otherwise).
    Account deletion reversed transfers with UPDATE ... FROM over raw rows,
    which applies a single joined row per account, so only one of N transfers
    to the same counterparty was undone; the amounts are summed per account
    first. The account list returned a shared account twice when it had both
    a family and a personal grant; one grant (the most permissive) is picked,
    the same rule accountForWrite/accountForRead now follow. Visibility
    helpers ignore grants whose owner no longer owns the account.
    Balance arithmetic and the split-sum check use decimal.js instead of float
    math and a half-cent tolerance; the transaction count matches the rows the
    list pages through (split children included).
    The test stub db now fails any statement whose bind array does not match
    its highest $n placeholder, so this class of bug cannot pass tests again.
  • Harden compose secrets, image init, SSL migrations and CI tests by @John710
    Ship no usable default secrets: .env.example leaves POSTGRES_PASSWORD,
    JWT_SECRET and ENCRYPTION_KEY empty and compose requires them (${VAR:?}),
    so a copied example can no longer yield forgeable admin tokens or a
    known DB password. Forward SHOUTRRR_BLOCK_PRIVATE_TARGETS, COINGECKO_API_KEY,
    LOG_LEVEL and PUSH_SEND_TIMEOUT, which the app reads but compose dropped.
    Run the image under tini with a HEALTHCHECK start-period, drop tests and
    tooling from the build context, and switch su-exec to uid:gid so PGID is
    honoured when PUID is an existing user. Map DATABASE_SSL* to PGSSLMODE for
    node-pg-migrate, run backend/frontend vitest in the CI lint job, add
    dependabot docker-compose and minor/patch grouping, and fix README quick
    start, env tables and API list (EN and RU).
  • Keep the recurring section named Регулярные everywhere by @John710
    The nav label and the dashboard cards already said Регулярные; the page heading
    still carried the older Повторяющиеся.
  • Rebuild the schedule floor from booked occurrences by @John710
    Migration 31 seeded start_date and billing_anchor_day from next_date, the cursor
    the old engine had already advanced, so every rule created before 1.4 lost the
    occurrences it had already billed. The projection never reaches before
    start_date, which emptied the current and past months of the payment calendar,
    the dashboard and the iCal feed.
    Reconcile both columns with the transactions ledger and recount occurrences_paid,
    touching only the rows the ledger contradicts.
  • Qualify history columns against the joined users row by @John710
    recurring_history and users both have id and created_at, so the unqualified
    select list raised "column reference is ambiguous" and the endpoint answered
    500. CI's blackbox run caught it; the unit suite cannot, because its stub
    never executes SQL.

CI/CD

  • Use a high-entropy JWT secret for the blackbox stack by @John710
    The startup check now rejects low-entropy secrets; the repeated
    ci-jwt-secret string would stop the CI container from booting.

Documentation

  • Mirror the calendar feed section in the Russian README by @John710- Describe the Traefik router the feed actually needs by @John710
    The first draft told readers to set tls on the router and kept the app's
    forward-auth chain — with Tinyauth in front, a calendar client never gets past
    the gate, and TLS already comes from the websecure entrypoint.
  • Warn that Shoutrrr webhook tokens leak over plain HTTP by @John710
    The last open item from the v1.2.4 audit. Also records why a dead refresh
    token reloads the whole SPA instead of surfacing an in-app error.

Features

  • Installment schedule editor and debt installments in the payment calendar by @John710
    Debts with a repayment schedule were invisible in the UI: the calendar showed
    only subscriptions and the debt form had no way to create the schedule. Add the
    schedule editor (equal parts / manual rows, exact-sum check in integer minor
    units), the pay/skip/unpay actions on calendar chips, installment rows on the
    dashboard, and handle the new DELETE /debts/:id answer (reversed/kept
    transactions). Debt PUT now re-reads the list so installments and paid_amount
    survive an edit.
  • Debt installments in the calendar, dashboard and iCal feed by @John710
    Installment schedules only reached GET /debts, so the calendar, the
    dashboard card and the feed kept showing subscriptions alone. All three now
    read loadInstallmentOccurrences() and merge it with the subscription
    projection through one mergeOccurrences() (date, kind, id), so they agree.
    Subscription rows gain kind 'subscription' and is_owner. The feed gives
    installments a finapp-debt- UID namespace and names the debt account only
    when the feed owner can see it; its calendar name no longer says
    "subscriptions".
    Two audit leftovers: monthlyEquivalent() cut every rule to cents in its own
    currency, so a small crypto rule was 0 before conversion; it now returns the
    value at the numeric scale, the dashboard sums that exactly and GET /recurring
    shows it at the currency precision. Rule reads joined entity_shares, listing a
    rule shared to the family and to the user directly twice; visibility is an
    EXISTS now.
  • Installment schedules and reversal of booked money on delete by @John710
    A debt can now carry a finite payment schedule (debt_installments,
    migration 38) so it can show up in the recurring calendar. Each
    installment is due, paid or skipped; the rows must add up to the debt
    amount exactly (422 installments_sum_mismatch), and a schedule with a
    paid or skipped row can no longer be replaced (409 installments_locked).
    New endpoints: PUT /debts/:id/installments (rows or an equal split at the
    currency precision), POST|DELETE /debts/installments/:id/pay,
    POST|DELETE /debts/installments/:id/skip, PATCH /debts/installments/:id.
    GET /debts returns each debt's installments.
    The payment logic (payment row, transaction, balance, automatic 'paid')
    moves out of the route into recordDebtPayment so POST /debts/:id/payments
    and paying an installment book money the same way; a statement-level test
    pins that the payment route writes exactly what it wrote before.
    Undoing an installment payment reverses its transaction only while it is
    untouched (409 payment_edited otherwise).
    DELETE /debts/:id used to leave the creation booking and the payment
    transactions on the accounts, so balances kept the effect of a debt that
    no longer existed. They are now deleted and reversed in the same
    transaction; transactions edited by hand or on accounts the owner cannot
    write are kept and listed in the response (kept_transactions), which is
    now 200 with a JSON body instead of 204.
    The month-anchor helpers of recurring-schedule.js are exported so
    installment dates follow the same 31 -> 28 -> 31 rule as subscriptions.
  • Allow-list trusted hosts for the private-target guard by @John710
    A self-hosted Gotify behind split-horizon DNS resolves to a LAN address
    and would be blocked now that the guard is on by default.
    SHOUTRRR_ALLOWED_HOSTS exempts exact hostnames while every other target
    stays checked.
  • Block private-network Shoutrrr targets by default by @John710
    Any user of a shared instance could point a webhook at the host's LAN or
    loopback services. The guard is now on unless the admin sets
    SHOUTRRR_BLOCK_PRIVATE_TARGETS=false to allow local services such as a
    self-hosted Gotify.
  • Follow up on backend audit fixes in admin, catalog, debts and import UIs by @John710
    The backend now resets 2FA for admins, makes categories and tags admin-only, locks debt booking fields once payments exist, and returns richer CSV import results. The UI had to catch up so users see the right controls and server messages instead of silent 403/409 failures.
    • admin users list shows 2FA status and a danger-confirmed Reset 2FA action
    • hide category edit/merge and tag edit/delete for non-admins
    • recurring: danger delete confirm, saving/deleting guards, error toasts
    • debts: edit form (locked amount/currency/type/account when payments exist), empty due_date no longer sent
    • api(): keep a route's own localized 429 message (rates refresh throttle) while still replacing framework rate-limit text
    • CSV import: date format select defaulting to the profile, uncategorized count, per-row error list
  • Publish subscriptions as an iCal feed by @John710
    Calendar clients can only be given a URL, so the feed carries a dedicated
    token in the query string instead of an API bearer token: it is hashed at
    rest, shown once, and revoked without deleting the row. The plan is a
    projection of the next 180 days, never the ledger.
    A token in a query string is also something a proxy logs, so the README
    documents the Traefik router that turns access logging off for this path.
  • Recurring commitments and plan-vs-actual by @John710
    The dashboard could only list rules whose stale next_date fell inside five
    days, so a subscription that is actually owed showed nothing about what the
    user is signed up to pay. It now carries the monthly total the active schedules
    are worth and the next outstanding occurrences from the projection, and reports
    gain a plan-vs-actual chart: the plan side is what the schedules ask for, the
    actual side is what the ledger booked under those rules, so a payment made at a
    different amount is visible as variance.
  • Recurring subscriptions UI with calendar and history by @John710
    Expose the backend occurrence contract where users act on it: the recurring
    list shows the occurrence actually owed (pay/skip targets it), a month
    calendar projects occurrences across a window, and a history modal renders
    the audit trail with amounts converted through the rates store.
    Calendar pay/skip is click-based rather than drag-based: the API only
    accepts exact scheduled occurrence dates, so dropping a payment on an
    arbitrary day would be rejected.
  • Expose the owed occurrence the pay/skip actions target by @John710
    The row buttons and run-now act on the first unpaid occurrence from the
    ledger cursor, which points backwards from today for overdue rules while
    next_occurrence always looks forwards.
  • Anchor-based recurring cron with reminders and overdue alerts by @John710
    The hourly job drove generation from next_date + INTERVAL, which ratchets a
    31st-of-month anchor backwards and can only emit one occurrence per run, so a
    rule that fell behind stayed behind for weeks.
    Occurrence dates now come from the schedule service: the cron asks for the
    dates owed up to today (bounded catch-up, rewound ones recorded as skipped in
    history) and writes them through one transaction per rule with
    FOR UPDATE SKIP LOCKED, so a crash rolls back cleanly and two instances cannot
    double-book. Balance moves use numeric strings, notifications fire only after
    COMMIT, and the legacy frequency column stays in sync so the image can roll
    back. Adds daily due-soon and overdue reminders, deduplicated per rule.
  • Anchor-based schedule API with pay/skip and history by @John710
    The recurring endpoints only stored next_date and a fixed frequency, so the
    list could not show what a rule will actually ask for, and PUT rebound every
    column (currency included) whether the client sent it or not.
    Adds PATCH with a column whitelist built by the schedule service, an occurrence
    projection window, per-occurrence pay/unpay and skip, run-now, and a change
    history read — all with dates cast to text so the container timezone cannot move
    a calendar day. PUT stays for one release behind a Deprecation header.
    Editing an auto-transaction onto a date its rule already paid now hits the new
    (recurring_rule_id, date) unique index; the route maps that conflict to a
    localized 400 instead of a 500.
  • Recurring schedule fields, occurrence integrity, history and exceptions by @John710
    Schedules need real state: interval + anchor fields so the next date is derived
    instead of nudged, a FK and a partial unique index so one occurrence can only
    ever be one transaction, and tables for skips and price/schedule changes.
    frequency stays populated so the container can roll back to finapp:1.3.2.
  • Recurring schedule service with anchor-based occurrence math by @John710

Performance

  • Add the indexes the report and sharing queries need by @John710
    Adds (IF NOT EXISTS) indexes no earlier migration covers:
    transactions(account_id, date DESC), transactions(transfer_to_account_id),
    exchange_rates(from_currency, to_currency, updated_at DESC),
    notifications(user_id, created_at DESC), debt_payments(debt_id),
    budgets(category_id), budget_tags(tag_id), recurring_rules(account_id),
    debts(account_id).
    exchange_rates.rate is widened from numeric(15,6) to numeric(30,18):
    six decimals flatten small crypto prices. down() narrows it back and
    drops the indexes.

Refactor

  • Load rules and project occurrences from one place by @John710
    The REST routes, the dashboard and the reports each rebuilt the same
    family-sharing join, the same paid/skipped exception query and the same
    flattened occurrence list, which is three chances for them to disagree about
    what is owed. Move the reads into services/recurring-rules.js and the
    projection into recurring-schedule.projectOccurrences().

Testing

  • Match the UID the feed actually emits by @John710
    The blackbox assertion assumed a compact date in the UID while both the
    service and its unit test pin the dashed occurrence date, so the shape check
    rejected a valid feed. It now reads the UID line it is talking about.
  • Cover the calendar feed end to end by @John710
    The feed is the only recurring surface a browser never calls, so nothing else
    proves the token round-trip, the all-day DTSTART shape or that a revoked link
    stops serving. Runs inside the container against a live instance.
  • Cover recurring projection, pay/skip cycle and run-now in blackbox by @John710
    Runs against a live container, so it pins what the unit suites cannot: the
    server-side date casting, the partial unique index and the balance effects.
    Appended after the net-worth section on purpose — it writes transactions and
    that assertion expects exact balances. Fixtures start in 2027 so the expected
    occurrence dates do not follow the container clock.

Full Changelog: v1.3.2...v1.4.0

Don't miss a new finapp release

NewReleases is sending notifications on new releases.