github John710/finapp v1.4.0-beta.7

latest releases: v1.4.0, v1.4.0-beta.8
pre-release3 hours ago

[Unreleased]

Bug Fixes

  • 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.

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.

Full Changelog: v1.3.2...

Don't miss a new finapp release

NewReleases is sending notifications on new releases.