"""Report handlers.""" from http import HTTPStatus from typing import Optional from flask import Response from flask.typing import ResponseReturnValue from flask_pydantic import validate from collaborator.api import app from collaborator.constants import error from collaborator.constants.header import ABACUS_PROFILE from collaborator.logic import reports from collaborator.schemas import BaseSchema, PaginatedRequestMixin, SortableRequestMixin from collaborator.schemas.report import ReportIdsBody from collaborator.utils import handlers as handler_utils from collaborator.utils import handlers as utils from collaborator.utils.error import OwsError from collaborator.utils.helpers import ( check_collaborators_authorization, check_vendors_authorization, ) from collaborator.utils.typing import ACCOUNT_TYPE_VENDOR, Account, User def _account_from_vendors_auth(authorized_resources, vendor_id): check_vendors_authorization(authorized_resources, [vendor_id]) return Account(type=ACCOUNT_TYPE_VENDOR, id=int(vendor_id)) def _account_from_collabs_by_id(collabs_by_id): account_id = list(collabs_by_id.values())[0]["vendor_id"] return Account(type=ACCOUNT_TYPE_VENDOR, id=account_id) class ReportsQuery(BaseSchema, SortableRequestMixin, PaginatedRequestMixin): """Query model for getting reports.""" vendor_id: Optional[int] = None status: Optional[str] = None has_transaction: Optional[bool] = None report_run_uuid: Optional[str] = None collaborator_id: Optional[int] = None report_id: Optional[int] = None collaborator_dp_enabled: Optional[bool] = None @app.route("/reports", methods=["GET"]) @validate() @handler_utils.fetch_authorized_resources @utils.fetch_profile_type def get_reports( authorized_resources, profile_type, user, query: ReportsQuery ) -> ResponseReturnValue: """Get reports. Args: authorized_resources: List of resources to which the requestor has access user (User): User who made the request Returns: flask.Response """ account = None vendor_id = query.vendor_id collaborator_id = query.collaborator_id if profile_type != ABACUS_PROFILE and (not vendor_id and not collaborator_id): raise OwsError( code=error.ERROR_CODE_MISSING_PARAMS, message=error.ERROR_MESSAGE_BAD_PARAMS, status=HTTPStatus.BAD_REQUEST, ) if vendor_id: account = _account_from_vendors_auth(authorized_resources, vendor_id) elif collaborator_id: collabs_by_id = check_collaborators_authorization( authorized_resources, [collaborator_id] ) account = _account_from_collabs_by_id(collabs_by_id) return reports.get_reports( account=account, **query.model_dump(exclude={"vendor_id"}, exclude_none=True) ).message @app.route("/reports/dataloader", methods=["POST"]) @validate() @handler_utils.fetch_authorized_resources def get_reports_dataloader( authorized_resources, user, body: ReportIdsBody ) -> ResponseReturnValue: """Get reports (dataloader). Args: authorized_resources: List of resources to which the requestor has access user (User): User who made the request body (ReportIdsBody): Body parameters Returns: flask.Response """ reports_list, _ = reports.check_can_access_reports_data( authorized_resources, body.report_ids ) reports_map = {report["id"]: report for report in reports_list} return [{"data": reports_map.get(report_id, None)} for report_id in body.report_ids] class TriggerReportGenerationBody(BaseSchema): """Request body for triggering report generation.""" vendor_id: int first_period_id: int last_period_id: int period_name: str report_run_name: str collaborator_ids: Optional[list[int]] = None @app.route("/reports", methods=["POST"]) @validate() @handler_utils.fetch_authorized_resources def trigger_report_generation( authorized_resources, user: User, body: TriggerReportGenerationBody ) -> ResponseReturnValue: """Trigger report generation for the specified accounting periods. Args: authorized_resources: List of resources to which the requestor has access user (User): User who made the request body (TriggerReportGenerationBody): Body parameters Returns: flask.Response: HTTP 202 Accepted """ account = _account_from_vendors_auth(authorized_resources, body.vendor_id) result = reports.trigger_report_generation( account=account, user=user, first_period_id=body.first_period_id, last_period_id=body.last_period_id, period_name=body.period_name, report_run_name=body.report_run_name, collaborator_ids=body.collaborator_ids, ) if result is None: return Response(status=HTTPStatus.NO_CONTENT) return result, HTTPStatus.CREATED @app.route("/reports/bulk-delete", methods=["POST"]) @validate() @handler_utils.fetch_authorized_resources def delete_bulk_report( authorized_resources, user, body: ReportIdsBody ) -> ResponseReturnValue: """Delete multiple reports. Args: authorized_resources: List of resources to which the requestor has access user (User): User who made the request body (ReportIdsBody): Body parameters Returns: flask.Response: Object containing the presigned URL """ _, collabs_by_id = reports.check_can_access_reports_data( authorized_resources, body.report_ids ) account = _account_from_collabs_by_id(collabs_by_id) reports.bulk_delete_reports(account, body.report_ids, user) return Response(status=HTTPStatus.NO_CONTENT) @app.route("/reports//download", methods=["GET"]) @handler_utils.fetch_authorized_resources def get_download_for_report( report_id: int, authorized_resources, user ) -> ResponseReturnValue: """Get presigned S3 url for downloading a report. Args: report_id (int): RDS report id authorized_resources: List of resources to which the requestor has access user (User): User who made the request Returns: flask.Response: Object containing the presigned URL """ _, collabs_by_id = reports.check_can_access_reports_data( authorized_resources, [report_id] ) account = _account_from_collabs_by_id(collabs_by_id) return reports.get_report_download(account, report_id) @app.route("/reports/overlapping-reports-dataloader", methods=["POST"]) @validate() @handler_utils.fetch_authorized_resources def get_reports_with_overlapping_period( authorized_resources, user, body: ReportIdsBody ) -> ResponseReturnValue: """Get reports whose period range overlaps with the supplied reports. Args: authorized_resources: List of resources to which the requestor has access user (User): User who made the request body (ReportIdsBody): Body parameters Returns: flask.Response """ reports.check_can_access_reports_data(authorized_resources, body.report_ids) overlaps = reports.get_reports_with_overlapping_period(body.report_ids) return [{"data": overlaps.get(report_id, [])} for report_id in body.report_ids] @app.route("/reports/contract-subtotals-dataloader", methods=["POST"]) @validate() @handler_utils.fetch_authorized_resources def get_report_contract_subtotals_dataloader( authorized_resources, user, body: ReportIdsBody ) -> ResponseReturnValue: """Get non-zero report contract subtotals (dataloader). Zero-amount entries are excluded. Args: authorized_resources: List of resources to which the requestor has access user (User): User who made the request Returns: flask.Response """ return reports.get_report_contract_subtotals_dataloader( body.report_ids, authorized_resources ) class ReportContractSubtotalAggregationsQuery(BaseSchema): """Query model for report contract subtotal aggregations.""" report_run_id: int collaborator_dp_enabled: Optional[bool] = None @app.route("/reports/contract-subtotal-aggregations", methods=["GET"]) @validate() def get_report_contract_subtotal_aggregations( query: ReportContractSubtotalAggregationsQuery, ) -> ResponseReturnValue: """Get report contract subtotal aggregations. Zero-amount entries are excluded. Returns: flask.Response """ aggregations = reports.get_report_contract_subtotal_aggregations( report_run_id=query.report_run_id, collaborator_dp_enabled=query.collaborator_dp_enabled, ) return { "total_count": aggregations.total_count, "currency_agnostic_total_amount": float( aggregations.currency_agnostic_total_amount or 0 ), }