Files
2026-06-26 09:04:34 -04:00

194 lines
6.5 KiB
Python

"""
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/<facility_id>/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/<int:facility_id>/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)})