production-evyos-systems-an.../ServicesBank/Finder/Payment/runner.py

854 lines
42 KiB
Python

import arrow
import calendar
import time
from decimal import Decimal
from datetime import datetime, timedelta
from Schemas import BuildDecisionBookPayments, AccountRecords, ApiEnumDropdown, Build, BuildDecisionBook
from time import perf_counter
from sqlalchemy import select, func, distinct, cast, Date, String, literal, desc, and_, or_
from Controllers.Postgres.engine import get_session_factory
#from ServicesApi.Schemas.account.account import AccountRecords
#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)
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 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
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
)
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]
def generate_total_paid_amount_for_spesific_build_part_id(build_parts_id: int, session):
"""
Calculate the total amount paid for a specific build part ID.
Args:
build_parts_id: The build part ID to calculate payments for
session: Database session
Returns:
float: The total amount paid (absolute value)
"""
payment_query = session.query(func.sum(BuildDecisionBookPayments.payment_amount)).filter(
BuildDecisionBookPayments.build_parts_id == build_parts_id,
BuildDecisionBookPayments.account_is_debit == False,
cast(BuildDecisionBookPayments.process_date, Date) >= '2022-01-01'
).scalar()
return payment_query if payment_query is not None else 0
def generate_total_debt_amount_for_spesific_build_part_id(build_parts_id: int, session):
# Use SQLAlchemy's func.sum to calculate the total debts
# For total debt, we want to include ALL debts, both processed and unprocessed
result = session.query(
func.sum(BuildDecisionBookPayments.payment_amount)
).filter(
BuildDecisionBookPayments.build_parts_id == build_parts_id,
BuildDecisionBookPayments.account_is_debit == True,
cast(BuildDecisionBookPayments.process_date, Date) >= '2022-01-01'
).scalar()
# Return 0 if no debts found, otherwise return the absolute value of the sum
return abs(result) if result is not None else 0
def generate_total_amount_that_user_has_in_account(account_record: AccountRecords, session):
# Get total amount that user has in account
result = session.query(
func.sum(AccountRecords.currency_value)
).filter(
AccountRecords.build_parts_id == account_record.build_parts_id,
AccountRecords.currency_value > 0,
cast(AccountRecords.bank_date, Date) >= '2022-01-01'
).scalar()
# Return 0 if no payments found, otherwise return the absolute value of the sum
return abs(result)
def _print_debt_details(debt, session):
"""Helper function to print detailed information about an unpaid debt.
Args:
debt: The BuildDecisionBookPayments object representing the debt
session: Database session
"""
# Get the sum of payments for this debt
payments_sum = session.query(
func.sum(BuildDecisionBookPayments.payment_amount)
).filter(
BuildDecisionBookPayments.ref_id == debt.ref_id,
BuildDecisionBookPayments.account_is_debit == False
).scalar() or 0
# Calculate remaining amount
debit_amount = abs(debt.payment_amount)
remaining = debit_amount - abs(payments_sum)
payment_percentage = (abs(payments_sum) / debit_amount) * 100 if debit_amount > 0 else 0
# Format the date for display
date_str = debt.process_date.strftime('%Y-%m-%d') if debt.process_date else 'Unknown date'
def analyze_payment_function():
session_factory = get_session_factory()
session = session_factory()
# Set session for all models
AccountRecords.set_session(session)
BuildDecisionBookPayments.set_session(session)
order_pay = get_enums_from_database()
# Get distinct build_parts_id values from account records with positive currency_value
# This avoids redundant processing of the same build_parts_id
distinct_build_parts = session.query(
distinct(AccountRecords.build_parts_id)
).filter(
AccountRecords.build_parts_id.isnot(None),
AccountRecords.currency_value > 0,
AccountRecords.bank_date >= '2022-01-01'
).order_by(AccountRecords.build_parts_id.desc()).all()
start_time = time.time()
for build_part_id_tuple in distinct_build_parts:
build_part_id = build_part_id_tuple[0] # Extract the ID from the tuple
process_date = datetime.now()
last_date_of_process_date = datetime(process_date.year, process_date.month, 1) - timedelta(days=1)
first_date_of_process_date = datetime(process_date.year, process_date.month, 1)
print(f"\n{'=' * 50}")
print(f"ACCOUNT ANALYSIS FOR BUILD PART ID: {build_part_id}")
print(f"{'=' * 50}")
# Calculate total paid amount for this build_part_id
total_amount_paid = generate_total_paid_amount_for_spesific_build_part_id(build_part_id, session)
# Calculate total debt amount for this build_part_id
total_debt_amount = generate_total_debt_amount_for_spesific_build_part_id(build_part_id, session)
# Get total amount in account for this build_part_id
account_record = AccountRecords()
account_record.build_parts_id = build_part_id
total_amount_in_account = generate_total_amount_that_user_has_in_account(account_record, session)
# Calculate remaining amount to be paid
amount_need_to_paid = total_debt_amount - total_amount_paid
total_amount_that_user_need_to_transfer = abs(amount_need_to_paid) - abs(total_amount_in_account)
# Print summary with clear descriptions
print(f"PAYMENT SUMMARY:")
print(f" • Total debt amount: {total_debt_amount:,.2f} TL")
print(f" • Amount already paid: {total_amount_paid:,.2f} TL")
print(f" • Remaining debt to be collected: {amount_need_to_paid:,.2f} TL")
print(f" • Current account balance: {total_amount_in_account:,.2f} TL")
if total_amount_that_user_need_to_transfer > 0:
print(f" • Additional funds needed: {total_amount_that_user_need_to_transfer:,.2f} TL")
elif amount_need_to_paid <= 0:
print(f" • Account is fully paid with no outstanding debt")
else:
print(f" • Sufficient funds available to close all debt")
# Show debt coverage percentage
if total_debt_amount > 0:
# Calculate current coverage (already paid)
current_coverage_percentage = (total_amount_paid / total_debt_amount) * 100
# Calculate potential coverage (including available funds)
potential_coverage = min(100, ((total_amount_paid + total_amount_in_account) / total_debt_amount) * 100)
# Display both percentages
print(f" • Current debt coverage: {current_coverage_percentage:.2f}%")
print(f" • Potential debt coverage with available funds: {potential_coverage:.2f}%")
# Analyze unpaid debts for each payment type
print("\nUNPAID DEBTS ANALYSIS BY PAYMENT TYPE:")
for payment_type in order_pay:
# Get unpaid debts for current month
date_query_current = (
BuildDecisionBookPayments.process_date >= first_date_of_process_date,
BuildDecisionBookPayments.process_date <= process_date
)
date_query_previous = (
BuildDecisionBookPayments.process_date < first_date_of_process_date,
)
current_unpaid_debts = get_unpaid_debts(build_parts_id=build_part_id, session=session, debit_type=payment_type, date_query=date_query_current)
# Get unpaid debts from previous months
previous_unpaid_debts = get_unpaid_debts(build_parts_id=build_part_id, session=session, debit_type=payment_type, date_query=date_query_previous)
# Calculate totals
current_total = sum(abs(debt[2]) for debt in current_unpaid_debts)
previous_total = sum(abs(debt[2]) for debt in previous_unpaid_debts)
grand_total = current_total + previous_total
# Print summary for this payment type
if current_unpaid_debts or previous_unpaid_debts:
print(f"{payment_type.key}: Total unpaid: {grand_total:,.2f} TL")
# Current month details
if current_unpaid_debts:
print(f" - Current month: {len(current_unpaid_debts)} debts, {current_total:,.2f} TL")
# Show details of each unpaid debt if there aren't too many
# if len(current_unpaid_debts) <= 3:
# for debt in current_unpaid_debts:
# _print_debt_details(debt, session)
# Previous months details
if previous_unpaid_debts:
print(f" - Previous months: {len(previous_unpaid_debts)} debts, {previous_total:,.2f} TL")
# Show details of each unpaid debt if there aren't too many
# if len(previous_unpaid_debts) <= 3:
# for debt in previous_unpaid_debts:
# _print_debt_details(debt, session)
else:
print(f"{payment_type.key}: All debts paid")
print(f"{'=' * 50}\n")
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)
# 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,
account_is_debit=False,
)
# Save the new payment record
saved_row = new_row.save()
session.commit()
session.refresh(saved_row)
session.flush()
return saved_row
def update_account_remainder_if_spent(account_record, ref_id: str, 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)
BuildDecisionBookPayments.set_session(session)
session.commit()
sum_of_paid = session.query(func.sum(func.abs(BuildDecisionBookPayments.payment_amount))).filter(
BuildDecisionBookPayments.account_records_id == account_record.id,
BuildDecisionBookPayments.account_is_debit == False
).scalar()
if not sum_of_paid:
return False
debit_row = BuildDecisionBookPayments.query.filter_by(ref_id=ref_id).first()
account_record_to_update = AccountRecords.query.filter_by(id=account_record.id).first()
account_record_to_update.remainder_balance = (-1 * abs(sum_of_paid)) or 0
account_record_to_update.save()
session.commit()
session.refresh(account_record_to_update)
# Get the current remainder balance
if abs(sum_of_paid) == abs(account_record_to_update.currency_value):
return True
return False
def update_all_spent_accounts(session):
"""Update remainder_balance for all accounts with payments.
This function finds account records in BuildDecisionBookPayments and updates
their remainder_balance based on the sum of payments made, regardless of whether
all funds have been spent or not.
Args:
session: Database session
"""
with AccountRecords.new_session() as session:
# Set sessions for models
AccountRecords.set_session(session)
BuildDecisionBookPayments.set_session(session)
# Get distinct account_records_id values from BuildDecisionBookPayments
distinct_account_ids = session.query(BuildDecisionBookPayments.account_records_id).filter(
BuildDecisionBookPayments.account_records_id.isnot(None),
BuildDecisionBookPayments.account_is_debit == False # Credit entries (payments)
).distinct().all()
updated_count = 0
for account_id_tuple in distinct_account_ids:
account_id = account_id_tuple[0]
# Get the account record
account = AccountRecords.query.filter_by(id=account_id).first()
if not account or not account.build_parts_id or account.currency_value <= 0:
continue
# Calculate the sum of payments made using this account
# Note: payment_amount is negative for credit entries, so we need to use abs() to get the positive amount
payment_query = session.query(func.sum(func.abs(BuildDecisionBookPayments.payment_amount))).filter(
BuildDecisionBookPayments.account_records_id == account_id,
BuildDecisionBookPayments.account_is_debit == False # Credit entries (payments)
)
payment_sum = payment_query.scalar() or 0
# Update remainder_balance for ALL accounts, regardless of payment_sum value
threshold = Decimal('0.01')
fully_spent = abs(payment_sum) >= abs(account.currency_value) - threshold
status = "All funds spent" if fully_spent else "Partial payment"
# Store the positive value in remainder_balance
account.remainder_balance = -1 * abs(payment_sum)
account.save()
updated_count += 1
session.commit()
print(f"\nTotal accounts updated: {updated_count}")
# def find_amount_to_pay_by_ref_id(ref_id, session):
# """Calculate the remaining amount to pay for a specific debt reference ID.
# Args:
# ref_id: The reference ID of the debt (this is the uu_id of the debt record)
# session: Database session
# Returns:
# float: The remaining amount to pay
# """
# # Get the original debt amount - the debt is identified by its uu_id which is passed as ref_id
# debit = BuildDecisionBookPayments.query.filter(
# BuildDecisionBookPayments.uu_id == ref_id,
# BuildDecisionBookPayments.account_is_debit == True
# ).first()
# if not debit:
# return 0 # No debit found, nothing to pay
# debit_amount = abs(debit.payment_amount) # Ensure positive value for debit amount
# # Get the sum of payments already made for this debt
# # The ref_id in credit records points to the uu_id of the original debit
# # Note: payment_amount is negative for credit entries, so we use abs() to get positive values
# credit_amount = session.query(
# func.sum(func.abs(BuildDecisionBookPayments.payment_amount))
# ).filter(
# BuildDecisionBookPayments.ref_id == str(ref_id),
# BuildDecisionBookPayments.account_is_debit == False
# ).scalar() or 0
# # Calculate remaining amount to pay
# remaining = abs(debit_amount) - abs(credit_amount)
# # Ensure we don't return negative values
# if remaining < 0:
# return 0
# return remaining
def get_unpaid_debts_via_build_parts(build_parts_id: int, session, debit_type, date_query: tuple):
"""Find BuildDecisionBookPayments entries where the debt has NOT been fully paid for a specific debit type.
This function identifies payments where the sum of payments is less than the debit amount,
meaning the debt has not been fully closed.
Args:
build_parts_id: The build part ID to check
session: Database session
debit_type: The specific debit type to check
date_query: Tuple of date filters to apply
Returns:
list: List of unpaid debt entries (full BuildDecisionBookPayments objects)
query:
SELECT
bpf.ref_id,
bpf.process_date,
ABS(COALESCE(SUM(bpf.payment_amount), 0)) AS total_payments
FROM public.build_decision_book_payments AS bpf
GROUP BY
bpf.ref_id,
bpf.process_date
HAVING ABS(COALESCE(SUM(bpf.payment_amount), 0)) > 0
order by bpf.process_date
"""
# Create a subquery for the payment sums without executing it separately
payment_sums_subquery = select(
BuildDecisionBookPayments.ref_id,
BuildDecisionBookPayments.process_date,
func.abs(func.coalesce(func.sum(BuildDecisionBookPayments.payment_amount), 0)).label("total_payments")
).filter(
BuildDecisionBookPayments.build_parts_id == build_parts_id,
BuildDecisionBookPayments.payment_types_id == debit_type.id,
*date_query
).group_by(
BuildDecisionBookPayments.ref_id, BuildDecisionBookPayments.process_date
).having(
func.abs(func.coalesce(func.sum(BuildDecisionBookPayments.payment_amount), 0)) > 0
).order_by(BuildDecisionBookPayments.process_date.desc())
# Use the subquery directly in the main query
payment_sums = session.execute(payment_sums_subquery).all()
payment_sums_list = []
for item in payment_sums:
payment_sums_list.append({"ref_id": item[0], "process_date": item[1], "total_payments": item[2]})
return payment_sums
def get_unpaid_debts(session, debit_type, date_query: tuple):
"""Find BuildDecisionBookPayments entries where the debt has NOT been fully paid for a specific debit type.
This function identifies payments where the sum of payments is less than the debit amount,
meaning the debt has not been fully closed.
Args:
build_parts_id: The build part ID to check
session: Database session
debit_type: The specific debit type to check
date_query: Tuple of date filters to apply
Returns:
list: List of unpaid debt entries (full BuildDecisionBookPayments objects)
query:
SELECT
bpf.ref_id,
bpf.process_date,
ABS(COALESCE(SUM(bpf.payment_amount), 0)) AS total_payments
FROM public.build_decision_book_payments AS bpf
GROUP BY
bpf.ref_id,
bpf.process_date
HAVING ABS(COALESCE(SUM(bpf.payment_amount), 0)) > 0
order by bpf.process_date
"""
# Create a subquery for the payment sums without executing it separately
do_payments_set = lambda ref, date, total: {"ref_id": ref, "process_date": date, "total_payments": total}
payment_sums_subquery = select(BuildDecisionBookPayments.ref_id, BuildDecisionBookPayments.process_date,
func.abs(func.coalesce(func.sum(BuildDecisionBookPayments.payment_amount), 0)).label("total_payments")
).filter(BuildDecisionBookPayments.payment_types_id == debit_type.id, *date_query
).group_by(BuildDecisionBookPayments.ref_id, BuildDecisionBookPayments.process_date
).having(func.abs(func.coalesce(func.sum(BuildDecisionBookPayments.payment_amount), 0)) > 0
).order_by(BuildDecisionBookPayments.process_date.asc())
payment_sums = session.execute(payment_sums_subquery).all()
return [do_payments_set(item[0], item[1], item[2]) for item in payment_sums]
def do_payments_of_this_month(build_id: int = 1):
"""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.
"""
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
target_build = Build.query.filter(Build.id == build_id).first()
if not target_build:
raise ValueError(f"Build with id {build_id} not found")
now = datetime.now()
decision_book = BuildDecisionBook.query.filter(
BuildDecisionBook.build_id == build_id, cast(BuildDecisionBook.expiry_starts, Date) <= now.date(),
cast(BuildDecisionBook.expiry_ends, Date) >= now.date(), BuildDecisionBook.decision_type == "RBM"
).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(now)
last_date_of_process_date = find_last_day_of_month(now)
# 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()
)
update_all_spent_accounts(session)
for payment_type in payment_type_list:
unpaid_debts = get_unpaid_debts(session, payment_type, date_query_tuple)
for unpaid_debt in unpaid_debts:
amount_to_pay = unpaid_debt["total_payments"]
ref_id = unpaid_debt["ref_id"]
process_date = unpaid_debt["process_date"]
debit_row = BuildDecisionBookPayments.query.filter(BuildDecisionBookPayments.uu_id == ref_id).first()
build_parts_id = debit_row.build_parts_id
money_to_pay_rows = AccountRecords.query.filter(
AccountRecords.build_parts_id == build_parts_id, AccountRecords.currency_value > 0, AccountRecords.currency_value > func.abs(AccountRecords.remainder_balance),
cast(AccountRecords.bank_date, Date) >= first_date_of_process_date.date(), cast(AccountRecords.bank_date, Date) <= last_date_of_process_date.date(),
).order_by(AccountRecords.bank_date.asc()).all()
for money_to_pay_row in money_to_pay_rows:
update_account_remainder_if_spent(account_record=money_to_pay_row, ref_id=debit_row.ref_id, session=session)
money_to_pay_row = AccountRecords.query.filter(AccountRecords.id == money_to_pay_row.id).first()
available_money = abs(money_to_pay_row.currency_value) - abs(money_to_pay_row.remainder_balance)
if available_money > amount_to_pay:
print('NOT All money is spent amount_to_pay:', amount_to_pay, 'debit_row payment_amount:', debit_row.payment_amount)
close_payment_book(debit_row, money_to_pay_row, amount_to_pay, session)
total_amount_paid += amount_to_pay
paid_count += 1
payments_made += 1
update_account_remainder_if_spent(account_record=money_to_pay_row, ref_id=debit_row.ref_id, session=session)
break
elif available_money <= amount_to_pay:
print('All money is spent amount_to_pay:', amount_to_pay, 'debit_row payment_amount:', debit_row.payment_amount)
close_payment_book(debit_row, money_to_pay_row, available_money, session)
total_amount_paid += available_money
paid_count += 1
payments_made += 1
update_account_remainder_if_spent(account_record=money_to_pay_row, ref_id=debit_row.ref_id, session=session)
continue
else:
print(f"Something else happened available_money: {available_money}, amount_to_pay: {amount_to_pay}")
update_all_spent_accounts(session)
print('payments_made', payments_made)
print('total_amount_paid', total_amount_paid)
print('paid_count', paid_count)
def do_payments_of_previos_months(build_id: int = 1):
"""Process payments for previous months' unpaid debts.
This function retrieves account records with available funds and processes
payments for previous months' unpaid debts in order of payment type priority.
"""
"""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.
"""
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
target_build = Build.query.filter(Build.id == build_id).first()
if not target_build:
raise ValueError(f"Build with id {build_id} not found")
now = datetime.now()
decision_book = BuildDecisionBook.query.filter(
BuildDecisionBook.build_id == build_id,
cast(BuildDecisionBook.expiry_starts, Date) <= now.date(),
cast(BuildDecisionBook.expiry_ends, Date) >= now.date(),
BuildDecisionBook.decision_type == "RBM"
).first()
if not decision_book:
raise ValueError(f"Decision book not found for build with id {build_id}")
early_date = datetime(now.year - 1, now.month, now.day)
early_decision_book = BuildDecisionBook.query.filter(
BuildDecisionBook.build_id == build_id,
cast(BuildDecisionBook.expiry_starts, Date) <= early_date.date(),
cast(BuildDecisionBook.expiry_ends, Date) >= early_date.date(),
BuildDecisionBook.decision_type == "RBM"
).first()
period_date_start = decision_book.expiry_starts
period_date_end = decision_book.expiry_ends
period_id = decision_book.id
early_period_date_start = early_decision_book.expiry_starts
early_period_date_end = early_decision_book.expiry_ends
early_period_id = early_decision_book.id
first_date_of_process_date = arrow.get(period_date_start).datetime
last_date_of_process_date = now
# 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()
)
update_all_spent_accounts(session)
for payment_type in payment_type_list:
unpaid_debts = get_unpaid_debts(session, payment_type, date_query_tuple)
print('length unpaid debts: ', len(unpaid_debts))
for unpaid_debt in unpaid_debts:
amount_to_pay = unpaid_debt["total_payments"]
ref_id = unpaid_debt["ref_id"]
process_date = unpaid_debt["process_date"]
debit_row = BuildDecisionBookPayments.query.filter(BuildDecisionBookPayments.uu_id == ref_id).first()
build_parts_id = debit_row.build_parts_id
money_to_pay_rows = AccountRecords.query.filter(
AccountRecords.build_parts_id == build_parts_id, AccountRecords.currency_value > 0, AccountRecords.currency_value > func.abs(AccountRecords.remainder_balance),
cast(AccountRecords.bank_date, Date) >= find_first_day_of_month(process_date).date(), cast(AccountRecords.bank_date, Date) <= find_last_day_of_month(process_date).date(),
).order_by(AccountRecords.bank_date.asc()).all()
if not money_to_pay_rows:
money_to_pay_rows = AccountRecords.query.filter(
AccountRecords.build_parts_id == build_parts_id, AccountRecords.currency_value > 0, AccountRecords.currency_value > func.abs(AccountRecords.remainder_balance),
cast(AccountRecords.bank_date, Date) >= first_date_of_process_date.date(), cast(AccountRecords.bank_date, Date) <= last_date_of_process_date.date(),
).order_by(AccountRecords.bank_date.asc()).all()
for money_to_pay_row in money_to_pay_rows:
update_account_remainder_if_spent(account_record=money_to_pay_row, ref_id=debit_row.ref_id, session=session)
money_to_pay_row = AccountRecords.query.filter(AccountRecords.id == money_to_pay_row.id).first()
available_money = abs(money_to_pay_row.currency_value) - abs(money_to_pay_row.remainder_balance)
if available_money > amount_to_pay:
print('NOT All money is spent amount_to_pay:', amount_to_pay, 'debit_row payment_amount:', debit_row.payment_amount)
close_payment_book(debit_row, money_to_pay_row, amount_to_pay, session)
total_amount_paid += amount_to_pay
paid_count += 1
payments_made += 1
update_account_remainder_if_spent(account_record=money_to_pay_row, ref_id=debit_row.ref_id, session=session)
break
elif available_money <= amount_to_pay:
print('All money is spent amount_to_pay:', available_money, 'debit_row payment_amount:', debit_row.payment_amount)
close_payment_book(debit_row, money_to_pay_row, available_money, session)
total_amount_paid += available_money
paid_count += 1
payments_made += 1
update_account_remainder_if_spent(account_record=money_to_pay_row, ref_id=debit_row.ref_id, session=session)
continue
else:
print(f"Something else happened available_money: {available_money}, amount_to_pay: {amount_to_pay}")
print('This years decision book payments')
print('payments_made', payments_made)
print('total_amount_paid', total_amount_paid)
print('paid_count', paid_count)
payments_made, total_amount_paid, paid_count = 0, 0, 0
update_all_spent_accounts(session)
first_date_of_process_date = arrow.get(early_period_date_start).datetime
last_date_of_process_date = arrow.get(early_period_date_end).datetime
# Early 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()
)
for payment_type in payment_type_list:
unpaid_debts = get_unpaid_debts(session, payment_type, date_query_tuple)
print('length unpaid debts: ', len(unpaid_debts))
for unpaid_debt in unpaid_debts:
amount_to_pay = unpaid_debt["total_payments"]
ref_id = unpaid_debt["ref_id"]
process_date = unpaid_debt["process_date"]
debit_row = BuildDecisionBookPayments.query.filter(BuildDecisionBookPayments.uu_id == ref_id).first()
build_parts_id = debit_row.build_parts_id
first_date_of_process_date = find_first_day_of_month(process_date)
last_date_of_process_date = find_last_day_of_month(process_date)
money_to_pay_rows = AccountRecords.query.filter(
AccountRecords.build_parts_id == build_parts_id, AccountRecords.currency_value > 0, AccountRecords.currency_value > func.abs(AccountRecords.remainder_balance),
cast(AccountRecords.bank_date, Date) >= first_date_of_process_date.date(), cast(AccountRecords.bank_date, Date) <= last_date_of_process_date.date(),
).order_by(AccountRecords.bank_date.asc()).all()
if not money_to_pay_rows:
money_to_pay_rows = AccountRecords.query.filter(
AccountRecords.build_parts_id == build_parts_id, AccountRecords.currency_value > 0, AccountRecords.currency_value > func.abs(AccountRecords.remainder_balance),
cast(AccountRecords.bank_date, Date) >= first_date_of_process_date.date(), cast(AccountRecords.bank_date, Date) <= last_date_of_process_date.date(),
).order_by(AccountRecords.bank_date.asc()).all()
for money_to_pay_row in money_to_pay_rows:
update_account_remainder_if_spent(account_record=money_to_pay_row, ref_id=debit_row.ref_id, session=session)
money_to_pay_row = AccountRecords.query.filter(AccountRecords.id == money_to_pay_row.id).first()
available_money = abs(money_to_pay_row.currency_value) - abs(money_to_pay_row.remainder_balance)
if available_money > amount_to_pay:
print('NOT All money is spent amount_to_pay:', amount_to_pay, 'debit_row payment_amount:', debit_row.payment_amount)
close_payment_book(debit_row, money_to_pay_row, amount_to_pay, session)
total_amount_paid += amount_to_pay
paid_count += 1
payments_made += 1
update_account_remainder_if_spent(account_record=money_to_pay_row, ref_id=debit_row.ref_id, session=session)
break
elif available_money <= amount_to_pay:
print('All money is spent amount_to_pay:', available_money, 'debit_row payment_amount:', debit_row.payment_amount)
close_payment_book(debit_row, money_to_pay_row, available_money, session)
total_amount_paid += available_money
paid_count += 1
payments_made += 1
update_account_remainder_if_spent(account_record=money_to_pay_row, ref_id=debit_row.ref_id, session=session)
continue
else:
print(f"Something else happened available_money: {available_money}, amount_to_pay: {amount_to_pay}")
update_all_spent_accounts(session)
print('Early years decision book payments')
print('payments_made', payments_made)
print('total_amount_paid', total_amount_paid)
print('paid_count', paid_count)
if __name__ == "__main__":
start_time = perf_counter()
print("\n===== PROCESSING PAYMENTS =====\n")
print("Starting payment processing at:", datetime.now())
# Process payments for current month first
print("\n1. Processing current month payments...")
do_payments_of_this_month()
# Process payments for previous months
print("\n2. Processing previous months payments...")
do_payments_of_previos_months()
print("\n===== PAYMENT PROCESSING COMPLETE =====\n")
print("Payment processing completed at:", datetime.now())
# 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")
# # Create a subquery to get the sum of payments for each debit's uu_id
# # For credit entries, ref_id points to the original debit's uu_id
# payment_sums = session.query(
# BuildDecisionBookPayments.ref_id.label('original_debt_id'),
# func.sum(func.abs(BuildDecisionBookPayments.payment_amount)).label('payment_sum')
# ).filter(
# BuildDecisionBookPayments.account_is_debit == False # Credit entries only
# ).group_by(BuildDecisionBookPayments.ref_id).subquery()
# # Main query to find debits with their payment sums
# query = session.query(BuildDecisionBookPayments)
# # Join with payment sums - cast uu_id to string to match ref_id type
# query = query.outerjoin(
# payment_sums,
# func.cast(BuildDecisionBookPayments.uu_id, String) == payment_sums.c.original_debt_id
# )
# # Filter for debits of the specified build part and payment type
# query = query.filter(
# BuildDecisionBookPayments.build_parts_id == build_parts_id,
# BuildDecisionBookPayments.payment_types_id == debit_type.id,
# BuildDecisionBookPayments.account_is_debit == True, # Debit entries only
# )
# # Apply date filters if provided
# if date_query:
# for date_filter in date_query:
# query = query.filter(date_filter)
# # Filter for debits that are not fully paid
# # (payment_sum < debit_amount or payment_sum is NULL)
# query = query.filter(
# or_(
# payment_sums.c.payment_sum.is_(None),
# func.coalesce(payment_sums.c.payment_sum, 0) < func.abs(BuildDecisionBookPayments.payment_amount)
# )
# )
# # Execute the query and return the results
# results = query.order_by(BuildDecisionBookPayments.process_date).all()