Skip to content

feat(ledger): owner-only money commands and routine wiring - #356

Merged
sonikro merged 10 commits into
feat/ledger-fxfrom
feat/ledger-commands
Oct 16, 2026
Merged

sonikro merged 10 commits into
feat/ledger-fxfrom
feat/ledger-commands

Conversation

@sonikro

@sonikro sonikro commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Stack 3/4 — Owner-only money commands and routine wiring (base: feat/ledger-fx)

The commands that drive the ledger, plus the monthly routine that settles its costs.

What this PR adds

  • /balance — current ledger balance, owner only.
  • /register-donation — records a manual donation, converts USD at the PTAX rate, posts the
    donation and the new balance to #money.
  • /set-ledger-opening — records the opening balance exactly once. It replaces the npm seed
    script that feat(ledger): immutable BRL ledger core #354 removed, and refuses to run unless the ledger holds no transactions; the
    dedupe key is a ledger-wide constant, so two concurrent invocations cannot both write an
    opening even if both see an empty ledger.
  • One shared authorization guard (requireMoneyOwner) used by all three commands: guild only,
    Administrator permission, and the moneyOwnerDiscordIds allowlist.
  • One shared #money poster and one shared entry-amount formatter, used by the commands and the
    routine. The poster fetches the channel, logs every failure through the telemetry logger and
    reports whether the message was delivered; the donation command's previous bare catch {} is
    gone.
  • Config: discord.moneyChannelId and discord.moneyOwnerDiscordIds.

Routine behaviour worth knowing

  • Costs are settled independently of usage-report delivery. Previously the settlement ran
    after the report was posted, so a deleted report channel or a lost permission meant the month's
    costs were never written at all — and with no re-run path, lost for good.
  • The money summary distinguishes new entries, already-recorded entries, segments with no cost to
    record, and failures. A month with failed segments is no longer summarised as settled, and the
    event log avoids the word "settled" on that path, so a query for settled months cannot return a
    partially failed one.
  • The settlement's event logging cannot reject out of the settlement, so a rate-limited log can no
    longer suppress the usage report.

Testing

  • CI=true npx vitest run → 114 files, 828 tests, 0 failures.
  • npm run build:backend → clean.
  • Command handlers: denial paths (DM, missing Administrator, null permissions, not allowlisted),
    the success paths, the "ledger already holds transactions" rejection, and the channel-missing
    cases. Routine: settlement independence from report delivery, failure reporting, and the
    byte-exact usage-report regression.

Notes / follow-ups

  • A wholly failed monthly settlement is still never retried. Partial re-runs are safe because of
    the dedupe keys, but there is no way to re-run a month on demand; an ops command would close it.
  • A genuinely repeated donation — same donor, amount, currency and source text — is rejected
    forever, because that tuple is the dedupe key. Change the source text to record a second one.
  • The opening balance can only be set while the ledger is completely empty, so it must be set
    before the first donation or the first monthly settlement.
  • CheckSharedIps keeps its own copy of the same authorization pattern with a different
    allowlist; only the money commands share the extracted guard.

Stack

Previous: #355 (feat/ledger-fx). Next: #357 (feat/ledger-settlement → this branch).

The settlement now takes the parsed money config as a required dependency,
so the composition root has to provide it or the branch does not compile.
The opening balance may only be set once, so the use case that sets it needs
to ask the ledger whether anything is already recorded. Expose that as an
existence probe rather than a count, since nothing needs the number.
The ledger needs an opening balance, and it may only ever be set once: an
opening recorded twice would double the money the ledger believes it has.

Add a use case that refuses to run unless the ledger is empty, converts a
USD opening at the PTAX rate while a BRL opening needs no rate at all, and
records the entry as OPENING with the admin who set it as the actor. The
dedupe key is a ledger-wide constant, so the unique index is the real
guarantee: two concurrent invocations that both see an empty ledger cannot
both write an opening.
The balance and donation commands each repeated the same three checks:
guild only, Administrator permission, and the owner allowlist. Three copies
of an authorization check is how one of them ends up weaker than the others
when someone edits just one.

Move the checks into one guard that takes the action it is protecting, and
have both handlers call it. The replies are unchanged, so nothing the owner
sees changes.
…ures

Posting to #money was written twice: the donation command fetched the
channel and swallowed any error without a trace, while the monthly routine
read it from the channel cache, which is empty right after a restart and
silently dropped the monthly money post.

Add one poster that fetches the channel, logs every failure through the
telemetry logger and reports whether the message was delivered, and route
both paths through it. The entry amount formatter moves next to it so both
commands render a donation the same way.

The monthly money summary now distinguishes costs that failed to settle from
costs that were already recorded. Reporting a failed segment as already
recorded told the owner a month was settled when its costs had been lost.
Opening the ledger is a one-time decision the owner makes, and it used to be
an npm script that anyone with shell access could run again at will.

Add an owner-gated slash command with an amount and a currency that records
the opening balance, refuses once the ledger holds any transaction, and
announces the result in #money. It shares the money owner guard, the amount
formatter and the channel poster with the other money commands.
The monthly settlement ran after the usage report was posted, so a deleted
report channel, a lost permission or a stale cache entry meant the month's
cost entries were never written at all - and with no re-run path those costs
were lost for good. Run the settlement before report delivery, in its own
error boundary, so nothing about the report can prevent it.

An unconfigured money channel also skipped the settlement while logging that
only the post was skipped. The settlement now always runs and the shared
poster owns the missing-channel case.

A month with failed segments was still summarised as settled, which is the
same false report this stack set out to remove; the summary now names the
failures and avoids the word settled entirely. Segments with no cost to
record are counted separately from segments already recorded, so the money
channel no longer reports idle regions as already billed.

Also stop the amount options from offering zero, which the use cases always
reject, and drop the opening post's self-referential source clause.
Settling the costs ahead of report delivery made the report depend on the
settlement's error path: its event logging performs a Discord call, and a
rejection there escaped the settlement and aborted the routine before the
usage report was posted. Route every settlement event through one helper
that cannot reject, and record a failed log through the telemetry logger so
the loss stays visible.

Also make the routine's test invoke the scheduled job through a single
awaited helper. It asserted on an unawaited job started while the file was
collected, which held only because the report happened to be posted before
the assertion ran; settling earlier added enough awaits to make that race
reachable.
@sonikro
sonikro force-pushed the feat/ledger-commands branch from ece411b to 81f312c Compare October 15, 2026 13:37
@sonikro
sonikro marked this pull request as ready for review October 15, 2026 13:52
@sonikro sonikro changed the title feat(ledger): owner-only donation/balance commands and routine wiring feat(ledger): owner-only money commands and routine wiring Oct 15, 2026
@sonikro
sonikro merged commit b2694d5 into main Oct 16, 2026
10 checks passed
@sonikro
sonikro deleted the feat/ledger-commands branch October 16, 2026 23:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant