mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Resolves the authenticated requester's email (ADR-0059), resolves + caps the property selection, pins the recipe (landlord_property_ids, recipient_email, package_name) onto one pre-created sub_task (raw SQL, dodging the mirror double-registration), and enqueues one message to the worker. Refuses double-submits (409) and oversized selections (400). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
132 lines
4.4 KiB
Python
132 lines
4.4 KiB
Python
"""Bulk Document Download trigger: POST /v1/documents/bulk-download (ADR-0060).
|
|
|
|
Resolves the authenticated requester's email, resolves + caps the property
|
|
selection, pins the recipe onto a single pre-created sub_task under the
|
|
app-owned task, and enqueues one message to the bulk_document_download worker.
|
|
Never assembles the package synchronously — the worker emails the link.
|
|
"""
|
|
|
|
import json
|
|
from collections.abc import Callable, Iterator
|
|
from typing import Any, cast
|
|
from uuid import uuid4
|
|
|
|
import boto3
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlmodel import Session
|
|
|
|
from backend.app.config import get_settings
|
|
from backend.app.db.connection import db_engine
|
|
from backend.app.dependencies import validate_jwt_token, validate_token
|
|
from backend.app.documents.download_tasks import DocumentDownloadTasks
|
|
from backend.app.documents.schemas import BulkDownloadRequest
|
|
|
|
# The selection cap (ADR-0060) — rejected at the trigger so an oversized request
|
|
# never reaches the worker. A conservative, tunable starting value.
|
|
MAX_PROPERTIES = 500
|
|
|
|
MessageSender = Callable[[str], None]
|
|
|
|
|
|
def get_session() -> Iterator[Session]:
|
|
with Session(db_engine) as session:
|
|
yield session
|
|
|
|
|
|
def get_message_sender() -> MessageSender:
|
|
settings = get_settings()
|
|
client: Any = cast(Any, boto3.client("sqs", settings.AWS_DEFAULT_REGION)) # pyright: ignore[reportUnknownMemberType]
|
|
queue_url = settings.BULK_DOCUMENT_DOWNLOAD_SQS_URL
|
|
|
|
def send(body: str) -> None:
|
|
client.send_message(QueueUrl=queue_url, MessageBody=body)
|
|
|
|
return send
|
|
|
|
|
|
def get_requesting_user_email(user: Any = Depends(validate_jwt_token)) -> str:
|
|
"""The authenticated requester's email — the Download Package recipient.
|
|
|
|
ADR-0059: the route resolves the user (JWT ``dbId`` -> ``UserModel``) rather
|
|
than only gating the token, and threads the email into the job."""
|
|
email = getattr(user, "email", None)
|
|
if email is None and isinstance(user, dict):
|
|
email = user.get("email")
|
|
if not email:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Could not determine the requesting user's email address.",
|
|
)
|
|
return str(email)
|
|
|
|
|
|
router = APIRouter(
|
|
prefix="/documents",
|
|
tags=["documents"],
|
|
dependencies=[Depends(validate_token)],
|
|
)
|
|
|
|
|
|
@router.post("/bulk-download", status_code=202)
|
|
async def bulk_download(
|
|
body: BulkDownloadRequest,
|
|
session: Session = Depends(get_session),
|
|
send_message: MessageSender = Depends(get_message_sender),
|
|
recipient_email: str = Depends(get_requesting_user_email),
|
|
) -> dict[str, str]:
|
|
tasks = DocumentDownloadTasks(session)
|
|
if tasks.already_distributed(body.task_id):
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail=(
|
|
f"Task {body.task_id} already has a sub_task — a download has "
|
|
"already been started for it."
|
|
),
|
|
)
|
|
|
|
if not body.select_all and not body.property_ids:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Provide property_ids or set select_all.",
|
|
)
|
|
|
|
property_count, landlord_property_ids = tasks.resolve_selection(
|
|
body.portfolio_id, body.property_ids, body.select_all
|
|
)
|
|
if property_count == 0:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=(
|
|
f"The selection resolves to no properties in portfolio "
|
|
f"{body.portfolio_id}."
|
|
),
|
|
)
|
|
if property_count > MAX_PROPERTIES:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=(
|
|
f"The selection has {property_count} properties, over the "
|
|
f"{MAX_PROPERTIES} limit — narrow your selection."
|
|
),
|
|
)
|
|
if not landlord_property_ids:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=(
|
|
"None of the selected properties have a landlord_property_id, "
|
|
"so no documents can be matched."
|
|
),
|
|
)
|
|
|
|
subtask_id = uuid4()
|
|
recipe = {
|
|
"landlord_property_ids": landlord_property_ids,
|
|
"recipient_email": recipient_email,
|
|
"package_name": f"portfolio-{body.portfolio_id}-{body.task_id}",
|
|
}
|
|
tasks.create_download_subtask(body.task_id, subtask_id, recipe)
|
|
|
|
send_message(
|
|
json.dumps({"task_id": str(body.task_id), "subtask_id": str(subtask_id)})
|
|
)
|
|
return {"message": "Bulk document download started"}
|