cloud-api/src/organisation/router.py

201 lines
7.5 KiB
Python
Raw Normal View History

2026-04-06 12:41:49 +01:00
"""
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, status
from fastapi.params import Query
from psycopg.errors import UniqueViolation
from sqlalchemy.exc import IntegrityError
2026-04-06 12:41:49 +01:00
from src.exceptions import UnprocessableContent, Conflict
from src.contact.models import Contact
from src.contact.schemas import ContactAddress
from src.contact.exceptions import ContactNotFoundException
2026-04-06 12:41:49 +01:00
from src.database import db_dependency
from src.user.models import User
from user.dependencies import user_model_body_dependency, user_model_claims_dependency
from src.auth.dependencies import super_admin_dependency, org_model_root_claim_query_dependency, org_model_root_claim_body_dependency
2026-04-06 12:41:49 +01:00
from src.organisation.dependencies import org_model_body_dependency
2026-04-06 12:41:49 +01:00
from src.organisation.constants import ContactType
from src.organisation.models import Organisation as Org
2026-04-06 12:41:49 +01:00
from src.organisation.schemas import OrgOrgPostRequest, OrgQuestionnairePatchRequest, OrgStatusPatchRequest, \
OrgContactPatchRequest, \
OrgUserPostRequest, OrgUserGetResponse, OrgContactGetResponse, OrgOrgGetResponse, OrgRootPatchRequest, \
OrgGroupGetResponse, OrgUserDeleteRequest, OrgDeleteOrgRequest
2026-04-06 12:41:49 +01:00
2026-04-06 12:41:49 +01:00
router = APIRouter(
prefix="/org",
tags=["org"],
)
@router.get("/id", response_model=OrgOrgGetResponse)
async def get_org_by_id(org_model: org_model_root_claim_query_dependency):
2026-04-06 12:41:49 +01:00
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
2026-04-06 12:41:49 +01:00
}
return response
@router.post("/")
async def create_org(db: db_dependency, user_model: user_model_claims_dependency, request_model: OrgOrgPostRequest):
if request_model.intake_questionnaire:
intake_questionnaire = request_model.intake_questionnaire.model_dump()
else:
intake_questionnaire = None
org_model = Org(name=request_model.name, intake_questionnaire=intake_questionnaire)
2026-04-06 12:41:49 +01:00
org_model.status = "partial" # Status is always set to partial at first, see update_questionnaire() doc
db.add(org_model)
try:
db.flush()
except IntegrityError as e:
if isinstance(e.orig, UniqueViolation):
raise Conflict(message="Organisation with this name already exists")
# Adds currently logged-in user to org users list and sets them as root_user
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)
2026-04-06 12:41:49 +01:00
db.commit()
@router.patch("/questionnaire")
async def update_questionnaire(db: db_dependency, org_model: org_model_root_claim_query_dependency, request_model: OrgQuestionnairePatchRequest):
2026-04-06 12:41:49 +01:00
"""
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 = request_model.intake_questionnaire.model_dump()
2026-04-06 12:41:49 +01:00
# Allows for partially completed questionnaires to be saved without being submitted for review
if not request_model.partial:
2026-04-06 12:41:49 +01:00
org_model.status = "submitted"
db.commit()
@router.patch("/status")
async def update_status(db: db_dependency, org_model: org_model_body_dependency, su: super_admin_dependency, request_model: OrgStatusPatchRequest):
org_model.status = request_model.status
2026-04-06 12:41:49 +01:00
db.commit()
@router.get("/users", response_model=OrgUserGetResponse)
async def get_users(org_model: org_model_root_claim_query_dependency):
return {"users": [user.email for user in org_model.user_rel]}
2026-04-06 12:41:49 +01:00
@router.post("/users")
async def add_user_to_org(db: db_dependency, org_model: org_model_root_claim_body_dependency, user_model: user_model_body_dependency, request_model: OrgUserPostRequest):
if user_model in org_model.user_rel:
raise Conflict(message="User already a part of this organisation")
org_model.user_rel.append(user_model)
2026-04-06 12:41:49 +01:00
db.commit()
@router.delete("/", status_code=status.HTTP_204_NO_CONTENT)
async def delete_organisation_by_id(db: db_dependency, org_model: org_model_body_dependency, su: super_admin_dependency, request_model: OrgDeleteOrgRequest):
2026-04-06 12:41:49 +01:00
db.delete(org_model)
db.commit()
@router.patch("/root_user", status_code=status.HTTP_204_NO_CONTENT)
async def update_root_user(db: db_dependency, org_model: org_model_body_dependency, user_model: user_model_body_dependency, su: super_admin_dependency, request_model: OrgRootPatchRequest):
org_model.root_user_rel = user_model
db.commit()
@router.get("/groups", response_model=OrgGroupGetResponse)
async def get_org_groups(org_model: org_model_root_claim_query_dependency):
return {"groups": [group.name for group in org_model.group_rel]}
@router.delete("/user", status_code=status.HTTP_204_NO_CONTENT)
async def remove_user_from_org(db: db_dependency, org_model: org_model_root_claim_body_dependency, user_model: user_model_body_dependency, request_model: OrgUserDeleteRequest):
if user_model not in org_model.user_rel:
return
org_model.user_rel.remove(user_model)
db.commit()
@router.get("/contact", response_model=OrgContactGetResponse)
async def get_contact(org_model: org_model_root_claim_query_dependency, contact_type: Annotated[ContactType, Query()]):
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 UnprocessableContent("Invalid contact type")
if contact_model is None:
raise ContactNotFoundException()
return OrgContactGetResponse.model_construct(
**contact_model.__dict__,
address=ContactAddress.model_validate(contact_model)
)
@router.patch("/contact", response_model=OrgContactGetResponse)
async def update_contact(db: db_dependency, org_model: org_model_root_claim_body_dependency, request_model: OrgContactPatchRequest):
match request_model.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 UnprocessableContent("Invalid contact type")
if contact_model is None:
raise ContactNotFoundException()
update_data = request_model.model_dump(exclude_none=True)
for key, value in update_data.items():
if hasattr(contact_model, key):
setattr(contact_model, key, value)
else:
raise UnprocessableContent("Invalid keys in update request")
db.flush()
response = OrgContactGetResponse.model_construct(
**contact_model.__dict__,
address=ContactAddress.model_validate(contact_model)
)
db.commit()
return response