"""Bulk create splits for collaborator.""" from collections.abc import Sequence from dataclasses import dataclass import datetime import logging from multiprocessing.pool import ThreadPool import os import time import sqlalchemy from sqlalchemy.engine import RowMapping from collaborator.api import app from collaborator.constants import transferwise_transfer as tt_constants from collaborator.logic import transferwise from collaborator.models.transferwise import Transferwise from collaborator.utils.error import OwsError from collaborator.utils.typing import ACCOUNT_TYPE_VENDOR, Account from scripts import script_util as util COMPLETED_STATUSES = [ tt_constants.STATUS_OUTGOING_PAYMENT_SENT, tt_constants.STATUS_FUNDS_REFUNDED, tt_constants.STATUS_CANCELLED, ] TwTxn = RowMapping CompareResult = tuple[str, TwTxn] | bool @dataclass class Mismatch: """A wise/rds mismatch.""" tw_txn_id: str created_date: str wise_status: str rds_status: str def split_by_comma(string: str) -> list[int]: """Split a comma-delimited string into a list.""" result = [int(subst.strip()) for subst in string.strip().split(",") if subst != ""] return result def log_mismatch_table_row( tw_txn_id: str, created_date: str, wise_status: str, rds_status: str ) -> None: """Log a row of the mismatch table.""" logging.info( "| {} | {} | {} | {} |".format( str(tw_txn_id).center(len("tw_transaction_id")), str(created_date).center(len("0000-00-00 00:00:00")), str(wise_status).center(len("transfer_fetch_err:00000000")), str(rds_status).center(len("transfer_fetch_err:00000000")), ) ) def compare_wise_transaction_status_to_rds( mismatches: list[Mismatch], tw_txn: TwTxn, ignore_profile_error: bool = False ) -> CompareResult: """Compare wise transaction status to status in RDS.""" account = Account(id=tw_txn["vendor_id"], type=ACCOUNT_TYPE_VENDOR) wise = Transferwise(account=account) rds_status = str(tw_txn["status"]) with app.app_context(): try: wise_status = wise.get_transfer_status(tw_txn["transfer_id"]) if wise_status != rds_status: mismatches.append( Mismatch( tw_txn_id=tw_txn["id"], created_date=tw_txn["created_date"], wise_status=wise_status, rds_status=rds_status, ) ) return wise_status, tw_txn else: return False except OwsError: if not ignore_profile_error: mismatches.append( Mismatch( tw_txn_id=tw_txn["id"], created_date=tw_txn["created_date"], wise_status=f"profile_fetch_err:{account}", rds_status=rds_status, ) ) return False def run_transaction_status_check( env: str = "dev", transaction_ids: list[int] | None = None, excluded_vendors: list[int] | None = None, should_update_transactions: bool = False, exclude_completed: bool = False, ignore_profile_error: bool = False, ) -> None: """Check if a list of transactions have the same status in Wise and RDS.""" collabs_conn = util.ows_collaborator_connection() transaction_ids = transaction_ids or [] excluded_vendors = excluded_vendors or [] sql = """ SELECT tt.*, c.vendor_id FROM transferwise_transaction tt INNER JOIN collaborator c on tt.collaborator_id = c.id WHERE TRUE """ # Filter out excluded vendor IDs excluded_vendors_bindings = {f"v{i}": vid for i, vid in enumerate(excluded_vendors)} if len(excluded_vendors) > 0: excluded_subst = ",".join([f":{vi}" for vi in excluded_vendors_bindings.keys()]) sql = f"{sql} AND c.vendor_id NOT IN({excluded_subst})" # Filter out transactions not specified transaction_ids_bindings = {f"t{i}": tid for i, tid in enumerate(transaction_ids)} if len(transaction_ids) > 0: transaction_subst = ",".join( [f":{ti}" for ti in transaction_ids_bindings.keys()] ) sql = f"{sql} AND tt.id IN({transaction_subst})" # Filter out completed transactions if exclude_completed: statuses = ", ".join([f"'{status}'" for status in COMPLETED_STATUSES]) sql = f"{sql} AND tt.status NOT IN ({statuses})" tw_txns: Sequence[TwTxn] = ( collabs_conn.execute( sqlalchemy.text(sql), {**excluded_vendors_bindings, **transaction_ids_bindings}, ) .mappings() .all() ) tw_txn_count = len(tw_txns) logging.info("Checking {} transactions...".format(tw_txn_count)) # Do the comparisons in parallel mismatches: list[Mismatch] = [] with ThreadPool(10) as pool: results: list[CompareResult] = pool.map( lambda transaction: compare_wise_transaction_status_to_rds( mismatches, transaction, ignore_profile_error ), tw_txns, ) mismatches_count = sum(1 for result in results if result) if mismatches_count == 0: logging.info("No mismatches found.") return logging.info(f"Found mismatches for {mismatches_count} transactions:") log_mismatch_table_row( "TW TRANSACTION ID", "CREATED DATE", "WISE STATUS or ERROR", "RDS STATUS" ) for mismatch in mismatches: log_mismatch_table_row(**mismatch.__dict__) if not should_update_transactions: return logging.info("Auto-fixing mismatches...") for result in results: if not isinstance(result, tuple): continue wise_status, tw_txn = result occurred_at = datetime.datetime.now().replace(tzinfo=datetime.timezone.utc) transferwise.update_payment_status( tw_txn["transfer_id"], wise_status, occurred_at ) logging.info( "TW Txn {} (Vendor {}): Updated status {} → {}".format( tw_txn["id"], tw_txn["vendor_id"], tw_txn["status"], wise_status, ) ) logging.info("Auto-fixing done.") if __name__ == "__main__": start = time.time() logging.basicConfig(level=logging.INFO) logging.info("Run transaction status check") env = os.environ.get("Environment", "dev") logging.info(f"Environment: {env}") transaction_ids_env = os.environ.get("TRANSACTION_IDS", "") exclude_vendor_ids_env = os.environ.get("EXCLUDE_VENDOR_IDS", "") should_update_transactions = os.environ.get("SHOULD_UPDATE_TRANSACTIONS") == "true" exclude_completed = os.environ.get("EXCLUDE_COMPLETED") == "true" ignore_profile_error = os.environ.get("IGNORE_PROFILE_ERROR") == "true" transaction_ids = split_by_comma(transaction_ids_env) exclude_vendor_ids = split_by_comma(exclude_vendor_ids_env) run_transaction_status_check( env, transaction_ids, exclude_vendor_ids, should_update_transactions, exclude_completed, ignore_profile_error, ) elapsed = time.time() - start logging.info("Status check finished in {0:.2f} seconds.".format(elapsed))