from flask import Blueprint, request, jsonify from tinydb import Query from api.utils import send_event_for_current_user, get_validated_user_id from events.types.child_rewards_set import ChildRewardsSet from db.db import reward_db, child_db from db.child_overrides import delete_overrides_for_entity from events.types.event import Event from events.types.event_types import EventType from events.types.reward_modified import RewardModified from models.reward import Reward reward_api = Blueprint('reward_api', __name__) # Reward endpoints @reward_api.route('/reward/add', methods=['PUT']) def add_reward(): 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') description = data.get('description') cost = data.get('cost') image = data.get('image_id', '') if not name or description is None or cost is None: return jsonify({'error': 'Name, description, and cost are required'}), 400 reward = Reward(name=name, description=description, cost=cost, image_id=image, user_id=user_id) reward_db.insert(reward.to_dict()) send_event_for_current_user(Event(EventType.REWARD_MODIFIED.value, RewardModified(reward.id, RewardModified.OPERATION_ADD))) return jsonify({'message': f'Reward {name} added.'}), 201 @reward_api.route('/reward/', methods=['GET']) def get_reward(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 RewardQuery = Query() result = reward_db.search((RewardQuery.id == id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None))) if not result: return jsonify({'error': 'Reward not found'}), 404 return jsonify(result[0]), 200 @reward_api.route('/reward/list', methods=['GET']) def list_rewards(): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 ids_param = request.args.get('ids') RewardQuery = Query() rewards = reward_db.search((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)) if ids_param is not None: if ids_param.strip() == '': rewards = [] else: ids = set(ids_param.split(',')) rewards = [reward for reward in rewards if reward.get('id') in ids] # Filter out default rewards if user-specific version exists (case/whitespace-insensitive) user_rewards = {r['name'].strip().lower(): r for r in rewards if r.get('user_id') == user_id} filtered_rewards = [] for r in rewards: if r.get('user_id') is None and r['name'].strip().lower() in user_rewards: continue # Skip default if user version exists filtered_rewards.append(r) # Sort: user-created items first (by name), then default items (by name) user_created = sorted([r for r in filtered_rewards if r.get('user_id') == user_id], key=lambda x: x['name'].lower()) default_items = sorted([r for r in filtered_rewards if r.get('user_id') is None], key=lambda x: x['name'].lower()) sorted_rewards = user_created + default_items return jsonify({'rewards': sorted_rewards}), 200 @reward_api.route('/reward/', methods=['DELETE']) def delete_reward(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 RewardQuery = Query() reward = reward_db.get(RewardQuery.id == id) if not reward: return jsonify({'error': 'Reward not found'}), 404 if reward.get('user_id') is None: import logging logging.warning(f"Forbidden delete attempt on system reward: id={id}, by user_id={user_id}") return jsonify({'error': 'System rewards cannot be deleted.'}), 403 removed = reward_db.remove((RewardQuery.id == id) & (RewardQuery.user_id == user_id)) if removed: # Cascade delete overrides for this reward deleted_count = delete_overrides_for_entity(id) if deleted_count > 0: import logging logging.info(f"Cascade deleted {deleted_count} overrides for reward {id}") # remove the reward id from any child's reward list ChildQuery = Query() for child in child_db.all(): rewards = child.get('rewards', []) if id in rewards: rewards.remove(id) child_db.update({'rewards': rewards}, ChildQuery.id == child.get('id')) send_event_for_current_user(Event(EventType.CHILD_REWARDS_SET.value, ChildRewardsSet(id, rewards))) send_event_for_current_user(Event(EventType.REWARD_MODIFIED.value, RewardModified(id, RewardModified.OPERATION_DELETE))) return jsonify({'message': f'Reward {id} deleted.'}), 200 return jsonify({'error': 'Reward not found'}), 404 @reward_api.route('/reward//edit', methods=['PUT']) def edit_reward(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 RewardQuery = Query() existing = reward_db.get((RewardQuery.id == id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None))) if not existing: return jsonify({'error': 'Reward not found'}), 404 reward = Reward.from_dict(existing) is_dirty = False data = request.get_json(force=True) or {} if 'name' in data: name = (data.get('name') or '').strip() if not name: return jsonify({'error': 'Name cannot be empty'}), 400 reward.name = name is_dirty = True if 'description' in data: desc = (data.get('description') or '').strip() if not desc: return jsonify({'error': 'Description cannot be empty'}), 400 reward.description = desc is_dirty = True if 'cost' in data: cost = data.get('cost') if not isinstance(cost, int): return jsonify({'error': 'Cost must be an integer'}), 400 if cost <= 0: return jsonify({'error': 'Cost must be a positive integer'}), 400 reward.cost = cost is_dirty = True if 'image_id' in data: reward.image_id = data.get('image_id', '') is_dirty = True if not is_dirty: return jsonify({'error': 'No valid fields to update'}), 400 if reward.user_id is None: # public reward new_reward = Reward(name=reward.name, description=reward.description, cost=reward.cost, image_id=reward.image_id, user_id=user_id) reward_db.insert(new_reward.to_dict()) send_event_for_current_user(Event(EventType.REWARD_MODIFIED.value, RewardModified(new_reward.id, RewardModified.OPERATION_ADD))) return jsonify(new_reward.to_dict()), 200 reward_db.update(reward.to_dict(), (RewardQuery.id == id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None))) send_event_for_current_user(Event(EventType.REWARD_MODIFIED.value, RewardModified(id, RewardModified.OPERATION_EDIT))) return jsonify(reward.to_dict()), 200