# Feature: Dynamic Point and Cost Customization ## Overview **Goal:** Allow parents to customize the point value of tasks/penalties and the cost of rewards on a per-child basis after assignment. **User Story:** As a parent, I want to assign different point values to the same task for different children, so I can tailor rewards to each child's needs and motivations. For example, "Clean Room" might be worth 10 points for one child but 5 points for another. **Process:** 1. **Assignment First**: Tasks, penalties, and rewards must be assigned to a child before their points/cost can be customized. 2. **Edit Button Access**: After the first click on an item in ScrollingList (when it centers), an edit button appears in the corner (34x34px, using `edit.png` icon). 3. **Modal Customization**: Clicking the edit button opens a modal with a number input field allowing values from **0 to 10000**. 4. **Default Values**: The field defaults to the last user-set value or the entity's default points/cost if never customized. 5. **Visual Indicator**: Items with custom values show a ✏️ emoji badge next to the points/cost number. 6. **Activation Behavior**: The second click on an item activates it (triggers task/reward), not the first click. **Architecture Decisions:** - **Storage**: Use a separate `child_overrides.json` table (not embedded in child model) to store per-child customizations. - **Lifecycle**: Overrides reset to default when a child is unassigned from a task/reward. Overrides are deleted when the entity or child is deleted (cascade). - **Validation**: Allow 0 points/cost (not minimum 1). Disable save button on invalid input (empty, negative, >10000). - **UI Flow**: First click centers item and shows edit button. Second click activates entity. Edit button opens modal for customization. **UI:** - Before first click: [feat-dynamic-points-before.png](feat-dynamic-points-before.png) - After first click: [feat-dynamic-points-after.png](feat-dynamic-points-after.png) - Edit button icon: `frontend/vue-app/public/edit.png` (34x34px) - Button position: Corner of ScrollingList item, not interfering with text - Badge: ✏️ emoji displayed next to points/cost number when override exists --- ## Configuration **No new configuration required.** Range validation (0-10000) is hardcoded per requirements. --- ## Data Model Changes ### New Model: `ChildOverride` **Python** (`backend/models/child_override.py`): Create a dataclass that inherits from `BaseModel` with the following fields: - `child_id` (str): ID of the child this override applies to - `entity_id` (str): ID of the task/penalty/reward being customized - `entity_type` (Literal['task', 'reward']): Type of entity - `custom_value` (int): Custom points or cost value Validation requirements: - `custom_value` must be between 0 and 10000 (inclusive) - `entity_type` must be either 'task' or 'reward' - Include `__post_init__` method to enforce these validations - Include static factory method `create_override()` that accepts the four main fields and returns a new instance **TypeScript** (`frontend/vue-app/src/common/models.ts`): Create an interface with 1:1 parity to the Python model: - Define `EntityType` as a union type: 'task' | 'reward' - Include all fields: `id`, `child_id`, `entity_id`, `entity_type`, `custom_value`, `created_at`, `updated_at` - All string fields except `custom_value` which is number ### Database Table **New Table**: `child_overrides.json` **Indexes**: - `child_id` (for lookup by child) - `entity_id` (for lookup by task/reward) - Composite `(child_id, entity_id)` (for uniqueness constraint) **Database Helper** (`backend/db/child_overrides.py`): Create database helper functions using TinyDB and the `child_overrides_db` table: - `insert_override(override)`: Insert or update (upsert) based on composite key (child_id, entity_id). Only one override allowed per child-entity pair. - `get_override(child_id, entity_id)`: Return Optional[ChildOverride] for a specific child and entity combination - `get_overrides_for_child(child_id)`: Return List[ChildOverride] for all overrides belonging to a child - `delete_override(child_id, entity_id)`: Delete specific override, return bool indicating success - `delete_overrides_for_child(child_id)`: Delete all overrides for a child, return count deleted - `delete_overrides_for_entity(entity_id)`: Delete all overrides for an entity, return count deleted All functions should use `from_dict()` and `to_dict()` for model serialization. --- ## SSE Events ### 1. `child_override_set` **Emitted When**: A parent sets or updates a custom value for a task/reward. **Payload** (`backend/events/types/child_override_set.py`): Create a dataclass `ChildOverrideSetPayload` that inherits from `EventPayload` with a single field: - `override` (ChildOverride): The override object that was set **TypeScript** (`frontend/vue-app/src/common/backendEvents.ts`): Create an interface `ChildOverrideSetPayload` with: - `override` (ChildOverride): The override object that was set ### 2. `child_override_deleted` **Emitted When**: An override is deleted (manual reset, unassignment, or cascade). **Payload** (`backend/events/types/child_override_deleted.py`): Create a dataclass `ChildOverrideDeletedPayload` that inherits from `EventPayload` with three fields: - `child_id` (str): ID of the child - `entity_id` (str): ID of the entity - `entity_type` (str): Type of entity ('task' or 'reward') **TypeScript** (`frontend/vue-app/src/common/backendEvents.ts`): Create an interface `ChildOverrideDeletedPayload` with: - `child_id` (string): ID of the child - `entity_id` (string): ID of the entity - `entity_type` (string): Type of entity --- ## API Design ### 1. **PUT** `/child//override` **Purpose**: Set or update a custom value for a task/reward. **Auth**: User must own the child. **Request Body**: JSON object with three required fields: - `entity_id` (string): UUID of the task or reward - `entity_type` (string): Either "task" or "reward" - `custom_value` (number): Integer between 0 and 10000 **Validation**: - `entity_type` must be "task" or "reward" - `custom_value` must be 0-10000 - Entity must be assigned to child - Child must exist and belong to user **Response**: JSON object with a single key `override` containing the complete ChildOverride object with all fields (id, child_id, entity_id, entity_type, custom_value, created_at, updated_at in ISO format). **Errors**: - 404: Child not found or not owned - 404: Entity not assigned to child - 400: Invalid entity_type - 400: custom_value out of range **SSE**: Emits `child_override_set` to user. ### 2. **GET** `/child//overrides` **Purpose**: Get all overrides for a child. **Auth**: User must own the child. **Response**: JSON object with a single key `overrides` containing an array of ChildOverride objects. Each object includes all standard fields (id, child_id, entity_id, entity_type, custom_value, created_at, updated_at). **Errors**: - 404: Child not found or not owned ### 3. **DELETE** `/child//override/` **Purpose**: Delete an override (reset to default). **Auth**: User must own the child. **Response**: JSON object with `message` field set to "Override deleted". **Errors**: - 404: Child not found or not owned - 404: Override not found **SSE**: Emits `child_override_deleted` to user. ### Modified Endpoints Update these existing endpoints to include override information: 1. **GET** `/child//list-tasks` - Include `custom_value` in task objects if override exists 2. **GET** `/child//list-rewards` - Include `custom_value` in reward objects if override exists 3. **POST** `/child//trigger-task` - Use `custom_value` if override exists when awarding points 4. **POST** `/child//trigger-reward` - Use `custom_value` if override exists when deducting points 5. **PUT** `/child//set-tasks` - Delete overrides for unassigned tasks 6. **PUT** `/child//set-rewards` - Delete overrides for unassigned rewards --- ## Implementation Details ### File Structure **Backend**: - `backend/models/child_override.py` - ChildOverride model - `backend/db/child_overrides.py` - Database helpers - `backend/api/child_override_api.py` - New API endpoints (PUT, GET, DELETE) - `backend/events/types/child_override_set.py` - SSE event payload - `backend/events/types/child_override_deleted.py` - SSE event payload - `backend/events/types/event_types.py` - Add CHILD_OVERRIDE_SET, CHILD_OVERRIDE_DELETED enums - `backend/tests/test_child_override_api.py` - Unit tests **Frontend**: - `frontend/vue-app/src/common/models.ts` - Add ChildOverride interface - `frontend/vue-app/src/common/api.ts` - Add setChildOverride(), getChildOverrides(), deleteChildOverride() - `frontend/vue-app/src/common/backendEvents.ts` - Add event types - `frontend/vue-app/src/components/OverrideEditModal.vue` - New modal component - `frontend/vue-app/src/components/ScrollingList.vue` - Add edit button and ✏️ badge - `frontend/vue-app/components/__tests__/OverrideEditModal.spec.ts` - Component tests ### Logging Strategy **Backend**: Log override operations to per-user rotating log files (same pattern as tracking). **Log Messages**: - `Override set: child={child_id}, entity={entity_id}, type={entity_type}, value={custom_value}` - `Override deleted: child={child_id}, entity={entity_id}` - `Overrides cascade deleted for child: child_id={child_id}, count={count}` - `Overrides cascade deleted for entity: entity_id={entity_id}, count={count}` **Frontend**: No additional logging beyond standard error handling. --- ## Acceptance Criteria (Definition of Done) ### Data Model - [x] `ChildOverride` Python dataclass created with validation (0-10000 range, entity_type literal) - [x] `ChildOverride` TypeScript interface created (1:1 parity with Python) - [x] `child_overrides.json` TinyDB table created in `backend/db/db.py` - [x] Database helper functions created (insert, get, delete by child, delete by entity) - [x] Composite uniqueness constraint enforced (child_id, entity_id) ### Backend Implementation - [x] PUT `/child//override` endpoint created with validation - [x] GET `/child//overrides` endpoint created - [x] DELETE `/child//override/` endpoint created - [x] GET `/child//list-tasks` modified to include `custom_value` when override exists - [x] GET `/child//list-rewards` modified to include `custom_value` when override exists - [x] POST `/child//trigger-task` modified to use override value - [x] POST `/child//trigger-reward` modified to use override value - [x] PUT `/child//set-tasks` modified to delete overrides for unassigned tasks - [x] PUT `/child//set-rewards` modified to delete overrides for unassigned rewards - [x] Cascade delete implemented: deleting child removes all its overrides - [x] Cascade delete implemented: deleting task/reward removes all its overrides - [x] Authorization checks: user must own child to access overrides - [x] Validation: entity must be assigned to child before override can be set ### SSE Events - [x] `child_override_set` event type added to event_types.py - [x] `child_override_deleted` event type added to event_types.py - [x] `ChildOverrideSetPayload` class created (Python) - [x] `ChildOverrideDeletedPayload` class created (Python) - [x] PUT endpoint emits `child_override_set` event - [x] DELETE endpoint emits `child_override_deleted` event - [x] Frontend TypeScript interfaces for event payloads created ### Frontend Implementation - [x] `OverrideEditModal.vue` component created - [x] Modal has number input field with 0-10000 validation - [x] Modal disables save button on invalid input (empty, negative, >10000) - [x] Modal defaults to current override value or entity default - [x] Modal calls PUT `/child//override` API on save - [x] Edit button (34x34px) added to ScrollingList items - [x] Edit button only appears after first click (when item is centered) - [x] Edit button uses `edit.png` icon from public folder - [x] ✏️ emoji badge displayed next to points/cost when override exists - [x] Badge only shows for items with active overrides - [x] Second click on item activates entity (not first click) - [x] SSE listeners registered for `child_override_set` and `child_override_deleted` - [x] Real-time UI updates when override events received ### Backend Unit Tests #### API Tests (`backend/tests/test_child_override_api.py`) - [x] Test PUT creates new override with valid data - [x] Test PUT updates existing override - [x] Test PUT returns 400 for custom_value < 0 - [x] Test PUT returns 400 for custom_value > 10000 - [x] Test PUT returns 400 for invalid entity_type - [ ] Test PUT returns 404 for non-existent child - [ ] Test PUT returns 404 for unassigned entity - [ ] Test PUT returns 403 for child not owned by user - [ ] Test PUT emits child_override_set event - [x] Test GET returns all overrides for child - [ ] Test GET returns empty array when no overrides - [ ] Test GET returns 404 for non-existent child - [ ] Test GET returns 403 for child not owned by user - [x] Test DELETE removes override - [ ] Test DELETE returns 404 when override doesn't exist - [ ] Test DELETE returns 404 for non-existent child - [ ] Test DELETE returns 403 for child not owned by user - [ ] Test DELETE emits child_override_deleted event #### Integration Tests - [ ] Test list-tasks includes custom_value for overridden tasks - [ ] Test list-tasks shows default points for non-overridden tasks - [ ] Test list-rewards includes custom_value for overridden rewards - [ ] Test trigger-task uses custom_value when awarding points - [ ] Test trigger-task uses default points when no override - [ ] Test trigger-reward uses custom_value when deducting points - [ ] Test trigger-reward uses default cost when no override - [ ] Test set-tasks deletes overrides for unassigned tasks - [ ] Test set-tasks preserves overrides for still-assigned tasks - [ ] Test set-rewards deletes overrides for unassigned rewards - [ ] Test set-rewards preserves overrides for still-assigned rewards #### Cascade Delete Tests - [x] Test deleting child removes all its overrides - [x] Test deleting task removes all overrides for that task - [x] Test deleting reward removes all overrides for that reward - [x] Test unassigning task from child deletes override - [x] Test reassigning task to child resets override (not preserved) #### Edge Cases - [x] Test custom_value = 0 is allowed - [x] Test custom_value = 10000 is allowed - [ ] Test cannot set override for entity not assigned to child - [ ] Test cannot set override for non-existent entity - [ ] Test multiple children can have different overrides for same entity ### Frontend Unit Tests #### Component Tests (`components/__tests__/OverrideEditModal.spec.ts`) - [x] Test modal renders with default value - [x] Test modal renders with existing override value - [x] Test save button disabled when input is empty - [x] Test save button disabled when value < 0 - [x] Test save button disabled when value > 10000 - [x] Test save button enabled when value is 0-10000 - [x] Test modal calls API with correct parameters on save - [x] Test modal emits close event after successful save - [x] Test modal shows error message on API failure - [x] Test cancel button closes modal without saving #### Component Tests (`components/__tests__/ScrollingList.spec.ts`) - [ ] Test edit button hidden before first click - [ ] Test edit button appears after first click (when centered) - [ ] Test edit button opens OverrideEditModal - [ ] Test ✏️ badge displayed when override exists - [ ] Test ✏️ badge hidden when no override exists - [ ] Test second click activates entity (not first click) - [ ] Test edit button positioned correctly (34x34px, corner) - [ ] Test edit button doesn't interfere with text #### Integration Tests - [ ] Test SSE event updates UI when override is set - [ ] Test SSE event updates UI when override is deleted - [ ] Test override value displayed in task/reward list - [ ] Test points calculation uses override when triggering task - [ ] Test cost calculation uses override when triggering reward #### Edge Cases - [ ] Test 0 points/cost displays correctly - [ ] Test 10000 points/cost displays correctly - [ ] Test badge updates immediately after setting override - [ ] Test badge disappears immediately after deleting override ### Logging & Monitoring - [ ] Override set operations logged to per-user log files - [ ] Override delete operations logged - [ ] Cascade delete operations logged with count - [ ] Log messages include child_id, entity_id, entity_type, custom_value ### Documentation - [ ] API endpoints documented in this spec - [ ] Data model documented in this spec - [ ] SSE events documented in this spec - [ ] UI behavior documented in this spec - [ ] Edge cases and validation rules documented --- ## Testing Strategy ### Unit Test Files **Backend** (`backend/tests/test_child_override_api.py`): Create six test classes: 1. **TestChildOverrideModel**: Test model validation (6 tests) - Valid override creation - Negative custom_value raises ValueError - custom_value > 10000 raises ValueError - custom_value = 0 is allowed - custom_value = 10000 is allowed - Invalid entity_type raises ValueError 2. **TestChildOverrideDB**: Test database operations (8 tests) - Insert new override - Insert updates existing (upsert behavior) - Get existing override returns object - Get nonexistent override returns None - Get all overrides for a child - Delete specific override - Delete all overrides for a child (returns count) - Delete all overrides for an entity (returns count) 3. **TestChildOverrideAPI**: Test all three API endpoints (18 tests) - PUT creates new override - PUT updates existing override - PUT returns 400 for negative value - PUT returns 400 for value > 10000 - PUT returns 400 for invalid entity_type - PUT returns 404 for nonexistent child - PUT returns 404 for unassigned entity - PUT returns 403 for child not owned by user - PUT emits child_override_set event - GET returns all overrides for child - GET returns empty array when no overrides - GET returns 404 for nonexistent child - GET returns 403 for child not owned - DELETE removes override successfully - DELETE returns 404 when override doesn't exist - DELETE returns 404 for nonexistent child - DELETE returns 403 for child not owned - DELETE emits child_override_deleted event 4. **TestIntegration**: Test override integration with existing endpoints (11 tests) - list-tasks includes custom_value for overridden tasks - list-tasks shows default points for non-overridden tasks - list-rewards includes custom_value for overridden rewards - trigger-task uses custom_value when awarding points - trigger-task uses default points when no override - trigger-reward uses custom_value when deducting points - trigger-reward uses default cost when no override - set-tasks deletes overrides for unassigned tasks - set-tasks preserves overrides for still-assigned tasks - set-rewards deletes overrides for unassigned rewards - set-rewards preserves overrides for still-assigned rewards 5. **TestCascadeDelete**: Test cascade deletion behavior (5 tests) - Deleting child removes all its overrides - Deleting task removes all overrides for that task - Deleting reward removes all overrides for that reward - Unassigning task deletes override - Reassigning task resets override (not preserved) 6. **TestEdgeCases**: Test boundary conditions (5 tests) - custom_value = 0 is allowed - custom_value = 10000 is allowed - Cannot set override for unassigned entity - Cannot set override for nonexistent entity - Multiple children can have different overrides for same entity ### Test Fixtures Create pytest fixtures for common test scenarios: - `child_with_task`: Uses existing `child` and `task` fixtures, calls set-tasks endpoint to assign task to child, asserts 200 response, returns child dict - `child_with_task_override`: Builds on `child_with_task`, calls PUT override endpoint to set custom_value=15 for the task, asserts 200 response, returns child dict - Similar fixtures for rewards: `child_with_reward`, `child_with_reward_override` - `child_with_overrides`: Child with multiple overrides for testing bulk operations ### Assertions Test assertions should verify three main areas: 1. **API Response Correctness**: Check status code (200, 400, 403, 404), verify returned override object has correct values for all fields (custom_value, child_id, entity_id, etc.) 2. **SSE Event Emission**: Use mock_sse fixture to assert `send_event_for_current_user` was called exactly once with the correct EventType (CHILD_OVERRIDE_SET or CHILD_OVERRIDE_DELETED) 3. **Points Calculation**: After triggering tasks/rewards, verify the child's points reflect the custom_value (not the default). For example, if default is 10 but override is 15, child.points should increase by 15. --- ## Future Considerations 1. **Bulk Override Management**: Add endpoint to set/get/delete multiple overrides at once for performance. 2. **Override History**: Track changes to override values over time for analytics. 3. **Copy Overrides**: Allow copying overrides from one child to another. 4. **Override Templates**: Save common override patterns as reusable templates. 5. **Percentage-Based Overrides**: Allow setting overrides as percentage of default (e.g., "150% of default"). 6. **Override Expiration**: Add optional expiration dates for temporary adjustments. 7. **Undo Override**: Add "Restore Default" button in UI that deletes override with one click. 8. **Admin Dashboard**: Show overview of all overrides across all children for analysis.