# 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/<child_id>/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/<child_id>/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/<child_id>/override/<entity_id>`

**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/<child_id>/list-tasks` - Include `custom_value` in task objects if override exists
2. **GET** `/child/<child_id>/list-rewards` - Include `custom_value` in reward objects if override exists
3. **POST** `/child/<child_id>/trigger-task` - Use `custom_value` if override exists when awarding points
4. **POST** `/child/<child_id>/trigger-reward` - Use `custom_value` if override exists when deducting points
5. **PUT** `/child/<child_id>/set-tasks` - Delete overrides for unassigned tasks
6. **PUT** `/child/<child_id>/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/<child_id>/override` endpoint created with validation
- [x] GET `/child/<child_id>/overrides` endpoint created
- [x] DELETE `/child/<child_id>/override/<entity_id>` endpoint created
- [x] GET `/child/<child_id>/list-tasks` modified to include `custom_value` when override exists
- [x] GET `/child/<child_id>/list-rewards` modified to include `custom_value` when override exists
- [x] POST `/child/<child_id>/trigger-task` modified to use override value
- [x] POST `/child/<child_id>/trigger-reward` modified to use override value
- [x] PUT `/child/<child_id>/set-tasks` modified to delete overrides for unassigned tasks
- [x] PUT `/child/<child_id>/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/<id>/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.
