""" 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 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 role: honour facility-level scoping facility_ids = get_customer_scope(user) if facility_ids is not None: # Customer — scope to assigned facilities only if not facility_ids: 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_(facility_ids), Facility.active == True, # noqa: E712 ) .order_by(Facility.name) .all() ) else: # Internal 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) # Customer scope validation — ensure the customer is assigned to this facility facility_ids = get_customer_scope(user) if facility_ids is not None and facility_id not in facility_ids: 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)})