"""Collaborator handlers.""" from datetime import datetime from http import HTTPStatus from typing import Optional from flask import g, request from flask.typing import ResponseReturnValue from flask_pydantic import validate from pydantic import Field, model_validator from collaborator.api import app from collaborator.constants import header from collaborator.constants.header import MONEYHUB_PROFILE, SETTINGS_PROFILE from collaborator.logic import collaborator, reports, transferwise_transaction from collaborator.models.ows import ows_users from collaborator.models.ows.ows_account import has_direct_payments from collaborator.models.rds.collaborator import Collaborator from collaborator.schemas import BaseSchema, CommaSeparatedList from collaborator.schemas.collaborator import ( CollaboratorIdsBody, GetCollaboratorSplitsRequestSchema, GetCollaboratorSplitsResponseSchema, ) from collaborator.utils import handlers as utils from collaborator.utils.helpers import ( check_admin_access_to_vendor, check_collaborators_authorization, check_vendors_authorization, get_column_by_name, ) from collaborator.utils.typing import ACCOUNT_TYPE_VENDOR, Account def _account_from_collabs_auth_result(collabs, collab_id): vendor_id = collabs[collab_id]["vendor_id"] account = Account(id=vendor_id, type=ACCOUNT_TYPE_VENDOR) return account class GetCollaboratorsQuery(BaseSchema): """Query parameters for GET /collaborators.""" vendor_id: Optional[int] = None term: Optional[str] = None dimensions: CommaSeparatedList[str] = ["name"] limit: int = 0 offset: int = 0 has_recipient: Optional[bool] = None type: Optional[str] = None exactly_match_term: Optional[bool] = None @app.route("/collaborators", methods=["GET"]) @validate() @utils.fetch_authorized_resources def get_collaborators( authorized_resources, user, query: GetCollaboratorsQuery ) -> ResponseReturnValue: """Get the list of collaborators for the specified account. Args: account (Account): Account which made the request. Returns: flask.Response: list of collaborators """ if query.vendor_id is not None: check_vendors_authorization(authorized_resources, [query.vendor_id]) search_dimensions = [ get_column_by_name(Collaborator, dim) for dim in query.dimensions ] result = collaborator.search_collaborators( authorized_resources=authorized_resources, vendor_id=query.vendor_id, search_term=query.term, dimensions=search_dimensions, offset=query.offset, limit=query.limit, has_recipient=query.has_recipient, collaborator_type=query.type, exactly_match_term=query.exactly_match_term, ) return result.message @app.route("/collaborators/", methods=["GET"]) @utils.fetch_authorized_resources def get_collaborator( collaborator_id, authorized_resources, user ) -> ResponseReturnValue: """Get a single collaborator based on its ID. Args: collaborator_id (int): ID of the collaborator. account (Account): Account which made the request. Returns: flask.Response: a collaborator """ collabs = check_collaborators_authorization(authorized_resources, [collaborator_id]) account = _account_from_collabs_auth_result(collabs, collaborator_id) return collaborator.get_by_id_and_account(collaborator_id, account) @app.route("/collaborators//splits", methods=["GET"]) @validate() @utils.fetch_authorized_resources def get_collaborator_splits( collaborator_id, authorized_resources, user, query: GetCollaboratorSplitsRequestSchema, ): """Get a collaborator's splits. Returns: flask.Response: collaborator splits """ check_collaborators_authorization(authorized_resources, [collaborator_id]) splits = collaborator.get_splits_by_collaborator_id( collaborator_id=collaborator_id, split_types=query.split_types, ) return GetCollaboratorSplitsResponseSchema( splits=splits, ) @app.route("/collaborators/dataloader", methods=["POST"]) @validate() @utils.fetch_authorized_resources def get_collaborators_dataloader( authorized_resources, user, body: CollaboratorIdsBody ) -> ResponseReturnValue: """Get multiple collaborators by ID. Args: collaborator_id (int): ID of the collaborator. account (Account): Account which made the request. Returns: flask.Response: a list of collaborators """ collaborator_ids = body.collaborator_ids # If the request originates from Settings, also fetch their MoneyhubProfile resources. # TODO: We probably want to systematise this, such that we establish a profile # hierarchy and check all valid profiles for resource authorization irrespective # of the request's profile type. authorized_collaborators_by_id = {} if request.headers.get(header.ORCHARD_PROFILE_TYPE) == SETTINGS_PROFILE: moneyhub_profile = ows_users.get_profile_for_identity( MONEYHUB_PROFILE, g.request_context.identity_id ) if moneyhub_profile is not None: resources_result = utils.fetch_profile_resources( MONEYHUB_PROFILE, moneyhub_profile["profile_id"] ) if resources_result is not None: authorized_collaborators_by_id.update( check_collaborators_authorization( resources_result, collaborator_ids, throw_if_unauthorized=False ) ) authorized_collaborators_by_id.update( check_collaborators_authorization( authorized_resources, collaborator_ids, throw_if_unauthorized=False ) ) return [ {"data": authorized_collaborators_by_id.get(collaborator_id)} for collaborator_id in collaborator_ids ] @app.route("/collaborators/balances-dataloader", methods=["POST"]) @validate() @utils.fetch_authorized_resources def get_collaborator_balances_dataloader( authorized_resources, user, body: CollaboratorIdsBody ) -> ResponseReturnValue: """Get balances for multiple collaborators.""" return collaborator.get_balances_by_ids(authorized_resources, body.collaborator_ids) class CreateCollaboratorBody(BaseSchema): """Request body for POST /collaborators. Sample request body: { "name": "First Person", "participant_id": "1", "subaccount_id": 1234, "currency": "USD", "collaborator_type": "SUBACCOUNT", "vendor_id": 12345 } """ name: str = Field(min_length=1) vendor_id: int participant_id: Optional[str] = None subaccount_id: Optional[int] = None currency: Optional[str] = None collaborator_type: Optional[str] = None description: Optional[str] = None internal_id: Optional[str] = None performance_rights: bool = True @app.route("/collaborators", methods=["POST"]) @validate() @utils.fetch_authorized_resources def create_collaborator( authorized_resources, user, body: CreateCollaboratorBody ) -> ResponseReturnValue: """Create a collaborator. Args: account (Account): Account which made the request. user (User): User who is creating the collaborator. body (CreateCollaboratorBody): Body parameters Returns: flask.Response """ if has_direct_payments(str(body.vendor_id)): check_admin_access_to_vendor(user.id, body.vendor_id) check_vendors_authorization(authorized_resources, [body.vendor_id]) account = Account(id=str(body.vendor_id), type=ACCOUNT_TYPE_VENDOR) result, created = collaborator.create_collaborator( body.participant_id, body.name, account, body.collaborator_type, body.currency, user, body.subaccount_id, body.description, body.internal_id, body.performance_rights, ) return result, (HTTPStatus.CREATED if created else HTTPStatus.OK) class UpdateCollaboratorBody(BaseSchema): """Schema for update collaborator request body.""" currency: Optional[str] = None description: Optional[str] = None email: Optional[str] = None internal_id: Optional[str] = None name: Optional[str] = None performance_rights: Optional[bool] = None dp_enabled: Optional[bool] = None dp_enabled_date: Optional[datetime] = None dp_splits_agreed_date: Optional[datetime] = None @model_validator(mode="before") @classmethod def load_dates_from_boolean_fields(cls, data: dict): """Load dp_enabled_date and dp_splits_agreed_date from boolean fields.""" if not isinstance(data, dict): return data if "dp_enabled" in data: data["dp_enabled_date"] = datetime.now() if data.get("dp_enabled") else None if "dp_splits_agreed" in data: data["dp_splits_agreed_date"] = ( datetime.now() if data.pop("dp_splits_agreed") else None ) return data @app.route("/collaborators/", methods=["PUT"]) @validate() @utils.fetch_authorized_resources def update_collaborator( collaborator_id: int, authorized_resources, user, body: UpdateCollaboratorBody ) -> ResponseReturnValue: """Update a collaborator. Sample request body: { "name": "First Person", "performance_rights": 1 } Args: collaborator_id (int): The collaborator's unique identifier account (Account): Account which made the request. user (User): User who is updating the collaborator. Returns: flask.Response """ collabs = check_collaborators_authorization(authorized_resources, [collaborator_id]) collab: dict = collabs.get(collaborator_id, {}) if collab.get("dp_enabled_date") is not None: check_admin_access_to_vendor(user.id, collab["vendor_id"]) account = _account_from_collabs_auth_result(collabs, collaborator_id) return collaborator.update_collaborator( collaborator_id, body.model_dump(exclude_unset=True), account, user ) @app.route("/collaborators/latest-reports-dataloader", methods=["POST"]) @validate() @utils.fetch_authorized_resources def get_collaborator_latest_reports_dataloader( authorized_resources, user, body: CollaboratorIdsBody ) -> ResponseReturnValue: """Get latest reports for multiple collaborators.""" return reports.get_latest_reports_by_collaborator_ids( authorized_resources, body.collaborator_ids ) class GetCollaboratorPaymentsQuery(BaseSchema): """Query parameters for GET /collaborators//payments.""" payment_statuses: CommaSeparatedList[str] = [] limit: int = 0 offset: int = 0 sort_key: Optional[str] = None sort_direction: Optional[str] = None @app.route("/collaborators//payments", methods=["GET"]) @validate() @utils.fetch_authorized_resources def get_collaborator_payments( collaborator_id, authorized_resources, user, query: GetCollaboratorPaymentsQuery ) -> ResponseReturnValue: """Get the up to date collaborator payments. Args: collaborator_id (int): ID of the collaborator. Returns: flask.Response: With the collaborator transferwise transactions """ collabs = check_collaborators_authorization(authorized_resources, [collaborator_id]) account = _account_from_collabs_auth_result(collabs, collaborator_id) result = transferwise_transaction.get_payments( vendor_id=account.id, collaborator_id=collaborator_id, payment_statuses=query.payment_statuses, limit=query.limit, offset=query.offset, sort_key=query.sort_key, sort_direction=query.sort_direction, ) return result.message