feat: party loot ledger, XP & gold tracking (#112) #192
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/112-loot-xp-ledger"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #112. First feature of the v3.7.0 Table Tools milestone — the foundational ledger data model the loot generator (#149) will build on.
Backend (
a35d611)models/loot.py):LootEntry(soft-deletable),XpAward(party/member scope),GoldLedgerEntry. Holder/member FKs are nullable →NULLmeans party pool / party fund;ondelete SET NULLso removing a member preserves history as party-owned.Decimal(serialized as JSON strings) to avoid float drift.e2f3a4b5c6d7(down_revisiond1e2f3a4b5c6) — round-trips clean (upgrade → downgrade → upgrade) on a reset test DB.campaigns.py:GET/POST /{id}/loot,PATCH /loot/{lid},POST /loot/{lid}/transfer,DELETE /loot/{lid};GET/POST /{id}/xp+DELETE;GET/POST /{id}/gold+DELETE;GET /{id}/loot-totals. Session-scoped reads under/sessions/{id}/{loot,xp,gold}.players_can_add_lootis on, and then may modify only entries they created (creator-only, enforced server-side with 403).Frontend (
1293a3d)api/loot.js— shared-client wrappers; gold passed through as opaque strings.components/LootLedger.jsx— campaign panel: loot table (holder filter + transfer/edit/delete), party & per-member XP totals + award form, gold ledger with running balances + add form. Write controls gated onisGm || players_can_add_loot; per-entry ownership is the backend's 403 to enforce, surfaced inline.CampaignDetail— renders the panel; adds the "players can add loot/XP" setting toggle.SessionDetail— read-only "Loot & XP this session" block.Tests
tests/test_loot.py); full suite 456 pass.Notes
BOT_CONTRACT_VERSIONuntouched) — no/api/bot/*surface touched.🤖 Generated with Claude Code
"Who's carrying the gem / did we split that 800gp" is the most universal unmet bookkeeping need at the table. Add three campaign-scoped models with CRUD, totals, permissions, and export/import. Models (migration e2f3a4b5c6d7, down_revision d1e2f3a4b5c6): - LootEntry: name/qty/est_value/currency, holder_user_id (nullable FK users; NULL = party pool), soft-delete via deleted_at, session link. - XpAward: amount + scope (party|member); member_user_id required for member scope. - GoldLedgerEntry: signed Numeric amount, member_user_id (NULL = party fund). - campaigns.players_can_add_loot (bool, default off). Holder/member are modelled as nullable user FKs (CampaignMember has a composite PK, so a member is identified by user within the campaign) with ondelete SET NULL. Endpoints (inline in campaigns.py, per the arcs/threads precedent): campaign-scoped list/create/update/delete under /loot, /xp, /gold; /loot-totals; and session-scoped GET /sessions/{id}/{loot,xp,gold}. Permissions: GM full CRUD; players read-only, create only when players_can_add_loot is on, and may modify only entries they created. Totals: party_xp and per-member XP kept separate (a party award isn't auto-split); loot grouped party_pool vs per_holder; gold signed total + party_fund + per_member. Export/import: EXPORT_SCHEMA_VERSION 7 -> 8; the three tables ride the campaign zip (FKs via the existing session/member key maps; v7 zips still import with empty ledgers). Note: member-scoped XP collapses to party on import because members import as unlinked stubs with no resolvable user id (same policy as attendance). Gold amounts are Decimal, so they serialize as JSON strings (precision) — the frontend must treat gold as strings. Verified via the dev-host harness: migration applies from base and downgrade -1 + upgrade round-trips; 456 passed (21 new incl. transfer-totals + export/import round-trip + permission matrix); ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>