"""Parse transfer of earnings CSV and create configuration objects.""" import csv import os from typing import Dict, List, Tuple from decimal import Decimal from contract import Contract from transfer_of_earnings_config import Calculation, TransferOfEarningsConfiguration def parse_calculation_type(input_description: str) -> Tuple[str, float, str]: """ Parse a calculation description into (calculation_id, percentage, base_type). Examples: "100% Net Revenue Override" -> ("100pct_net_revenue_override", 100.0, "net_revenue") "50% Closing Balance Override" -> ("50pct_closing_balance_override", 50.0, "closing_balance") "6,300 Override" -> ("6300_override", 6300.0, "fixed_amount") """ if '%' in input_description: parts = input_description.split('%') percentage = float(parts[0].strip().split()[-1]) if 'Net Revenue' in input_description: base_type = 'net_revenue' elif 'Gross Revenue' in input_description: base_type = 'gross_revenue' elif 'Closing Balance' in input_description: base_type = 'closing_balance' else: base_type = 'unknown' else: parts = input_description.split() if parts and parts[0].replace(',', '').replace('.', '').isdigit(): percentage = float(parts[0].replace(',', '')) base_type = 'fixed_amount' else: percentage = 0.0 base_type = 'unknown' calc_id = input_description.replace(' ', '_').replace('%', 'pct').lower() return calc_id, percentage, base_type def create_calculation_formula(percentage: float, base_type: str): """Create a callable that calculates the transfer amount for the given type.""" if base_type == 'fixed_amount': return lambda contract_data: Decimal(str(percentage)) else: return lambda contract_data: Decimal(str(contract_data.get(base_type, 0))) * Decimal(str(percentage)) / Decimal('100') def parse_csv_to_objects(csv_path: str) -> Tuple[Dict[str, Contract], Dict[str, TransferOfEarningsConfiguration], Dict[str, Calculation]]: """Parse the CSV file and create Contract, Calculation, and TransferOfEarningsConfiguration objects.""" contracts: Dict[str, Contract] = {} calculations: Dict[str, Calculation] = {} configurations: Dict[str, TransferOfEarningsConfiguration] = {} # Skip first 2 metadata rows with open(csv_path, 'r', encoding='utf-8') as f: next(f) next(f) reader = csv.DictReader(f) rows = list(reader) transfer_pairs: Dict[str, List[dict]] = {} for row in rows: if row.get('Actionable') != 'Action Required': continue comment = row.get('Comment', '').strip() if not comment or comment == '#N/A': continue group = row.get('Group', '') if group not in transfer_pairs: transfer_pairs[group] = [] transfer_pairs[group].append(row) config_counter = 1 for group, rows_in_group in transfer_pairs.items(): from_row = None to_row = None for row in rows_in_group: transfer_type = row.get('Type', '').strip() if transfer_type == 'from': from_row = row elif transfer_type == 'to': to_row = row if not from_row or not to_row: continue # "to" row = source contract (sending money) # "from" row = destination contract (receiving money) from_contract_id = to_row.get('Contract ID *', '').strip() to_contract_id = from_row.get('Contract ID *', '').strip() input_desc = from_row.get('Input', '').strip() if not input_desc or input_desc == '#N/A': continue calc_id, percentage, base_type = parse_calculation_type(input_desc) if calc_id not in calculations: formula = create_calculation_formula(percentage, base_type) calculations[calc_id] = Calculation( calculation_id=calc_id, formula=formula, description=input_desc ) config_id = f"config_{config_counter:04d}_{group.replace(' ', '_')}" configurations[config_id] = TransferOfEarningsConfiguration( config_id=config_id, to_contract_id=to_contract_id, from_contract_id=from_contract_id, calculation=calculations[calc_id] ) if from_contract_id not in contracts: from_contract_name = to_row.get('Agreement Name', '').strip() from_source = to_row.get('Source', '0').replace(',', '').strip() from_closing_balance = to_row.get('Closing Balance', '0').replace(',', '').strip() try: from_balance = float(from_source) if from_source else 0.0 closing_bal = float(from_closing_balance) if from_closing_balance else 0.0 except ValueError: from_balance = 0.0 closing_bal = 0.0 contracts[from_contract_id] = Contract( contract_id=from_contract_id, name=from_contract_name, projected_balance=closing_bal, ) contracts[from_contract_id].source_value = from_balance if to_contract_id not in contracts: to_contract_name = from_row.get('Agreement Name', '').strip() to_closing_balance = from_row.get('Closing Balance', '0').replace(',', '').strip() try: to_balance = float(to_closing_balance) if to_closing_balance else 0.0 except ValueError: to_balance = 0.0 contracts[to_contract_id] = Contract( contract_id=to_contract_id, name=to_contract_name, projected_balance=to_balance, ) config_counter += 1 return contracts, configurations, calculations def calculate_transfer_amount(config: TransferOfEarningsConfiguration, from_contract: Contract) -> Decimal: """Calculate the transfer amount, using source_value if available.""" base_value = getattr(from_contract, 'source_value', from_contract.projected_balance) contract_data = { 'net_revenue': base_value, 'gross_revenue': base_value, 'closing_balance': from_contract.projected_balance, } return config.apply_transfer(contract_data) def main(): base_dir = os.path.dirname(os.path.abspath(__file__)) csv_path = os.path.join(base_dir, 'MASTER_ Cross Recoupment_Transfer Automated Sheet - Overrides.csv') print("Parsing CSV and creating configuration objects...\n") contracts, configurations, calculations = parse_csv_to_objects(csv_path) print(f"Created {len(contracts)} contracts") print(f"Created {len(calculations)} unique calculations") print(f"Created {len(configurations)} transfer configurations\n") print("=" * 80) print("CALCULATIONS") print("=" * 80) for calc_id, calc in list(calculations.items())[:5]: print(f"{calc}") print(f"... and {len(calculations) - 5} more\n") print("=" * 80) print("TRANSFER CONFIGURATIONS") print("=" * 80) for config_id, config in list(configurations.items())[:5]: print(f"{config}") if config.from_contract_id in contracts: from_contract = contracts[config.from_contract_id] try: transfer_amount = calculate_transfer_amount(config, from_contract) print(f" → Calculated Transfer: {transfer_amount}") except Exception as e: print(f" → Calculation error: {e}") print() print(f"... and {len(configurations) - 5} more\n") print("=" * 80) print("SAMPLE CONTRACTS") print("=" * 80) for contract_id, contract in list(contracts.items())[:5]: print(f"Contract ID: {contract_id}") print(f" {contract}") print() print(f"... and {len(contracts) - 5} more") print("\n" + "=" * 80) print("VERIFICATION: Comparing calculated vs. CSV amounts") print("=" * 80) with open(csv_path, 'r', encoding='utf-8') as f: next(f) next(f) reader = csv.DictReader(f) rows = list(reader) matches = 0 total_checks = 0 checked_count = 0 for row in rows: if checked_count >= 10: break if row.get('Actionable') != 'Action Required': continue transfer_type = row.get('Type', '').strip() if transfer_type != 'from': continue from_contract_id = row.get('Contract', '').strip() to_contract_id = row.get('Contract ID *', '').strip() csv_amount = row.get('Amount', '0').replace(',', '').strip() try: csv_amount_decimal = Decimal(csv_amount) except (ValueError, ArithmeticError): continue matching_config = None for config in configurations.values(): if config.from_contract_id == from_contract_id and config.to_contract_id == to_contract_id: matching_config = config break if matching_config and from_contract_id in contracts: calculated = calculate_transfer_amount(matching_config, contracts[from_contract_id]) total_checks += 1 checked_count += 1 if abs(calculated - csv_amount_decimal) < Decimal('0.01'): matches += 1 status = " MATCH" else: status = "✗ MISMATCH" print(f"{status} | {from_contract_id}→{to_contract_id}: CSV={csv_amount_decimal}, Calculated={calculated}") if total_checks > 0: print(f"\nVerification: {matches}/{total_checks} matches ({100*matches/total_checks:.1f}%)") if __name__ == "__main__": main()