"""Transaction handlers.""" from datetime import date, datetime from http import HTTPStatus from typing import Optional from flask import Response from flask.typing import ResponseReturnValue from flask_pydantic import validate from pydantic import Field, RootModel from collaborator.api import app from collaborator.constants import error from collaborator.constants.transaction import TYPE_REVENUE from collaborator.logic import reports, transaction from collaborator.schemas import BaseSchema from collaborator.utils import handlers as utils from collaborator.utils.error import OwsError from collaborator.utils.helpers import check_collaborators_authorization from collaborator.utils.typing import ACCOUNT_TYPE_VENDOR, Account def _check_can_write_transaction_data(authorized_resources, data): """Check profile has write access to transaction data.""" collaborator_ids = list({item["collaborator_id"] for item in data}) collabs_by_id = check_collaborators_authorization( authorized_resources, collaborator_ids ) return collabs_by_id class CreateTransactionEntry(BaseSchema): """A single transaction in the POST /transactions request body. Fields mirror the GraphQL ``CreateCollaboratorTransactionInput`` (a closed input type). ``currency`` and ``date`` are optional on the GraphQL side but the create logic dereferences them unconditionally, so they are required here. """ collaborator_id: int date: date type: str original_amount: float currency: str collaborator_share: Optional[float] = None description: Optional[str] = None report_id: Optional[int] = None class CreateTransactionsBody(RootModel[list[CreateTransactionEntry]]): """Top-level JSON array body for POST /transactions.""" @app.route("/transactions", methods=["POST"]) @validate() @utils.fetch_authorized_resources def create_transactions_in_bulk( authorized_resources, user, body: CreateTransactionsBody ) -> ResponseReturnValue: """Create transactions for the collaborator. Args: authorized_resources: Resources the requestor has access to. user (User): User who is making the transaction. body (CreateTransactionsBody): List of transactions to create. Returns: flask.Response: List of created transactions """ transaction_data = [entry.dump() for entry in body.root] collabs_by_id = _check_can_write_transaction_data( authorized_resources, transaction_data ) return ( transaction.create_transactions( user=user, transaction_data=transaction_data, collabs_by_id=collabs_by_id, ), HTTPStatus.CREATED, ) class GetTransactionsQuery(BaseSchema): """Query parameters for GET /collaborators//transactions.""" statement_period_id: Optional[int] = None limit: int = 0 offset: int = 0 @app.route("/collaborators//transactions", methods=["GET"]) @validate() @utils.fetch_authorized_resources def get_transactions( authorized_resources, user, collaborator_id: int, query: GetTransactionsQuery ) -> ResponseReturnValue: """Get transactions for a collaborator. Args: authorized_resources: Resources the requestor has access to. collaborator_id (int): The collaborator unique identifier. query (GetTransactionsQuery): Pagination and filter parameters. """ collabs = check_collaborators_authorization(authorized_resources, [collaborator_id]) vendor_id = collabs[collaborator_id]["vendor_id"] account = Account(type=ACCOUNT_TYPE_VENDOR, id=vendor_id) return transaction.get_transactions( collaborator_id, account, query.statement_period_id, query.limit, query.offset ).message class DeleteTransactionsBody(BaseSchema): """Schema for delete transactions request body.""" transaction_ids: list[int] @app.route("/transactions/bulk-delete", methods=["POST"]) @validate() @utils.fetch_authorized_resources def delete_transactions( authorized_resources, user, body: DeleteTransactionsBody ) -> ResponseReturnValue: """Soft-delete multiple transactions. Args: account (Account): Account which made the request. """ transaction.delete_transactions(body.transaction_ids, authorized_resources) return Response(status=HTTPStatus.NO_CONTENT) class AddToRoyaltiesBody(BaseSchema): """Request body for POST /transactions/royalties.""" report_ids: list[int] @app.route("/transactions/royalties", methods=["POST"]) @validate() @utils.fetch_authorized_resources def add_to_royalties_in_bulk( authorized_resources, user, body: AddToRoyaltiesBody ) -> ResponseReturnValue: """Add to royalties (create REVENUE transactions) in bulk. Args: authorized_resources: Resources the requestor has access to. user (User): User who is making the transaction. body (AddToRoyaltiesBody): Reports to convert into royalty transactions. Returns: flask.Response: List of created transactions """ reports_list, collabs_by_id = reports.check_can_access_reports_data( authorized_resources, body.report_ids ) vendor_ids = {collab["vendor_id"] for collab in collabs_by_id.values()} if len(vendor_ids) != 1: # We don't account for creating transaction for multiple vendors at once raise OwsError( code=error.ERROR_CODE_BAD_PARAMS, message=error.ERROR_MESSAGE_BAD_PARAMS, ) transaction_data = [ { "collaborator_id": report["collaborator_id"], "report_id": int(report["id"]), "type": TYPE_REVENUE, "original_amount": report["amount"], "currency": report["currency"], "description": report["filename"], "date": ( datetime.strptime( report["generated_datetime"], "%Y-%m-%dT%H:%M:%S" ).strftime("%Y-%m-%d") ), } for report in reports_list ] return ( transaction.create_transactions( user=user, transaction_data=transaction_data, collabs_by_id=collabs_by_id, ), HTTPStatus.CREATED, ) class TransactionsDataloaderBody(BaseSchema): """Schema for transactions dataloader request body.""" transaction_ids: list[int] @app.route("/transactions/dataloader", methods=["POST"]) @utils.fetch_authorized_resources @validate() def transactions_dataloader( authorized_resources, user, body: TransactionsDataloaderBody ) -> ResponseReturnValue: """Transactions dataloader enpoint.""" return transaction.transactions_dataloader( body.transaction_ids, authorized_resources ) class TransactionsAggregationsQuery(BaseSchema): """Schema for transactions aggregations request query parameters.""" report_run_id: int collaborator_dp_enabled: Optional[bool] = True @app.route("/transactions/aggregations", methods=["GET"]) @validate() def transactions_aggregations( query: TransactionsAggregationsQuery, ) -> ResponseReturnValue: """Transactions aggregations enpoint.""" return transaction.transaction_aggregations(**query.model_dump()) class ParticipationDataloaderEntry(BaseSchema): """Schema for a single participation entry in the dataloader request.""" collaborator_id: int statement_period_id: int limit: Optional[int] = Field(default=None, ge=0) offset: Optional[int] = Field(default=None, ge=0) class TransactionsParticipationDataloaderBody(BaseSchema): """Schema for transactions participation dataloader request body.""" participations: list[ParticipationDataloaderEntry] @app.route("/transactions/participation-dataloader", methods=["POST"]) @utils.fetch_authorized_resources @validate() def transactions_participation_dataloader( authorized_resources, user, body: TransactionsParticipationDataloaderBody ) -> ResponseReturnValue: """Transactions participation dataloader endpoint.""" return transaction.transactions_participation_dataloader( body.participations, authorized_resources )