From 65efe0573a7fcb550d33286b318bfa43cf750d31 Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Fri, 28 Mar 2025 08:38:32 +0100 Subject: [PATCH 01/13] [add]: Database connection and migrations --- .example.env | 21 +++++ Dockerfile | 5 +- README.md | 36 ++++++--- app/__init__.py | 21 ++++- app/api/services/account_service.py | 15 ++++ app/api/services/customer_service.py | 7 ++ app/api/services/loan_service.py | 10 +++ app/config.py | 14 +++- app/models/__init__.py | 6 ++ app/models/account.py | 15 ++++ app/models/customer.py | 12 +++ app/models/loan.py | 16 ++++ app/models/offer.py | 12 +++ app/models/transaction.py | 14 ++++ docker-compose.yml | 33 +++++++- migrations/README | 1 + migrations/alembic.ini | 50 ++++++++++++ migrations/env.py | 113 +++++++++++++++++++++++++++ migrations/script.py.mako | 24 ++++++ requirements.txt | 18 +++++ scripts/entrypoint.sh | 7 ++ 21 files changed, 431 insertions(+), 19 deletions(-) create mode 100644 .example.env create mode 100644 app/api/services/account_service.py create mode 100644 app/api/services/customer_service.py create mode 100644 app/api/services/loan_service.py create mode 100644 app/models/__init__.py create mode 100644 app/models/account.py create mode 100644 app/models/customer.py create mode 100644 app/models/loan.py create mode 100644 app/models/offer.py create mode 100644 app/models/transaction.py create mode 100644 migrations/README create mode 100644 migrations/alembic.ini create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100755 scripts/entrypoint.sh diff --git a/.example.env b/.example.env new file mode 100644 index 0000000..4878aad --- /dev/null +++ b/.example.env @@ -0,0 +1,21 @@ +VALID_APP_ID=********** +VALID_API_KEY=************* +BASIC_AUTH_USERNAME=****** +BASIC_AUTH_PASSWORD=****** + + +SWAGGER_URL="/documentation" +API_URL="/swagger.json" + + + +DATABASE_USER=***** +DATABASE_PASSWORD=***** +DATABASE_HOST=****** +DATABASE_PORT=****** +DATABASE_NAME=***** + +# Flask Configuration +FLASK_APP=wsgi.py +FLASK_ENV=development +APP_PORT=4500 diff --git a/Dockerfile b/Dockerfile index fa0ff11..0bc8e59 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,5 +17,6 @@ EXPOSE 5000 ENV FLASK_APP=app.py ENV FLASK_RUN_HOST=0.0.0.0 -# Run the application -CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "wsgi:wsgi_app"] \ No newline at end of file +RUN chmod +x scripts/entrypoint.sh + +ENTRYPOINT ["scripts/entrypoint.sh"] \ No newline at end of file diff --git a/README.md b/README.md index 7911c12..76963e2 100644 --- a/README.md +++ b/README.md @@ -20,25 +20,39 @@ cd repository ``` -### 2. Create a `.env` File +### 2. Configure Environment Variables -Before running the application, create a `.env` file in the root directory and add the required environment variables: +Instead of creating a new `.env` file, rename the provided `.env.example` file and update the necessary variables: ```bash -touch .env +cp .env.example .env ``` -Then, open the `.env` file and add the following: +Then, open the `.env` file and **update the following variables with your actual configuration:** -```ini -# Environment Variables -BASIC_AUTH_USERNAME=user -BASIC_AUTH_PASSWORD=password -SWAGGER_URL="/documentation" -API_URL="/swagger.json" +- Database credentials: + ```ini + DATABASE_USER=your_database_username + DATABASE_PASSWORD=your_database_password + DATABASE_NAME=your_database_name + DATABASE_PORT=5432 + ``` +- App ID and API Key: + ```ini + APP_ID=your_app_id + API_KEY=your_api_key + ``` + +This ensures that the application is properly configured with your environment variables. + +> **Optional:** Change file permissions for the entrypoint script + +```bash +chmod +x scripts/entrypoint.sh ``` -This ensures that the application uses secure API keys and app IDs. +--- + ### 3. Run the Application with Docker Compose diff --git a/app/__init__.py b/app/__init__.py index e7b7836..2a8d34d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -5,6 +5,11 @@ from flask_cors import CORS from app.config import Config from app.api.routes import api from app.errors import register_error_handlers +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate + +db = SQLAlchemy() +migrate = Migrate() def create_app(): """ Factory function to create a Flask app instance """ @@ -31,7 +36,21 @@ def create_app(): # Error Handlers register_error_handlers(app) - + import logging + from sqlalchemy import create_engine + + # Set up logging + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + + # Log the database URI + logger.info(f"Database URI: {app.config['SQLALCHEMY_DATABASE_URI']}") + + # Database and Migrations + db.init_app(app) + + + migrate.init_app(app, db) return app diff --git a/app/api/services/account_service.py b/app/api/services/account_service.py new file mode 100644 index 0000000..a452b17 --- /dev/null +++ b/app/api/services/account_service.py @@ -0,0 +1,15 @@ +from app.models import Account + +def check_account_settings(account_id, customer_id): + """ + Checks if the account belongs to the customer and if it has existing liens. + """ + account = Account.query.filter_by(id=account_id, customer_id=customer_id).first() + + if not account: + return False, "Account not found or doesn't belong to customer" + + if account.lien_amount > 0: + return False, "Account has an existing lien" + + return True, "Account is valid" \ No newline at end of file diff --git a/app/api/services/customer_service.py b/app/api/services/customer_service.py new file mode 100644 index 0000000..582aa1e --- /dev/null +++ b/app/api/services/customer_service.py @@ -0,0 +1,7 @@ +from app.models import Customer + +def check_customer_eligibility(data): + # Verify customer exists + customer = Customer.query.filter_by(id=data['customerId']).first() + if not customer: + return False, "Customer not found" \ No newline at end of file diff --git a/app/api/services/loan_service.py b/app/api/services/loan_service.py new file mode 100644 index 0000000..3beb98c --- /dev/null +++ b/app/api/services/loan_service.py @@ -0,0 +1,10 @@ + +from app.models import Loan + +def check_active_loans(data): + active_loans = Loan.query.filter_by( + customer_id=data['customerId'], + status='active' + ).count() + if active_loans > 0: + return False, "Customer has active loans" \ No newline at end of file diff --git a/app/config.py b/app/config.py index 9d37a22..e237b8c 100644 --- a/app/config.py +++ b/app/config.py @@ -14,4 +14,16 @@ class Config: VALID_APP_ID = os.getenv("VALID_APP_ID", "app1") VALID_API_KEY = os.getenv("VALID_API_KEY", "test-api-key-12345") BASIC_AUTH_USERNAME = os.environ.get("BASIC_AUTH_USERNAME", "user") - BASIC_AUTH_PASSWORD = os.environ.get("BASIC_AUTH_PASSWORD", "password") \ No newline at end of file + BASIC_AUTH_PASSWORD = os.environ.get("BASIC_AUTH_PASSWORD", "password") + + DATABASE_USER = os.environ.get("DATABASE_USER") + DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD") + DATABASE_HOST = os.environ.get("DATABASE_HOST") + DATABASE_PORT = os.environ.get("DATABASE_PORT", 10532) + DATABASE_NAME = os.environ.get("DATABASE_NAME") + + SQLALCHEMY_DATABASE_URI = ( + # f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}" + f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}?options=-csearch_path%3Dflask_app" + ) + SQLALCHEMY_TRACK_MODIFICATIONS = False \ No newline at end of file diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..8fd1277 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,6 @@ +from .customer import Customer +from .account import Account +from .loan import Loan +from .transaction import Transaction + +__all__ = ['Customer', 'Account', 'Loan', 'Transaction'] \ No newline at end of file diff --git a/app/models/account.py b/app/models/account.py new file mode 100644 index 0000000..0b11842 --- /dev/null +++ b/app/models/account.py @@ -0,0 +1,15 @@ +from datetime import datetime +from app import db + +class Account(db.Model): + id = db.Column(db.String(50), primary_key=True) + customer_id = db.Column(db.String(50), db.ForeignKey('customer.id'), nullable=False) + account_type = db.Column(db.String(50)) + status = db.Column(db.String(20), default='active') + lien_amount = db.Column(db.Float, default=0.0) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def __repr__(self): + return f'' + \ No newline at end of file diff --git a/app/models/customer.py b/app/models/customer.py new file mode 100644 index 0000000..eb83fb3 --- /dev/null +++ b/app/models/customer.py @@ -0,0 +1,12 @@ +from datetime import datetime +from app import db + +class Customer(db.Model): + id = db.Column(db.String(50), primary_key=True) + msisdn = db.Column(db.String(20), unique=True, nullable=False) + country_code = db.Column(db.String(3), nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def __repr__(self): + return f'' \ No newline at end of file diff --git a/app/models/loan.py b/app/models/loan.py new file mode 100644 index 0000000..4774508 --- /dev/null +++ b/app/models/loan.py @@ -0,0 +1,16 @@ +from datetime import datetime +from app import db + + +class Loan(db.Model): + id = db.Column(db.String(50), primary_key=True) + customer_id = db.Column(db.String(50), db.ForeignKey('customer.id'), nullable=False) + account_id = db.Column(db.String(50), db.ForeignKey('account.id'), nullable=False) + product_id = db.Column(db.String(20), nullable=False) + principal_amount = db.Column(db.Float, nullable=False) + status = db.Column(db.String(20), default='pending') + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def __repr__(self): + return f'' \ No newline at end of file diff --git a/app/models/offer.py b/app/models/offer.py new file mode 100644 index 0000000..75dac3d --- /dev/null +++ b/app/models/offer.py @@ -0,0 +1,12 @@ +from datetime import datetime +from app import db + +class Offer(db.Model): + id = db.Column(db.Integer, primary_key=True) + amount = db.Column(db.Float, nullable=False) + interest_rate = db.Column(db.Float, nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def __repr__(self): + return f'' \ No newline at end of file diff --git a/app/models/transaction.py b/app/models/transaction.py new file mode 100644 index 0000000..1a1cfd3 --- /dev/null +++ b/app/models/transaction.py @@ -0,0 +1,14 @@ +from datetime import datetime +from app import db + +class Transaction(db.Model): + id = db.Column(db.String(50), primary_key=True) + account_id = db.Column(db.String(50), db.ForeignKey('account.id'), nullable=False) + type = db.Column(db.String(50), nullable=False) + amount = db.Column(db.Float, nullable=False) + status = db.Column(db.String(20), default='pending') + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def __repr__(self): + return f'' \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index e3765f2..80445eb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,36 @@ +version: '3.8' + services: - digifi-flaska002: + db: + image: postgres:13 + environment: + - POSTGRES_USER=${DATABASE_USER} + - POSTGRES_PASSWORD=${DATABASE_PASSWORD} + - POSTGRES_DB=${DATABASE_NAME} + ports: + - "${DATABASE_PORT}:${DATABASE_PORT}" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USER} -d ${DATABASE_NAME}"] + interval: 5s + timeout: 5s + retries: 5 + + digifi-bank-to-product-core: build: . ports: - - "4500:5000" + - "${APP_PORT:-4500}:5000" environment: - - FLASK_APP=app.py - - FLASK_RUN_HOST=0.0.0.0 + - FLASK_APP=wsgi.py + - FLASK_ENV=development + - DATABASE_URL=postgresql+psycopg2://${DATABASE_USER}:${DATABASE_PASSWORD}@db:${DATABASE_PORT}/${DATABASE_NAME}?options=-csearch_path%3Dflask_app volumes: - .:/app + depends_on: + db: + condition: service_healthy restart: always + +volumes: + postgres_data: \ No newline at end of file diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/requirements.txt b/requirements.txt index d5cb30f..3c9ceeb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,29 @@ # Flask and Extensions Flask==2.3.3 + +# Schema for validations Flask-Marshmallow==0.15.0 marshmallow==3.19.0 + +# CORS Flask-Cors==3.0.10 + +# Deployment gunicorn + +# Swagger flask-swagger-ui +# Database +flask-sqlalchemy +flask-migrate +psycopg2-binary +alembic + + +# Env +python-dotenv + diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh new file mode 100755 index 0000000..8c7018a --- /dev/null +++ b/scripts/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +echo "Running DB migrations..." +flask db upgrade + +echo "Starting Gunicorn server..." +exec gunicorn -w 4 -b 0.0.0.0:5000 wsgi:wsgi_app From 50647a566b6d49d43708f77ae04127ac0d17f881 Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Fri, 28 Mar 2025 08:41:07 +0100 Subject: [PATCH 02/13] Update __init__.py --- app/__init__.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 2a8d34d..2c29158 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -35,16 +35,7 @@ def create_app(): # Error Handlers register_error_handlers(app) - - import logging - from sqlalchemy import create_engine - - # Set up logging - logging.basicConfig(level=logging.INFO) - logger = logging.getLogger(__name__) - - # Log the database URI - logger.info(f"Database URI: {app.config['SQLALCHEMY_DATABASE_URI']}") + # Database and Migrations db.init_app(app) From b4e541ceb91e3516ea52671aac361097102670bc Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Fri, 28 Mar 2025 09:12:13 +0100 Subject: [PATCH 03/13] [update]: schema in models --- app/models/account.py | 3 + app/models/customer.py | 3 + app/models/loan.py | 3 + app/models/offer.py | 3 + app/models/transaction.py | 3 + migrations/env.py | 113 -------------------------------------- 6 files changed, 15 insertions(+), 113 deletions(-) delete mode 100644 migrations/env.py diff --git a/app/models/account.py b/app/models/account.py index 0b11842..1f52087 100644 --- a/app/models/account.py +++ b/app/models/account.py @@ -2,6 +2,9 @@ from datetime import datetime from app import db class Account(db.Model): + __tablename__ = 'accounts' + __table_args__ = {'schema': 'flask_app'} + id = db.Column(db.String(50), primary_key=True) customer_id = db.Column(db.String(50), db.ForeignKey('customer.id'), nullable=False) account_type = db.Column(db.String(50)) diff --git a/app/models/customer.py b/app/models/customer.py index eb83fb3..3f70a8f 100644 --- a/app/models/customer.py +++ b/app/models/customer.py @@ -2,6 +2,9 @@ from datetime import datetime from app import db class Customer(db.Model): + __tablename__ = 'customers' + __table_args__ = {'schema': 'flask_app'} + id = db.Column(db.String(50), primary_key=True) msisdn = db.Column(db.String(20), unique=True, nullable=False) country_code = db.Column(db.String(3), nullable=False) diff --git a/app/models/loan.py b/app/models/loan.py index 4774508..879834a 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -3,6 +3,9 @@ from app import db class Loan(db.Model): + __tablename__ = 'loans' + __table_args__ = {'schema': 'flask_app'} + id = db.Column(db.String(50), primary_key=True) customer_id = db.Column(db.String(50), db.ForeignKey('customer.id'), nullable=False) account_id = db.Column(db.String(50), db.ForeignKey('account.id'), nullable=False) diff --git a/app/models/offer.py b/app/models/offer.py index 75dac3d..613b8d2 100644 --- a/app/models/offer.py +++ b/app/models/offer.py @@ -2,6 +2,9 @@ from datetime import datetime from app import db class Offer(db.Model): + __tablename__ = 'offers' + __table_args__ = {'schema': 'flask_app'} + id = db.Column(db.Integer, primary_key=True) amount = db.Column(db.Float, nullable=False) interest_rate = db.Column(db.Float, nullable=False) diff --git a/app/models/transaction.py b/app/models/transaction.py index 1a1cfd3..73bacf8 100644 --- a/app/models/transaction.py +++ b/app/models/transaction.py @@ -2,6 +2,9 @@ from datetime import datetime from app import db class Transaction(db.Model): + __tablename__ = 'transactions' + __table_args__ = {'schema': 'flask_app'} + id = db.Column(db.String(50), primary_key=True) account_id = db.Column(db.String(50), db.ForeignKey('account.id'), nullable=False) type = db.Column(db.String(50), nullable=False) diff --git a/migrations/env.py b/migrations/env.py deleted file mode 100644 index 4c97092..0000000 --- a/migrations/env.py +++ /dev/null @@ -1,113 +0,0 @@ -import logging -from logging.config import fileConfig - -from flask import current_app - -from alembic import context - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -fileConfig(config.config_file_name) -logger = logging.getLogger('alembic.env') - - -def get_engine(): - try: - # this works with Flask-SQLAlchemy<3 and Alchemical - return current_app.extensions['migrate'].db.get_engine() - except (TypeError, AttributeError): - # this works with Flask-SQLAlchemy>=3 - return current_app.extensions['migrate'].db.engine - - -def get_engine_url(): - try: - return get_engine().url.render_as_string(hide_password=False).replace( - '%', '%%') - except AttributeError: - return str(get_engine().url).replace('%', '%%') - - -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata -config.set_main_option('sqlalchemy.url', get_engine_url()) -target_db = current_app.extensions['migrate'].db - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - - -def get_metadata(): - if hasattr(target_db, 'metadatas'): - return target_db.metadatas[None] - return target_db.metadata - - -def run_migrations_offline(): - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, target_metadata=get_metadata(), literal_binds=True - ) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online(): - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - - # this callback is used to prevent an auto-migration from being generated - # when there are no changes to the schema - # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html - def process_revision_directives(context, revision, directives): - if getattr(config.cmd_opts, 'autogenerate', False): - script = directives[0] - if script.upgrade_ops.is_empty(): - directives[:] = [] - logger.info('No changes in schema detected.') - - conf_args = current_app.extensions['migrate'].configure_args - if conf_args.get("process_revision_directives") is None: - conf_args["process_revision_directives"] = process_revision_directives - - connectable = get_engine() - - with connectable.connect() as connection: - context.configure( - connection=connection, - target_metadata=get_metadata(), - **conf_args - ) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() From ad39b1c75ca04900c2e1d2ee8ce2988739abbd10 Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Fri, 28 Mar 2025 09:56:27 +0100 Subject: [PATCH 04/13] [update] Remove foreign key constraints --- .gitignore | 4 +- app/__init__.py | 3 +- app/models/account.py | 11 +- app/models/customer.py | 1 + app/models/loan.py | 4 +- app/models/transaction.py | 2 +- migrations/env.py | 113 ++++++++++++++++++ .../versions/4a12e1b143a4_create_tables.py | 74 ++++++++++++ 8 files changed, 206 insertions(+), 6 deletions(-) create mode 100644 migrations/env.py create mode 100644 migrations/versions/4a12e1b143a4_create_tables.py diff --git a/.gitignore b/.gitignore index b316843..20bea99 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ __pycache__/ .env app.log -.DS_Store \ No newline at end of file +.DS_Store +migrations/__pycache__/ +migrations/*.pycg \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py index 2c29158..732af48 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -8,6 +8,7 @@ from app.errors import register_error_handlers from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate + db = SQLAlchemy() migrate = Migrate() @@ -36,7 +37,7 @@ def create_app(): # Error Handlers register_error_handlers(app) - + from . import models # Database and Migrations db.init_app(app) diff --git a/app/models/account.py b/app/models/account.py index 1f52087..1c65985 100644 --- a/app/models/account.py +++ b/app/models/account.py @@ -6,13 +6,22 @@ class Account(db.Model): __table_args__ = {'schema': 'flask_app'} id = db.Column(db.String(50), primary_key=True) - customer_id = db.Column(db.String(50), db.ForeignKey('customer.id'), nullable=False) + customer_id = db.Column(db.String(50), nullable=False) account_type = db.Column(db.String(50)) status = db.Column(db.String(20), default='active') lien_amount = db.Column(db.Float, default=0.0) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + # Database relationship + # customer = db.relationship( + # 'Customer', + # primaryjoin='Account.customer_id == Customer.id', + # backref='accounts', + # foreign_keys=[customer_id], + # viewonly=True + # ) + def __repr__(self): return f'' \ No newline at end of file diff --git a/app/models/customer.py b/app/models/customer.py index 3f70a8f..54a7807 100644 --- a/app/models/customer.py +++ b/app/models/customer.py @@ -11,5 +11,6 @@ class Customer(db.Model): created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + def __repr__(self): return f'' \ No newline at end of file diff --git a/app/models/loan.py b/app/models/loan.py index 879834a..ce26cd8 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -7,8 +7,8 @@ class Loan(db.Model): __table_args__ = {'schema': 'flask_app'} id = db.Column(db.String(50), primary_key=True) - customer_id = db.Column(db.String(50), db.ForeignKey('customer.id'), nullable=False) - account_id = db.Column(db.String(50), db.ForeignKey('account.id'), nullable=False) + customer_id = db.Column(db.String(50), nullable=False) + account_id = db.Column(db.String(50), nullable=False) product_id = db.Column(db.String(20), nullable=False) principal_amount = db.Column(db.Float, nullable=False) status = db.Column(db.String(20), default='pending') diff --git a/app/models/transaction.py b/app/models/transaction.py index 73bacf8..1bea982 100644 --- a/app/models/transaction.py +++ b/app/models/transaction.py @@ -6,7 +6,7 @@ class Transaction(db.Model): __table_args__ = {'schema': 'flask_app'} id = db.Column(db.String(50), primary_key=True) - account_id = db.Column(db.String(50), db.ForeignKey('account.id'), nullable=False) + account_id = db.Column(db.String(50), nullable=False) type = db.Column(db.String(50), nullable=False) amount = db.Column(db.Float, nullable=False) status = db.Column(db.String(20), default='pending') diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/versions/4a12e1b143a4_create_tables.py b/migrations/versions/4a12e1b143a4_create_tables.py new file mode 100644 index 0000000..c99e599 --- /dev/null +++ b/migrations/versions/4a12e1b143a4_create_tables.py @@ -0,0 +1,74 @@ +"""Create tables + +Revision ID: 4a12e1b143a4 +Revises: +Create Date: 2025-03-28 09:24:49.669509 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '4a12e1b143a4' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('accounts', + sa.Column('id', sa.String(length=50), nullable=False), + sa.Column('customer_id', sa.String(length=50), nullable=False), + sa.Column('account_type', sa.String(length=50), nullable=True), + sa.Column('status', sa.String(length=20), nullable=True), + sa.Column('lien_amount', sa.Float(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + schema='flask_app' + ) + op.create_table('customers', + sa.Column('id', sa.String(length=50), nullable=False), + sa.Column('msisdn', sa.String(length=20), nullable=False), + sa.Column('country_code', sa.String(length=3), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('msisdn'), + schema='flask_app' + ) + op.create_table('loans', + sa.Column('id', sa.String(length=50), nullable=False), + sa.Column('customer_id', sa.String(length=50), nullable=False), + sa.Column('account_id', sa.String(length=50), nullable=False), + sa.Column('product_id', sa.String(length=20), nullable=False), + sa.Column('principal_amount', sa.Float(), nullable=False), + sa.Column('status', 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'), + schema='flask_app' + ) + op.create_table('transactions', + sa.Column('id', sa.String(length=50), nullable=False), + sa.Column('account_id', sa.String(length=50), nullable=False), + sa.Column('type', sa.String(length=50), nullable=False), + sa.Column('amount', sa.Float(), nullable=False), + sa.Column('status', 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'), + schema='flask_app' + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('transactions', schema='flask_app') + op.drop_table('loans', schema='flask_app') + op.drop_table('customers', schema='flask_app') + op.drop_table('accounts', schema='flask_app') + # ### end Alembic commands ### From bc8f8e2cdd68b538e59f13eb3ab912f0fd6ccec3 Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Fri, 28 Mar 2025 10:11:44 +0100 Subject: [PATCH 05/13] [update]: model class methods --- app/api/services/account_service.py | 15 --------------- app/api/services/customer_service.py | 7 ------- app/api/services/loan_service.py | 10 ---------- app/models/account.py | 9 +++++++++ app/models/customer.py | 9 ++++++++- app/models/loan.py | 13 +++++++++++++ 6 files changed, 30 insertions(+), 33 deletions(-) delete mode 100644 app/api/services/account_service.py delete mode 100644 app/api/services/customer_service.py delete mode 100644 app/api/services/loan_service.py diff --git a/app/api/services/account_service.py b/app/api/services/account_service.py deleted file mode 100644 index a452b17..0000000 --- a/app/api/services/account_service.py +++ /dev/null @@ -1,15 +0,0 @@ -from app.models import Account - -def check_account_settings(account_id, customer_id): - """ - Checks if the account belongs to the customer and if it has existing liens. - """ - account = Account.query.filter_by(id=account_id, customer_id=customer_id).first() - - if not account: - return False, "Account not found or doesn't belong to customer" - - if account.lien_amount > 0: - return False, "Account has an existing lien" - - return True, "Account is valid" \ No newline at end of file diff --git a/app/api/services/customer_service.py b/app/api/services/customer_service.py deleted file mode 100644 index 582aa1e..0000000 --- a/app/api/services/customer_service.py +++ /dev/null @@ -1,7 +0,0 @@ -from app.models import Customer - -def check_customer_eligibility(data): - # Verify customer exists - customer = Customer.query.filter_by(id=data['customerId']).first() - if not customer: - return False, "Customer not found" \ No newline at end of file diff --git a/app/api/services/loan_service.py b/app/api/services/loan_service.py deleted file mode 100644 index 3beb98c..0000000 --- a/app/api/services/loan_service.py +++ /dev/null @@ -1,10 +0,0 @@ - -from app.models import Loan - -def check_active_loans(data): - active_loans = Loan.query.filter_by( - customer_id=data['customerId'], - status='active' - ).count() - if active_loans > 0: - return False, "Customer has active loans" \ No newline at end of file diff --git a/app/models/account.py b/app/models/account.py index 1c65985..ebe299c 100644 --- a/app/models/account.py +++ b/app/models/account.py @@ -22,6 +22,15 @@ class Account(db.Model): # viewonly=True # ) + @classmethod + def is_valid_account(cls, account_id, customer_id): + account = cls.query.filter_by(id=account_id, customer_id=customer_id).first() + if not account: + return False, "Account not found or doesn't belong to customer" + if account.lien_amount > 0: + return False, "Account has an existing lien" + return True, "Account is valid" + def __repr__(self): return f'' \ No newline at end of file diff --git a/app/models/customer.py b/app/models/customer.py index 54a7807..6dc5f7e 100644 --- a/app/models/customer.py +++ b/app/models/customer.py @@ -11,6 +11,13 @@ class Customer(db.Model): created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + @classmethod + def is_eligible(cls, customer_id): + customer = cls.query.filter_by(id=customer_id).first() + if not customer: + return False, "Customer not found" + return True, "Customer is eligible" def __repr__(self): - return f'' \ No newline at end of file + return f'' + diff --git a/app/models/loan.py b/app/models/loan.py index ce26cd8..1c27bec 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -15,5 +15,18 @@ class Loan(db.Model): created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + @classmethod + def has_active_loans(cls, customer_id): + active_loans = cls.query.filter_by( + customer_id=customer_id, + status='active' + ).count() + + if active_loans > 0: + return False, "Customer has active loans" + return True, "No active loans" + + def __repr__(self): return f'' \ No newline at end of file From 68ad9e35a1862749eab4b7a5599549f1d74b9a13 Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:49:37 +0100 Subject: [PATCH 06/13] [update]: Database schema --- app/config.py | 2 +- app/models/account.py | 7 ++-- app/models/customer.py | 7 ++-- app/models/loan.py | 7 ++-- app/models/offer.py | 13 ++++---- app/models/transaction.py | 7 ++-- docker-compose.yml | 4 +-- ...ables.py => fd58e10e4968_update_offers.py} | 33 ++++++++++--------- 8 files changed, 38 insertions(+), 42 deletions(-) rename migrations/versions/{4a12e1b143a4_create_tables.py => fd58e10e4968_update_offers.py} (79%) diff --git a/app/config.py b/app/config.py index e237b8c..85ded3a 100644 --- a/app/config.py +++ b/app/config.py @@ -24,6 +24,6 @@ class Config: SQLALCHEMY_DATABASE_URI = ( # f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}" - f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}?options=-csearch_path%3Dflask_app" + f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}" ) SQLALCHEMY_TRACK_MODIFICATIONS = False \ No newline at end of file diff --git a/app/models/account.py b/app/models/account.py index ebe299c..3a66113 100644 --- a/app/models/account.py +++ b/app/models/account.py @@ -1,17 +1,16 @@ -from datetime import datetime +from datetime import datetime, timezone from app import db class Account(db.Model): __tablename__ = 'accounts' - __table_args__ = {'schema': 'flask_app'} id = db.Column(db.String(50), primary_key=True) customer_id = db.Column(db.String(50), nullable=False) account_type = db.Column(db.String(50)) status = db.Column(db.String(20), default='active') lien_amount = db.Column(db.Float, default=0.0) - created_at = db.Column(db.DateTime, default=datetime.utcnow) - updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + 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)) # Database relationship # customer = db.relationship( diff --git a/app/models/customer.py b/app/models/customer.py index 6dc5f7e..aa31073 100644 --- a/app/models/customer.py +++ b/app/models/customer.py @@ -1,15 +1,14 @@ -from datetime import datetime +from datetime import datetime, timezone from app import db class Customer(db.Model): __tablename__ = 'customers' - __table_args__ = {'schema': 'flask_app'} id = db.Column(db.String(50), primary_key=True) msisdn = db.Column(db.String(20), unique=True, nullable=False) country_code = db.Column(db.String(3), nullable=False) - created_at = db.Column(db.DateTime, default=datetime.utcnow) - updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + 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 is_eligible(cls, customer_id): diff --git a/app/models/loan.py b/app/models/loan.py index 1c27bec..5b9557b 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -1,10 +1,9 @@ -from datetime import datetime +from datetime import datetime, timezone from app import db class Loan(db.Model): __tablename__ = 'loans' - __table_args__ = {'schema': 'flask_app'} id = db.Column(db.String(50), primary_key=True) customer_id = db.Column(db.String(50), nullable=False) @@ -12,8 +11,8 @@ class Loan(db.Model): product_id = db.Column(db.String(20), nullable=False) principal_amount = db.Column(db.Float, nullable=False) status = db.Column(db.String(20), default='pending') - created_at = db.Column(db.DateTime, default=datetime.utcnow) - updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + 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 diff --git a/app/models/offer.py b/app/models/offer.py index 613b8d2..47fe6b3 100644 --- a/app/models/offer.py +++ b/app/models/offer.py @@ -1,15 +1,16 @@ -from datetime import datetime +from datetime import datetime, timezone from app import db class Offer(db.Model): __tablename__ = 'offers' - __table_args__ = {'schema': 'flask_app'} id = db.Column(db.Integer, primary_key=True) - amount = db.Column(db.Float, nullable=False) - interest_rate = db.Column(db.Float, nullable=False) - created_at = db.Column(db.DateTime, default=datetime.utcnow) - updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + product_id = db.Column(db.String, nullable=False) + min_amount = db.Column(db.Float, nullable=False) + max_amount = db.Column(db.Float, nullable=False) + tenor = db.Column(db.Integer, nullable=False) + 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)) def __repr__(self): return f'' \ No newline at end of file diff --git a/app/models/transaction.py b/app/models/transaction.py index 1bea982..85317bd 100644 --- a/app/models/transaction.py +++ b/app/models/transaction.py @@ -1,17 +1,16 @@ -from datetime import datetime +from datetime import datetime, timezone from app import db class Transaction(db.Model): __tablename__ = 'transactions' - __table_args__ = {'schema': 'flask_app'} id = db.Column(db.String(50), primary_key=True) account_id = db.Column(db.String(50), nullable=False) type = db.Column(db.String(50), nullable=False) amount = db.Column(db.Float, nullable=False) status = db.Column(db.String(20), default='pending') - created_at = db.Column(db.DateTime, default=datetime.utcnow) - updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + 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)) def __repr__(self): return f'' \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 80445eb..7841806 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: db: image: postgres:13 @@ -24,7 +22,7 @@ services: environment: - FLASK_APP=wsgi.py - FLASK_ENV=development - - DATABASE_URL=postgresql+psycopg2://${DATABASE_USER}:${DATABASE_PASSWORD}@db:${DATABASE_PORT}/${DATABASE_NAME}?options=-csearch_path%3Dflask_app + - DATABASE_URL=postgresql+psycopg2://${DATABASE_USER}:${DATABASE_PASSWORD}@db:${DATABASE_PORT}/${DATABASE_NAME} volumes: - .:/app depends_on: diff --git a/migrations/versions/4a12e1b143a4_create_tables.py b/migrations/versions/fd58e10e4968_update_offers.py similarity index 79% rename from migrations/versions/4a12e1b143a4_create_tables.py rename to migrations/versions/fd58e10e4968_update_offers.py index c99e599..eb2d52b 100644 --- a/migrations/versions/4a12e1b143a4_create_tables.py +++ b/migrations/versions/fd58e10e4968_update_offers.py @@ -1,8 +1,8 @@ -"""Create tables +"""Update Offers -Revision ID: 4a12e1b143a4 +Revision ID: fd58e10e4968 Revises: -Create Date: 2025-03-28 09:24:49.669509 +Create Date: 2025-03-28 15:47:35.620664 """ from alembic import op @@ -10,7 +10,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '4a12e1b143a4' +revision = 'fd58e10e4968' down_revision = None branch_labels = None depends_on = None @@ -26,8 +26,7 @@ def upgrade(): sa.Column('lien_amount', sa.Float(), nullable=True), sa.Column('created_at', sa.DateTime(), nullable=True), sa.Column('updated_at', sa.DateTime(), nullable=True), - sa.PrimaryKeyConstraint('id'), - schema='flask_app' + sa.PrimaryKeyConstraint('id') ) op.create_table('customers', sa.Column('id', sa.String(length=50), nullable=False), @@ -36,8 +35,7 @@ def upgrade(): sa.Column('created_at', sa.DateTime(), nullable=True), sa.Column('updated_at', sa.DateTime(), nullable=True), sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('msisdn'), - schema='flask_app' + sa.UniqueConstraint('msisdn') ) op.create_table('loans', sa.Column('id', sa.String(length=50), nullable=False), @@ -48,8 +46,7 @@ def upgrade(): sa.Column('status', 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'), - schema='flask_app' + sa.PrimaryKeyConstraint('id') ) op.create_table('transactions', sa.Column('id', sa.String(length=50), nullable=False), @@ -59,16 +56,20 @@ def upgrade(): sa.Column('status', 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'), - schema='flask_app' + sa.PrimaryKeyConstraint('id') ) + op.drop_table('test') # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('transactions', schema='flask_app') - op.drop_table('loans', schema='flask_app') - op.drop_table('customers', schema='flask_app') - op.drop_table('accounts', schema='flask_app') + op.create_table('test', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('name', sa.VARCHAR(length=125), autoincrement=False, nullable=True) + ) + op.drop_table('transactions') + op.drop_table('loans') + op.drop_table('customers') + op.drop_table('accounts') # ### end Alembic commands ### From 917a42d7fc0e4c3156efe1b1ed9985b778813849 Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Mon, 31 Mar 2025 10:52:26 +0100 Subject: [PATCH 07/13] [update]: Eligibility check request --- Dockerfile | 2 +- app/__init__.py | 5 +--- app/api/enums/__init__.py | 1 + app/api/enums/transaction_type.py | 6 +++++ app/api/services/base_service.py | 28 +++++++++++++++++++++ app/api/services/eligibility_check.py | 25 ++++++++++--------- app/extensions.py | 5 ++++ app/models/account.py | 20 ++++++++------- app/models/customer.py | 23 ++++++++++++++++-- app/models/loan.py | 2 +- app/models/offer.py | 2 +- app/models/transaction.py | 35 +++++++++++++++++++++++---- 12 files changed, 119 insertions(+), 35 deletions(-) create mode 100644 app/api/enums/__init__.py create mode 100644 app/api/enums/transaction_type.py create mode 100644 app/api/services/base_service.py create mode 100644 app/extensions.py diff --git a/Dockerfile b/Dockerfile index 0bc8e59..8bd47b3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ WORKDIR /app COPY . /app # Install dependencies -RUN pip install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir -r requirements.txt --verbose # Expose port 5000 for the Flask app EXPOSE 5000 diff --git a/app/__init__.py b/app/__init__.py index 732af48..d2fab1c 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -7,10 +7,7 @@ from app.api.routes import api from app.errors import register_error_handlers from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate - - -db = SQLAlchemy() -migrate = Migrate() +from app.extensions import db, migrate def create_app(): """ Factory function to create a Flask app instance """ diff --git a/app/api/enums/__init__.py b/app/api/enums/__init__.py new file mode 100644 index 0000000..cf543fd --- /dev/null +++ b/app/api/enums/__init__.py @@ -0,0 +1 @@ +from .transaction_type import transaction_type \ No newline at end of file diff --git a/app/api/enums/transaction_type.py b/app/api/enums/transaction_type.py new file mode 100644 index 0000000..2f5ac09 --- /dev/null +++ b/app/api/enums/transaction_type.py @@ -0,0 +1,6 @@ +from enum import Enum + +class transaction_type(str, Enum): + ELIGIBILITY_CHECK = "eligibility_check" + PAYMENT = "payment" + REPAYMENT = "repayment" diff --git a/app/api/services/base_service.py b/app/api/services/base_service.py new file mode 100644 index 0000000..02e73c2 --- /dev/null +++ b/app/api/services/base_service.py @@ -0,0 +1,28 @@ +from app.models import Transaction +from app.api.enums import transaction_type +from flask import jsonify +from marshmallow import ValidationError +import logging + +logger = logging.getLogger(__name__) + +class BaseService: + TRANSACTION_TYPE = None + + @classmethod + def log_transaction(cls, data, schema): + logger.info(f"Processing {cls.TRANSACTION_TYPE} request") + + validated_data = schema.load(data) + + transaction = Transaction.create_transaction( + id=validated_data.get("transactionId"), + account_id=validated_data.get("accountId"), + customer_id=validated_data.get("customerId"), + type=cls.TRANSACTION_TYPE, + channel=validated_data.get("channel"), + msisdn=validated_data.get("msisdn"), + country_code=validated_data.get("countryCode") + ) + + return transaction \ No newline at end of file diff --git a/app/api/services/eligibility_check.py b/app/api/services/eligibility_check.py index 2d7cd63..5343a3a 100644 --- a/app/api/services/eligibility_check.py +++ b/app/api/services/eligibility_check.py @@ -1,9 +1,14 @@ from flask import session, jsonify from app.utils.logger import logger +from app.api.services.base_service import BaseService from app.api.schemas.eligibility_check import EligibilityCheckSchema from marshmallow import ValidationError +from app.models import Transaction +from app.api.enums import transaction_type + +class EligibilityCheckService(BaseService): + TRANSACTION_TYPE = transaction_type.ELIGIBILITY_CHECK -class EligibilityCheckService: @staticmethod def process_request(data): """ @@ -16,11 +21,14 @@ class EligibilityCheckService: dict: A standardized response. """ try: - logger.info("Processing EligibilityCheck request") + transaction = EligibilityCheckService.log_transaction(data, EligibilityCheckSchema()) + + if not transaction: + logger.error(f"Customer creation failed") + return jsonify({ + "message": "Customer creation failed." . customer + }), 400 - # Validate input data using Schema - schema = EligibilityCheckSchema() - validated_data = schema.load(data) # Raises an error if invalid offers = [ { @@ -51,13 +59,6 @@ class EligibilityCheckService: "accountId": "ACN8263457" } - - # Return a success response - # return ResponseHelper.success( - # data=response_data, - # message="Eligibility check completed successfully" - # ) - return response_data except ValidationError as err: logger.error(f"Validation Error: {err.messages}") diff --git a/app/extensions.py b/app/extensions.py new file mode 100644 index 0000000..de1947d --- /dev/null +++ b/app/extensions.py @@ -0,0 +1,5 @@ +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate + +db = SQLAlchemy() +migrate = Migrate() \ No newline at end of file diff --git a/app/models/account.py b/app/models/account.py index 3a66113..c4254b9 100644 --- a/app/models/account.py +++ b/app/models/account.py @@ -1,5 +1,5 @@ from datetime import datetime, timezone -from app import db +from app.extensions import db class Account(db.Model): __tablename__ = 'accounts' @@ -12,14 +12,16 @@ class Account(db.Model): 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)) - # Database relationship - # customer = db.relationship( - # 'Customer', - # primaryjoin='Account.customer_id == Customer.id', - # backref='accounts', - # foreign_keys=[customer_id], - # viewonly=True - # ) + @classmethod + def create_account(cls, id, customer_id, account_type, status='active'): + account = cls( + id=id, + customer_id=customer_id, + account_type=account_type + ) + db.session.add(account) + db.session.commit() + return account @classmethod def is_valid_account(cls, account_id, customer_id): diff --git a/app/models/customer.py b/app/models/customer.py index aa31073..b6eb1de 100644 --- a/app/models/customer.py +++ b/app/models/customer.py @@ -1,5 +1,6 @@ from datetime import datetime, timezone -from app import db +from app.extensions import db +from app.models.account import Account class Customer(db.Model): __tablename__ = 'customers' @@ -17,6 +18,24 @@ class Customer(db.Model): return False, "Customer not found" return True, "Customer is eligible" + @classmethod + def create_customer(cls, id, msisdn, country_code, account_id, account_type='savings'): + if cls.query.filter_by(msisdn=msisdn).first(): + return False, "Customer with this MSISDN already exists" + + # 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() + return customer + def __repr__(self): return f'' - diff --git a/app/models/loan.py b/app/models/loan.py index 5b9557b..6fa73a1 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -1,5 +1,5 @@ from datetime import datetime, timezone -from app import db +from app.extensions import db class Loan(db.Model): diff --git a/app/models/offer.py b/app/models/offer.py index 47fe6b3..ce62fa2 100644 --- a/app/models/offer.py +++ b/app/models/offer.py @@ -1,5 +1,5 @@ from datetime import datetime, timezone -from app import db +from app.extensions import db class Offer(db.Model): __tablename__ = 'offers' diff --git a/app/models/transaction.py b/app/models/transaction.py index 85317bd..010b31e 100644 --- a/app/models/transaction.py +++ b/app/models/transaction.py @@ -1,16 +1,41 @@ from datetime import datetime, timezone -from app import db +from app.extensions import db +from app.models import Customer class Transaction(db.Model): __tablename__ = 'transactions' id = db.Column(db.String(50), primary_key=True) - account_id = db.Column(db.String(50), nullable=False) + customer_id = db.Column(db.String(50), nullable=False) type = db.Column(db.String(50), nullable=False) - amount = db.Column(db.Float, nullable=False) - status = db.Column(db.String(20), default='pending') + channel = db.Column(db.String(50), nullable=False) 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)) def __repr__(self): - return f'' \ No newline at end of file + return f'' + + @classmethod + def create_transaction(cls, id, account_id, customer_id, type, channel, msisdn, country_code,): + transaction = cls( + id=id, + customer_id=customer_id, + type=type, + channel=channel + + ) + + customer = Customer.create_customer( + id=customer_id, + msisdn= msisdn, + country_code= country_code, + account_id= account_id, + ) + + db.session.add(transaction) + db.session.commit() + return transaction + + @classmethod + def get_transaction_by_id(cls, transaction_id): + return cls.query.get(transaction_id) \ No newline at end of file From 3160bc30b7ec84c4246543b932e76f7cd8a33105 Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Mon, 31 Mar 2025 11:36:23 +0100 Subject: [PATCH 08/13] [update]: docker compose file --- Dockerfile | 2 +- app/config.py | 6 +----- docker-compose.yml | 30 ++++-------------------------- 3 files changed, 6 insertions(+), 32 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8bd47b3..0bc8e59 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ WORKDIR /app COPY . /app # Install dependencies -RUN pip install --no-cache-dir -r requirements.txt --verbose +RUN pip install --no-cache-dir -r requirements.txt # Expose port 5000 for the Flask app EXPOSE 5000 diff --git a/app/config.py b/app/config.py index 85ded3a..a5609ac 100644 --- a/app/config.py +++ b/app/config.py @@ -3,9 +3,6 @@ import os class Config: """Base configuration for Flask app""" - # SQLALCHEMY_DATABASE_URI = "mysql://root:password@localhost/flask_app" - # SQLALCHEMY_TRACK_MODIFICATIONS = False - # SECRET_KEY = os.environ.get("SECRET_KEY", "your_secret_key") SWAGGER_URL = os.getenv("SWAGGER_URL", "/documentation") API_URL = os.getenv("API_URL", "/swagger.json") @@ -23,7 +20,6 @@ class Config: DATABASE_NAME = os.environ.get("DATABASE_NAME") SQLALCHEMY_DATABASE_URI = ( - # f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}" - f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}" + f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}" ) SQLALCHEMY_TRACK_MODIFICATIONS = False \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 7841806..aca41e0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,34 +1,12 @@ services: - db: - image: postgres:13 - environment: - - POSTGRES_USER=${DATABASE_USER} - - POSTGRES_PASSWORD=${DATABASE_PASSWORD} - - POSTGRES_DB=${DATABASE_NAME} - ports: - - "${DATABASE_PORT}:${DATABASE_PORT}" - volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USER} -d ${DATABASE_NAME}"] - interval: 5s - timeout: 5s - retries: 5 - digifi-bank-to-product-core: build: . ports: - "${APP_PORT:-4500}:5000" environment: - - FLASK_APP=wsgi.py - - FLASK_ENV=development - - DATABASE_URL=postgresql+psycopg2://${DATABASE_USER}:${DATABASE_PASSWORD}@db:${DATABASE_PORT}/${DATABASE_NAME} + - FLASK_APP=${FLASK_APP} + - FLASK_ENV=${FLASK_ENV} + - DATABASE_URL=postgresql+psycopg2://${DATABASE_USER}:${DATABASE_PASSWORD}@${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME} volumes: - .:/app - depends_on: - db: - condition: service_healthy - restart: always - -volumes: - postgres_data: \ No newline at end of file + restart: always \ No newline at end of file From 86a4b4f9b42a40847d44dee2bb50e906f4762687 Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Mon, 31 Mar 2025 12:16:27 +0100 Subject: [PATCH 09/13] [fix]: env in docker config --- app/api/services/eligibility_check.py | 2 +- app/models/transaction.py | 3 +++ docker-compose.yml | 2 ++ requirements.txt | 19 ++++++------------- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/app/api/services/eligibility_check.py b/app/api/services/eligibility_check.py index 5343a3a..a4bffcb 100644 --- a/app/api/services/eligibility_check.py +++ b/app/api/services/eligibility_check.py @@ -60,7 +60,7 @@ class EligibilityCheckService(BaseService): } return response_data - except ValidationError as err: + except (ValidationError, ValueError) as err: logger.error(f"Validation Error: {err.messages}") return jsonify({ "message": "Validation exception" diff --git a/app/models/transaction.py b/app/models/transaction.py index 010b31e..5343e80 100644 --- a/app/models/transaction.py +++ b/app/models/transaction.py @@ -17,6 +17,9 @@ class Transaction(db.Model): @classmethod def create_transaction(cls, id, account_id, customer_id, type, channel, msisdn, country_code,): + if cls.query.filter_by(id=id).first(): + raise ValueError("Transaction with this id already exists") + transaction = cls( id=id, customer_id=customer_id, diff --git a/docker-compose.yml b/docker-compose.yml index aca41e0..ef41164 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,8 @@ services: digifi-bank-to-product-core: build: . + env_file: + - .env ports: - "${APP_PORT:-4500}:5000" environment: diff --git a/requirements.txt b/requirements.txt index 3c9ceeb..b807973 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,12 @@ # Flask and Extensions Flask==2.3.3 +# Database +flask-sqlalchemy +flask-migrate +psycopg2-binary +alembic + # Schema for validations Flask-Marshmallow==0.15.0 marshmallow==3.19.0 @@ -14,19 +20,6 @@ gunicorn # Swagger flask-swagger-ui -# Database -flask-sqlalchemy -flask-migrate -psycopg2-binary -alembic - # Env python-dotenv - - - - -# Logging (Python Standard Library, for reference) - - From 6185c2df08f54307d56817eb302f0058f7254d62 Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Mon, 31 Mar 2025 14:09:30 +0100 Subject: [PATCH 10/13] [update]: Eligibility check request --- app/api/services/base_service.py | 53 +++++++++++++++++++++------ app/api/services/customer_consent.py | 5 ++- app/api/services/eligibility_check.py | 32 ++++++++++++---- app/models/account.py | 6 +-- app/models/customer.py | 4 +- app/models/transaction.py | 25 ++++++------- 6 files changed, 84 insertions(+), 41 deletions(-) diff --git a/app/api/services/base_service.py b/app/api/services/base_service.py index 02e73c2..453f629 100644 --- a/app/api/services/base_service.py +++ b/app/api/services/base_service.py @@ -1,4 +1,4 @@ -from app.models import Transaction +from app.models import Customer, Account, Transaction from app.api.enums import transaction_type from flask import jsonify from marshmallow import ValidationError @@ -10,19 +10,48 @@ class BaseService: TRANSACTION_TYPE = None @classmethod - def log_transaction(cls, data, schema): - logger.info(f"Processing {cls.TRANSACTION_TYPE} request") + def validate_data(cls, data, schema): + """ + Validate input data based on the provided schema. + """ + logger.info(f"Processing {cls.TRANSACTION_TYPE} request") + return schema.load(data) - validated_data = schema.load(data) + @classmethod + def get_or_create_customer(cls, validated_data): + + """ + Check if a customer exists; if not, create one. + """ - transaction = Transaction.create_transaction( - id=validated_data.get("transactionId"), - account_id=validated_data.get("accountId"), - customer_id=validated_data.get("customerId"), - type=cls.TRANSACTION_TYPE, - channel=validated_data.get("channel"), + customer = Customer.query.filter_by(id=validated_data.get("customerId")).first() + if not customer: + customer = Customer.create_customer( + id=validated_data.get("customerId"), msisdn=validated_data.get("msisdn"), - country_code=validated_data.get("countryCode") + country_code=validated_data.get("countryCode"), + account_id=validated_data.get("accountId"), ) + return customer - return transaction \ No newline at end of file + @classmethod + def validate_account_ownership(cls, account_id, customer_id): + """ + Check if the provided account belongs to the customer. + """ + is_valid = Account.is_valid_account(account_id, customer_id) + + if not is_valid: + raise ValueError("Account does not belong to customer") + + @classmethod + def create_transaction(cls, validated_data): + """ + Create a new transaction. + """ + return Transaction.create_transaction( + id=validated_data.get("transactionId"), + account_id=validated_data.get("accountId"), + type=BaseService.TRANSACTION_TYPE, + channel=validated_data.get("channel"), + ) diff --git a/app/api/services/customer_consent.py b/app/api/services/customer_consent.py index cd76120..5db10e4 100644 --- a/app/api/services/customer_consent.py +++ b/app/api/services/customer_consent.py @@ -1,10 +1,11 @@ from flask import request, jsonify +from app.api.services.base_service import BaseService from marshmallow import ValidationError from app.utils.logger import logger from app.api.schemas.customer_consent import CustomerConsentSchema -class CustomerConsentService: +class CustomerConsentService(BaseService): @staticmethod def process_request(data): """ @@ -21,7 +22,7 @@ class CustomerConsentService: # Validate input data using the CustomerConsent schema schema = CustomerConsentSchema() - validated_data = schema.load(data) # Raises ValidationError if invalid + validated_data = schema.load(data) # Simulated processing logic response_data = { diff --git a/app/api/services/eligibility_check.py b/app/api/services/eligibility_check.py index a4bffcb..146218f 100644 --- a/app/api/services/eligibility_check.py +++ b/app/api/services/eligibility_check.py @@ -3,7 +3,6 @@ from app.utils.logger import logger from app.api.services.base_service import BaseService from app.api.schemas.eligibility_check import EligibilityCheckSchema from marshmallow import ValidationError -from app.models import Transaction from app.api.enums import transaction_type class EligibilityCheckService(BaseService): @@ -21,14 +20,29 @@ class EligibilityCheckService(BaseService): dict: A standardized response. """ try: - transaction = EligibilityCheckService.log_transaction(data, EligibilityCheckSchema()) - if not transaction: - logger.error(f"Customer creation failed") + validated_data = EligibilityCheckService.validate_data(data, EligibilityCheckSchema()) + account_id = validated_data.get('accountId') + customer_id = validated_data.get('customerId') + + customer = EligibilityCheckService.get_or_create_customer(validated_data = validated_data) + + logger.error(account_id) + logger.error(customer_id) + + if (EligibilityCheckService.validate_account_ownership(account_id = account_id, customer_id = customer_id)): + transaction = EligibilityCheckService.create_transaction(validated_data = validated_data) + + if not transaction: + logger.error(f"Transaction creation failed") + return jsonify({ + "message": "Transaction creation failed." + }), 400 + else: return jsonify({ - "message": "Customer creation failed." . customer - }), 400 - + "message": "Invalid account" + }), 400 + offers = [ { @@ -61,7 +75,9 @@ class EligibilityCheckService(BaseService): return response_data except (ValidationError, ValueError) as err: - logger.error(f"Validation Error: {err.messages}") + + logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}") + return jsonify({ "message": "Validation exception" }) , 422 diff --git a/app/models/account.py b/app/models/account.py index c4254b9..95ddbc4 100644 --- a/app/models/account.py +++ b/app/models/account.py @@ -27,10 +27,10 @@ class Account(db.Model): def is_valid_account(cls, account_id, customer_id): account = cls.query.filter_by(id=account_id, customer_id=customer_id).first() if not account: - return False, "Account not found or doesn't belong to customer" + return False if account.lien_amount > 0: - return False, "Account has an existing lien" - return True, "Account is valid" + return False + return True def __repr__(self): return f'' diff --git a/app/models/customer.py b/app/models/customer.py index b6eb1de..a41efe3 100644 --- a/app/models/customer.py +++ b/app/models/customer.py @@ -20,8 +20,8 @@ class Customer(db.Model): @classmethod def create_customer(cls, id, msisdn, country_code, account_id, account_type='savings'): - if cls.query.filter_by(msisdn=msisdn).first(): - return False, "Customer with this MSISDN already exists" + if cls.query.filter_by(id=id).first(): + raise ValueError("Customer already exists") # Create the customer customer = cls(id=id, msisdn=msisdn, country_code=country_code) diff --git a/app/models/transaction.py b/app/models/transaction.py index 5343e80..62831ae 100644 --- a/app/models/transaction.py +++ b/app/models/transaction.py @@ -1,12 +1,12 @@ from datetime import datetime, timezone from app.extensions import db -from app.models import Customer +from sqlalchemy.exc import IntegrityError class Transaction(db.Model): __tablename__ = 'transactions' id = db.Column(db.String(50), primary_key=True) - customer_id = db.Column(db.String(50), nullable=False) + account_id = db.Column(db.String(50), nullable=False) type = db.Column(db.String(50), nullable=False) channel = db.Column(db.String(50), nullable=False) created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc)) @@ -16,27 +16,24 @@ class Transaction(db.Model): return f'' @classmethod - def create_transaction(cls, id, account_id, customer_id, type, channel, msisdn, country_code,): + def create_transaction(cls, id, account_id, type, channel): if cls.query.filter_by(id=id).first(): - raise ValueError("Transaction with this id already exists") + raise ValueError("Duplicate Transaction") transaction = cls( id=id, - customer_id=customer_id, + account_id=account_id, type=type, channel=channel - ) - customer = Customer.create_customer( - id=customer_id, - msisdn= msisdn, - country_code= country_code, - account_id= account_id, - ) + try: + db.session.add(transaction) + db.session.commit() + except IntegrityError as err: + db.session.rollback() + raise ValueError(f"Database integrity error: {err}") - db.session.add(transaction) - db.session.commit() return transaction @classmethod From d9c99627ae9b749ec145a23030269cde959129b7 Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Mon, 31 Mar 2025 14:50:22 +0100 Subject: [PATCH 11/13] [update]: transaction logging and account ownership --- app/api/enums/__init__.py | 2 +- app/api/enums/transaction_type.py | 8 +++- app/api/services/base_service.py | 10 ++--- app/api/services/customer_consent.py | 48 ++++++++++++++++------- app/api/services/eligibility_check.py | 24 +++++++----- app/api/services/loan_status.py | 47 ++++++++++++++++------ app/api/services/notification_callback.py | 17 +++++++- app/api/services/provide_loan.py | 45 +++++++++++++++------ app/api/services/repayment.py | 38 ++++++++++++++---- app/api/services/select_offer.py | 43 ++++++++++++++------ 10 files changed, 203 insertions(+), 79 deletions(-) diff --git a/app/api/enums/__init__.py b/app/api/enums/__init__.py index cf543fd..2e3a1d8 100644 --- a/app/api/enums/__init__.py +++ b/app/api/enums/__init__.py @@ -1 +1 @@ -from .transaction_type import transaction_type \ No newline at end of file +from .transaction_type import TransactionType \ No newline at end of file diff --git a/app/api/enums/transaction_type.py b/app/api/enums/transaction_type.py index 2f5ac09..7d14546 100644 --- a/app/api/enums/transaction_type.py +++ b/app/api/enums/transaction_type.py @@ -1,6 +1,10 @@ from enum import Enum -class transaction_type(str, Enum): +class TransactionType(str, Enum): ELIGIBILITY_CHECK = "eligibility_check" - PAYMENT = "payment" + CUSTOMER_CONSENT = "customer_consent" + LOAN_STATUS = "loan_status" + NOTIFICATION_CALLBACK = "notification_callback" + PROVIDE_LOAN = "provide_loan" REPAYMENT = "repayment" + SELECT_OFFER = "select_offer" diff --git a/app/api/services/base_service.py b/app/api/services/base_service.py index 453f629..40413c3 100644 --- a/app/api/services/base_service.py +++ b/app/api/services/base_service.py @@ -1,5 +1,5 @@ from app.models import Customer, Account, Transaction -from app.api.enums import transaction_type +from app.api.enums import TransactionType from flask import jsonify from marshmallow import ValidationError import logging @@ -40,18 +40,16 @@ class BaseService: Check if the provided account belongs to the customer. """ is_valid = Account.is_valid_account(account_id, customer_id) - - if not is_valid: - raise ValueError("Account does not belong to customer") + return is_valid @classmethod - def create_transaction(cls, validated_data): + def log_transaction(cls, validated_data): """ Create a new transaction. """ return Transaction.create_transaction( id=validated_data.get("transactionId"), account_id=validated_data.get("accountId"), - type=BaseService.TRANSACTION_TYPE, + type=cls.TRANSACTION_TYPE, channel=validated_data.get("channel"), ) diff --git a/app/api/services/customer_consent.py b/app/api/services/customer_consent.py index 5db10e4..2d1048a 100644 --- a/app/api/services/customer_consent.py +++ b/app/api/services/customer_consent.py @@ -2,10 +2,14 @@ from flask import request, jsonify from app.api.services.base_service import BaseService from marshmallow import ValidationError from app.utils.logger import logger -from app.api.schemas.customer_consent import CustomerConsentSchema +from app.api.schemas.customer_consent import CustomerConsentSchema +from app.api.services.base_service import BaseService +from app.api.enums import TransactionType class CustomerConsentService(BaseService): + TRANSACTION_TYPE = TransactionType.CUSTOMER_CONSENT + @staticmethod def process_request(data): """ @@ -18,34 +22,50 @@ class CustomerConsentService(BaseService): dict: A standardized response. """ try: - logger.info("Processing CustomerConsent request") - # Validate input data using the CustomerConsent schema - schema = CustomerConsentSchema() - validated_data = schema.load(data) + validated_data = CustomerConsentService.validate_data(data, CustomerConsentSchema()) + account_id = validated_data.get('accountId') + customer_id = validated_data.get('customerId') + if(CustomerConsentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)): + transaction = CustomerConsentService.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: + return jsonify({ + "message": "Invalid Customer or Account" + }), 400 + + # Simulated processing logic response_data = { "resultCode": "00", "resultDescription": "Request is received" } - - # return ResponseHelper.success( - # data=response_data, - # message="Customer consent processed successfully" - # ) - return response_data - + except ValidationError as err: - logger.error(f"Validation Error: {err.messages}") + + logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}") + return jsonify({ "message": "Validation exception" }) , 422 + + except ValueError as err: + logger.error(f"{getattr(err, 'messages', str(err))}") + + return jsonify({ + "message": str(err) + }) , 400 except Exception as e: logger.error(f"An error occurred: {str(e)}", exc_info=True) return jsonify({ "message": "Internal Server Error" - }) , 500 + }) , 500 \ No newline at end of file diff --git a/app/api/services/eligibility_check.py b/app/api/services/eligibility_check.py index 146218f..37fcaa1 100644 --- a/app/api/services/eligibility_check.py +++ b/app/api/services/eligibility_check.py @@ -3,10 +3,10 @@ from app.utils.logger import logger from app.api.services.base_service import BaseService from app.api.schemas.eligibility_check import EligibilityCheckSchema from marshmallow import ValidationError -from app.api.enums import transaction_type +from app.api.enums import TransactionType class EligibilityCheckService(BaseService): - TRANSACTION_TYPE = transaction_type.ELIGIBILITY_CHECK + TRANSACTION_TYPE = TransactionType.ELIGIBILITY_CHECK @staticmethod def process_request(data): @@ -27,20 +27,17 @@ class EligibilityCheckService(BaseService): customer = EligibilityCheckService.get_or_create_customer(validated_data = validated_data) - logger.error(account_id) - logger.error(customer_id) - if (EligibilityCheckService.validate_account_ownership(account_id = account_id, customer_id = customer_id)): - transaction = EligibilityCheckService.create_transaction(validated_data = validated_data) + transaction = EligibilityCheckService.log_transaction(validated_data = validated_data) if not transaction: - logger.error(f"Transaction creation failed") + logger.error(f"Failed to log transaction") return jsonify({ - "message": "Transaction creation failed." + "message": "Failed to log transaction." }), 400 else: return jsonify({ - "message": "Invalid account" + "message": "Invalid Customer or Account" }), 400 @@ -74,13 +71,20 @@ class EligibilityCheckService(BaseService): } return response_data - except (ValidationError, ValueError) as err: + except ValidationError as err: logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}") return jsonify({ "message": "Validation exception" }) , 422 + + except ValueError as err: + logger.error(f"{getattr(err, 'messages', str(err))}") + + return jsonify({ + "message": str(err) + }) , 400 except Exception as e: logger.error(f"An error occurred: {str(e)}", exc_info=True) diff --git a/app/api/services/loan_status.py b/app/api/services/loan_status.py index b163e89..95d5048 100644 --- a/app/api/services/loan_status.py +++ b/app/api/services/loan_status.py @@ -1,9 +1,14 @@ from flask import request, jsonify from marshmallow import ValidationError from app.utils.logger import logger -from app.api.schemas.loan_status import LoanStatusSchema +from app.api.schemas.loan_status import LoanStatusSchema +from app.api.services.base_service import BaseService +from app.api.enums import TransactionType + + +class LoanStatusService(BaseService): + TRANSACTION_TYPE = TransactionType.LOAN_STATUS -class LoanStatusService: @staticmethod def process_request(data): """ @@ -16,11 +21,23 @@ class LoanStatusService: dict: A standardized response. """ try: - logger.info("Processing LoanStatus request") + validated_data = LoanStatusService.validate_data(data, LoanStatusSchema()) + account_id = validated_data.get('accountId') + customer_id = validated_data.get('customerId') - # Validate input data using the imported schema - schema = LoanStatusSchema() - validated_data = schema.load(data) # Raises ValidationError if invalid + if (LoanStatusService.validate_account_ownership(account_id = account_id, customer_id = customer_id)): + transaction = LoanStatusService.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: + return jsonify({ + "message": "Invalid Customer or Account" + }), 400 + loans = [ { @@ -46,21 +63,25 @@ class LoanStatusService: } - # return ResponseHelper.success( - # data=response_data, - # message="Loan information retrieved successfully" - # ) - return response_data except ValidationError as err: - logger.error(f"Validation Error: {err.messages}") + + logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}") + return jsonify({ "message": "Validation exception" }) , 422 + + except ValueError as err: + logger.error(f"{getattr(err, 'messages', str(err))}") + + return jsonify({ + "message": str(err) + }) , 400 except Exception as e: logger.error(f"An error occurred: {str(e)}", exc_info=True) return jsonify({ "message": "Internal Server Error" - }) , 500 + }) , 500 \ No newline at end of file diff --git a/app/api/services/notification_callback.py b/app/api/services/notification_callback.py index 2c876db..b3a1c8b 100644 --- a/app/api/services/notification_callback.py +++ b/app/api/services/notification_callback.py @@ -1,9 +1,13 @@ from flask import request, jsonify from marshmallow import ValidationError +from app.api.services.base_service import BaseService +from app.api.enums import TransactionType from app.utils.logger import logger from app.api.schemas.notification_callback import NotificationCallbackSchema -class NotificationCallbackService: +class NotificationCallbackService(BaseService): + TRANSACTION_TYPE = TransactionType.NOTIFICATION_CALLBACK + @staticmethod def process_request(data): """ @@ -37,10 +41,19 @@ class NotificationCallbackService: return response_data except ValidationError as err: - logger.error(f"Validation Error: {err.messages}") + + logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}") + return jsonify({ "message": "Validation exception" }) , 422 + + except ValueError as err: + logger.error(f"{getattr(err, 'messages', str(err))}") + + return jsonify({ + "message": str(err) + }) , 400 except Exception as e: logger.error(f"An error occurred: {str(e)}", exc_info=True) diff --git a/app/api/services/provide_loan.py b/app/api/services/provide_loan.py index 8d57343..49dcefe 100644 --- a/app/api/services/provide_loan.py +++ b/app/api/services/provide_loan.py @@ -1,9 +1,14 @@ from flask import request, jsonify from marshmallow import ValidationError +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 -class ProvideLoanService: +class ProvideLoanService(BaseService): + TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN + + @staticmethod def process_request(data): """ @@ -16,13 +21,24 @@ class ProvideLoanService: dict: A standardized response. """ try: - logger.info("Processing ProvideLoan request") + validated_data = ProvideLoanService.validate_data(data, ProvideLoanSchema()) + account_id = validated_data.get('accountId') + customer_id = validated_data.get('customerId') - # Validate input data using the imported schema - schema = ProvideLoanSchema() - validated_data = schema.load(data) # Raises ValidationError if invalid + if (ProvideLoanService.validate_account_ownership(account_id = account_id, customer_id = customer_id)): + transaction = ProvideLoanService.log_transaction(validated_data = validated_data) - # Business logic - providing a loan + if not transaction: + logger.error(f"Failed to log transaction") + return jsonify({ + "message": "Failed to log transaction." + }), 400 + else: + return jsonify({ + "message": "Invalid Customer or Account" + }), 400 + + response_data = { "requestId": "202111170001371256908", "transactionId": "Tr201712RK9232P115", @@ -34,21 +50,26 @@ class ProvideLoanService: } - # return ResponseHelper.success( - # data=response_data, - # message="Loan successfully provided" - # ) return response_data except ValidationError as err: - logger.error(f"Validation Error: {err.messages}") + + logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}") + return jsonify({ "message": "Validation exception" }) , 422 + + except ValueError as err: + logger.error(f"{getattr(err, 'messages', str(err))}") + + return jsonify({ + "message": str(err) + }) , 400 except Exception as e: logger.error(f"An error occurred: {str(e)}", exc_info=True) return jsonify({ "message": "Internal Server Error" - }) , 500 + }) , 500 \ No newline at end of file diff --git a/app/api/services/repayment.py b/app/api/services/repayment.py index 1aff386..7a53452 100644 --- a/app/api/services/repayment.py +++ b/app/api/services/repayment.py @@ -1,9 +1,13 @@ from flask import request, jsonify from marshmallow import ValidationError from app.utils.logger import logger -from app.api.schemas.repayment import RepaymentSchema +from app.api.schemas.repayment import RepaymentSchema +from app.api.services.base_service import BaseService +from app.api.enums import TransactionType + +class RepaymentService(BaseService): + TRANSACTION_TYPE = TransactionType.REPAYMENT -class RepaymentService: @staticmethod def process_request(data): """ @@ -16,11 +20,22 @@ class RepaymentService: dict: A standardized response. """ try: - logger.info("Processing Repayment request") + validated_data = RepaymentService.validate_data(data, RepaymentSchema()) + account_id = validated_data.get('accountId') + customer_id = validated_data.get('customerId') - # Validate input data using the Repayment schema - schema = RepaymentSchema() - validated_data = schema.load(data) # Raises ValidationError if invalid + if (RepaymentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)): + transaction = RepaymentService.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: + return jsonify({ + "message": "Invalid Customer or Account" + }), 400 # Simulated processing logic response_data = { @@ -39,10 +54,19 @@ class RepaymentService: return response_data except ValidationError as err: - logger.error(f"Validation Error: {err.messages}") + + logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}") + return jsonify({ "message": "Validation exception" }) , 422 + + except ValueError as err: + logger.error(f"{getattr(err, 'messages', str(err))}") + + return jsonify({ + "message": str(err) + }) , 400 except Exception as e: logger.error(f"An error occurred: {str(e)}", exc_info=True) diff --git a/app/api/services/select_offer.py b/app/api/services/select_offer.py index 8656cbc..9d502c5 100644 --- a/app/api/services/select_offer.py +++ b/app/api/services/select_offer.py @@ -1,9 +1,13 @@ from flask import request, jsonify from marshmallow import ValidationError +from app.api.services.base_service import BaseService +from app.api.enums import TransactionType from app.utils.logger import logger from app.api.schemas.select_offer import SelectOfferSchema -class SelectOfferService: +class SelectOfferService(BaseService): + TRANSACTION_TYPE = TransactionType.SELECT_OFFER + @staticmethod def process_request(data): """ @@ -16,12 +20,23 @@ class SelectOfferService: dict: A standardized response. """ try: - logger.info("Processing SelectOffer request") + validated_data = SelectOfferService.validate_data(data, SelectOfferSchema()) + account_id = validated_data.get('accountId') + customer_id = validated_data.get('customerId') - # Validate input data using the imported schema - schema = SelectOfferSchema() - validated_data = schema.load(data) # Raises ValidationError if invalid + if (SelectOfferService.validate_account_ownership(account_id = account_id, customer_id = customer_id)): + transaction = SelectOfferService.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: + return jsonify({ + "message": "Invalid Customer or Account" + }), 400 + offers = [ { "offerId": "14451", @@ -54,21 +69,25 @@ class SelectOfferService: } - # return ResponseHelper.success( - # data=response_data, - # message="Offer selection completed successfully" - # ) - return response_data except ValidationError as err: - logger.error(f"Validation Error: {err.messages}") + + logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}") + return jsonify({ "message": "Validation exception" }) , 422 + + except ValueError as err: + logger.error(f"{getattr(err, 'messages', str(err))}") + + return jsonify({ + "message": str(err) + }) , 400 except Exception as e: logger.error(f"An error occurred: {str(e)}", exc_info=True) return jsonify({ "message": "Internal Server Error" - }) , 500 + }) , 500 \ No newline at end of file From c826bdc36ba1318484a2b27bba2ae5fe004f125f Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Mon, 31 Mar 2025 16:24:05 +0100 Subject: [PATCH 12/13] [add]: RACCHECK --- app/api/integrations/__init__.py | 1 + app/api/integrations/simbrella.py | 33 +++++++++++++++++++++++++++ app/api/services/eligibility_check.py | 14 ++++++++++++ app/config.py | 6 ++++- requirements.txt | 4 ++++ 5 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 app/api/integrations/__init__.py create mode 100644 app/api/integrations/simbrella.py diff --git a/app/api/integrations/__init__.py b/app/api/integrations/__init__.py new file mode 100644 index 0000000..54ad10b --- /dev/null +++ b/app/api/integrations/__init__.py @@ -0,0 +1 @@ +from .simbrella import SimbrellaClient \ No newline at end of file diff --git a/app/api/integrations/simbrella.py b/app/api/integrations/simbrella.py new file mode 100644 index 0000000..d6f3a0e --- /dev/null +++ b/app/api/integrations/simbrella.py @@ -0,0 +1,33 @@ +import requests +from app.utils.logger import logger +from app.config import settings + +class SimbrellaClient: + BASE_URL = settings.SIMBRELLA_BASE_URL + + @staticmethod + def rac_check(customer_id, account_id, transaction_id, country_code, msisdn): + """ + Calls the RACCheck endpoit + """ + url = f"{SimbrellaClient.BASE_URL}/RACCheck" + + payload = { + "customerId": customer_id, + "accountId": account_id, + "transactionId": transaction_id, + "countryCode": country_code, + "msisdn": msisdn + } + + try: + response = requests.post(url, json=payload, timeout=10) + + # Raise an error for non-200 responses + # response.raise_for_status() + + + return response.json() + except requests.exceptions.RequestException as err: + logger.error(f"RACCheck API call failed: {str(err)}", exc_info=True) + return {"error": "RACCheck API error", "details": str(err)} diff --git a/app/api/services/eligibility_check.py b/app/api/services/eligibility_check.py index 37fcaa1..852a584 100644 --- a/app/api/services/eligibility_check.py +++ b/app/api/services/eligibility_check.py @@ -4,6 +4,7 @@ from app.api.services.base_service import BaseService from app.api.schemas.eligibility_check import EligibilityCheckSchema from marshmallow import ValidationError from app.api.enums import TransactionType +from app.api.integrations import SimbrellaClient class EligibilityCheckService(BaseService): TRANSACTION_TYPE = TransactionType.ELIGIBILITY_CHECK @@ -40,6 +41,19 @@ class EligibilityCheckService(BaseService): "message": "Invalid Customer or Account" }), 400 + # Call RACCheck + response = SimbrellaClient.rac_check( + customer_id=customer_id, + account_id=account_id, + transaction_id=validated_data.get("transactionId"), + country_code=validated_data.get("countryCode"), + msisdn=validated_data.get("msisdn") + ) + + if "error" in response or response.get("status") != 200: + return jsonify({"message": "RACCheck failed", "error": response.get("message", response)}), 400 + + offers = [ { diff --git a/app/config.py b/app/config.py index a5609ac..fb66b2d 100644 --- a/app/config.py +++ b/app/config.py @@ -22,4 +22,8 @@ class Config: SQLALCHEMY_DATABASE_URI = ( f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}" ) - SQLALCHEMY_TRACK_MODIFICATIONS = False \ No newline at end of file + SQLALCHEMY_TRACK_MODIFICATIONS = False + SIMBRELLA_BASE_URL = os.getenv("SIMBRELLA_BASE_URL", "http://127.0.0.1:6337") + + +settings = Config() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index b807973..7e47465 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,3 +23,7 @@ flask-swagger-ui # Env python-dotenv + +# Requests +requests + From 39d1a1eddc94415f526d6068e58c328844433342 Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Tue, 1 Apr 2025 07:26:32 +0100 Subject: [PATCH 13/13] [add]: Simbrella integration --- .example.env | 2 ++ app/api/integrations/simbrella.py | 19 ++++++++++++++++--- app/api/services/eligibility_check.py | 8 +++----- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/.example.env b/.example.env index 4878aad..006be8c 100644 --- a/.example.env +++ b/.example.env @@ -19,3 +19,5 @@ DATABASE_NAME=***** FLASK_APP=wsgi.py FLASK_ENV=development APP_PORT=4500 + +SIMBRELLA_BASE_URL=*************** \ No newline at end of file diff --git a/app/api/integrations/simbrella.py b/app/api/integrations/simbrella.py index d6f3a0e..7b6296a 100644 --- a/app/api/integrations/simbrella.py +++ b/app/api/integrations/simbrella.py @@ -6,7 +6,7 @@ class SimbrellaClient: BASE_URL = settings.SIMBRELLA_BASE_URL @staticmethod - def rac_check(customer_id, account_id, transaction_id, country_code, msisdn): + def rac_check(customer_id, account_id, transaction_id): """ Calls the RACCheck endpoit """ @@ -16,8 +16,21 @@ class SimbrellaClient: "customerId": customer_id, "accountId": account_id, "transactionId": transaction_id, - "countryCode": country_code, - "msisdn": msisdn + "RAC_Array": [ + { + "salaryAccount": True, + "bvn": "12345678901", + "crc": False, + "crms": True, + "accountStatus": "active", + "lien": False, + "noBouncedCheck": True, + "existingLoan": False, + "whitelist": True, + "noPastDueSalaryLoan": True, + "noPastDueOtherLoans": False + } + ] } try: diff --git a/app/api/services/eligibility_check.py b/app/api/services/eligibility_check.py index 852a584..c5a5d15 100644 --- a/app/api/services/eligibility_check.py +++ b/app/api/services/eligibility_check.py @@ -43,11 +43,9 @@ class EligibilityCheckService(BaseService): # Call RACCheck response = SimbrellaClient.rac_check( - customer_id=customer_id, - account_id=account_id, - transaction_id=validated_data.get("transactionId"), - country_code=validated_data.get("countryCode"), - msisdn=validated_data.get("msisdn") + customer_id = customer_id, + account_id = account_id, + transaction_id = transaction.id, ) if "error" in response or response.get("status") != 200: