Living documentation
SplitSheet scenario storyboard
Each scenario is a recipe e2e test, replayed as a storyboard: the setup, then every user action and the exact Telegram screen it produced, with the test's assertions shown where they hold. Pick one from the left.
Expenses
- adds an expense when a member replies to the splitsheet
- a settled debt stays settled across a base change (invariant)
- converts bare + currencied + settlement amounts on a base change
- settlements convert (not relabel) on a base change
- settling the exact displayed debt leaves no residual across base changes
- keeps the HUF amount and converts to € (never relabels 6250 as €)
- does not add the expense twice when the same callback update is redelivered
- inherits the prior currency when a modify omits the currency
- reuses the sheet rate when a modify restates the currency (no re-snapshot)
- seeds a setRate for the base and the foreign currency
- seeds base + expense rates (2 setRates) for a currencied expense
- re-parses the expense when the user edits their message
- auto-freezes the base rate on the next expense add, without moving debts
- reads a rate-less legacy base as no base, with totals intact, then heals on re-pick
- modifies an expense when a member replies to its message
- relabels a bare expense in the base currency symbol
- renders an all-bare sheet unchanged (no ≈, no prompt)
- shows the "set a currency" prompt when a currencied expense has no base
- shows the original amount and the ≈ base conversion for a foreign expense
- does not add the expense twice when the same update is redelivered
- flips the debt section between pairwise and simplified
Clarification
Resolution
Inline menu
Mini App
- changes the splitsheet currency via the Mini App API
- freezes the base eurRate when the currency is set via the Mini App
- a chat reply to the announcement modifies the same expense
- a currencied add freezes its setRate, converts, and stays computable
- adds a new expense over HTTP: the bot replies to the sheet, its id becomes the replyTreeId, the pin updates
- adds even when the sheet has no pinned message, sending the announcement unanchored
- blocks an add at the cap with the expense-cap shape; the unlock payment lifts it
- escapes HTML-hostile description in the announcement and the pin, keeps raw text in storage
- fails with 409 when the bot cannot post the announcement, persisting nothing
- never cap-blocks edits, removes or restores of existing expenses
- rejects an add from a non-member with 403 and persists nothing
- rejects an invalid add with 400 (share for a non-member)
- a chat reply to the edit announcement reconciles against the announced body
- a chat reply-modify pushes the next webapp edit downthread (replies to the user message)
- a remove announced in place is re-rendered back on undo restore
- a webapp remove posts a struck-through announcement; restoring right after deletes it (undo)
- an in-place announcement edit failure falls back to the invisible write
- an in-place edit by the adder keeps the plain added label (Telegram badge marks the edit)
- chat-created: the first webapp edit replies to the source message, the second edits that reply in place
- remove → restore (undo) → remove again posts a fresh announcement
- webapp-created, untouched: an edit by another member rewrites the announcement in place
- when the undo delete fails (48h window), a normal restored announcement is posted
- edits an expense over HTTP (amount, payers, shares, description change)
- edits to a new currency, seeding its setRate and keeping totals computable
- edits with an exact (unequal) split that round-trips over HTTP
- edits with two payers (multi-payer) that round-trip over HTTP
- persists an UNANNOUNCED webapp edit invisibly, then a later telegram edit re-parses
- reconciles a 1-cent share drift via processExpense instead of rejecting
- rejects a guest (non-member) save and remove with 403
- rejects a share sum that drifts beyond what processExpense can reconcile
- rejects an invalid payload with 400 (share for a non-member)
- removes an expense over HTTP (tombstoned, excluded from totals)
- restores a bare expense by re-sending its detected base currency, value-preserving
- restores a removed expense (save after remove, same body, removedAt cleared)
- stamps source: webapp on the modify and remove events
- the same add posted twice with the same Idempotency-Key produces exactly one expense
- censors amounts and offers an Unlock-balances CTA when balances are locked
- expands in place via "Show all" when the payer list overflows
- lists each payer with amounts, biggest first, in the sheet currency
- stays hidden until there is a verdict to summarize
- responds 404 for an unknown sheet id
- serves the sheet to an unauthenticated preload GET, without user-bound fields
- is uncomputable until a base is picked, then lifts
- refuses to edit the base currency and rejects a non-positive rate
- revalues history and can reopen a settled debt (gated by the confirm)
- renames the list via the Mini App API and re-renders the message
- renames the calling member via the Mini App API
- records a settlement via the Mini App API
- rejects settle on a base-less sheet, then succeeds after a currency is set
- undo (unsettle) reopens the debt to its prior amount
Voice
Messages
Payments
general
paywall
- exempts private chats server-side — the webapp never sees a lock
- replaces the debts block with a lock line once there is a verdict
- shows neither lock nor debts on an empty stamped sheet
- unlocks the debts block after a Stars payment, without a 🔓 badge
- blocks a legacy free sheet at its old cap of 10
- blocks a stamped free sheet at 50 with the payment promo
- nudges a paid sheet near 500 and blocks at 500 with the fresh-sheet copy
- processes voice messages on a free sheet with no separate voice quota
- pays through a boost button the bot no longer renders
- sells the cap lift through the upgrade button
- shows full debts with neither lock nor badge
- grandfathers a long-expired 30-day boost as paid forever
- records a fresh Stars payment without expiry semantics
- still accepts a crypto payment from an in-flight 30-day-era invoice
- stamps a sheet auto-created when the bot joins a chat
- stamps a sheet created via /newsplit
- unstampPaywall strips the stamp — the fixture for pre-paywall recipes
- is never gated: links are minted even with nothing to sell (display-only rule)
- mints payable links and the Stars payment unlocks the sheet