Skip to content

Economy: categories of its own, separate from the store catalogue - #64

Merged
CaYatur merged 2 commits into
mainfrom
feat/economy-categories
Jul 27, 2026
Merged

Economy: categories of its own, separate from the store catalogue#64
CaYatur merged 2 commits into
mainfrom
feat/economy-categories

Conversation

@CaYatur

@CaYatur CaYatur commented Jul 27, 2026

Copy link
Copy Markdown
Owner

What

Closes #13. The economy gets its own categories, defined independently of the store catalogue, and the desktop view splits into Economy | Store tabs so balances stop living inside the shop.

  • shared/web.tsEconomyCategory + an optional category on LedgerEntry.
  • shared/economy.tsDEFAULT_CATEGORIES (Reward, Event payout, Refund, Penalty, Correction), categoryName(), and filterLedger gains a category dimension.
  • store/economy.ts — category CRUD; addBalance/setBalance accept one.
  • Desktop: Economy tab (adjust with a category picker, balances, category manager, ledger) and Store tab (currency, products). Panel: same picker + ledger filter.

Interpretation, stated explicitly

"Distinct economy categories, not tied to store products" is read here as categories for balance movements — an economy runs on grants, refunds, event payouts and penalties that have no product behind them, and labelling those with a store product would force fake products into the shop just to name a payout.

The other possible reading is multiple currencies/wallets (Coins + Gems + Tokens). That is a much larger change — every balance becomes a map, and purchase/public-store/API all change shape — so I did not assume it. If wallets were the intent, say so and I will open it as its own issue rather than bending this one.

Decisions worth reviewing

A category is validated on write. addBalance(..., 'not-a-real-category') records the entry with no category rather than storing the string. A ledger entry is an audit record; letting a caller write a free-string label means the log can claim a category nothing on the server defines.

Deleting a category does not rewrite history. Past entries keep the id they were recorded with, and the UI falls back to showing that raw id. Editing an audit trail to tidy up a dropdown is exactly what an audit trail must never do.

'none' is a real filter value, not the absence of one. Purchases never get a category, and neither does anything recorded before this PR — "what was never labelled" is a question an operator will actually ask.

Verification

  • typecheck + build clean; MSMS_SMOKE_WEB exit 0; MSMS_SMOKE exit 0.
  • New smoke units: category filter selects correctly; 'all' and an absent category do not filter; 'none' returns exactly the uncategorised entries; category AND text both apply; a real category is recorded and an invented one is not; deleting a category leaves the past entry's category intact; categoryName falls back to the raw id.
  • Panel JS executed in a VM: category filter identical to filterLedger across 7 combinations; the picker and filter render the right options; a category named Reward <b> comes out Reward &lt;b&gt; (escaped); the filter defaults to all.
  • en/tr store block parity: 53/53.

Disclosed gaps

  • Migration is verified by construction, not against a real pre-existing store file. Old store.json files have no categories key; the loader seeds defaults when the field is missing but keeps an empty array if an operator deleted them all. I did not replay an actual v1 file from disk.
  • The desktop Economy/Store tab split and the category manager were not rendered in a browser — the smoke asserts the pure logic, the store round-trip, and that the app mounts.
  • The panel filter remains a hand-kept mirror of the shared function (same constraint as Economy: collapsible, searchable balance ledger #63), now covering the category dimension too.

Closes #13.

An economy runs on grants, refunds, event payouts and penalties that have no
product behind them, so labelling a balance change with a store product would
force fake products into the shop just to name a payout. Categories are now a
separate, editable list per server, seeded with sensible defaults.

- shared/web.ts: EconomyCategory + an optional category on LedgerEntry.
- shared/economy.ts: DEFAULT_CATEGORIES, categoryName(), and filterLedger gains
  a category dimension including a 'none' bucket - purchases and every entry
  recorded before categories existed carry none, and 'show me what was never
  labelled' is how you find them.
- store/economy.ts: category CRUD; addBalance/setBalance take one. Only a
  category that actually exists is recorded - a ledger entry is an audit record
  and must not claim a label nothing on the server defines. Deleting a category
  does NOT rewrite past entries; the UI falls back to the raw id.
- Desktop Store view splits into Economy | Store tabs, so balances, categories
  and the ledger stop living inside the shop. Panel gets the same picker and
  ledger filter.

Old store files migrate on load (categories seeded once); an operator who
deletes them all keeps an empty list rather than having them reappear.
Copilot AI review requested due to automatic review settings July 27, 2026 20:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Restoring a deleted category id left the select with no matching option, so
select.value became '' - which the filter read as a real category and matched
nothing, blanking the panel ledger with no visible cause.
@CaYatur

CaYatur commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

/code-review — self review

One real bug, fixed on-branch.

Deleting a category could blank the panel ledger with no explanation

renderCategories() rebuilt both selects and restored the previous selection unconditionally:

f.value = keepF || 'all'

If the remembered value was a category that had since been deleted, no matching <option> exists, so the browser silently leaves select.value as ''. ledgerFiltered() then reads '', which is neither 'all' nor 'none', and evaluates e.category !== '' for every entry — everything is filtered out. The operator sees an empty ledger, and the select showing blank does not read as "a filter is active".

Reachable path: an admin deletes a category on the desktop while the panel is open on that server; the panel's next loadManage() (which runs after any balance adjustment) re-renders the selects and the ledger goes blank.

This is the same failure mode as the review finding on #63 — a stale filter hiding data with nothing on screen to explain it — arriving by a different route, which is why it was worth looking for.

Fixed by restoring a selection only when it still exists:

f.value = (keepF === 'none' || (keepF && ids.indexOf(keepF) >= 0)) ? keepF : 'all'

Verified in a VM against the real generated panel script: after deleting the selected category the adjust picker falls back to "" and the ledger filter to "all"; a still-existing selection survives a re-render (bonusbonus), and none is preserved rather than being reset.

Checked and fine

  • Category validation on write rejects an invented id without throwing — the adjustment still applies, it just carries no label. Asserted in the smoke.
  • deleteCategory leaves past ledger entries untouched; categoryName falls back to the raw id. Asserted in the smoke.
  • The migration seeds defaults only when categories is missing, so an operator who deletes them all does not get them back on next load.
  • Category names are escaped into the panel selects — Reward <b> renders as Reward &lt;b&gt;, so a category name cannot inject markup into the panel.
  • Desktop removeCategory already resets both the picker and the ledger filter.

Accepted, not fixed

The new smoke section writes an economy for a synthetic cat-smoke-server into store.json and there is no delete-store API to clean it up. It lands in dev-root only, never a user's data dir, so I left it rather than inventing an API for the test's benefit.

Re-verified: build clean, MSMS_SMOKE_WEB exit 0, MSMS_SMOKE exit 0.

@CaYatur
CaYatur merged commit 2521fd1 into main Jul 27, 2026
@CaYatur
CaYatur deleted the feat/economy-categories branch July 27, 2026 20:57
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.

Economy: separate balance categories from the store

2 participants