From 10118a9bbbcb0ae04f5df9032832dfab88b1c536 Mon Sep 17 00:00:00 2001 From: SamuelNittala Date: Tue, 13 Jul 2021 19:24:25 +0530 Subject: [PATCH 01/15] Reset Loan Schedule --- src/rush/loan_schedule/loan_schedule.py | 93 +++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 5 deletions(-) diff --git a/src/rush/loan_schedule/loan_schedule.py b/src/rush/loan_schedule/loan_schedule.py index 17befb71..3bb483f3 100644 --- a/src/rush/loan_schedule/loan_schedule.py +++ b/src/rush/loan_schedule/loan_schedule.py @@ -9,11 +9,9 @@ func, ) from sqlalchemy.orm import Session - -from rush.card.base_card import ( - BaseBill, - BaseLoan, -) +from typing import List +from rush.card import get_user_loan +from rush.card.base_card import BaseBill, BaseLoan, Loan from rush.loan_schedule.moratorium import add_moratorium_emis from rush.models import ( LedgerTriggerEvent, @@ -21,6 +19,8 @@ LoanSchedule, MoratoriumInterest, PaymentMapping, + PaymentRequestData, + PaymentSplit, ) @@ -334,3 +334,86 @@ def readjust_future_payment(user_loan: BaseLoan, date_to_check_after: date): emi_id=emi_id, amount_settled=amount_slid, ) + +def reset_loan_schedule(loan_id: Loan.id, session: Session) -> None: + def reset_bill_emis(user_loan: Loan, session: Session) -> None: + bill = user_loan.get_latest_bill() + bill_emis = ( + session.query(LoanSchedule) + .filter(LoanSchedule.loan_id == user_loan.loan_id, LoanSchedule.bill_id == bill.id) + .order_by(LoanSchedule.emi_number) + .all() + ) + instalment = bill.get_instalment_amount() + opening_principal = bill.table.principal + interest_due = round(bill.get_interest_to_charge(), 2) + principal_due = round(instalment - interest_due, 2) + for bill_emi in bill_emis: + bill_emi.interest_due = interest_due + bill_emi.principal_due = principal_due + bill_emi.total_closing_balance = (round(opening_principal, 2),) + opening_principal -= principal_due + + def reset_emis(user_loan: Loan) -> None: + emis = user_loan.get_loan_schedule() + for emi in emis: + emi.last_payment_date = None + emi.payment_received = 0 + emi.dpd = -999 + emi.payment_status = "UnPaid" + + def get_payment_events(user_loan: Loan, session: Session) -> List[LedgerTriggerEvent]: + reset_joining_fee_request_id = ( + session.query(PaymentRequestData.payment_request_id) + .filter( + PaymentRequestData.row_status == "active", + PaymentRequestData.payment_request_status == "Paid", + PaymentRequestData.type == "reset_joining_fees", + PaymentRequestData.user_id == user_loan.user_id, + ) + .scalar() + ) + payment_events = ( + session.query(LedgerTriggerEvent) + .filter( + LedgerTriggerEvent.loan_id == user_loan.loan_id, + LedgerTriggerEvent.name == "payment_received", + LedgerTriggerEvent.extra_details["payment_request_id"].astext + != reset_joining_fee_request_id, + ) + .order_by(LedgerTriggerEvent.post_date) + .all() + ) + return payment_events + + def make_emi_payment_mappings_inactive(user_loan: Loan, session: Session) -> None: + emis = user_loan.get_loan_schedule() + emi_ids = [emi.id for emi in emis] + ( + session.query(PaymentMapping) + .filter(PaymentMapping.emi_id.in_(emi_ids), PaymentMapping.row_status == "active") + .update({PaymentMapping.row_status: "inactive"}, synchronize_session=False) + ) + + user_loan = get_user_loan(session=Session, loan_id=loan_id) + + reset_bill_emis(user_loan=user_loan) + reset_emis(user_loan=user_loan) + group_bills(user_loan) + make_emi_payment_mappings_inactive(user_loan) + + payment_events = get_payment_events(user_loan) + + for payment_event in payment_events: + amount_to_slide = ( + session.query(func.sum(PaymentSplit.amount_settled)) + .filter( + PaymentSplit.payment_request_id == payment_event.extra_details["payment_request_id"], + PaymentSplit.component.in_(("principal", "interest", "unbilled")), + ) + .scalar() + ) + slide_payment_to_emis(user_loan, payment_event, amount_to_slide) + if user_loan.can_close_loan(as_of_event_id=payment_event.id): + close_loan(user_loan, payment_event.post_date) + break From 0791b8700ddcced5deccf0798de49dc5ea75a21d Mon Sep 17 00:00:00 2001 From: SamuelNittala Date: Tue, 13 Jul 2021 20:46:18 +0530 Subject: [PATCH 02/15] Import error fix --- src/rush/loan_schedule/loan_schedule.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/rush/loan_schedule/loan_schedule.py b/src/rush/loan_schedule/loan_schedule.py index 3874172d..ee1fbc77 100644 --- a/src/rush/loan_schedule/loan_schedule.py +++ b/src/rush/loan_schedule/loan_schedule.py @@ -19,7 +19,7 @@ LoanSchedule, MoratoriumInterest, PaymentMapping, - PaymentRequestData, + PaymentRequestsData, PaymentSplit, ) @@ -365,12 +365,12 @@ def reset_emis(user_loan: Loan) -> None: def get_payment_events(user_loan: Loan, session: Session) -> List[LedgerTriggerEvent]: reset_joining_fee_request_id = ( - session.query(PaymentRequestData.payment_request_id) + session.query(PaymentRequestsData.payment_request_id) .filter( - PaymentRequestData.row_status == "active", - PaymentRequestData.payment_request_status == "Paid", - PaymentRequestData.type == "reset_joining_fees", - PaymentRequestData.user_id == user_loan.user_id, + PaymentRequestsData.row_status == "active", + PaymentRequestsData.payment_request_status == "Paid", + PaymentRequestsData.type == "reset_joining_fees", + PaymentRequestsData.user_id == user_loan.user_id, ) .scalar() ) From 2cd45063a7aae688952903cfd5b15c63642eae6a Mon Sep 17 00:00:00 2001 From: SamuelNittala Date: Tue, 13 Jul 2021 20:50:49 +0530 Subject: [PATCH 03/15] Black format fix --- src/rush/loan_schedule/loan_schedule.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rush/loan_schedule/loan_schedule.py b/src/rush/loan_schedule/loan_schedule.py index ee1fbc77..685f0d4f 100644 --- a/src/rush/loan_schedule/loan_schedule.py +++ b/src/rush/loan_schedule/loan_schedule.py @@ -336,6 +336,7 @@ def readjust_future_payment(user_loan: BaseLoan, date_to_check_after: date): amount_settled=amount_slid, ) + def reset_loan_schedule(loan_id: Loan.id, session: Session) -> None: def reset_bill_emis(user_loan: Loan, session: Session) -> None: bill = user_loan.get_latest_bill() From d5b57c4319ff615174f123e2955e7603acf1ba95 Mon Sep 17 00:00:00 2001 From: SamuelNittala Date: Wed, 14 Jul 2021 01:27:46 +0530 Subject: [PATCH 04/15] Added test-case for emi payment reset --- pytest.ini | 2 +- src/rush/loan_schedule/loan_schedule.py | 8 +- src/test/test_reset_card.py | 123 +++++++++++++++++++++++- 3 files changed, 126 insertions(+), 7 deletions(-) diff --git a/pytest.ini b/pytest.ini index bbce6d06..5624b62b 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,2 @@ [pytest] -addopts = -p no:warnings src/test +addopts = -p no:warnings src/test/ diff --git a/src/rush/loan_schedule/loan_schedule.py b/src/rush/loan_schedule/loan_schedule.py index 685f0d4f..b0d4c6ca 100644 --- a/src/rush/loan_schedule/loan_schedule.py +++ b/src/rush/loan_schedule/loan_schedule.py @@ -397,14 +397,14 @@ def make_emi_payment_mappings_inactive(user_loan: Loan, session: Session) -> Non .update({PaymentMapping.row_status: "inactive"}, synchronize_session=False) ) - user_loan = get_user_loan(session=Session, loan_id=loan_id) + user_loan = get_user_loan(session=session, loan_id=loan_id) - reset_bill_emis(user_loan=user_loan) + reset_bill_emis(user_loan=user_loan,session=session) reset_emis(user_loan=user_loan) group_bills(user_loan) - make_emi_payment_mappings_inactive(user_loan) + make_emi_payment_mappings_inactive(user_loan,session=session) - payment_events = get_payment_events(user_loan) + payment_events = get_payment_events(user_loan,session=session) for payment_event in payment_events: amount_to_slide = ( diff --git a/src/test/test_reset_card.py b/src/test/test_reset_card.py index 27e5c0e4..cce664fb 100644 --- a/src/test/test_reset_card.py +++ b/src/test/test_reset_card.py @@ -9,7 +9,7 @@ from dateutil.relativedelta import relativedelta from pendulum import parse as parse_date # type: ignore from sqlalchemy.orm import Session - +from rush.loan_schedule.loan_schedule import reset_loan_schedule from rush.accrue_financial_charges import ( accrue_interest_on_all_bills, accrue_late_charges, @@ -38,7 +38,9 @@ JournalEntry, LedgerTriggerEvent, Lenders, + Loan, LoanData, + LoanSchedule, Product, User, ) @@ -249,7 +251,6 @@ def test_create_term_loan(session: Session) -> None: assert interest_left_to_accrue == Decimal("2608.04") assert user_loan.get_remaining_max() == Decimal(0) - payment_date = parse_date("2020-08-30") amount = Decimal(2000) payment_request_id = "dummy_reset_fee_3" @@ -1174,3 +1175,121 @@ def test_reset_card_versions(session: Session) -> None: v2 = ResetCardV2(session=session) assert is_reset_loan(v2) is True assert is_reset_product_type(v2.product_type) is True + +def test_reset_loan_schedule(session: Session) -> None: + + create_lenders(session=session) + create_products(session=session) + create_user(session=session) + + user_product = create_user_product_mapping( + session=session, user_id=6, product_type="term_loan_reset" + ) + create_loan(session=session, user_product=user_product, lender_id=1756833) + user_loan = get_user_product( + session=session, user_id=user_product.user_id, card_type="term_loan_reset" + ) + assert isinstance(user_loan, ResetCard) == True + + fee = create_loan_fee( + session=session, + user_loan=user_loan, + post_date=parse_date("2020-08-01 00:00:00"), + gross_amount=Decimal("100"), + include_gst_from_gross_amount=False, + fee_name="reset_joining_fees", + ) + + payment_date = parse_date("2020-08-01") + amount = fee.gross_amount + payment_request_id = "dummy_reset_fee_2" + payment_request_data( + session=session, + type="reset_joining_fees", + payment_request_amount=amount, + user_id=user_product.user_id, + payment_request_id=payment_request_id, + ) + payment_requests_data = pay_payment_request( + session=session, payment_request_id=payment_request_id, payment_date=payment_date + ) + payment_received( + session=session, + user_loan=user_loan, + payment_request_data=payment_requests_data, + ) + settle_payment_in_bank( + session=session, + payment_request_id=payment_request_id, + gateway_expenses=payment_requests_data.payment_execution_charges, + gross_payment_amount=payment_requests_data.payment_request_amount, + settlement_date=payment_requests_data.payment_received_in_bank_date, + user_loan=user_loan, + ) + payment_ledger_event = ( + session.query(LedgerTriggerEvent) + .filter( + LedgerTriggerEvent.name == "payment_received", + LedgerTriggerEvent.extra_details["payment_request_id"].astext == payment_request_id, + ) + .first() + ) + assert payment_ledger_event.amount == amount + + session.flush() + + loan_creation_data = {"date_str": "2020-08-01", "user_product_id": user_product.id} + + # create loan + loan = create_test_term_loan(session=session, **loan_creation_data) + + loan_data = session.query(LoanData).filter(LoanData.loan_id == user_loan.loan_id).one() + + payment_date = parse_date("2020-08-25") + amount = Decimal(11000) + payment_request_id = "dummy_reset_fee_3" + payment_request_data( + session=session, + type="collection", + payment_request_amount=amount, + user_id=user_product.user_id, + payment_request_id=payment_request_id, + ) + payment_requests_data = pay_payment_request( + session=session, payment_request_id=payment_request_id, payment_date=payment_date + ) + payment_received( + session=session, + user_loan=user_loan, + payment_request_data=payment_requests_data, + ) + settle_payment_in_bank( + session=session, + payment_request_id=payment_request_id, + gateway_expenses=payment_requests_data.payment_execution_charges, + gross_payment_amount=payment_requests_data.payment_request_amount, + settlement_date=payment_requests_data.payment_received_in_bank_date, + user_loan=user_loan, + ) + + first_emi = ( + session.query(LoanSchedule) + .filter( + LoanSchedule.bill_id == None, + ) + .order_by(id) + .first() + ) + assert first_emi.payment_status == "Paid" + + reset_loan_schedule(loan_id=user_loan.loan_id,session=session) + + first_emi = ( + session.query(LoanSchedule) + .filter( + LoanSchedule.bill_id == None, + ) + .order_by(id) + .first() + ) + assert first_emi.payment_status == "UnPaid" \ No newline at end of file From 87c6cf6aec49723e880dbe81e136e6597d686d7e Mon Sep 17 00:00:00 2001 From: SamuelNittala Date: Wed, 14 Jul 2021 02:47:12 +0530 Subject: [PATCH 05/15] ct --- src/rush/loan_schedule/loan_schedule.py | 21 ++++++++++++++------- src/test/test_reset_card.py | 12 +++++++----- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/rush/loan_schedule/loan_schedule.py b/src/rush/loan_schedule/loan_schedule.py index b0d4c6ca..ec746b57 100644 --- a/src/rush/loan_schedule/loan_schedule.py +++ b/src/rush/loan_schedule/loan_schedule.py @@ -1,6 +1,7 @@ from collections import defaultdict from datetime import date from decimal import Decimal +from typing import List from dateutil.relativedelta import relativedelta from pendulum import datetime @@ -9,9 +10,13 @@ func, ) from sqlalchemy.orm import Session -from typing import List + from rush.card import get_user_loan -from rush.card.base_card import BaseBill, BaseLoan, Loan +from rush.card.base_card import ( + BaseBill, + BaseLoan, + Loan, +) from rush.loan_schedule.moratorium import add_moratorium_emis from rush.models import ( LedgerTriggerEvent, @@ -356,13 +361,15 @@ def reset_bill_emis(user_loan: Loan, session: Session) -> None: bill_emi.total_closing_balance = (round(opening_principal, 2),) opening_principal -= principal_due - def reset_emis(user_loan: Loan) -> None: + def reset_emis(user_loan: Loan, session: Session) -> None: emis = user_loan.get_loan_schedule() for emi in emis: emi.last_payment_date = None emi.payment_received = 0 emi.dpd = -999 emi.payment_status = "UnPaid" + session.add(emi) + session.flush() def get_payment_events(user_loan: Loan, session: Session) -> List[LedgerTriggerEvent]: reset_joining_fee_request_id = ( @@ -399,12 +406,12 @@ def make_emi_payment_mappings_inactive(user_loan: Loan, session: Session) -> Non user_loan = get_user_loan(session=session, loan_id=loan_id) - reset_bill_emis(user_loan=user_loan,session=session) - reset_emis(user_loan=user_loan) + reset_bill_emis(user_loan=user_loan, session=session) + reset_emis(user_loan=user_loan, session=session) group_bills(user_loan) - make_emi_payment_mappings_inactive(user_loan,session=session) + make_emi_payment_mappings_inactive(user_loan, session=session) - payment_events = get_payment_events(user_loan,session=session) + payment_events = get_payment_events(user_loan, session=session) for payment_event in payment_events: amount_to_slide = ( diff --git a/src/test/test_reset_card.py b/src/test/test_reset_card.py index cce664fb..6e352f9a 100644 --- a/src/test/test_reset_card.py +++ b/src/test/test_reset_card.py @@ -9,7 +9,7 @@ from dateutil.relativedelta import relativedelta from pendulum import parse as parse_date # type: ignore from sqlalchemy.orm import Session -from rush.loan_schedule.loan_schedule import reset_loan_schedule + from rush.accrue_financial_charges import ( accrue_interest_on_all_bills, accrue_late_charges, @@ -31,6 +31,7 @@ from rush.create_card_swipe import create_card_swipe from rush.ledger_utils import get_account_balance_from_str from rush.limit_unlock import limit_unlock +from rush.loan_schedule.loan_schedule import reset_loan_schedule from rush.min_payment import add_min_to_all_bills from rush.models import ( CollectionOrders, @@ -1271,25 +1272,26 @@ def test_reset_loan_schedule(session: Session) -> None: settlement_date=payment_requests_data.payment_received_in_bank_date, user_loan=user_loan, ) - + first_emi = ( session.query(LoanSchedule) .filter( LoanSchedule.bill_id == None, ) - .order_by(id) + .order_by(LoanSchedule.id) .first() ) assert first_emi.payment_status == "Paid" + + to_check_emi_id = first_emi.id reset_loan_schedule(loan_id=user_loan.loan_id,session=session) first_emi = ( session.query(LoanSchedule) .filter( - LoanSchedule.bill_id == None, + LoanSchedule.id == to_check_emi_id ) - .order_by(id) .first() ) assert first_emi.payment_status == "UnPaid" \ No newline at end of file From 3c9bdd804d44084de3f230dd8c846b1d6f7933cb Mon Sep 17 00:00:00 2001 From: SamuelNittala Date: Wed, 14 Jul 2021 13:07:41 +0530 Subject: [PATCH 06/15] Fixed test-case --- src/rush/loan_schedule/loan_schedule.py | 2 +- src/test/test_reset_card.py | 39 +++++++++---------------- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/src/rush/loan_schedule/loan_schedule.py b/src/rush/loan_schedule/loan_schedule.py index ec746b57..5c416745 100644 --- a/src/rush/loan_schedule/loan_schedule.py +++ b/src/rush/loan_schedule/loan_schedule.py @@ -358,7 +358,7 @@ def reset_bill_emis(user_loan: Loan, session: Session) -> None: for bill_emi in bill_emis: bill_emi.interest_due = interest_due bill_emi.principal_due = principal_due - bill_emi.total_closing_balance = (round(opening_principal, 2),) + bill_emi.total_closing_balance = round(opening_principal, 2) opening_principal -= principal_due def reset_emis(user_loan: Loan, session: Session) -> None: diff --git a/src/test/test_reset_card.py b/src/test/test_reset_card.py index 6e352f9a..77232e8a 100644 --- a/src/test/test_reset_card.py +++ b/src/test/test_reset_card.py @@ -1167,7 +1167,6 @@ def test_reset_loan_limit_unlock_error(session: Session) -> None: def test_reset_loan_early_payment(session: Session) -> None: pass - def test_reset_card_versions(session: Session) -> None: v1 = ResetCard(session=session) assert is_reset_loan(v1) is True @@ -1177,6 +1176,7 @@ def test_reset_card_versions(session: Session) -> None: assert is_reset_loan(v2) is True assert is_reset_product_type(v2.product_type) is True + def test_reset_loan_schedule(session: Session) -> None: create_lenders(session=session) @@ -1203,7 +1203,7 @@ def test_reset_loan_schedule(session: Session) -> None: payment_date = parse_date("2020-08-01") amount = fee.gross_amount - payment_request_id = "dummy_reset_fee_2" + payment_request_id = "dummy_reset_fee_1" payment_request_data( session=session, type="reset_joining_fees", @@ -1244,11 +1244,9 @@ def test_reset_loan_schedule(session: Session) -> None: # create loan loan = create_test_term_loan(session=session, **loan_creation_data) - loan_data = session.query(LoanData).filter(LoanData.loan_id == user_loan.loan_id).one() - payment_date = parse_date("2020-08-25") amount = Decimal(11000) - payment_request_id = "dummy_reset_fee_3" + payment_request_id = "dummy_reset_fee_2" payment_request_data( session=session, type="collection", @@ -1273,25 +1271,16 @@ def test_reset_loan_schedule(session: Session) -> None: user_loan=user_loan, ) - first_emi = ( - session.query(LoanSchedule) - .filter( - LoanSchedule.bill_id == None, - ) - .order_by(LoanSchedule.id) - .first() - ) - assert first_emi.payment_status == "Paid" + emis = user_loan.get_loan_schedule() - to_check_emi_id = first_emi.id - - reset_loan_schedule(loan_id=user_loan.loan_id,session=session) + assert emis[0].payment_status == "Paid" + initial_payment_received = emis[0].payment_received - first_emi = ( - session.query(LoanSchedule) - .filter( - LoanSchedule.id == to_check_emi_id - ) - .first() - ) - assert first_emi.payment_status == "UnPaid" \ No newline at end of file + # changing data manually to check the reset_loan_schedule function. + emis[0].payment_status = "UnPaid" + emis[0].payment_received = 0 + + reset_loan_schedule(loan_id=user_loan.loan_id, session=session) + + assert emis[0].payment_status == "Paid" + assert emis[0].payment_received == initial_payment_received From be19342d8c9c87738d14863b1445cae5f58458a8 Mon Sep 17 00:00:00 2001 From: SamuelNittala Date: Wed, 14 Jul 2021 13:18:19 +0530 Subject: [PATCH 07/15] Type fix --- src/test/test_reset_card.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/test_reset_card.py b/src/test/test_reset_card.py index 77232e8a..df7a5fba 100644 --- a/src/test/test_reset_card.py +++ b/src/test/test_reset_card.py @@ -1167,6 +1167,7 @@ def test_reset_loan_limit_unlock_error(session: Session) -> None: def test_reset_loan_early_payment(session: Session) -> None: pass + def test_reset_card_versions(session: Session) -> None: v1 = ResetCard(session=session) assert is_reset_loan(v1) is True @@ -1278,7 +1279,7 @@ def test_reset_loan_schedule(session: Session) -> None: # changing data manually to check the reset_loan_schedule function. emis[0].payment_status = "UnPaid" - emis[0].payment_received = 0 + emis[0].payment_received = Decimal("0") reset_loan_schedule(loan_id=user_loan.loan_id, session=session) From e9a9f1e6ec1893faab3dda9de8f0752b6156dda0 Mon Sep 17 00:00:00 2001 From: SamuelNittala Date: Wed, 14 Jul 2021 17:29:29 +0530 Subject: [PATCH 08/15] Added dpd check --- src/test/test_reset_card.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/test/test_reset_card.py b/src/test/test_reset_card.py index df7a5fba..a9260d10 100644 --- a/src/test/test_reset_card.py +++ b/src/test/test_reset_card.py @@ -1273,15 +1273,25 @@ def test_reset_loan_schedule(session: Session) -> None: ) emis = user_loan.get_loan_schedule() - assert emis[0].payment_status == "Paid" initial_payment_received = emis[0].payment_received + dpd = emis[0].dpd # changing data manually to check the reset_loan_schedule function. emis[0].payment_status = "UnPaid" emis[0].payment_received = Decimal("0") + emis[0].dpd = -999 reset_loan_schedule(loan_id=user_loan.loan_id, session=session) assert emis[0].payment_status == "Paid" assert emis[0].payment_received == initial_payment_received + assert emis[0].dpd == dpd + + bill = user_loan.get_latest_bill() + bill_emis = ( + session.query(LoanSchedule) + .filter(LoanSchedule.loan_id == user_loan.loan_id, LoanSchedule.bill_id == bill.id) + .order_by(LoanSchedule.emi_number) + .all() + ) \ No newline at end of file From 7eb1ed19078441dc5e4761d5eb35e9aedb805d80 Mon Sep 17 00:00:00 2001 From: SamuelNittala Date: Wed, 14 Jul 2021 19:58:26 +0530 Subject: [PATCH 09/15] Bill level tests --- pytest.ini | 2 +- src/test/test_reset_card.py | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pytest.ini b/pytest.ini index 5624b62b..bbce6d06 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,2 @@ [pytest] -addopts = -p no:warnings src/test/ +addopts = -p no:warnings src/test diff --git a/src/test/test_reset_card.py b/src/test/test_reset_card.py index a9260d10..3c6cae5b 100644 --- a/src/test/test_reset_card.py +++ b/src/test/test_reset_card.py @@ -9,6 +9,7 @@ from dateutil.relativedelta import relativedelta from pendulum import parse as parse_date # type: ignore from sqlalchemy.orm import Session +from sqlalchemy.sql.functions import user from rush.accrue_financial_charges import ( accrue_interest_on_all_bills, @@ -1272,8 +1273,10 @@ def test_reset_loan_schedule(session: Session) -> None: user_loan=user_loan, ) + # emi-level emis = user_loan.get_loan_schedule() assert emis[0].payment_status == "Paid" + initial_payment_received = emis[0].payment_received dpd = emis[0].dpd @@ -1287,11 +1290,19 @@ def test_reset_loan_schedule(session: Session) -> None: assert emis[0].payment_status == "Paid" assert emis[0].payment_received == initial_payment_received assert emis[0].dpd == dpd - + + # bill-level bill = user_loan.get_latest_bill() bill_emis = ( session.query(LoanSchedule) .filter(LoanSchedule.loan_id == user_loan.loan_id, LoanSchedule.bill_id == bill.id) .order_by(LoanSchedule.emi_number) .all() - ) \ No newline at end of file + ) + original_interest_due = bill_emis[0].interest_due + + # changing values manually to check the reset_loan_schedule function + bill_emis[0].interest_due = 0 + + reset_loan_schedule(loan_id=user_loan.loan_id, session=session) + assert bill_emis[0].interest_due == original_interest_due From a4c791b9cbdabc65388c00ae5a218e8b47dfc6f1 Mon Sep 17 00:00:00 2001 From: SamuelNittala Date: Thu, 15 Jul 2021 14:28:40 +0530 Subject: [PATCH 10/15] Remove unused imports, added principal_due test --- src/test/test_reset_card.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test/test_reset_card.py b/src/test/test_reset_card.py index 3c6cae5b..06ab2ef0 100644 --- a/src/test/test_reset_card.py +++ b/src/test/test_reset_card.py @@ -9,7 +9,6 @@ from dateutil.relativedelta import relativedelta from pendulum import parse as parse_date # type: ignore from sqlalchemy.orm import Session -from sqlalchemy.sql.functions import user from rush.accrue_financial_charges import ( accrue_interest_on_all_bills, @@ -1300,9 +1299,13 @@ def test_reset_loan_schedule(session: Session) -> None: .all() ) original_interest_due = bill_emis[0].interest_due + original_principal_due = bill_emis[0].principal_due # changing values manually to check the reset_loan_schedule function bill_emis[0].interest_due = 0 + bill_emis[0].principal_due = 0 reset_loan_schedule(loan_id=user_loan.loan_id, session=session) assert bill_emis[0].interest_due == original_interest_due + assert bill_emis[0].principal_due == original_principal_due + From a63af9b6b083b2b162e27b43830b906b25db21f5 Mon Sep 17 00:00:00 2001 From: SamuelNittala Date: Fri, 16 Jul 2021 16:13:18 +0530 Subject: [PATCH 11/15] Added individual file testing to README Improved reset_loan_schedule function. --- README.md | 8 ++- pytest.ini | 2 +- src/rush/loan_schedule/loan_schedule.py | 86 +++++++++++++------------ src/test/test_reset_card.py | 4 +- 4 files changed, 56 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 26be9c57..9c366592 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,13 @@ - rerun `pip install -e "."` if you make changes to models/source code. not needed for testcase changes - run `pytest --mypy --black --isort --cov=rush --cov-report=xml --cov-report=term` to run your tests - if black formatting tests fail, just run `black .` from your top level directory. Alternatively you can setup black in vscode (I highly recommend setting up all three ***"format on paste/save/type"***) - - if isort formatting tests fail, just run `isort .` from your top level directory. + - if isort formatting tests fail, just run `isort .` from your top level directory. +- to run tests on a particular file, go to pytest.ini file and add the file name to the end + - example: + ```python + [pytest] + addopts = -p no:warnings src/test/test_reset_card.py + ``` - all source code is under `src/rush/` . That is where you should make your code - docker kill rush_pg - psql postgresql://alem_user:password@localhost:5680/alem_db diff --git a/pytest.ini b/pytest.ini index bbce6d06..9614b051 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,2 @@ [pytest] -addopts = -p no:warnings src/test +addopts = -p no:warnings src/test/test_reset_card.py diff --git a/src/rush/loan_schedule/loan_schedule.py b/src/rush/loan_schedule/loan_schedule.py index 5c416745..749bef7f 100644 --- a/src/rush/loan_schedule/loan_schedule.py +++ b/src/rush/loan_schedule/loan_schedule.py @@ -342,53 +342,46 @@ def readjust_future_payment(user_loan: BaseLoan, date_to_check_after: date): ) -def reset_loan_schedule(loan_id: Loan.id, session: Session) -> None: +def reset_loan_schedule(user_loan: Loan, session: Session) -> None: def reset_bill_emis(user_loan: Loan, session: Session) -> None: - bill = user_loan.get_latest_bill() - bill_emis = ( + bills = user_loan.get_all_bills() + for bill in bills: + bill_emis = ( + session.query(LoanSchedule) + .filter(LoanSchedule.loan_id == user_loan.loan_id, LoanSchedule.bill_id == bill.id) + .order_by(LoanSchedule.emi_number) + .all() + ) + instalment = bill.get_instalment_amount() + opening_principal = bill.table.principal + interest_due = round(bill.get_interest_to_charge(), 2) + principal_due = round(instalment - interest_due, 2) + for bill_emi in bill_emis: + bill_emi.interest_due = interest_due + bill_emi.principal_due = principal_due + bill_emi.total_closing_balance = round(opening_principal, 2) + opening_principal -= principal_due + + def reset_payment_info(user_loan: Loan, session: Session) -> None: + _ = ( session.query(LoanSchedule) - .filter(LoanSchedule.loan_id == user_loan.loan_id, LoanSchedule.bill_id == bill.id) - .order_by(LoanSchedule.emi_number) - .all() + .filter(LoanSchedule.loan_id == user_loan.id) + .update( + { + LoanSchedule.last_payment_date.name: None, + LoanSchedule.payment_received.name: Decimal("0"), + LoanSchedule.dpd.name: -999, + LoanSchedule.payment_status.name: "UnPaid", + } + ) ) - instalment = bill.get_instalment_amount() - opening_principal = bill.table.principal - interest_due = round(bill.get_interest_to_charge(), 2) - principal_due = round(instalment - interest_due, 2) - for bill_emi in bill_emis: - bill_emi.interest_due = interest_due - bill_emi.principal_due = principal_due - bill_emi.total_closing_balance = round(opening_principal, 2) - opening_principal -= principal_due - - def reset_emis(user_loan: Loan, session: Session) -> None: - emis = user_loan.get_loan_schedule() - for emi in emis: - emi.last_payment_date = None - emi.payment_received = 0 - emi.dpd = -999 - emi.payment_status = "UnPaid" - session.add(emi) - session.flush() def get_payment_events(user_loan: Loan, session: Session) -> List[LedgerTriggerEvent]: - reset_joining_fee_request_id = ( - session.query(PaymentRequestsData.payment_request_id) - .filter( - PaymentRequestsData.row_status == "active", - PaymentRequestsData.payment_request_status == "Paid", - PaymentRequestsData.type == "reset_joining_fees", - PaymentRequestsData.user_id == user_loan.user_id, - ) - .scalar() - ) payment_events = ( session.query(LedgerTriggerEvent) .filter( LedgerTriggerEvent.loan_id == user_loan.loan_id, LedgerTriggerEvent.name == "payment_received", - LedgerTriggerEvent.extra_details["payment_request_id"].astext - != reset_joining_fee_request_id, ) .order_by(LedgerTriggerEvent.post_date) .all() @@ -404,10 +397,21 @@ def make_emi_payment_mappings_inactive(user_loan: Loan, session: Session) -> Non .update({PaymentMapping.row_status: "inactive"}, synchronize_session=False) ) - user_loan = get_user_loan(session=session, loan_id=loan_id) + user_loan = get_user_loan(session=session, loan_id=user_loan.loan_id) + + # returning if the loan is extended, because we don't have context of past tenure. + is_loan_extended = ( + session.query(LedgerTriggerEvent.loan_id) + .filter( + LedgerTriggerEvent.loan_id == user_loan.loan_id, LedgerTriggerEvent.name == "bill_extended" + ) + .first() + ) + if is_loan_extended: + return reset_bill_emis(user_loan=user_loan, session=session) - reset_emis(user_loan=user_loan, session=session) + reset_payment_info(user_loan=user_loan, session=session) group_bills(user_loan) make_emi_payment_mappings_inactive(user_loan, session=session) @@ -422,7 +426,9 @@ def make_emi_payment_mappings_inactive(user_loan: Loan, session: Session) -> Non ) .scalar() ) - slide_payment_to_emis(user_loan, payment_event, amount_to_slide) + #dont slide amount for reset_joining_fees + if amount_to_slide is not None: + slide_payment_to_emis(user_loan, payment_event, amount_to_slide) if user_loan.can_close_loan(as_of_event_id=payment_event.id): close_loan(user_loan, payment_event.post_date) break diff --git a/src/test/test_reset_card.py b/src/test/test_reset_card.py index fb56e7fd..f74dd2f9 100644 --- a/src/test/test_reset_card.py +++ b/src/test/test_reset_card.py @@ -1298,7 +1298,7 @@ def test_reset_loan_schedule(session: Session) -> None: emis[0].payment_received = Decimal("0") emis[0].dpd = -999 - reset_loan_schedule(loan_id=user_loan.loan_id, session=session) + reset_loan_schedule(user_loan=user_loan, session=session) assert emis[0].payment_status == "Paid" assert emis[0].payment_received == original_payment_received @@ -1319,6 +1319,6 @@ def test_reset_loan_schedule(session: Session) -> None: bill_emis[0].interest_due = 0 bill_emis[0].principal_due = 0 - reset_loan_schedule(loan_id=user_loan.loan_id, session=session) + reset_loan_schedule(user_loan=user_loan, session=session) assert bill_emis[0].interest_due == original_interest_due assert bill_emis[0].principal_due == original_principal_due From c7e226578c54b0020c0aeebfa94a3b7597629165 Mon Sep 17 00:00:00 2001 From: SamuelNittala Date: Fri, 16 Jul 2021 16:15:02 +0530 Subject: [PATCH 12/15] Fixed pytest.ini --- pytest.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index 9614b051..bbce6d06 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,2 @@ [pytest] -addopts = -p no:warnings src/test/test_reset_card.py +addopts = -p no:warnings src/test From 7a72bb55faf64dc6e01613269912a42195031290 Mon Sep 17 00:00:00 2001 From: SamuelNittala Date: Fri, 16 Jul 2021 17:56:45 +0530 Subject: [PATCH 13/15] Added extend schedule test and improvements to rest_loan_schedule --- src/rush/loan_schedule/loan_schedule.py | 44 +++++++++++-------------- src/test/test_reset_card.py | 12 +++++++ 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/rush/loan_schedule/loan_schedule.py b/src/rush/loan_schedule/loan_schedule.py index 749bef7f..72483e6f 100644 --- a/src/rush/loan_schedule/loan_schedule.py +++ b/src/rush/loan_schedule/loan_schedule.py @@ -376,18 +376,6 @@ def reset_payment_info(user_loan: Loan, session: Session) -> None: ) ) - def get_payment_events(user_loan: Loan, session: Session) -> List[LedgerTriggerEvent]: - payment_events = ( - session.query(LedgerTriggerEvent) - .filter( - LedgerTriggerEvent.loan_id == user_loan.loan_id, - LedgerTriggerEvent.name == "payment_received", - ) - .order_by(LedgerTriggerEvent.post_date) - .all() - ) - return payment_events - def make_emi_payment_mappings_inactive(user_loan: Loan, session: Session) -> None: emis = user_loan.get_loan_schedule() emi_ids = [emi.id for emi in emis] @@ -415,20 +403,26 @@ def make_emi_payment_mappings_inactive(user_loan: Loan, session: Session) -> Non group_bills(user_loan) make_emi_payment_mappings_inactive(user_loan, session=session) - payment_events = get_payment_events(user_loan, session=session) - - for payment_event in payment_events: - amount_to_slide = ( - session.query(func.sum(PaymentSplit.amount_settled)) - .filter( - PaymentSplit.payment_request_id == payment_event.extra_details["payment_request_id"], - PaymentSplit.component.in_(("principal", "interest", "unbilled")), - ) - .scalar() + amount_to_slide_per_event = ( + session.query(LedgerTriggerEvent.id, func.sum(PaymentSplit.amount_settled)) + .filter( + PaymentSplit.payment_request_id + == LedgerTriggerEvent.extra_details["payment_request_id"].astext, + PaymentSplit.component.in_(("principal", "interest", "unbilled")), + LedgerTriggerEvent.name == "payment_received", + LedgerTriggerEvent.loan_id == user_loan.loan_id, ) - #dont slide amount for reset_joining_fees - if amount_to_slide is not None: - slide_payment_to_emis(user_loan, payment_event, amount_to_slide) + .group_by(LedgerTriggerEvent.id) + .all() + ) + + event_ids = [id for id, _ in amount_to_slide_per_event] + payment_events = session.query(LedgerTriggerEvent).filter(LedgerTriggerEvent.id.in_(event_ids)).all() + event_id_to_object_mapping = {event.id: event for event in payment_events} + + for event_id, amount_to_slide in amount_to_slide_per_event: + payment_event = event_id_to_object_mapping[event_id] + slide_payment_to_emis(user_loan, payment_event, amount_to_slide) if user_loan.can_close_loan(as_of_event_id=payment_event.id): close_loan(user_loan, payment_event.post_date) break diff --git a/src/test/test_reset_card.py b/src/test/test_reset_card.py index f74dd2f9..f3f7f743 100644 --- a/src/test/test_reset_card.py +++ b/src/test/test_reset_card.py @@ -31,6 +31,7 @@ from rush.create_card_swipe import create_card_swipe from rush.ledger_utils import get_account_balance_from_str from rush.limit_unlock import limit_unlock +from rush.loan_schedule.extension import extend_schedule from rush.loan_schedule.loan_schedule import reset_loan_schedule from rush.min_payment import add_min_to_all_bills from rush.models import ( @@ -1322,3 +1323,14 @@ def test_reset_loan_schedule(session: Session) -> None: reset_loan_schedule(user_loan=user_loan, session=session) assert bill_emis[0].interest_due == original_interest_due assert bill_emis[0].principal_due == original_principal_due + + # changing values manually to check the reset_loan_schedule function + bill_emis[0].interest_due = 0 + bill_emis[0].principal_due = 0 + + # function should'nt work when the loan is extended. + extend_schedule(user_loan=user_loan, new_tenure=24, from_date=parse_date("2020-08-24")) + reset_loan_schedule(user_loan=user_loan, session=session) + + assert bill_emis[0].interest_due == 0 + assert bill_emis[0].principal_due == 0 From 0b6235272b4455f22705a48838c9379770b1238c Mon Sep 17 00:00:00 2001 From: Nikhil Vangumalla Date: Sat, 24 Jul 2021 11:30:15 +0530 Subject: [PATCH 14/15] include early_close_fee for loan_schedule_reset - early_close_fee is interest for reset loans --- src/rush/loan_schedule/loan_schedule.py | 4 +--- src/rush/payments.py | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/rush/loan_schedule/loan_schedule.py b/src/rush/loan_schedule/loan_schedule.py index 97d95e4d..2441cb68 100644 --- a/src/rush/loan_schedule/loan_schedule.py +++ b/src/rush/loan_schedule/loan_schedule.py @@ -385,8 +385,6 @@ def make_emi_payment_mappings_inactive(user_loan: Loan, session: Session) -> Non .update({PaymentMapping.row_status: "inactive"}, synchronize_session=False) ) - user_loan = get_user_loan(session=session, loan_id=user_loan.loan_id) - # returning if the loan is extended, because we don't have context of past tenure. is_loan_extended = ( session.query(LedgerTriggerEvent.loan_id) @@ -408,7 +406,7 @@ def make_emi_payment_mappings_inactive(user_loan: Loan, session: Session) -> Non .filter( PaymentSplit.payment_request_id == LedgerTriggerEvent.extra_details["payment_request_id"].astext, - PaymentSplit.component.in_(("principal", "interest", "unbilled")), + PaymentSplit.component.in_(("principal", "interest", "unbilled", "early_close_fee")), LedgerTriggerEvent.name == "payment_received", LedgerTriggerEvent.loan_id == user_loan.loan_id, ) diff --git a/src/rush/payments.py b/src/rush/payments.py index 56f87c9c..6d3ad373 100644 --- a/src/rush/payments.py +++ b/src/rush/payments.py @@ -411,6 +411,7 @@ def adjust_payment( amount_to_adjust - user_loan.get_remaining_max(event_id=event.id), ) add_early_close_charges(session, user_loan, event.post_date, extra_amount) + slide_payment_to_emis(user_loan, event, extra_amount) split_data = find_split_to_slide_in_loan(session, user_loan, amount_to_adjust) From f45e53c6905bcf4b361d0eb0fe61be3e31ed3a3b Mon Sep 17 00:00:00 2001 From: Nikhil Vangumalla Date: Mon, 16 Aug 2021 07:18:29 +0530 Subject: [PATCH 15/15] call slide_payment_to_emis for early close fee after it has been handled --- src/rush/payments.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rush/payments.py b/src/rush/payments.py index 6d3ad373..4f44510d 100644 --- a/src/rush/payments.py +++ b/src/rush/payments.py @@ -411,7 +411,6 @@ def adjust_payment( amount_to_adjust - user_loan.get_remaining_max(event_id=event.id), ) add_early_close_charges(session, user_loan, event.post_date, extra_amount) - slide_payment_to_emis(user_loan, event, extra_amount) split_data = find_split_to_slide_in_loan(session, user_loan, amount_to_adjust) @@ -438,6 +437,8 @@ def adjust_payment( # The amount to adjust is computed for this bill. It should all settle. assert remaining_amount == 0 slide_payment_to_emis(user_loan, event, data["amount_to_adjust"]) + if data["type"] == "early_close_fee": + slide_payment_to_emis(user_loan, event, data["amount_to_adjust"]) amount_to_adjust -= data["amount_to_adjust"] # After doing the sliding we check if the loan can be closed.