Repository navigation
feat(ledger): owner-only money commands and routine wiring - #356
Merged
Merged
Conversation
sonikro
force-pushed
the
feat/ledger-commands
branch
from
October 11, 2026 02:55
aafcb8b to
db9d006
Compare
sonikro
force-pushed
the
feat/ledger-commands
branch
from
October 13, 2026 00:29
db9d006 to
b801dea
Compare
sonikro
force-pushed
the
feat/ledger-commands
branch
from
October 15, 2026 12:39
b801dea to
038d864
Compare
sonikro
force-pushed
the
feat/ledger-commands
branch
from
October 15, 2026 13:02
038d864 to
3a82340
Compare
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
force-pushed
the
feat/ledger-commands
branch
from
October 15, 2026 13:37
ece411b to
81f312c
Compare
This was referenced Oct 15, 2026
sonikro
marked this pull request as ready for review
October 15, 2026 13:52
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 thedonation and the new balance to
#money./set-ledger-opening— records the opening balance exactly once. It replaces the npm seedscript 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.
requireMoneyOwner) used by all three commands: guild only,Administrator permission, and the
moneyOwnerDiscordIdsallowlist.#moneyposter and one shared entry-amount formatter, used by the commands and theroutine. 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 {}isgone.
discord.moneyChannelIdanddiscord.moneyOwnerDiscordIds.Routine behaviour worth knowing
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.
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.
longer suppress the usage report.
Testing
CI=true npx vitest run→ 114 files, 828 tests, 0 failures.npm run build:backend→ clean.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
the dedupe keys, but there is no way to re-run a month on demand; an ops command would close it.
forever, because that tuple is the dedupe key. Change the source text to record a second one.
before the first donation or the first monthly settlement.
CheckSharedIpskeeps its own copy of the same authorization pattern with a differentallowlist; only the money commands share the extracted guard.
Stack
Previous: #355 (
feat/ledger-fx). Next: #357 (feat/ledger-settlement→ this branch).