# Local family dashboard task + coin ledger pattern

Use for local family/PWA dashboards with children, tasks, points/coins, benefits, and day-close logic.

## Durable implementation pattern

1. **Ledger, not mutable balances**
   - Never update a child balance directly.
   - Store immutable `CoinTransaction` rows and derive `posted`, `reserved`, and `available` from the ledger.
   - Benefits should reserve/capture/release via ledger transactions, not hidden balance columns.
   - For a benefit reservation flow, keep `posted = sum(amount)`, `reserved = sum(reserved_amount)`, and `available = posted - reserved`.

2. **Idempotency everywhere a child can double-tap**
   - Task completion, undo, benefit redemption, day-close penalties, reversals, and quiz/learning rewards need stable idempotency keys.
   - API should accept an `Idempotency-Key` header or body key for user-triggered actions.
   - System-triggered rows should use deterministic keys like `penalty:task:<id>` and `penalty-reversal:task:<id>`.
   - Non-task rewards such as daily phrase quizzes should book immutable `CoinTransaction` rows directly with deterministic keys that include child/date/slot/phrase (for example `daily-phrase-quiz:<child>:<date>:<slot>:<phrase>`), so a correct answer grants coins once even after reloads or double taps.
   - Benefit actions also need stable keys for redeem/reserve, fulfill/capture, and cancel/release; repeated admin taps must return idempotent success rather than double-booking.

3. **Task completion semantics**
   - Bonus task complete: mark done and book exactly one positive earned transaction.
   - Required task complete: mark done, no positive coins.
   - Undo bonus task: reopen task and book a negative counter-transaction.
   - Undo required task: reopen task; do not fabricate a reward/penalty unless day-close rules require it later.

4. **Lazy day close instead of scheduler first**
   - When opening a past dashboard date, create that date’s task instances first, then finalize the day.
   - Only past dates (`target_date < today`) are finalized.
   - Open required tasks with negative `penalty_value` book exactly one penalty transaction.
   - Bonus tasks receive no day-close penalty.
   - Re-running the same dashboard request must not create duplicate penalties.

5. **Late completion reversal**
   - If a required task already has a day-close penalty and is later completed, book exactly one positive reversal for `abs(penalty_value)`.
   - Do not delete the penalty; keep the audit trail fair and explainable.
   - Repeated complete calls should be idempotent and not create more reversals.

6. **Testing strategy**
   - Reset the test database between tests; ledger assertions otherwise leak state across TestClient calls.
   - Test bonus complete idempotency, bonus undo, required complete without reward, day-close penalty once, late reversal once, and bonus-no-penalty.
   - For benefits, test: list/affordability, redeem reserves coins exactly once, fulfill captures exactly once, cancel releases the reservation, insufficient-coins returns a conflict, and fulfilled redemptions cannot be cancelled.
   - When tests earn coins via daily seeded bonus tasks, assert the actual earned total from the fixture rather than assuming a target amount; completing all bonus tasks in a day may overshoot the minimum needed.
   - For API smoke, prove the sequence: open past dashboard → ledger negative → complete required task late → ledger partially restored; for benefits, earn coins → reserve benefit → reserved balance increases → fulfill benefit → posted decreases and reserved returns to zero.

7. **Benefit redemption semantics**
   - `GET /api/benefits` may optionally accept `child_id` to add affordability without duplicating benefit definitions per child.
   - `POST /api/benefits/{id}/redeem`: create a `BenefitRedemption(status=reserved)` and a reserve ledger row with `amount=0`, `reserved_amount=price`.
   - `POST /api/admin/redemptions/{id}/fulfill`: only for reserved redemptions; book one capture row with `amount=-price`, `reserved_amount=-price`, then mark fulfilled.
   - `POST /api/admin/redemptions/{id}/cancel`: only for reserved redemptions; book one release row with `amount=0`, `reserved_amount=-price`, then mark cancelled.
   - Do not delete reservation/capture/release rows; keep the audit trail explainable.
   - UI should refresh dashboard/coin summaries and redemptions after every mutation. Buttons should disable when the child cannot afford the benefit or a mutation is pending.

## UX notes

- Keep penalty copy calm and non-punitive in child-facing UI; detailed reasons belong in the ledger/admin view.
- Coin badges should refresh by invalidating the dashboard query after task mutations and after any non-task coin reward such as a quiz answer.
- Large touch targets matter more than clever animations for child-operated iPad dashboards.
- When a child-facing widget sits above the task boards and belongs to both children, mirror the two-column mental model used by the task boards: Emilia left, Valerie right, with separate touch targets and feedback per child.
- E2E tests against the live persistent DB should not assume one specific child still has a seeded bonus task open; production state may have prior completions. Prefer finding an available bonus in either child column or explicitly creating isolated test data through an admin/test API.
