All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m8s
- Implemented routine child-mode flow tests to ensure proper functionality of routine assignment and task completion. - Created notification tests for parent view to verify routine completion notifications for children. - Developed ChildRoutineOverlay component for displaying routine tasks and handling user interactions. - Added RoutineApproveDialog component for approving or rejecting completed routines. - Created unit tests for ChildRoutineOverlay and RoutineEditView components to ensure correct behavior and rendering. - Enhanced RoutineEditView with proper handling of task addition and form submission. Co-authored-by: Copilot <copilot@github.com>
639 lines
25 KiB
Python
639 lines
25 KiB
Python
from collections import defaultdict
|
|
from datetime import datetime, timezone
|
|
from zoneinfo import ZoneInfo
|
|
|
|
from flask import Blueprint, request, jsonify
|
|
from tinydb import Query
|
|
|
|
from api.error_codes import ErrorCodes
|
|
from api.utils import get_validated_user_id, send_event_for_current_user
|
|
from db.child_overrides import delete_override, get_override
|
|
from db.db import child_db, pending_confirmations_db, routine_db, users_db
|
|
from db.routine_extensions import delete_extension_for_child_routine, get_extension_for_child_routine
|
|
from db.routine_items import get_items_for_routine
|
|
from db.routine_schedules import delete_schedule, get_schedule
|
|
from events.types.child_routine_confirmation import ChildRoutineConfirmation
|
|
from events.types.child_routines_set import ChildRoutinesSet
|
|
from events.types.event import Event
|
|
from events.types.event_types import EventType
|
|
from models.child import Child
|
|
from models.pending_confirmation import PendingConfirmation
|
|
from models.routine import Routine
|
|
from utils.digest_token import create_action_token
|
|
from utils.push_sender import send_push_to_user
|
|
|
|
child_routine_api = Blueprint('child_routine_api', __name__)
|
|
|
|
|
|
def _get_user_timezone(user_id: str) -> str | None:
|
|
user = users_db.get(Query().id == user_id)
|
|
if not user:
|
|
return None
|
|
return user.get('timezone')
|
|
|
|
|
|
def _get_user_today_local(user_id: str) -> tuple[str, str | None]:
|
|
tz_str = _get_user_timezone(user_id)
|
|
try:
|
|
now_local = datetime.now(ZoneInfo(tz_str)) if tz_str else datetime.now(timezone.utc)
|
|
except Exception:
|
|
tz_str = None
|
|
now_local = datetime.now(timezone.utc)
|
|
return now_local.strftime('%Y-%m-%d'), tz_str
|
|
|
|
|
|
def _is_iso_timestamp_on_local_day(timestamp: str | None, local_day: str, tz_str: str | None) -> bool:
|
|
if not timestamp:
|
|
return False
|
|
try:
|
|
normalized = timestamp.replace('Z', '+00:00')
|
|
parsed = datetime.fromisoformat(normalized)
|
|
if parsed.tzinfo is None:
|
|
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
try:
|
|
tz = ZoneInfo(tz_str) if tz_str else timezone.utc
|
|
except Exception:
|
|
tz = timezone.utc
|
|
return parsed.astimezone(tz).strftime('%Y-%m-%d') == local_day
|
|
except (TypeError, ValueError):
|
|
return False
|
|
|
|
|
|
def _is_epoch_timestamp_on_local_day(epoch_ts, local_day: str, tz_str: str | None) -> bool:
|
|
if epoch_ts is None:
|
|
return False
|
|
try:
|
|
tz = ZoneInfo(tz_str) if tz_str else timezone.utc
|
|
except Exception:
|
|
tz = timezone.utc
|
|
try:
|
|
return datetime.fromtimestamp(float(epoch_ts), tz).strftime('%Y-%m-%d') == local_day
|
|
except (TypeError, ValueError, OSError):
|
|
return False
|
|
|
|
|
|
class ChildRoutine:
|
|
def __init__(self, name, points, image_id, _id):
|
|
self.id = _id
|
|
self.name = name
|
|
self.points = points
|
|
self.image_id = image_id
|
|
|
|
def to_dict(self):
|
|
return {
|
|
'id': self.id,
|
|
'name': self.name,
|
|
'points': self.points,
|
|
'image_id': self.image_id,
|
|
}
|
|
|
|
|
|
def _validate_child_for_user(child_id: str, user_id: str):
|
|
child_q = Query()
|
|
result = child_db.search((child_q.id == child_id) & (child_q.user_id == user_id))
|
|
return Child.from_dict(result[0]) if result else None
|
|
|
|
|
|
def _resolve_routine_for_user(routine_id: str, user_id: str):
|
|
routine_q = Query()
|
|
routine_result = routine_db.get(
|
|
(routine_q.id == routine_id) & ((routine_q.user_id == user_id) | (routine_q.user_id == None))
|
|
)
|
|
return Routine.from_dict(routine_result) if routine_result else None
|
|
|
|
|
|
@child_routine_api.route('/child/<id>/assign-routine', methods=['POST'])
|
|
def assign_routine_to_child(id):
|
|
user_id = get_validated_user_id()
|
|
if not user_id:
|
|
return jsonify({'error': 'Unauthorized', 'code': ErrorCodes.UNAUTHORIZED}), 401
|
|
|
|
data = request.get_json() or {}
|
|
routine_id = data.get('routine_id')
|
|
if not routine_id:
|
|
return jsonify({'error': 'routine_id is required', 'code': ErrorCodes.MISSING_FIELD}), 400
|
|
|
|
child = _validate_child_for_user(id, user_id)
|
|
if not child:
|
|
return jsonify({'error': 'Child not found', 'code': ErrorCodes.CHILD_NOT_FOUND}), 404
|
|
|
|
routine = _resolve_routine_for_user(routine_id, user_id)
|
|
if not routine:
|
|
return jsonify({'error': 'Routine not found'}), 404
|
|
|
|
routine_ids = list(child.routines)
|
|
if routine_id not in routine_ids:
|
|
routine_ids.append(routine_id)
|
|
child_db.update({'routines': routine_ids}, Query().id == id)
|
|
|
|
send_event_for_current_user(
|
|
Event(EventType.CHILD_ROUTINES_SET.value, ChildRoutinesSet(id, routine_ids))
|
|
)
|
|
return jsonify({'message': f'Routine {routine_id} assigned to {child.name}.'}), 200
|
|
|
|
|
|
@child_routine_api.route('/child/<id>/remove-routine', methods=['POST'])
|
|
def remove_routine_from_child(id):
|
|
user_id = get_validated_user_id()
|
|
if not user_id:
|
|
return jsonify({'error': 'Unauthorized', 'code': ErrorCodes.UNAUTHORIZED}), 401
|
|
|
|
data = request.get_json() or {}
|
|
routine_id = data.get('routine_id')
|
|
if not routine_id:
|
|
return jsonify({'error': 'routine_id is required', 'code': ErrorCodes.MISSING_FIELD}), 400
|
|
|
|
child = _validate_child_for_user(id, user_id)
|
|
if not child:
|
|
return jsonify({'error': 'Child not found', 'code': ErrorCodes.CHILD_NOT_FOUND}), 404
|
|
|
|
routine_ids = list(child.routines)
|
|
if routine_id not in routine_ids:
|
|
return jsonify({'error': 'Routine not assigned to child', 'code': ErrorCodes.ENTITY_NOT_ASSIGNED}), 400
|
|
|
|
routine_ids.remove(routine_id)
|
|
child_db.update({'routines': routine_ids}, Query().id == id)
|
|
|
|
override = get_override(id, routine_id)
|
|
if override and override.entity_type == 'routine':
|
|
delete_override(id, routine_id)
|
|
|
|
delete_schedule(id, routine_id)
|
|
delete_extension_for_child_routine(id, routine_id)
|
|
|
|
pending_q = Query()
|
|
pending_confirmations_db.remove(
|
|
(pending_q.child_id == id) & (pending_q.entity_id == routine_id) &
|
|
(pending_q.entity_type == 'routine') & (pending_q.user_id == user_id)
|
|
)
|
|
|
|
send_event_for_current_user(
|
|
Event(EventType.CHILD_ROUTINES_SET.value, ChildRoutinesSet(id, routine_ids))
|
|
)
|
|
send_event_for_current_user(
|
|
Event(
|
|
EventType.CHILD_ROUTINE_CONFIRMATION.value,
|
|
ChildRoutineConfirmation(id, routine_id, ChildRoutineConfirmation.OPERATION_RESET)
|
|
)
|
|
)
|
|
return jsonify({'message': f'Routine {routine_id} removed from {child.name}.'}), 200
|
|
|
|
|
|
@child_routine_api.route('/child/<id>/set-routines', methods=['PUT'])
|
|
def set_child_routines(id):
|
|
user_id = get_validated_user_id()
|
|
if not user_id:
|
|
return jsonify({'error': 'Unauthorized', 'code': ErrorCodes.UNAUTHORIZED}), 401
|
|
|
|
data = request.get_json() or {}
|
|
routine_ids = data.get('routine_ids')
|
|
if not isinstance(routine_ids, list):
|
|
return jsonify({'error': 'routine_ids must be a list'}), 400
|
|
|
|
child = _validate_child_for_user(id, user_id)
|
|
if not child:
|
|
return jsonify({'error': 'Child not found', 'code': ErrorCodes.CHILD_NOT_FOUND}), 404
|
|
|
|
routine_q = Query()
|
|
valid_ids = []
|
|
for rid in dict.fromkeys(routine_ids):
|
|
if routine_db.get((routine_q.id == rid) & ((routine_q.user_id == user_id) | (routine_q.user_id == None))):
|
|
valid_ids.append(rid)
|
|
|
|
old_ids = set(child.routines)
|
|
new_ids = set(valid_ids)
|
|
unassigned_ids = old_ids - new_ids
|
|
|
|
pending_q = Query()
|
|
for rid in unassigned_ids:
|
|
override = get_override(id, rid)
|
|
if override and override.entity_type == 'routine':
|
|
delete_override(id, rid)
|
|
|
|
delete_schedule(id, rid)
|
|
delete_extension_for_child_routine(id, rid)
|
|
|
|
pending_confirmations_db.remove(
|
|
(pending_q.child_id == id) & (pending_q.entity_id == rid) &
|
|
(pending_q.entity_type == 'routine') & (pending_q.user_id == user_id)
|
|
)
|
|
|
|
child_db.update({'routines': valid_ids}, Query().id == id)
|
|
send_event_for_current_user(Event(EventType.CHILD_ROUTINES_SET.value, ChildRoutinesSet(id, valid_ids)))
|
|
|
|
return jsonify({'message': f'Routines set for child {id}.', 'routine_ids': valid_ids, 'count': len(valid_ids)}), 200
|
|
|
|
|
|
@child_routine_api.route('/child/<id>/list-routines', methods=['GET'])
|
|
def list_child_routines(id):
|
|
user_id = get_validated_user_id()
|
|
if not user_id:
|
|
return jsonify({'error': 'Unauthorized', 'code': ErrorCodes.UNAUTHORIZED}), 401
|
|
|
|
child = _validate_child_for_user(id, user_id)
|
|
if not child:
|
|
return jsonify({'error': 'Child not found', 'code': ErrorCodes.CHILD_NOT_FOUND}), 404
|
|
|
|
routine_q = Query()
|
|
pending_q = Query()
|
|
today_local, tz_str = _get_user_today_local(user_id)
|
|
|
|
child_routines = []
|
|
for rid in child.routines:
|
|
routine_record = routine_db.get((routine_q.id == rid) & ((routine_q.user_id == user_id) | (routine_q.user_id == None)))
|
|
if not routine_record:
|
|
continue
|
|
|
|
routine = Routine.from_dict(routine_record)
|
|
override = get_override(id, rid)
|
|
custom_value = override.custom_value if override and override.entity_type == 'routine' else None
|
|
|
|
cr = ChildRoutine(routine.name, routine.points, routine.image_id, routine.id)
|
|
cr_dict = cr.to_dict()
|
|
if custom_value is not None:
|
|
cr_dict['custom_value'] = custom_value
|
|
|
|
schedule = get_schedule(id, rid)
|
|
cr_dict['schedule'] = schedule.to_dict() if schedule else None
|
|
|
|
extension = get_extension_for_child_routine(id, rid)
|
|
cr_dict['extension_date'] = extension.date if extension else None
|
|
|
|
items = get_items_for_routine(rid)
|
|
cr_dict['items'] = [item.to_dict() for item in items]
|
|
|
|
pending = pending_confirmations_db.get(
|
|
(pending_q.child_id == id) & (pending_q.entity_id == rid) &
|
|
(pending_q.entity_type == 'routine') & (pending_q.user_id == user_id)
|
|
)
|
|
if pending:
|
|
status = pending.get('status')
|
|
approved_at = pending.get('approved_at')
|
|
created_at = pending.get('created_at')
|
|
confirmation_id = pending.get('id')
|
|
|
|
if status == 'approved' and _is_iso_timestamp_on_local_day(approved_at, today_local, tz_str):
|
|
cr_dict['pending_status'] = 'approved'
|
|
cr_dict['approved_at'] = approved_at
|
|
cr_dict['pending_confirmation_id'] = confirmation_id
|
|
elif status == 'pending' and _is_epoch_timestamp_on_local_day(created_at, today_local, tz_str):
|
|
cr_dict['pending_status'] = 'pending'
|
|
cr_dict['approved_at'] = None
|
|
cr_dict['pending_confirmation_id'] = confirmation_id
|
|
else:
|
|
pending_id = pending.get('id')
|
|
if pending_id:
|
|
pending_confirmations_db.remove(pending_q.id == pending_id)
|
|
cr_dict['pending_status'] = None
|
|
cr_dict['approved_at'] = None
|
|
cr_dict['pending_confirmation_id'] = None
|
|
else:
|
|
cr_dict['pending_status'] = None
|
|
cr_dict['approved_at'] = None
|
|
cr_dict['pending_confirmation_id'] = None
|
|
|
|
child_routines.append(cr_dict)
|
|
|
|
return jsonify({'routines': child_routines}), 200
|
|
|
|
|
|
@child_routine_api.route('/child/<id>/list-assignable-routines', methods=['GET'])
|
|
def list_assignable_routines(id):
|
|
user_id = get_validated_user_id()
|
|
if not user_id:
|
|
return jsonify({'error': 'Unauthorized', 'code': ErrorCodes.UNAUTHORIZED}), 401
|
|
|
|
child = _validate_child_for_user(id, user_id)
|
|
if not child:
|
|
return jsonify({'error': 'Child not found', 'code': ErrorCodes.CHILD_NOT_FOUND}), 404
|
|
|
|
assigned_ids = set(child.routines)
|
|
all_routines = [r for r in routine_db.all() if r and r.get('id') and r.get('id') not in assigned_ids]
|
|
|
|
name_to_routines = defaultdict(list)
|
|
for routine in all_routines:
|
|
name_to_routines[routine.get('name')].append(routine)
|
|
|
|
filtered_routines = []
|
|
for _, routines in name_to_routines.items():
|
|
user_routines = [r for r in routines if r.get('user_id') is not None]
|
|
if len(user_routines) == 0:
|
|
filtered_routines.append(routines[0])
|
|
elif len(user_routines) == 1:
|
|
filtered_routines.append(user_routines[0])
|
|
else:
|
|
filtered_routines.extend(user_routines)
|
|
|
|
assignable = [
|
|
ChildRoutine(r.get('name'), r.get('points'), r.get('image_id'), r.get('id')).to_dict()
|
|
for r in filtered_routines
|
|
]
|
|
return jsonify({'routines': assignable, 'count': len(assignable)}), 200
|
|
|
|
|
|
@child_routine_api.route('/child/<id>/confirm-routine', methods=['POST'])
|
|
def confirm_routine(id):
|
|
user_id = get_validated_user_id()
|
|
if not user_id:
|
|
return jsonify({'error': 'Unauthorized', 'code': ErrorCodes.UNAUTHORIZED}), 401
|
|
|
|
data = request.get_json() or {}
|
|
routine_id = data.get('routine_id')
|
|
if not routine_id:
|
|
return jsonify({'error': 'routine_id is required', 'code': ErrorCodes.MISSING_FIELD}), 400
|
|
|
|
child = _validate_child_for_user(id, user_id)
|
|
if not child:
|
|
return jsonify({'error': 'Child not found', 'code': ErrorCodes.CHILD_NOT_FOUND}), 404
|
|
|
|
if routine_id not in child.routines:
|
|
return jsonify({'error': 'Routine not assigned to child', 'code': ErrorCodes.ENTITY_NOT_ASSIGNED}), 400
|
|
|
|
routine = _resolve_routine_for_user(routine_id, user_id)
|
|
if not routine:
|
|
return jsonify({'error': 'Routine not found'}), 404
|
|
|
|
pending_q = Query()
|
|
existing = pending_confirmations_db.get(
|
|
(pending_q.child_id == id) & (pending_q.entity_id == routine_id) &
|
|
(pending_q.entity_type == 'routine') & (pending_q.user_id == user_id)
|
|
)
|
|
if existing:
|
|
today_local, tz_str = _get_user_today_local(user_id)
|
|
if existing.get('status') == 'pending':
|
|
if _is_epoch_timestamp_on_local_day(existing.get('created_at'), today_local, tz_str):
|
|
return jsonify({'error': 'Routine already pending confirmation', 'code': 'ROUTINE_ALREADY_PENDING'}), 400
|
|
pending_id = existing.get('id')
|
|
if pending_id:
|
|
pending_confirmations_db.remove(pending_q.id == pending_id)
|
|
if existing.get('status') == 'approved':
|
|
approved_at = existing.get('approved_at', '')
|
|
if _is_iso_timestamp_on_local_day(approved_at, today_local, tz_str):
|
|
return jsonify({'error': 'Routine already completed today', 'code': 'ROUTINE_ALREADY_COMPLETED'}), 400
|
|
pending_id = existing.get('id')
|
|
if pending_id:
|
|
pending_confirmations_db.remove(pending_q.id == pending_id)
|
|
|
|
confirmation = PendingConfirmation(
|
|
child_id=id,
|
|
entity_id=routine_id,
|
|
entity_type='routine',
|
|
user_id=user_id,
|
|
)
|
|
pending_confirmations_db.insert(confirmation.to_dict())
|
|
|
|
send_event_for_current_user(
|
|
Event(
|
|
EventType.CHILD_ROUTINE_CONFIRMATION.value,
|
|
ChildRoutineConfirmation(id, routine_id, ChildRoutineConfirmation.OPERATION_PENDING)
|
|
)
|
|
)
|
|
|
|
push_user = users_db.get(Query().id == user_id)
|
|
if push_user and push_user.get('push_notifications_enabled', True):
|
|
try:
|
|
approve_token = create_action_token(user_id, id, routine_id, 'routine', 'approve')
|
|
deny_token = create_action_token(user_id, id, routine_id, 'routine', 'deny')
|
|
push_payload = {
|
|
'type': 'routine_confirmed',
|
|
'title': 'Routine Pending',
|
|
'body': f'{child.name} completed {routine.name}',
|
|
'user_id': user_id,
|
|
'child_id': id,
|
|
'child_name': child.name,
|
|
'entity_id': routine_id,
|
|
'entity_type': 'routine',
|
|
'entity_name': routine.name,
|
|
'approve_token': approve_token.id,
|
|
'deny_token': deny_token.id,
|
|
}
|
|
send_push_to_user(user_id, push_payload)
|
|
except Exception:
|
|
pass
|
|
|
|
return jsonify({'message': f'Routine {routine.name} confirmed by {child.name}.', 'confirmation_id': confirmation.id}), 200
|
|
|
|
|
|
@child_routine_api.route('/child/<id>/cancel-routine-confirmation', methods=['POST'])
|
|
def cancel_routine_confirmation(id):
|
|
user_id = get_validated_user_id()
|
|
if not user_id:
|
|
return jsonify({'error': 'Unauthorized', 'code': ErrorCodes.UNAUTHORIZED}), 401
|
|
|
|
data = request.get_json() or {}
|
|
routine_id = data.get('routine_id')
|
|
if not routine_id:
|
|
return jsonify({'error': 'routine_id is required', 'code': ErrorCodes.MISSING_FIELD}), 400
|
|
|
|
child = _validate_child_for_user(id, user_id)
|
|
if not child:
|
|
return jsonify({'error': 'Child not found', 'code': ErrorCodes.CHILD_NOT_FOUND}), 404
|
|
|
|
pending_q = Query()
|
|
existing = pending_confirmations_db.get(
|
|
(pending_q.child_id == id) & (pending_q.entity_id == routine_id) &
|
|
(pending_q.entity_type == 'routine') & (pending_q.status == 'pending') &
|
|
(pending_q.user_id == user_id)
|
|
)
|
|
if not existing:
|
|
return jsonify({'error': 'No pending confirmation found', 'code': 'PENDING_NOT_FOUND'}), 400
|
|
|
|
pending_confirmations_db.remove(
|
|
(pending_q.child_id == id) & (pending_q.entity_id == routine_id) &
|
|
(pending_q.entity_type == 'routine') & (pending_q.status == 'pending') &
|
|
(pending_q.user_id == user_id)
|
|
)
|
|
|
|
send_event_for_current_user(
|
|
Event(
|
|
EventType.CHILD_ROUTINE_CONFIRMATION.value,
|
|
ChildRoutineConfirmation(id, routine_id, ChildRoutineConfirmation.OPERATION_RESET)
|
|
)
|
|
)
|
|
return jsonify({'message': 'Routine confirmation cancelled.'}), 200
|
|
|
|
|
|
@child_routine_api.route('/child/<id>/approve-routine/<confirmation_id>', methods=['POST'])
|
|
def approve_routine(id, confirmation_id):
|
|
user_id = get_validated_user_id()
|
|
if not user_id:
|
|
return jsonify({'error': 'Unauthorized', 'code': ErrorCodes.UNAUTHORIZED}), 401
|
|
|
|
child = _validate_child_for_user(id, user_id)
|
|
if not child:
|
|
return jsonify({'error': 'Child not found', 'code': ErrorCodes.CHILD_NOT_FOUND}), 404
|
|
|
|
pending_q = Query()
|
|
confirmation = pending_confirmations_db.get(
|
|
(pending_q.id == confirmation_id) & (pending_q.child_id == id) &
|
|
(pending_q.entity_type == 'routine') & (pending_q.user_id == user_id)
|
|
)
|
|
if not confirmation:
|
|
return jsonify({'error': 'Pending confirmation not found', 'code': 'PENDING_NOT_FOUND'}), 404
|
|
|
|
if confirmation.get('status') != 'pending':
|
|
return jsonify({'error': 'Confirmation is already resolved', 'code': 'ALREADY_RESOLVED'}), 400
|
|
|
|
routine_id = confirmation.get('entity_id')
|
|
routine = _resolve_routine_for_user(routine_id, user_id)
|
|
if not routine:
|
|
return jsonify({'error': 'Routine not found'}), 404
|
|
|
|
override = get_override(id, routine_id)
|
|
points_value = override.custom_value if override and override.entity_type == 'routine' else routine.points
|
|
|
|
new_points = max(0, child.points + points_value)
|
|
child_db.update({'points': new_points}, Query().id == id)
|
|
|
|
approved_at = datetime.now(timezone.utc).isoformat()
|
|
pending_confirmations_db.update(
|
|
{'status': 'approved', 'approved_at': approved_at},
|
|
pending_q.id == confirmation_id
|
|
)
|
|
|
|
send_event_for_current_user(
|
|
Event(
|
|
EventType.CHILD_ROUTINE_CONFIRMATION.value,
|
|
ChildRoutineConfirmation(id, routine_id, ChildRoutineConfirmation.OPERATION_APPROVED)
|
|
)
|
|
)
|
|
|
|
return jsonify({
|
|
'message': f'Routine {routine.name} approved for {child.name}.',
|
|
'points': new_points,
|
|
'id': child.id,
|
|
}), 200
|
|
|
|
|
|
@child_routine_api.route('/child/<id>/reject-routine/<confirmation_id>', methods=['POST'])
|
|
def reject_routine(id, confirmation_id):
|
|
user_id = get_validated_user_id()
|
|
if not user_id:
|
|
return jsonify({'error': 'Unauthorized', 'code': ErrorCodes.UNAUTHORIZED}), 401
|
|
|
|
child = _validate_child_for_user(id, user_id)
|
|
if not child:
|
|
return jsonify({'error': 'Child not found', 'code': ErrorCodes.CHILD_NOT_FOUND}), 404
|
|
|
|
pending_q = Query()
|
|
confirmation = pending_confirmations_db.get(
|
|
(pending_q.id == confirmation_id) & (pending_q.child_id == id) &
|
|
(pending_q.entity_type == 'routine') & (pending_q.user_id == user_id)
|
|
)
|
|
if not confirmation:
|
|
return jsonify({'error': 'Pending confirmation not found', 'code': 'PENDING_NOT_FOUND'}), 404
|
|
|
|
if confirmation.get('status') != 'pending':
|
|
return jsonify({'error': 'Confirmation is already resolved', 'code': 'ALREADY_RESOLVED'}), 400
|
|
|
|
pending_confirmations_db.update({'status': 'rejected', 'approved_at': None}, pending_q.id == confirmation_id)
|
|
|
|
send_event_for_current_user(
|
|
Event(
|
|
EventType.CHILD_ROUTINE_CONFIRMATION.value,
|
|
ChildRoutineConfirmation(id, confirmation.get('entity_id'), ChildRoutineConfirmation.OPERATION_REJECTED)
|
|
)
|
|
)
|
|
|
|
return jsonify({'message': 'Routine confirmation rejected.'}), 200
|
|
|
|
|
|
@child_routine_api.route('/child/<id>/reset-routine/<confirmation_id>', methods=['POST'])
|
|
def reset_routine(id, confirmation_id):
|
|
user_id = get_validated_user_id()
|
|
if not user_id:
|
|
return jsonify({'error': 'Unauthorized', 'code': ErrorCodes.UNAUTHORIZED}), 401
|
|
|
|
child = _validate_child_for_user(id, user_id)
|
|
if not child:
|
|
return jsonify({'error': 'Child not found', 'code': ErrorCodes.CHILD_NOT_FOUND}), 404
|
|
|
|
pending_q = Query()
|
|
confirmation = pending_confirmations_db.get(
|
|
(pending_q.id == confirmation_id) & (pending_q.child_id == id) &
|
|
(pending_q.entity_type == 'routine') & (pending_q.user_id == user_id)
|
|
)
|
|
if not confirmation:
|
|
return jsonify({'error': 'Pending confirmation not found', 'code': 'PENDING_NOT_FOUND'}), 404
|
|
|
|
routine_id = confirmation.get('entity_id')
|
|
pending_confirmations_db.remove(pending_q.id == confirmation_id)
|
|
|
|
send_event_for_current_user(
|
|
Event(
|
|
EventType.CHILD_ROUTINE_CONFIRMATION.value,
|
|
ChildRoutineConfirmation(id, routine_id, ChildRoutineConfirmation.OPERATION_RESET)
|
|
)
|
|
)
|
|
return jsonify({'message': 'Routine reset to available.'}), 200
|
|
|
|
|
|
@child_routine_api.route('/child/<id>/trigger-routine', methods=['POST'])
|
|
def trigger_child_routine(id):
|
|
"""Parent-triggered routine confirmation — directly awards points."""
|
|
user_id = get_validated_user_id()
|
|
if not user_id:
|
|
return jsonify({'error': 'Unauthorized', 'code': ErrorCodes.UNAUTHORIZED}), 401
|
|
|
|
data = request.get_json() or {}
|
|
routine_id = data.get('routine_id')
|
|
if not routine_id:
|
|
return jsonify({'error': 'routine_id is required', 'code': ErrorCodes.MISSING_FIELD}), 400
|
|
|
|
child = _validate_child_for_user(id, user_id)
|
|
if not child:
|
|
return jsonify({'error': 'Child not found', 'code': ErrorCodes.CHILD_NOT_FOUND}), 404
|
|
|
|
if routine_id not in child.routines:
|
|
return jsonify({'error': 'Routine not assigned to child', 'code': ErrorCodes.ENTITY_NOT_ASSIGNED}), 400
|
|
|
|
routine = _resolve_routine_for_user(routine_id, user_id)
|
|
if not routine:
|
|
return jsonify({'error': 'Routine not found'}), 404
|
|
|
|
# Check for override
|
|
override = get_override(id, routine_id)
|
|
points_value = override.custom_value if override and override.entity_type == 'routine' else routine.points
|
|
|
|
# Award points
|
|
new_points = max(0, child.points + points_value)
|
|
child_db.update({'points': new_points}, Query().id == id)
|
|
|
|
# Create an approved pending confirmation so it shows as completed in the routine list
|
|
pending_q = Query()
|
|
existing = pending_confirmations_db.get(
|
|
(pending_q.child_id == id) & (pending_q.entity_id == routine_id) &
|
|
(pending_q.entity_type == 'routine') & (pending_q.user_id == user_id)
|
|
)
|
|
if existing:
|
|
today_local, tz_str = _get_user_today_local(user_id)
|
|
# Remove old confirmation if it exists and is not from today
|
|
if existing.get('status') == 'approved' and _is_iso_timestamp_on_local_day(existing.get('approved_at'), today_local, tz_str):
|
|
return jsonify({'error': 'Routine already completed today', 'code': 'ROUTINE_ALREADY_COMPLETED'}), 400
|
|
pending_id = existing.get('id')
|
|
if pending_id:
|
|
pending_confirmations_db.remove(pending_q.id == pending_id)
|
|
|
|
confirmation = PendingConfirmation(
|
|
child_id=id,
|
|
entity_id=routine_id,
|
|
entity_type='routine',
|
|
user_id=user_id,
|
|
status='approved',
|
|
approved_at=datetime.now(timezone.utc).isoformat()
|
|
)
|
|
pending_confirmations_db.insert(confirmation.to_dict())
|
|
|
|
send_event_for_current_user(
|
|
Event(
|
|
EventType.CHILD_ROUTINE_CONFIRMATION.value,
|
|
ChildRoutineConfirmation(id, routine_id, ChildRoutineConfirmation.OPERATION_APPROVED)
|
|
)
|
|
)
|
|
|
|
return jsonify({
|
|
'message': f'Routine {routine.name} awarded to {child.name}.',
|
|
'points': new_points,
|
|
'id': child.id,
|
|
}), 200
|