""" app/api/issues.py ----------------- Mobile API endpoint for submitting issues from the iPad app. POST /api/v1/issues Creates a new issue record. Accepts a mobile_local_id for idempotency. Triggers notify_by_matrix() and log_action() identically to the web route. """ import logging from flask import Blueprint, request, g, current_app from app import db from app.models.issue import Issue from app.models.facility import Facility, Area from app.models.inspection import Inspection from app.api.errors import api_ok, api_error from app.api.decorators import jwt_required from app.utils.audit import log_action, ACTION_CREATE from app.utils.notifications import notify_by_matrix from app.utils.time_utils import now_eastern logger = logging.getLogger(__name__) bp = Blueprint('api_issues', __name__) _ALLOWED_ROLES = {'admin', 'director', 'inspector', 'project_manager'} _VALID_SEVERITY = {'low', 'medium', 'high', 'critical'} # ── Create Issue ────────────────────────────────────────────────────────────── @bp.route('/issues', methods=['POST']) @jwt_required def create_issue(): """ Create a new issue submitted from the iPad app. Idempotency: if mobile_local_id is provided and an issue with that ID already exists, the existing record is returned without duplication. Request JSON ------------ { "inspection_id": 42, // optional "facility_id": 5, // required "severity": "high", // "low"|"medium"|"high"|"critical" "description": "...", // required "photo_path": "uploads/...",// optional — already uploaded via /photos/upload "mobile_local_id": "uuid-string" // idempotency key } Response 200 ------------ { "ok": true, "data": { "issue_id": 99, "duplicate": false } } """ user = g.api_user if user.role not in _ALLOWED_ROLES: return api_error('Access denied', 403) data = request.get_json(silent=True) or {} # ── Idempotency check ───────────────────────────────────────────────── mobile_local_id = data.get('mobile_local_id') if mobile_local_id: existing = Issue.query.filter_by(mobile_local_id=mobile_local_id).first() if existing: logger.info('API ISSUES | duplicate | local_id=%s | issue_id=%d | user=%s', mobile_local_id, existing.id, user.username) return api_ok({'issue_id': existing.id, 'duplicate': True}) # ── Validate ────────────────────────────────────────────────────────── facility_id = data.get('facility_id') severity = data.get('severity', '').lower() description = (data.get('description') or '').strip() if not facility_id: return api_error('facility_id is required', 400) if severity not in _VALID_SEVERITY: return api_error(f'severity must be one of: {", ".join(sorted(_VALID_SEVERITY))}', 400) if not description: return api_error('description is required', 400) facility = db.session.get(Facility, facility_id) if facility is None: return api_error('Facility not found', 404) inspection_id = data.get('inspection_id') if inspection_id: inspection = db.session.get(Inspection, inspection_id) if inspection is None: return api_error('Inspection not found', 404) else: inspection = None # ── Create ──────────────────────────────────────────────────────────── issue = Issue( inspection_id = inspection_id, facility_id = facility_id, severity = severity, description = description, photo_path = data.get('photo_path') or None, status = 'open', reported_at = now_eastern(), mobile_local_id = mobile_local_id, ) db.session.add(issue) db.session.flush() # get issue.id # ── Notifications ───────────────────────────────────────────────────── try: from flask import url_for issue_link = url_for('issues.view', issue_id=issue.id, _external=False) except RuntimeError: issue_link = f'/issues/{issue.id}' inspection_ref = f'inspection #{inspection_id}' if inspection_id else 'a standalone report' notify_by_matrix( event_type = 'issue_flagged', title = f'New Issue #{issue.id} (Mobile)', body = ( f'A new {severity.title()}-severity issue was logged at ' f'{facility.name} during {inspection_ref}. ' f'Description: {description[:120]}' f'{"…" if len(description) > 120 else ""}' ), link = issue_link, issue_id = issue.id, facility_id = facility_id, exclude_user_ids = {user.id}, ) db.session.commit() log_action(ACTION_CREATE, 'Issue', issue.id, f'{severity} issue at {facility.name}', f'source=mobile; inspection_id={inspection_id}; ' f'local_id={mobile_local_id}') logger.info('API ISSUES | created | issue_id=%d | facility=%s | severity=%s | user=%s', issue.id, facility.name, severity, user.username) return api_ok({'issue_id': issue.id, 'duplicate': False})