"""Transaction logic.""" from datetime import datetime from decimal import Decimal from typing import List, Optional from uuid import uuid4 from collaborator.constants import error from collaborator.constants import transaction as constants from collaborator.models.rds.collaborator_persister import CollaboratorPersister from collaborator.models.rds.report_persister import ReportPersister from collaborator.models.rds.statement_period_persister import StatementPeriodPersister from collaborator.models.rds.transaction_persister import TransactionPersister from collaborator.utils import api as api_utils from collaborator.utils import logging from collaborator.utils.error import OwsError from collaborator.utils.helpers import ( check_collaborators_authorization, check_vendors_authorization, ) from collaborator.utils.transaction import round_amount from collaborator.utils.typing import Account, User def create_transactions( user: User, transaction_data: list, collabs_by_id: dict, ) -> list: """Create several transactions for a collaborator. The transaction data is a list of dict objects containing the contents for the transactions. Args: account (Account): The account creating the transactions. user (User): The user creating the transactions. transaction_data(data): Transaction data. Returns: Response: Created transactions. """ report_ids = [ t["report_id"] for t in transaction_data if "report_id" in t and t["report_id"] is not None ] existing_transactions_for_reports = ( TransactionPersister.get_transactions_by_report_ids(report_ids) ) if len(existing_transactions_for_reports) > 0: raise OwsError( code=error.ERROR_CODE_TRANSACTION_EXISTS_FOR_REPORT, message=error.ERROR_MESSAGE_TRANSACTION_EXISTS_FOR_REPORT, ) account_ids = [collab["vendor_id"] for collab in collabs_by_id.values()] open_periods_by_account_id = StatementPeriodPersister.get_open_statement_periods( account_ids=account_ids, ) transactions = [] creation_batch_uuid = str(uuid4()) for data in transaction_data: data["creation_batch_uuid"] = creation_batch_uuid data["date"] = datetime.strptime(data["date"], "%Y-%m-%d").date() collaborator = collabs_by_id[data["collaborator_id"]] open_period = open_periods_by_account_id[collaborator["vendor_id"]] if data["currency"] != collaborator["currency"]: raise OwsError( code=error.ERROR_CODE_UNEXPECTED_CURRENCY, message=error.ERROR_MESSAGE_UNEXPECTED_CURRENCY.format( received=data["currency"], expected=collaborator["currency"] ), ) original_amount = round_amount(Decimal(data["original_amount"])) chargeable_amount = round_amount( original_amount * Decimal(data["collaborator_share"]) if "collaborator_share" in data and data["collaborator_share"] is not None else original_amount ) if data["type"] in constants.DEBIT_TYPES: original_amount = -original_amount chargeable_amount = -chargeable_amount transactions.append( { **data, "collaborator_id": collaborator["id"], "original_amount": original_amount, "chargeable_amount": chargeable_amount, "statement_period_id": open_period.statement_period_id, "description": data.get("description"), "collaborator_share": data.get("collaborator_share"), "report_id": data.get("report_id"), } ) transactions = TransactionPersister.create_transactions( transactions, creation_batch_uuid=creation_batch_uuid, ) logging.bulk_log_events( logging.LOG_EVENT_CREATE, "transaction", user=user, data=[ { "id": txn["id"], "original": None, "updated": txn, } for txn in transactions ], ) transactions_with_reports = [ {"report_id": t["report_id"], "transaction_id": t["id"]} for t in transactions if "report_id" in t and t["report_id"] is not None ] if len(transactions_with_reports): ReportPersister.update_reports_with_transactions( transactions_with_reports, user ) return transactions def get_transactions( collaborator_id: int, account: Account, statement_period_id: Optional[int] = None, limit: int = 0, offset: int = 0, ): """Get transactions for a collaborator. Args: collaborator_id (int): The collaborator unique identifier. account (Account): Account which made the request. limit (int): how many transactions to retrieve. offset (int): the offset (for pagination). Returns: Response with the collaborator's transactions. """ account_ownership = CollaboratorPersister.get_by_id_and_account( collaborator_id, account ) if not account_ownership: return account_ownership transactions, total_records = TransactionPersister.get_transactions( collaborator_id=collaborator_id, statement_period_id=statement_period_id, limit=limit, offset=offset, ) return api_utils.create_paginated_response(transactions, total_records) def delete_transactions(transaction_ids: List[int], authorized_resources): """Soft-delete multiple transactions.""" vendor_ids = TransactionPersister.get_vendor_ids_for_transactions(transaction_ids) check_vendors_authorization( authorized_resources, vendor_ids, ) TransactionPersister.delete_by_ids(transaction_ids) ReportPersister.remove_transactions_from_reports(transaction_ids) return None def transactions_dataloader(transaction_ids: List[int], authorized_resources): """Get transactions by IDs.""" vendor_ids = TransactionPersister.get_vendor_ids_for_transactions(transaction_ids) check_vendors_authorization( authorized_resources, vendor_ids, throw_if_unauthorized=False, ) transactions, _ = TransactionPersister.get_transactions( transaction_ids=transaction_ids ) transactions_by_transaction_id = { transaction["id"]: transaction for transaction in transactions } message = [ {"data": transactions_by_transaction_id.get(transaction_id)} for transaction_id in transaction_ids ] return message def transaction_aggregations( report_run_id: int, collaborator_dp_enabled: bool, ): """Get transaction aggregations by report run ID.""" aggregations = TransactionPersister.get_aggregations_for_report_run( report_run_id=report_run_id, collaborator_dp_enabled=collaborator_dp_enabled, ) completed_date: Optional[datetime] = aggregations._mapping.completed_date return { "completed_date": ( None if completed_date is None else completed_date.strftime("%Y-%m-%dT%H:%M:%SZ") ), "total_count": aggregations._mapping.total_count, "non_zero_count": aggregations._mapping.non_zero_count, "currency_agnostic_total": float(aggregations._mapping.currency_agnostic_total), } def transactions_participation_dataloader(participations, authorized_resources): """Get transactions for a list of (collaborator_id, statement_period_id) pairs.""" collaborator_ids = list({p.collaborator_id for p in participations}) authorized_collaborator_ids = set( check_collaborators_authorization( authorized_resources, collaborator_ids, throw_if_unauthorized=False ).keys() ) results = TransactionPersister.get_transactions_for_participations( [ (p.collaborator_id, p.statement_period_id) for p in participations if p.collaborator_id in authorized_collaborator_ids ] ) # Apply per-participation pagination in Python. The persister always fetches all # transactions and sets total_count to the full count, so slicing here preserves # the accurate total_count while returning only the requested page. for p in participations: key = (p.collaborator_id, p.statement_period_id) if key in results: start = p.offset or 0 end = None if p.limit in (None, 0) else start + p.limit results[key] = { "transactions": results[key]["transactions"][start:end], "total_count": results[key]["total_count"], } message = [ { "data": results.get( (p.collaborator_id, p.statement_period_id), {"transactions": [], "total_count": 0}, ) } for p in participations ] return message