From 7cea5390c0726ad8535a811816286e45dd7206c9 Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Thu, 10 Apr 2025 17:02:54 +0100 Subject: [PATCH 1/4] [add]: loan_repayment event --- .vscode/settings.json | 48 ++++++++++++++++---------------- app/api/integrations/kafka.py | 6 ++-- app/api/services/base_service.py | 6 ++++ app/api/services/provide_loan.py | 7 ++--- app/api/services/repayment.py | 12 +++++++- app/models/loan.py | 10 +++++++ app/models/repayment.py | 45 ++++++++++++++++++++++++++++++ 7 files changed, 101 insertions(+), 33 deletions(-) create mode 100644 app/models/repayment.py diff --git a/.vscode/settings.json b/.vscode/settings.json index b79483b..bc7c726 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,24 +1,24 @@ -{ - "editor.lineNumbers": "off", - "editor.padding.top": 3, - "editor.padding.bottom": 3, - "editor.formatOnSave": true, - "editor.formatOnPaste": true, - "editor.fontSize": 14, - "editor.lineHeight": 4.5, - "editor.suggestFontSize": 15, - // "editor.suggestLineHeight": 4, - "breadcrumbs.enabled": false, - "workbench.tips.enabled": false, - "workbench.statusBar.visible": false, - // "workbench.editor.showTabs": "single", - "git.enableSmartCommit": true, - "workbench.editor.editorActionsLocation": "hidden", - // "workbench.activityBar.location": "hidden", - "workbench.editor.enablePreviewFromQuickOpen": false, - "editor.lightbulb.enabled": "off", - "editor.selectionHighlight": false, - "editor.overviewRulerBorder": false, - "editor.renderLineHighlight": "none", - "editor.occurrencesHighlight": "off" -} +// { +// "editor.lineNumbers": "off", +// "editor.padding.top": 3, +// "editor.padding.bottom": 3, +// "editor.formatOnSave": true, +// "editor.formatOnPaste": true, +// "editor.fontSize": 14, +// "editor.lineHeight": 4.5, +// "editor.suggestFontSize": 15, +// // "editor.suggestLineHeight": 4, +// "breadcrumbs.enabled": false, +// "workbench.tips.enabled": false, +// "workbench.statusBar.visible": false, +// // "workbench.editor.showTabs": "single", +// "git.enableSmartCommit": true, +// "workbench.editor.editorActionsLocation": "hidden", +// // "workbench.activityBar.location": "hidden", +// "workbench.editor.enablePreviewFromQuickOpen": false, +// "editor.lightbulb.enabled": "off", +// "editor.selectionHighlight": false, +// "editor.overviewRulerBorder": false, +// "editor.renderLineHighlight": "none", +// "editor.occurrencesHighlight": "off" +// } diff --git a/app/api/integrations/kafka.py b/app/api/integrations/kafka.py index 610ad1e..fa3f8d1 100644 --- a/app/api/integrations/kafka.py +++ b/app/api/integrations/kafka.py @@ -43,9 +43,9 @@ class KafkaIntegration: @staticmethod - def send_loan_request(loan_data, request_id): + def send_loan_request(loan_data, request_id, topic): """ - Send loan request to PROCESS_PAYMENT topic + Send loan request to topic Args: loan_data: Loan request payload as dict @@ -58,7 +58,7 @@ class KafkaIntegration: # Sending loan request message to Kafka producer.produce( - topic="PROCESS_PAYMENT", + topic=topic, key=str(request_id), value=json.dumps(loan_data).encode("utf-8"), callback=KafkaIntegration.delivery_report, diff --git a/app/api/services/base_service.py b/app/api/services/base_service.py index db4bc64..aa678e0 100644 --- a/app/api/services/base_service.py +++ b/app/api/services/base_service.py @@ -3,6 +3,7 @@ from app.api.enums import TransactionType from flask import jsonify from marshmallow import ValidationError import logging +from app.api.integrations import KafkaIntegration logger = logging.getLogger(__name__) @@ -53,3 +54,8 @@ class BaseService: type=cls.TRANSACTION_TYPE, channel=validated_data.get("channel"), ) + + @classmethod + def async_send_to_kafka(cls, loan_data, request_id, topic): + KafkaIntegration.send_loan_request(loan_data = loan_data, request_id = request_id, topic = topic) + KafkaIntegration.flush() diff --git a/app/api/services/provide_loan.py b/app/api/services/provide_loan.py index dfc5a96..dc43cc4 100644 --- a/app/api/services/provide_loan.py +++ b/app/api/services/provide_loan.py @@ -5,7 +5,6 @@ from app.api.services.base_service import BaseService from app.api.enums import TransactionType from app.utils.logger import logger from app.api.schemas.provide_loan import ProvideLoanSchema -from app.api.integrations import KafkaIntegration from threading import Thread @@ -58,7 +57,7 @@ class ProvideLoanService(BaseService): # KafkaIntegration.send_loan_request(loan_data = response_data, request_id = request_id) # Call Kafka in a background thread - thread = Thread(target=ProvideLoanService.async_send_to_kafka, args=(response_data, request_id)) + thread = Thread(target=ProvideLoanService.async_send_to_kafka, args=(response_data, request_id, "PROCESS_PAYMENT")) thread.start() return response_data @@ -85,8 +84,6 @@ class ProvideLoanService(BaseService): }) , 500 - def async_send_to_kafka(loan_data, request_id): - KafkaIntegration.send_loan_request(loan_data = loan_data, request_id = request_id) - KafkaIntegration.flush() + diff --git a/app/api/services/repayment.py b/app/api/services/repayment.py index 7a53452..556d0a3 100644 --- a/app/api/services/repayment.py +++ b/app/api/services/repayment.py @@ -3,7 +3,8 @@ from marshmallow import ValidationError from app.utils.logger import logger from app.api.schemas.repayment import RepaymentSchema from app.api.services.base_service import BaseService -from app.api.enums import TransactionType +from app.api.enums import TransactionType +from threading import Thread class RepaymentService(BaseService): TRANSACTION_TYPE = TransactionType.REPAYMENT @@ -23,6 +24,11 @@ class RepaymentService(BaseService): validated_data = RepaymentService.validate_data(data, RepaymentSchema()) account_id = validated_data.get('accountId') customer_id = validated_data.get('customerId') + customer = RepaymentService.get_or_create_customer(validated_data) + account = customer.accounts[0] + validated_data['accountId'] = account.id + request_id = validated_data.get('requestId') + if (RepaymentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)): transaction = RepaymentService.log_transaction(validated_data = validated_data) @@ -51,6 +57,10 @@ class RepaymentService(BaseService): # message="Repayment processed successfully" # ) + # Call Kafka in a background thread + thread = Thread(target=RepaymentService.async_send_to_kafka, args=(response_data, request_id, "LOAN_REPAYMENT")) + thread.start() + return response_data except ValidationError as err: diff --git a/app/models/loan.py b/app/models/loan.py index 6fa73a1..6779c3c 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -26,6 +26,16 @@ class Loan(db.Model): return False, "Customer has active loans" return True, "No active loans" + @classmethod + def get_customer_loan(cls, loan_id, customer_id): + """ + Check if a loan with the given ID exists and if the loan belongs to the specified customer_id. + """ + loan = cls.query.filter_by(id=loan_id, customer_id=customer_id).first() + if not loan: + raise ValueError(f"Loan with ID {loan_id} does not exist or does not belong to customer {customer_id}.") + return loan + def __repr__(self): return f'' \ No newline at end of file diff --git a/app/models/repayment.py b/app/models/repayment.py new file mode 100644 index 0000000..af62554 --- /dev/null +++ b/app/models/repayment.py @@ -0,0 +1,45 @@ +from datetime import datetime, timezone +from app.extensions import db +from app.models.customer import Customer +from app.models.loan import Loan + + +class Repayment(db.Model): + __tablename__ = 'repayments' + + id = db.Column( + db.Integer, + primary_key=True, + autoincrement=True, + ) + loan_id = db.Column(db.String(50), nullable=False) + customer_id = db.Column(db.String(50), nullable=False) + product_id = db.Column(db.String(20), nullable=True) + created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc)) + updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc)) + + @classmethod + def create_repayment(cls, customer_id, loan_id, product_id): + + + # Check customer exists + if not Customer.is_valid_customer(customer_id): + raise ValueError("Invalid customer") + + # Check loan exists + loan = Loan.get_customer_loan(loan_id = loan_id, customer_id = customer_id) + if not loan: + raise ValueError("Loan not found for customer") + + repayment = cls( + customer_id=customer_id, + loan_id=loan.id, + product_id=product_id, + ) + + db.session.add(repayment) + db.session.commit() + return repayment + + def __repr__(self): + return f'' From f252e33be2daeb9e31155eeef32602b93c598e3b Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Thu, 10 Apr 2025 17:15:19 +0100 Subject: [PATCH 2/4] [add]: Loan repayment event --- app/api/services/provide_loan.py | 4 +--- app/models/account.py | 10 ++++++++-- app/models/customer.py | 25 +++++++++++++++---------- app/models/loan.py | 9 +++++++-- app/models/repayment.py | 10 ++++++++-- 5 files changed, 39 insertions(+), 19 deletions(-) diff --git a/app/api/services/provide_loan.py b/app/api/services/provide_loan.py index e8562b7..bfb4f05 100644 --- a/app/api/services/provide_loan.py +++ b/app/api/services/provide_loan.py @@ -41,9 +41,7 @@ class ProvideLoanService(BaseService): "message": "Failed to log transaction." }), 400 - # Save the loan details - loan_id = f"loan_{transaction_id}" - + # Save the loan details loan = Loan.create_loan( customer_id=customer_id, account_id=account_id, diff --git a/app/models/account.py b/app/models/account.py index 8c94098..af4501e 100644 --- a/app/models/account.py +++ b/app/models/account.py @@ -1,6 +1,7 @@ from datetime import datetime, timezone from sqlalchemy.orm import relationship from app.extensions import db +from sqlalchemy.exc import IntegrityError class Account(db.Model): __tablename__ = 'accounts' @@ -27,8 +28,13 @@ class Account(db.Model): customer_id=customer_id, account_type=account_type ) - db.session.add(account) - db.session.commit() + + try: + db.session.add(account) + db.session.commit() + except IntegrityError as err: + db.session.rollback() + raise ValueError(f"Database integrity error: {err}") return account @classmethod diff --git a/app/models/customer.py b/app/models/customer.py index e6418c1..e0fb316 100644 --- a/app/models/customer.py +++ b/app/models/customer.py @@ -2,6 +2,7 @@ from datetime import datetime, timezone from sqlalchemy.orm import relationship from app.extensions import db from app.models.account import Account +from sqlalchemy.exc import IntegrityError class Customer(db.Model): __tablename__ = 'customers' @@ -33,16 +34,20 @@ class Customer(db.Model): # Create the customer customer = cls(id=id, msisdn=msisdn, country_code=country_code) - db.session.add(customer) - - # Create an associated account - account = Account.create_account( - id=account_id, - customer_id=id, - account_type=account_type - ) - - db.session.commit() + try: + db.session.add(customer) + + # Create an associated account + account = Account.create_account( + id=account_id, + customer_id=id, + account_type=account_type + ) + + db.session.commit() + except IntegrityError as err: + db.session.rollback() + raise ValueError(f"Database integrity error: {err}") return customer def __repr__(self): diff --git a/app/models/loan.py b/app/models/loan.py index a3d99e2..2d68a64 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -2,6 +2,7 @@ from datetime import datetime, timezone from app.extensions import db from app.models.customer import Customer from app.models.account import Account +from sqlalchemy.exc import IntegrityError class Loan(db.Model): @@ -44,8 +45,12 @@ class Loan(db.Model): status=status ) - db.session.add(loan) - db.session.commit() + try: + db.session.add(loan) + db.session.commit() + except IntegrityError as err: + db.session.rollback() + raise ValueError(f"Database integrity error: {err}") return loan diff --git a/app/models/repayment.py b/app/models/repayment.py index af62554..538eda3 100644 --- a/app/models/repayment.py +++ b/app/models/repayment.py @@ -2,6 +2,7 @@ from datetime import datetime, timezone from app.extensions import db from app.models.customer import Customer from app.models.loan import Loan +from sqlalchemy.exc import IntegrityError class Repayment(db.Model): @@ -37,8 +38,13 @@ class Repayment(db.Model): product_id=product_id, ) - db.session.add(repayment) - db.session.commit() + try: + db.session.add(repayment) + db.session.commit() + except IntegrityError as err: + db.session.rollback() + raise ValueError(f"Database integrity error: {err}") + return repayment def __repr__(self): From 1081467f6fba6234a6f9557451745d2249680c3f Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Thu, 10 Apr 2025 17:50:25 +0100 Subject: [PATCH 3/4] [add]: DB migrations fix and Save loan repayment details --- app/api/services/provide_loan.py | 18 ++-- app/api/services/repayment.py | 16 ++++ app/models/__init__.py | 3 +- app/swagger/schemas/RepaymentRequest.json | 4 +- ...8_migration_on_thu_apr_10_16_21_45_utc_.py | 86 +++++++++++++++++++ scripts/entrypoint.sh | 1 + 6 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 migrations/versions/b8f6fd76ead8_migration_on_thu_apr_10_16_21_45_utc_.py diff --git a/app/api/services/provide_loan.py b/app/api/services/provide_loan.py index bfb4f05..495c07b 100644 --- a/app/api/services/provide_loan.py +++ b/app/api/services/provide_loan.py @@ -33,14 +33,6 @@ class ProvideLoanService(BaseService): if (ProvideLoanService.validate_account_ownership(account_id = account_id, customer_id = customer_id)): - transaction = ProvideLoanService.log_transaction(validated_data = validated_data) - - if not transaction: - logger.error(f"Failed to log transaction") - return jsonify({ - "message": "Failed to log transaction." - }), 400 - # Save the loan details loan = Loan.create_loan( customer_id=customer_id, @@ -55,6 +47,16 @@ class ProvideLoanService(BaseService): return jsonify({ "message": "Failed to save loan details." }), 400 + + # Log Transaction + transaction = ProvideLoanService.log_transaction(validated_data = validated_data) + + if not transaction: + logger.error(f"Failed to log transaction") + return jsonify({ + "message": "Failed to log transaction." + }), 400 + else: diff --git a/app/api/services/repayment.py b/app/api/services/repayment.py index 0efd6f9..3129392 100644 --- a/app/api/services/repayment.py +++ b/app/api/services/repayment.py @@ -1,5 +1,6 @@ from flask import request, jsonify from marshmallow import ValidationError +from app.models import Repayment from app.utils.logger import logger from app.api.schemas.repayment import RepaymentSchema from app.api.services.base_service import BaseService @@ -30,6 +31,21 @@ class RepaymentService(BaseService): if (RepaymentService.validate_account_ownership(account_id = account.id, customer_id = customer_id)): + + # Save the repayment details + repayment = Repayment.create_repayment( + customer_id = customer_id, + loan_id = validated_data.get('debtId'), + product_id = validated_data.get('productId') + + ) + + if not repayment: + logger.error(f"Failed to save repayment details") + return jsonify({ + "message": "Failed to save repayment details." + }), 400 + transaction = RepaymentService.log_transaction(validated_data = validated_data) if not transaction: diff --git a/app/models/__init__.py b/app/models/__init__.py index 8fd1277..af5353a 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -2,5 +2,6 @@ from .customer import Customer from .account import Account from .loan import Loan from .transaction import Transaction +from .repayment import Repayment -__all__ = ['Customer', 'Account', 'Loan', 'Transaction'] \ No newline at end of file +__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment'] \ No newline at end of file diff --git a/app/swagger/schemas/RepaymentRequest.json b/app/swagger/schemas/RepaymentRequest.json index cb959a9..c31d585 100644 --- a/app/swagger/schemas/RepaymentRequest.json +++ b/app/swagger/schemas/RepaymentRequest.json @@ -7,7 +7,7 @@ }, "debtId": { "type": "string", - "example": "273194670" + "example": "10" }, "productId": { "type": "string", @@ -19,7 +19,7 @@ }, "customerId": { "type": "string", - "example": "CN621868" + "example": "CID0000025585" }, "channel": { "type": "string", diff --git a/migrations/versions/b8f6fd76ead8_migration_on_thu_apr_10_16_21_45_utc_.py b/migrations/versions/b8f6fd76ead8_migration_on_thu_apr_10_16_21_45_utc_.py new file mode 100644 index 0000000..69d16e7 --- /dev/null +++ b/migrations/versions/b8f6fd76ead8_migration_on_thu_apr_10_16_21_45_utc_.py @@ -0,0 +1,86 @@ +"""Migration on Thu Apr 10 16:21:45 UTC 2025 + +Revision ID: b8f6fd76ead8 +Revises: +Create Date: 2025-04-10 16:22:15.946157 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'b8f6fd76ead8' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('repayments', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('loan_id', sa.String(length=50), nullable=False), + sa.Column('customer_id', sa.String(length=50), nullable=False), + sa.Column('product_id', sa.String(length=20), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('loans', schema=None) as batch_op: + batch_op.alter_column('id', + existing_type=sa.VARCHAR(length=50), + type_=sa.Integer(), + existing_nullable=False, + autoincrement=True, + existing_server_default=sa.text("nextval('loan_id_seq'::regclass)")) + + with op.batch_alter_table('transactions', schema=None) as batch_op: + batch_op.alter_column('channel', + existing_type=sa.VARCHAR(length=8), + type_=sa.String(length=50), + existing_nullable=False) + batch_op.alter_column('created_at', + existing_type=postgresql.TIMESTAMP(timezone=True), + type_=sa.DateTime(), + existing_nullable=True, + existing_server_default=sa.text('now()')) + batch_op.alter_column('updated_at', + existing_type=postgresql.TIMESTAMP(timezone=True), + type_=sa.DateTime(), + existing_nullable=True, + existing_server_default=sa.text('now()')) + batch_op.drop_constraint('transactions_id_key', type_='unique') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('transactions', schema=None) as batch_op: + batch_op.create_unique_constraint('transactions_id_key', ['id']) + batch_op.alter_column('updated_at', + existing_type=sa.DateTime(), + type_=postgresql.TIMESTAMP(timezone=True), + existing_nullable=True, + existing_server_default=sa.text('now()')) + batch_op.alter_column('created_at', + existing_type=sa.DateTime(), + type_=postgresql.TIMESTAMP(timezone=True), + existing_nullable=True, + existing_server_default=sa.text('now()')) + batch_op.alter_column('channel', + existing_type=sa.String(length=50), + type_=sa.VARCHAR(length=8), + existing_nullable=False) + + with op.batch_alter_table('loans', schema=None) as batch_op: + batch_op.alter_column('id', + existing_type=sa.Integer(), + type_=sa.VARCHAR(length=50), + existing_nullable=False, + autoincrement=True, + existing_server_default=sa.text("nextval('loan_id_seq'::regclass)")) + + op.drop_table('repayments') + # ### end Alembic commands ### diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index 8c7018a..3e73752 100755 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -1,6 +1,7 @@ #!/bin/sh echo "Running DB migrations..." +flask db migrate -m "Migration on $(date)" flask db upgrade echo "Starting Gunicorn server..." From e5320c075e0cfe60c265f68a00fd161f5c7467d6 Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Thu, 10 Apr 2025 18:35:26 +0100 Subject: [PATCH 4/4] [add]: Update loan status after repayment --- app/api/enums/__init__.py | 3 ++- app/api/enums/loan_status.py | 6 ++++++ app/api/services/provide_loan.py | 4 ++-- app/api/services/repayment.py | 8 +++++++- app/models/loan.py | 25 ++++++++++++++++++++++--- app/models/repayment.py | 8 ++++++-- 6 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 app/api/enums/loan_status.py diff --git a/app/api/enums/__init__.py b/app/api/enums/__init__.py index 2e3a1d8..deb3f94 100644 --- a/app/api/enums/__init__.py +++ b/app/api/enums/__init__.py @@ -1 +1,2 @@ -from .transaction_type import TransactionType \ No newline at end of file +from .transaction_type import TransactionType +from .loan_status import LoanStatus \ No newline at end of file diff --git a/app/api/enums/loan_status.py b/app/api/enums/loan_status.py new file mode 100644 index 0000000..dace234 --- /dev/null +++ b/app/api/enums/loan_status.py @@ -0,0 +1,6 @@ +from enum import Enum + +class LoanStatus(str, Enum): + PENDING = "pending" + ACTIVE = "active" + REPAID = "repaid" \ No newline at end of file diff --git a/app/api/services/provide_loan.py b/app/api/services/provide_loan.py index 495c07b..e268a92 100644 --- a/app/api/services/provide_loan.py +++ b/app/api/services/provide_loan.py @@ -7,7 +7,7 @@ from app.utils.logger import logger from app.api.schemas.provide_loan import ProvideLoanSchema from threading import Thread from app.models.loan import Loan - +from app.api.enums import LoanStatus class ProvideLoanService(BaseService): TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN @@ -39,7 +39,7 @@ class ProvideLoanService(BaseService): account_id=account_id, offer_id=validated_data.get('offerId'), principal_amount=validated_data.get('requestedAmount'), - status="active" + status=LoanStatus.ACTIVE ) if not loan: diff --git a/app/api/services/repayment.py b/app/api/services/repayment.py index 3129392..d92f8b8 100644 --- a/app/api/services/repayment.py +++ b/app/api/services/repayment.py @@ -1,6 +1,8 @@ from flask import request, jsonify from marshmallow import ValidationError +from app.api.enums.loan_status import LoanStatus from app.models import Repayment +from app.models.loan import Loan from app.utils.logger import logger from app.api.schemas.repayment import RepaymentSchema from app.api.services.base_service import BaseService @@ -28,6 +30,7 @@ class RepaymentService(BaseService): account = customer.accounts[0] validated_data['accountId'] = account.id request_id = validated_data.get('requestId') + loan_id = validated_data.get('debtId') if (RepaymentService.validate_account_ownership(account_id = account.id, customer_id = customer_id)): @@ -35,7 +38,7 @@ class RepaymentService(BaseService): # Save the repayment details repayment = Repayment.create_repayment( customer_id = customer_id, - loan_id = validated_data.get('debtId'), + loan_id = loan_id, product_id = validated_data.get('productId') ) @@ -46,6 +49,9 @@ class RepaymentService(BaseService): "message": "Failed to save repayment details." }), 400 + #Update Loan status + Loan.update_status(loan_id = loan_id, status = LoanStatus.REPAID) + transaction = RepaymentService.log_transaction(validated_data = validated_data) if not transaction: diff --git a/app/models/loan.py b/app/models/loan.py index 2d68a64..0fec3d2 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -65,16 +65,35 @@ class Loan(db.Model): return False return True + @classmethod def get_customer_loan(cls, loan_id, customer_id): """ - Check if a loan with the given ID exists and if the loan belongs to the specified customer_id. + Get customer's active loans. """ - loan = cls.query.filter_by(id=loan_id, customer_id=customer_id).first() + loan = cls.query.filter_by(id = loan_id, customer_id = customer_id).first() if not loan: - raise ValueError(f"Loan with ID {loan_id} does not exist or does not belong to customer {customer_id}.") + raise ValueError(f"Loan with ID {loan_id} does not exist or does not belong to customer {customer_id}.") return loan + @classmethod + def update_status(cls, loan_id, status): + """ + Update the status of the loan with the given loan_id. + """ + # Retrieve loan + loan = cls.query.get(loan_id) + + if not loan: + raise ValueError(f"Loan with ID {loan_id} does not exist.") + + if loan.status == status: + return + + # Update loan status and the updated_at timestamp + loan.status = status + + db.session.commit() def __repr__(self): return f'' \ No newline at end of file diff --git a/app/models/repayment.py b/app/models/repayment.py index 538eda3..06b872c 100644 --- a/app/models/repayment.py +++ b/app/models/repayment.py @@ -1,4 +1,5 @@ from datetime import datetime, timezone +from app.api.enums.loan_status import LoanStatus from app.extensions import db from app.models.customer import Customer from app.models.loan import Loan @@ -29,8 +30,11 @@ class Repayment(db.Model): # Check loan exists loan = Loan.get_customer_loan(loan_id = loan_id, customer_id = customer_id) - if not loan: - raise ValueError("Loan not found for customer") + + # Check that the loan is active + if loan.status != LoanStatus.ACTIVE: + raise ValueError(f"Repayment cannot be processed. Loan status: ({loan.status})") + repayment = cls( customer_id=customer_id,