"""TransferWise logic tier.""" from datetime import datetime, timezone from functools import partial from multiprocessing.pool import ThreadPool from typing import Any, Dict, Optional from urllib.parse import urlencode from flask import g from oto import response, status from sentry_sdk import capture_exception from sqlalchemy.orm.session import Session from collaborator import config from collaborator.connectors import mysql from collaborator.connectors.datadog import call_datadog_with_event from collaborator.constants import email as email_constants from collaborator.constants import error from collaborator.constants import logging as logging_constants from collaborator.constants import transaction as txn_constants from collaborator.constants import transferwise_batch as tw_batch from collaborator.constants import transferwise_profile as tw_profile from collaborator.constants import transferwise_transfer as tw_transfer from collaborator.constants import transferwise_webhook_events as tw_events from collaborator.constants.transferwise_profile import TransferwiseProfileStatus from collaborator.logic import collaborator from collaborator.models.ows import ows_abacus_account, ows_account, ows_users from collaborator.models.rds.recipient_persister import RecipientPersister from collaborator.models.rds.statement_period_persister import StatementPeriodPersister from collaborator.models.rds.transaction_persister import TransactionPersister from collaborator.models.rds.transferwise_batch_persister import ( TransferwiseBatchPersister, ) from collaborator.models.rds.transferwise_profile_persister import ( TransferwiseProfilePersister, ) from collaborator.models.rds.transferwise_transaction_persister import ( TransferwiseTransactionPersister, ) from collaborator.models.transferwise import Transferwise from collaborator.utils import logging, ses from collaborator.utils.error import OwsError from collaborator.utils.helpers import ( check_vendors_authorization, get_from_response, sanitize_data, ) from collaborator.utils.typing import Account, User def check_can_access_batch_group(authorized_resources, batch_group_id): """Check profile can access batch group with ID.""" transferwise_batch = TransferwiseBatchPersister.get_by_batch_uuid(batch_group_id) vendor_id = transferwise_batch.get("vendor_id") check_vendors_authorization(authorized_resources, [vendor_id]) return vendor_id def check_can_access_batch(authorized_resources, batch_id): """Check profile can access batch with ID.""" transferwise_batch = TransferwiseBatchPersister.get_by_id(batch_id) vendor_id = transferwise_batch.get("vendor_id") check_vendors_authorization(authorized_resources, [vendor_id]) return vendor_id def _filter_profile_data(profile: dict) -> dict: """Filter the TransferWise profile data to remove access credentials. Args: profile (dict): The data to filter Returns: dict """ return {k: v for k, v in profile.items() if k not in tw_profile.CREDENTIAL_FIELDS} def _create_batch_transfer( transfer: dict, tw_client: Transferwise, profile_id: int, batch_id: str ) -> dict: """Create a transfer based on a quote. Args: transfer (dict): Transfer data. tw_client (Transferwise): TW client object. profile_id (int): Profile to create the transfer for. batch_id (str): Batch to create the transfer in. Returns: dict: Transfer and quote details. """ collaborator_id = transfer.pop("collaborator_id", None) quote = tw_client.get_quote(transfer["quoteUuid"], profile_id) quote_details = next( option for option in quote["paymentOptions"] if option["payIn"] == "BANK_TRANSFER" ) transfer = tw_client.create_batch_transfer(transfer, profile_id, batch_id) transfer["fee"] = quote_details["fee"] transfer["sourceValue"] = quote_details["sourceAmount"] transfer["targetValue"] = quote_details["targetAmount"] transfer["sourceCurrency"] = quote_details["sourceCurrency"] transfer["targetCurrency"] = quote_details["targetCurrency"] transfer["collaborator_id"] = collaborator_id return transfer def refresh_account_requirements(source, target, payload): """Refresh account requirements. Args: source (str): the source currency target (str): the target currency payload(dict): dictionary with the payload for refreshing account requirements Returns: response.Response: with the account requirements as a message. """ TransferwiseClient = Transferwise() return response.Response( TransferwiseClient.refresh_account_requirements(source, target, payload) ) def get_account_requirements(source, target): """Retrieve TW Account requirements based on currency. Args: source (string): The source currency. source (string): The target currency. Returns: response.Response: Contains a list of TW account requirements """ TranferwiseClient = Transferwise() return response.Response(TranferwiseClient.get_account_requirements(source, target)) def get_field_validation(validator, query_string_params): """Retrieve TW Account requirements based on currency. Args: validator (str): This would be the specific path of the validation. query_string_params (str): the constructed query string with the fields and values that needs validation. Returns: response.Response: with the validation of the field as payload. """ TranferwiseClient = Transferwise() return response.Response( TranferwiseClient.get_field_validation(validator, query_string_params) ) def _get_post_auth_redirect_url(account: Account) -> str: """Get the URL to redirect back to after completing the TransferWise OAuth flow. Args: account (Account): The account for which to get the redirect URL. Returns: str: The URL to be redirected to after completing the TransferWise OAuth flow. """ account_metadata = ows_account.get_account_metadata(account) company_brand = account_metadata["company_brand"] # Default to The Orchard if the brand doesn't have a specific redirect URL redirect_uri = config.TRANSFERWISE_POST_AUTHORIZATION_REDIRECT_URLS.get( company_brand, config.TRANSFERWISE_POST_AUTHORIZATION_REDIRECT_URLS["theorchard"], ) return redirect_uri def get_authorization_redirect_url(account: Account) -> str: """Get the redirect URI for the TransferWise OAuth flow. Args: account (Account): The account for which to get the redirect URI. Returns: str: The redirect URL for the TransferWise OAuth flow. """ post_auth_redirect_url = _get_post_auth_redirect_url(account) url = "{}?{}".format( config.TRANSFERWISE_AUTHORIZATION_REDIRECT_URL, urlencode( { "client_id": config.TRANSFERWISE_CLIENT_ID, "redirect_uri": post_auth_redirect_url, } ), ) return url def create_or_update_profile( account: Account, subaccount_id: Optional[int], user: User, profile_id: int, code: str, ) -> response.Response: """Create or update a TransferWise profile. Args: account (Account): Account which is creating the profile. subaccount_id (int): Optional subaccount for the account. user (User): User who made the request profile_id (int): TransferWise Profile ID. code (str): Temporary code from TW Returns: response.Response: with the profile data """ g.ows.log.warning(f"TW_PROFILE create_or_update_profile({account}, {profile_id})") redirect_uri = _get_post_auth_redirect_url(account) transferwise_client = Transferwise() oauth_response = transferwise_client.create_oauth_token(code, redirect_uri) access_token = oauth_response["access_token"] refresh_token = oauth_response["refresh_token"] abacus_account = ows_abacus_account.get_abacus_account_metadata(account) vendor_currency = get_from_response(abacus_account, "currency_code") verification_status = transferwise_client.get_profile_verification_status( profile_id, vendor_currency, access_token ) g.ows.log.warning(f"TW_PROFILE Verification status: {verification_status}") try: profile = TransferwiseProfilePersister.get_active_profile( account, subaccount_id ) new_profile = TransferwiseProfilePersister.update_active_profile( account, subaccount_id, profile_id, access_token, refresh_token, verification_status, ) g.ows.log.warning("TW_PROFILE Updated: {}".format(new_profile["id"])) logging.log_event( logging.LOG_EVENT_UPDATE, "transferwise_profile", new_profile["id"], profile, new_profile, user, ) profile_reponse = response.Response(_filter_profile_data(new_profile)) except OwsError: profile = TransferwiseProfilePersister.create_profile( account, subaccount_id, profile_id, access_token, refresh_token, verification_status, ) g.ows.log.warning("TW_PROFILE Created: {}".format(profile["id"])) logging.log_event( logging.LOG_EVENT_CREATE, "transferwise_profile", profile["id"], None, profile, user, ) profile_reponse = response.Response( message=_filter_profile_data(profile), status=status.CREATED ) return profile_reponse def create_recipient( account, payload: dict, collaborator_id: int, user: User, subaccount_id=None ): """Create a TW recipient. Args: account (Account): Account for the profile. payload (dict): the payload collaborator_id (int): collaborator unique identifier. user (User): the user object subaccount_id (int): optional subaccount_id Returns: dict: the updated collaborator data. """ TransferwiseClient = Transferwise(account=account, subaccount_id=subaccount_id) recipient_result = TransferwiseClient.create_recipient(payload) recipient = RecipientPersister.create_recipient( recipient_result["id"], recipient_result["accountHolderName"], recipient_result["currency"], user, ) updated_collaborator_response = collaborator.update_collaborator( collaborator_id, {"recipient_id": recipient.get("id")}, account, user ) collaborator_name = updated_collaborator_response.get("name") send_email_to_master_contact( account, user, email_constants.RECIPIENT, {"collaborator_name": collaborator_name}, ) return updated_collaborator_response def delete_recipient( account, collaborator_id: int, recipient_id: int, user: User, subaccount_id=None ): """Delete a TW recipient. Args: account (Account): Account for the profile. collaborator_id (int): collaborator unique identifier. recipient_id (int): recipient_id user (User): the user object subaccount_id (int): optional subaccount_id Returns: dict: the updated collaborator data. """ current_collaborator = collaborator.get_by_id_and_account(collaborator_id, account) RecipientPersister.delete_recipient(current_collaborator.get("recipient_id"), user) updated_collaborator_response = collaborator.update_collaborator( collaborator_id, {"recipient_id": None}, account, user ) collaborator_name = updated_collaborator_response.get("name") send_email_to_master_contact( account, user, email_constants.RECIPIENT_DELETED, {"collaborator_name": collaborator_name}, ) return updated_collaborator_response def get_vendor_profile(account): """Get a TransferWise profile for a vendor. Args: account (Account): Account for the profile. Returns: response.Response: with the profile data """ profile = TransferwiseProfilePersister.get_active_profile(account, None) return response.Response(sanitize_data(_filter_profile_data(profile))) def update_verification_status( profile_id: int, profile_status: str ) -> response.Response: """Update the verification status of an TransferWise profile. The profile_status is expected to be the one provided by TransferWise and will be converted into the one used in our data model. See the API documentation for more info: https://transferwise.github.io/api-docs-partners/#webhook-events-verification-state-change-event Args: profile_id (int): TransferWise profile ID profile_status (str): Status to update the profile to Returns: response.Response """ g.ows.log.warning( "TW_PROFILE update_verification_status({}, {})".format( profile_id, profile_status ) ) if profile_status == "verified": profile_status = TransferwiseProfileStatus.VERIFIED elif profile_status == "not_verified": profile_status = TransferwiseProfileStatus.UNVERIFIED else: raise Exception(f'Unhandled profile status "{profile_status}"') results = TransferwiseProfilePersister.update_profile_status( profile_id, profile_status ) updated_ids = [item["id"] for item in results] g.ows.log.warning(f"TW_PROFILE Updated: {updated_ids}") return response.Response(status=status.OK) @mysql.db_session def update_payment_status( transfer_id: int, transfer_status: str, occurred_at: datetime, session: Session ) -> response.Response: """Update the status of a TransferWise payment. The payment_status is expected to be the one provided by TransferWise and will be converted into the one used in our data model. See the API documentation for more info: https://transferwise.github.io/api-docs-partners/#webhook-events-verification-state-change-event Args: transfer_id (int): TransferWise transfer ID transfer_status (str): Status to update the profile to Returns: response.Response """ if transfer_status not in tw_transfer.STATUSES: raise Exception(f'Unhandled transfer status "{transfer_status}"') try: tw_transaction = TransferwiseTransactionPersister.get_by_transfer_id( transfer_id, session=session ) except OwsError as err: call_datadog_with_event( title=logging_constants.WEBHOOK_ERROR_FETCHING_WISE_TXN.format(transfer_id), text=err.message, tags=logging_constants.TRANSFERWISE_TAGS, ) return response.Response(status=status.OK) TransferwiseTransactionPersister.update_transaction_status( transfer_status, transfer_id, occurred_at, session=session ) # Exit early if there are no Transactions to create if transfer_status not in [ tw_transfer.STATUS_OUTGOING_PAYMENT_SENT, *tw_transfer.FAILED_STATUSES, ]: return response.Response(status=status.OK) transaction = TransactionPersister.get_transaction_by_transferwise_transaction_id( tw_transaction["id"], session=session ) collab = collaborator.get_collaborator_by_id(tw_transaction["collaborator_id"]) account_id = int(collab.get("vendor_id")) open_statement_period = StatementPeriodPersister.get_open_statement_period( account_id ) txn_to_create = { "collaborator_id": tw_transaction["collaborator_id"], "transaction_date": datetime.now(tz=timezone.utc).date(), "transferwise_transaction_id": (tw_transaction["id"]), "currency": tw_transaction["source_currency"], "statement_period_id": open_statement_period.statement_period_id, "collaborator_share": None, "report_id": None, "voided_transaction_id": None, } # If the transfer fails, we only care if the transfer already reached a "completed" # status before, i.e. if a Transaction already exists. In the case of _any_ failed status, # if a transaction exists, the Collaborator is owed money - so we must credit them. if transfer_status in tw_transfer.FAILED_STATUSES: if not transaction: return response.Response(status=status.OK) if TransactionPersister.is_transaction_credited( tw_transaction["id"], session=session ): return response.Response(status=status.OK) payment_name = txn_constants.DESCRIPTION_RETURNED_PAYMENT.format( desc=transaction["description"] ) txn_to_create.update( { "transaction_type": txn_constants.TYPE_CREDIT, "description": payment_name, "original_amount": tw_transaction["source_amount"], "chargeable_amount": tw_transaction["source_amount"], "credited_payment_id": transaction["id"], } ) elif transfer_status == tw_transfer.STATUS_OUTGOING_PAYMENT_SENT: if transaction: return response.Response(status=status.OK) batch_response = get_batch(account_id, tw_transaction["transferwise_batch_id"]) txn_to_create.update( { "transaction_type": txn_constants.TYPE_PAYMENT, "description": batch_response.message.get("batch_name"), "original_amount": -tw_transaction["source_amount"], "chargeable_amount": -tw_transaction["source_amount"], } ) created = TransactionPersister.create_transaction(**txn_to_create, session=session) logging.log_event( logging.LOG_EVENT_CREATE, "transaction", created["id"], None, created, None, ) return response.Response(status=status.OK) def subscribe_application_to_event(name: str, trigger_on: str) -> response.Response: """Subscribe to application event. Args: name (str): The name of the subscription trigger_on (str): the event hook trigger Returns: response.Response: with the created subscription """ transferwise_client = Transferwise() if trigger_on not in tw_events.application_subscriptions: raise OwsError.not_found( code=error.ERROR_CODE_INVALID_WEBHOOK_SUBSCRIPTION, message=error.ERROR_MESSAGE_INVALID_WEBHOOK_SUBSCRIPTION, ) subscriptions = transferwise_client.get_application_subscriptions() if tw_events.application_subscriptions[trigger_on] in subscriptions: raise OwsError( code=error.ERROR_CODE_WEBHOOK_SUBSCRIPTION_EXISTS, message=error.ERROR_MESSAGE_WEBHOOK_SUBSCRIPTION_EXISTS, ) return response.Response( transferwise_client.subscribe_application_to_event(name, trigger_on) ) def create_quote(account, payload, subaccount_id=None): """Create quote. account (Account): Account for the profile. payload (dict): the payload subaccount_id (int): optional subaccount_id """ TransferwiseClient = Transferwise(account=account, subaccount_id=subaccount_id) requested_quotes = payload.get("quotes") profile_id = requested_quotes[0]["profile"] TransferwiseClient.ping_api(profile_id) with ThreadPool(20) as pool: processed_quotes = pool.map(TransferwiseClient.create_quote, requested_quotes) return response.Response({"quotes": processed_quotes}) def _create_payment( account: Account, user: User, payload: dict, subaccount_id: Optional[int] = None ) -> dict: """Create payment transfers. Args: account (Account): Account for the profile. user (User): User who is creating the transactions. payload (dict): the payload subaccount_id (int): optional subaccount_id Returns: dict: with TransferWise batch which is the response from the TransferwiseBatchPersister """ profile_id = payload["profile_id"] transfers = payload["transfers"] quote_ids = [transfer["quoteUuid"] for transfer in transfers] processed_transfers: list = [] TransferwiseClient = Transferwise(account=account, subaccount_id=subaccount_id) if len(quote_ids) != len(set(quote_ids)): raise OwsError( code=error.ERROR_CODE_BAD_PARAMS, message="Cannot create a TransferWise batch with duplicate quotes.", ) existing_transfers = ( TransferwiseTransactionPersister.get_by_quote_ids_for_vendor_profile( account.id, profile_id, quote_ids ) ) if existing_transfers: existing_quote_ids = {transfer["quote_id"] for transfer in existing_transfers} existing_batch_ids = { transfer["transferwise_batch_id"] for transfer in existing_transfers } if existing_quote_ids == set(quote_ids) and len(existing_batch_ids) == 1: return TransferwiseBatchPersister.get_by_id(existing_batch_ids.pop()) raise OwsError( code=error.ERROR_CODE_BAD_PARAMS, message=( "Cannot create a TransferWise batch because one or more quotes " "already have transfers." ), ) # Create batch in TransferWise batch_payload = { "sourceCurrency": payload.get("sourceCurrency"), "name": payload.get("name"), } create_batch_response = TransferwiseClient.create_batch(profile_id, batch_payload) batch_id = create_batch_response["id"] # Create transfers in TransferWise with ThreadPool(20) as pool: processed_transfers = pool.map( partial( _create_batch_transfer, tw_client=TransferwiseClient, profile_id=profile_id, batch_id=batch_id, ), transfers, ) # Complete batch in TransferWise, retrieving the pay-in details batch_state = TransferwiseClient.get_batch_state(profile_id, batch_id) batch_version = batch_state["version"] complete_batch_response = TransferwiseClient.modify_batch_state( profile_id, batch_id, batch_version ) # Create TransferWise batch in collaborator database batch_data = { "vendor_id": account.id, "profile_id": profile_id, **complete_batch_response, } transferwise_batch = TransferwiseBatchPersister.create_batch( user, _map_batch_payload(batch_data) ) # Create TransferWise transactions in collaborator database processed_transfers_payload = [ { "profile_id": profile_id, "recipient_id": item["targetAccount"], "transferwise_batch_id": transferwise_batch["id"], "transfer_id": item["id"], "quote_id": item["quoteUuid"], "target_currency": item["targetCurrency"], "target_amount": item["targetValue"], "source_currency": item["sourceCurrency"], "source_amount": item["sourceValue"], # We want to ensure that all statuses are lower case for consistency. "status": item["status"].lower(), "wire_fee": item["fee"]["payIn"], "transferwise_fee": item["fee"]["transferwise"], "collaborator_id": item["collaborator_id"], "conversion_rate": item["rate"], } for item in processed_transfers ] TransferwiseTransactionPersister.save_transactions(processed_transfers_payload) # Update TransferWise batch with total sums of all fees total_wire_fee = sum([item["wire_fee"] for item in processed_transfers_payload]) total_transferwise_fee = sum( [item["transferwise_fee"] for item in processed_transfers_payload] ) updated_batch = TransferwiseBatchPersister.update_batch( transferwise_batch["id"], {"wire_fee": total_wire_fee, "transferwise_fee": total_transferwise_fee}, user, ) # Done! Send email send_email_to_master_contact(account, user, email_constants.PAYMENT) return updated_batch def create_batch_payment( account: Account, user: User, payload: dict, subaccount_id: Optional[int] = None ) -> response.Response: """Create payment transfers. Args: account (Account): Account for the profile. user (User): User who is creating the transactions. payload (dict): the payload subaccount_id (int): optional subaccount_id Returns: response.Response: with the created transfer """ payment = _create_payment(account, user, payload, subaccount_id) return response.Response(payment) def _map_batch_payload(batch_reponse_data: dict) -> dict: """Map batch response to batch payload for RDS. Args: batch_response_data (dict): dictionary with the batch metadata Returns: dictionary with the mapped data of the batch """ pay_in_details = next( details for details in batch_reponse_data["payInDetails"] if details["type"] == "bank_transfer" ) bank_address_data = pay_in_details.get("bankAddress", {}) transferwise_data = pay_in_details.get("transferWiseAddress", {}) bank_details = { "bank_address_name": bank_address_data.get("name", None), "bank_address_branch_name": bank_address_data.get("branchName", None), "bank_address_first_line": bank_address_data.get("firstLine", None), "bank_address_post_code": bank_address_data.get("postCode", None), "bank_address_city": bank_address_data.get("city", None), "bank_address_state_code": bank_address_data.get("stateCode", None), "bank_address_country": bank_address_data.get("country", None), } transferwise_details = { "tw_address_name": transferwise_data.get("name", None), "tw_address_first_line": transferwise_data.get("firstLine", None), "tw_address_post_code": transferwise_data.get("postCode", None), "tw_address_city": transferwise_data.get("city", None), "tw_address_state_code": transferwise_data.get("stateCode", None), "tw_address_country": transferwise_data.get("country", None), } return dict( vendor_id=batch_reponse_data["vendor_id"], profile_id=batch_reponse_data["profile_id"], batch_id=batch_reponse_data["id"], version=batch_reponse_data["version"], batch_name=batch_reponse_data["name"], amount=pay_in_details["amount"], currency=pay_in_details["currency"], status=batch_reponse_data["status"], batch_type=pay_in_details["type"], reference=pay_in_details["reference"], payin_name=pay_in_details["name"], bank_code=pay_in_details["bankCode"], account_number=pay_in_details["accountNumber"], iban=pay_in_details.get("iban", None), account_type=pay_in_details.get("accountType", None), bban=pay_in_details.get("bban", None), **bank_details, **transferwise_details, ) def send_email_to_master_contact( account: Account, user: User, topic: str, params: dict = {} ): """Send email to master user. Args: account (Account): Account for the profile. user (User): User who is creating the trans topic (str): the topic of the email params (dict) Optional params """ try: master_contact_response = ows_users.get_master_contact(account) if not master_contact_response: raise Exception(f"Unable to get master contact for account {str(account)}") user_metadata_response = ows_users.get_identity_metadata(user) if not user_metadata_response: raise Exception(f"Unable to get user metadata for user {str(user)}") account_metadata_response = ows_account.get_account_metadata(account) if not account_metadata_response: raise Exception(f"Unable to get vendor metadata for vendor {str(account)}") recipient = master_contact_response user_meta = user_metadata_response account_meta = account_metadata_response recipient_email = recipient.get("email") recipient_first_name = recipient.get("first_name") recipient_last_name = recipient.get("last_name") recipient_name = f"{recipient_first_name} {recipient_last_name}" user_first_name = user_meta.get("first_name") user_last_name = user_meta.get("last_name") user_name = f"{user_first_name} {user_last_name}" vendor_name = account_meta.get("name") if topic == email_constants.PAYMENT: message = email_constants.BODY_PAYMENT.format( master_user=recipient_name, user_name=user_name, vendor_name=vendor_name ) subject = email_constants.SUBJECT_PAYMENT elif topic == email_constants.RECIPIENT: collaborator_name = params.get("collaborator_name") message = email_constants.BODY_RECIPIENT.format( master_user=recipient_name, user_name=user_name, vendor_name=vendor_name, collaborator_name=collaborator_name, ) subject = email_constants.SUBJECT_RECIPIENT elif topic == email_constants.RECIPIENT_DELETED: collaborator_name = params.get("collaborator_name") message = email_constants.BODY_RECIPIENT_DELETED.format( master_user=recipient_name, user_name=user_name, vendor_name=vendor_name, collaborator_name=collaborator_name, ) subject = email_constants.SUBJECT_RECIPIENT_DELETED ses.send_email(recipient_email, config.EMAIL_SENDER, subject, message) call_datadog_with_event( title=logging_constants.EMAIL_REQUEST_SUCCESS_TITLE.format( email=recipient_email, subject=subject ), text=logging_constants.EMAIL_SUCCESS_TEXT, tags=logging_constants.EMAIL_TAGS, ) except Exception as e: capture_exception(e) def get_batch(account_id: int, batch_id: int) -> response.Response: """Get a TransferWise batch. Args: account (Account): Account which made the request. batch_id (int): ID of the batch to get. Returns: Response: Response containing the batch. """ batch = TransferwiseBatchPersister.get_by_id(batch_id) if batch["vendor_id"] != account_id: raise OwsError.forbidden( code=error.ERROR_CODE_AUTHORIZATION, message=error.ERROR_MESSAGE_FORBIDDEN_USER, ) transactions = TransferwiseTransactionPersister.get_for_transferwise_batch_id( batch_id ) imputed_status = _get_batch_imputed_status(transactions) batch_with_imputed_status = { **batch, "imputed_status": imputed_status, } return response.Response(batch_with_imputed_status) def _get_batch_imputed_status(transactions): awaiting_funds_count, completed_funds_count = (0, 0) for txn in transactions: status = txn.get("status", "").lower() if any(status in s for s in tw_transfer.FAILED_STATUSES): return tw_batch.IMPUTED_STATUS_CANCELLED if status == tw_transfer.STATUS_BOUNCED_BACK: return tw_batch.IMPUTED_STATUS_BOUNCED_BACK if status == tw_transfer.STATUS_INCOMING_PAYMENT_WAITING: awaiting_funds_count += 1 if status == tw_transfer.STATUS_OUTGOING_PAYMENT_SENT: completed_funds_count += 1 if awaiting_funds_count == len(transactions): return tw_batch.IMPUTED_STATUS_AWAITING_FUNDS if completed_funds_count == len(transactions): return tw_batch.IMPUTED_STATUS_COMPLETED return tw_batch.IMPUTED_STATUS_IN_PROGRESS def get_batches(account: Account) -> response.Response: """Get all TransferWise batches for this account. Args: account (Account): Account which made the request. Returns: Response: Response containing the batches. """ # Get the batches and the transactions in bulk batches = TransferwiseBatchPersister.get_by_vendor_id(account.id) batch_map = {b["id"]: b for b in batches} transactions = TransferwiseTransactionPersister.bulk_get_for_transferwise_batch_ids( batch_map.keys() ) # Group the transactions together by batch_id grouped_transactions: Dict[str, Any] = {} for txn in transactions: batch_id = txn["transferwise_batch_id"] group = grouped_transactions.setdefault(batch_id, []) group.append(txn) # Impute the status from the batch's txns, and add it to the batch object for batch_id, txns in grouped_transactions.items(): status = _get_batch_imputed_status(txns) batch_map[batch_id]["imputed_status"] = status result = list(batch_map.values()) return response.Response(result) def get_batch_group( account: Account, profile_id: int, batch_group_id: str ) -> response.Response: """Get batch transfer group details by id. Args: account (Account): Account for the profile. profile_id (int): TransferWise profile id batch_group_id (str): UUID for the transfer batch group Returns: response.Response: with the transfer group details. """ transferwise_client = Transferwise(account=account) return response.Response( transferwise_client.get_batch_group(profile_id, batch_group_id) ) def get_transactions_for_batch(batch_id: int, account: Account) -> response.Response: """Get transactions based on a batch ID. Args: batch_id (int): ID of the batch to get transactions for. account (Account): Account making the request. Returns: response.Response: list of transactions """ batch = TransferwiseBatchPersister.get_by_id(batch_id) if batch["vendor_id"] != account.id: raise OwsError.forbidden( code=error.ERROR_CODE_AUTHORIZATION, message=error.ERROR_MESSAGE_FORBIDDEN_USER, ) payments = TransferwiseTransactionPersister.get_for_transferwise_batch_id(batch_id) profile = TransferwiseProfilePersister.get_active_profile(account, None) tw_client = Transferwise(account=account) def _add_price_to_payment(payment): quote = tw_client.get_quote(payment["quote_id"], profile["profile_id"]) payment["price"] = next( option for option in quote["paymentOptions"] if option["payIn"] == "BANK_TRANSFER" )["price"] with ThreadPool(20) as pool: pool.map(_add_price_to_payment, payments) return response.Response(sanitize_data(payments)) def simulate_transfer_processing( transfer_id: int, transfer_status: str ) -> response.Response: """Simulate transfer processing. Args: account (Account): Account for the profile. transfer_id (int): transfer unique identifier transfer_status (str): the target transfer status. Returns: response.Response: with the transfer group details. """ transferwise_transaction = TransferwiseTransactionPersister.get_by_transfer_id( transfer_id ) transferwise_batch = TransferwiseBatchPersister.get_by_id( transferwise_transaction["transferwise_batch_id"] ) transferwise_client = Transferwise( account=Account("vendor", transferwise_batch["vendor_id"]) ) return response.Response( transferwise_client.simulate_transfer_processing(transfer_id, transfer_status) ) def cancel_batch_payment( account: Account, user: User, batch_id: int, subaccount_id: Optional[int] = None ) -> response.Response: """Cancel a batch payment. Args: account (Account): Account for the profile. user (User): User who is creating the transactions. batch_id (int): ID of the batch to get transactions for subaccount_id (int): optional subaccount_id Returns: response.Response: with the canceled batch payment """ existing_batch = TransferwiseBatchPersister.get_by_id(batch_id) if existing_batch["vendor_id"] != account.id: raise OwsError.forbidden( code=error.ERROR_CODE_AUTHORIZATION, message=error.ERROR_MESSAGE_FORBIDDEN_USER, ) TransferwiseClient = Transferwise(account=account, subaccount_id=subaccount_id) profile_id = existing_batch["profile_id"] batch_group_id = existing_batch["batch_id"] current_batch_group = TransferwiseClient.get_batch_group(profile_id, batch_group_id) batch_version = current_batch_group["version"] cancelled_batch = TransferwiseClient.modify_batch_state( profile_id, batch_group_id, batch_version, complete_batch=False ) cancelled_batch_persister = TransferwiseBatchPersister.update_batch( batch_id, {"status": tw_batch.STATUS_CANCELLED, "version": cancelled_batch["version"]}, user, ) return response.Response(cancelled_batch_persister) def get_transfer_requirements(transfers: list) -> response.Response: """Get transfer requirements for all currencies requested. Args: account (Account): Account for the profile. transfers (list): List of objects pairing currency with a xfer Returns: response.Response: with the canceled batch payment """ if len(transfers) < 1: return response.create_error_response( code=error.ERROR_CODE_BAD_PARAMS, message=error.ERROR_MESSAGE_BAD_PARAMS, status=status.BAD_REQUEST, ) transferwise_id = transfers[0]["transfer"]["targetAccount"] vendor_id = RecipientPersister.get_vendor_id_by_transferwise_id(transferwise_id) wise = Transferwise(account=Account("vendor", id=vendor_id)) # BRL requirements are currently hardcoded, so pop them if requested xfers_by_currency = {xfer["currency"]: xfer for xfer in transfers} brl_xfer = xfers_by_currency.pop(tw_transfer.CURRENCY_BRL, None) with ThreadPool(5) as pool: requirements = pool.map( lambda xfer: wise.get_transfer_requirements(xfer), xfers_by_currency.values(), ) reqs_by_currency = {req["currency"]: req for req in requirements} if brl_xfer is not None: reqs_by_currency[tw_transfer.CURRENCY_BRL] = { "currency": tw_transfer.CURRENCY_BRL, "requirements": tw_transfer.BRL_TRANSFER_REQUIREMENTS, } return response.Response([reqs_by_currency[xfer["currency"]] for xfer in transfers])