[Table Tools] Party loot ledger and XP tracking #112

Closed
opened 2026-07-14 19:50:59 +00:00 by claude-bot · 1 comment
Contributor

Context / Motivation

"Who's carrying the gem / did we split that 800 gp" is the most universal unmet bookkeeping need at the table. It fits Quest Board's campaign-scoped model cleanly, and later feeds the planned Foundry member writeback (member_to_actor, see issue #24's adapter interface).

Spec

Models

LootEntry:

  • id, campaign_id (FK, CASCADE, indexed), session_id (nullable FK, SET NULL)
  • name (Text, required), qty (int, default 1)
  • est_value (free-text) + currency (free-text, e.g. "gp") — no exchange-rate engine
  • holder_member_id (nullable FK → campaign_members; NULL = party pool)
  • notes (nullable), created_by_id (FK users), created_at/updated_at
  • soft-delete (deleted_at nullable) so accidental deletions are recoverable

XpAward:

  • id, campaign_id, session_id (nullable), amount (int), scope: party | member, member_id (nullable FK, required when scope=member), note, created_by_id, created_at

GoldLedgerEntry (simple credit/debit):

  • id, campaign_id, session_id (nullable), amount (signed numeric), currency (free-text), description, member_id (nullable = party fund), created_by_id, created_at

API — campaign-scoped REST under /api/campaigns/{campaign_id}/loot, /xp, /gold (list/create/update/delete), following the inline-router pattern used for planning arcs/threads in webapp/backend/app/routers/campaigns.py:3172-3390. Totals endpoint returns per-member XP totals, per-member + party-pool loot, and gold balance.

Permissions: GM full CRUD; players read-only by default; campaign setting players_can_add_loot (default off) allows player creates (still not edit/delete of others' entries). Reuse the existing GM/member dependency guards from campaigns.py.

UI: new campaign tab "Loot & XP": loot table (filter by holder, transfer-holder action), XP totals per member, gold ledger with running balance. Session detail page shows that session's loot/XP entries.

Export/import: add the three tables to the campaign export zip and importer — export_campaign (routers/campaigns.py:2580, bump EXPORT_SCHEMA_VERSION from "7" at :2566) and import_campaign (services/import_service.py:71, extend SUPPORTED_SCHEMA_VERSIONS at :40).

Out of scope (flagged follow-ups, do not build here)

  • LLM extraction proposing loot/XP entries from transcripts (GM-approved like lore proposals).
  • Foundry XP/inventory writeback (rides issue #24 Phase 3).
  • Encumbrance, item catalogs, or rules-aware valuation.

Acceptance criteria

  • CRUD works for all three models; totals correct including after holder transfers (transfer test: item moves member A → party pool → member B, totals follow).
  • Permission matrix enforced: player cannot mutate with setting off; can only create with setting on; GM unrestricted.
  • Campaign export zip round-trips loot/XP/gold through export → import with schema version bump validated.
  • Session detail lists only that session's entries.

References

  • webapp/backend/app/routers/campaigns.py:3172-3390 (planning arcs/threads inline-router pattern), :2580 (export_campaign), :2566 (EXPORT_SCHEMA_VERSION = "7")
  • webapp/backend/app/services/import_service.py:40 (SUPPORTED_SCHEMA_VERSIONS), :71 (import_campaign)
  • webapp/backend/app/models/session_attendance.py (campaign-scoped per-member model precedent)
  • Issue #24 (Foundry adapter interface — member_to_actor, item_to_item)

Filed from the July 2026 full-project review.

## Context / Motivation "Who's carrying the gem / did we split that 800 gp" is the most universal unmet bookkeeping need at the table. It fits Quest Board's campaign-scoped model cleanly, and later feeds the planned Foundry member writeback (`member_to_actor`, see issue #24's adapter interface). ## Spec **Models** `LootEntry`: - `id`, `campaign_id` (FK, CASCADE, indexed), `session_id` (nullable FK, SET NULL) - `name` (Text, required), `qty` (int, default 1) - `est_value` (free-text) + `currency` (free-text, e.g. "gp") — no exchange-rate engine - `holder_member_id` (nullable FK → campaign_members; NULL = party pool) - `notes` (nullable), `created_by_id` (FK users), `created_at`/`updated_at` - soft-delete (`deleted_at` nullable) so accidental deletions are recoverable `XpAward`: - `id`, `campaign_id`, `session_id` (nullable), `amount` (int), `scope`: `party` | `member`, `member_id` (nullable FK, required when scope=member), `note`, `created_by_id`, `created_at` `GoldLedgerEntry` (simple credit/debit): - `id`, `campaign_id`, `session_id` (nullable), `amount` (signed numeric), `currency` (free-text), `description`, `member_id` (nullable = party fund), `created_by_id`, `created_at` **API** — campaign-scoped REST under `/api/campaigns/{campaign_id}/loot`, `/xp`, `/gold` (list/create/update/delete), following the inline-router pattern used for planning arcs/threads in `webapp/backend/app/routers/campaigns.py:3172-3390`. Totals endpoint returns per-member XP totals, per-member + party-pool loot, and gold balance. **Permissions**: GM full CRUD; players read-only by default; campaign setting `players_can_add_loot` (default off) allows player creates (still not edit/delete of others' entries). Reuse the existing GM/member dependency guards from campaigns.py. **UI**: new campaign tab "Loot & XP": loot table (filter by holder, transfer-holder action), XP totals per member, gold ledger with running balance. Session detail page shows that session's loot/XP entries. **Export/import**: add the three tables to the campaign export zip and importer — `export_campaign` (`routers/campaigns.py:2580`, bump `EXPORT_SCHEMA_VERSION` from "7" at `:2566`) and `import_campaign` (`services/import_service.py:71`, extend `SUPPORTED_SCHEMA_VERSIONS` at `:40`). ## Out of scope (flagged follow-ups, do not build here) - LLM extraction proposing loot/XP entries from transcripts (GM-approved like lore proposals). - Foundry XP/inventory writeback (rides issue #24 Phase 3). - Encumbrance, item catalogs, or rules-aware valuation. ## Acceptance criteria - CRUD works for all three models; totals correct including after holder transfers (transfer test: item moves member A → party pool → member B, totals follow). - Permission matrix enforced: player cannot mutate with setting off; can only create with setting on; GM unrestricted. - Campaign export zip round-trips loot/XP/gold through export → import with schema version bump validated. - Session detail lists only that session's entries. ## References - `webapp/backend/app/routers/campaigns.py:3172-3390` (planning arcs/threads inline-router pattern), `:2580` (`export_campaign`), `:2566` (`EXPORT_SCHEMA_VERSION = "7"`) - `webapp/backend/app/services/import_service.py:40` (`SUPPORTED_SCHEMA_VERSIONS`), `:71` (`import_campaign`) - `webapp/backend/app/models/session_attendance.py` (campaign-scoped per-member model precedent) - Issue #24 (Foundry adapter interface — `member_to_actor`, `item_to_item`) _Filed from the July 2026 full-project review._
Author
Contributor

Done — merged in PR #192 (backend a35d611, frontend 1293a3d; squashed to main).

Shipped:

  • Data model: LootEntry (soft-delete), XpAward (party/member scope), GoldLedgerEntry; migration e2f3a4b5c6d7. Nullable holder/member FKs (SET NULL) → NULL = party pool / party fund, so removing a member preserves ledger history.
  • Gold stored as Decimal, serialized as JSON strings end-to-end (no float drift); the frontend never does math on gold — all sums come from GET /loot-totals.
  • Endpoints: campaign-scoped loot/xp/gold CRUD + /loot/{id}/transfer + /loot-totals; read-only session-scoped /sessions/{id}/{loot,xp,gold}. Export/import schema 7→8.
  • Permissions: GM full CRUD; players read-only unless the campaign's players_can_add_loot is on, then creator-only (server-enforced 403).
  • UI: LootLedger campaign panel (loot table + holder filter + transfer, XP totals + award, gold ledger + running balance), the players_can_add_loot setting toggle, and a read-only session-detail block.

Tests: backend 456 pass (+21 test_loot.py); frontend 202 pass (+33). CI run #3879 green (5m49s).

Deferred to #149 (loot generator, this milestone): the LLM-driven parcel generator writes into this ledger.

Closing.

Done — merged in PR #192 (backend `a35d611`, frontend `1293a3d`; squashed to main). **Shipped:** - Data model: `LootEntry` (soft-delete), `XpAward` (party/member scope), `GoldLedgerEntry`; migration `e2f3a4b5c6d7`. Nullable holder/member FKs (`SET NULL`) → `NULL` = party pool / party fund, so removing a member preserves ledger history. - Gold stored as `Decimal`, serialized as JSON strings end-to-end (no float drift); the frontend never does math on gold — all sums come from `GET /loot-totals`. - Endpoints: campaign-scoped loot/xp/gold CRUD + `/loot/{id}/transfer` + `/loot-totals`; read-only session-scoped `/sessions/{id}/{loot,xp,gold}`. Export/import schema 7→8. - Permissions: GM full CRUD; players read-only unless the campaign's `players_can_add_loot` is on, then creator-only (server-enforced 403). - UI: `LootLedger` campaign panel (loot table + holder filter + transfer, XP totals + award, gold ledger + running balance), the `players_can_add_loot` setting toggle, and a read-only session-detail block. **Tests:** backend 456 pass (+21 `test_loot.py`); frontend 202 pass (+33). CI run #3879 green (5m49s). **Deferred to #149** (loot generator, this milestone): the LLM-driven parcel generator writes into this ledger. Closing.
rbrooks referenced this issue from a commit 2026-07-18 04:59:00 +00:00
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#112
No description provided.