""" app/api/facilities.py --------------------- Mobile API endpoints for facilities and areas. GET /api/v1/facilities Returns all active facilities accessible to the current user. Respects customer scoping via get_customer_scope(). Staff roles (admin, director, inspector, project_manager) receive all active facilities. GET /api/v1/facilities//areas Returns all areas for a specific facility. Used by the iPad app to populate the area picker when starting an inspection. """ import logging from flask import Blueprint, g from app import db from app.models.facility import Facility, Area from app.models.project import Project from app.api.errors import api_ok, api_error from app.api.decorators import jwt_required from app.utils.scope import get_customer_scope, get_inspector_scope logger = logging.getLogger(__name__) bp = Blueprint('api_facilities', __name__) def _facility_payload(facility: Facility) -> dict: """Serialize a Facility to the dict returned in API responses.""" project_name = facility.project.name if facility.project else None return { 'id': facility.id, 'name': facility.name, 'address': facility.address or '', 'contact_person': facility.contact_person or '', 'contact_phone': facility.contact_phone or '', 'project_id': facility.project_id, 'project_name': project_name, 'is_active': facility.active, } def _area_payload(area: Area) -> dict: """Serialize an Area to the dict returned in API responses.""" return { 'id': area.id, 'facility_id': area.facility_id, 'name': area.name, 'area_type': area.area_type or '', } # ── Facilities List ─────────────────────────────────────────────────────────── @bp.route('/facilities', methods=['GET']) @jwt_required def list_facilities(): """ Return all active facilities the current user has access to. Staff roles (admin, director, inspector, project_manager) receive all active facilities across all contracts. Customer role receives only their scoped facilities (via CustomerAssignment records). Response 200 ------------ { "ok": true, "data": { "facilities": [ { "id": 7, "name": "Main Office Building", "address": "123 Corporate Dr", "contact_person": "Jane Smith", "contact_phone": "555-1234", "project_id": 2, "project_name": "Corporate Cleaning Contract", "is_active": true } ], "count": 1 } } """ user = g.api_user customer_fids = get_customer_scope(user) inspector_fids = get_inspector_scope(user) if customer_fids is not None: # Customer — scope to assigned facilities only if not customer_fids: logger.info('API FACILITIES | user=%s | role=customer | no_assignments', user.username) return api_ok({'facilities': [], 'count': 0}) facilities = ( Facility.query .filter(Facility.id.in_(customer_fids), Facility.active == True) .order_by(Facility.name) .all() ) elif inspector_fids is not None: # Inspector — scope to contracted facilities if not inspector_fids: logger.info('API FACILITIES | user=%s | role=inspector | no_assignments', user.username) return api_ok({'facilities': [], 'count': 0}) facilities = ( Facility.query .filter(Facility.id.in_(inspector_fids), Facility.active == True) .order_by(Facility.name) .all() ) else: # All other staff — all active facilities facilities = ( Facility.query .filter(Facility.active == True) # noqa: E712 .order_by(Facility.name) .all() ) payload = [_facility_payload(f) for f in facilities] logger.info('API FACILITIES | list | user=%s | role=%s | count=%d', user.username, user.role, len(payload)) return api_ok({'facilities': payload, 'count': len(payload)}) # ── Areas for a Facility ────────────────────────────────────────────────────── @bp.route('/facilities//areas', methods=['GET']) @jwt_required def list_areas(facility_id): """ Return all areas for the given facility. Used by the iPad app to populate the area picker when starting an inspection. Customer users are validated against their scope before the areas are returned. Response 200 ------------ { "ok": true, "data": { "facility_id": 7, "areas": [ { "id": 12, "facility_id": 7, "name": "Main Lobby", "area_type": "lobby" } ], "count": 1 } } """ user = g.api_user facility = db.session.get(Facility, facility_id) if facility is None or not facility.active: return api_error('Facility not found', 404) # Scope validation — customers and inspectors may only access their facilities customer_fids = get_customer_scope(user) inspector_fids = get_inspector_scope(user) if customer_fids is not None and facility_id not in customer_fids: logger.warning('API FACILITIES/AREAS | access denied | user=%s | facility_id=%d', user.username, facility_id) return api_error('Access denied', 403) if inspector_fids is not None and facility_id not in inspector_fids: logger.warning('API FACILITIES/AREAS | access denied | user=%s | facility_id=%d', user.username, facility_id) return api_error('Access denied', 403) areas = ( Area.query .filter_by(facility_id=facility_id) .order_by(Area.name) .all() ) payload = [_area_payload(a) for a in areas] logger.info('API FACILITIES/AREAS | user=%s | facility_id=%d | count=%d', user.username, facility_id, len(payload)) return api_ok({'facility_id': facility_id, 'areas': payload, 'count': len(payload)})