from time import sleep from flask import Blueprint, request, jsonify from tinydb import Query from api.child_rewards import ChildReward from api.child_tasks import ChildTask from api.pending_reward import PendingReward as PendingRewardResponse from api.reward_status import RewardStatus from api.utils import send_event_for_current_user from db.db import child_db, task_db, reward_db, pending_reward_db from db.tracking import insert_tracking_event from db.child_overrides import get_override, delete_override, delete_overrides_for_child from events.types.child_modified import ChildModified from events.types.child_reward_request import ChildRewardRequest from events.types.child_reward_triggered import ChildRewardTriggered from events.types.child_rewards_set import ChildRewardsSet from events.types.child_task_triggered import ChildTaskTriggered from events.types.child_tasks_set import ChildTasksSet from events.types.tracking_event_created import TrackingEventCreated from events.types.event import Event from events.types.event_types import EventType from models.child import Child from models.pending_reward import PendingReward from models.reward import Reward from models.task import Task from models.tracking_event import TrackingEvent from api.utils import get_validated_user_id from utils.tracking_logger import log_tracking_event from collections import defaultdict import logging child_api = Blueprint('child_api', __name__) logger = logging.getLogger(__name__) @child_api.route('/child/', methods=['GET']) @child_api.route('/child/', methods=['GET']) def get_child(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 ChildQuery = Query() result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id)) if not result: return jsonify({'error': 'Child not found'}), 404 return jsonify(Child.from_dict(result[0]).to_dict()), 200 @child_api.route('/child/add', methods=['PUT']) def add_child(): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 data = request.get_json() name = data.get('name') age = data.get('age') image = data.get('image_id', None) if not name: return jsonify({'error': 'Name is required'}), 400 if not image: image = 'boy01' child = Child(name=name, age=age, image_id=image, user_id=user_id) child_db.insert(child.to_dict()) resp = send_event_for_current_user( Event(EventType.CHILD_MODIFIED.value, ChildModified(child.id, ChildModified.OPERATION_ADD))) if resp: return resp return jsonify({'message': f'Child {name} added.'}), 201 @child_api.route('/child//edit', methods=['PUT']) def edit_child(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 data = request.get_json() name = data.get('name', None) age = data.get('age', None) points = data.get('points', None) image = data.get('image_id', None) ChildQuery = Query() result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id)) if not result: return jsonify({'error': 'Child not found'}), 404 child = Child.from_dict(result[0]) if name is not None: child.name = name if age is not None: child.age = age if points is not None: child.points = points if image is not None: child.image_id = image # Check if points changed and handle pending rewards if points is not None: PendingQuery = Query() pending_rewards = pending_reward_db.search((PendingQuery.child_id == id) & (PendingQuery.user_id == user_id)) RewardQuery = Query() for pr in pending_rewards: pending = PendingReward.from_dict(pr) reward_result = reward_db.get((RewardQuery.id == pending.reward_id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None))) if reward_result: reward = Reward.from_dict(reward_result) # If child can no longer afford the reward, remove the pending request if child.points < reward.cost: pending_reward_db.remove( (PendingQuery.child_id == id) & (PendingQuery.reward_id == reward.id) & (PendingQuery.user_id == user_id) ) resp = send_event_for_current_user( Event(EventType.CHILD_REWARD_REQUEST.value, ChildRewardRequest(id, reward.id, ChildRewardRequest.REQUEST_CANCELLED))) if resp: return resp child_db.update(child.to_dict(), ChildQuery.id == id) resp = send_event_for_current_user(Event(EventType.CHILD_MODIFIED.value, ChildModified(id, ChildModified.OPERATION_EDIT))) if resp: return resp return jsonify({'message': f'Child {id} updated.'}), 200 @child_api.route('/child/list', methods=['GET']) def list_children(): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 ChildQuery = Query() children = child_db.search(ChildQuery.user_id == user_id) return jsonify({'children': children}), 200 # Child DELETE @child_api.route('/child/', methods=['DELETE']) def delete_child(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 ChildQuery = Query() # Cascade delete overrides for this child deleted_count = delete_overrides_for_child(id) if deleted_count > 0: logger.info(f"Cascade deleted {deleted_count} overrides for child {id}") if child_db.remove((ChildQuery.id == id) & (ChildQuery.user_id == user_id)): resp = send_event_for_current_user(Event(EventType.CHILD_MODIFIED.value, ChildModified(id, ChildModified.OPERATION_DELETE))) if resp: return resp return jsonify({'message': f'Child {id} deleted.'}), 200 return jsonify({'error': 'Child not found'}), 404 @child_api.route('/child//assign-task', methods=['POST']) def assign_task_to_child(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 data = request.get_json() task_id = data.get('task_id') if not task_id: return jsonify({'error': 'task_id is required'}), 400 ChildQuery = Query() result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id)) if not result: return jsonify({'error': 'Child not found'}), 404 child = result[0] if task_id not in child.get('tasks', []): child['tasks'].append(task_id) child_db.update({'tasks': child['tasks']}, ChildQuery.id == id) return jsonify({'message': f"Task {task_id} assigned to {child.get('name')}."}), 200 # python @child_api.route('/child//set-tasks', methods=['PUT']) def set_child_tasks(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 data = request.get_json() or {} task_ids = data.get('task_ids') if 'type' not in data: return jsonify({'error': 'type is required (good or bad)'}), 400 task_type = data.get('type', 'good') if task_type not in ['good', 'bad']: return jsonify({'error': 'type must be either good or bad'}), 400 is_good = task_type == 'good' if not isinstance(task_ids, list): return jsonify({'error': 'task_ids must be a list'}), 400 ChildQuery = Query() result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id)) if not result: return jsonify({'error': 'Child not found'}), 404 child = Child.from_dict(result[0]) new_task_ids = set(task_ids) # Add all existing child tasks of the opposite type for task in task_db.all(): if task['id'] in child.tasks and task['is_good'] != is_good: new_task_ids.add(task['id']) # Convert back to list if needed new_tasks = list(new_task_ids) # Identify unassigned tasks and delete their overrides old_task_ids = set(child.tasks) unassigned_task_ids = old_task_ids - new_task_ids for task_id in unassigned_task_ids: # Only delete overrides for task entities override = get_override(id, task_id) if override and override.entity_type == 'task': delete_override(id, task_id) logger.info(f"Deleted override for unassigned task: child={id}, task={task_id}") # Replace tasks with validated IDs child_db.update({'tasks': new_tasks}, ChildQuery.id == id) resp = send_event_for_current_user(Event(EventType.CHILD_TASKS_SET.value, ChildTasksSet(id, new_tasks))) if resp: return resp return jsonify({ 'message': f'Tasks set for child {id}.', 'task_ids': new_tasks, 'count': len(new_tasks) }), 200 @child_api.route('/child//remove-task', methods=['POST']) def remove_task_from_child(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 data = request.get_json() task_id = data.get('task_id') if not task_id: return jsonify({'error': 'task_id is required'}), 400 ChildQuery = Query() result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id)) if not result: return jsonify({'error': 'Child not found'}), 404 child = result[0] if task_id in child.get('tasks', []): child['tasks'].remove(task_id) child_db.update({'tasks': child['tasks']}, ChildQuery.id == id) return jsonify({'message': f'Task {task_id} removed from {child["name"]}.'}), 200 return jsonify({'error': 'Task not assigned to child'}), 400 @child_api.route('/child//list-tasks', methods=['GET']) def list_child_tasks(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 ChildQuery = Query() result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id)) if not result: return jsonify({'error': 'Child not found'}), 404 child = result[0] task_ids = child.get('tasks', []) TaskQuery = Query() child_tasks = [] for tid in task_ids: task = task_db.get((TaskQuery.id == tid) & ((TaskQuery.user_id == user_id) | (TaskQuery.user_id == None))) if not task: continue # Check for override override = get_override(id, tid) custom_value = override.custom_value if override else None ct = ChildTask(task.get('name'), task.get('is_good'), task.get('points'), task.get('image_id'), task.get('id')) ct_dict = ct.to_dict() if custom_value is not None: ct_dict['custom_value'] = custom_value child_tasks.append(ct_dict) return jsonify({'tasks': child_tasks}), 200 @child_api.route('/child//list-assignable-tasks', methods=['GET']) def list_assignable_tasks(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 ChildQuery = Query() result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id)) if not result: return jsonify({'error': 'Child not found'}), 404 child = result[0] assigned_ids = set(child.get('tasks', [])) # Get all assignable tasks (not already assigned) all_tasks = [t for t in task_db.all() if t and t.get('id') and t.get('id') not in assigned_ids] # Group by name from collections import defaultdict name_to_tasks = defaultdict(list) for t in all_tasks: name_to_tasks[t.get('name')].append(t) filtered_tasks = [] for name, tasks in name_to_tasks.items(): user_tasks = [t for t in tasks if t.get('user_id') is not None] if len(user_tasks) == 0: # Only system task exists filtered_tasks.append(tasks[0]) elif len(user_tasks) == 1: # Only one user task: show it, not system filtered_tasks.append(user_tasks[0]) else: # Multiple user tasks: show all user tasks, not system filtered_tasks.extend(user_tasks) # Wrap in ChildTask and return assignable_tasks = [ChildTask(t.get('name'), t.get('is_good'), t.get('points'), t.get('image_id'), t.get('id')).to_dict() for t in filtered_tasks] return jsonify({'tasks': assignable_tasks, 'count': len(assignable_tasks)}), 200 @child_api.route('/child//list-all-tasks', methods=['GET']) def list_all_tasks(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 ChildQuery = Query() result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id)) if not result: return jsonify({'error': 'Child not found'}), 404 has_type = "type" in request.args if has_type and request.args.get('type') not in ['good', 'bad']: return jsonify({'error': 'type must be either good or bad'}), 400 good = request.args.get('type', False) == 'good' child = result[0] assigned_ids = set(child.get('tasks', [])) # Get all tasks from database (not filtering out assigned, since this is 'all') ChildTaskQuery = Query() all_tasks = task_db.search((ChildTaskQuery.user_id == user_id) | (ChildTaskQuery.user_id == None)) name_to_tasks = defaultdict(list) for t in all_tasks: name_to_tasks[t.get('name')].append(t) filtered_tasks = [] for name, tasks in name_to_tasks.items(): user_tasks = [t for t in tasks if t.get('user_id') is not None] if len(user_tasks) == 0: filtered_tasks.append(tasks[0]) elif len(user_tasks) == 1: filtered_tasks.append(user_tasks[0]) else: filtered_tasks.extend(user_tasks) result_tasks = [] for t in filtered_tasks: if has_type and t.get('is_good') != good: continue ct = ChildTask( t.get('name'), t.get('is_good'), t.get('points'), t.get('image_id'), t.get('id') ) task_dict = ct.to_dict() task_dict.update({'assigned': t.get('id') in assigned_ids}) result_tasks.append(task_dict) result_tasks.sort(key=lambda t: (not t['assigned'], t['name'].lower())) return jsonify({ 'tasks': result_tasks, 'count': len(result_tasks), 'list_type': 'task' }), 200 @child_api.route('/child//trigger-task', methods=['POST']) def trigger_child_task(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 data = request.get_json() task_id = data.get('task_id') if not task_id: return jsonify({'error': 'task_id is required'}), 400 ChildQuery = Query() result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id)) if not result: return jsonify({'error': 'Child not found'}), 404 child: Child = Child.from_dict(result[0]) if task_id not in child.tasks: return jsonify({'error': f'Task not found assigned to child {child.name}'}), 404 # look up the task and get the details TaskQuery = Query() task_result = task_db.search((TaskQuery.id == task_id) & ((TaskQuery.user_id == user_id) | (TaskQuery.user_id == None))) if not task_result: return jsonify({'error': 'Task not found in task database'}), 404 task: Task = Task.from_dict(task_result[0]) # Capture points before modification points_before = child.points # Check for override override = get_override(id, task_id) points_value = override.custom_value if override else task.points # update the child's points based on task type if task.is_good: child.points += points_value else: child.points -= points_value child.points = max(child.points, 0) # update the child in the database child_db.update({'points': child.points}, ChildQuery.id == id) # Create tracking event entity_type = 'penalty' if not task.is_good else 'task' tracking_metadata = { 'task_name': task.name, 'is_good': task.is_good, 'default_points': task.points } if override: tracking_metadata['custom_points'] = override.custom_value tracking_metadata['has_override'] = True tracking_event = TrackingEvent.create_event( user_id=user_id, child_id=child.id, entity_type=entity_type, entity_id=task.id, action='activated', points_before=points_before, points_after=child.points, metadata=tracking_metadata ) insert_tracking_event(tracking_event) log_tracking_event(tracking_event) # Send tracking event via SSE send_event_for_current_user(Event(EventType.TRACKING_EVENT_CREATED.value, TrackingEventCreated(tracking_event.id, child.id, entity_type, 'activated'))) resp = send_event_for_current_user(Event(EventType.CHILD_TASK_TRIGGERED.value, ChildTaskTriggered(task.id, child.id, child.points))) if resp: return resp return jsonify({'message': f'{task.name} points assigned to {child.name}.', 'points': child.points, 'id': child.id}), 200 @child_api.route('/child//assign-reward', methods=['POST']) def assign_reward_to_child(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 data = request.get_json() reward_id = data.get('reward_id') if not reward_id: return jsonify({'error': 'reward_id is required'}), 400 ChildQuery = Query() result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id)) if not result: return jsonify({'error': 'Child not found'}), 404 child = result[0] if reward_id not in child.get('rewards', []): child['rewards'].append(reward_id) child_db.update({'rewards': child['rewards']}, ChildQuery.id == id) return jsonify({'message': f'Reward {reward_id} assigned to {child["name"]}.'}), 200 @child_api.route('/child//list-all-rewards', methods=['GET']) def list_all_rewards(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 ChildQuery = Query() result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id)) if not result: return jsonify({'error': 'Child not found'}), 404 child = Child.from_dict(result[0]) assigned_ids = set(child.rewards) # Get all rewards from database ChildRewardQuery = Query() all_rewards = reward_db.search((ChildRewardQuery.user_id == user_id) | (ChildRewardQuery.user_id == None)) from collections import defaultdict name_to_rewards = defaultdict(list) for r in all_rewards: name_to_rewards[r.get('name')].append(r) filtered_rewards = [] for name, rewards in name_to_rewards.items(): user_rewards = [r for r in rewards if r.get('user_id') is not None] if len(user_rewards) == 0: filtered_rewards.append(rewards[0]) elif len(user_rewards) == 1: filtered_rewards.append(user_rewards[0]) else: filtered_rewards.extend(user_rewards) result_rewards = [] for r in filtered_rewards: cr = ChildReward( r.get('name'), r.get('cost'), r.get('image_id'), r.get('id') ) reward_dict = cr.to_dict() reward_dict.update({'assigned': r.get('id') in assigned_ids}) result_rewards.append(reward_dict) result_rewards.sort(key=lambda t: (not t['assigned'], t['name'].lower())) return jsonify({ 'rewards': result_rewards, 'rewards_count': len(result_rewards), 'list_type': 'reward' }), 200 @child_api.route('/child//set-rewards', methods=['PUT']) def set_child_rewards(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 data = request.get_json() or {} reward_ids = data.get('reward_ids') if not isinstance(reward_ids, list): return jsonify({'error': 'reward_ids must be a list'}), 400 # Deduplicate and drop falsy values new_reward_ids = [rid for rid in dict.fromkeys(reward_ids) if rid] ChildQuery = Query() result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id)) if not result: return jsonify({'error': 'Child not found'}), 404 child = Child.from_dict(result[0]) old_reward_ids = set(child.rewards) # Optional: validate reward IDs exist in the reward DB RewardQuery = Query() valid_reward_ids = [] for rid in new_reward_ids: if reward_db.get((RewardQuery.id == rid) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None))): valid_reward_ids.append(rid) # Identify unassigned rewards and delete their overrides new_reward_ids_set = set(valid_reward_ids) unassigned_reward_ids = old_reward_ids - new_reward_ids_set for reward_id in unassigned_reward_ids: override = get_override(id, reward_id) if override and override.entity_type == 'reward': delete_override(id, reward_id) logger.info(f"Deleted override for unassigned reward: child={id}, reward={reward_id}") # Replace rewards with validated IDs child_db.update({'rewards': valid_reward_ids}, ChildQuery.id == id) send_event_for_current_user(Event(EventType.CHILD_REWARDS_SET.value, ChildRewardsSet(id, valid_reward_ids))) return jsonify({ 'message': f'Rewards set for child {id}.', 'reward_ids': valid_reward_ids, 'count': len(valid_reward_ids) }), 200 @child_api.route('/child//remove-reward', methods=['POST']) def remove_reward_from_child(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 data = request.get_json() reward_id = data.get('reward_id') if not reward_id: return jsonify({'error': 'reward_id is required'}), 400 ChildQuery = Query() result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id)) if not result: return jsonify({'error': 'Child not found'}), 404 child = result[0] if reward_id in child.get('rewards', []): child['rewards'].remove(reward_id) child_db.update({'rewards': child['rewards']}, ChildQuery.id == id) return jsonify({'message': f'Reward {reward_id} removed from {child["name"]}.'}), 200 return jsonify({'error': 'Reward not assigned to child'}), 400 @child_api.route('/child//list-rewards', methods=['GET']) def list_child_rewards(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 ChildQuery = Query() result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id)) if not result: return jsonify({'error': 'Child not found'}), 404 child = result[0] reward_ids = child.get('rewards', []) RewardQuery = Query() child_rewards = [] for rid in reward_ids: reward = reward_db.get((RewardQuery.id == rid) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None))) if not reward: continue # Check for override override = get_override(id, rid) custom_value = override.custom_value if override else None cr = ChildReward(reward.get('name'), reward.get('cost'), reward.get('image_id'), reward.get('id')) cr_dict = cr.to_dict() if custom_value is not None: cr_dict['custom_value'] = custom_value child_rewards.append(cr_dict) return jsonify({'rewards': child_rewards}), 200 @child_api.route('/child//list-assignable-rewards', methods=['GET']) def list_assignable_rewards(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 ChildQuery = Query() result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id)) if not result: return jsonify({'error': 'Child not found'}), 404 child = result[0] assigned_ids = set(child.get('rewards', [])) # Get all assignable rewards (not already assigned) all_rewards = [r for r in reward_db.all() if r and r.get('id') and r.get('id') not in assigned_ids] # Group by name from collections import defaultdict name_to_rewards = defaultdict(list) for r in all_rewards: name_to_rewards[r.get('name')].append(r) filtered_rewards = [] for name, rewards in name_to_rewards.items(): user_rewards = [r for r in rewards if r.get('user_id') is not None] if len(user_rewards) == 0: filtered_rewards.append(rewards[0]) elif len(user_rewards) == 1: filtered_rewards.append(user_rewards[0]) else: filtered_rewards.extend(user_rewards) assignable_rewards = [ChildReward(r.get('name'), r.get('cost'), r.get('image_id'), r.get('id')).to_dict() for r in filtered_rewards] return jsonify({'rewards': assignable_rewards, 'count': len(assignable_rewards)}), 200 @child_api.route('/child//trigger-reward', methods=['POST']) def trigger_child_reward(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 data = request.get_json() reward_id = data.get('reward_id') if not reward_id: return jsonify({'error': 'reward_id is required'}), 400 ChildQuery = Query() result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id)) if not result: return jsonify({'error': 'Child not found'}), 404 child: Child = Child.from_dict(result[0]) if reward_id not in child.rewards: return jsonify({'error': f'Reward not found assigned to child {child.name}'}), 404 # look up the task and get the details RewardQuery = Query() reward_result = reward_db.search((RewardQuery.id == reward_id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None))) if not reward_result: return jsonify({'error': 'Reward not found in reward database'}), 404 reward: Reward = Reward.from_dict(reward_result[0]) # Check for override override = get_override(id, reward_id) cost_value = override.custom_value if override else reward.cost # Check if child has enough points logger.info(f'Child {child.name} has {child.points} points, reward {reward.name} costs {cost_value} points') if child.points < cost_value: points_needed = cost_value - child.points return jsonify({ 'error': 'Insufficient points', 'points_needed': points_needed, 'current_points': child.points, 'reward_cost': cost_value }), 400 # Remove matching pending reward requests for this child and reward PendingQuery = Query() removed = pending_reward_db.remove( (PendingQuery.child_id == child.id) & (PendingQuery.reward_id == reward.id) ) if removed: send_event_for_current_user(Event(EventType.CHILD_REWARD_REQUEST.value, ChildRewardRequest(reward.id, child.id, ChildRewardRequest.REQUEST_GRANTED))) # Capture points before modification points_before = child.points # update the child's points based on reward cost child.points -= cost_value # update the child in the database child_db.update({'points': child.points}, ChildQuery.id == id) # Create tracking event tracking_metadata = { 'reward_name': reward.name, 'reward_cost': reward.cost, 'default_cost': reward.cost } if override: tracking_metadata['custom_cost'] = override.custom_value tracking_metadata['has_override'] = True tracking_event = TrackingEvent.create_event( user_id=user_id, child_id=child.id, entity_type='reward', entity_id=reward.id, action='redeemed', points_before=points_before, points_after=child.points, metadata=tracking_metadata ) insert_tracking_event(tracking_event) log_tracking_event(tracking_event) # Send tracking event via SSE send_event_for_current_user(Event(EventType.TRACKING_EVENT_CREATED.value, TrackingEventCreated(tracking_event.id, child.id, 'reward', 'redeemed'))) send_event_for_current_user(Event(EventType.CHILD_REWARD_TRIGGERED.value, ChildRewardTriggered(reward.id, child.id, child.points))) return jsonify({'message': f'{reward.name} assigned to {child.name}.', 'points': child.points, 'id': child.id}), 200 @child_api.route('/child//affordable-rewards', methods=['GET']) def list_affordable_rewards(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 ChildQuery = Query() result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id)) if not result: return jsonify({'error': 'Child not found'}), 404 child = Child.from_dict(result[0]) points = child.points reward_ids = child.rewards RewardQuery = Query() affordable = [ Reward.from_dict(reward).to_dict() for reward_id in reward_ids if (reward := reward_db.get((RewardQuery.id == reward_id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))) and points >= Reward.from_dict(reward).cost ] return jsonify({'affordable_rewards': affordable}), 200 @child_api.route('/child//reward-status', methods=['GET']) def reward_status(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 ChildQuery = Query() result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id)) if not result: return jsonify({'error': 'Child not found'}), 404 child = Child.from_dict(result[0]) points = child.points reward_ids = child.rewards RewardQuery = Query() statuses = [] for reward_id in reward_ids: reward_dict = reward_db.get((RewardQuery.id == reward_id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None))) if not reward_dict: continue reward: Reward = Reward.from_dict(reward_dict) # Check for override override = get_override(id, reward_id) cost_value = override.custom_value if override else reward.cost points_needed = max(0, cost_value - points) #check to see if this reward id and child id is in the pending rewards db if so set its redeeming flag to true pending_query = Query() pending = pending_reward_db.get((pending_query.child_id == child.id) & (pending_query.reward_id == reward.id) & (pending_query.user_id == user_id)) status = RewardStatus(reward.id, reward.name, points_needed, cost_value, pending is not None, reward.image_id) status_dict = status.to_dict() if override: status_dict['custom_value'] = override.custom_value statuses.append(status_dict) statuses.sort(key=lambda s: (not s['redeeming'], s['cost'])) return jsonify({'reward_status': statuses}), 200 @child_api.route('/child//request-reward', methods=['POST']) def request_reward(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 data = request.get_json() reward_id = data.get('reward_id') if not reward_id: return jsonify({'error': 'reward_id is required'}), 400 ChildQuery = Query() result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id)) if not result: return jsonify({'error': 'Child not found'}), 404 child = Child.from_dict(result[0]) if reward_id not in child.rewards: return jsonify({'error': f'Reward not assigned to child {child.name}'}), 404 RewardQuery = Query() reward_result = reward_db.search(RewardQuery.id == reward_id) if not reward_result: return jsonify({'error': 'Reward not found in reward database'}), 404 reward = Reward.from_dict(reward_result[0]) # Check if child has enough points logger.info(f'Child {child.name} has {child.points} points, reward {reward.name} costs {reward.cost} points') if child.points < reward.cost: points_needed = reward.cost - child.points return jsonify({ 'error': 'Insufficient points', 'points_needed': points_needed, 'current_points': child.points, 'reward_cost': reward.cost }), 400 pending = PendingReward(child_id=child.id, reward_id=reward.id, user_id=user_id) pending_reward_db.insert(pending.to_dict()) logger.info(f'Pending reward request created for child {child.name} for reward {reward.name}') # Create tracking event (no points change on request) tracking_event = TrackingEvent.create_event( user_id=user_id, child_id=child.id, entity_type='reward', entity_id=reward.id, action='requested', points_before=child.points, points_after=child.points, metadata={'reward_name': reward.name, 'reward_cost': reward.cost} ) insert_tracking_event(tracking_event) log_tracking_event(tracking_event) # Send tracking event via SSE send_event_for_current_user(Event(EventType.TRACKING_EVENT_CREATED.value, TrackingEventCreated(tracking_event.id, child.id, 'reward', 'requested'))) send_event_for_current_user(Event(EventType.CHILD_REWARD_REQUEST.value, ChildRewardRequest(child.id, reward.id, ChildRewardRequest.REQUEST_CREATED))) return jsonify({ 'message': f'Reward request for {reward.name} submitted for {child.name}.', 'reward_id': reward.id, 'reward_name': reward.name, 'child_id': child.id, 'child_name': child.name, 'cost': reward.cost }), 200 @child_api.route('/child//cancel-request-reward', methods=['POST']) def cancel_request_reward(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 data = request.get_json() reward_id = data.get('reward_id') if not reward_id: return jsonify({'error': 'reward_id is required'}), 400 ChildQuery = Query() result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id)) if not result: return jsonify({'error': 'Child not found'}), 404 child = Child.from_dict(result[0]) # Fetch reward details for tracking metadata RewardQuery = Query() reward_result = reward_db.get((RewardQuery.id == reward_id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None))) reward_name = reward_result.get('name') if reward_result else 'Unknown' reward_cost = reward_result.get('cost', 0) if reward_result else 0 # Remove matching pending reward request PendingQuery = Query() removed = pending_reward_db.remove( (PendingQuery.child_id == child.id) & (PendingQuery.reward_id == reward_id) & (PendingQuery.user_id == user_id) ) if not removed: return jsonify({'error': 'No pending request found for this reward'}), 404 # Create tracking event (no points change on cancel) tracking_event = TrackingEvent.create_event( user_id=user_id, child_id=child.id, entity_type='reward', entity_id=reward_id, action='cancelled', points_before=child.points, points_after=child.points, metadata={'reward_name': reward_name, 'reward_cost': reward_cost} ) insert_tracking_event(tracking_event) log_tracking_event(tracking_event) # Send tracking event via SSE send_event_for_current_user(Event(EventType.TRACKING_EVENT_CREATED.value, TrackingEventCreated(tracking_event.id, child.id, 'reward', 'cancelled'))) # Notify user that the request was cancelled resp = send_event_for_current_user(Event(EventType.CHILD_REWARD_REQUEST.value, ChildRewardRequest(child.id, reward_id, ChildRewardRequest.REQUEST_CANCELLED))) if resp: return resp return jsonify({ 'message': f'Reward request cancelled for {child.name}.', 'child_id': child.id, 'reward_id': reward_id, 'removed_count': len(removed) }), 200 @child_api.route('/pending-rewards', methods=['GET']) def list_pending_rewards(): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 PendingQuery = Query() pending_rewards = pending_reward_db.search(PendingQuery.user_id == user_id) reward_responses = [] RewardQuery = Query() ChildQuery = Query() for pr in pending_rewards: pending = PendingReward.from_dict(pr) # Look up reward details reward_result = reward_db.get((RewardQuery.id == pending.reward_id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None))) if not reward_result: continue reward = Reward.from_dict(reward_result) # Look up child details child_result = child_db.get(ChildQuery.id == pending.child_id) if not child_result: continue child = Child.from_dict(child_result) # Create response object response = PendingRewardResponse( _id=pending.id, child_id=child.id, child_name=child.name, child_image_id=child.image_id, reward_id=reward.id, reward_name=reward.name, reward_image_id=reward.image_id ) reward_responses.append(response.to_dict()) return jsonify({'rewards': reward_responses, 'count': len(reward_responses), 'list_type': 'notification'}), 200