""" Router endpoints for organisation module Endpoints: - [get]/id/{org_id} - Retrieves an organisation by its ID - [post]/ - Creates a new organisation - [patch]/{org_id}/questionnaire - Updates the questionnaire data for an organisation (can be partial or final submission) - [patch]/{org_id}/status - Updates the status of an organisation - [patch]/{org_id}/contact - Assigns a contact to an organisation (as billing, security, or owner) - [get]/{org_id}/users - Retrieves all users associated with an organisation - [post]/{org_id}/users - Adds a new user to an organisation - [delete]/{org_id} - Deletes an organisation by ID - [get]/{org_id}/contact/{contact_type} - Retrieves the contact of a specific type (owner, billing, security) for an organisation """ from typing import Annotated, Optional from fastapi import APIRouter, HTTPException, status from fastapi.params import Path, Query from src.contact.schemas import ContactAddress from src.database import db_dependency from src.contact.models import Contact from src.user.models import User from src.user.exceptions import UserNotFoundException from src.auth.service import root_user_dependency, claims_dependency from src.organisation.dependencies import org_model_dependency from src.organisation.constants import ContactType from src.organisation.models import Organisation as Org from src.organisation.schemas import OrgOrgPostRequest, OrgQuestionnairePatchRequest, OrgStatusPatchRequest, \ OrgContactPatchRequest, \ OrgUserPostRequest, OrgUserGetResponse, OrgContactGetResponse, OrgOrgGetResponse, OrgRootPatchRequest, \ OrgGroupGetResponse, OrgUserDeleteRequest router = APIRouter( prefix="/org", tags=["org"], ) @router.get("/id/{org_id}", response_model=OrgOrgGetResponse) async def get_org_by_id(org_model: org_model_dependency, org_id: Annotated[int, Path(gt=0)]): response = { "name": org_model.name, "status": org_model.status, "owner_contact": org_model.owner_contact_rel.email, "billing_contact": org_model.billing_contact_rel.email, "security_contact": org_model.security_contact_rel.email, "root_user": org_model.root_user_email } return response @router.post("/") async def create_org(db: db_dependency, user: claims_dependency, org_request: OrgOrgPostRequest): db_id: Optional[int] = user.get("db_id", None) if db_id is None: raise UserNotFoundException() org_model = Org(name=org_request.name, intake_questionnaire=org_request.intake_questionnaire.model_dump()) org_model.status = "partial" # Status is always set to partial at first, see update_questionnaire() doc db.add(org_model) db.flush() # Adds currently logged-in user to org users list and sets them as root_user user_model = db.get(User, db_id) org_model.user_rel.append(user_model) org_model.root_user_rel = user_model for contact_type in ["billing_contact_id", "security_contact_id", "owner_contact_id"]: contact_model = Contact(org_id=org_model.id) db.add(contact_model) db.flush() org_model.__setattr__(contact_type, contact_model.id) db.commit() @router.patch("/{org_id}/questionnaire") async def update_questionnaire(db: db_dependency, org_model: org_model_dependency, q_request: OrgQuestionnairePatchRequest, org_id: Annotated[int, Path(gt=0)]): """ Route for updating questionnaire. The partial bool allows for submission of partially completed questionnaire and/or final "are you sure" check before setting the org to be in "submitted" status, awaiting admin approval. """ org_model.intake_questionnaire = q_request.intake_questionnaire.model_dump() # Allows for partially completed questionnaires to be saved without being submitted for review if not q_request.partial: org_model.status = "submitted" db.commit() @router.patch("/{org_id}/status") async def update_status(db: db_dependency, org_model: org_model_dependency, status_request: OrgStatusPatchRequest, org_id: Annotated[int, Path(gt=0)]): org_model.status = status_request.status db.commit() @router.get("/{org_id}/users", response_model=OrgUserGetResponse) async def get_users(org_model: org_model_dependency, org_id: Annotated[int, Path(gt=0)]): return {"users": [user.email for user in org_model.user_rel]} @router.post("/{org_id}/users") async def add_user_to_org(db: db_dependency, org_model: org_model_dependency, user_request: OrgUserPostRequest, org_id: Annotated[int, Path(gt=0)]): user_model = db.get(User, user_request.user_id) if user_model in org_model.user_rel: return org_model.user_rel.append(user_model) db.commit() @router.delete("/{org_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_organisation_by_id(db: db_dependency, org_model: org_model_dependency, org_id: Annotated[int, Path(gt=0)]): db.delete(org_model) db.commit() @router.patch("/{org_id}/root_user", status_code=status.HTTP_204_NO_CONTENT) async def update_root_user(db: db_dependency, org_model: org_model_dependency, org_id: Annotated[int, Path(gt=0)], user_request: OrgRootPatchRequest): root_user_model = db.get(User, user_request.user_id) if root_user_model is None: raise UserNotFoundException(user_id=user_request.user_id) org_model.root_user_rel = root_user_model db.commit() @router.get("/{org_id}/groups", response_model=OrgGroupGetResponse) async def get_org_groups(org_model: org_model_dependency, org_id: Annotated[int, Path(gt=0)]): return {"groups": [group.name for group in org_model.group_rel]} @router.delete("/{org_id}/user", status_code=status.HTTP_204_NO_CONTENT) async def remove_user_from_org(db: db_dependency, org_model: org_model_dependency, org_id: Annotated[int, Path(gt=0)], user_request: OrgUserDeleteRequest): user_id = user_request.user_id user = db.get(User, user_id) if user is None: raise UserNotFoundException(user_id=user_id) if user not in org_model.user_rel: raise HTTPException(status_code=status.HTTP_204_NOT_FOUND) org_model.user_rel.remove(user) db.commit() @router.get("/{org_id}/contact", response_model=OrgContactGetResponse) async def get_contact(org_model: org_model_dependency, contact_type: Annotated[ContactType, Query()], org_id: Annotated[int, Path(gt=0)]): match contact_type: case "billing": contact_model = org_model.billing_contact_rel case "security": contact_model = org_model.security_contact_rel case "owner": contact_model = org_model.owner_contact_rel case _: raise HTTPException(status_code=422, detail="Invalid contact type") if contact_model is None: raise HTTPException(status_code=404, detail="Contact not found") return OrgContactGetResponse.model_construct( **contact_model.__dict__, address=ContactAddress.model_validate(contact_model) ) @router.patch("/{org_id}/contact", response_model=OrgContactGetResponse) async def update_contact(db: db_dependency, org_model: org_model_dependency, contact_type: Annotated[ContactType, Query()], contact_request: OrgContactPatchRequest, org_id: Annotated[int, Path(gt=0)]): match contact_type: case "billing": contact_model = org_model.billing_contact_rel case "security": contact_model = org_model.security_contact_rel case "owner": contact_model = org_model.owner_contact_rel case _: raise HTTPException(status_code=422, detail="Invalid contact type") if contact_model is None: raise HTTPException(status_code=404, detail="Contact not found") update_data = contact_request.model_dump(exclude_none=True) for key, value in update_data.items(): if hasattr(contact_model, key): setattr(contact_model, key, value) else: raise HTTPException(status_code=422, detail="Invalid keys in update request") db.flush() response = OrgContactGetResponse.model_construct( **contact_model.__dict__, address=ContactAddress.model_validate(contact_model) ) db.commit() return response