import arrow import calendar import time from decimal import Decimal from datetime import datetime, timedelta from Schemas import BuildDecisionBookPayments, AccountRecords, ApiEnumDropdown, Build, BuildDecisionBook, AccountDelayInterest from time import perf_counter from sqlalchemy import select, func, distinct, cast, Date, String, literal, desc, and_, or_, case from Controllers.Postgres.engine import get_session_factory from interest_calculate import hesapla_gecikme_faizi # from ServicesApi.Schemas.account.account import AccountRecords, AccountDelayInterest # from ServicesApi.Schemas.building.decision_book import BuildDecisionBookPayments def find_last_day_of_month(date_value): today = date_value.date() _, last_day = calendar.monthrange(today.year, today.month) return datetime(today.year, today.month, last_day, 23, 59, 59) def find_first_day_of_month(date_value): today = date_value.date() return datetime(today.year, today.month, 1) def add_month_to_date(date_value): month_to_process = date_value.month year_to_process = date_value.year if date_value.month == 12: month_to_process = 1 year_to_process += 1 else: month_to_process += 1 _, last_day = calendar.monthrange(year_to_process, month_to_process) return datetime(year_to_process, month_to_process, 1), datetime(year_to_process, month_to_process, last_day) class BuildDuesTypes: def __init__(self): self.debit: ApiEnumDropdownShallowCopy = None self.add_debit: ApiEnumDropdownShallowCopy = None self.renovation: ApiEnumDropdownShallowCopy = None self.lawyer_expence: ApiEnumDropdownShallowCopy = None self.service_fee: ApiEnumDropdownShallowCopy = None self.information: ApiEnumDropdownShallowCopy = None class ApiEnumDropdownShallowCopy: id: int uuid: str enum_class: str key: str value: str def __init__(self, id: int, uuid: str, enum_class: str, key: str, value: str): self.id = id self.uuid = uuid self.enum_class = enum_class self.key = key self.value = value def retrieve_remainder_balance_set_if_needed(account_record_id: int, session): """Update the remainder_balance of an account after spending money. Args: account_record: The account record to update amount_spent: The amount spent in this transaction session: Database session Returns: bool: True if all money is spent, False otherwise """ AccountRecords.set_session(session) account_record_remainder_balance = session.query(func.sum(func.abs(BuildDecisionBookPayments.payment_amount))).filter( BuildDecisionBookPayments.account_records_id == account_record_id, BuildDecisionBookPayments.account_is_debit == False, BuildDecisionBookPayments.active == True, ).scalar() if not account_record_remainder_balance: account_record_remainder_balance = 0 account_record = AccountRecords.query.filter_by(id=account_record_id).first() account_record.remainder_balance = -1 * abs(account_record_remainder_balance) if account_record_remainder_balance != 0 else 0 account_record.save() return account_record_remainder_balance def retrieve_current_debt_to_pay_from_database(ref_id: int, session): debt_to_pay = session.query(func.sum(BuildDecisionBookPayments.payment_amount)).filter(BuildDecisionBookPayments.ref_id == ref_id, BuildDecisionBookPayments.active == True).scalar() return abs(debt_to_pay) def check_current_debt_to_pay_from_database_is_closed(ref_id: int, session): session.commit() BuildDecisionBookPayments.set_session(session) payment_row_book = BuildDecisionBookPayments.query.filter(BuildDecisionBookPayments.ref_id == str(ref_id), BuildDecisionBookPayments.account_is_debit == True, BuildDecisionBookPayments.active == True).first() debt_paid = session.query(func.sum(func.abs(BuildDecisionBookPayments.payment_amount))).filter( BuildDecisionBookPayments.ref_id == str(ref_id), BuildDecisionBookPayments.account_is_debit == False, BuildDecisionBookPayments.active == True ).scalar() payment_row_book.debt_paid = abs(debt_paid) if debt_paid else 0 # Debt Reminder is how much money is needed to close record payment_row_book.debt_to_pay = abs(payment_row_book.payment_amount) - abs(payment_row_book.debt_paid) # Debt To Pay is how much money is needed to close record payment_row_book.is_closed = payment_row_book.debt_to_pay == 0 payment_row_book.save() def close_payment_book(payment_row_book, account_record, value, session): """Create a credit entry in BuildDecisionBookPayments to close a debt. Args: payment_row_book: The debit entry to be paid account_record: The account record containing the funds value: The amount to pay session: Database session Returns: The newly created payment record """ BuildDecisionBookPayments.set_session(session) account_record_remainder_balance = retrieve_remainder_balance_set_if_needed(account_record_id=account_record.id, session=session) print(f'NOT Updated remainder balance: {account_record_remainder_balance} | Account record id: {account_record.id}') # Create a new credit entry (payment) new_row = BuildDecisionBookPayments.create( ref_id=str(payment_row_book.uu_id), payment_plan_time_periods=payment_row_book.payment_plan_time_periods, period_time=payment_row_book.period_time, currency=payment_row_book.currency, account_records_id=account_record.id, account_records_uu_id=str(account_record.uu_id), build_parts_id=payment_row_book.build_parts_id, build_parts_uu_id=str(payment_row_book.build_parts_uu_id), payment_amount=abs(value), # Negative for credit entries payment_types_id=payment_row_book.payment_types_id, payment_types_uu_id=str(payment_row_book.payment_types_uu_id), process_date_m=payment_row_book.process_date.month, process_date_y=payment_row_book.process_date.year, process_date=payment_row_book.process_date, build_decision_book_item_id=payment_row_book.build_decision_book_item_id if payment_row_book.build_decision_book_item_id else None, build_decision_book_item_uu_id=str(payment_row_book.build_decision_book_item_uu_id) if payment_row_book.build_decision_book_item_uu_id else None, decision_book_project_id=payment_row_book.decision_book_project_id if payment_row_book.decision_book_project_id else None, decision_book_project_uu_id=str(payment_row_book.decision_book_project_uu_id) if payment_row_book.decision_book_project_uu_id else None, build_decision_book_id=payment_row_book.build_decision_book_id if payment_row_book.build_decision_book_id else None, build_decision_book_uu_id=str(payment_row_book.build_decision_book_uu_id) if payment_row_book.build_decision_book_uu_id else None, is_confirmed=True, active=True, account_is_debit=False, ) # Save the new payment record saved_row = new_row.save() session.commit() session.refresh(saved_row) account_record_remainder_balance = retrieve_remainder_balance_set_if_needed(account_record_id=account_record.id, session=session) print(f'Updated remainder balance: {account_record_remainder_balance} | Account record id: {account_record.id}') check_current_debt_to_pay_from_database_is_closed(ref_id=new_row.ref_id, session=session) return account_record_remainder_balance def get_enums_from_database(): build_dues_types = BuildDuesTypes() with ApiEnumDropdown.new_session() as session: ApiEnumDropdown.set_session(session) debit_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-D").first() # Debit add_debit_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-A").first() # Add Debit renovation_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-R").first() # Renovation late_payment_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-L").first() # Lawyer expence service_fee_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-S").first() # Service fee information_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-I").first() # Information # close_last_period_receipt_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-CL").first() # Close Last Period Receipt # open_new_period_receipt_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-OP").first() # Open New Period Receipt build_dues_types.debit = ApiEnumDropdownShallowCopy( debit_enum_shallow.id, str(debit_enum_shallow.uu_id), debit_enum_shallow.enum_class, debit_enum_shallow.key, debit_enum_shallow.value ) build_dues_types.add_debit = ApiEnumDropdownShallowCopy( add_debit_enum_shallow.id, str(add_debit_enum_shallow.uu_id), add_debit_enum_shallow.enum_class, add_debit_enum_shallow.key, add_debit_enum_shallow.value ) build_dues_types.renovation = ApiEnumDropdownShallowCopy( renovation_enum_shallow.id, str(renovation_enum_shallow.uu_id), renovation_enum_shallow.enum_class, renovation_enum_shallow.key, renovation_enum_shallow.value ) build_dues_types.lawyer_expence = ApiEnumDropdownShallowCopy( late_payment_enum_shallow.id, str(late_payment_enum_shallow.uu_id), late_payment_enum_shallow.enum_class, late_payment_enum_shallow.key, late_payment_enum_shallow.value ) build_dues_types.service_fee = ApiEnumDropdownShallowCopy( service_fee_enum_shallow.id, str(service_fee_enum_shallow.uu_id), service_fee_enum_shallow.enum_class, service_fee_enum_shallow.key, service_fee_enum_shallow.value ) build_dues_types.information = ApiEnumDropdownShallowCopy( information_enum_shallow.id, str(information_enum_shallow.uu_id), information_enum_shallow.enum_class, information_enum_shallow.key, information_enum_shallow.value ) # build_dues_types.close_last_period_receipt = ApiEnumDropdownShallowCopy( # close_last_period_receipt_enum_shallow.id, str(close_last_period_receipt_enum_shallow.uu_id), close_last_period_receipt_enum_shallow.enum_class, close_last_period_receipt_enum_shallow.key, close_last_period_receipt_enum_shallow.value # ) # build_dues_types.open_new_period_receipt = ApiEnumDropdownShallowCopy( # open_new_period_receipt_enum_shallow.id, str(open_new_period_receipt_enum_shallow.uu_id), open_new_period_receipt_enum_shallow.enum_class, open_new_period_receipt_enum_shallow.key, open_new_period_receipt_enum_shallow.value # ) return [ build_dues_types.debit, build_dues_types.lawyer_expence, build_dues_types.add_debit, build_dues_types.renovation, build_dues_types.service_fee, build_dues_types.information, # build_dues_types.close_last_period_receipt, # build_dues_types.open_new_period_receipt ] def do_payments(session, build_id: int, work_date: datetime): """Process payments for the current month's unpaid debts. This function retrieves account records with available funds and processes payments for current month's unpaid debts in order of payment type priority. """ # Set session for all models AccountRecords.set_session(session) BuildDecisionBookPayments.set_session(session) Build.set_session(session) BuildDecisionBook.set_session(session) today = datetime.now() # Get payment types in priority order payment_type_list = get_enums_from_database() fund_finished = lambda money_spend, money_in_account: money_spend == money_in_account payments_made, total_amount_paid, paid_count = 0, 0, 0 target_build = Build.query.filter(Build.id == build_id).first() if not target_build: raise ValueError(f"Build with id {build_id} not found") decision_book = BuildDecisionBook.query.filter( BuildDecisionBook.build_id == build_id, cast(BuildDecisionBook.expiry_starts, Date) <= work_date.date(), cast(BuildDecisionBook.expiry_ends, Date) >= work_date.date(), BuildDecisionBook.decision_type == "RBM", BuildDecisionBook.active == True ).first() if not decision_book: raise ValueError(f"Decision book not found for build with id {build_id}") period_date_start = decision_book.expiry_starts period_date_end = decision_book.expiry_ends # period_id = decision_book.id first_date_of_process_date = find_first_day_of_month(datetime(work_date.year, work_date.month, 1)) last_date_of_process_date = find_last_day_of_month(datetime(work_date.year, work_date.month, 1)) if work_date.month == today.month and work_date.year == today.year: last_date_of_process_date = today # Current month date filter date_query_tuple = ( cast(BuildDecisionBookPayments.process_date, Date) >= first_date_of_process_date.date(), cast(BuildDecisionBookPayments.process_date, Date) <= last_date_of_process_date.date() ) date_query_account_tuple = ( cast(AccountRecords.bank_date, Date) >= first_date_of_process_date.date(), cast(AccountRecords.bank_date, Date) <= last_date_of_process_date.date() ) for payment_type in payment_type_list: query_of_payments = BuildDecisionBookPayments.query.filter(*date_query_tuple) query_of_payments = query_of_payments.filter( BuildDecisionBookPayments.payment_types_id == payment_type.id, BuildDecisionBookPayments.account_is_debit == True, BuildDecisionBookPayments.is_closed == False, BuildDecisionBookPayments.active == True, ).order_by(BuildDecisionBookPayments.process_date.asc(), BuildDecisionBookPayments.debt_to_pay.desc()).all() # priority_uuids = [payment_type.uuid, *[pt.uuid for pt in payment_type_list if pt.uuid != payment_type.uuid]] # case_order = case(*[(AccountRecords.payment_result_type_uu_id == uuid, index) for index, uuid in enumerate(priority_uuids)], else_=len(priority_uuids)) for payment_row in query_of_payments: money_to_pay_rows = AccountRecords.query.filter( AccountRecords.build_parts_id == payment_row.build_parts_id, AccountRecords.payment_result_type == payment_type.id, AccountRecords.active == True, AccountRecords.currency_value > 0, AccountRecords.currency_value > func.abs(AccountRecords.remainder_balance), *date_query_account_tuple, ).order_by(AccountRecords.bank_date.asc(), AccountRecords.remainder_balance.desc()).all() for money_to_pay_row in money_to_pay_rows: # Get remainder balance from database regardless trust over AccountRecords row from first query remainder_balance_from_database = retrieve_remainder_balance_set_if_needed(money_to_pay_row.id, session) available_funds = abs(money_to_pay_row.currency_value) - abs(remainder_balance_from_database) # BuildDecisionBookPayments must be refreshed to check is there still money to be paid for this row debt_to_pay = retrieve_current_debt_to_pay_from_database(ref_id=payment_row.ref_id, session=session) if debt_to_pay == 0: break # For session caused errors double check the database FOR multi process actions if abs(available_funds) > abs(debt_to_pay): payments_made += 1 total_amount_paid += debt_to_pay paid_count += 1 close_payment_book(payment_row, money_to_pay_row, abs(debt_to_pay), session) break # More fund to spend so go to next BuildDecisionBookPayments else: payments_made += 1 total_amount_paid += available_funds paid_count += 1 close_payment_book(payment_row, money_to_pay_row, abs(available_funds), session) continue # Fund has finished so go to next AccountRecords print('payments_made', payments_made) print('total_amount_paid', total_amount_paid) print('paid_count', paid_count) session.close() session_factory.remove() # Clean up the session from the registry def calculate_interest_from_date_paid_to_date_debt_to_pay(session, payment_value, payment, record): """Calculate interest from date paid to date debt to pay.""" AccountDelayInterest.set_session(session) today = datetime.now() calculated_interest = hesapla_gecikme_faizi(borc=payment_value, vade_tarihi=payment.process_date, islem_tarihi=today, faiz_turu="gecikme") if not calculated_interest.toplam > 0: return None print('interest', dict( debt_to_pay=payment_value, payment_process_date=str(payment.process_date.date()), record_bank_date=str(record.bank_date.date()), available_money=abs(record.currency_value) - abs(record.remainder_balance), account_records_id=record.id, build_decision_book_payment_id=payment.id, new_build_decision_book_payment_id=payment.build_decision_book_id, interest_rate=calculated_interest.gunluk_oran, delay_day=calculated_interest.gecikme_gunu, daily_rate=calculated_interest.gunluk_oran, interest=calculated_interest.faiz, bsmv=calculated_interest.bsmv, kkdf=calculated_interest.kkdf, total=calculated_interest.toplam, )) # new_row_account_delay_interest = AccountDelayInterest.create( # account_records_id=record.id, account_records_uu_id=str(record.uu_id), build_decision_book_payment_id=payment.id, build_decision_book_payment_uu_id=str(payment.uu_id), # new_build_decision_book_payment_id=payment.build_decision_book_id, new_build_decision_book_payment_uu_id=str(payment.build_decision_book_uu_id), # interest_turn="gecikme", interest_rate=calculated_interest.gunluk_oran, delay_day=calculated_interest.gecikme_gunu, daily_rate=calculated_interest.gunluk_oran, # interest=calculated_interest.faiz, bsmv=calculated_interest.bsmv, kkdf=calculated_interest.kkdf, total=calculated_interest.toplam, # ) # new_row_account_delay_interest.save() # session.commit() # session.refresh(new_row_account_delay_interest) # return new_row_account_delay_interest def do_payments_of_overdue_payments(session, build_id: int): """Process payments for the previous month's unpaid debts. This function retrieves account records with available funds and processes payments for previous month's unpaid debts in order of payment type priority. """ session_factory = get_session_factory() session = session_factory() # Set session for all models AccountRecords.set_session(session) BuildDecisionBookPayments.set_session(session) Build.set_session(session) BuildDecisionBook.set_session(session) # Get payment types in priority order payment_type_list = get_enums_from_database() fund_finished = lambda money_spend, money_in_account: money_spend == money_in_account payments_made, total_amount_paid, paid_count = 0, 0, 0 # first_date_of_process_date = find_first_day_of_month(datetime(period_date_start.year, period_date_start.month, 1)) # last_date_of_process_date = find_first_day_of_month(datetime(now.year, now.month - 1, 1)) # print('first_date_of_process_date', first_date_of_process_date) # print('last_date_of_process_date', last_date_of_process_date) # # Current month date filter # date_query_tuple = ( # cast(BuildDecisionBookPayments.process_date, Date) >= first_date_of_process_date.date(), cast(BuildDecisionBookPayments.process_date, Date) <= last_date_of_process_date.date() # ) priority_uuids = [pt.uuid for pt in payment_type_list] case_order = case(*[(AccountRecords.payment_result_type_uu_id == str(uuid), index) for index, uuid in enumerate(priority_uuids)], else_=len(priority_uuids)) case_order_payment = case(*[(BuildDecisionBookPayments.payment_types_uu_id == str(uuid), index) for index, uuid in enumerate(priority_uuids)], else_=len(priority_uuids)) # for payment_type in payment_type_list: query_of_payments = BuildDecisionBookPayments.query.filter( BuildDecisionBookPayments.account_is_debit == True, BuildDecisionBookPayments.active == True, cast(BuildDecisionBookPayments.process_date, Date) < find_first_day_of_month(today) ).order_by( # Order by process_date in ascending order (oldest first) cast(BuildDecisionBookPayments.process_date, Date).asc(), BuildDecisionBookPayments.payment_amount.desc(), case_order_payment.asc(), ).all() for payment_row in query_of_payments: # Get the payment_row's process_date to find closest bank_date in AccountRecords # First get all eligible records money_to_pay_rows = AccountRecords.query.filter( AccountRecords.build_parts_id == payment_row.build_parts_id, # AccountRecords.payment_result_type == payment_type.id, AccountRecords.active == True, AccountRecords.currency_value > 0, func.abs(AccountRecords.currency_value) > func.abs(AccountRecords.remainder_balance), # Filter for bank_dates that are valid for this payment # cast(AccountRecords.bank_date, Date) <= arrow.get(payment_row.process_date).to("Europe/Istanbul").date() ).order_by( # Order by bank_date in ascending order (oldest first) cast(AccountRecords.bank_date, Date).asc(), case_order.asc(), ).all() # Sort the results in Python by the absolute difference between dates if money_to_pay_rows: payment_date = payment_row.process_date.date() if hasattr(payment_row.process_date, 'date') else payment_row.process_date money_to_pay_rows.sort(key=lambda x: abs((x.bank_date.date() if hasattr(x.bank_date, 'date') else x.bank_date) - payment_date)) for money_to_pay_row in money_to_pay_rows: # Get remainder balance from database regardless trust over AccountRecords row from first query remainder_balance_from_database = retrieve_remainder_balance_set_if_needed(money_to_pay_row.id, session) available_funds = abs(money_to_pay_row.currency_value) - abs(remainder_balance_from_database) # BuildDecisionBookPayments must be refreshed to check is there still money to be paid for this row debt_to_pay = retrieve_current_debt_to_pay_from_database(ref_id=payment_row.ref_id, session=session) if debt_to_pay == 0: break # For session caused errors double check the database FOR multi process actions if abs(available_funds) > abs(debt_to_pay): print('More Money than debt to pay ------------------------------------------------------------------------------------------------------------------------') payments_made += 1 total_amount_paid += debt_to_pay paid_count += 1 close_payment_book(payment_row, money_to_pay_row, abs(debt_to_pay), session) calculate_interest_from_date_paid_to_date_debt_to_pay(session=session, payment_value=abs(debt_to_pay), payment=payment_row, record=money_to_pay_row) break # More fund to spend so go to next BuildDecisionBookPayments else: print('All Money is spent ---------------------------------------------------------------------------------------------------------------------------------') payments_made += 1 total_amount_paid += available_funds paid_count += 1 close_payment_book(payment_row, money_to_pay_row, abs(available_funds), session) calculate_interest_from_date_paid_to_date_debt_to_pay(session=session, payment_value=abs(available_funds), payment=payment_row, record=money_to_pay_row) continue # Fund has finished so go to next AccountRecords print('payments_made', payments_made) print('total_amount_paid', total_amount_paid) print('paid_count', paid_count) def do_regular_monthly_payers_payment_function(session, build_id: int, period_count: int = 1, skip_count: int = 0): today = datetime.now() Build.set_session(session) BuildDecisionBook.set_session(session) target_build = Build.query.filter(Build.id == build_id).first() if not target_build: raise ValueError(f"Build with id {build_id} not found") decision_books = BuildDecisionBook.query.filter( BuildDecisionBook.build_id == build_id, cast(BuildDecisionBook.expiry_starts, Date) <= today.date(), BuildDecisionBook.active == True, ).order_by(BuildDecisionBook.expiry_starts.desc()).limit(period_count + skip_count).offset(skip_count).all() if not decision_books: raise ValueError(f"Decision book not found for build with id {build_id}") if skip_count > 0: decision_books = decision_books[skip_count:] first_decision_book = decision_books[-1] work_date = find_first_day_of_month(first_decision_book.expiry_starts) while work_date <= today: do_payments(session=session, build_id=build_id, work_date=work_date) work_date, _ = add_month_to_date(work_date) return True if __name__ == "__main__": today, build_id, start_time = datetime.now(), 1, perf_counter() session_factory = get_session_factory() session = session_factory() print("\n===== PROCESSING PAYMENTS =====\n") print("Starting payment processing at:", str(today)) # Process payments for current month first print("\n2. Processing current month payments...") do_payments(session=session, build_id=build_id, work_date=today) # Do regular monthly payers payments print("\n1. Processing regular monthly payers payments...") do_regular_monthly_payers_payment_function(session=session, build_id=build_id, period_count=4, skip_count=0) # Commit and flush session to prevent session caused errors session.commit() session.flush() # Process payments for previous months print("\n2. Processing previous months payments...") total_amount_of_available_funds = session.query(func.sum(AccountRecords.currency_value)).filter( AccountRecords.currency_value > 0, func.abs(AccountRecords.currency_value) > func.abs(AccountRecords.remainder_balance), AccountRecords.build_parts_id.isnot(None), AccountRecords.active == True ).scalar() print(f"Total amount of available funds: {total_amount_of_available_funds:,.2f}") total_amount_of_debt_to_pay = session.query(func.sum(BuildDecisionBookPayments.debt_to_pay)).filter( BuildDecisionBookPayments.debt_to_pay > 0, BuildDecisionBookPayments.account_is_debit == True, BuildDecisionBookPayments.active == True ).scalar() print(f"Total amount of debt to pay: {total_amount_of_debt_to_pay:,.2f}") if total_amount_of_available_funds - total_amount_of_debt_to_pay > 0: do_payments_of_overdue_payments(session=session, build_id=build_id) print("\n===== PAYMENT PROCESSING COMPLETE =====\n") # Analyze the payment situation after processing payments print("\n===== ANALYZING PAYMENT SITUATION AFTER PROCESSING =====\n") # analyze_payment_function() end_time = perf_counter() print(f"\n{end_time - start_time:.3f} : seconds")