Compare commits
363 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64fc119ca7 | |||
| d94d104380 | |||
| ff78788254 | |||
| 3acb5f098a | |||
| 363cdaf192 | |||
| 697e848994 | |||
| 7fa5cb6a83 | |||
| 03d5882a3c | |||
| a87259a6ca | |||
| e2eea4d455 | |||
| d23a088c84 | |||
| c2eb7fa21a | |||
| 53555a178a | |||
| e6d4a441b2 | |||
| e5d9310563 | |||
| ad27a26aec | |||
| 537b6d68f9 | |||
| 7cb34a995b | |||
| e78e7402c8 | |||
| 8f82964b70 | |||
| 771e8c00ed | |||
| 2a184d134d | |||
| 5e5f1b83ad | |||
| 2413446107 | |||
| ff5cbcc49d | |||
| 9f2daad7c8 | |||
| 9f7435227f | |||
| 0961a65b19 | |||
| 798d264748 | |||
| 5b1d867d49 | |||
| c4b2df714c | |||
| 6138085c0e | |||
| fc9f7fe175 | |||
| 2aee3d08ed | |||
| d99640345a | |||
| 6221447353 | |||
| 54e52c639b | |||
| bb85c8f166 | |||
| 6077c78840 | |||
| 9cbc824661 | |||
| 2e08c636a2 | |||
| ad043ba3f8 | |||
| 63da7e8292 | |||
| b73bf9a234 | |||
| e8abb3c668 | |||
| 396516b941 | |||
| c59068d3bb | |||
| ccd5b12f2c | |||
| 9913f6500c | |||
| 17c760981a | |||
| 68aca1407c | |||
| 550895b8ef | |||
| b7e3527f35 | |||
| 1bab78ec1a | |||
| fe5d3fbc6e | |||
| c516c3f52c | |||
| 55140efed8 | |||
| f8da81d564 | |||
| 0faf01bcfa | |||
| 9da259900c | |||
| 4da0b8c716 | |||
| ddb08d8063 | |||
| 8c405c5d2e | |||
| 4c95112bde | |||
| 244f648974 | |||
| 324293ee96 | |||
| 06b266c3a7 | |||
| ab9330bb23 | |||
| f5cf4e5bdd | |||
| 0b78698db9 | |||
| d8abd0ff2a | |||
| c6acd9e73a | |||
| 18dda45fa1 | |||
| 363c6b498a | |||
| 2a12215931 | |||
| 2f3b589420 | |||
| f0679b8c1e | |||
| c8e21be48c | |||
| f1e9e39fe5 | |||
| b412313fc5 | |||
| cba6eac501 | |||
| 443c6262b7 | |||
| 2ba97cee4a | |||
| 8c714cb45c | |||
| c93e1a8bdd | |||
| 6d8fe24718 | |||
| deaddd8132 | |||
| 6e08b0680f | |||
| 3c0e00d9e4 | |||
| b502c128f9 | |||
| c81a029e20 | |||
| 469d94cea1 | |||
| d6aabb959e | |||
| 3a970816f5 | |||
| 65c1608b92 | |||
| 2493c075c0 | |||
| 4ea06de2bc | |||
| 1aa6a1710c | |||
| dff000dbb2 | |||
| 617738b785 | |||
| 9db3b68b13 | |||
| 1795db35be | |||
| 8bb5ce69e2 | |||
| 5087afcca6 | |||
| 11b52357ba | |||
| e8361668e5 | |||
| f659fa9cf2 | |||
| 23e340be27 | |||
| adc2498044 | |||
| eb7f783b18 | |||
| 5040002c54 | |||
| dc9415ff79 | |||
| 4822de764a | |||
| 51995a3e02 | |||
| 265bba2365 | |||
| 9985a58b56 | |||
| b41df3fe02 | |||
| 08fe04b7b9 | |||
| 79317632b6 | |||
| 0d87036b92 | |||
| 1734007476 | |||
| 6ef2be9625 | |||
| 48020f5284 | |||
| 1a6ac6a37f | |||
| bbf6953dc5 | |||
| a2158a768e | |||
| 0af1b7567b | |||
| 4d08983ae3 | |||
| 70e15cd325 | |||
| e8d930f9b8 | |||
| c400f1d69d | |||
| f7daa12531 | |||
| 3242a57586 | |||
| 463c0a0def | |||
| c061c9b5a4 | |||
| 201fa4202e | |||
| bb4d7ac064 | |||
| 5a2161acaa | |||
| 10138f66f3 | |||
| 9ea0027f71 | |||
| d1b8d15f31 | |||
| f716b47603 | |||
| ec5db19e20 | |||
| 729cc26698 | |||
| c95e2786b5 | |||
| 65472d3f07 | |||
| 29b2697b0e | |||
| 7a2ff6586f | |||
| 066ced55b0 | |||
| f6c98d9bfd | |||
| 9e22e0fcf3 | |||
| c9aba07e9c | |||
| 20c9a5c713 | |||
| 1cb0d88cc2 | |||
| 3e9d5d4089 | |||
| 2ae49ace86 | |||
| e9de001340 | |||
| 0bdc11423f | |||
| a4ed936392 | |||
| 1a315b1d80 | |||
| 4c30f81bfd | |||
| 916261fa94 | |||
| 081b73a932 | |||
| 6852986ce5 | |||
| 0038c22577 | |||
| 326ee87b13 | |||
| ca22ee86f7 | |||
| aa033a50a3 | |||
| 31b0367e6a | |||
| 89760f81ed | |||
| 701840abd1 | |||
| df6c42ca2d | |||
| a81313447b | |||
| a321832d43 | |||
| 0369323dd6 | |||
| d7b8addeb6 | |||
| 0db3f44c7b | |||
| a7d465bd5c | |||
| a0ba49f208 | |||
| 1e4f9102c8 | |||
| dee1edee40 | |||
| 06b5f98f06 | |||
| 746ca486da | |||
| 3d81322515 | |||
| eeacffad9a | |||
| 11a239c67a | |||
| 4ce0142ee0 | |||
| c268c4d92b | |||
| 6d743ea09b | |||
| 89dd4bb191 | |||
| 4718c9c50b | |||
| feb97c3fa8 | |||
| 4bcaa3d13d | |||
| b86bd3dece | |||
| a0a2c01a1c | |||
| d6faa14b54 | |||
| 332c344efa | |||
| e377858c47 | |||
| ed64d2c97c | |||
| bbdb7214d1 | |||
| e9c50f75b1 | |||
| c330c3f0e7 | |||
| 976fb14614 | |||
| 334cb0f2d6 | |||
| 40158b1c54 | |||
| b7ae0e6baa | |||
| 89b621b9a8 | |||
| cc3cd5b72b | |||
| f573d5e643 | |||
| 09b57d81a2 | |||
| 17db2cf8f9 | |||
| f07866a884 | |||
| 6f8e269a50 | |||
| 4435ca2776 | |||
| d851222024 | |||
| 52ab33f260 | |||
| af7e0f8624 | |||
| c8ab2cd6ba | |||
| 8ac22fa95f | |||
| 57207faf6f | |||
| 9a90609d33 | |||
| 50ca27abfe | |||
| 74066bae56 | |||
| 4c4ef909c2 | |||
| fdd7c58fab | |||
| 4bd163fb31 | |||
| d77181f627 | |||
| 4a236fdd2f | |||
| cae7ffd772 | |||
| 4f92f2a1a0 | |||
| 03adb266bb | |||
| d28bf95c97 | |||
| bd6edf52e1 | |||
| b1260895e0 | |||
| 2addf25a67 | |||
| 9dc431e66d | |||
| 9dae2d951c | |||
| a1d44e0e23 | |||
| d9f972a425 | |||
| 8aa2c86ea2 | |||
| 9c42332a83 | |||
| 92eadbfa16 | |||
| 0fbdebceb3 | |||
| 488a1b4bdd | |||
| cdc74d05c4 | |||
| 1b92ede296 | |||
| 7de4e3651f | |||
| 5f9b1f4cb8 | |||
| ed95865834 | |||
| 6973630845 | |||
| 5d37ba30fb | |||
| e8044d8fed | |||
| cf0502459b | |||
| 851422c335 | |||
| ddbabcaca9 | |||
| c216c55928 | |||
| 0995f08aea | |||
| e034c0ff9d | |||
| 4d4e4fcd3e | |||
| 1cce111d1f | |||
| b9b7988877 | |||
| 841393c470 | |||
| bbb903b27c | |||
| c895cc36e0 | |||
| 67c6d909f8 | |||
| e08dfe9894 | |||
| 7d691db7a5 | |||
| 4b92c33d5a | |||
| 8cfa957cc0 | |||
| 5768b537b1 | |||
| f2f592d507 | |||
| bc894c7856 | |||
| 0587efb95c | |||
| 57fa4d72d9 | |||
| 75f71a807d | |||
| b6a4af5cc6 | |||
| 829bd976b2 | |||
| 9a1c81ab10 | |||
| f461b826e6 | |||
| 2c8fda1792 | |||
| e04f54bf83 | |||
| e14e290ff9 | |||
| 93ed8b3d17 | |||
| 359621dc9d | |||
| 9cfa4a67b1 | |||
| f55f179672 | |||
| 86801b13fb | |||
| aba5a02197 | |||
| 142a7eb886 | |||
| cb18234008 | |||
| 46b8d99409 | |||
| 3c0443d0c7 | |||
| 7bee948c83 | |||
| 8ab485d920 | |||
| 9df8e31fdd | |||
| a2399a2eae | |||
| a6e7eaac3c | |||
| 482a860bd2 | |||
| 7c10d8263d | |||
| 6476b62d3c | |||
| e69335cb71 | |||
| 5e49b4bb35 | |||
| 8a018545ec | |||
| 3031251519 | |||
| 1d97304f4e | |||
| 9f9512b060 | |||
| d397c834f4 | |||
| a196d4d3c4 | |||
| b8190a0050 | |||
| e5320c075e | |||
| 1081467f6f | |||
| f252e33be2 | |||
| 6f15ae97f4 | |||
| 7cea5390c0 | |||
| 729ebd5d08 | |||
| 7026c8378b | |||
| b22d2f358e | |||
| f13ca508c2 | |||
| 87f1fa2152 | |||
| 2c34c16c34 | |||
| 1b973322c9 | |||
| edd19b9a39 | |||
| 773dedcd5b | |||
| eb4bcb025d | |||
| 37d808c144 | |||
| 1f86d30c3a | |||
| 946818dae6 | |||
| 333e48d32a | |||
| 6e24b498dc | |||
| cbf57f4bfb | |||
| bae92d36c6 | |||
| dd2da9c462 | |||
| f20563a9b1 | |||
| 85671081ec | |||
| 53cff748ed | |||
| 3b098969e2 | |||
| 751bcb7d28 | |||
| 25ccb240ca | |||
| de4554e247 | |||
| e1b3f4930e | |||
| 900d7b509c | |||
| 31ffbe686a | |||
| 7b0d19c0e0 | |||
| 7087254859 | |||
| d2d9603914 | |||
| f2b5703bcb | |||
| 49c3422c94 | |||
| 07f1db2050 | |||
| 7cbe3aefea | |||
| e7ae1f6732 | |||
| 30233b7283 | |||
| 3a30e0abff | |||
| d3b9708216 | |||
| 0e8bc63b4d | |||
| cbc9d6363e | |||
| 24fe303304 | |||
| 681ac19028 | |||
| d2ffe1c439 | |||
| 12fc6d0ab6 | |||
| 04e5548a4a | |||
| 823ae9e2a7 | |||
| 79ac972841 | |||
| b180f8411d |
@@ -0,0 +1,36 @@
|
|||||||
|
# Environment Variables
|
||||||
|
BASIC_AUTH_USERNAME=user
|
||||||
|
BASIC_AUTH_PASSWORD=password
|
||||||
|
|
||||||
|
#swagger Configuration
|
||||||
|
SWAGGER_URL="/documentation"
|
||||||
|
API_URL="/swagger.json"
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DATABASE_USER=firstadvance
|
||||||
|
DATABASE_PASSWORD=FirstAdvance!
|
||||||
|
DATABASE_HOST=dev-data.simbrellang.net
|
||||||
|
DATABASE_PORT=10532
|
||||||
|
DATABASE_NAME=firstadvancedev
|
||||||
|
#SQLALCHEMY_DATABASE_URI_FULL="oracle+oracledb://FIRSTADVSTG:Pchanged_56789@10.2.110.30:1521/?service_name=firstadv"
|
||||||
|
|
||||||
|
# DATABASE_HOST=10.20.30.60
|
||||||
|
# DATABASE_USER=firstadvance
|
||||||
|
# DATABASE_PASSWORD=firstadvance
|
||||||
|
# DATABASE_NAME=firstadvancedev
|
||||||
|
# DATABASE_PORT=5432
|
||||||
|
|
||||||
|
# Flask Configuration
|
||||||
|
FLASK_APP=wsgi.py
|
||||||
|
FLASK_ENV=development
|
||||||
|
APP_PORT=4500
|
||||||
|
|
||||||
|
|
||||||
|
# Bank Call Service Connection
|
||||||
|
SIMBRELLA_BASE_URL="https://bank-emulator.dev.simbrellang.net"
|
||||||
|
VALID_APP_ID=app1
|
||||||
|
VALID_API_KEY=test-api-key-12345
|
||||||
|
|
||||||
|
|
||||||
|
# Event Bus Broker Configuration
|
||||||
|
KAFKA_BROKER="10.0.0.246:9092"
|
||||||
@@ -5,3 +5,4 @@ app.log
|
|||||||
migrations/__pycache__/
|
migrations/__pycache__/
|
||||||
migrations/*.pycg
|
migrations/*.pycg
|
||||||
./vscode
|
./vscode
|
||||||
|
.vscode/settings.json
|
||||||
|
|||||||
Vendored
+24
-24
@@ -1,24 +1,24 @@
|
|||||||
{
|
// {
|
||||||
"editor.lineNumbers": "off",
|
// "editor.lineNumbers": "off",
|
||||||
"editor.padding.top": 3,
|
// "editor.padding.top": 3,
|
||||||
"editor.padding.bottom": 3,
|
// "editor.padding.bottom": 3,
|
||||||
"editor.formatOnSave": true,
|
// "editor.formatOnSave": true,
|
||||||
"editor.formatOnPaste": true,
|
// "editor.formatOnPaste": true,
|
||||||
"editor.fontSize": 14,
|
// "editor.fontSize": 14,
|
||||||
"editor.lineHeight": 4.5,
|
// "editor.lineHeight": 4.5,
|
||||||
"editor.suggestFontSize": 15,
|
// "editor.suggestFontSize": 15,
|
||||||
// "editor.suggestLineHeight": 4,
|
// // "editor.suggestLineHeight": 4,
|
||||||
"breadcrumbs.enabled": false,
|
// "breadcrumbs.enabled": false,
|
||||||
"workbench.tips.enabled": false,
|
// "workbench.tips.enabled": false,
|
||||||
"workbench.statusBar.visible": false,
|
// "workbench.statusBar.visible": false,
|
||||||
// "workbench.editor.showTabs": "single",
|
// // "workbench.editor.showTabs": "single",
|
||||||
"git.enableSmartCommit": true,
|
// "git.enableSmartCommit": true,
|
||||||
"workbench.editor.editorActionsLocation": "hidden",
|
// "workbench.editor.editorActionsLocation": "hidden",
|
||||||
// "workbench.activityBar.location": "hidden",
|
// // "workbench.activityBar.location": "hidden",
|
||||||
"workbench.editor.enablePreviewFromQuickOpen": false,
|
// "workbench.editor.enablePreviewFromQuickOpen": false,
|
||||||
"editor.lightbulb.enabled": "off",
|
// "editor.lightbulb.enabled": "off",
|
||||||
"editor.selectionHighlight": false,
|
// "editor.selectionHighlight": false,
|
||||||
"editor.overviewRulerBorder": false,
|
// "editor.overviewRulerBorder": false,
|
||||||
"editor.renderLineHighlight": "none",
|
// "editor.renderLineHighlight": "none",
|
||||||
"editor.occurrencesHighlight": "off"
|
// "editor.occurrencesHighlight": "off"
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from flask import Flask
|
||||||
|
from app.extensions import mail
|
||||||
|
from app.utils.mail import send_report_email, get_report_data
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config.from_object(settings)
|
||||||
|
|
||||||
|
mail.init_app(app)
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
report_data = get_report_data()
|
||||||
|
recipients = ["vdagbue@gmail.com"]
|
||||||
|
result = send_report_email(report_data, recipients)
|
||||||
|
print(result)
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
|
||||||
|
# Environment Variables ======================================================
|
||||||
|
BASIC_AUTH_USERNAME=user
|
||||||
|
BASIC_AUTH_PASSWORD=password
|
||||||
|
SWAGGER_URL="/documentation"
|
||||||
|
API_URL="/swagger.json"
|
||||||
|
|
||||||
|
# Flask Configuration =========================================================
|
||||||
|
FLASK_APP=wsgi.py
|
||||||
|
FLASK_ENV=development
|
||||||
|
APP_PORT=4500
|
||||||
|
|
||||||
|
#Database Configuration =======================================================
|
||||||
|
DATABASE_USER=FIRSTADVSTG
|
||||||
|
DATABASE_PASSWORD=Pchanged_56789
|
||||||
|
DATABASE_HOST=ig-x6-uat-scan
|
||||||
|
DATABASE_PORT=1521
|
||||||
|
DATABASE_NAME=FIRSTADVSTG
|
||||||
|
DATABASE_SID=firstadv
|
||||||
|
SQLALCHEMY_DATABASE_URI_FULL="oracle+oracledb://FIRSTADVSTG:Pchanged_56789@10.2.110.30:1521/?service_name=firstadv"
|
||||||
|
|
||||||
|
# Event Bus =====================================================================
|
||||||
|
KAFKA_BROKER="10.2.110.20:9082"
|
||||||
|
|
||||||
|
#Bank Calls =====================================================================
|
||||||
|
SIMBRELLA_BASE_URL="https://bank-emulator.dev.simbrellang.net"
|
||||||
|
SIMBRELLA_APP_ID="app1"
|
||||||
|
SIMBRELLA_API_KEY="testtest-api-key-12345"
|
||||||
|
|
||||||
|
#Events Direct Location =========================================================
|
||||||
|
EVENTS_SERVICE_BASE_URL="http://10.2.24.133:5000"
|
||||||
|
ENDPOINT_DIRECT_LOAN="/autocall/direct/loan"
|
||||||
|
ENDPOINT_DIRECT_REPAYMENT="/autocall/direct/repayment"
|
||||||
|
|
||||||
|
#EVENTS_SERVICE_BASE_URL2="https://event-core.simbrellang.net"
|
||||||
|
#EVENTS_SERVICE_BASE_URL="http://10.10.11.17:14700"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
CREATE TABLE transactions (
|
||||||
|
id SERIAL,
|
||||||
|
transaction_id VARCHAR(50) NOT NULL,
|
||||||
|
account_id VARCHAR(50) NOT NULL,
|
||||||
|
type VARCHAR(50) NOT NULL,
|
||||||
|
channel VARCHAR(8) NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
updated_at timestamp with time zone DEFAULT now()
|
||||||
|
);
|
||||||
|
ALTER TABLE ONLY transactions
|
||||||
|
ADD CONSTRAINT transactions_id_key UNIQUE (id);
|
||||||
|
|
||||||
|
|
||||||
+32
-16
@@ -1,4 +1,5 @@
|
|||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
from flask_mail import Mail
|
||||||
import os
|
import os
|
||||||
from flask_swagger_ui import get_swaggerui_blueprint
|
from flask_swagger_ui import get_swaggerui_blueprint
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
@@ -7,7 +8,7 @@ from app.api.routes import api
|
|||||||
from app.errors import register_error_handlers
|
from app.errors import register_error_handlers
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_migrate import Migrate
|
from flask_migrate import Migrate
|
||||||
from app.extensions import db, migrate
|
from app.extensions import db, migrate, mail
|
||||||
from flask_jwt_extended import (
|
from flask_jwt_extended import (
|
||||||
JWTManager,
|
JWTManager,
|
||||||
jwt_required,
|
jwt_required,
|
||||||
@@ -18,33 +19,48 @@ from flask_jwt_extended import (
|
|||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
"""Factory function to create a Flask app instance"""
|
"""Factory function to create a Flask app instance"""
|
||||||
|
# import oracledb
|
||||||
|
|
||||||
|
# oracledb.init_oracle_client(lib_dir=None)
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
# Load configuration
|
# Load configuration
|
||||||
app.config.from_object(Config)
|
app.config.from_object(Config)
|
||||||
|
|
||||||
CORS(app)
|
CORS(app)
|
||||||
|
|
||||||
JWTManager(app)
|
JWTManager(app)
|
||||||
|
CORS(app, supports_credentials=True)
|
||||||
|
|
||||||
# Swagger Doc
|
|
||||||
SWAGGER_URL = app.config.get("SWAGGER_URL")
|
|
||||||
API_URL = app.config.get("API_URL")
|
|
||||||
|
|
||||||
# Register blueprints
|
try:
|
||||||
app.register_blueprint(api)
|
# Swagger Doc
|
||||||
|
SWAGGER_URL = app.config.get("SWAGGER_URL")
|
||||||
|
API_URL = app.config.get("API_URL")
|
||||||
|
|
||||||
swagger_ui_blueprint = get_swaggerui_blueprint(SWAGGER_URL, API_URL)
|
# Register blueprints
|
||||||
app.register_blueprint(swagger_ui_blueprint, url_prefix=SWAGGER_URL)
|
app.register_blueprint(api)
|
||||||
|
|
||||||
# Error Handlers
|
swagger_ui_blueprint = get_swaggerui_blueprint(SWAGGER_URL, API_URL)
|
||||||
register_error_handlers(app)
|
app.register_blueprint(swagger_ui_blueprint, url_prefix=SWAGGER_URL)
|
||||||
|
|
||||||
from . import models
|
except Exception as e:
|
||||||
|
print(f"Swagger Unexpected error occurred: {e}")
|
||||||
|
|
||||||
# Database and Migrations
|
|
||||||
db.init_app(app)
|
|
||||||
|
|
||||||
migrate.init_app(app, db)
|
try:
|
||||||
|
# Error Handlers
|
||||||
|
register_error_handlers(app)
|
||||||
|
|
||||||
return app
|
from . import models
|
||||||
|
|
||||||
|
# Initialize Flask-Mail
|
||||||
|
mail.init_app(app)
|
||||||
|
|
||||||
|
# Database and Migrations
|
||||||
|
db.init_app(app)
|
||||||
|
|
||||||
|
migrate.init_app(app, db)
|
||||||
|
|
||||||
|
return app
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An unexpected error occurred: {e}")
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
from .transaction_type import TransactionType
|
from .transaction_type import TransactionType
|
||||||
|
from .loan_status import LoanStatus
|
||||||
|
from .repayment_schedule_status import RepaymentScheduleStatus
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class LoanStatus(str, Enum):
|
||||||
|
PENDING = "pending"
|
||||||
|
ACTIVE = "active"
|
||||||
|
ACTIVE_PARTIAL = "active_partial"
|
||||||
|
START_REPAY = "start_repay"
|
||||||
|
REPAID = "repaid"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class RepaymentScheduleStatus(str, Enum):
|
||||||
|
ACTIVE = "active"
|
||||||
|
PARTIALLY_PAID = "partially_paid"
|
||||||
|
REPAID = "repaid"
|
||||||
@@ -1,251 +1,112 @@
|
|||||||
from flask import jsonify
|
from flask import jsonify
|
||||||
from typing import List, Dict, Union, Optional, Any
|
from typing import Optional, Union, Dict, List, Any
|
||||||
|
|
||||||
|
|
||||||
class ResponseHelper:
|
class ResponseHelper:
|
||||||
"""
|
"""
|
||||||
A helper class for building standardized JSON responses in Flask.
|
A helper class for building standardized JSON responses using resultCode and resultDescription.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def build_response(
|
def build_response(
|
||||||
status: bool,
|
result_code: str,
|
||||||
message: str,
|
result_description: str,
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
data: Optional[Union[Dict, List, str]] = None
|
||||||
status_code: int = 200,
|
|
||||||
error: Optional[Union[Dict, str]] = None,
|
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
|
||||||
Build a standardized JSON response.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
status (bool): Indicates whether the request was successful.
|
|
||||||
message (str): A message describing the result of the request.
|
|
||||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
|
||||||
status_code (int): The HTTP status code for the response.
|
|
||||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, Any]: A dictionary representing the JSON response.
|
|
||||||
"""
|
|
||||||
response = {
|
response = {
|
||||||
"status": status,
|
"resultCode": result_code,
|
||||||
"statusCode": status_code,
|
"resultDescription": result_description
|
||||||
"message": message,
|
|
||||||
"data": data if data is not None else {},
|
|
||||||
"error": error if error is not None else {},
|
|
||||||
}
|
}
|
||||||
return jsonify(response), status_code
|
|
||||||
|
if isinstance(data, dict):
|
||||||
|
response.update(data)
|
||||||
|
|
||||||
|
return jsonify(response)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def success(
|
def success(
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
result_description: str = "Successful",
|
||||||
message: str = "Successful",
|
result_code: str = "00",
|
||||||
status_code: int = 200,
|
data: Optional[Dict[str, Any]] = None
|
||||||
error: Optional[Union[Dict, str]] = None,
|
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
return ResponseHelper.build_response(result_code, result_description, data)
|
||||||
Return a success response.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
|
||||||
message (str): A message describing the result of the request.
|
|
||||||
status_code (int): The HTTP status code for the response.
|
|
||||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, Any]: A dictionary representing the JSON response.
|
|
||||||
"""
|
|
||||||
return ResponseHelper.build_response(True, message, data, status_code, error)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def error(
|
def error(
|
||||||
message: str = "An error occurred",
|
result_description: str = "An error occurred",
|
||||||
status_code: int = 400,
|
result_code: str = "01",
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
data: Optional[Dict[str, Any]] = None
|
||||||
error: Optional[Union[Dict, str]] = None,
|
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
return ResponseHelper.build_response(result_code, result_description, data)
|
||||||
Return an error response.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message (str): A message describing the error.
|
|
||||||
status_code (int): The HTTP status code for the response.
|
|
||||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
|
||||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, Any]: A dictionary representing the JSON response.
|
|
||||||
"""
|
|
||||||
return ResponseHelper.build_response(False, message, data, status_code, error)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def created(
|
def created(
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
result_description: str = "Resource created successfully",
|
||||||
message: str = "Resource created successfully",
|
result_code: str = "00",
|
||||||
error: Optional[Union[Dict, str]] = None,
|
data: Optional[Dict[str, Any]] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
return ResponseHelper.build_response(result_code, result_description, data)
|
||||||
Return a response for a created resource.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
|
||||||
message (str): A message describing the result of the request.
|
|
||||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, Any]: A dictionary representing the JSON response.
|
|
||||||
"""
|
|
||||||
return ResponseHelper.build_response(True, message, data, 201, error)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def updated(
|
def updated(
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
result_description: str = "Resource updated successfully",
|
||||||
message: str = "Resource updated successfully",
|
result_code: str = "00",
|
||||||
error: Optional[Union[Dict, str]] = None,
|
data: Optional[Dict[str, Any]] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
return ResponseHelper.build_response(result_code, result_description, data)
|
||||||
Return a response for an updated resource.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
|
||||||
message (str): A message describing the result of the request.
|
|
||||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, Any]: A dictionary representing the JSON response.
|
|
||||||
"""
|
|
||||||
return ResponseHelper.build_response(True, message, data, 200, error)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def internal_server_error(
|
def internal_server_error(
|
||||||
message: str = "Internal Server Error",
|
result_description: str = "Internal Server Error",
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
result_code: str = "500",
|
||||||
error: Optional[Union[Dict, str]] = None,
|
data: Optional[Dict[str, Any]] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
return ResponseHelper.build_response(result_code, result_description, data)
|
||||||
Return a response for an internal server error.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message (str): A message describing the error.
|
|
||||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
|
||||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, Any]: A dictionary representing the JSON response.
|
|
||||||
"""
|
|
||||||
return ResponseHelper.build_response(False, message, data, 500, error)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def unauthorized(
|
def unauthorized(
|
||||||
message: str = "Unauthorized",
|
result_description: str = "Unauthorized",
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
result_code: str = "401",
|
||||||
error: Optional[Union[Dict, str]] = None,
|
data: Optional[Dict[str, Any]] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
return ResponseHelper.build_response(result_code, result_description, data)
|
||||||
Return a response for an unauthorized request.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message (str): A message describing the error.
|
|
||||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
|
||||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, Any]: A dictionary representing the JSON response.
|
|
||||||
"""
|
|
||||||
return ResponseHelper.build_response(False, message, data, 401, error)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def forbidden(
|
def forbidden(
|
||||||
message: str = "Forbidden",
|
result_description: str = "Forbidden",
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
result_code: str = "403",
|
||||||
error: Optional[Union[Dict, str]] = None,
|
data: Optional[Dict[str, Any]] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
return ResponseHelper.build_response(result_code, result_description, data)
|
||||||
Return a response for a forbidden request.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message (str): A message describing the error.
|
|
||||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
|
||||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, Any]: A dictionary representing the JSON response.
|
|
||||||
"""
|
|
||||||
return ResponseHelper.build_response(False, message, data, 403, error)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def not_found(
|
def not_found(
|
||||||
message: str = "Resource not found",
|
result_description: str = "Resource not found",
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
result_code: str = "404",
|
||||||
error: Optional[Union[Dict, str]] = None,
|
data: Optional[Dict[str, Any]] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
return ResponseHelper.build_response(result_code, result_description, data)
|
||||||
Return a response for a not found resource.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message (str): A message describing the error.
|
|
||||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
|
||||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, Any]: A dictionary representing the JSON response.
|
|
||||||
"""
|
|
||||||
return ResponseHelper.build_response(False, message, data, 404, error)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def unprocessable_entity(
|
def unprocessable_entity(
|
||||||
message: str = "Unprocessable entity",
|
result_description: str = "Unprocessable entity",
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
result_code: str = "422",
|
||||||
error: Optional[Union[Dict, str]] = None,
|
data: Optional[Dict[str, Any]] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
return ResponseHelper.build_response(result_code, result_description, data)
|
||||||
Return a response for an unprocessable entity.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message (str): A message describing the error.
|
|
||||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
|
||||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, Any]: A dictionary representing the JSON response.
|
|
||||||
"""
|
|
||||||
return ResponseHelper.build_response(False, message, data, 422, error)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def method_not_allowed(
|
def method_not_allowed(
|
||||||
message: str = "Method Not Allowed",
|
result_description: str = "Method Not Allowed",
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
result_code: str = "405",
|
||||||
error: Optional[Union[Dict, str]] = None,
|
data: Optional[Dict[str, Any]] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
return ResponseHelper.build_response(result_code, result_description, data)
|
||||||
Return a response for a method not allowed error.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message (str): A message describing the error.
|
|
||||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
|
||||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, Any]: A dictionary representing the JSON response.
|
|
||||||
"""
|
|
||||||
return ResponseHelper.build_response(False, message, data, 405, error)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def bad_request(
|
def bad_request(
|
||||||
message: str = "Bad Request",
|
result_description: str = "Bad Request",
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
result_code: str = "400",
|
||||||
error: Optional[Union[Dict, str]] = None,
|
data: Optional[Dict[str, Any]] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
return ResponseHelper.build_response(result_code, result_description, data)
|
||||||
Return a response for a bad request error.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message (str): A message describing the error.
|
|
||||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
|
||||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, Any]: A dictionary representing the JSON response.
|
|
||||||
"""
|
|
||||||
return ResponseHelper.build_response(False, message, data, 400, error)
|
|
||||||
@@ -1 +1,3 @@
|
|||||||
from .simbrella import SimbrellaClient
|
from .simbrella import SimbrellaIntegration
|
||||||
|
from .kafka import KafkaIntegration
|
||||||
|
from .events_service import EventServiceIntegration
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import httpx
|
||||||
|
from app.utils.logger import logger
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
|
|
||||||
|
class EventServiceIntegration:
|
||||||
|
BASE_URL = settings.SIMBRELLA_BASE_URL
|
||||||
|
EVENTS_SERVICE_BASE_URL = settings.EVENTS_SERVICE_BASE_URL
|
||||||
|
ENDPOINT_DIRECT_LOAN = settings.ENDPOINT_DIRECT_LOAN
|
||||||
|
ENDPOINT_DIRECT_REPAYMENT = settings.ENDPOINT_DIRECT_REPAYMENT
|
||||||
|
TIMEOUT = settings.TIMEOUT
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def direct_loan(transaction_id: str):
|
||||||
|
"""
|
||||||
|
Calls the Direct Loan endpoint
|
||||||
|
"""
|
||||||
|
url = f"{EventServiceIntegration.EVENTS_SERVICE_BASE_URL}{EventServiceIntegration.ENDPOINT_DIRECT_LOAN}"
|
||||||
|
logger.info(f"Direct Loan URL: {url}")
|
||||||
|
payload = {"transactionId": str(transaction_id)}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = httpx.post(url, json=payload, headers=headers, timeout=EventServiceIntegration.TIMEOUT)
|
||||||
|
logger.info(f"Loan Response: {response.text}")
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Direct Loan API call failed: {str(e)}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def direct_repayment(transaction_id: str):
|
||||||
|
"""
|
||||||
|
Calls the Direct Repayment endpoint
|
||||||
|
"""
|
||||||
|
url = f"{EventServiceIntegration.EVENTS_SERVICE_BASE_URL}{EventServiceIntegration.ENDPOINT_DIRECT_REPAYMENT}"
|
||||||
|
logger.info(f"Direct Repayment URL: {url}")
|
||||||
|
payload = {"transactionId": str(transaction_id)}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = httpx.post(url, json=payload, headers=headers, timeout=EventServiceIntegration.TIMEOUT)
|
||||||
|
logger.info(f"Repayment Response: {response.text}")
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Direct Repayment API call failed: {str(e)}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def health_check():
|
||||||
|
"""
|
||||||
|
Health check for Events Service
|
||||||
|
"""
|
||||||
|
url = f"{EventServiceIntegration.EVENTS_SERVICE_BASE_URL}/health"
|
||||||
|
logger.info(f"Health Check URL: {url}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = httpx.get(url, timeout=EventServiceIntegration.TIMEOUT)
|
||||||
|
logger.info(f"Health Check Response: {response.text}")
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Health Check API call failed: {str(e)}", exc_info=True)
|
||||||
|
raise
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
from confluent_kafka import Producer
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class KafkaIntegration:
|
||||||
|
_producer = None
|
||||||
|
_config = {
|
||||||
|
"bootstrap.servers": settings.KAFKA_BROKER,
|
||||||
|
"client.id": "loan-service-producer",
|
||||||
|
"acks": "all",
|
||||||
|
"retries": 3,
|
||||||
|
"debug": "broker,topic,msg",
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_producer():
|
||||||
|
"""Kafka producer"""
|
||||||
|
if not KafkaIntegration._producer:
|
||||||
|
KafkaIntegration._producer = Producer(KafkaIntegration._config)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Connected to Kafka broker at {KafkaIntegration._config['bootstrap.servers']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return KafkaIntegration._producer
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delivery_report(err, msg):
|
||||||
|
"""Called once for each message produced"""
|
||||||
|
if err is not None:
|
||||||
|
|
||||||
|
logger.error(f'Message delivery failed: {err}')
|
||||||
|
raise RuntimeError(f"Message delivery failed: {err}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.debug(f'Message delivered to {msg.topic()} [{msg.partition()}] @ offset {msg.offset()}')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def send_loan_request(loan_data, request_id, topic):
|
||||||
|
"""
|
||||||
|
Send loan request to topic
|
||||||
|
|
||||||
|
Args:
|
||||||
|
loan_data: Loan request payload as dict
|
||||||
|
request_id: Unique request identifier (used as Kafka key)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
|
||||||
|
# Proceed to send loan request to Kafka
|
||||||
|
producer = KafkaIntegration._get_producer()
|
||||||
|
|
||||||
|
# Sending loan request message to Kafka
|
||||||
|
producer.produce(
|
||||||
|
topic=topic,
|
||||||
|
key=str(request_id),
|
||||||
|
value=json.dumps(loan_data).encode("utf-8"),
|
||||||
|
callback=KafkaIntegration.delivery_report,
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
producer.poll(0)
|
||||||
|
|
||||||
|
logger.info(f"Loan request {request_id} queued for processing")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Failed to send loan request to Kafka: {str(e)}", exc_info=True
|
||||||
|
)
|
||||||
|
raise Exception(f"Failed to send loan request to Kafka: {str(e)}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def flush():
|
||||||
|
"""Shutdown"""
|
||||||
|
producer = KafkaIntegration._get_producer()
|
||||||
|
producer.flush()
|
||||||
@@ -1,46 +1,174 @@
|
|||||||
import requests
|
from os import access
|
||||||
|
import httpx
|
||||||
|
import time
|
||||||
from app.utils.logger import logger
|
from app.utils.logger import logger
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
|
|
||||||
class SimbrellaClient:
|
|
||||||
|
class SimbrellaIntegration:
|
||||||
BASE_URL = settings.SIMBRELLA_BASE_URL
|
BASE_URL = settings.SIMBRELLA_BASE_URL
|
||||||
|
ENDPOINT_RAC_CHECKS = settings.SIMBRELLA_ENDPOINT_RAC_CHECKS
|
||||||
|
HEALTH_ENDPOINT = settings.SIMBRELLA_HEALTH
|
||||||
|
AUTH_ENDPOINT = settings.BANK_CALL_AUTH_ENDPOINT
|
||||||
|
SIMBRELLA_VERIFY_BALANCE_ENDPOINT = settings.SIMBRELLA_VERIFY_BALANCE_ENDPOINT
|
||||||
|
TIMEOUT = settings.TIMEOUT
|
||||||
|
|
||||||
|
_access_token = None # cache token in memory
|
||||||
|
_token_expiry = 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_token():
|
||||||
|
"""
|
||||||
|
Generate a new access token using the username and password from settings.
|
||||||
|
"""
|
||||||
|
url = f"{SimbrellaIntegration.BASE_URL}{SimbrellaIntegration.AUTH_ENDPOINT}"
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"username": settings.BANK_CALL_USERNAME,
|
||||||
|
"password": settings.BANK_CALL_PASSWORD,
|
||||||
|
"grant_type": "password"
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"Requesting Bank token from {url}")
|
||||||
|
|
||||||
|
response = httpx.post(url, json=payload, headers=headers, timeout=SimbrellaIntegration.TIMEOUT)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
expires_in = data.get("expires_in", 1800)
|
||||||
|
|
||||||
|
SimbrellaIntegration._access_token = data.get("access_token")
|
||||||
|
SimbrellaIntegration._token_expiry = time.time() + expires_in - 60
|
||||||
|
|
||||||
|
if not SimbrellaIntegration._access_token:
|
||||||
|
raise Exception("Access token not found in Bank Authorization response")
|
||||||
|
|
||||||
|
logger.info("Successfully retrieved Bank access token")
|
||||||
|
return SimbrellaIntegration._access_token
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Token generation failed: {str(e)}", exc_info=True)
|
||||||
|
raise Exception(f"Token generation failed: {str(e)}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_token():
|
||||||
|
"""
|
||||||
|
Return a valid token, refreshing if expired or missing
|
||||||
|
"""
|
||||||
|
if not SimbrellaIntegration._access_token or time.time() >= SimbrellaIntegration._token_expiry:
|
||||||
|
return SimbrellaIntegration.generate_token()
|
||||||
|
return SimbrellaIntegration._access_token
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def rac_check(customer_id, account_id, transaction_id):
|
def rac_check(customer_id, account_id, transaction_id):
|
||||||
"""
|
"""
|
||||||
Calls the RACCheck endpoit
|
Calls the RACCheck endpoit
|
||||||
"""
|
"""
|
||||||
url = f"{SimbrellaClient.BASE_URL}/RACCheck"
|
url = f"{SimbrellaIntegration.BASE_URL}/{SimbrellaIntegration.ENDPOINT_RAC_CHECKS}"
|
||||||
|
logger.info(f"Contacting Rack Checks EndPoint: {str(url)}", exc_info=True)
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"customerId": customer_id,
|
"customerId": customer_id,
|
||||||
"accountId": account_id,
|
"accountId": account_id,
|
||||||
"transactionId": transaction_id,
|
"transactionId": str(transaction_id),
|
||||||
"RAC_Array": [
|
"fbnTransactionId": str(transaction_id),
|
||||||
{
|
"countryCode": "NG",
|
||||||
"salaryAccount": True,
|
"channel": "USSD"
|
||||||
"bvn": "12345678901",
|
|
||||||
"crc": False,
|
|
||||||
"crms": True,
|
|
||||||
"accountStatus": "active",
|
|
||||||
"lien": False,
|
|
||||||
"noBouncedCheck": True,
|
|
||||||
"existingLoan": False,
|
|
||||||
"whitelist": True,
|
|
||||||
"noPastDueSalaryLoan": True,
|
|
||||||
"noPastDueOtherLoans": False
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(url, json=payload, timeout=10)
|
access_token = SimbrellaIntegration._get_token()
|
||||||
|
headers = {
|
||||||
# Raise an error for non-200 responses
|
"Content-Type": "application/json",
|
||||||
# response.raise_for_status()
|
"Authorization": f"Bearer {access_token}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return response.json()
|
response = httpx.post(url, json=payload, headers=headers, timeout=SimbrellaIntegration.TIMEOUT)
|
||||||
except requests.exceptions.RequestException as err:
|
|
||||||
logger.error(f"RACCheck API call failed: {str(err)}", exc_info=True)
|
logger.info(f"This is Response: {str(response)}", exc_info=True)
|
||||||
return {"error": "RACCheck API error", "details": str(err)}
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"RACCheck API call failed: {str(e)}", exc_info=True)
|
||||||
|
raise Exception(f"RACCheck API call failed: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def verify_account_balance(account_id: str, amount: float, request_id: str):
|
||||||
|
"""
|
||||||
|
Calls the Verify Account Balance endpoint
|
||||||
|
"""
|
||||||
|
url = f"{SimbrellaIntegration.BASE_URL}/{SimbrellaIntegration.SIMBRELLA_VERIFY_BALANCE_ENDPOINT}"
|
||||||
|
logger.info(f"Contacting Verify Account Balance Endpoint: {url}")
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"accountId": account_id,
|
||||||
|
"amount": amount,
|
||||||
|
"requestId": str(request_id),
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
access_token = SimbrellaIntegration._get_token()
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer {access_token}",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = httpx.post(
|
||||||
|
url,
|
||||||
|
json=payload,
|
||||||
|
headers=headers,
|
||||||
|
timeout=SimbrellaIntegration.TIMEOUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Verify Account Balance Response: "
|
||||||
|
f"status={response.status_code}, body={response.text}"
|
||||||
|
)
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
return response
|
||||||
|
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
logger.error(
|
||||||
|
f"Verify Account Balance failed with status "
|
||||||
|
f"{e.response.status_code}: {e.response.text}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
raise Exception("Verify Account Balance API returned an error")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Verify Account Balance API call failed: {str(e)}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
raise Exception(f"Verify Account Balance API call failed: {str(e)}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def health_check():
|
||||||
|
"""
|
||||||
|
Health check for Bank Service
|
||||||
|
"""
|
||||||
|
|
||||||
|
url = f"{SimbrellaIntegration.BASE_URL}/{SimbrellaIntegration.HEALTH_ENDPOINT}"
|
||||||
|
logger.info(f"Bank Health Check URL: {url}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
access_token = SimbrellaIntegration._get_token()
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer {access_token}"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = httpx.get(url, headers=headers, timeout=SimbrellaIntegration.TIMEOUT)
|
||||||
|
logger.info(f"Bank Health Check Response: {response.text}")
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Bank Health Check API call failed: {str(e)}", exc_info=True)
|
||||||
|
raise Exception(f"Bank Health Check API call failed: {str(e)}")
|
||||||
|
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ def require_app_id(f):
|
|||||||
|
|
||||||
if not app_id:
|
if not app_id:
|
||||||
logger.error("Unauthorized access: Missing App-ID.")
|
logger.error("Unauthorized access: Missing App-ID.")
|
||||||
return jsonify({"message": "Invalid request parameters"}), 400
|
return jsonify({"message": "Invalid request"}), 400
|
||||||
|
|
||||||
|
|
||||||
if app_id != VALID_APP_ID:
|
if app_id != VALID_APP_ID:
|
||||||
logger.error(f"Unauthorized access: Invalid App-ID {app_id}.")
|
logger.error(f"Unauthorized access: Invalid App-ID {app_id}.")
|
||||||
return jsonify({"message": "Invalid request parameters"}), 400
|
return jsonify({"message": "Invalid request"}), 400
|
||||||
|
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ def require_auth(f):
|
|||||||
def decorated(*args, **kwargs):
|
def decorated(*args, **kwargs):
|
||||||
auth = request.headers.get('Authorization')
|
auth = request.headers.get('Authorization')
|
||||||
if not auth or not check_auth(auth):
|
if not auth or not check_auth(auth):
|
||||||
return jsonify({"message": "Invalid request parameters"}), 401
|
return jsonify({"message": "Invalid request"}), 401
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ from flask import request, jsonify
|
|||||||
def enforce_json():
|
def enforce_json():
|
||||||
"""Middleware to enforce JSON Content-Type for incoming requests"""
|
"""Middleware to enforce JSON Content-Type for incoming requests"""
|
||||||
if request.method in ["POST", "PUT", "PATCH"] and request.content_type != "application/json":
|
if request.method in ["POST", "PUT", "PATCH"] and request.content_type != "application/json":
|
||||||
return jsonify({"message": "Invalid request parameters"}), 400
|
return jsonify({"message": "Invalid request"}), 400
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ def require_api_key(f):
|
|||||||
|
|
||||||
if not api_key:
|
if not api_key:
|
||||||
logger.error("Unauthorized access: Missing API key.")
|
logger.error("Unauthorized access: Missing API key.")
|
||||||
return jsonify({"message": "Invalid request parameters"}), 400
|
return jsonify({"message": "Invalid request"}), 400
|
||||||
|
|
||||||
if api_key != VALID_API_KEY:
|
if api_key != VALID_API_KEY:
|
||||||
logger.error("Unauthorized access: Invalid API key.")
|
logger.error("Unauthorized access: Invalid API key.")
|
||||||
return jsonify({"message": "Invalid request parameters"}), 400
|
return jsonify({"message": "Invalid request"}), 400
|
||||||
|
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from app.api.integrations.events_service import EventServiceIntegration
|
||||||
|
from app.api.integrations.simbrella import SimbrellaIntegration
|
||||||
from flask import Blueprint, request, jsonify, send_from_directory
|
from flask import Blueprint, request, jsonify, send_from_directory
|
||||||
from app.api.services import (
|
from app.api.services import (
|
||||||
EligibilityCheckService,
|
EligibilityCheckService,
|
||||||
@@ -19,6 +21,9 @@ from flask_jwt_extended import (
|
|||||||
get_jwt_identity,
|
get_jwt_identity,
|
||||||
create_refresh_token,
|
create_refresh_token,
|
||||||
)
|
)
|
||||||
|
from sqlalchemy import text
|
||||||
|
from app.extensions import db
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
|
|
||||||
api = Blueprint("api", __name__)
|
api = Blueprint("api", __name__)
|
||||||
@@ -88,7 +93,8 @@ def loan_status():
|
|||||||
@jwt_required()
|
@jwt_required()
|
||||||
def repayment():
|
def repayment():
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
# logger.info(f"Repayment request received: {data}")
|
# logger.error(f"Loan Repayment Data: {data} ")
|
||||||
|
logger.info(f"Repayment request received: {data}")
|
||||||
response = RepaymentService.process_request(data)
|
response = RepaymentService.process_request(data)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -112,11 +118,77 @@ def notification_callback():
|
|||||||
response = NotificationCallbackService.process_request(data)
|
response = NotificationCallbackService.process_request(data)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
# Health Check Endpoint
|
# Health Check Endpoint
|
||||||
@api.route("/health", methods=["GET"])
|
@api.route("/health", methods=["GET"])
|
||||||
def health_check():
|
def health_check():
|
||||||
return {"status": "ok"}, 200
|
SQLALCHEMY_DATABASE_URI = settings.SQLALCHEMY_DATABASE_URI
|
||||||
|
response = {}
|
||||||
|
db_status = "Connection Successful"
|
||||||
|
events_service_status = "Connection Successful"
|
||||||
|
bank_status = "Connection Successful"
|
||||||
|
errors = []
|
||||||
|
status = "ok"
|
||||||
|
|
||||||
|
|
||||||
|
# Extract the database URI
|
||||||
|
try:
|
||||||
|
db_uri = db.engine.url.render_as_string(hide_password=False)
|
||||||
|
db_uri = db_uri
|
||||||
|
except Exception as e:
|
||||||
|
db_uri = "Unavailable"
|
||||||
|
|
||||||
|
|
||||||
|
# Check database connection
|
||||||
|
try:
|
||||||
|
logger.info(f"Database Health == : {SQLALCHEMY_DATABASE_URI}")
|
||||||
|
db.session.execute(text("SELECT table_name FROM user_tables ORDER BY table_name"))
|
||||||
|
except Exception as e:
|
||||||
|
db_status = "Connection Failed"
|
||||||
|
errors.append(f"Database Error: {str(e)}")
|
||||||
|
status = "failed"
|
||||||
|
|
||||||
|
|
||||||
|
# Check Events Service health
|
||||||
|
try:
|
||||||
|
events_service_response = EventServiceIntegration.health_check()
|
||||||
|
|
||||||
|
if events_service_response.status_code != 200:
|
||||||
|
events_service_status = "Connection Failed"
|
||||||
|
status = "failed"
|
||||||
|
errors.append(f"Events Service response: {events_service_response.text}")
|
||||||
|
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
events_service_status = "Connection Failed"
|
||||||
|
status = "failed"
|
||||||
|
errors.append(f"Events Service connection failed: {str(e)}")
|
||||||
|
|
||||||
|
# Check Bank health
|
||||||
|
try:
|
||||||
|
emulator_response = SimbrellaIntegration.health_check()
|
||||||
|
|
||||||
|
if emulator_response.status_code != 200:
|
||||||
|
bank_status = "Connection Failed"
|
||||||
|
status = "failed"
|
||||||
|
errors.append(f"Bank Connection response: {emulator_response.text}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
bank_status = "Connection Failed"
|
||||||
|
status = "failed"
|
||||||
|
errors.append(f"Connection to Bank failed: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"status": status,
|
||||||
|
"db_status": db_status,
|
||||||
|
"events_service_status": events_service_status,
|
||||||
|
"bank_status": bank_status,
|
||||||
|
"db_uri": db_uri,
|
||||||
|
"errors": errors or None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return jsonify(response), 200 if status == "ok" else 500
|
||||||
|
|
||||||
|
|
||||||
# Authorize endpoint
|
# Authorize endpoint
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from marshmallow import Schema, fields
|
|||||||
# Loan Information Schema
|
# Loan Information Schema
|
||||||
class LoanStatusSchema(Schema):
|
class LoanStatusSchema(Schema):
|
||||||
transactionId = fields.Str(required=True)
|
transactionId = fields.Str(required=True)
|
||||||
|
accountId = fields.Str(required=True)
|
||||||
customerId = fields.Str(required=True)
|
customerId = fields.Str(required=True)
|
||||||
msisdn = fields.Str(required=False)
|
msisdn = fields.Str(required=False)
|
||||||
channel = fields.Str(required=True)
|
channel = fields.Str(required=True)
|
||||||
@@ -8,9 +8,9 @@ class ProvideLoanSchema(Schema):
|
|||||||
customerId = fields.Str(required=True)
|
customerId = fields.Str(required=True)
|
||||||
accountId = fields.Str(required=True)
|
accountId = fields.Str(required=True)
|
||||||
msisdn = fields.Str(required=False)
|
msisdn = fields.Str(required=False)
|
||||||
productId = fields.Str(required=True)
|
# productId = fields.Str(required=True)
|
||||||
lienAmount = fields.Float(required=True)
|
# lienAmount = fields.Float(required=True)
|
||||||
requestedAmount = fields.Float(required=True)
|
requestedAmount = fields.Float(required=True)
|
||||||
collectionType = fields.Int(required=True)
|
collectionType = fields.Int(required=True)
|
||||||
offerId = fields.Int(required=True)
|
offerId = fields.Str(required=True)
|
||||||
channel = fields.Str(required=True)
|
channel = fields.Str(required=True)
|
||||||
@@ -5,7 +5,8 @@ class RepaymentSchema(Schema):
|
|||||||
type = fields.Str(required=False)
|
type = fields.Str(required=False)
|
||||||
msisdn = fields.Str(required=False) #optional
|
msisdn = fields.Str(required=False) #optional
|
||||||
debtId = fields.Str(required=True)
|
debtId = fields.Str(required=True)
|
||||||
productId = fields.Str(required=True)
|
|
||||||
transactionId = fields.Str(required=True)
|
transactionId = fields.Str(required=True)
|
||||||
|
accountId = fields.Str(required=True)
|
||||||
customerId = fields.Str(required=True)
|
customerId = fields.Str(required=True)
|
||||||
channel = fields.Str(required=True)
|
loanRef = fields.Str(required=True)
|
||||||
|
initiatedBy = fields.Str(required=False)
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ class SelectOfferSchema(Schema):
|
|||||||
msisdn = fields.Str(required=True)
|
msisdn = fields.Str(required=True)
|
||||||
requestedAmount = fields.Float(required=True)
|
requestedAmount = fields.Float(required=True)
|
||||||
productId = fields.Str(required=True)
|
productId = fields.Str(required=True)
|
||||||
|
offerId = fields.Str(required=True)
|
||||||
channel = fields.Str(required=True)
|
channel = fields.Str(required=True)
|
||||||
|
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ from app.api.services.repayment import RepaymentService
|
|||||||
from app.api.services.customer_consent import CustomerConsentService
|
from app.api.services.customer_consent import CustomerConsentService
|
||||||
from app.api.services.notification_callback import NotificationCallbackService
|
from app.api.services.notification_callback import NotificationCallbackService
|
||||||
from app.api.services.authorization import AuthorizationService
|
from app.api.services.authorization import AuthorizationService
|
||||||
|
from app.api.services.offer_analysis import OfferAnalysis
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class AuthorizationService(BaseService):
|
|||||||
logger.info("Processing Authorization request")
|
logger.info("Processing Authorization request")
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
return ResponseHelper.bad_request(message="Missing JSON in request")
|
return ResponseHelper.bad_request(result_description="Missing JSON in request")
|
||||||
|
|
||||||
# Validate input data using the Authorization schema
|
# Validate input data using the Authorization schema
|
||||||
schema = AuthorizeRequestSchema()
|
schema = AuthorizeRequestSchema()
|
||||||
@@ -44,7 +44,7 @@ class AuthorizationService(BaseService):
|
|||||||
validated_data["username"] != USERNAME
|
validated_data["username"] != USERNAME
|
||||||
or validated_data["password"] != PASSWORD
|
or validated_data["password"] != PASSWORD
|
||||||
):
|
):
|
||||||
return ResponseHelper.unauthorized(message="Invalid credentials")
|
return ResponseHelper.unauthorized(result_description="Invalid credentials")
|
||||||
|
|
||||||
access_token = create_access_token(identity=validated_data["username"])
|
access_token = create_access_token(identity=validated_data["username"])
|
||||||
refresh_token = create_refresh_token(identity=validated_data["username"])
|
refresh_token = create_refresh_token(identity=validated_data["username"])
|
||||||
@@ -56,17 +56,17 @@ class AuthorizationService(BaseService):
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ResponseHelper.success(
|
return ResponseHelper.success(
|
||||||
data=response_data, message="Authorization processed successfully"
|
data={"data": response_data}, result_description="Authorization processed successfully"
|
||||||
)
|
)
|
||||||
|
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
logger.error(f"Validation error: {e}")
|
logger.error(f"Validation error: {e}")
|
||||||
return ResponseHelper.bad_request(message=f"Validation error: {e}")
|
return ResponseHelper.bad_request(result_description=f"Validation error: {e}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing Authorization request: {e}")
|
logger.error(f"Error processing Authorization request: {e}")
|
||||||
return ResponseHelper.internal_server_error(
|
return ResponseHelper.internal_server_error(
|
||||||
message=f"Error processing Authorization request: {e}"
|
result_description=f"Error processing Authorization request: {e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -92,11 +92,11 @@ class AuthorizationService(BaseService):
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ResponseHelper.success(
|
return ResponseHelper.success(
|
||||||
data=response_data, message="RefreshToken processed successfully"
|
data={"data": response_data}, result_description="RefreshToken processed successfully"
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing RefreshToken request: {e}")
|
logger.error(f"Error processing RefreshToken request: {e}")
|
||||||
return ResponseHelper.internal_server_error(
|
return ResponseHelper.internal_server_error(
|
||||||
message=f"Error processing RefreshToken request: {e}"
|
result_description=f"Error processing RefreshToken request: {e}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ from app.api.enums import TransactionType
|
|||||||
from flask import jsonify
|
from flask import jsonify
|
||||||
from marshmallow import ValidationError
|
from marshmallow import ValidationError
|
||||||
import logging
|
import logging
|
||||||
|
from app.api.integrations import KafkaIntegration
|
||||||
|
from app.utils.mail import send_report_email, get_report_data
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -47,9 +49,131 @@ class BaseService:
|
|||||||
"""
|
"""
|
||||||
Create a new transaction.
|
Create a new transaction.
|
||||||
"""
|
"""
|
||||||
|
channel = "USSD" if validated_data.get("channel") is None else validated_data.get("channel")
|
||||||
|
|
||||||
return Transaction.create_transaction(
|
return Transaction.create_transaction(
|
||||||
id=validated_data.get("transactionId"),
|
transaction_id = validated_data.get("transactionId"),
|
||||||
account_id=validated_data.get("accountId"),
|
customer_id = validated_data.get('customerId', None),
|
||||||
type=cls.TRANSACTION_TYPE,
|
account_id = validated_data.get("accountId", None),
|
||||||
channel=validated_data.get("channel"),
|
type = cls.TRANSACTION_TYPE,
|
||||||
|
channel = channel,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def async_send_to_kafka(cls, loan_data, request_id, topic):
|
||||||
|
KafkaIntegration.send_loan_request(loan_data = loan_data, request_id = request_id, topic = topic)
|
||||||
|
KafkaIntegration.flush()
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def calculate_charges(cls, offer, amount):
|
||||||
|
"""
|
||||||
|
Calculates and returns the charges for the given offer and amount.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
offer (Offer): The offer object that contains the charges.
|
||||||
|
amount (float): The requested loan amount.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: A dictionary containing the calculated charges.
|
||||||
|
"""
|
||||||
|
if not offer or not offer.charges:
|
||||||
|
logger.error(f"No charges found for offer ID {offer.id}")
|
||||||
|
return {"error": "No charges found for the offer"}
|
||||||
|
|
||||||
|
loan_charges = offer.charges
|
||||||
|
tenor = offer.schedule # offer.tenor // 30 # Convert to months
|
||||||
|
interest = cls.get_charge_detail(rates = offer.interest_rate, charges = loan_charges, code = "INTEREST", amount = amount)
|
||||||
|
management = cls.get_charge_detail(rates = offer.management_rate, charges = loan_charges, code = "MGTFEE", amount = amount)
|
||||||
|
insurance = cls.get_charge_detail(rates = offer.insurance_rate, charges = loan_charges, code = "INSURANCE", amount = amount)
|
||||||
|
vat = cls.get_charge_detail(rates = offer.vat_rate, charges = loan_charges, code = "VAT", amount = amount, management_fee = management["fee"])
|
||||||
|
|
||||||
|
# Separate fees into upfront and postpaid
|
||||||
|
upfront_fees = [
|
||||||
|
fee["fee"]
|
||||||
|
for fee in [interest, management, insurance, vat]
|
||||||
|
if fee["due_days"] == 0
|
||||||
|
]
|
||||||
|
|
||||||
|
postpaid_fees = [
|
||||||
|
fee["fee"]
|
||||||
|
for fee in [interest, management, insurance, vat]
|
||||||
|
if fee["due_days"] != 0
|
||||||
|
]
|
||||||
|
vat_test = vat["fee"]
|
||||||
|
logger.info(f"VAT fee == *************** : {vat_test}")
|
||||||
|
|
||||||
|
# Up-front payment: (only those fees due immediately i.e due_days == 0)
|
||||||
|
# upfront_payment = sum(upfront_fees)
|
||||||
|
if offer.schedule == 1:
|
||||||
|
upfront_payment = vat["fee"] + management["fee"] + insurance["fee"] + interest["fee"]
|
||||||
|
interest_amount = interest["fee"]
|
||||||
|
repayment_amount = amount
|
||||||
|
else:
|
||||||
|
upfront_payment = vat["fee"] + insurance["fee"]+management["fee"]
|
||||||
|
interest_amount = interest["fee"]*offer.schedule
|
||||||
|
repayment_amount = amount + interest_amount
|
||||||
|
|
||||||
|
|
||||||
|
# Repayment amount: (principal + only those fees not due immediately i.e due_days != 0)
|
||||||
|
# repayment_amount = amount + (sum(postpaid_fees) * tenor)
|
||||||
|
|
||||||
|
# Total amount: (upfront_payment + repayment_amount)
|
||||||
|
total_amount = upfront_payment + repayment_amount
|
||||||
|
|
||||||
|
# Calculate the installment amount
|
||||||
|
installment_amount = repayment_amount / offer.schedule
|
||||||
|
|
||||||
|
return {
|
||||||
|
"interest": interest,
|
||||||
|
"interest_amount": interest_amount,
|
||||||
|
"management": management,
|
||||||
|
"insurance": insurance,
|
||||||
|
"vat": vat,
|
||||||
|
"upfront_payment": round(upfront_payment, 2),
|
||||||
|
"repayment_amount": round(repayment_amount, 2),
|
||||||
|
"installment_amount": round(installment_amount, 2),
|
||||||
|
"total_amount": round(total_amount, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_charge_detail(cls, rates, charges, code, amount, management_fee= 0.0):
|
||||||
|
"""
|
||||||
|
Get details for a specific charge code from a list of loan charges.
|
||||||
|
|
||||||
|
Returns default values if not found.
|
||||||
|
"""
|
||||||
|
fee = 0.0
|
||||||
|
|
||||||
|
if code == "VAT" and management_fee > 0:
|
||||||
|
fee = management_fee * rates / 100
|
||||||
|
else:
|
||||||
|
fee = amount * rates / 100
|
||||||
|
|
||||||
|
return {
|
||||||
|
"rate": rates,
|
||||||
|
"fee": round(fee, 2),
|
||||||
|
"due_days": 30,
|
||||||
|
"code": code,
|
||||||
|
"description" : "have no idea how to get this yet"
|
||||||
|
}
|
||||||
|
|
||||||
|
# if charge.code == code:
|
||||||
|
# if code == "VAT" and management_fee > 0:
|
||||||
|
# fee = management_fee * rates / 100
|
||||||
|
# else:
|
||||||
|
# fee = amount * rates / 100
|
||||||
|
#
|
||||||
|
# return {
|
||||||
|
# "rate": rates,
|
||||||
|
# "fee": round(fee, 2),
|
||||||
|
# "due_days": charge.due
|
||||||
|
# }
|
||||||
|
|
||||||
|
# return {"rate": 0, "fee": 0, "due_days": 0}
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def send_mail(cls, report_data, recipients):
|
||||||
|
send_report_email(report_data, recipients)
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
from flask import request, jsonify
|
from flask import request, jsonify
|
||||||
|
from app.api.helpers.response_helper import ResponseHelper
|
||||||
from app.api.services.base_service import BaseService
|
from app.api.services.base_service import BaseService
|
||||||
from marshmallow import ValidationError
|
from marshmallow import ValidationError
|
||||||
from app.utils.logger import logger
|
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.services.base_service import BaseService
|
||||||
from app.api.enums import TransactionType
|
from app.api.enums import TransactionType
|
||||||
|
from app.extensions import db
|
||||||
|
|
||||||
|
|
||||||
class CustomerConsentService(BaseService):
|
class CustomerConsentService(BaseService):
|
||||||
@@ -22,50 +24,37 @@ class CustomerConsentService(BaseService):
|
|||||||
dict: A standardized response.
|
dict: A standardized response.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
with db.session.begin():
|
||||||
|
validated_data = CustomerConsentService.validate_data(data, CustomerConsentSchema())
|
||||||
|
account_id = validated_data.get('accountId')
|
||||||
|
customer_id = validated_data.get('customerId')
|
||||||
|
|
||||||
validated_data = CustomerConsentService.validate_data(data, CustomerConsentSchema())
|
if(CustomerConsentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
||||||
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)
|
||||||
transaction = CustomerConsentService.log_transaction(validated_data = validated_data)
|
|
||||||
|
|
||||||
if not transaction:
|
if not transaction:
|
||||||
logger.error(f"Failed to log transaction")
|
logger.error(f"Failed to log transaction")
|
||||||
return jsonify({
|
return ResponseHelper.error(result_description="Failed to log transaction.")
|
||||||
"message": "Failed to log transaction."
|
else:
|
||||||
}), 400
|
return ResponseHelper.error(result_description="Invalid Customer or Account")
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
"message": "Invalid Customer or Account"
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
|
|
||||||
# Simulated processing logic
|
db.session.commit()
|
||||||
response_data = {
|
return ResponseHelper.success(result_description="Request is received")
|
||||||
"resultCode": "00",
|
|
||||||
"resultDescription": "Request is received"
|
|
||||||
}
|
|
||||||
|
|
||||||
return response_data
|
|
||||||
|
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
|
|
||||||
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
||||||
|
db.session.rollback()
|
||||||
return jsonify({
|
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
|
||||||
"message": "Validation exception"
|
|
||||||
}) , 422
|
|
||||||
|
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
logger.error(f"{getattr(err, 'messages', str(err))}")
|
logger.error(f"{getattr(err, 'messages', str(err))}")
|
||||||
|
db.session.rollback()
|
||||||
return jsonify({
|
return ResponseHelper.error(result_description=str(err))
|
||||||
"message": str(err)
|
|
||||||
}) , 400
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
||||||
return jsonify({
|
db.session.rollback()
|
||||||
"message": "Internal Server Error"
|
return ResponseHelper.internal_server_error()
|
||||||
}) , 500
|
|
||||||
@@ -1,10 +1,18 @@
|
|||||||
from flask import session, jsonify
|
from flask import session, jsonify
|
||||||
|
from app.models.loan import Loan
|
||||||
from app.utils.logger import logger
|
from app.utils.logger import logger
|
||||||
from app.api.services.base_service import BaseService
|
from app.api.services.base_service import BaseService
|
||||||
from app.api.schemas.eligibility_check import EligibilityCheckSchema
|
from app.api.schemas.eligibility_check import EligibilityCheckSchema
|
||||||
from marshmallow import ValidationError
|
from marshmallow import ValidationError
|
||||||
from app.api.enums import TransactionType
|
from app.api.enums import TransactionType
|
||||||
from app.api.integrations import SimbrellaClient
|
from app.api.integrations import SimbrellaIntegration
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models import Offer, RACCheck
|
||||||
|
from app.api.services.offer_analysis import OfferAnalysis
|
||||||
|
from app.api.helpers.response_helper import ResponseHelper
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
class EligibilityCheckService(BaseService):
|
class EligibilityCheckService(BaseService):
|
||||||
TRANSACTION_TYPE = TransactionType.ELIGIBILITY_CHECK
|
TRANSACTION_TYPE = TransactionType.ELIGIBILITY_CHECK
|
||||||
@@ -21,85 +29,182 @@ class EligibilityCheckService(BaseService):
|
|||||||
dict: A standardized response.
|
dict: A standardized response.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
with db.session.begin():
|
||||||
|
|
||||||
validated_data = EligibilityCheckService.validate_data(data, EligibilityCheckSchema())
|
validated_data = EligibilityCheckService.validate_data(data, EligibilityCheckSchema())
|
||||||
account_id = validated_data.get('accountId')
|
account_id = validated_data.get('accountId')
|
||||||
customer_id = validated_data.get('customerId')
|
customer_id = validated_data.get('customerId')
|
||||||
|
transactionId = validated_data.get('transactionId')
|
||||||
|
msisdn = validated_data.get('msisdn')
|
||||||
|
|
||||||
customer = EligibilityCheckService.get_or_create_customer(validated_data = validated_data)
|
customer = EligibilityCheckService.get_or_create_customer(validated_data = validated_data)
|
||||||
|
|
||||||
if (EligibilityCheckService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
if (EligibilityCheckService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
||||||
transaction = EligibilityCheckService.log_transaction(validated_data = validated_data)
|
|
||||||
|
|
||||||
if not transaction:
|
transaction = EligibilityCheckService.log_transaction(validated_data = validated_data)
|
||||||
logger.error(f"Failed to log transaction")
|
|
||||||
return jsonify({
|
|
||||||
"message": "Failed to log transaction."
|
|
||||||
}), 400
|
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
"message": "Invalid Customer or Account"
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
# Call RACCheck
|
if not transaction:
|
||||||
response = SimbrellaClient.rac_check(
|
logger.error(f"Failed to log transaction")
|
||||||
customer_id = customer_id,
|
return ResponseHelper.error(result_description="Failed to log transaction.")
|
||||||
account_id = account_id,
|
else:
|
||||||
transaction_id = transaction.id,
|
return ResponseHelper.error(result_description="Invalid Customer or Account")
|
||||||
)
|
|
||||||
|
|
||||||
if "error" in response or response.get("status") != 200:
|
db.session.flush()
|
||||||
return jsonify({"message": "RACCheck failed", "error": response.get("message", response)}), 400
|
|
||||||
|
# Determine if there is any loan of 3MPC active
|
||||||
|
current_loan = EligibilityCheckService.get_current_active_loans_by_account_id(account_id = account_id)
|
||||||
|
if current_loan:
|
||||||
|
logger.info(f"Account {current_loan.account_id} has active loan {current_loan}")
|
||||||
|
if current_loan.product_id =='3MPC':
|
||||||
|
return ResponseHelper.error(result_description="CUSTOMER HAS EXCEEDED THE NUMBER OF DISBURSALS: Disbursal Count 1")
|
||||||
|
|
||||||
|
# Determine Loan count
|
||||||
|
is_eligible, count = EligibilityCheckService.check_loan_limits(customer_id)
|
||||||
|
|
||||||
|
if not is_eligible:
|
||||||
|
return ResponseHelper.error(result_description=f"CUSTOMER HAS EXCEEDED THE NUMBER OF DISBURSALS FOR THE DAY: Disbursal Count Today {count}")
|
||||||
|
|
||||||
|
# Call RACCheck
|
||||||
|
response = SimbrellaIntegration.rac_check(
|
||||||
|
customer_id = customer_id,
|
||||||
|
account_id = account_id,
|
||||||
|
transaction_id = transaction.transaction_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# this chek for error is not valid
|
||||||
|
if response.status_code != 200:
|
||||||
|
return ResponseHelper.error(result_description="RACCheck failed")
|
||||||
|
|
||||||
|
response = response.json()
|
||||||
|
|
||||||
|
logger.info(f"This is Response (from Eligibility Check): {str(response)}", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
if not response or response['responseCode'] != '00':
|
||||||
|
|
||||||
offers = [
|
if response:
|
||||||
{
|
logger.error(f"{response['responseMessage']}")
|
||||||
"offerId": "Offer1",
|
|
||||||
"productId": "Product1",
|
|
||||||
"minAamount": 100,
|
|
||||||
"maxAamount": 1000,
|
|
||||||
"tenor": 12
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"offerId": "Offer2",
|
|
||||||
"productId": "Product2",
|
|
||||||
"minAamount": 200,
|
|
||||||
"maxAamount": 2000,
|
|
||||||
"tenor": 24
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Simulate processing
|
return ResponseHelper.error(result_description=f"RACCheck failed")
|
||||||
response_data = {
|
|
||||||
"customerId": "CN621868",
|
rack_checks_response = response['data']['racResponse']
|
||||||
"transactionId": "TX12345",
|
|
||||||
"countryCode": "NG",
|
rac_check = RACCheck.add_rac_check(
|
||||||
"msisdn": "3451342",
|
customer_id = customer_id,
|
||||||
"eligibleOffers": offers,
|
account_id = account_id,
|
||||||
"resultDescription": "Successful",
|
transaction_id = transaction.transaction_id,
|
||||||
"resultCode": "00",
|
data = rack_checks_response
|
||||||
"accountId": "ACN8263457"
|
)
|
||||||
}
|
|
||||||
|
if not rac_check:
|
||||||
|
logger.error(f"Failed to save RACCheck")
|
||||||
|
return ResponseHelper.error(result_description="Failed to save RACCheck.")
|
||||||
|
|
||||||
|
# -----------------TIME FOR ANALYSIS TO REGISTER OFFER ----------------------
|
||||||
|
# eligible_offers = []
|
||||||
|
try:
|
||||||
|
eligible_offers = OfferAnalysis.decide_offer(
|
||||||
|
transaction_id=transactionId,
|
||||||
|
rac_check=rac_check,
|
||||||
|
validated_data=validated_data,
|
||||||
|
customer_id=customer_id,
|
||||||
|
rack_checks_response =rack_checks_response
|
||||||
|
)
|
||||||
|
except ValueError as ve:
|
||||||
|
logger.error(str(ve))
|
||||||
|
return ResponseHelper.error(result_description= str(ve))
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# s = Offer.get_all_offers()
|
||||||
|
|
||||||
|
# eligible_offers = []
|
||||||
|
|
||||||
|
# for offer in offers:
|
||||||
|
# # Determine an approved amount
|
||||||
|
# random_float = random.random() # temporary to play data
|
||||||
|
# approved_amount = min(offer.max_amount, offer.max_amount * random_float) #temporary for now
|
||||||
|
# approved_amount = round(approved_amount, 2)
|
||||||
|
#
|
||||||
|
# transaction_offer = TransactionOffer.create_transaction_offer(
|
||||||
|
# customer_id = customer.id,
|
||||||
|
# transaction_id = transaction.transaction_id,
|
||||||
|
# offer_id = offer.id,
|
||||||
|
# min_amount = offer.min_amount,
|
||||||
|
# max_amount = offer.max_amount,
|
||||||
|
# eligible_amount = approved_amount,
|
||||||
|
# product_id = offer.product_id,
|
||||||
|
# tenor = offer.tenor
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # Visible offer ID: offer_id + padded(transaction_offer.id)
|
||||||
|
# padded_id = str(transaction_offer.id).zfill(6)
|
||||||
|
# public_offer_id = f"{offer.id}{padded_id}"
|
||||||
|
#
|
||||||
|
# eligible_offers.append({
|
||||||
|
# "offerId": public_offer_id,
|
||||||
|
# "product_id": offer.product_id,
|
||||||
|
# "min_amount": offer.min_amount,
|
||||||
|
# "max_amount": approved_amount,
|
||||||
|
# "tenor": offer.tenor
|
||||||
|
# })
|
||||||
|
|
||||||
|
# Simulate processing
|
||||||
|
response_data = {
|
||||||
|
"customerId": customer_id,
|
||||||
|
"transactionId": transactionId,
|
||||||
|
"countryCode": "NG",
|
||||||
|
"msisdn": msisdn,
|
||||||
|
"eligibleOffers": eligible_offers,
|
||||||
|
"accountId": account_id
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseHelper.success(data=response_data)
|
||||||
|
|
||||||
return response_data
|
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
|
|
||||||
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
||||||
|
db.session.rollback()
|
||||||
return jsonify({
|
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
|
||||||
"message": "Validation exception"
|
|
||||||
}) , 422
|
|
||||||
|
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
logger.error(f"{getattr(err, 'messages', str(err))}")
|
logger.error(f"{getattr(err, 'messages', str(err))}")
|
||||||
|
db.session.rollback()
|
||||||
return jsonify({
|
return ResponseHelper.error(result_description=str(err))
|
||||||
"message": str(err)
|
|
||||||
}) , 400
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
||||||
return jsonify({
|
db.session.rollback()
|
||||||
"message": "Internal Server Error"
|
return ResponseHelper.internal_server_error()
|
||||||
}) , 500
|
|
||||||
|
@staticmethod
|
||||||
|
def get_current_active_loans_by_account_id(account_id):
|
||||||
|
current_loan = Loan.get_current_active_loans_by_account_id(account_id)
|
||||||
|
return current_loan
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_loan_limits(customer_id):
|
||||||
|
"""
|
||||||
|
Checks if a customer has exceeded the loan limits for given offer.
|
||||||
|
"""
|
||||||
|
loan = Loan.get_customer_last_loan(customer_id)
|
||||||
|
|
||||||
|
if not loan:
|
||||||
|
return True, 0
|
||||||
|
|
||||||
|
offer_id = loan.offer_id[:5]
|
||||||
|
|
||||||
|
offer = Offer.get_offer_by_id(offer_id)
|
||||||
|
if not offer:
|
||||||
|
logger.error(f"Offer not found for offer_id: {offer_id} (customer_id: {customer_id})")
|
||||||
|
return False, 0
|
||||||
|
|
||||||
|
daily_count = Loan.get_daily_loan_count(customer_id, offer.product_id)
|
||||||
|
|
||||||
|
|
||||||
|
logger.info(f"daily_count: {daily_count}, Max: {offer.max_daily_loans}")
|
||||||
|
|
||||||
|
if offer.max_daily_loans is not None and daily_count >= offer.max_daily_loans:
|
||||||
|
return False, daily_count
|
||||||
|
|
||||||
|
|
||||||
|
return True, daily_count
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
from flask import request, jsonify
|
from flask import request, jsonify
|
||||||
from marshmallow import ValidationError
|
from marshmallow import ValidationError
|
||||||
|
from app.api.enums.loan_status import LoanStatus
|
||||||
|
from app.models import Customer
|
||||||
from app.utils.logger import logger
|
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.services.base_service import BaseService
|
||||||
from app.api.enums import TransactionType
|
from app.api.enums import TransactionType
|
||||||
|
from app.extensions import db
|
||||||
|
from app.api.helpers.response_helper import ResponseHelper
|
||||||
|
|
||||||
|
|
||||||
class LoanStatusService(BaseService):
|
class LoanStatusService(BaseService):
|
||||||
@@ -21,67 +25,90 @@ class LoanStatusService(BaseService):
|
|||||||
dict: A standardized response.
|
dict: A standardized response.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
validated_data = LoanStatusService.validate_data(data, LoanStatusSchema())
|
with db.session.begin():
|
||||||
account_id = validated_data.get('accountId')
|
# Validate data
|
||||||
customer_id = validated_data.get('customerId')
|
validated_data = LoanStatusService.validate_data(
|
||||||
|
data, LoanStatusSchema()
|
||||||
|
)
|
||||||
|
|
||||||
if (LoanStatusService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
customer_id = validated_data.get("customerId")
|
||||||
transaction = LoanStatusService.log_transaction(validated_data = validated_data)
|
|
||||||
|
|
||||||
if not transaction:
|
logger.info(f"Looking for customer *** {customer_id}")
|
||||||
logger.error(f"Failed to log transaction")
|
customer = Customer.get_customer_with_loan_list(customer_id)
|
||||||
return jsonify({
|
|
||||||
"message": "Failed to log transaction."
|
|
||||||
}), 400
|
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
"message": "Invalid Customer or Account"
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
|
transactionId = validated_data.get("transactionId")
|
||||||
|
account_id = validated_data.get("accountId")
|
||||||
|
|
||||||
loans = [
|
if LoanStatusService.validate_account_ownership(
|
||||||
{
|
account_id=account_id, customer_id=customer_id
|
||||||
"debtId": "123456789",
|
):
|
||||||
"loanDate": "2019-10-18 14:26:21.063",
|
# Get loans
|
||||||
"dueDate": "2019-11-20 14:26:21.063",
|
customer_loans = customer.loans
|
||||||
"currentLoanAmount": 8500,
|
loans = [
|
||||||
"initialLoanAmount": 10000,
|
loan.to_dict()
|
||||||
"defaultPenaltyFee": 0,
|
for loan in customer_loans
|
||||||
"continuousFee": 0,
|
if loan.status in [LoanStatus.ACTIVE, LoanStatus.START_REPAY, LoanStatus.ACTIVE_PARTIAL]
|
||||||
"productId": "101"
|
]
|
||||||
|
|
||||||
|
transaction = LoanStatusService.log_transaction(
|
||||||
|
validated_data=validated_data
|
||||||
|
)
|
||||||
|
if not transaction:
|
||||||
|
logger.error(f"Failed to log transaction")
|
||||||
|
return ResponseHelper.error(
|
||||||
|
result_description="Failed to log transaction."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return ResponseHelper.error(
|
||||||
|
result_description="Invalid Customer or Account"
|
||||||
|
)
|
||||||
|
|
||||||
|
# CONFIRM IF THE TOTAL DEBT IF FOR ONLY ACTIVE LOANS OR ALL LOANS
|
||||||
|
total_debt_amount = sum(
|
||||||
|
loan.get("currentLoanAmount") or 0 for loan in loans
|
||||||
|
)
|
||||||
|
|
||||||
|
total_outstanding_amount = sum(
|
||||||
|
loan.get("currentLoanAmount") or 0 for loan in loans
|
||||||
|
)
|
||||||
|
|
||||||
|
total_active_loan_amount = sum(
|
||||||
|
loan.get("repaymentAmount") or 0 for loan in loans
|
||||||
|
)
|
||||||
|
|
||||||
|
total_settled_amount = total_active_loan_amount - total_outstanding_amount
|
||||||
|
|
||||||
|
# Simulated processing logic
|
||||||
|
response_data = {
|
||||||
|
"customerId": customer_id,
|
||||||
|
"accountId": account_id,
|
||||||
|
"transactionId": transactionId,
|
||||||
|
"loans": loans,
|
||||||
|
"totalDebtAmount": total_debt_amount,
|
||||||
|
"summary": {
|
||||||
|
"totalSettledAmount": total_settled_amount,
|
||||||
|
"totalOutstandingAmount": total_outstanding_amount,
|
||||||
|
"totalActiveLoanAmount": total_active_loan_amount,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
|
||||||
|
|
||||||
# Simulated processing logic
|
db.session.commit()
|
||||||
response_data = {
|
return ResponseHelper.success(data=response_data)
|
||||||
"customerId": "CN621868",
|
|
||||||
"transactionId": "Tr201712RK9232P115",
|
|
||||||
"loans": loans,
|
|
||||||
"totalDebtAmount": 8500,
|
|
||||||
"resultCode": "00",
|
|
||||||
"resultDescription": "Successful"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return response_data
|
|
||||||
|
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
|
|
||||||
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
||||||
|
db.session.rollback()
|
||||||
|
return ResponseHelper.unprocessable_entity(
|
||||||
|
result_description="Validation exception"
|
||||||
|
)
|
||||||
|
|
||||||
return jsonify({
|
except ValueError as err:
|
||||||
"message": "Validation exception"
|
|
||||||
}) , 422
|
|
||||||
|
|
||||||
except ValueError as err:
|
|
||||||
logger.error(f"{getattr(err, 'messages', str(err))}")
|
logger.error(f"{getattr(err, 'messages', str(err))}")
|
||||||
|
db.session.rollback()
|
||||||
return jsonify({
|
return ResponseHelper.error(result_description=str(err))
|
||||||
"message": str(err)
|
|
||||||
}) , 400
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
||||||
return jsonify({
|
db.session.rollback()
|
||||||
"message": "Internal Server Error"
|
return ResponseHelper.internal_server_error()
|
||||||
}) , 500
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ from app.api.services.base_service import BaseService
|
|||||||
from app.api.enums import TransactionType
|
from app.api.enums import TransactionType
|
||||||
from app.utils.logger import logger
|
from app.utils.logger import logger
|
||||||
from app.api.schemas.notification_callback import NotificationCallbackSchema
|
from app.api.schemas.notification_callback import NotificationCallbackSchema
|
||||||
|
from app.extensions import db
|
||||||
|
from app.api.helpers.response_helper import ResponseHelper
|
||||||
|
|
||||||
class NotificationCallbackService(BaseService):
|
class NotificationCallbackService(BaseService):
|
||||||
TRANSACTION_TYPE = TransactionType.NOTIFICATION_CALLBACK
|
TRANSACTION_TYPE = TransactionType.NOTIFICATION_CALLBACK
|
||||||
@@ -26,37 +28,20 @@ class NotificationCallbackService(BaseService):
|
|||||||
schema = NotificationCallbackSchema()
|
schema = NotificationCallbackSchema()
|
||||||
validated_data = schema.load(data) # Raises ValidationError if invalid
|
validated_data = schema.load(data) # Raises ValidationError if invalid
|
||||||
|
|
||||||
# Simulated processing logic
|
return ResponseHelper.success()
|
||||||
response_data = {
|
|
||||||
"resultCode": "00",
|
|
||||||
"resultDescription": "Successful"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# return ResponseHelper.success(
|
|
||||||
# data=response_data,
|
|
||||||
# message="Notification callback processed successfully"
|
|
||||||
# )
|
|
||||||
|
|
||||||
return response_data
|
|
||||||
|
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
|
|
||||||
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
||||||
|
db.session.rollback()
|
||||||
return jsonify({
|
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
|
||||||
"message": "Validation exception"
|
|
||||||
}) , 422
|
|
||||||
|
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
logger.error(f"{getattr(err, 'messages', str(err))}")
|
logger.error(f"{getattr(err, 'messages', str(err))}")
|
||||||
|
db.session.rollback()
|
||||||
return jsonify({
|
return ResponseHelper.error(result_description=str(err))
|
||||||
"message": str(err)
|
|
||||||
}) , 400
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
||||||
return jsonify({
|
db.session.rollback()
|
||||||
"message": "Internal Server Error"
|
return ResponseHelper.internal_server_error()
|
||||||
}) , 500
|
|
||||||
|
|||||||
@@ -0,0 +1,263 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
from app.models import Offer, TransactionOffer
|
||||||
|
from app.models.loan import Loan
|
||||||
|
import random
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from app.config import Config
|
||||||
|
|
||||||
|
RAC_TRUE_CHECK_RULES = Config.rac_true_rules
|
||||||
|
RAC_FALSE_CHECK_RULES = Config.rac_false_rules
|
||||||
|
RAC_SALARY_PAYMENTS = Config.rac_salary_payments
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class OfferAnalysis:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_offer(transaction_id, rac_response, validated_data):
|
||||||
|
customer_id = validated_data.get("customerId")
|
||||||
|
product_id = validated_data.get("productId")
|
||||||
|
offer_id = validated_data.get("offerId")
|
||||||
|
|
||||||
|
transaction_offer_id = int(offer_id[5:]) # The last part is int
|
||||||
|
|
||||||
|
logger.info(f"customer_id == *************** : {customer_id}")
|
||||||
|
logger.info(f"product_id == *************** : {product_id}")
|
||||||
|
logger.info(f"offer_id == *************** : {offer_id}")
|
||||||
|
logger.info(f"transaction_offer_id == *************** : {transaction_offer_id}")
|
||||||
|
|
||||||
|
transaction_offer = TransactionOffer.is_valid_transaction_offer(transaction_offer_id, customer_id, product_id)
|
||||||
|
|
||||||
|
if not transaction_offer:
|
||||||
|
raise ValueError("Invalid Transaction Offer.")
|
||||||
|
|
||||||
|
eligible_amount = transaction_offer.eligible_amount
|
||||||
|
offer = Offer.is_valid_offer( transaction_offer.offer_id)
|
||||||
|
|
||||||
|
if not offer:
|
||||||
|
raise ValueError("Invalid Offer.")
|
||||||
|
original_transaction = transaction_id
|
||||||
|
|
||||||
|
return transaction_offer, offer, eligible_amount, original_transaction
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _analyze_rack_checks(rack_response, offer):
|
||||||
|
logger.info(f"This is PayLoad for ANALYSYS ***** : {str(rack_response)}", exc_info=True)
|
||||||
|
logger.info(f"RACk TRUE RULES {str(RAC_TRUE_CHECK_RULES)}", exc_info=True)
|
||||||
|
logger.info(f"RACk FALSE RULES {str(RAC_FALSE_CHECK_RULES)}", exc_info=True)
|
||||||
|
logger.info(f"RACk SALARY PAYMENTS {str(RAC_SALARY_PAYMENTS)}", exc_info=True)
|
||||||
|
|
||||||
|
if not isinstance(rack_response, dict) or not offer :
|
||||||
|
raise ValueError("Invalid RAC response format.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
failed_true_rules = []
|
||||||
|
failed_false_rules = []
|
||||||
|
salaries = []
|
||||||
|
|
||||||
|
# Expects true
|
||||||
|
for rule in RAC_TRUE_CHECK_RULES:
|
||||||
|
if not rack_response.get(rule, False):
|
||||||
|
failed_true_rules.append(rule)
|
||||||
|
|
||||||
|
# Expects false
|
||||||
|
for rule in RAC_FALSE_CHECK_RULES:
|
||||||
|
if rack_response.get(rule, True):
|
||||||
|
failed_false_rules.append(rule)
|
||||||
|
|
||||||
|
|
||||||
|
# Salary rules
|
||||||
|
for key in RAC_SALARY_PAYMENTS:
|
||||||
|
value = rack_response.get(key)
|
||||||
|
|
||||||
|
|
||||||
|
if isinstance(value, Decimal):
|
||||||
|
# Only use values greater than 0
|
||||||
|
if value > 0:
|
||||||
|
salaries.append(value)
|
||||||
|
elif isinstance(value, (int, float, str)):
|
||||||
|
try:
|
||||||
|
value = Decimal(str(value))
|
||||||
|
if value > 0:
|
||||||
|
salaries.append(value)
|
||||||
|
except:
|
||||||
|
logger.warning(f"Could not convert value of {key} to Decimal: {value}")
|
||||||
|
|
||||||
|
|
||||||
|
if failed_true_rules or failed_false_rules or not salaries:
|
||||||
|
logger.warning(f"Failed TRUE rules: {failed_true_rules}")
|
||||||
|
logger.warning(f"Failed FALSE rules: {failed_false_rules}")
|
||||||
|
logger.warning("No salary records found in RAC response.")
|
||||||
|
raise ValueError(f"RAC analysis failed")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
logger.info(f"These are the salary amounts ***** : {str(salaries)}", exc_info=True)
|
||||||
|
|
||||||
|
#Least salary in the last 6 months
|
||||||
|
min_salary = min(salaries)
|
||||||
|
|
||||||
|
# Check consistency rule
|
||||||
|
consistent_income = rack_response.get("rule7_consistent_salary_amount", False)
|
||||||
|
|
||||||
|
# Determine percentage based on offer tenor
|
||||||
|
tenor = offer.tenor
|
||||||
|
|
||||||
|
if tenor == 30 and consistent_income:
|
||||||
|
eligible_amount = min_salary * Decimal("0.5")
|
||||||
|
logger.info("Applying 50% of least salary in 6 months due to 1-month offer tenor with stable income.")
|
||||||
|
elif tenor == 90 and consistent_income:
|
||||||
|
eligible_amount = min_salary * Decimal("0.75")
|
||||||
|
logger.info("Applying 75% of least salary in 6 months due to 3-months offer tenor with stable income.")
|
||||||
|
|
||||||
|
else: # Income is not consistent
|
||||||
|
eligible_amount = 0
|
||||||
|
logger.info("Applying no percentage on least salary due unstable income.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
logger.info(f"Calculated eligible amount from RAC: {eligible_amount} based on {'stable' if consistent_income else 'unstable'} income.")
|
||||||
|
|
||||||
|
return eligible_amount.quantize(Decimal("1.00"))
|
||||||
|
|
||||||
|
# "racResponse": {
|
||||||
|
# "accountStatus": true,
|
||||||
|
# "bvnValidated": true,
|
||||||
|
# "creditBureauCheck": false,
|
||||||
|
# "crmsCheck": true,
|
||||||
|
# "hasLien": false,
|
||||||
|
# "hasPastDueLoan": false,
|
||||||
|
# "hasSalaryAccount": true,
|
||||||
|
# "isWhitelisted": true,
|
||||||
|
# "noBouncedCheck": true
|
||||||
|
# },
|
||||||
|
#
|
||||||
|
|
||||||
|
'''
|
||||||
|
30 days
|
||||||
|
Eligibility amount (monthly SOL) - Adoption of 50% of the least salary inflow in the past 6 months
|
||||||
|
to determine loan eligibility for potential customers.
|
||||||
|
|
||||||
|
3 months
|
||||||
|
Adoption of 75% of the least salary inflow in the past 6 months to determine loan eligibility for
|
||||||
|
potential customers" for customers that have unstable income. 3 months
|
||||||
|
'''
|
||||||
|
# rac_true_rules
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decide_offer(transaction_id, rac_check, validated_data, customer_id, rack_checks_response):
|
||||||
|
eligible_offers = []
|
||||||
|
# if we have active offers - we have to feed off it
|
||||||
|
logger.info(f"**RACK ANALYSIS** {customer_id}")
|
||||||
|
# Analyze Rack Checks
|
||||||
|
# new_eligible_amount = OfferAnalysis._analyze_rack_checks(rack_checks_response) #--> We need detail analysis
|
||||||
|
|
||||||
|
# we can now find the origin transactions
|
||||||
|
# Find the last loan - it will have original_transaction
|
||||||
|
last_customer_loan = Loan.get_customer_last_loan(customer_id)
|
||||||
|
# logger.info(f"{last_customer_loan}")
|
||||||
|
|
||||||
|
if last_customer_loan:
|
||||||
|
original_transaction = last_customer_loan.original_transaction or last_customer_loan.transaction_id
|
||||||
|
logger.info(f"transaction_id |-| original_transaction === > {transaction_id} {original_transaction}")
|
||||||
|
original_loan = Loan.get_customer_original_loan(customer_id, original_transaction)
|
||||||
|
if original_loan is not None:
|
||||||
|
logger.info(f"original_loan === > {original_loan}")
|
||||||
|
logger.info(f"loan_offer_id === > {original_loan.offer_id}")
|
||||||
|
|
||||||
|
original_offer_id = str(original_loan.offer_id[:5]) # The last part is str
|
||||||
|
transaction_offer_id = int(original_loan.offer_id[5:]) # The last part is int
|
||||||
|
original_transaction_offer = TransactionOffer.is_valid_transaction_offer(transaction_offer_id, customer_id, original_loan.product_id)
|
||||||
|
|
||||||
|
active_loans = Loan.get_active_loans_by_original_transaction(original_transaction)
|
||||||
|
sum_active_loans = sum(loan.current_loan_amount for loan in active_loans)
|
||||||
|
logger.info(f"sum_active_loans === > {sum_active_loans}")
|
||||||
|
real_eligible_amount = original_loan.eligible_amount - sum_active_loans
|
||||||
|
|
||||||
|
if real_eligible_amount < original_transaction_offer.min_amount:
|
||||||
|
logger.error(f"Max eligible amount ({real_eligible_amount}) is less than the minimum offer amount ({original_transaction_offer.min_amount}).")
|
||||||
|
raise ValueError("You are not eligible for a loan at this time - Minimum amount not met.")
|
||||||
|
|
||||||
|
# if real_eligible_amount < 100:
|
||||||
|
# logger.error(f"Max eligible amount ({real_eligible_amount}) is less than the minimum offer amount ({original_transaction_offer.min_amount}).")
|
||||||
|
# raise ValueError("You are not eligible for a loan at this time.")
|
||||||
|
|
||||||
|
transaction_offer = TransactionOffer.create_transaction_offer(
|
||||||
|
customer_id=customer_id,
|
||||||
|
transaction_id=transaction_id,
|
||||||
|
original_transaction=original_transaction,
|
||||||
|
offer_id=original_offer_id,
|
||||||
|
min_amount=original_transaction_offer.min_amount,
|
||||||
|
max_amount=original_transaction_offer.max_amount,
|
||||||
|
eligible_amount=real_eligible_amount,
|
||||||
|
product_id=original_loan.product_id,
|
||||||
|
tenor=original_loan.tenor
|
||||||
|
)
|
||||||
|
|
||||||
|
# Visible offer ID: offer_id + padded(transaction_offer.id)
|
||||||
|
padded_id = str(transaction_offer.id).zfill(6)
|
||||||
|
public_offer_id = f"{original_offer_id}{padded_id}"
|
||||||
|
|
||||||
|
eligible_offers.append({
|
||||||
|
"offerId": public_offer_id,
|
||||||
|
"product_id": original_transaction_offer.product_id,
|
||||||
|
"min_amount": original_transaction_offer.min_amount,
|
||||||
|
"max_amount": round(real_eligible_amount, 2),
|
||||||
|
"tenor": original_loan.tenor
|
||||||
|
})
|
||||||
|
return eligible_offers
|
||||||
|
|
||||||
|
|
||||||
|
offers = Offer.get_all_offers()
|
||||||
|
|
||||||
|
|
||||||
|
for offer in offers:
|
||||||
|
|
||||||
|
new_eligible_amount = OfferAnalysis._analyze_rack_checks(rack_checks_response, offer)
|
||||||
|
|
||||||
|
|
||||||
|
approved_amount = new_eligible_amount
|
||||||
|
approved_amount = round(approved_amount, 2)
|
||||||
|
|
||||||
|
if approved_amount < offer.min_amount:
|
||||||
|
logger.error(f"Max eligible amount ({approved_amount}) is less than the minimum offer amount ({offer.min_amount}).")
|
||||||
|
continue
|
||||||
|
# raise ValueError("You are not eligible for a loan at this time.")
|
||||||
|
|
||||||
|
# if approved_amount < 100:
|
||||||
|
# logger.error(f"Max eligible amount ({approved_amount}) is less than the minimum offer amount ({offer.min_amount}).")
|
||||||
|
# raise ValueError("You are not eligible for a loan at this time.")
|
||||||
|
|
||||||
|
transaction_offer = TransactionOffer.create_transaction_offer(
|
||||||
|
customer_id=customer_id,
|
||||||
|
transaction_id=transaction_id,
|
||||||
|
original_transaction=transaction_id,
|
||||||
|
offer_id=offer.id,
|
||||||
|
min_amount=offer.min_amount,
|
||||||
|
max_amount=offer.max_amount,
|
||||||
|
eligible_amount=approved_amount,
|
||||||
|
product_id=offer.product_id,
|
||||||
|
tenor=offer.tenor
|
||||||
|
)
|
||||||
|
|
||||||
|
# Visible offer ID: offer_id + padded(transaction_offer.id)
|
||||||
|
padded_id = str(transaction_offer.id).zfill(6)
|
||||||
|
public_offer_id = f"{offer.id}{padded_id}"
|
||||||
|
|
||||||
|
eligible_offers.append({
|
||||||
|
"offerId": public_offer_id,
|
||||||
|
"product_id": offer.product_id,
|
||||||
|
"min_amount": offer.min_amount,
|
||||||
|
"max_amount": approved_amount,
|
||||||
|
"tenor": offer.tenor
|
||||||
|
})
|
||||||
|
|
||||||
|
if not eligible_offers:
|
||||||
|
logger.error("No eligible offers found for customer: {customer_id} - Minimum amount not met")
|
||||||
|
raise ValueError("You are not eligible for a loan at this time - Minimum amount not met")
|
||||||
|
|
||||||
|
return eligible_offers
|
||||||
@@ -1,9 +1,23 @@
|
|||||||
|
from gettext import install
|
||||||
from flask import request, jsonify
|
from flask import request, jsonify
|
||||||
from marshmallow import ValidationError
|
from marshmallow import ValidationError
|
||||||
|
from app.api.integrations.kafka import KafkaIntegration
|
||||||
from app.api.services.base_service import BaseService
|
from app.api.services.base_service import BaseService
|
||||||
from app.api.enums import TransactionType
|
from app.api.enums import TransactionType
|
||||||
|
from app.models.customer import Customer
|
||||||
|
from app.models.loan_charge import LoanCharge
|
||||||
from app.utils.logger import logger
|
from app.utils.logger import logger
|
||||||
from app.api.schemas.provide_loan import ProvideLoanSchema
|
from app.api.schemas.provide_loan import ProvideLoanSchema
|
||||||
|
from threading import Thread
|
||||||
|
from app.models import Loan, Offer, Charge , TransactionOffer, RACCheck
|
||||||
|
from app.api.enums import LoanStatus
|
||||||
|
from app.extensions import db
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from app.api.integrations import EventServiceIntegration
|
||||||
|
from app.models import LoanRepaymentSchedule
|
||||||
|
from app.api.services.offer_analysis import OfferAnalysis
|
||||||
|
from app.api.helpers.response_helper import ResponseHelper
|
||||||
|
|
||||||
class ProvideLoanService(BaseService):
|
class ProvideLoanService(BaseService):
|
||||||
TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN
|
TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN
|
||||||
@@ -21,55 +35,256 @@ class ProvideLoanService(BaseService):
|
|||||||
dict: A standardized response.
|
dict: A standardized response.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
validated_data = ProvideLoanService.validate_data(data, ProvideLoanSchema())
|
with db.session.begin():
|
||||||
account_id = validated_data.get('accountId')
|
validated_data = ProvideLoanService.validate_data(data, ProvideLoanSchema())
|
||||||
customer_id = validated_data.get('customerId')
|
account_id = validated_data.get('accountId')
|
||||||
|
customer_id = validated_data.get('customerId')
|
||||||
|
request_id = validated_data.get('requestId')
|
||||||
|
collection_type = validated_data.get('collectionType')
|
||||||
|
transaction_id = validated_data.get('transactionId')
|
||||||
|
offer_id = validated_data.get('offerId')
|
||||||
|
amount = validated_data.get("requestedAmount")
|
||||||
|
product_id = validated_data.get("productId")
|
||||||
|
channel = validated_data.get('channel')
|
||||||
|
|
||||||
if (ProvideLoanService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
customer = Customer.is_valid_customer(customer_id)
|
||||||
transaction = ProvideLoanService.log_transaction(validated_data = validated_data)
|
|
||||||
|
|
||||||
if not transaction:
|
if (ProvideLoanService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
||||||
logger.error(f"Failed to log transaction")
|
|
||||||
return jsonify({
|
rac_response = RACCheck.get_rac_check(customer_id = customer_id, account_id = account_id)
|
||||||
"message": "Failed to log transaction."
|
|
||||||
}), 400
|
try:
|
||||||
else:
|
transaction_offer, offer, eligible_amount, original_transaction = OfferAnalysis.get_offer(
|
||||||
return jsonify({
|
transaction_id=transaction_id,
|
||||||
"message": "Invalid Customer or Account"
|
rac_response=rac_response,
|
||||||
}), 400
|
validated_data=validated_data
|
||||||
|
)
|
||||||
|
except ValueError as ve:
|
||||||
|
logger.error(str(ve))
|
||||||
|
return ResponseHelper.error(result_description=str(ve))
|
||||||
|
|
||||||
|
|
||||||
response_data = {
|
if(amount < transaction_offer.min_amount):
|
||||||
"requestId": "202111170001371256908",
|
return ResponseHelper.error(result_description="The amount is less than the minimum allowed transaction amount.")
|
||||||
"transactionId": "Tr201712RK9232P115",
|
elif amount > transaction_offer.max_amount:
|
||||||
"customerId": "CN621868",
|
return ResponseHelper.error(result_description="The amount is greater than the maximum allowed transaction amount.")
|
||||||
"accountId": "ACN8263457",
|
|
||||||
"msisdn": "3451342",
|
|
||||||
"resultCode": "00",
|
|
||||||
"resultDescription": "Successful"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
# transaction_offer_id = int(offer_id[5:]) # The last part is int
|
||||||
|
|
||||||
return response_data
|
# transaction_offer = TransactionOffer.is_valid_transaction_offer(transaction_offer_id)
|
||||||
|
|
||||||
|
# if not transaction_offer:
|
||||||
|
# logger.error(f"Invalid Transaction Offer")
|
||||||
|
# return jsonify({
|
||||||
|
# "message": "Invalid Transaction Offer."
|
||||||
|
# }), 400
|
||||||
|
|
||||||
|
# eligible_amount = transaction_offer.eligible_amount
|
||||||
|
# offer = Offer.is_valid_offer( transaction_offer.offer_id)
|
||||||
|
|
||||||
|
# if not offer:
|
||||||
|
# logger.error(f"Invalid Offer")
|
||||||
|
# return jsonify({
|
||||||
|
# "message": "Invalid Offer."
|
||||||
|
# }), 400
|
||||||
|
|
||||||
|
|
||||||
|
# Log Transaction
|
||||||
|
transaction = ProvideLoanService.log_transaction(validated_data=validated_data)
|
||||||
|
|
||||||
|
if not transaction:
|
||||||
|
logger.error(f"Failed to log transaction")
|
||||||
|
return ResponseHelper.error(result_description="Failed to log transaction.")
|
||||||
|
|
||||||
|
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
charges = ProvideLoanService.calculate_charges(offer, amount)
|
||||||
|
upfront_fee = charges["upfront_payment"]
|
||||||
|
repayment_amount = charges["repayment_amount"]
|
||||||
|
#installment_amount = charges["installment_amount"]
|
||||||
|
num_schedules = offer.schedule
|
||||||
|
|
||||||
|
upfront_payment = charges["upfront_payment"]
|
||||||
|
total_amount = charges["total_amount"]
|
||||||
|
installment_amount = charges["installment_amount"]
|
||||||
|
interest = charges["interest"]
|
||||||
|
management = charges["management"]
|
||||||
|
insurance = charges["insurance"]
|
||||||
|
vat = charges["vat"]
|
||||||
|
|
||||||
|
padded_id = str(transaction_id).zfill(12)
|
||||||
|
loan_ref = f"{padded_id}{channel}{offer.product_id}"
|
||||||
|
|
||||||
|
|
||||||
|
# Save the loan details
|
||||||
|
loan = Loan.create_loan(
|
||||||
|
customer_id = customer_id,
|
||||||
|
account_id = account_id,
|
||||||
|
offer_id = offer_id,
|
||||||
|
product_id = offer.product_id,
|
||||||
|
collection_type = collection_type,
|
||||||
|
transaction_id = validated_data.get('transactionId'),
|
||||||
|
original_transaction = transaction_offer.original_transaction,
|
||||||
|
initial_loan_amount = validated_data.get('requestedAmount'),
|
||||||
|
upfront_fee = upfront_fee,
|
||||||
|
repayment_amount = repayment_amount,
|
||||||
|
installment_amount = installment_amount,
|
||||||
|
eligible_amount=eligible_amount,
|
||||||
|
status = LoanStatus.ACTIVE,
|
||||||
|
tenor = offer.tenor,
|
||||||
|
reference = loan_ref
|
||||||
|
)
|
||||||
|
|
||||||
|
if not loan:
|
||||||
|
logger.error(f"Failed to save loan details")
|
||||||
|
|
||||||
|
return ResponseHelper.error(result_description="Failed to save loan details.")
|
||||||
|
|
||||||
|
db.session.flush()
|
||||||
|
current_product_id = offer.product_id
|
||||||
|
schedules = LoanRepaymentSchedule.add_repayment_schedule(loan = loan, num_schedules = num_schedules, transaction_id = transaction_id)
|
||||||
|
|
||||||
|
|
||||||
|
if not schedules:
|
||||||
|
logger.error(f"Failed to create repayment schedule for loan ID {loan.id}")
|
||||||
|
return ResponseHelper.error(result_description="Failed to generate loan repayment schedule.")
|
||||||
|
|
||||||
|
# charges = Charge.get_offer_charges(offer.id)
|
||||||
|
|
||||||
|
# logger.info(f"{charges}")
|
||||||
|
|
||||||
|
loan_id = loan.id
|
||||||
|
|
||||||
|
loan_charges = LoanCharge.create_charges_for_loan(loan_id = loan_id, transaction_id = transaction_id, referenced_amount = 800, charges = charges)
|
||||||
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
return ResponseHelper.error(result_description="Invalid Customer or Account")
|
||||||
|
|
||||||
|
|
||||||
|
charge_schedule_items = ProvideLoanService.get_charge_schedule_items(
|
||||||
|
loan_charges=loan_charges,
|
||||||
|
offer=offer,
|
||||||
|
loan_ref=loan_ref,
|
||||||
|
amount=amount,
|
||||||
|
schedules=schedules
|
||||||
|
)
|
||||||
|
|
||||||
|
response_data = {
|
||||||
|
"requestId": request_id,
|
||||||
|
"transactionId": transaction_id,
|
||||||
|
"loanRef": loan_ref,
|
||||||
|
"customerId": customer_id,
|
||||||
|
"accountId": account_id,
|
||||||
|
"msisdn": customer.msisdn,
|
||||||
|
"schedule": charge_schedule_items,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
event_thread = Thread(target=ProvideLoanService.trigger_loan_disbursement, args=(transaction_id,))
|
||||||
|
event_thread.start()
|
||||||
|
|
||||||
|
# KafkaIntegration.send_loan_request(loan_data = response_data, request_id = request_id)
|
||||||
|
# Call Kafka in a background thread
|
||||||
|
kafka_thread = Thread(target=ProvideLoanService.async_send_to_kafka, args=(response_data, request_id, "PROCESS_PAYMENT"))
|
||||||
|
kafka_thread.start()
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return ResponseHelper.success(data=response_data)
|
||||||
|
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
|
|
||||||
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
||||||
|
db.session.rollback()
|
||||||
return jsonify({
|
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
|
||||||
"message": "Validation exception"
|
|
||||||
}) , 422
|
|
||||||
|
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
logger.error(f"{getattr(err, 'messages', str(err))}")
|
logger.error(f"{getattr(err, 'messages', str(err))}")
|
||||||
|
db.session.rollback()
|
||||||
return jsonify({
|
return ResponseHelper.error(result_description=str(err))
|
||||||
"message": str(err)
|
|
||||||
}) , 400
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
||||||
return jsonify({
|
db.session.rollback()
|
||||||
"message": "Internal Server Error"
|
return ResponseHelper.internal_server_error()
|
||||||
}) , 500
|
|
||||||
|
@classmethod
|
||||||
|
def trigger_loan_disbursement(cls, transaction_id: str):
|
||||||
|
response = EventServiceIntegration.direct_loan(transaction_id=transaction_id)
|
||||||
|
return response
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_charge_schedule_items(cls, loan_charges, offer, loan_ref, amount, schedules):
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
due_date = now + timedelta(days=offer.tenor)
|
||||||
|
id_counter = 1
|
||||||
|
|
||||||
|
charge_schedule_items = []
|
||||||
|
|
||||||
|
charge_schedule_items.append({
|
||||||
|
"id": id_counter,
|
||||||
|
"dueDate": due_date.isoformat(),
|
||||||
|
"amountDue": amount,
|
||||||
|
"componentName": "PRINCIPAL",
|
||||||
|
"startDate": now.isoformat(),
|
||||||
|
})
|
||||||
|
|
||||||
|
interest_amount = 0.0
|
||||||
|
|
||||||
|
for charge in loan_charges:
|
||||||
|
|
||||||
|
code = charge.code.upper()
|
||||||
|
|
||||||
|
if code == "INTEREST":
|
||||||
|
interest_amount = float(charge.amount)
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
item = {
|
||||||
|
"id": id_counter,
|
||||||
|
"dueDate": charge.due_date.isoformat(),
|
||||||
|
"amountDue": float(charge.amount),
|
||||||
|
"componentName": charge.code.upper(), # e.g. INTEREST, MGMT_FEE, VAT_FEE
|
||||||
|
"startDate": charge.created_at.isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
charge_schedule_items.append(item)
|
||||||
|
id_counter += 1
|
||||||
|
|
||||||
|
|
||||||
|
num_schedules = len(schedules)
|
||||||
|
if num_schedules > 0:
|
||||||
|
principal_per_schedule = amount / num_schedules
|
||||||
|
else:
|
||||||
|
principal_per_schedule = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
for schedule in schedules:
|
||||||
|
default = {
|
||||||
|
"id": id_counter,
|
||||||
|
"installmentNo": schedule.installment_number,
|
||||||
|
"dueDate": schedule.due_date.isoformat(),
|
||||||
|
"amountDue": round(principal_per_schedule, 2),
|
||||||
|
"componentName": "DEFAULT",
|
||||||
|
"startDate": schedule.created_at.isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
charge_schedule_items.append(default)
|
||||||
|
id_counter += 1
|
||||||
|
|
||||||
|
interest = {
|
||||||
|
"id": id_counter,
|
||||||
|
"dueDate": schedule.due_date.isoformat(),
|
||||||
|
"amountDue": round(interest_amount, 2),
|
||||||
|
"componentName": "INTEREST",
|
||||||
|
"startDate": schedule.created_at.isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
charge_schedule_items.append(interest)
|
||||||
|
id_counter += 1
|
||||||
|
|
||||||
|
return charge_schedule_items
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+115
-38
@@ -1,12 +1,23 @@
|
|||||||
|
from app.api.integrations.simbrella import SimbrellaIntegration
|
||||||
from flask import request, jsonify
|
from flask import request, jsonify
|
||||||
from marshmallow import ValidationError
|
from marshmallow import ValidationError
|
||||||
|
from app.api.enums.loan_status import LoanStatus
|
||||||
|
from app.api.helpers.response_helper import ResponseHelper
|
||||||
|
from app.models import Repayment
|
||||||
|
from app.models.customer import Customer
|
||||||
|
from app.models.loan import Loan
|
||||||
from app.utils.logger import logger
|
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.services.base_service import BaseService
|
||||||
from app.api.enums import TransactionType
|
from app.api.enums import TransactionType
|
||||||
|
from threading import Thread
|
||||||
|
from app.extensions import db
|
||||||
|
from app.api.integrations import EventServiceIntegration
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
class RepaymentService(BaseService):
|
class RepaymentService(BaseService):
|
||||||
TRANSACTION_TYPE = TransactionType.REPAYMENT
|
TRANSACTION_TYPE = TransactionType.REPAYMENT
|
||||||
|
ENABLE_ACCOUNT_BALANCE_CHECK = settings.ENABLE_ACCOUNT_BALANCE_CHECK
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def process_request(data):
|
def process_request(data):
|
||||||
@@ -20,56 +31,122 @@ class RepaymentService(BaseService):
|
|||||||
dict: A standardized response.
|
dict: A standardized response.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
validated_data = RepaymentService.validate_data(data, RepaymentSchema())
|
with db.session.begin():
|
||||||
account_id = validated_data.get('accountId')
|
validated_data = RepaymentService.validate_data(data, RepaymentSchema())
|
||||||
customer_id = validated_data.get('customerId')
|
|
||||||
|
|
||||||
if (RepaymentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
customer_id = validated_data.get('customerId')
|
||||||
transaction = RepaymentService.log_transaction(validated_data = validated_data)
|
request_id = validated_data.get('requestId')
|
||||||
|
loan_id = validated_data.get('debtId')
|
||||||
|
account_id = validated_data.get('accountId')
|
||||||
|
loan_ref = validated_data.get('loanRef')
|
||||||
|
# customer = Customer.get_customer_with_loan_list(customer_id)
|
||||||
|
transaction_id = validated_data.get('transactionId')
|
||||||
|
initiated_by = validated_data.get('initiatedBy')
|
||||||
|
logger.error(f"RepaymentService Received **** {data}")
|
||||||
|
|
||||||
if not transaction:
|
if(RepaymentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
||||||
logger.error(f"Failed to log transaction")
|
logger.error(f"HERE 0001a **** ")
|
||||||
return jsonify({
|
# Check loan exists
|
||||||
"message": "Failed to log transaction."
|
load_loan = Loan.get_customer_loan(loan_id = loan_id, customer_id = customer_id)
|
||||||
}), 400
|
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
"message": "Invalid Customer or Account"
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
# Simulated processing logic
|
|
||||||
response_data = {
|
|
||||||
"customerId": "CN621868",
|
|
||||||
"productId": "101",
|
|
||||||
"debtId": "273194670",
|
|
||||||
"resultCode": "00",
|
|
||||||
"resultDescription": "Successful"
|
|
||||||
}
|
|
||||||
|
|
||||||
# return ResponseHelper.success(
|
# Check Customer Account Balance if enabled
|
||||||
# data=response_data,
|
if RepaymentService.ENABLE_ACCOUNT_BALANCE_CHECK:
|
||||||
# message="Repayment processed successfully"
|
response = SimbrellaIntegration.verify_account_balance(
|
||||||
# )
|
account_id = account_id,
|
||||||
|
amount = load_loan.balance,
|
||||||
|
request_id = request_id,
|
||||||
|
)
|
||||||
|
|
||||||
return response_data
|
# this check for error is not valid
|
||||||
|
if response.status_code != 200:
|
||||||
|
return ResponseHelper.error(result_description="Balance Check failed")
|
||||||
|
|
||||||
|
response = response.json()
|
||||||
|
|
||||||
|
logger.info(f"This is Response (Balance Check): {str(response)}", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
if not response or response['responseCode'] != '00':
|
||||||
|
|
||||||
|
if response:
|
||||||
|
logger.error(f"{response['responseMessage']}")
|
||||||
|
|
||||||
|
return ResponseHelper.error(result_description=f"Balance Check failed")
|
||||||
|
|
||||||
|
verify_account_balance_response = response['isSufficient']
|
||||||
|
|
||||||
|
if not verify_account_balance_response or verify_account_balance_response in [False, "false"]:
|
||||||
|
logger.error(f"Balance Check failed: Insufficient Account Balance")
|
||||||
|
return ResponseHelper.error(result_description=f"Insufficient Account Balance")
|
||||||
|
|
||||||
|
|
||||||
|
# Save the repayment details
|
||||||
|
repayment = Repayment.create_repayment(
|
||||||
|
customer_id = customer_id,
|
||||||
|
loan = load_loan,
|
||||||
|
transaction_id = transaction_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not repayment:
|
||||||
|
logger.error(f"Failed to save repayment details")
|
||||||
|
return ResponseHelper.error(result_description="Failed to save repayment details.")
|
||||||
|
|
||||||
|
loan_transaction_id = load_loan.transaction_id
|
||||||
|
|
||||||
|
#Update Loan status
|
||||||
|
Loan.update_status(loan_id = loan_id, status = LoanStatus.START_REPAY) # repay started by user
|
||||||
|
transaction = RepaymentService.log_transaction(validated_data = validated_data)
|
||||||
|
|
||||||
|
if not transaction:
|
||||||
|
logger.error(f"Failed to log transaction")
|
||||||
|
return ResponseHelper.error(result_description="Failed to log transaction.")
|
||||||
|
else:
|
||||||
|
logger.error(f"Invalid Customer or AccountID {account_id} to CustomerID{customer_id} ")
|
||||||
|
return ResponseHelper.error(result_description="Invalid Customer or Account")
|
||||||
|
|
||||||
|
# Simulated processing logic
|
||||||
|
# TODO start using repayment_id instead if id or Id
|
||||||
|
response_data = {
|
||||||
|
"Id": repayment.id,
|
||||||
|
"repayment_id": repayment.id,
|
||||||
|
"initiated_by": repayment.initiated_by,
|
||||||
|
"transactionId": loan_transaction_id,
|
||||||
|
"customerId": customer_id,
|
||||||
|
"productId": load_loan.product_id,
|
||||||
|
"loanRef": loan_ref,
|
||||||
|
"debtId": loan_id
|
||||||
|
}
|
||||||
|
|
||||||
|
event_thread = Thread(target=RepaymentService.trigger_loan_repayment, args=(loan_transaction_id,))
|
||||||
|
event_thread.start()
|
||||||
|
|
||||||
|
# Call Kafka in a background thread
|
||||||
|
kafka_thread = Thread(target=RepaymentService.async_send_to_kafka, args=(response_data, request_id, "LOAN_REPAYMENT"))
|
||||||
|
kafka_thread.start()
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return ResponseHelper.success(data=response_data)
|
||||||
|
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
|
|
||||||
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
||||||
|
db.session.rollback()
|
||||||
return jsonify({
|
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
|
||||||
"message": "Validation exception"
|
|
||||||
}) , 422
|
|
||||||
|
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
logger.error(f"{getattr(err, 'messages', str(err))}")
|
logger.error(f"{getattr(err, 'messages', str(err))}")
|
||||||
|
db.session.rollback()
|
||||||
return jsonify({
|
return ResponseHelper.error(result_description=str(err))
|
||||||
"message": str(err)
|
|
||||||
}) , 400
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
||||||
return jsonify({
|
db.session.rollback()
|
||||||
"message": "Internal Server Error"
|
return ResponseHelper.internal_server_error()
|
||||||
}) , 500
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def trigger_loan_repayment(cls, transaction_id: str):
|
||||||
|
response = EventServiceIntegration.direct_repayment(transaction_id=transaction_id)
|
||||||
|
return response
|
||||||
|
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
from flask import request, jsonify
|
from flask import request, jsonify
|
||||||
from marshmallow import ValidationError
|
from marshmallow import ValidationError
|
||||||
|
from app.api.helpers.response_helper import ResponseHelper
|
||||||
from app.api.services.base_service import BaseService
|
from app.api.services.base_service import BaseService
|
||||||
from app.api.enums import TransactionType
|
from app.api.enums import TransactionType
|
||||||
|
from app.models.transaction_offers import TransactionOffer
|
||||||
from app.utils.logger import logger
|
from app.utils.logger import logger
|
||||||
from app.api.schemas.select_offer import SelectOfferSchema
|
from app.api.schemas.select_offer import SelectOfferSchema
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models import Offer
|
||||||
|
from datetime import date
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
class SelectOfferService(BaseService):
|
class SelectOfferService(BaseService):
|
||||||
TRANSACTION_TYPE = TransactionType.SELECT_OFFER
|
TRANSACTION_TYPE = TransactionType.SELECT_OFFER
|
||||||
@@ -20,74 +26,144 @@ class SelectOfferService(BaseService):
|
|||||||
dict: A standardized response.
|
dict: A standardized response.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
validated_data = SelectOfferService.validate_data(data, SelectOfferSchema())
|
with db.session.begin():
|
||||||
account_id = validated_data.get('accountId')
|
validated_data = SelectOfferService.validate_data(
|
||||||
customer_id = validated_data.get('customerId')
|
data, SelectOfferSchema()
|
||||||
|
)
|
||||||
|
account_id = validated_data.get("accountId")
|
||||||
|
customer_id = validated_data.get("customerId")
|
||||||
|
amount = validated_data.get("requestedAmount")
|
||||||
|
product_id = validated_data.get("productId")
|
||||||
|
transaction_offer_id = validated_data.get("offerId")
|
||||||
|
transaction_id = validated_data.get("transactionId")
|
||||||
|
request_id = validated_data.get("requestId")
|
||||||
|
|
||||||
if (SelectOfferService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
|
||||||
transaction = SelectOfferService.log_transaction(validated_data = validated_data)
|
|
||||||
|
|
||||||
if not transaction:
|
offer_id = int(transaction_offer_id[5:]) # The last part is int
|
||||||
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": "SAL30001129",
|
||||||
|
|
||||||
|
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 ResponseHelper.error(result_description="Failed to log transaction.")
|
||||||
|
else:
|
||||||
|
return ResponseHelper.error(result_description="Invalid Customer or Account")
|
||||||
|
|
||||||
|
# Get the offer by product ID
|
||||||
|
offer = Offer.get_offer_by_product_id(product_id)
|
||||||
|
|
||||||
|
transaction_offer = TransactionOffer.get_transaction_offer(transaction_offer_id=offer_id)
|
||||||
|
|
||||||
|
if not transaction_offer:
|
||||||
|
logger.error(f"offer {offer_id} not found for customer {customer_id} and transaction {transaction_id}.")
|
||||||
|
return ResponseHelper.error(result_description="Offer not found.")
|
||||||
|
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
if amount < transaction_offer.min_amount:
|
||||||
|
logger.error(f"The amount {amount} is less than the minimum allowed offer amount {transaction_offer.min_amount}.")
|
||||||
|
return ResponseHelper.error(result_description="The amount is less than the minimum allowed offer amount.")
|
||||||
|
elif amount > transaction_offer.eligible_amount:
|
||||||
|
logger.error(f"The amount {amount} is greater than the eligible offer amount {transaction_offer.eligible_amount}.")
|
||||||
|
return ResponseHelper.error(result_description="The amount is greater than the eligible offer amount.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
charges = SelectOfferService.calculate_charges(offer, amount)
|
||||||
|
upfront_payment = charges["upfront_payment"]
|
||||||
|
total_amount = charges["total_amount"]
|
||||||
|
installment_amount = charges["installment_amount"]
|
||||||
|
interest = charges["interest"]
|
||||||
|
management = charges["management"]
|
||||||
|
insurance = charges["insurance"]
|
||||||
|
vat = charges["vat"]
|
||||||
|
repayment_amount = charges["repayment_amount"]
|
||||||
|
interest_amount = charges["interest_amount"]
|
||||||
|
|
||||||
|
|
||||||
|
# Calculate the repayment dates
|
||||||
|
tenor = offer.tenor
|
||||||
|
start_date = date.today()
|
||||||
|
|
||||||
|
# Convert tenor to months
|
||||||
|
months = offer.schedule # tenor // 30
|
||||||
|
|
||||||
|
recommended_repayment_dates = [
|
||||||
|
(start_date + relativedelta(months=i + 1)).isoformat()
|
||||||
|
for i in range(months)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
offers = [
|
||||||
{
|
{
|
||||||
"offerId": "14451",
|
"offerId": transaction_offer_id,
|
||||||
"productId": "2030",
|
"productId": product_id,
|
||||||
"amount": 10000.0,
|
"amount": amount,
|
||||||
"upfrontPayment": 1000.0,
|
"upfrontPayment": upfront_payment,
|
||||||
"interestRate": 3.0,
|
"interestRate": offer.interest_rate,
|
||||||
"managementRate": 1.0,
|
"interestFee": interest_amount,
|
||||||
"managementFee": 1.0,
|
"managementRate": offer.management_rate,
|
||||||
"insuranceRate": 1.0,
|
"managementFee": management["fee"],
|
||||||
"insuranceFee": 100.0,
|
"insuranceRate": offer.insurance_rate,
|
||||||
"VATRate": 7.5,
|
"insuranceFee": insurance["fee"],
|
||||||
"VATAmount": 100.0,
|
"VATRate": offer.vat_rate,
|
||||||
"recommendedRepaymentDates": ["2022-11-30"],
|
"VATAmount": vat["fee"],
|
||||||
"installmentAmount": 11000.0,
|
"recommendedRepaymentDates": recommended_repayment_dates,
|
||||||
"totalRepaymentAmount": 11000.0
|
"repaymentAmount": repayment_amount,
|
||||||
|
"installmentAmount": installment_amount,
|
||||||
|
"totalRepaymentAmount": total_amount,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
# Business logic - selecting an offer
|
# "offerId": offer.id,
|
||||||
response_data = {
|
# "productId": product_id,
|
||||||
"outstandingDebtAmount": 0,
|
# "amount": amount,
|
||||||
"requestId": "202111170001371256908",
|
# "upfrontPayment": upfront_payment,
|
||||||
"transactionId": "1231231321232",
|
# "interestRate": interest["rate"],
|
||||||
"customerId": "1256907",
|
# "managementRate": management["rate"],
|
||||||
"accountId": "5948306019",
|
# "managementFee": management["fee"],
|
||||||
"loan": offers,
|
# "insuranceRate": insurance["rate"],
|
||||||
"resultCode": "00",
|
# "insuranceFee": insurance["fee"],
|
||||||
"resultDescription": "Successful"
|
# "VATRate": vat["rate"],
|
||||||
|
# "VATAmount": vat["fee"],
|
||||||
|
# "recommendedRepaymentDates": recommended_repayment_dates,
|
||||||
|
# "installmentAmount": installment_amount,
|
||||||
|
# "totalRepaymentAmount": total_amount,
|
||||||
|
#
|
||||||
|
# Business logic - selecting an offer
|
||||||
|
response_data = {
|
||||||
|
"outstandingDebtAmount": 0,
|
||||||
|
"requestId": request_id,
|
||||||
|
"transactionId": transaction_id,
|
||||||
|
"customerId": customer_id,
|
||||||
|
"accountId": account_id,
|
||||||
|
"loan": offers,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
return response_data
|
return ResponseHelper.success(data=response_data)
|
||||||
|
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
|
|
||||||
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
||||||
|
db.session.rollback()
|
||||||
return jsonify({
|
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
|
||||||
"message": "Validation exception"
|
|
||||||
}) , 422
|
|
||||||
|
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
logger.error(f"{getattr(err, 'messages', str(err))}")
|
logger.error(f"{getattr(err, 'messages', str(err))}")
|
||||||
|
db.session.rollback()
|
||||||
return jsonify({
|
return ResponseHelper.error(result_description=str(err))
|
||||||
"message": str(err)
|
|
||||||
}) , 400
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
||||||
return jsonify({
|
db.session.rollback()
|
||||||
"message": "Internal Server Error"
|
return ResponseHelper.internal_server_error()
|
||||||
}) , 500
|
|
||||||
+87
-6
@@ -1,7 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""Base configuration for Flask app"""
|
"""Base configuration for Flask app"""
|
||||||
|
|
||||||
@@ -9,20 +8,29 @@ class Config:
|
|||||||
API_URL = os.getenv("API_URL", "/swagger.json")
|
API_URL = os.getenv("API_URL", "/swagger.json")
|
||||||
|
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
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_USERNAME = os.environ.get("BASIC_AUTH_USERNAME", "user")
|
||||||
BASIC_AUTH_PASSWORD = os.environ.get("BASIC_AUTH_PASSWORD", "password")
|
BASIC_AUTH_PASSWORD = os.environ.get("BASIC_AUTH_PASSWORD", "password")
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
DATABASE_USER = os.environ.get("DATABASE_USER")
|
DATABASE_USER = os.environ.get("DATABASE_USER")
|
||||||
DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD")
|
DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD")
|
||||||
DATABASE_HOST = os.environ.get("DATABASE_HOST")
|
DATABASE_HOST = os.environ.get("DATABASE_HOST")
|
||||||
DATABASE_PORT = os.environ.get("DATABASE_PORT", 10532)
|
DATABASE_PORT = os.environ.get("DATABASE_PORT", 10532)
|
||||||
DATABASE_NAME = os.environ.get("DATABASE_NAME")
|
DATABASE_NAME = os.environ.get("DATABASE_NAME", "firstadvancedev")
|
||||||
|
DATABASE_SID = os.environ.get("DATABASE_SID", "FREE")
|
||||||
|
DNS = f"(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST={DATABASE_HOST})(PORT={DATABASE_PORT}))(CONNECT_DATA=(SID={DATABASE_SID})))"
|
||||||
|
|
||||||
|
|
||||||
|
# Database Connection
|
||||||
|
# SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}"
|
||||||
|
|
||||||
|
SQLALCHEMY_DATABASE_URI_INTERNAL = (f"oracle+oracledb://{DATABASE_USER}:{DATABASE_PASSWORD}@{DNS}")
|
||||||
|
SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI_FULL", SQLALCHEMY_DATABASE_URI_INTERNAL)
|
||||||
|
|
||||||
|
#SQLALCHEMY_DATABASE_URI_FULL = 'oracle+oracledb://FIRSTADVSTG:Pchanged_56789@10.2.110.30:1521/?service_name=firstadv'
|
||||||
|
|
||||||
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}"
|
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
SIMBRELLA_BASE_URL = os.getenv("SIMBRELLA_BASE_URL", "http://127.0.0.1:6337")
|
|
||||||
|
|
||||||
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "secret-key")
|
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "secret-key")
|
||||||
JWT_ACCESS_TOKEN_EXPIRES = os.getenv("JWT_ACCESS_TOKEN_EXPIRES", timedelta(hours=1))
|
JWT_ACCESS_TOKEN_EXPIRES = os.getenv("JWT_ACCESS_TOKEN_EXPIRES", timedelta(hours=1))
|
||||||
@@ -30,5 +38,78 @@ class Config:
|
|||||||
"JWT_REFRESH_TOKEN_EXPIRES", timedelta(days=30)
|
"JWT_REFRESH_TOKEN_EXPIRES", timedelta(days=30)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# KAFKA_BROKER = 'dev-events.simbrellang.net:9085'
|
||||||
|
KAFKA_BROKER = os.getenv("KAFKA_BROKER", "dev-events.simbrellang.net:9085")
|
||||||
|
|
||||||
|
# SIMBRELLA_ENDPOINT_RAC_CHECKS = os.getenv("SIMBRELLA_ENDPOINT_RAC_CHECKS", "RACCheck")
|
||||||
|
VALID_APP_ID = os.getenv("SIMBRELLA_APP_ID", "app1")
|
||||||
|
VALID_API_KEY = os.getenv("SIMBRELLA_API_KEY", "test-api-key-12345")
|
||||||
|
SIMBRELLA_BASE_URL = os.getenv("SIMBRELLA_BASE_URL", "http://127.0.0.1:6337")
|
||||||
|
SIMBRELLA_ENDPOINT_RAC_CHECKS = os.getenv("SIMBRELLA_ENDPOINT_RAC_CHECKS","api/rac-check")
|
||||||
|
SIMBRELLA_VERIFY_BALANCE_ENDPOINT = os.getenv("SIMBRELLA_VERIFY_BALANCE_ENDPOINT", "api/VerifyAccountBalance")
|
||||||
|
ENABLE_ACCOUNT_BALANCE_CHECK = os.getenv("ENABLE_ACCOUNT_BALANCE_CHECK", True)
|
||||||
|
SIMBRELLA_HEALTH = os.getenv("SIMBRELLA_ENDPOINT_RAC_CHECKS","api/system-health-check")
|
||||||
|
BANK_CALL_AUTH_ENDPOINT = os.getenv("BANK_CALL_AUTH_ENDPOINT", "/api/Auth/generate-token")
|
||||||
|
BANK_CALL_USERNAME = os.getenv("BANK_CALL_USERNAME", "simbrella")
|
||||||
|
BANK_CALL_PASSWORD = os.getenv("BANK_CALL_PASSWORD", "G7$k9@pL2!qR")
|
||||||
|
TIMEOUT = os.getenv("TIMEOUT", 60.0)
|
||||||
|
|
||||||
|
|
||||||
|
EVENTS_SERVICE_BASE_URL = os.getenv("EVENTS_SERVICE_BASE_URL","https://event-core.simbrellang.net")
|
||||||
|
ENDPOINT_DIRECT_LOAN = os.getenv("ENDPOINT_DIRECT_LOAN","/autocall/direct/loan")
|
||||||
|
ENDPOINT_DIRECT_REPAYMENT = os.getenv("ENDPOINT_DIRECT_REPAYMENT","/autocall/direct/repayment")
|
||||||
|
|
||||||
|
|
||||||
|
RAC_RESULT_accountStatus = os.environ.get("RAC_RESULT_accountStatus", "true")
|
||||||
|
RAC_RESULT_bvnValidated = os.environ.get("RAC_RESULT_bvnValidated", "true")
|
||||||
|
RAC_RESULT_creditBureauCheck = os.environ.get("RAC_RESULT_creditBureauCheck", "false")
|
||||||
|
RAC_RESULT_crmsCheck = os.environ.get("RAC_RESULT_crmsCheck", "true")
|
||||||
|
RAC_RESULT_hasLien = os.environ.get("RAC_RESULT_hasLien", "false")
|
||||||
|
RAC_RESULT_hasPastDueLoan = os.environ.get("RAC_RESULT_hasPastDueLoan", "false")
|
||||||
|
RAC_RESULT_hasSalaryAccount = os.environ.get("RAC_RESULT_hasSalaryAccount", "true")
|
||||||
|
RAC_RESULT_isWhitelisted = os.environ.get("RAC_RESULT_isWhitelisted", "true")
|
||||||
|
RAC_RESULT_noBouncedCheck = os.environ.get("RAC_RESULT_noBouncedCheck", "true")
|
||||||
|
|
||||||
|
rac_true_rules = [
|
||||||
|
"rule1_45day_sal",
|
||||||
|
"rule2_2m_sal",
|
||||||
|
"rule3_no_bounced_check",
|
||||||
|
"rule4_current_loan_payments",
|
||||||
|
"rule5_no_past_due_fadv_loan",
|
||||||
|
"rule6_no_past_due_other_loan",
|
||||||
|
"rule7_consistent_salary_amount",
|
||||||
|
"rule8_whitelisted",
|
||||||
|
"rule9_regular_account",
|
||||||
|
"rule10_bvn_validation",
|
||||||
|
"rule11_CRC_no_delinquency",
|
||||||
|
"rule12_CRMS_no_delinquency",
|
||||||
|
"rule13_BVN_ignore",
|
||||||
|
"rule14_no_lien",
|
||||||
|
"rule15_null_ignore"
|
||||||
|
]
|
||||||
|
|
||||||
|
rac_false_rules = [
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
rac_salary_payments = [
|
||||||
|
"salarypaymenT_1",
|
||||||
|
"salarypaymenT_2",
|
||||||
|
"salarypaymenT_3",
|
||||||
|
"salarypaymenT_4",
|
||||||
|
"salarypaymenT_5",
|
||||||
|
"salarypaymenT_6"
|
||||||
|
]
|
||||||
|
|
||||||
|
MAIL_SERVER = os.getenv('MAIL_SERVER','smtp.zoho.com')
|
||||||
|
MAIL_PORT = 587
|
||||||
|
MAIL_USERNAME = os.getenv('MAIL_USERNAME', 'firstadvance@dynamikservices.tech')
|
||||||
|
MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
|
||||||
|
MAIL_USE_TLS = True
|
||||||
|
MAIL_USE_SSL = False
|
||||||
|
MAIL_DEFAULT_SENDER = ('FirstAdvance', 'firstadvance@dynamikservices.tech')
|
||||||
|
MAIL_RECEIVER= os.getenv('MAIL_RECEIVER', 'vdagbue@gmail.com')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
settings = Config()
|
settings = Config()
|
||||||
|
|||||||
@@ -5,20 +5,20 @@ from app.api.helpers.response_helper import ResponseHelper
|
|||||||
def register_error_handlers(app):
|
def register_error_handlers(app):
|
||||||
@app.errorhandler(HTTPException)
|
@app.errorhandler(HTTPException)
|
||||||
def handle_http_exception(e):
|
def handle_http_exception(e):
|
||||||
return jsonify({'error': e.description}), e.code
|
return ResponseHelper.error(result_description=e.description, result_code=e.code )
|
||||||
|
|
||||||
@app.errorhandler(405)
|
@app.errorhandler(405)
|
||||||
def method_not_allowed(error):
|
def method_not_allowed(error):
|
||||||
return jsonify({"message": "Method Not Allowed"}), 405
|
return ResponseHelper.method_not_allowed()
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def not_found(error):
|
def not_found(error):
|
||||||
return jsonify({"message": "Resource not found"}), 404
|
return ResponseHelper.not_found()
|
||||||
|
|
||||||
@app.errorhandler(400)
|
@app.errorhandler(400)
|
||||||
def bad_request(error):
|
def bad_request(error):
|
||||||
return jsonify({"message": "Bad Request"}), 400
|
return ResponseHelper.bad_request()
|
||||||
|
|
||||||
@app.errorhandler(415)
|
@app.errorhandler(415)
|
||||||
def unsupported_media_type(error):
|
def unsupported_media_type(error):
|
||||||
return jsonify({"message": "Unsupported Media Type"}), 415
|
return ResponseHelper.error(result_description="Unsupported Media Type", result_code="415")
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_migrate import Migrate
|
from flask_migrate import Migrate
|
||||||
|
from flask_mail import Mail
|
||||||
|
|
||||||
|
mail = Mail()
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
migrate = Migrate()
|
migrate = Migrate()
|
||||||
+11
-1
@@ -2,5 +2,15 @@ from .customer import Customer
|
|||||||
from .account import Account
|
from .account import Account
|
||||||
from .loan import Loan
|
from .loan import Loan
|
||||||
from .transaction import Transaction
|
from .transaction import Transaction
|
||||||
|
from .repayment import Repayment
|
||||||
|
from .loan_charge import LoanCharge
|
||||||
|
from .offer import Offer
|
||||||
|
from .charge import Charge
|
||||||
|
from .rac_checks import RACCheck
|
||||||
|
from .loan_repayment_schedule import LoanRepaymentSchedule
|
||||||
|
from .transaction_offers import TransactionOffer
|
||||||
|
from .repayments_data import RepaymentsData
|
||||||
|
from .salary import Salary
|
||||||
|
|
||||||
__all__ = ['Customer', 'Account', 'Loan', 'Transaction']
|
|
||||||
|
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge', 'RACCheck', 'LoanRepaymentSchedule', 'TransactionOffer', 'RepaymentsData', 'Salary']
|
||||||
+21
-6
@@ -1,5 +1,8 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
class Account(db.Model):
|
class Account(db.Model):
|
||||||
__tablename__ = 'accounts'
|
__tablename__ = 'accounts'
|
||||||
@@ -9,18 +12,30 @@ class Account(db.Model):
|
|||||||
account_type = db.Column(db.String(50))
|
account_type = db.Column(db.String(50))
|
||||||
status = db.Column(db.String(20), default='active')
|
status = db.Column(db.String(20), default='active')
|
||||||
lien_amount = db.Column(db.Float, default=0.0)
|
lien_amount = db.Column(db.Float, default=0.0)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
customer = relationship(
|
||||||
|
"Customer",
|
||||||
|
primaryjoin="Customer.id == Account.customer_id",
|
||||||
|
foreign_keys=[customer_id],
|
||||||
|
back_populates="accounts",
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_account(cls, id, customer_id, account_type, status='active'):
|
def create_account(cls, id, customer_id, account_type, status='active'):
|
||||||
account = cls(
|
account = cls(
|
||||||
id=id,
|
id=id,
|
||||||
customer_id=customer_id,
|
customer_id=customer_id,
|
||||||
account_type=account_type
|
account_type=account_type,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
updated_at=datetime.now(timezone.utc)
|
||||||
)
|
)
|
||||||
db.session.add(account)
|
|
||||||
db.session.commit()
|
try:
|
||||||
|
db.session.add(account)
|
||||||
|
except IntegrityError as err:
|
||||||
|
raise ValueError(f"Database integrity error: {err}")
|
||||||
return account
|
return account
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -30,7 +45,7 @@ class Account(db.Model):
|
|||||||
return False
|
return False
|
||||||
if account.lien_amount > 0:
|
if account.lien_amount > 0:
|
||||||
return False
|
return False
|
||||||
return True
|
return account
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Account {self.id}>'
|
return f'<Account {self.id}>'
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
from app.extensions import db
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
|
|
||||||
|
class Charge(db.Model):
|
||||||
|
__tablename__ = 'charges'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
offer_id = db.Column(db.String(50), nullable=False)
|
||||||
|
code = db.Column(db.String(50), nullable=False)
|
||||||
|
percent = db.Column(db.Float, default=0.0)
|
||||||
|
description = db.Column(db.Text, nullable=True)
|
||||||
|
due = db.Column(db.Integer, nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
offer = relationship(
|
||||||
|
"Offer",
|
||||||
|
primaryjoin="Charge.offer_id == Offer.id",
|
||||||
|
foreign_keys=[offer_id],
|
||||||
|
back_populates="charges",
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_charges(cls, offer_id, charges):
|
||||||
|
"""
|
||||||
|
Add charges to an offer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
offer_id (int): ID of the offer to associate charges with.
|
||||||
|
charges (list): A list of dictionaries with keys:
|
||||||
|
code (str), amount (float), percent (float), description (str), due (int)
|
||||||
|
"""
|
||||||
|
if not charges or not isinstance(charges, list):
|
||||||
|
raise ValueError("Charges must be a non-empty list of dictionaries")
|
||||||
|
|
||||||
|
if offer_id is None:
|
||||||
|
raise ValueError("offer_id cannot be None")
|
||||||
|
|
||||||
|
offer_charges = []
|
||||||
|
|
||||||
|
|
||||||
|
for charge in charges:
|
||||||
|
code = charge.get("code")
|
||||||
|
percent = charge.get("percent", 0.0)
|
||||||
|
description = charge.get("description", "")
|
||||||
|
due_days = charge.get("due", 0)
|
||||||
|
|
||||||
|
existing = cls.query.filter_by(offer_id=offer_id, code=code).first()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
continue
|
||||||
|
|
||||||
|
charge_obj = cls(
|
||||||
|
offer_id = offer_id,
|
||||||
|
code = code,
|
||||||
|
percent = percent,
|
||||||
|
description = description,
|
||||||
|
due = due_days,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
updated_at=datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(charge_obj)
|
||||||
|
offer_charges.append(charge_obj)
|
||||||
|
|
||||||
|
return offer_charges
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_offer_charges(cls, offer_id):
|
||||||
|
"""
|
||||||
|
Get all charges for a particular offer as a dictionary
|
||||||
|
|
||||||
|
Args:
|
||||||
|
offer_id (str): The offer ID.
|
||||||
|
"""
|
||||||
|
if not offer_id:
|
||||||
|
raise ValueError("offer_id not found")
|
||||||
|
|
||||||
|
charges = cls.query.filter_by(offer_id=offer_id).all()
|
||||||
|
|
||||||
|
return charges
|
||||||
|
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'offerId': self.offer_id,
|
||||||
|
'code': self.code,
|
||||||
|
'percent': self.percent,
|
||||||
|
'description': self.description,
|
||||||
|
'due': self.due
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Charge {self.id} - Offer {self.offer_id} - {self.code}>"
|
||||||
+64
-15
@@ -1,6 +1,12 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
#
|
||||||
|
# from app.api.services.offer_analysis import logger
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models.account import Account
|
from app.models.account import Account
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
# from app.utils.logger import logger
|
||||||
|
|
||||||
class Customer(db.Model):
|
class Customer(db.Model):
|
||||||
__tablename__ = 'customers'
|
__tablename__ = 'customers'
|
||||||
@@ -8,33 +14,76 @@ class Customer(db.Model):
|
|||||||
id = db.Column(db.String(50), primary_key=True)
|
id = db.Column(db.String(50), primary_key=True)
|
||||||
msisdn = db.Column(db.String(20), unique=True, nullable=False)
|
msisdn = db.Column(db.String(20), unique=True, nullable=False)
|
||||||
country_code = db.Column(db.String(3), nullable=False)
|
country_code = db.Column(db.String(3), nullable=False)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
accounts = relationship(
|
||||||
|
"Account",
|
||||||
|
primaryjoin="Customer.id == Account.customer_id",
|
||||||
|
foreign_keys="Account.customer_id",
|
||||||
|
back_populates="customer",
|
||||||
|
)
|
||||||
|
|
||||||
|
loans = relationship(
|
||||||
|
"Loan",
|
||||||
|
primaryjoin="Customer.id == Loan.customer_id",
|
||||||
|
foreign_keys="Loan.customer_id",
|
||||||
|
back_populates="customer",
|
||||||
|
)
|
||||||
|
|
||||||
|
transaction_offers = relationship(
|
||||||
|
"TransactionOffer",
|
||||||
|
primaryjoin="Customer.id == TransactionOffer.customer_id",
|
||||||
|
foreign_keys="TransactionOffer.customer_id",
|
||||||
|
back_populates="customer",
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_eligible(cls, customer_id):
|
def is_valid_customer(cls, customer_id):
|
||||||
customer = cls.query.filter_by(id=customer_id).first()
|
customer = cls.query.filter_by(id=customer_id).first()
|
||||||
if not customer:
|
if not customer:
|
||||||
return False, "Customer not found"
|
return False
|
||||||
return True, "Customer is eligible"
|
return customer
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_customer(cls, id, msisdn, country_code, account_id, account_type='savings'):
|
def create_customer(cls, id, msisdn, country_code, account_id, account_type='savings'):
|
||||||
if cls.query.filter_by(id=id).first():
|
if cls.query.filter_by(id=id).first():
|
||||||
raise ValueError("Customer already exists")
|
raise ValueError("Customer ID '{id}' already exists.")
|
||||||
|
elif Account.query.filter_by(id=account_id).first():
|
||||||
|
raise ValueError(f"Account ID '{account_id}' already exists.")
|
||||||
|
elif cls.query.filter_by(msisdn=msisdn).first():
|
||||||
|
raise ValueError("MSISDN '{msisdn}' already exists")
|
||||||
|
|
||||||
# Create the customer
|
# Create the customer
|
||||||
customer = cls(id=id, msisdn=msisdn, country_code=country_code)
|
customer = cls(
|
||||||
db.session.add(customer)
|
id=id,
|
||||||
|
msisdn=msisdn,
|
||||||
|
country_code=country_code,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
updated_at=datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
db.session.add(customer)
|
||||||
|
|
||||||
# Create an associated account
|
# Create an associated account
|
||||||
account = Account.create_account(
|
account = Account.create_account(
|
||||||
id=account_id,
|
id=account_id,
|
||||||
customer_id=id,
|
customer_id=id,
|
||||||
account_type=account_type
|
account_type=account_type
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.commit()
|
except IntegrityError as err:
|
||||||
|
raise ValueError(f"Database integrity error: {err}")
|
||||||
|
return customer
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_customer_with_loan_list(cls, customer_id):
|
||||||
|
"""
|
||||||
|
Get customer by ID.
|
||||||
|
"""
|
||||||
|
customer = cls.query.filter_by(id=customer_id).first()
|
||||||
|
|
||||||
|
if not customer:
|
||||||
|
raise ValueError(f"Customer does not exist")
|
||||||
return customer
|
return customer
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
|||||||
+263
-8
@@ -1,19 +1,138 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone, timedelta
|
||||||
|
from itertools import product
|
||||||
|
from app.api.enums.loan_status import LoanStatus
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
|
from app.models.customer import Customer
|
||||||
|
from app.models.account import Account
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
from sqlalchemy import and_, or_, not_
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Loan(db.Model):
|
class Loan(db.Model):
|
||||||
__tablename__ = 'loans'
|
__tablename__ = 'loans'
|
||||||
|
|
||||||
id = db.Column(db.String(50), primary_key=True)
|
id = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
primary_key=True,
|
||||||
|
autoincrement=True,
|
||||||
|
)
|
||||||
customer_id = db.Column(db.String(50), nullable=False)
|
customer_id = db.Column(db.String(50), nullable=False)
|
||||||
|
transaction_id = db.Column(db.String(50), nullable=True)
|
||||||
|
original_transaction = db.Column(db.String(50), nullable=True)
|
||||||
account_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)
|
offer_id = db.Column(db.String(20), nullable=False)
|
||||||
principal_amount = db.Column(db.Float, nullable=False)
|
product_id = db.Column(db.String(20), nullable=True)
|
||||||
|
collection_type = db.Column(db.String(20), nullable=True)
|
||||||
|
current_loan_amount = db.Column(db.Float, nullable=True)
|
||||||
|
initial_loan_amount = db.Column(db.Float, nullable=False)
|
||||||
|
default_penalty_fee = db.Column(db.Float, default=0)
|
||||||
|
continuous_fee = db.Column(db.Float, default=0)
|
||||||
|
upfront_fee = db.Column(db.Float, nullable=True, default=0.0)
|
||||||
|
repayment_amount = db.Column(db.Float, nullable=True, default=0.0)
|
||||||
|
balance = db.Column(db.Float, nullable=True, default=0.0)
|
||||||
|
installment_amount = db.Column(db.Float, nullable=True, default=0.0)
|
||||||
status = db.Column(db.String(20), default='pending')
|
status = db.Column(db.String(20), default='pending')
|
||||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
tenor = db.Column(db.Integer, nullable=True)
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
due_date = db.Column(db.DateTime, nullable=True)
|
||||||
|
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
eligible_amount = db.Column(db.Float, nullable=True, default=0.0)
|
||||||
|
disburse_date = db.Column(db.DateTime, nullable=True)
|
||||||
|
disburse_verify = db.Column(db.DateTime, nullable=True)
|
||||||
|
reference = db.Column(db.String(50), nullable=True)
|
||||||
|
disburse_result = db.Column(db.String(10), nullable=True)
|
||||||
|
disburse_description = db.Column(db.String(100), nullable=True)
|
||||||
|
verify_result = db.Column(db.String(10), nullable=True)
|
||||||
|
verify_description = db.Column(db.String(100), nullable=True)
|
||||||
|
|
||||||
|
total_penal_charge = db.Column(db.Float, default=0.0)
|
||||||
|
last_penal_date = db.Column(db.DateTime, nullable=True)
|
||||||
|
|
||||||
|
customer = relationship(
|
||||||
|
"Customer",
|
||||||
|
primaryjoin="Customer.id == Loan.customer_id",
|
||||||
|
foreign_keys=[customer_id],
|
||||||
|
back_populates="loans",
|
||||||
|
)
|
||||||
|
|
||||||
|
loan_charges = relationship(
|
||||||
|
"LoanCharge",
|
||||||
|
primaryjoin="LoanCharge.loan_id == Loan.id",
|
||||||
|
foreign_keys="LoanCharge.loan_id",
|
||||||
|
back_populates="loan",
|
||||||
|
)
|
||||||
|
|
||||||
|
loan_repayment_schedules = relationship(
|
||||||
|
"LoanRepaymentSchedule",
|
||||||
|
primaryjoin="LoanRepaymentSchedule.loan_id == Loan.id",
|
||||||
|
foreign_keys="LoanRepaymentSchedule.loan_id",
|
||||||
|
back_populates="loan",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_loan(
|
||||||
|
cls,
|
||||||
|
customer_id,
|
||||||
|
account_id,
|
||||||
|
offer_id,
|
||||||
|
product_id,
|
||||||
|
initial_loan_amount,
|
||||||
|
collection_type,
|
||||||
|
transaction_id,
|
||||||
|
original_transaction,
|
||||||
|
upfront_fee,
|
||||||
|
repayment_amount,
|
||||||
|
installment_amount,
|
||||||
|
tenor,
|
||||||
|
eligible_amount,
|
||||||
|
reference,
|
||||||
|
status = "pending",
|
||||||
|
):
|
||||||
|
# Check if customer exists
|
||||||
|
customer = Customer.is_valid_customer(customer_id)
|
||||||
|
if not customer:
|
||||||
|
raise ValueError("Customer does not exist")
|
||||||
|
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
due_date = now + timedelta(days=tenor)
|
||||||
|
|
||||||
|
# Create and save the loan
|
||||||
|
loan = cls(
|
||||||
|
customer_id = customer_id,
|
||||||
|
account_id = account_id,
|
||||||
|
offer_id = offer_id,
|
||||||
|
product_id = product_id,
|
||||||
|
collection_type = collection_type,
|
||||||
|
transaction_id = transaction_id,
|
||||||
|
original_transaction = original_transaction,
|
||||||
|
initial_loan_amount = initial_loan_amount,
|
||||||
|
current_loan_amount = initial_loan_amount,
|
||||||
|
upfront_fee = upfront_fee,
|
||||||
|
repayment_amount = repayment_amount,
|
||||||
|
balance = repayment_amount,
|
||||||
|
installment_amount = installment_amount,
|
||||||
|
due_date=due_date,
|
||||||
|
tenor = tenor,
|
||||||
|
status = status,
|
||||||
|
eligible_amount =eligible_amount,
|
||||||
|
reference = reference,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
updated_at=datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.add(loan)
|
||||||
|
except IntegrityError as err:
|
||||||
|
raise ValueError(f"Database integrity error: {err}")
|
||||||
|
return loan
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def has_active_loans(cls, customer_id):
|
def has_active_loans(cls, customer_id):
|
||||||
@@ -23,9 +142,145 @@ class Loan(db.Model):
|
|||||||
).count()
|
).count()
|
||||||
|
|
||||||
if active_loans > 0:
|
if active_loans > 0:
|
||||||
return False, "Customer has active loans"
|
return False
|
||||||
return True, "No active loans"
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_customer_loan(cls, loan_id, customer_id):
|
||||||
|
"""
|
||||||
|
Get customer's active loans by loan_id.
|
||||||
|
"""
|
||||||
|
loan = cls.query.filter_by(id = loan_id, customer_id = customer_id).first()
|
||||||
|
if not loan:
|
||||||
|
raise ValueError(f"Loan with ID {loan_id} does not exist or does not belong to customer {customer_id}.")
|
||||||
|
return loan
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_customer_original_loan(cls, customer_id, original_transaction):
|
||||||
|
"""
|
||||||
|
Get customer's original loan offer.
|
||||||
|
"""
|
||||||
|
original_loan = cls.query.filter(and_( cls.customer_id ==customer_id, cls.original_transaction==original_transaction, cls.transaction_id==original_transaction )).first()
|
||||||
|
if not original_loan:
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.info(f" get_customer_original_loan ==>>>> {original_loan}")
|
||||||
|
return original_loan
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_customer_last_loan(cls, customer_id):
|
||||||
|
"""
|
||||||
|
Get customer's active loans.
|
||||||
|
"""
|
||||||
|
logger.info(f"get_customer_last_loan [customer_id] ==>>>> {customer_id}")
|
||||||
|
# loan = cls.query.filter_by( cls.customer_id == customer_id).first()
|
||||||
|
loan = cls.query.filter(and_( cls.customer_id ==customer_id, cls.status=='active')).first()
|
||||||
|
|
||||||
|
if not loan:
|
||||||
|
return None
|
||||||
|
# loan = {
|
||||||
|
# "original_transaction":"",
|
||||||
|
# "eligible_amount": 0,
|
||||||
|
# "loan_amount": 0,
|
||||||
|
# "customer_id": customer_id,
|
||||||
|
# "transaction_id": "",
|
||||||
|
# "resultDescription": "No Active Loan"
|
||||||
|
# }
|
||||||
|
logger.info(f" get_customer_last_loan ==>>>> {loan}")
|
||||||
|
return loan
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_active_loans_by_original_transaction(cls, original_transaction_id):
|
||||||
|
"""
|
||||||
|
Get all active loans with the same original_transaction ID.
|
||||||
|
"""
|
||||||
|
|
||||||
|
active_loans = cls.query.filter(
|
||||||
|
cls.original_transaction == original_transaction_id,
|
||||||
|
or_(
|
||||||
|
cls.status == LoanStatus.ACTIVE.value,
|
||||||
|
cls.status == LoanStatus.START_REPAY.value,
|
||||||
|
cls.status == LoanStatus.ACTIVE_PARTIAL.value,
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
return active_loans
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_current_active_loans_by_account_id(cls, account_id):
|
||||||
|
"""
|
||||||
|
Get the first active loan based on the accountID.
|
||||||
|
"""
|
||||||
|
first_active_loan = cls.query.filter(
|
||||||
|
cls.account_id == account_id,
|
||||||
|
or_(
|
||||||
|
cls.status == LoanStatus.ACTIVE.value,
|
||||||
|
cls.status == LoanStatus.START_REPAY.value,
|
||||||
|
cls.status == LoanStatus.ACTIVE_PARTIAL.value,
|
||||||
|
)
|
||||||
|
).order_by(cls.id.desc()).first()
|
||||||
|
|
||||||
|
return first_active_loan
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update_status(cls, loan_id, status):
|
||||||
|
"""
|
||||||
|
Update the status of the loan with the given loan_id.
|
||||||
|
"""
|
||||||
|
# Retrieve loan
|
||||||
|
loan = cls.query.get(loan_id)
|
||||||
|
|
||||||
|
if not loan:
|
||||||
|
raise ValueError(f"Loan with ID {loan_id} does not exist.")
|
||||||
|
|
||||||
|
if loan.status == status:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update loan status and the updated_at timestamp
|
||||||
|
loan.status = status
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_daily_loan_count(cls, customer_id, product_id):
|
||||||
|
"""
|
||||||
|
Returns the count of loans created today for a customer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
start_of_day = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
end_of_day = start_of_day + timedelta(days=1)
|
||||||
|
|
||||||
|
return cls.query.filter_by(
|
||||||
|
customer_id=customer_id,
|
||||||
|
product_id=product_id,
|
||||||
|
).filter(
|
||||||
|
cls.created_at >= start_of_day,
|
||||||
|
cls.created_at < end_of_day
|
||||||
|
).count()
|
||||||
|
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""
|
||||||
|
Convert the Loan object to a dictionary format for JSON serialization.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'debtId': self.id,
|
||||||
|
'transactionId': self.transaction_id,
|
||||||
|
'loanRef': self.reference,
|
||||||
|
'productId': self.product_id,
|
||||||
|
'initialLoanAmount': self.initial_loan_amount,
|
||||||
|
'currentLoanAmount': self.balance,
|
||||||
|
'defaultPenaltyFee': self.default_penalty_fee,
|
||||||
|
'continuousFee': self.continuous_fee,
|
||||||
|
'collectionType': self.collection_type,
|
||||||
|
'upfrontFee': self.upfront_fee,
|
||||||
|
'repaymentAmount': self.repayment_amount,
|
||||||
|
'installmentAmount': self.installment_amount,
|
||||||
|
'status': self.status,
|
||||||
|
'tenor': self.tenor,
|
||||||
|
'dueDate': self.due_date.isoformat() if self.due_date else None,
|
||||||
|
'loanDate': self.created_at.isoformat() if self.created_at else None,
|
||||||
|
}
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Loan {self.id}>'
|
return f'<Loan {self.id}>'
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
from app.extensions import db
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from app.utils.logger import logger
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
|
|
||||||
|
class LoanCharge(db.Model):
|
||||||
|
__tablename__ = 'loan_charges'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
loan_id = db.Column(db.Integer, nullable=False)
|
||||||
|
transaction_id = db.Column(db.String(50), nullable=True)
|
||||||
|
code = db.Column(db.String(50), nullable=False)
|
||||||
|
amount = db.Column(db.Float, default=0.0)
|
||||||
|
percent = db.Column(db.Float, default=0.0)
|
||||||
|
description = db.Column(db.Text, nullable=True)
|
||||||
|
due = db.Column(db.Integer, nullable=False)
|
||||||
|
due_date = db.Column(db.DateTime, nullable=True)
|
||||||
|
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
loan = relationship(
|
||||||
|
"Loan",
|
||||||
|
primaryjoin="LoanCharge.loan_id == Loan.id",
|
||||||
|
foreign_keys=[loan_id],
|
||||||
|
back_populates="loan_charges",
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_charges_for_loan(cls, loan_id, transaction_id, charges, referenced_amount = 0.0):
|
||||||
|
"""
|
||||||
|
Create loan charges for a given loan.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
loan_id (int): ID of the loan to associate charges with.
|
||||||
|
charges (list): A list of dictionaries with keys:
|
||||||
|
code (str), amount (float), percent (float), description (str), due (int)
|
||||||
|
"""
|
||||||
|
# if not charges or not isinstance(charges, list):
|
||||||
|
# raise ValueError("Charges must be a non-empty list of dictionaries")
|
||||||
|
|
||||||
|
if loan_id is None:
|
||||||
|
raise ValueError("loan_id cannot be None")
|
||||||
|
|
||||||
|
loan_charges = []
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
subset_keys = ['interest', 'management', 'insurance', 'vat']
|
||||||
|
for item in subset_keys:
|
||||||
|
charge = charges[item]
|
||||||
|
due_days = charge['due_days'] # getattr(charge, "due_days", 0)
|
||||||
|
amount = charge['fee'] # getattr(charge, "fee", 0.0)
|
||||||
|
percent = charge['rate'] # getattr(charge, "rate", 0.0)
|
||||||
|
code = charge['code'] # getattr(charge, "code","")
|
||||||
|
description = charge['description'] # getattr(charge, "description", "")
|
||||||
|
|
||||||
|
charge_obj = cls(
|
||||||
|
loan_id = loan_id,
|
||||||
|
transaction_id = transaction_id,
|
||||||
|
code = code,
|
||||||
|
amount = round(amount, 2),
|
||||||
|
percent = percent,
|
||||||
|
description = description,
|
||||||
|
due = due_days,
|
||||||
|
due_date = now + timedelta(days=due_days),
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
updated_at=datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(charge_obj)
|
||||||
|
loan_charges.append(charge_obj)
|
||||||
|
|
||||||
|
return loan_charges
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'loanId': self.loan_id,
|
||||||
|
'transactionId': self.transaction_id,
|
||||||
|
'code': self.code,
|
||||||
|
'amount': self.amount,
|
||||||
|
'percent': self.percent,
|
||||||
|
'description': self.description,
|
||||||
|
'due': self.due,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<LoanCharge {self.id} - Loan {self.loan_id} - {self.code}>"
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
from app.extensions import db
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.api.enums.repayment_schedule_status import RepaymentScheduleStatus
|
||||||
|
|
||||||
|
class LoanRepaymentSchedule(db.Model):
|
||||||
|
__tablename__ = 'loan_repayment_schedules'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
loan_id = db.Column(db.Integer, nullable=False)
|
||||||
|
transaction_id = db.Column(db.String(50), nullable=True)
|
||||||
|
product_id = db.Column(db.String(20), nullable=True)
|
||||||
|
installment_number = db.Column(db.Integer, nullable=False)
|
||||||
|
due_date = db.Column(db.DateTime, nullable=False)
|
||||||
|
installment_amount= db.Column(db.Float, default=0.0)
|
||||||
|
total_repayment_amount = db.Column(db.Float, default=0.0)
|
||||||
|
paid = db.Column(db.Boolean, default=False)
|
||||||
|
paid_at = db.Column(db.DateTime, nullable=True)
|
||||||
|
due_process_date = db.Column(db.DateTime, nullable=True)
|
||||||
|
due_process_count = db.Column(db.Integer, default=0)
|
||||||
|
paid_status = db.Column(db.String(20), nullable=True)
|
||||||
|
repay_description = db.Column(db.String(255), nullable=True)
|
||||||
|
partial_balance = db.Column(db.Float, default=0.0)
|
||||||
|
|
||||||
|
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
penal_charge = db.Column(db.Float, default=0.0)
|
||||||
|
penal_count = db.Column(db.Integer, default=0)
|
||||||
|
last_penal_date = db.Column(db.DateTime, nullable=True)
|
||||||
|
|
||||||
|
loan = relationship(
|
||||||
|
"Loan",
|
||||||
|
primaryjoin="LoanRepaymentSchedule.loan_id == Loan.id",
|
||||||
|
foreign_keys=[loan_id],
|
||||||
|
back_populates="loan_repayment_schedules",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_repayment_schedule(cls, loan, num_schedules, transaction_id):
|
||||||
|
"""
|
||||||
|
Add repayment schedules for a given loan.
|
||||||
|
"""
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
schedules = []
|
||||||
|
|
||||||
|
for i in range(num_schedules):
|
||||||
|
due_date = now + relativedelta(months=i + 1)
|
||||||
|
schedule = LoanRepaymentSchedule(
|
||||||
|
loan_id=loan.id,
|
||||||
|
installment_number=i + 1,
|
||||||
|
due_date=due_date,
|
||||||
|
total_repayment_amount = round(loan.repayment_amount, 2),
|
||||||
|
installment_amount=round(loan.installment_amount, 2),
|
||||||
|
product_id = loan.product_id,
|
||||||
|
transaction_id = transaction_id,
|
||||||
|
paid_status = RepaymentScheduleStatus.ACTIVE,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
updated_at=datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(schedule)
|
||||||
|
schedules.append(schedule)
|
||||||
|
|
||||||
|
return schedules
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'loanId': self.loan_id,
|
||||||
|
'installmentNumber': self.installment_number,
|
||||||
|
'dueDate': self.due_date.isoformat(),
|
||||||
|
'principalAmount': self.principal_amount,
|
||||||
|
'interestAmount': self.interest_amount,
|
||||||
|
'totalInstallment': self.total_installment,
|
||||||
|
'paid': self.paid,
|
||||||
|
'paidStatus': self.paid_status,
|
||||||
|
'paidAt': self.paid_at.isoformat() if self.paid_at else None
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<LoanRepaymentSchedule Loan:{self.loan_id} Installment:{self.installment_number}>'
|
||||||
+82
-4
@@ -1,16 +1,94 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
|
from app.models.charge import Charge
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
class Offer(db.Model):
|
class Offer(db.Model):
|
||||||
__tablename__ = 'offers'
|
__tablename__ = 'offers'
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.String(50), primary_key=True)
|
||||||
product_id = db.Column(db.String, nullable=False)
|
product_id = db.Column(db.String(50), nullable=False)
|
||||||
min_amount = db.Column(db.Float, nullable=False)
|
min_amount = db.Column(db.Float, nullable=False)
|
||||||
max_amount = db.Column(db.Float, nullable=False)
|
max_amount = db.Column(db.Float, nullable=False)
|
||||||
tenor = db.Column(db.Integer, nullable=False)
|
tenor = db.Column(db.Integer, nullable=False)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
schedule = db.Column(db.Integer, nullable=True)
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
interest_rate = db.Column(db.Float, default=3.0)
|
||||||
|
management_rate = db.Column(db.Float, default=1.0)
|
||||||
|
insurance_rate = db.Column(db.Float, default=1.0)
|
||||||
|
vat_rate = db.Column(db.Float, default=7.5)
|
||||||
|
list_order = db.Column(db.Integer, nullable=True)
|
||||||
|
max_daily_loans = db.Column(db.Integer, nullable=True)
|
||||||
|
max_active_loans = db.Column(db.Integer, nullable=True)
|
||||||
|
max_life_loans = db.Column(db.Integer, nullable=True)
|
||||||
|
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
charges = relationship(
|
||||||
|
"Charge",
|
||||||
|
primaryjoin="Offer.id == Charge.offer_id",
|
||||||
|
foreign_keys="Charge.offer_id",
|
||||||
|
back_populates="offer",
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all_offers(cls):
|
||||||
|
"""
|
||||||
|
Return all offers in dictionary format.
|
||||||
|
"""
|
||||||
|
offers = cls.query.all()
|
||||||
|
|
||||||
|
if not offers:
|
||||||
|
raise ValueError(f"No available offers")
|
||||||
|
return offers
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_valid_offer(cls, offer_id):
|
||||||
|
offer = cls.query.filter_by(id=str(offer_id)).first()
|
||||||
|
|
||||||
|
|
||||||
|
if not offer:
|
||||||
|
return False
|
||||||
|
return offer
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_offer_by_id(cls, offer_id):
|
||||||
|
"""
|
||||||
|
Return an offer by its ID.
|
||||||
|
"""
|
||||||
|
offer = cls.query.filter_by(id=str(offer_id)).first()
|
||||||
|
|
||||||
|
if not offer:
|
||||||
|
raise ValueError(f"Offer with ID {offer_id} not found")
|
||||||
|
return offer
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_offer_by_product_id(cls, product_id):
|
||||||
|
"""
|
||||||
|
Return an offer by its product ID.
|
||||||
|
"""
|
||||||
|
offer = cls.query.filter_by(product_id=str(product_id)).first()
|
||||||
|
|
||||||
|
if not offer:
|
||||||
|
raise ValueError(f"Offer with Product ID {product_id} not found")
|
||||||
|
return offer
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"offerId": self.id,
|
||||||
|
"productId": self.product_id,
|
||||||
|
"minAmount": self.min_amount,
|
||||||
|
"maxAmount": self.max_amount,
|
||||||
|
"tenor": self.tenor,
|
||||||
|
"interest_rate": self.interest_rate,
|
||||||
|
"management_rate": self.management_rate,
|
||||||
|
"insurance_rate": self.insurance_rate,
|
||||||
|
"vat_rate": self.vat_rate,
|
||||||
|
"maxDailyLoans": self.max_daily_loans,
|
||||||
|
"maxActiveLoans": self.max_active_loans,
|
||||||
|
"maxLifeLoans": self.max_life_loans
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<LoanOffer {self.id}>'
|
return f'<LoanOffer {self.id}>'
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
from app.extensions import db
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from uuid import uuid4
|
||||||
|
import json
|
||||||
|
from sqlalchemy.types import JSON
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
|
class RACCheck(db.Model):
|
||||||
|
__tablename__ = 'rac_checks'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
transaction_id = db.Column(db.String(50), nullable=False)
|
||||||
|
customer_id = db.Column(db.String(50), nullable=False)
|
||||||
|
account_id = db.Column(db.String(50), nullable=False)
|
||||||
|
rac_response = db.Column(db.Text, nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rac_response_data(self):
|
||||||
|
return json.loads(self.rac_response)
|
||||||
|
|
||||||
|
@rac_response_data.setter
|
||||||
|
def rac_response_data(self, value):
|
||||||
|
self.rac_response = json.dumps(value)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_rac_check(cls, customer_id, account_id, transaction_id, data = None):
|
||||||
|
|
||||||
|
|
||||||
|
# Save the response
|
||||||
|
rac_check = cls(
|
||||||
|
customer_id = customer_id,
|
||||||
|
account_id = account_id,
|
||||||
|
transaction_id = transaction_id,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
updated_at=datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
|
|
||||||
|
rac_check.rac_response_data = data or {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.add(rac_check)
|
||||||
|
except IntegrityError as err:
|
||||||
|
raise ValueError(f"Database integrity error: {err}")
|
||||||
|
return rac_check
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all_rac_checks(cls):
|
||||||
|
"""
|
||||||
|
Return all RAC checks in dictionary format.
|
||||||
|
"""
|
||||||
|
rac_checks = cls.query.all()
|
||||||
|
|
||||||
|
if not rac_checks:
|
||||||
|
return None
|
||||||
|
return rac_checks
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_rac_check(cls, customer_id, account_id):
|
||||||
|
"""
|
||||||
|
Return a RAC check by its ID.
|
||||||
|
"""
|
||||||
|
rac_check = cls.query.filter_by( customer_id = customer_id,
|
||||||
|
account_id = account_id,).first()
|
||||||
|
|
||||||
|
if not rac_check:
|
||||||
|
raise ValueError(f"RAC Check for customer not found")
|
||||||
|
return rac_check
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"id": str(self.id),
|
||||||
|
"transactionId": str(self.transaction_id),
|
||||||
|
"customerId": self.customer_id,
|
||||||
|
"accountId": self.account_id,
|
||||||
|
"racResponse": self.rac_response_data,
|
||||||
|
"createdAt": self.created_at.isoformat(),
|
||||||
|
"updatedAt": self.updated_at.isoformat() if self.updated_at else None
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<RACCheck {self.id}>'
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
from app.api.enums.loan_status import LoanStatus
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models.customer import Customer
|
||||||
|
from app.models.loan import Loan
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
|
|
||||||
|
class Repayment(db.Model):
|
||||||
|
__tablename__ = 'repayments'
|
||||||
|
|
||||||
|
id = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
primary_key=True,
|
||||||
|
autoincrement=True,
|
||||||
|
)
|
||||||
|
loan_id = db.Column(db.String(50), nullable=False)
|
||||||
|
customer_id = db.Column(db.String(50), nullable=False)
|
||||||
|
product_id = db.Column(db.String(20), nullable=True)
|
||||||
|
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
transaction_id = db.Column(db.String(50), nullable=True)
|
||||||
|
# repay_date = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
||||||
|
repay_date = db.Column(db.DateTime, nullable=True)
|
||||||
|
repay_result = db.Column(db.String(10), nullable=True)
|
||||||
|
repay_description = db.Column(db.String(100), nullable=True)
|
||||||
|
# verify_date = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
||||||
|
verify_date = db.Column(db.DateTime, nullable=True)
|
||||||
|
verify_result = db.Column(db.String(10), nullable=True)
|
||||||
|
verify_description = db.Column(db.String(100), nullable=True)
|
||||||
|
initiated_by = db.Column(db.String(50), nullable=True)
|
||||||
|
salary_amount = db.Column(db.Float, nullable=True, default=0.0)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_repayment(cls, customer_id, loan, transaction_id):
|
||||||
|
|
||||||
|
# Check that the loan is active
|
||||||
|
if loan.status not in [LoanStatus.ACTIVE, LoanStatus.START_REPAY, LoanStatus.ACTIVE_PARTIAL]:
|
||||||
|
raise ValueError(f"Repayment cannot be processed. Loan status: ({loan.status})")
|
||||||
|
|
||||||
|
repayment = cls(
|
||||||
|
customer_id=customer_id,
|
||||||
|
loan_id=loan.id,
|
||||||
|
product_id=loan.product_id,
|
||||||
|
transaction_id = transaction_id,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
updated_at=datetime.now(timezone.utc),
|
||||||
|
initiated_by='USER_INITIATED'
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.add(repayment)
|
||||||
|
except IntegrityError as err:
|
||||||
|
raise ValueError(f"Database integrity error: {err}")
|
||||||
|
|
||||||
|
return repayment
|
||||||
|
|
||||||
|
# Get loan repayments
|
||||||
|
@classmethod
|
||||||
|
def get_repayments_by_id(cls, loan_id):
|
||||||
|
return cls.query.filter_by(loan_id=loan_id).all()
|
||||||
|
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"loan_id": self.loan_id,
|
||||||
|
"customer_id": self.customer_id,
|
||||||
|
"product_id": self.product_id,
|
||||||
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||||
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||||
|
"transaction_id": self.transaction_id,
|
||||||
|
"repay_date": self.repay_date.isoformat() if self.repay_date else None,
|
||||||
|
"repay_result": self.repay_result,
|
||||||
|
"repay_description": self.repay_description,
|
||||||
|
"verify_date": self.verify_date.isoformat() if self.verify_date else None,
|
||||||
|
"verify_result": self.verify_result,
|
||||||
|
"verify_description": self.verify_description,
|
||||||
|
"initiated_by": self.initiated_by,
|
||||||
|
"salary_amount": self.salary_amount
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Repayment {self.id}>'
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
from app.extensions import db
|
||||||
|
|
||||||
|
|
||||||
|
class RepaymentsData(db.Model):
|
||||||
|
__tablename__ = "repayments_data"
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
transaction_id = db.Column(db.String(50), nullable=False)
|
||||||
|
fbn_transaction_id = db.Column(db.String(50), nullable=True)
|
||||||
|
customer_id = db.Column(db.String(50), nullable=True)
|
||||||
|
account_id = db.Column(db.String(50), nullable=True)
|
||||||
|
repayment_amount = db.Column(db.Float, nullable=True, default=0.0)
|
||||||
|
amount_collected = db.Column(db.Float, nullable=True, default=0.0)
|
||||||
|
added_date = db.Column(db.DateTime(timezone=True), default=datetime.now(timezone.utc), nullable=False)
|
||||||
|
response_code = db.Column(db.String(10), nullable=True)
|
||||||
|
response_descr = db.Column(db.String(255), nullable=True)
|
||||||
|
balance = db.Column(db.Float, nullable=True, default=0.0)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"transaction_id": self.transaction_id,
|
||||||
|
"fbn_transaction_id": self.fbn_transaction_id,
|
||||||
|
"customer_id": self.customer_id,
|
||||||
|
"account_id": self.account_id,
|
||||||
|
"repayment_amount": self.repayment_amount,
|
||||||
|
"amount_collected": self.amount_collected,
|
||||||
|
"added_date": self.added_date.isoformat() if self.added_date else None,
|
||||||
|
"response_code": self.response_code,
|
||||||
|
"response_descr": self.response_descr,
|
||||||
|
"balance": self.balance,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<RepaymentsData id={self.id}, transaction_id={self.transaction_id}>"
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
from app.extensions import db
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
|
class Salary(db.Model):
|
||||||
|
__tablename__ = 'salaries'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
customer_id = db.Column(db.String(50), nullable=False)
|
||||||
|
account_id = db.Column(db.String(50), nullable=True)
|
||||||
|
status = db.Column(db.String(20), default='active')
|
||||||
|
amount = db.Column(db.Float, nullable=False, default=0.0)
|
||||||
|
salary_date = db.Column(db.DateTime(timezone=False), server_default=func.now())
|
||||||
|
created_at = db.Column(db.DateTime(timezone=False), server_default=func.now())
|
||||||
|
updated_at = db.Column(db.DateTime(timezone=False), server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"customer_id": self.customer_id,
|
||||||
|
"account_id": self.account_id,
|
||||||
|
"status": self.status,
|
||||||
|
"amount": self.amount,
|
||||||
|
"salary_date": self.salary_date.isoformat() if self.salary_date else None,
|
||||||
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||||
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Salary {self.id} - {self.amount}>'
|
||||||
|
|
||||||
+38
-12
@@ -1,37 +1,63 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
|
from app.models import account
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from sqlalchemy import and_, or_, not_
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.api.enums import TransactionType
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Transaction(db.Model):
|
class Transaction(db.Model):
|
||||||
__tablename__ = 'transactions'
|
__tablename__ = 'transactions'
|
||||||
|
id = db.Column(
|
||||||
id = db.Column(db.String(50), primary_key=True)
|
db.Integer,
|
||||||
account_id = db.Column(db.String(50), nullable=False)
|
primary_key=True,
|
||||||
|
autoincrement=True,
|
||||||
|
)
|
||||||
|
transaction_id = db.Column(db.String(50), nullable=False)
|
||||||
|
account_id = db.Column(db.String(50), nullable=True)
|
||||||
|
customer_id = db.Column(db.String(50), nullable=True)
|
||||||
type = db.Column(db.String(50), nullable=False)
|
type = db.Column(db.String(50), nullable=False)
|
||||||
channel = 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))
|
phone_number = db.Column(db.String(50), nullable=True)
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Transaction {self.id}>'
|
return f'<Transaction {self.id}>'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_transaction(cls, id, account_id, type, channel):
|
def create_transaction(cls, transaction_id, account_id, customer_id, type, channel):
|
||||||
if cls.query.filter_by(id=id).first():
|
|
||||||
raise ValueError("Duplicate Transaction")
|
# if cls.query.filter_by(transaction_id=transaction_id).first():
|
||||||
|
# raise ValueError("Duplicate Transaction")
|
||||||
|
|
||||||
|
if cls.query.filter(and_(cls.transaction_id == transaction_id, cls.type == type)).first():
|
||||||
|
if type == TransactionType.REPAYMENT:
|
||||||
|
logger.info('Repayment transaction already exists :::: But we like to continue.')
|
||||||
|
now = datetime.now()
|
||||||
|
type = TransactionType.REPAYMENT + '.'+ now.strftime("%Y%m%d%H%M%S")
|
||||||
|
logger.info('Modify Type :::: {0}'.format(type))
|
||||||
|
else:
|
||||||
|
raise ValueError("Duplicate Transaction")
|
||||||
|
|
||||||
transaction = cls(
|
transaction = cls(
|
||||||
id=id,
|
transaction_id=transaction_id,
|
||||||
|
customer_id=customer_id,
|
||||||
account_id=account_id,
|
account_id=account_id,
|
||||||
type=type,
|
type=type,
|
||||||
channel=channel
|
channel=channel,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
updated_at=datetime.now(timezone.utc)
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db.session.add(transaction)
|
db.session.add(transaction)
|
||||||
db.session.commit()
|
|
||||||
except IntegrityError as err:
|
except IntegrityError as err:
|
||||||
db.session.rollback()
|
|
||||||
raise ValueError(f"Database integrity error: {err}")
|
raise ValueError(f"Database integrity error: {err}")
|
||||||
|
|
||||||
return transaction
|
return transaction
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
from app.api.enums.loan_status import LoanStatus
|
||||||
|
from app.extensions import db
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionOffer(db.Model):
|
||||||
|
__tablename__ = 'transaction_offers'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
customer_id = db.Column(db.String(50), nullable=False)
|
||||||
|
transaction_id = db.Column(db.String(50), nullable=False)
|
||||||
|
original_transaction = db.Column(db.String(50), nullable=True)
|
||||||
|
offer_id = db.Column(db.String(20), nullable=False)
|
||||||
|
product_id = db.Column(db.String(20), nullable=True)
|
||||||
|
min_amount = db.Column(db.Float, nullable=False)
|
||||||
|
max_amount = db.Column(db.Float, nullable=False)
|
||||||
|
eligible_amount = db.Column(db.Float, nullable=True)
|
||||||
|
tenor = db.Column(db.Integer, nullable=True) # tenor in months, typically
|
||||||
|
|
||||||
|
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
customer = relationship(
|
||||||
|
"Customer",
|
||||||
|
primaryjoin="Customer.id == TransactionOffer.customer_id",
|
||||||
|
foreign_keys=[customer_id],
|
||||||
|
back_populates="transaction_offers",
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_valid_transaction_offer(cls, transaction_offer, customer_id, product_id):
|
||||||
|
transaction_offer = cls.query.filter_by(
|
||||||
|
id = transaction_offer,
|
||||||
|
customer_id = customer_id,
|
||||||
|
# product_id = product_id
|
||||||
|
# transaction_id = transaction_id,
|
||||||
|
).first()
|
||||||
|
|
||||||
|
|
||||||
|
if not transaction_offer:
|
||||||
|
return False
|
||||||
|
return transaction_offer
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_transaction_offer(cls, customer_id, transaction_id, original_transaction, offer_id, min_amount, max_amount, eligible_amount=None, product_id=None, tenor=None):
|
||||||
|
"""
|
||||||
|
Class method to create and save a TransactionOffer.
|
||||||
|
"""
|
||||||
|
transaction_offer = cls(
|
||||||
|
customer_id=customer_id,
|
||||||
|
transaction_id=transaction_id,
|
||||||
|
original_transaction=original_transaction,
|
||||||
|
offer_id=offer_id,
|
||||||
|
min_amount=min_amount,
|
||||||
|
max_amount=max_amount,
|
||||||
|
eligible_amount=eligible_amount,
|
||||||
|
product_id=product_id,
|
||||||
|
tenor=tenor,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
updated_at=datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(transaction_offer)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
return transaction_offer
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_lifetime_loan_count(cls, customer_id):
|
||||||
|
"""
|
||||||
|
Returns the total number of loans ever created for a customer.
|
||||||
|
"""
|
||||||
|
return cls.query.filter_by(customer_id=customer_id).count()
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_latest_transaction_offer(cls, customer_id):
|
||||||
|
"""
|
||||||
|
Returns the most recent transaction offer for the given customer based on creation time.
|
||||||
|
"""
|
||||||
|
return cls.query.filter_by(customer_id=customer_id) \
|
||||||
|
.order_by(cls.created_at.desc()) \
|
||||||
|
.first()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_transaction_offer(cls, transaction_offer_id):
|
||||||
|
"""
|
||||||
|
Returns a transaction offer by its ID.
|
||||||
|
"""
|
||||||
|
return cls.query.get(transaction_offer_id)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'customerId': self.customer_id,
|
||||||
|
'transactionId': self.transaction_id,
|
||||||
|
'offerId': self.offer_id,
|
||||||
|
'productId': self.product_id,
|
||||||
|
'minAmount': self.min_amount,
|
||||||
|
'maxAmount': self.max_amount,
|
||||||
|
'eligibleAmount': self.eligible_amount,
|
||||||
|
'tenor': self.tenor,
|
||||||
|
'createdAt': self.created_at.isoformat() if self.created_at else None,
|
||||||
|
'updatedAt': self.updated_at.isoformat() if self.updated_at else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<TransactionOffer {self.id}>'
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"openapi": "3.0.3",
|
"openapi": "3.0.3",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "Swagger Bank Channel to Simbrella FirstAdvance - OpenAPI 3.0",
|
"title": "Swagger Simbrella Core [10/22/2025] - OpenAPI 3.0",
|
||||||
"description": "This is a Simbrella FirstAdvance Backend Server with the OpenAPI 3.0 specification. \n\n\nSome useful links:\n- [Web Simulated Demo Page](https://digifi-salaryloan.chiefsoft.net/)\n- [Web Management Support Portal](https://digifi-office.chiefsoft.net/auth/login)",
|
"description": "This is a Simbrella FirstAdvance Backend Server with the OpenAPI 3.0 specification. \n\n\nSome useful links:\n- [Web Simulated Demo Page](https://digifi-salaryloan.chiefsoft.net/)\n- [Web Management Support Portal](https://digifi-office.chiefsoft.net/auth/login)",
|
||||||
"termsOfService": "http://swagger.io/terms/",
|
"termsOfService": "http://swagger.io/terms/",
|
||||||
"contact": {
|
"contact": {
|
||||||
@@ -19,9 +19,35 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "http://api.dev.simbrellang.net:4500"
|
"url": "http://api.dev.simbrellang.net:4500"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://api.dev.simbrellang.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "http://10.2.249.133:4500"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "Health",
|
||||||
|
"description": "System health check including DB status."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Authorize",
|
||||||
|
"description": "This feature will be used for authorizing customers.",
|
||||||
|
"externalDocs": {
|
||||||
|
"description": "Find out more",
|
||||||
|
"url": "https://www.simbrellang.net"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AuthorizeRefresh",
|
||||||
|
"description": "This feature will be used for refreshing authorized customers.",
|
||||||
|
"externalDocs": {
|
||||||
|
"description": "Find out more",
|
||||||
|
"url": "https://www.simbrellang.net"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "EligibilityCheck",
|
"name": "EligibilityCheck",
|
||||||
"description": "Eligibility Check Request",
|
"description": "Eligibility Check Request",
|
||||||
@@ -61,41 +87,54 @@
|
|||||||
"description": "Find out more",
|
"description": "Find out more",
|
||||||
"url": "https://www.simbrellang.net"
|
"url": "https://www.simbrellang.net"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "CustomerConsent",
|
|
||||||
"description": "CustomerConsent Request.",
|
|
||||||
"externalDocs": {
|
|
||||||
"description": "Find out more",
|
|
||||||
"url": "https://www.simbrellang.net"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "NotificationCallback",
|
|
||||||
"description": "This new feature will be used for informing Simbrella about status of the transactions that FBN have processed.",
|
|
||||||
"externalDocs": {
|
|
||||||
"description": "Find out more",
|
|
||||||
"url": "https://www.simbrellang.net"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Authorize",
|
|
||||||
"description": "This feature will be used for authorizing customers.",
|
|
||||||
"externalDocs": {
|
|
||||||
"description": "Find out more",
|
|
||||||
"url": "https://www.simbrellang.net"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "AuthorizeRefresh",
|
|
||||||
"description": "This feature will be used for refreshing authorized customers.",
|
|
||||||
"externalDocs": {
|
|
||||||
"description": "Find out more",
|
|
||||||
"url": "https://www.simbrellang.net"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"/health": {
|
||||||
|
"get": {
|
||||||
|
"tags": ["Health"],
|
||||||
|
"summary": "Health Check",
|
||||||
|
"description": "Returns service health information including DB connection status.",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Health check successful",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"example": {
|
||||||
|
"status": "ok",
|
||||||
|
"db_status": "Connection Successful",
|
||||||
|
"events_service_status":"Connection Successful",
|
||||||
|
"bank_status":"Connection Successful",
|
||||||
|
"db_uri": "postgresql://user:****@localhost:5432/digifi_db",
|
||||||
|
"error": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Health check failed",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"example": {
|
||||||
|
"status": "failed",
|
||||||
|
"db_status": "Connection Failed",
|
||||||
|
"events_service_status":"Connection Failed",
|
||||||
|
"bank_status":"Connection Failed",
|
||||||
|
"db_uri": "Unavailable",
|
||||||
|
"error":["could not connect to server: Connection refused"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/Authorize": {
|
||||||
|
"$ref": "swagger/paths/Authorize.json"
|
||||||
|
},
|
||||||
|
"/AuthorizeRefresh": {
|
||||||
|
"$ref": "swagger/paths/AuthorizeRefresh.json"
|
||||||
|
},
|
||||||
"/EligibilityCheck": {
|
"/EligibilityCheck": {
|
||||||
"$ref": "swagger/paths/EligibilityCheck.json"
|
"$ref": "swagger/paths/EligibilityCheck.json"
|
||||||
},
|
},
|
||||||
@@ -110,18 +149,6 @@
|
|||||||
},
|
},
|
||||||
"/Repayment": {
|
"/Repayment": {
|
||||||
"$ref": "swagger/paths/Repayment.json"
|
"$ref": "swagger/paths/Repayment.json"
|
||||||
},
|
|
||||||
"/CustomerConsent": {
|
|
||||||
"$ref": "swagger/paths/CustomerConsent.json"
|
|
||||||
},
|
|
||||||
"/NotificationCallback": {
|
|
||||||
"$ref": "swagger/paths/NotificationCallback.json"
|
|
||||||
},
|
|
||||||
"/Authorize": {
|
|
||||||
"$ref": "swagger/paths/Authorize.json"
|
|
||||||
},
|
|
||||||
"/AuthorizeRefresh": {
|
|
||||||
"$ref": "swagger/paths/AuthorizeRefresh.json"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
@@ -150,18 +177,6 @@
|
|||||||
"RepaymentResponse": {
|
"RepaymentResponse": {
|
||||||
"$ref": "swagger/schemas/RepaymentResponse.json"
|
"$ref": "swagger/schemas/RepaymentResponse.json"
|
||||||
},
|
},
|
||||||
"CustomerConsentRequest": {
|
|
||||||
"$ref": "swagger/schemas/CustomerConsentRequest.json"
|
|
||||||
},
|
|
||||||
"CustomerConsentResponse": {
|
|
||||||
"$ref": "swagger/schemas/CustomerConsentResponse.json"
|
|
||||||
},
|
|
||||||
"NotificationCallbackRequest": {
|
|
||||||
"$ref": "swagger/schemas/NotificationCallbackRequest.json"
|
|
||||||
},
|
|
||||||
"NotificationCallbackResponse": {
|
|
||||||
"$ref": "swagger/schemas/NotificationCallbackResponse.json"
|
|
||||||
},
|
|
||||||
"ApiResponse": {
|
"ApiResponse": {
|
||||||
"$ref": "swagger/schemas/ApiResponse.json"
|
"$ref": "swagger/schemas/ApiResponse.json"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"post": {
|
"post": {
|
||||||
"tags": ["Authorize Refresh"],
|
"tags": ["AuthorizeRefresh"],
|
||||||
"summary": "Customer Authorize Refresh Request",
|
"summary": "Customer Authorize Refresh Request",
|
||||||
"description": "Customer Authorize Refresh Request",
|
"description": "Customer Authorize Refresh Request",
|
||||||
"operationId": "AuthorizeRefresh",
|
"operationId": "AuthorizeRefresh",
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
{
|
|
||||||
"post": {
|
|
||||||
"tags": [
|
|
||||||
"CustomerConsent"
|
|
||||||
],
|
|
||||||
"summary": "Customer Consent Request",
|
|
||||||
"description": "Customer Consent Request",
|
|
||||||
"operationId": "CustomerConsent",
|
|
||||||
"requestBody": {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "../schemas/CustomerConsentRequest.json"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"application/xml": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "../schemas/CustomerConsentRequest.json"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"application/x-www-form-urlencoded": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "../schemas/CustomerConsentRequest.json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Successful",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "../schemas/CustomerConsentResponse.json"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"application/xml": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "../schemas/CustomerConsentResponse.json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Invalid request parameters"
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation exception"
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal server error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Invalid request parameters"
|
"description": "Invalid request"
|
||||||
},
|
},
|
||||||
"422": {
|
"422": {
|
||||||
"description": "Validation exception"
|
"description": "Validation exception"
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Invalid request parameters"
|
"description": "Invalid request"
|
||||||
},
|
},
|
||||||
"422": {
|
"422": {
|
||||||
"description": "Validation exception"
|
"description": "Validation exception"
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
{
|
|
||||||
"post": {
|
|
||||||
"tags": [
|
|
||||||
"NotificationCallback"
|
|
||||||
],
|
|
||||||
"summary": "Loan Information Request ",
|
|
||||||
"description": "Loan Information Request",
|
|
||||||
"operationId": "NotificationCallback",
|
|
||||||
"requestBody": {
|
|
||||||
"description": "Post JSON to conduct eligibility tests",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "../schemas/NotificationCallbackRequest.json"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"application/xml": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "../schemas/NotificationCallbackRequest.json"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"application/x-www-form-urlencoded": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "../schemas/NotificationCallbackRequest.json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Successful operation",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "../schemas/NotificationCallbackResponse.json"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"application/xml": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "../schemas/NotificationCallbackResponse.json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Invalid request parameters"
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation exception"
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal server error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Invalid request parameters"
|
"description": "Invalid request"
|
||||||
},
|
},
|
||||||
"422": {
|
"422": {
|
||||||
"description": "Validation exception"
|
"description": "Validation exception"
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Invalid request parameters"
|
"description": "Invalid request"
|
||||||
},
|
},
|
||||||
"422": {
|
"422": {
|
||||||
"description": "Validation exception"
|
"description": "Validation exception"
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Invalid request parameters"
|
"description": "Invalid request"
|
||||||
},
|
},
|
||||||
"422": {
|
"422": {
|
||||||
"description": "Validation exception"
|
"description": "Validation exception"
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "CustomerConsentRequest"
|
|
||||||
},
|
|
||||||
"transactionId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "20171209232177"
|
|
||||||
},
|
|
||||||
"customerId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "CN621868"
|
|
||||||
},
|
|
||||||
"accountId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "ACN8263457"
|
|
||||||
},
|
|
||||||
"requestTime": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"example": "2019-10-18 14:26:21.063"
|
|
||||||
},
|
|
||||||
"consentType": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Revoke"
|
|
||||||
},
|
|
||||||
"channel": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "USSD"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"xml": {
|
|
||||||
"name": "CustomerConsentRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"resultCode": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "00"
|
|
||||||
},
|
|
||||||
"resultDescription": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Request is received"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"xml": {
|
|
||||||
"name": "CustomerConsentResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"transactionId": {
|
"transactionId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Tr201712RK9232P115"
|
"example": "TRX201712RK9232P115"
|
||||||
},
|
},
|
||||||
"countryCode": {
|
"countryCode": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -11,19 +11,19 @@
|
|||||||
},
|
},
|
||||||
"customerId": {
|
"customerId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "CN621868"
|
"example": "5268548"
|
||||||
},
|
|
||||||
"msisdn": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "8093451342"
|
|
||||||
},
|
|
||||||
"channel": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "100"
|
|
||||||
},
|
},
|
||||||
"accountId": {
|
"accountId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "ACN8263457"
|
"example": "4348094226"
|
||||||
|
},
|
||||||
|
"msisdn": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "2348093451342"
|
||||||
|
},
|
||||||
|
"channel": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "USSD"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"xml": {
|
"xml": {
|
||||||
|
|||||||
@@ -3,19 +3,19 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"customerId": {
|
"customerId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "CN621868"
|
"example": "5268548"
|
||||||
},
|
},
|
||||||
"transactionId": {
|
"transactionId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "TX12345"
|
"example": "TRX201712RK9232P115"
|
||||||
},
|
},
|
||||||
"countryCode": {
|
"countryCode": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "NG"
|
"example": "NGR"
|
||||||
},
|
},
|
||||||
"msisdn": {
|
"msisdn": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "3451342"
|
"example": "2348093451342"
|
||||||
},
|
},
|
||||||
"eligibleOffers": {
|
"eligibleOffers": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@@ -24,42 +24,42 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"offerId": {
|
"offerId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Offer1"
|
"example": "SAL90000204"
|
||||||
},
|
},
|
||||||
"productId": {
|
"productId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Product1"
|
"example": "3MPC"
|
||||||
},
|
},
|
||||||
"minAamount": {
|
"minAamount": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"format": "decimal",
|
"format": "decimal",
|
||||||
"example": 100.00
|
"example": 20000.00
|
||||||
},
|
},
|
||||||
"maxAamount": {
|
"maxAamount": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"format": "decimal",
|
"format": "decimal",
|
||||||
"example": 1000.00
|
"example": 31257.00
|
||||||
},
|
},
|
||||||
"tenor": {
|
"tenor": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"example": 12
|
"example": 90
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"example": [
|
"example": [
|
||||||
{
|
{
|
||||||
"offerId": "Offer1",
|
"max_amount": "31257.00",
|
||||||
"productId": "Product1",
|
"min_amount": 20000.0,
|
||||||
"minAamount": 100.00,
|
"offerId": "SAL90000204",
|
||||||
"maxAamount": 1000.00,
|
"product_id": "3MPC",
|
||||||
"tenor": 12
|
"tenor": 90
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"offerId": "Offer2",
|
"max_amount": "20838.00",
|
||||||
"productId": "Product2",
|
"min_amount": 5000.0,
|
||||||
"minAamount": 200.00,
|
"offerId": "SAL30000205",
|
||||||
"maxAamount": 2000.00,
|
"product_id": "AMPC",
|
||||||
"tenor": 24
|
"tenor": 30
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,19 +3,23 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"transactionId": {
|
"transactionId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Tr201712RK9232P115"
|
"example": "TRCVIC73089465966"
|
||||||
},
|
},
|
||||||
"customerId": {
|
"customerId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "CN621868"
|
"example": "ZX48440946"
|
||||||
},
|
},
|
||||||
"msisdn": {
|
"msisdn": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "3451342"
|
"example": "2348093451342"
|
||||||
},
|
},
|
||||||
"channel": {
|
"channel": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "USSD"
|
"example": "USSD"
|
||||||
|
},
|
||||||
|
"accountId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "361005323"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"xml": {
|
"xml": {
|
||||||
|
|||||||
@@ -1,70 +1,133 @@
|
|||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"customerId": {
|
"customerId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "CN621868"
|
"example": "ZX48440946"
|
||||||
},
|
|
||||||
"transactionId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Tr201712RK9232P115"
|
|
||||||
},
|
|
||||||
"loans": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"debtId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "123456789"
|
|
||||||
},
|
|
||||||
"loanDate": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"example": "2019-10-18 14:26:21.063"
|
|
||||||
},
|
|
||||||
"dueDate": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"example": "2019-11-20 14:26:21.063"
|
|
||||||
},
|
|
||||||
"currentLoanAmount": {
|
|
||||||
"type": "integer",
|
|
||||||
"example": 8500
|
|
||||||
},
|
|
||||||
"initialLoanAmount": {
|
|
||||||
"type": "integer",
|
|
||||||
"example": 10000
|
|
||||||
},
|
|
||||||
"defaultPenaltyFee": {
|
|
||||||
"type": "integer",
|
|
||||||
"example": 0
|
|
||||||
},
|
|
||||||
"continuousFee": {
|
|
||||||
"type": "integer",
|
|
||||||
"example": 0
|
|
||||||
},
|
|
||||||
"productId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "101"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"totalDebtAmount": {
|
|
||||||
"type": "integer",
|
|
||||||
"example": 8500
|
|
||||||
},
|
|
||||||
"resultCode": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "00"
|
|
||||||
},
|
|
||||||
"resultDescription": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Successful"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"xml": {
|
"transactionId": {
|
||||||
"name": "LoanStatusResponse"
|
"type": "string",
|
||||||
|
"example": "TRCVIC49381378037"
|
||||||
|
},
|
||||||
|
"accountId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "361005323"
|
||||||
|
},
|
||||||
|
"loans": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"debtId": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 80
|
||||||
|
},
|
||||||
|
"loanDate": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"example": "2025-09-12T11:58:58"
|
||||||
|
},
|
||||||
|
"dueDate": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"example": "2025-12-11T11:58:58"
|
||||||
|
},
|
||||||
|
"currentLoanAmount": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 30000.0
|
||||||
|
},
|
||||||
|
"initialLoanAmount": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 30000.0
|
||||||
|
},
|
||||||
|
"defaultPenaltyFee": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 0.0
|
||||||
|
},
|
||||||
|
"continuousFee": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 0.0
|
||||||
|
},
|
||||||
|
"productId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "3MPC"
|
||||||
|
},
|
||||||
|
"installmentAmount": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 10900.0
|
||||||
|
},
|
||||||
|
"repaymentAmount": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 32700.0
|
||||||
|
},
|
||||||
|
"loanRef": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "TRCVIC73089465966USSD3MPC"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "active"
|
||||||
|
},
|
||||||
|
"tenor": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 90
|
||||||
|
},
|
||||||
|
"transactionId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "TRCVIC73089465966"
|
||||||
|
},
|
||||||
|
"upfrontFee": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 622.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resultCode": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "00"
|
||||||
|
},
|
||||||
|
"resultDescription": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Successful"
|
||||||
|
},
|
||||||
|
"totalDebtAmount": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 30000.0
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"totalOutstandingAmount": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 114450.0,
|
||||||
|
"description": "Total amount still owed across all unpaid loans."
|
||||||
|
},
|
||||||
|
"totalActiveLoanAmount": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 40000.0,
|
||||||
|
"description": "Total principal amount of currently active loans."
|
||||||
|
},
|
||||||
|
"totalSettledAmount": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 80000.0,
|
||||||
|
"description": "Total amount that has been fully repaid."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"xml": {
|
||||||
|
"name": "LoanStatusResponse"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"fbnTransactionId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "123456789"
|
|
||||||
},
|
|
||||||
"transactionId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "123456789"
|
|
||||||
},
|
|
||||||
"customerId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "CN621868"
|
|
||||||
},
|
|
||||||
"accountId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "ACN8263457"
|
|
||||||
},
|
|
||||||
"debtId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "987654321"
|
|
||||||
},
|
|
||||||
"transactionType": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Disbursement"
|
|
||||||
},
|
|
||||||
"amountProvided": {
|
|
||||||
"type": "number",
|
|
||||||
"format": "float",
|
|
||||||
"example": 1000.00
|
|
||||||
},
|
|
||||||
"amountCollected": {
|
|
||||||
"type": "number",
|
|
||||||
"format": "float",
|
|
||||||
"example": 0.00
|
|
||||||
},
|
|
||||||
"responseCode": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "00"
|
|
||||||
},
|
|
||||||
"responseDescription": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Successful"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"xml": {
|
|
||||||
"name": "NotificationCallbackRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"resultCode": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "00"
|
|
||||||
},
|
|
||||||
"resultDescription": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Successful"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"xml": {
|
|
||||||
"name": "NotificationCallbackResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,49 +3,40 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"requestId": {
|
"requestId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "202111170001371256908"
|
"example": "RQID11170001371256908"
|
||||||
},
|
},
|
||||||
"transactionId": {
|
"transactionId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Tr201712RK9232P115"
|
"example": "TRCVIC73089465966"
|
||||||
},
|
},
|
||||||
"customerId": {
|
"customerId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "CN621868"
|
"example": "ZX48440946"
|
||||||
},
|
},
|
||||||
"accountId": {
|
"accountId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "ACN8263457"
|
"example": "361005323"
|
||||||
},
|
},
|
||||||
"msisdn": {
|
"msisdn": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "3451342"
|
"example": "2348093451342"
|
||||||
},
|
|
||||||
"productId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "101"
|
|
||||||
},
|
|
||||||
"lienAmount": {
|
|
||||||
"type": "number",
|
|
||||||
"format": "decimal",
|
|
||||||
"example": 400
|
|
||||||
},
|
},
|
||||||
"requestedAmount": {
|
"requestedAmount": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"format": "decimal",
|
"format": "decimal",
|
||||||
"example": 900
|
"example": 20000
|
||||||
},
|
},
|
||||||
"collectionType": {
|
"collectionType": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"example": 1
|
"example": 0
|
||||||
},
|
},
|
||||||
"offerId": {
|
"offerId": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"example": 1127
|
"example": "SAL900004543304"
|
||||||
},
|
},
|
||||||
"channel": {
|
"channel": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "100"
|
"example": "USSD"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"xml": {
|
"xml": {
|
||||||
|
|||||||
@@ -1,36 +1,75 @@
|
|||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"requestId": {
|
"requestId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "202111170001371256908"
|
"example": "81757678335583"
|
||||||
},
|
|
||||||
"transactionId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Tr201712RK9232P115"
|
|
||||||
},
|
|
||||||
"customerId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "CN621868"
|
|
||||||
},
|
|
||||||
"accountId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "ACN8263457"
|
|
||||||
},
|
|
||||||
"msisdn": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "3451342"
|
|
||||||
},
|
|
||||||
"resultCode": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "00"
|
|
||||||
},
|
|
||||||
"resultDescription": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Successful"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"xml": {
|
"transactionId": {
|
||||||
"name": "ProvideLoanResponse"
|
"type": "string",
|
||||||
|
"example": "TRCVIC73089465966"
|
||||||
|
},
|
||||||
|
"loanRef": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "TRCVIC73089465966USSD3MPC"
|
||||||
|
},
|
||||||
|
"customerId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "ZX48440946"
|
||||||
|
},
|
||||||
|
"accountId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "361005323"
|
||||||
|
},
|
||||||
|
"msisdn": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "98016510058"
|
||||||
|
},
|
||||||
|
"schedule": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "List of loan repayment components with due dates and amounts.",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 1
|
||||||
|
},
|
||||||
|
"amountDue": {
|
||||||
|
"type": "number",
|
||||||
|
"example": 2000.0
|
||||||
|
},
|
||||||
|
"componentName": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "INTEREST"
|
||||||
|
},
|
||||||
|
"dueDate": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"example": "2026-01-13T11:36:39.890747+00:00"
|
||||||
|
},
|
||||||
|
"startDate": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"example": "2025-10-15T11:36:39.890747+00:00"
|
||||||
|
},
|
||||||
|
"loanRef": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "TRX1760528156816285USSD3MPC"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resultCode": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "00"
|
||||||
|
},
|
||||||
|
"resultDescription": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Successful"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"xml": {
|
||||||
|
"name": "ProvideLoanResponse"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,27 +3,27 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"msisdn": {
|
"msisdn": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "3451342"
|
"example": "2348093451342"
|
||||||
},
|
},
|
||||||
"debtId": {
|
"debtId": {
|
||||||
"type": "string",
|
"type": "number",
|
||||||
"example": "273194670"
|
"example": 80
|
||||||
},
|
|
||||||
"productId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "101"
|
|
||||||
},
|
},
|
||||||
"transactionId": {
|
"transactionId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "20171209232115"
|
"example": "TRCVIC73089465966"
|
||||||
},
|
},
|
||||||
"customerId": {
|
"customerId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "CN621868"
|
"example": "ZX48440946"
|
||||||
},
|
},
|
||||||
"channel": {
|
"loanRef": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "USSD"
|
"example": "TRCVIC73089465966USSD3MPC"
|
||||||
|
},
|
||||||
|
"accountId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "361005323"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"xml": {
|
"xml": {
|
||||||
|
|||||||
@@ -1,28 +1,48 @@
|
|||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"customerId": {
|
"Id": {
|
||||||
"type": "string",
|
"type": "integer",
|
||||||
"example": "CN621868"
|
"example": 195
|
||||||
},
|
|
||||||
"productId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "101"
|
|
||||||
},
|
|
||||||
"debtId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "273194670"
|
|
||||||
},
|
|
||||||
"resultCode": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "00"
|
|
||||||
},
|
|
||||||
"resultDescription": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Successful"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"xml": {
|
"customerId": {
|
||||||
"name": "RepaymentResponse"
|
"type": "string",
|
||||||
|
"example": "ZX48440946"
|
||||||
|
},
|
||||||
|
"debtId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "80"
|
||||||
|
},
|
||||||
|
"initiated_by": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "USER_INITIATED"
|
||||||
|
},
|
||||||
|
"loanRef": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "TRCVIC73089465966USSD3MPC"
|
||||||
|
},
|
||||||
|
"productId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "3MPC"
|
||||||
|
},
|
||||||
|
"repayment_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 195
|
||||||
|
},
|
||||||
|
"transactionId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "TRCVIC73089465966"
|
||||||
|
},
|
||||||
|
"resultCode": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "00"
|
||||||
|
},
|
||||||
|
"resultDescription": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Successful"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"xml": {
|
||||||
|
"name": "RepaymentResponse"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,36 +3,40 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"requestId": {
|
"requestId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "202111170001371256908"
|
"example": "RQID11170001371256908"
|
||||||
},
|
},
|
||||||
"transactionId": {
|
"transactionId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "1231231321232"
|
"example": "TRX1231231321232"
|
||||||
},
|
},
|
||||||
"customerId": {
|
"customerId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "CN621868"
|
"example": "CN6215268548868"
|
||||||
},
|
|
||||||
"msisdn": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "123456789"
|
|
||||||
},
|
|
||||||
"requestedAmount": {
|
|
||||||
"type": "number",
|
|
||||||
"format": "double",
|
|
||||||
"example": 10000.55
|
|
||||||
},
|
},
|
||||||
"accountId": {
|
"accountId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "ACN8263457"
|
"example": "4348094226"
|
||||||
},
|
},
|
||||||
|
"msisdn": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "2348093451342"
|
||||||
|
},
|
||||||
|
"requestedAmount": {
|
||||||
|
"type": "number",
|
||||||
|
"example": 20000
|
||||||
|
},
|
||||||
|
|
||||||
"productId": {
|
"productId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "101"
|
"example": "3MPC"
|
||||||
|
},
|
||||||
|
"offerId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "SAL900004543304"
|
||||||
},
|
},
|
||||||
"channel": {
|
"channel": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": ""
|
"example": "USSD"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"xml": {
|
"xml": {
|
||||||
|
|||||||
@@ -1,112 +1,129 @@
|
|||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"requestId": {
|
"requestId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "81757678225025"
|
||||||
|
},
|
||||||
|
"transactionId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "TRCVIC73089465966"
|
||||||
|
},
|
||||||
|
"customerId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "ZX48440946"
|
||||||
|
},
|
||||||
|
"accountId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "361005323"
|
||||||
|
},
|
||||||
|
"loan": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"offerId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "202111170001371256908"
|
"example": "SAL90000204"
|
||||||
},
|
},
|
||||||
"transactionId": {
|
"productId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "1231231321232"
|
"example": "3MPC"
|
||||||
},
|
},
|
||||||
"customerId": {
|
"amount": {
|
||||||
"type": "string",
|
"type": "number",
|
||||||
"example": "1256907"
|
"format": "float",
|
||||||
},
|
"example": 30000.0
|
||||||
"accountId": {
|
},
|
||||||
"type": "string",
|
"upfrontPayment": {
|
||||||
"example": "5948306019"
|
"type": "number",
|
||||||
},
|
"format": "float",
|
||||||
"loan": {
|
"example": 622.5
|
||||||
|
},
|
||||||
|
"interestRate": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 3.0
|
||||||
|
},
|
||||||
|
"interestFee": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 2700.0
|
||||||
|
},
|
||||||
|
"managementRate": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 1.0
|
||||||
|
},
|
||||||
|
"managementFee": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 300.0
|
||||||
|
},
|
||||||
|
"insuranceRate": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 1.0
|
||||||
|
},
|
||||||
|
"insuranceFee": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 300.0
|
||||||
|
},
|
||||||
|
"VATRate": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 7.5
|
||||||
|
},
|
||||||
|
"VATAmount": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 22.5
|
||||||
|
},
|
||||||
|
"recommendedRepaymentDates": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "string"
|
||||||
"properties": {
|
},
|
||||||
"offerId": {
|
"example": [
|
||||||
"type": "string",
|
"2025-10-12",
|
||||||
"example": "14451"
|
"2025-11-12",
|
||||||
},
|
"2025-12-12"
|
||||||
"productId": {
|
]
|
||||||
"type": "string",
|
},
|
||||||
"example": "2030"
|
"installmentAmount": {
|
||||||
},
|
"type": "number",
|
||||||
"amount": {
|
"format": "float",
|
||||||
"type": "number",
|
"example": 10900.0
|
||||||
"format": "float",
|
},
|
||||||
"example": 10000.0
|
"repaymentAmount": {
|
||||||
},
|
"type": "number",
|
||||||
"upfrontPayment": {
|
"format": "float",
|
||||||
"type": "number",
|
"example": 32700.0
|
||||||
"format": "float",
|
},
|
||||||
"example": 1000.0
|
"totalRepaymentAmount": {
|
||||||
},
|
"type": "number",
|
||||||
"interestRate": {
|
"format": "float",
|
||||||
"type": "number",
|
"example": 33322.5
|
||||||
"format": "float",
|
}
|
||||||
"example": 3.0
|
|
||||||
},
|
|
||||||
"ManagementRate": {
|
|
||||||
"type": "number",
|
|
||||||
"format": "float",
|
|
||||||
"example": 1.0
|
|
||||||
},
|
|
||||||
"ManagementFee": {
|
|
||||||
"type": "number",
|
|
||||||
"format": "float",
|
|
||||||
"example": 1.0
|
|
||||||
},
|
|
||||||
"InsuranceRate": {
|
|
||||||
"type": "number",
|
|
||||||
"format": "float",
|
|
||||||
"example": 1.0
|
|
||||||
},
|
|
||||||
"InsuranceFee": {
|
|
||||||
"type": "number",
|
|
||||||
"format": "float",
|
|
||||||
"example": 100.0
|
|
||||||
},
|
|
||||||
"VATRate": {
|
|
||||||
"type": "number",
|
|
||||||
"format": "float",
|
|
||||||
"example": 7.5
|
|
||||||
},
|
|
||||||
"VATamount": {
|
|
||||||
"type": "number",
|
|
||||||
"format": "float",
|
|
||||||
"example": 100.0
|
|
||||||
},
|
|
||||||
"recommendedRepaymentDates": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"example": [
|
|
||||||
"2022-11-30"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"installmentAmount": {
|
|
||||||
"type": "number",
|
|
||||||
"format": "float",
|
|
||||||
"example": 11000.0
|
|
||||||
},
|
|
||||||
"totalRepaymentAmount": {
|
|
||||||
"type": "number",
|
|
||||||
"format": "float",
|
|
||||||
"example": 11000.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"resultCode": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "00"
|
|
||||||
},
|
|
||||||
"resultDescription": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Successful"
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"xml": {
|
"outstandingDebtAmount": {
|
||||||
"name": "SelectOffersResponse"
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 0
|
||||||
|
},
|
||||||
|
"resultCode": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "00"
|
||||||
|
},
|
||||||
|
"resultDescription": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Successful"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"xml": {
|
||||||
|
"name": "SelectOffersResponse"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
from flask_mail import Message
|
||||||
|
from flask import current_app
|
||||||
|
from app.extensions import mail
|
||||||
|
import pandas as pd
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
def get_report_data():
|
||||||
|
"""
|
||||||
|
Fetch and return loan summary data.
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
{"Type": "Disbursement", "Count": 45},
|
||||||
|
{"Type": "Repayment", "Count": 32},
|
||||||
|
]
|
||||||
|
def send_report_email(report_data: list, recipients: list):
|
||||||
|
"""
|
||||||
|
Sends an HTML + Excel report to the given email recipients.
|
||||||
|
"""
|
||||||
|
df = pd.DataFrame(report_data)
|
||||||
|
output = BytesIO()
|
||||||
|
df.to_excel(output, index=False)
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
html_table = df.to_html(index=False, border=1)
|
||||||
|
|
||||||
|
msg = Message(
|
||||||
|
subject="Loan Report Summary",
|
||||||
|
recipients=recipients,
|
||||||
|
html=f"<h3>Loan Report Summary</h3>{html_table}",
|
||||||
|
)
|
||||||
|
msg.attach(
|
||||||
|
"loan_report.xlsx",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
output.read()
|
||||||
|
)
|
||||||
|
|
||||||
|
with current_app.app_context():
|
||||||
|
mail.send(msg)
|
||||||
|
|
||||||
|
return "Report email sent"
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
version: '3.8'
|
||||||
services:
|
services:
|
||||||
digifi-bank-to-product-core:
|
digifi-bank-to-product-core:
|
||||||
build: .
|
build: .
|
||||||
@@ -12,3 +13,13 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
restart: always
|
restart: always
|
||||||
|
networks:
|
||||||
|
- my_custom_network
|
||||||
|
|
||||||
|
|
||||||
|
networks:
|
||||||
|
my_custom_network:
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 10.244.0.0/26
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 05b5494ad406
|
||||||
|
Revises: 33e09efd85e3
|
||||||
|
Create Date: 2025-07-06 09:28:26.264927
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "05b5494ad406"
|
||||||
|
down_revision = "33e09efd85e3"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column("loans", sa.Column("balance", sa.Float(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column("loans", "balance")
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 284be77cb54e
|
||||||
|
Revises: 718dc29fbd38
|
||||||
|
Create Date: 2026-03-12 09:10:37.170288
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "284be77cb54e"
|
||||||
|
down_revision = "718dc29fbd38"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column(
|
||||||
|
"loan_repayment_schedules",
|
||||||
|
sa.Column("last_penal_date", sa.Float(), nullable=True),
|
||||||
|
)
|
||||||
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column("loan_repayment_schedules", "last_penal_date")
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 30b45df851fa
|
||||||
|
Revises: d59bfb9ead82
|
||||||
|
Create Date: 2025-08-26 13:48:27.458593
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '30b45df851fa'
|
||||||
|
down_revision = 'd59bfb9ead82'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column(
|
||||||
|
'loan_repayment_schedules',
|
||||||
|
sa.Column('paid_status', sa.String(length=20), nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
op.add_column(
|
||||||
|
'loan_repayment_schedules',
|
||||||
|
sa.Column('repay_description', sa.String(length=255), nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
op.add_column(
|
||||||
|
'loan_repayment_schedules',
|
||||||
|
sa.Column('partial_balance', sa.Float(), nullable=True),
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('loan_repayment_schedules', 'partial_balance')
|
||||||
|
op.drop_column('loan_repayment_schedules', 'repay_description')
|
||||||
|
op.drop_column('loan_repayment_schedules', 'paid_status')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 33e09efd85e3
|
||||||
|
Revises:
|
||||||
|
Create Date: 2025-07-03 14:07:14.424548
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import literal_column
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '33e09efd85e3'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
sequences_and_triggers = [
|
||||||
|
("transactions", "transactions_seq", "trg_transactions_id"),
|
||||||
|
("transaction_offers", "transaction_offers_seq", "trg_transaction_offers_id"),
|
||||||
|
("salaries", "salaries_seq", "trg_salaries_id"),
|
||||||
|
("repayments_data", "repayments_data_seq", "trg_repayments_data_id"),
|
||||||
|
("repayments", "repayments_seq", "trg_repayments_id"),
|
||||||
|
("rac_checks", "rac_checks_seq", "trg_rac_checks_id"),
|
||||||
|
("loans", "loans_seq", "trg_loans_id"),
|
||||||
|
(
|
||||||
|
"loan_repayment_schedules",
|
||||||
|
"loan_repayment_schedules_seq",
|
||||||
|
"trg_loan_repayment_schedules_id",
|
||||||
|
),
|
||||||
|
("loan_charges", "loan_charges_seq", "trg_loan_charges_id"),
|
||||||
|
("charges", "charges_seq", "trg_charges_id"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('charges',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('offer_id', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('code', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('percent', sa.Float(), nullable=True),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('due', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
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(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('msisdn')
|
||||||
|
)
|
||||||
|
op.create_table('loan_charges',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('loan_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('transaction_id', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('code', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('amount', sa.Float(), nullable=True),
|
||||||
|
sa.Column('percent', sa.Float(), nullable=True),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('due', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('due_date', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('loan_repayment_schedules',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('loan_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('transaction_id', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('product_id', sa.String(length=20), nullable=True),
|
||||||
|
sa.Column('installment_number', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('due_date', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('installment_amount', sa.Float(), nullable=True),
|
||||||
|
sa.Column('total_repayment_amount', sa.Float(), nullable=True),
|
||||||
|
sa.Column('paid', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('paid_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('loans',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('customer_id', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('transaction_id', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('original_transaction', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('account_id', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('offer_id', sa.String(length=20), nullable=False),
|
||||||
|
sa.Column('product_id', sa.String(length=20), nullable=True),
|
||||||
|
sa.Column('collection_type', sa.String(length=20), nullable=True),
|
||||||
|
sa.Column('current_loan_amount', sa.Float(), nullable=True),
|
||||||
|
sa.Column('initial_loan_amount', sa.Float(), nullable=False),
|
||||||
|
sa.Column('default_penalty_fee', sa.Float(), nullable=True),
|
||||||
|
sa.Column('continuous_fee', sa.Float(), nullable=True),
|
||||||
|
sa.Column('upfront_fee', sa.Float(), nullable=True),
|
||||||
|
sa.Column('repayment_amount', sa.Float(), nullable=True),
|
||||||
|
sa.Column('installment_amount', sa.Float(), nullable=True),
|
||||||
|
sa.Column('status', sa.String(length=20), nullable=True),
|
||||||
|
sa.Column('tenor', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('due_date', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.Column('eligible_amount', sa.Float(), nullable=True),
|
||||||
|
sa.Column('disburse_date', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('disburse_verify', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('reference', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('disburse_result', sa.String(length=10), nullable=True),
|
||||||
|
sa.Column('disburse_description', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('verify_result', sa.String(length=10), nullable=True),
|
||||||
|
sa.Column('verify_description', sa.String(length=100), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('offers',
|
||||||
|
sa.Column('id', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('product_id', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('min_amount', sa.Float(), nullable=False),
|
||||||
|
sa.Column('max_amount', sa.Float(), nullable=False),
|
||||||
|
sa.Column('tenor', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('schedule', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('interest_rate', sa.Float(), nullable=True),
|
||||||
|
sa.Column('management_rate', sa.Float(), nullable=True),
|
||||||
|
sa.Column('insurance_rate', sa.Float(), nullable=True),
|
||||||
|
sa.Column('vat_rate', sa.Float(), nullable=True),
|
||||||
|
sa.Column('list_order', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('max_daily_loans', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('max_active_loans', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('max_life_loans', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('rac_checks',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('transaction_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('rac_response', sa.Text(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('repayments',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('loan_id', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('customer_id', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('product_id', sa.String(length=20), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.Column('transaction_id', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('repay_date', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('repay_result', sa.String(length=10), nullable=True),
|
||||||
|
sa.Column('repay_description', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('verify_date', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('verify_result', sa.String(length=10), nullable=True),
|
||||||
|
sa.Column('verify_description', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('initiated_by', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('salary_amount', sa.Float(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('repayments_data',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('transaction_id', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('fbn_transaction_id', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('customer_id', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('account_id', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('repayment_amount', sa.Float(), nullable=True),
|
||||||
|
sa.Column('amount_collected', sa.Float(), nullable=True),
|
||||||
|
sa.Column('added_date', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column('response_code', sa.String(length=10), nullable=True),
|
||||||
|
sa.Column('response_descr', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('balance', sa.Float(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('salaries',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('customer_id', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('account_id', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('status', sa.String(length=20), nullable=True),
|
||||||
|
sa.Column('amount', sa.Float(), nullable=False),
|
||||||
|
sa.Column('salary_date', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('transaction_offers',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('customer_id', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('transaction_id', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('original_transaction', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('offer_id', sa.String(length=20), nullable=False),
|
||||||
|
sa.Column('product_id', sa.String(length=20), nullable=True),
|
||||||
|
sa.Column('min_amount', sa.Float(), nullable=False),
|
||||||
|
sa.Column('max_amount', sa.Float(), nullable=False),
|
||||||
|
sa.Column('eligible_amount', sa.Float(), nullable=True),
|
||||||
|
sa.Column('tenor', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('transactions',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('transaction_id', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('account_id', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('customer_id', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('type', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('channel', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('phone_number', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
|
||||||
|
for table, seq, trg in sequences_and_triggers:
|
||||||
|
op.execute(
|
||||||
|
text(
|
||||||
|
f"""
|
||||||
|
BEGIN
|
||||||
|
EXECUTE IMMEDIATE 'CREATE SEQUENCE {seq} START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
IF SQLCODE != -955 THEN RAISE; END IF;
|
||||||
|
END;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
op.execute(
|
||||||
|
text(
|
||||||
|
f"""
|
||||||
|
CREATE OR REPLACE TRIGGER {trg}
|
||||||
|
BEFORE INSERT ON {table}
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
IF ||':'||'NEW.id IS NULL THEN
|
||||||
|
SELECT {seq}.NEXTVAL INTO '||':'||'NEW.id FROM dual;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('transactions')
|
||||||
|
op.drop_table('transaction_offers')
|
||||||
|
op.drop_table('salaries')
|
||||||
|
op.drop_table('repayments_data')
|
||||||
|
op.drop_table('repayments')
|
||||||
|
op.drop_table('rac_checks')
|
||||||
|
op.drop_table('offers')
|
||||||
|
op.drop_table('loans')
|
||||||
|
op.drop_table('loan_repayment_schedules')
|
||||||
|
op.drop_table('loan_charges')
|
||||||
|
op.drop_table('customers')
|
||||||
|
op.drop_table('charges')
|
||||||
|
op.drop_table('accounts')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
for table, seq, trg in sequences_and_triggers:
|
||||||
|
op.execute(text(f"DROP TRIGGER {trg}"))
|
||||||
|
op.execute(text(f"DROP SEQUENCE {seq}"))
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 718dc29fbd38
|
||||||
|
Revises: 30b45df851fa
|
||||||
|
Create Date: 2026-03-11 14:18:47.523948
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '718dc29fbd38'
|
||||||
|
down_revision = '30b45df851fa'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('loan_repayment_schedules',
|
||||||
|
sa.Column('penal_charge', sa.Float(), nullable=True),
|
||||||
|
)
|
||||||
|
op.add_column('loan_repayment_schedules',
|
||||||
|
sa.Column('penal_count', sa.Integer(), nullable=True),
|
||||||
|
)
|
||||||
|
op.add_column('loans',
|
||||||
|
sa.Column('total_penal_charge', sa.Float(), nullable=True),
|
||||||
|
)
|
||||||
|
op.add_column('loans',
|
||||||
|
sa.Column('last_penal_date', sa.DateTime(), nullable=True),
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('loan_repayment_schedules', 'penal_count')
|
||||||
|
op.drop_column('loan_repayment_schedules', 'penal_charge')
|
||||||
|
op.drop_column('loans', 'total_penal_charge')
|
||||||
|
op.drop_column('loans', 'last_penal_date')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: d59bfb9ead82
|
||||||
|
Revises: 05b5494ad406
|
||||||
|
Create Date: 2025-08-21 14:22:19.220158
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'd59bfb9ead82'
|
||||||
|
down_revision = '05b5494ad406'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
op.add_column(
|
||||||
|
'loan_repayment_schedules',
|
||||||
|
sa.Column('due_process_date', sa.DateTime(), nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add due_process_count column
|
||||||
|
op.add_column(
|
||||||
|
'loan_repayment_schedules',
|
||||||
|
sa.Column('due_process_count', sa.Integer(), nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('loan_repayment_schedules', 'due_process_date')
|
||||||
|
op.drop_column('loan_repayment_schedules', 'due_process_count')
|
||||||
|
# ### end Alembic commands ###
|
||||||
+14
-1
@@ -6,6 +6,7 @@ flask-sqlalchemy
|
|||||||
flask-migrate
|
flask-migrate
|
||||||
psycopg2-binary
|
psycopg2-binary
|
||||||
alembic
|
alembic
|
||||||
|
oracledb
|
||||||
|
|
||||||
# Schema for validations
|
# Schema for validations
|
||||||
Flask-Marshmallow==0.15.0
|
Flask-Marshmallow==0.15.0
|
||||||
@@ -25,8 +26,20 @@ flask-swagger-ui
|
|||||||
python-dotenv
|
python-dotenv
|
||||||
|
|
||||||
# Requests
|
# Requests
|
||||||
requests
|
httpx
|
||||||
|
|
||||||
|
|
||||||
# JWT
|
# JWT
|
||||||
flask-jwt-extended
|
flask-jwt-extended
|
||||||
|
|
||||||
|
|
||||||
|
# Kafka
|
||||||
|
confluent-kafka==1.9.2
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
python-dateutil
|
||||||
|
|
||||||
|
Flask-Mail==0.10.0
|
||||||
|
pandas==2.1.3
|
||||||
|
openpyxl==3.1.5
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
echo "Running DB migrations..."
|
# echo "Running DB migrations..."
|
||||||
flask db upgrade
|
# flask db migrate -m "Migration on $(date)"
|
||||||
|
# flask db upgrade
|
||||||
|
|
||||||
echo "Starting Gunicorn server..."
|
echo "Starting Gunicorn server..."
|
||||||
exec gunicorn -w 4 -b 0.0.0.0:5000 wsgi:wsgi_app
|
exec gunicorn -w 4 -b 0.0.0.0:5000 wsgi:wsgi_app
|
||||||
|
|||||||
@@ -0,0 +1,651 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.3">
|
||||||
|
<hashTree>
|
||||||
|
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Simbrella FirstAdvance API Test">
|
||||||
|
<boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
|
||||||
|
<elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables">
|
||||||
|
<collectionProp name="Arguments.arguments">
|
||||||
|
<elementProp name="baseUrl" elementType="Argument">
|
||||||
|
<stringProp name="Argument.name">baseUrl</stringProp>
|
||||||
|
<stringProp name="Argument.value">http://localhost:4500</stringProp>
|
||||||
|
<stringProp name="Argument.metadata">=</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="username" elementType="Argument">
|
||||||
|
<stringProp name="Argument.name">username</stringProp>
|
||||||
|
<stringProp name="Argument.value">user</stringProp>
|
||||||
|
<stringProp name="Argument.metadata">=</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="password" elementType="Argument">
|
||||||
|
<stringProp name="Argument.name">password</stringProp>
|
||||||
|
<stringProp name="Argument.value">password</stringProp>
|
||||||
|
<stringProp name="Argument.metadata">=</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</elementProp>
|
||||||
|
<boolProp name="TestPlan.functional_mode">false</boolProp>
|
||||||
|
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
|
||||||
|
</TestPlan>
|
||||||
|
<hashTree>
|
||||||
|
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Authorizaton Thread Group">
|
||||||
|
<intProp name="ThreadGroup.num_threads">1</intProp>
|
||||||
|
<intProp name="ThreadGroup.ramp_time">1</intProp>
|
||||||
|
<boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
|
||||||
|
<stringProp name="ThreadGroup.on_sample_error">stopthread</stringProp>
|
||||||
|
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller">
|
||||||
|
<stringProp name="LoopController.loops">1</stringProp>
|
||||||
|
<boolProp name="LoopController.continue_forever">false</boolProp>
|
||||||
|
</elementProp>
|
||||||
|
</ThreadGroup>
|
||||||
|
<hashTree>
|
||||||
|
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="1. Authorize" enabled="true">
|
||||||
|
<stringProp name="HTTPSampler.domain">localhost</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.port">4500</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.protocol">http</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.path">/Authorize</stringProp>
|
||||||
|
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||||
|
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||||
|
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||||
|
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||||
|
<collectionProp name="Arguments.arguments">
|
||||||
|
<elementProp name="" elementType="HTTPArgument">
|
||||||
|
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||||
|
<stringProp name="Argument.value">{
|
||||||
|
"username":"${username}",
|
||||||
|
"password":"${password}"
|
||||||
|
}</stringProp>
|
||||||
|
<stringProp name="Argument.metadata">=</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</elementProp>
|
||||||
|
</HTTPSamplerProxy>
|
||||||
|
<hashTree>
|
||||||
|
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||||
|
<collectionProp name="HeaderManager.headers">
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Content-Type</stringProp>
|
||||||
|
<stringProp name="Header.value">application/json</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Accept</stringProp>
|
||||||
|
<stringProp name="Header.value">application/json</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</HeaderManager>
|
||||||
|
<hashTree/>
|
||||||
|
<JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="JSON Extractor - Auth Token" enabled="true">
|
||||||
|
<stringProp name="JSONPostProcessor.referenceNames">access_token</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.jsonPathExprs">$.data.access_token</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.match_numbers">1</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.defaultValues">NOT_FOUND</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.scope">variable</stringProp>
|
||||||
|
<stringProp name="Scope.variable"></stringProp>
|
||||||
|
<stringProp name="Sample.scope">all</stringProp>
|
||||||
|
</JSONPostProcessor>
|
||||||
|
<hashTree/>
|
||||||
|
<JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="JSON Extractor - Auth Refresh Token" enabled="true">
|
||||||
|
<stringProp name="JSONPostProcessor.referenceNames">refresh_token</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.jsonPathExprs">$.data.refresh_token</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.match_numbers">1</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.defaultValues">NOT_FOUND</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.scope">variable</stringProp>
|
||||||
|
<stringProp name="Scope.variable"></stringProp>
|
||||||
|
<stringProp name="Sample.scope">all</stringProp>
|
||||||
|
</JSONPostProcessor>
|
||||||
|
<hashTree/>
|
||||||
|
<JSR223PostProcessor guiclass="TestBeanGUI" testclass="JSR223PostProcessor" testname="JSR223 PostProcessor" enabled="true">
|
||||||
|
<stringProp name="cacheKey">true</stringProp>
|
||||||
|
<stringProp name="filename"></stringProp>
|
||||||
|
<stringProp name="parameters"></stringProp>
|
||||||
|
<stringProp name="script">props.put("GLOBAL_ACCESS_TOKEN", vars.get("access_token"));
|
||||||
|
props.put("GLOBAL_REFRESH_TOKEN", vars.get("refresh_token"));</stringProp>
|
||||||
|
<stringProp name="scriptLanguage">groovy</stringProp>
|
||||||
|
</JSR223PostProcessor>
|
||||||
|
<hashTree/>
|
||||||
|
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Response Assertion" enabled="true">
|
||||||
|
<collectionProp name="Asserion.test_strings"/>
|
||||||
|
<collectionProp name="Asserter.test_strings">
|
||||||
|
<stringProp name="49586">200</stringProp>
|
||||||
|
</collectionProp>
|
||||||
|
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||||
|
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||||
|
<intProp name="Assertion.test_type">16</intProp>
|
||||||
|
<stringProp name="Assertion.custom_message"></stringProp>
|
||||||
|
</ResponseAssertion>
|
||||||
|
<hashTree/>
|
||||||
|
</hashTree>
|
||||||
|
<DebugSampler guiclass="TestBeanGUI" testclass="DebugSampler" testname="Debug Sampler" enabled="true">
|
||||||
|
<boolProp name="displayJMeterProperties">false</boolProp>
|
||||||
|
<boolProp name="displayJMeterVariables">true</boolProp>
|
||||||
|
<boolProp name="displaySystemProperties">false</boolProp>
|
||||||
|
</DebugSampler>
|
||||||
|
<hashTree/>
|
||||||
|
<ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="true">
|
||||||
|
<boolProp name="ResultCollector.error_logging">false</boolProp>
|
||||||
|
<objProp>
|
||||||
|
<name>saveConfig</name>
|
||||||
|
<value class="SampleSaveConfiguration">
|
||||||
|
<time>true</time>
|
||||||
|
<latency>true</latency>
|
||||||
|
<timestamp>true</timestamp>
|
||||||
|
<success>true</success>
|
||||||
|
<label>true</label>
|
||||||
|
<code>true</code>
|
||||||
|
<message>true</message>
|
||||||
|
<threadName>true</threadName>
|
||||||
|
<dataType>true</dataType>
|
||||||
|
<encoding>false</encoding>
|
||||||
|
<assertions>true</assertions>
|
||||||
|
<subresults>true</subresults>
|
||||||
|
<responseData>false</responseData>
|
||||||
|
<samplerData>false</samplerData>
|
||||||
|
<xml>false</xml>
|
||||||
|
<fieldNames>true</fieldNames>
|
||||||
|
<responseHeaders>false</responseHeaders>
|
||||||
|
<requestHeaders>false</requestHeaders>
|
||||||
|
<responseDataOnError>false</responseDataOnError>
|
||||||
|
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
|
||||||
|
<assertionsResultsToSave>0</assertionsResultsToSave>
|
||||||
|
<bytes>true</bytes>
|
||||||
|
<sentBytes>true</sentBytes>
|
||||||
|
<url>true</url>
|
||||||
|
<threadCounts>true</threadCounts>
|
||||||
|
<idleTime>true</idleTime>
|
||||||
|
<connectTime>true</connectTime>
|
||||||
|
</value>
|
||||||
|
</objProp>
|
||||||
|
<stringProp name="filename"></stringProp>
|
||||||
|
</ResultCollector>
|
||||||
|
<hashTree/>
|
||||||
|
</hashTree>
|
||||||
|
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="API Test Thread Group">
|
||||||
|
<intProp name="ThreadGroup.num_threads">1</intProp>
|
||||||
|
<intProp name="ThreadGroup.ramp_time">1</intProp>
|
||||||
|
<boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
|
||||||
|
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
|
||||||
|
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller">
|
||||||
|
<stringProp name="LoopController.loops">1</stringProp>
|
||||||
|
<boolProp name="LoopController.continue_forever">false</boolProp>
|
||||||
|
</elementProp>
|
||||||
|
</ThreadGroup>
|
||||||
|
<hashTree>
|
||||||
|
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="2. Authorize Refresh" enabled="true">
|
||||||
|
<stringProp name="HTTPSampler.domain">localhost</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.port">4500</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.protocol">http</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.path">/AuthorizeRefresh</stringProp>
|
||||||
|
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||||
|
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||||
|
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||||
|
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||||
|
<collectionProp name="Arguments.arguments">
|
||||||
|
<elementProp name="" elementType="HTTPArgument">
|
||||||
|
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||||
|
<stringProp name="Argument.value">{
|
||||||
|

|
||||||
|
}
|
||||||
|
</stringProp>
|
||||||
|
<stringProp name="Argument.metadata">=</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</elementProp>
|
||||||
|
</HTTPSamplerProxy>
|
||||||
|
<hashTree>
|
||||||
|
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||||
|
<collectionProp name="HeaderManager.headers">
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Content-Type</stringProp>
|
||||||
|
<stringProp name="Header.value">application/json</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Accept</stringProp>
|
||||||
|
<stringProp name="Header.value">application/json</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Authorization</stringProp>
|
||||||
|
<stringProp name="Header.value">Bearer ${__P(GLOBAL_REFRESH_TOKEN)}</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</HeaderManager>
|
||||||
|
<hashTree/>
|
||||||
|
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Response Assertion" enabled="true">
|
||||||
|
<collectionProp name="Asserion.test_strings"/>
|
||||||
|
<collectionProp name="Asserter.test_strings">
|
||||||
|
<stringProp name="49586">200</stringProp>
|
||||||
|
</collectionProp>
|
||||||
|
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||||
|
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||||
|
<intProp name="Assertion.test_type">16</intProp>
|
||||||
|
<stringProp name="Assertion.custom_message"></stringProp>
|
||||||
|
</ResponseAssertion>
|
||||||
|
<hashTree/>
|
||||||
|
<JSR223PreProcessor guiclass="TestBeanGUI" testclass="JSR223PreProcessor" testname="JSR223 PreProcessor" enabled="true">
|
||||||
|
<stringProp name="scriptLanguage">groovy</stringProp>
|
||||||
|
<stringProp name="parameters"></stringProp>
|
||||||
|
<stringProp name="filename"></stringProp>
|
||||||
|
<stringProp name="cacheKey">true</stringProp>
|
||||||
|
<stringProp name="script">// Generate random IDs and store them in JMeter variables
|
||||||
|
def transactionId = "TR" + org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric(12)
|
||||||
|
def customerId = "CN" + org.apache.commons.lang3.RandomUtils.nextInt(1000000, 9999999)
|
||||||
|
def accountId = "ACN" + org.apache.commons.lang3.RandomUtils.nextInt(1000000, 9999999)
|
||||||
|
def msisdn = "809" + org.apache.commons.lang3.RandomUtils.nextInt(1000000, 9999999)
|
||||||
|
|
||||||
|
// Generate requestId: current timestamp + 6-digit random number
|
||||||
|
def timestamp = new Date().format("yyyyMMddHHmmssSSS") // e.g., 20250414161243123
|
||||||
|
def randomSuffix = org.apache.commons.lang3.RandomStringUtils.randomNumeric(6)
|
||||||
|
def requestId = timestamp + randomSuffix
|
||||||
|
|
||||||
|
vars.put("transactionId", transactionId)
|
||||||
|
vars.put("customerId", customerId.toString())
|
||||||
|
vars.put("accountId", accountId.toString())
|
||||||
|
vars.put("msisdn", msisdn.toString())
|
||||||
|
vars.put("requestId", requestId)
|
||||||
|
</stringProp>
|
||||||
|
</JSR223PreProcessor>
|
||||||
|
<hashTree/>
|
||||||
|
</hashTree>
|
||||||
|
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="3. Eligibility Check">
|
||||||
|
<stringProp name="HTTPSampler.domain">localhost</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.port">4500</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.protocol">http</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.path">/EligibilityCheck</stringProp>
|
||||||
|
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||||
|
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||||
|
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||||
|
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||||
|
<collectionProp name="Arguments.arguments">
|
||||||
|
<elementProp name="" elementType="HTTPArgument">
|
||||||
|
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||||
|
<stringProp name="Argument.value">{
|
||||||
|
"transactionId":"${transactionId}",
|
||||||
|
"countryCode":"NGR",
|
||||||
|
"customerId":"${customerId}",
|
||||||
|
"msisdn":"${msisdn}",
|
||||||
|
"channel":"100",
|
||||||
|
"accountId":"${accountId}"
|
||||||
|
}</stringProp>
|
||||||
|
<stringProp name="Argument.metadata">=</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</elementProp>
|
||||||
|
</HTTPSamplerProxy>
|
||||||
|
<hashTree>
|
||||||
|
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||||
|
<collectionProp name="HeaderManager.headers">
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Content-Type</stringProp>
|
||||||
|
<stringProp name="Header.value">application/json</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Accept</stringProp>
|
||||||
|
<stringProp name="Header.value">application/json</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Authorization</stringProp>
|
||||||
|
<stringProp name="Header.value">Bearer ${__P(GLOBAL_ACCESS_TOKEN)}</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</HeaderManager>
|
||||||
|
<hashTree/>
|
||||||
|
<JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="JSON Extractor - Product ID">
|
||||||
|
<stringProp name="JSONPostProcessor.referenceNames">productId</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.jsonPathExprs">$.eligibleOffers[0].productId</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.match_numbers">0</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.defaultValues">NOT_FOUND</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.scope">variable</stringProp>
|
||||||
|
</JSONPostProcessor>
|
||||||
|
<hashTree/>
|
||||||
|
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Response Assertion" enabled="true">
|
||||||
|
<collectionProp name="Asserion.test_strings"/>
|
||||||
|
<collectionProp name="Asserter.test_strings">
|
||||||
|
<stringProp name="49586">200</stringProp>
|
||||||
|
</collectionProp>
|
||||||
|
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||||
|
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||||
|
<intProp name="Assertion.test_type">16</intProp>
|
||||||
|
<stringProp name="Assertion.custom_message"></stringProp>
|
||||||
|
</ResponseAssertion>
|
||||||
|
<hashTree/>
|
||||||
|
</hashTree>
|
||||||
|
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="4. Select Offer">
|
||||||
|
<stringProp name="HTTPSampler.domain">localhost</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.port">4500</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.protocol">http</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.path">/SelectOffer</stringProp>
|
||||||
|
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||||
|
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||||
|
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||||
|
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||||
|
<collectionProp name="Arguments.arguments">
|
||||||
|
<elementProp name="" elementType="HTTPArgument">
|
||||||
|
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||||
|
<stringProp name="Argument.value">{
|
||||||
|
"requestId": "${requestId}",
|
||||||
|
"transactionId": "${transactionId}",
|
||||||
|
"customerId":"${customerId}",
|
||||||
|
"msisdn": "${msisdn}",
|
||||||
|
"requestedAmount": ${__Random(500000,1000000,)}.00,
|
||||||
|
"accountId":"${accountId}",
|
||||||
|
"productId": "${productId}",
|
||||||
|
"channel": "100"
|
||||||
|
}</stringProp>
|
||||||
|
<stringProp name="Argument.metadata">=</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</elementProp>
|
||||||
|
</HTTPSamplerProxy>
|
||||||
|
<hashTree>
|
||||||
|
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||||
|
<collectionProp name="HeaderManager.headers">
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Content-Type</stringProp>
|
||||||
|
<stringProp name="Header.value">application/json</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Accept</stringProp>
|
||||||
|
<stringProp name="Header.value">application/json</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Authorization</stringProp>
|
||||||
|
<stringProp name="Header.value">Bearer ${__P(GLOBAL_ACCESS_TOKEN)}</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</HeaderManager>
|
||||||
|
<hashTree/>
|
||||||
|
<JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="JSON Extractor - Offer ID">
|
||||||
|
<stringProp name="JSONPostProcessor.referenceNames">offerId</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.jsonPathExprs">$.loan[0].offerId</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.match_numbers">0</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.scope">variable</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.defaultValues">NOT_FOUND</stringProp>
|
||||||
|
<stringProp name="Sample.scope">all</stringProp>
|
||||||
|
</JSONPostProcessor>
|
||||||
|
<hashTree/>
|
||||||
|
<JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="JSON Extractor - Requested Amount" enabled="true">
|
||||||
|
<stringProp name="JSONPostProcessor.referenceNames">amount</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.jsonPathExprs">$.loan[0].amount</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.match_numbers">0</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.defaultValues">800</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.scope">variable</stringProp>
|
||||||
|
<stringProp name="Sample.scope">all</stringProp>
|
||||||
|
</JSONPostProcessor>
|
||||||
|
<hashTree/>
|
||||||
|
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Response Assertion" enabled="true">
|
||||||
|
<collectionProp name="Asserion.test_strings"/>
|
||||||
|
<collectionProp name="Asserter.test_strings">
|
||||||
|
<stringProp name="49586">200</stringProp>
|
||||||
|
</collectionProp>
|
||||||
|
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||||
|
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||||
|
<intProp name="Assertion.test_type">16</intProp>
|
||||||
|
<stringProp name="Assertion.custom_message"></stringProp>
|
||||||
|
</ResponseAssertion>
|
||||||
|
<hashTree/>
|
||||||
|
</hashTree>
|
||||||
|
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="5. Provide Loan">
|
||||||
|
<stringProp name="HTTPSampler.domain">localhost</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.port">4500</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.protocol">http</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.path">/ProvideLoan</stringProp>
|
||||||
|
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||||
|
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||||
|
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||||
|
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||||
|
<collectionProp name="Arguments.arguments">
|
||||||
|
<elementProp name="" elementType="HTTPArgument">
|
||||||
|
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||||
|
<stringProp name="Argument.value">{
|
||||||
|
"requestId":"${requestId}",
|
||||||
|
"transactionId":"${transactionId}",
|
||||||
|
"customerId":"${customerId}",
|
||||||
|
"accountId":"${accountId}",
|
||||||
|
"msisdn":"${msisdn}",
|
||||||
|
"requestedAmount":${amount},
|
||||||
|
"collectionType":1,
|
||||||
|
"offerId":"${offerId}",
|
||||||
|
"channel":"100"
|
||||||
|
}</stringProp>
|
||||||
|
<stringProp name="Argument.metadata">=</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</elementProp>
|
||||||
|
</HTTPSamplerProxy>
|
||||||
|
<hashTree>
|
||||||
|
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||||
|
<collectionProp name="HeaderManager.headers">
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Content-Type</stringProp>
|
||||||
|
<stringProp name="Header.value">application/json</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Accept</stringProp>
|
||||||
|
<stringProp name="Header.value">application/json</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Authorization</stringProp>
|
||||||
|
<stringProp name="Header.value">Bearer ${__P(GLOBAL_ACCESS_TOKEN)}</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</HeaderManager>
|
||||||
|
<hashTree/>
|
||||||
|
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Response Assertion" enabled="true">
|
||||||
|
<collectionProp name="Asserion.test_strings"/>
|
||||||
|
<collectionProp name="Asserter.test_strings">
|
||||||
|
<stringProp name="49586">200</stringProp>
|
||||||
|
</collectionProp>
|
||||||
|
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||||
|
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||||
|
<intProp name="Assertion.test_type">16</intProp>
|
||||||
|
<stringProp name="Assertion.custom_message"></stringProp>
|
||||||
|
</ResponseAssertion>
|
||||||
|
<hashTree/>
|
||||||
|
</hashTree>
|
||||||
|
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="6. Loan Status" enabled="true">
|
||||||
|
<stringProp name="HTTPSampler.domain">localhost</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.port">4500</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.protocol">http</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.path">/LoanStatus</stringProp>
|
||||||
|
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||||
|
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||||
|
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||||
|
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||||
|
<collectionProp name="Arguments.arguments">
|
||||||
|
<elementProp name="" elementType="HTTPArgument">
|
||||||
|
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||||
|
<stringProp name="Argument.value">{
|
||||||
|
"transactionId":"${transactionId}",
|
||||||
|
"customerId":"${customerId}",
|
||||||
|
"msisdn":"${msisdn}",
|
||||||
|
"channel":"100",
|
||||||
|
"accountId":"${accountId}"
|
||||||
|
}</stringProp>
|
||||||
|
<stringProp name="Argument.metadata">=</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</elementProp>
|
||||||
|
</HTTPSamplerProxy>
|
||||||
|
<hashTree>
|
||||||
|
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||||
|
<collectionProp name="HeaderManager.headers">
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Content-Type</stringProp>
|
||||||
|
<stringProp name="Header.value">application/json</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Accept</stringProp>
|
||||||
|
<stringProp name="Header.value">application/json</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Authorization</stringProp>
|
||||||
|
<stringProp name="Header.value">Bearer ${__P(GLOBAL_ACCESS_TOKEN)}</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</HeaderManager>
|
||||||
|
<hashTree/>
|
||||||
|
<JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="JSON Extractor - Debt Id" enabled="true">
|
||||||
|
<stringProp name="JSONPostProcessor.referenceNames">debtId</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.jsonPathExprs">$.loans[0].debtId</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.match_numbers">0</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.defaultValues">800</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.scope">variable</stringProp>
|
||||||
|
<stringProp name="Sample.scope">all</stringProp>
|
||||||
|
</JSONPostProcessor>
|
||||||
|
<hashTree/>
|
||||||
|
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Response Assertion" enabled="true">
|
||||||
|
<collectionProp name="Asserion.test_strings"/>
|
||||||
|
<collectionProp name="Asserter.test_strings">
|
||||||
|
<stringProp name="49586">200</stringProp>
|
||||||
|
</collectionProp>
|
||||||
|
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||||
|
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||||
|
<intProp name="Assertion.test_type">16</intProp>
|
||||||
|
<stringProp name="Assertion.custom_message"></stringProp>
|
||||||
|
</ResponseAssertion>
|
||||||
|
<hashTree/>
|
||||||
|
</hashTree>
|
||||||
|
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="7. Repayment" enabled="true">
|
||||||
|
<stringProp name="HTTPSampler.domain">localhost</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.port">4500</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.protocol">http</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.path">/Repayment</stringProp>
|
||||||
|
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||||
|
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||||
|
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||||
|
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||||
|
<collectionProp name="Arguments.arguments">
|
||||||
|
<elementProp name="" elementType="HTTPArgument">
|
||||||
|
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||||
|
<stringProp name="Argument.value">{
|
||||||
|
"debtId":"${debtId}",
|
||||||
|
"transactionId":"${transactionId}",
|
||||||
|
"customerId":"${customerId}",
|
||||||
|
"msisdn":"${msisdn}",
|
||||||
|
"channel":"100",
|
||||||
|
"accountId":"${accountId}",
|
||||||
|
"productId": "${productId}"
|
||||||
|
}</stringProp>
|
||||||
|
<stringProp name="Argument.metadata">=</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</elementProp>
|
||||||
|
</HTTPSamplerProxy>
|
||||||
|
<hashTree>
|
||||||
|
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||||
|
<collectionProp name="HeaderManager.headers">
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Content-Type</stringProp>
|
||||||
|
<stringProp name="Header.value">application/json</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Accept</stringProp>
|
||||||
|
<stringProp name="Header.value">application/json</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Authorization</stringProp>
|
||||||
|
<stringProp name="Header.value">Bearer ${__P(GLOBAL_ACCESS_TOKEN)}</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</HeaderManager>
|
||||||
|
<hashTree/>
|
||||||
|
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Response Assertion" enabled="true">
|
||||||
|
<collectionProp name="Asserion.test_strings"/>
|
||||||
|
<collectionProp name="Asserter.test_strings">
|
||||||
|
<stringProp name="49586">200</stringProp>
|
||||||
|
</collectionProp>
|
||||||
|
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||||
|
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||||
|
<intProp name="Assertion.test_type">16</intProp>
|
||||||
|
<stringProp name="Assertion.custom_message"></stringProp>
|
||||||
|
</ResponseAssertion>
|
||||||
|
<hashTree/>
|
||||||
|
</hashTree>
|
||||||
|
<ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="true">
|
||||||
|
<boolProp name="ResultCollector.error_logging">false</boolProp>
|
||||||
|
<objProp>
|
||||||
|
<name>saveConfig</name>
|
||||||
|
<value class="SampleSaveConfiguration">
|
||||||
|
<time>true</time>
|
||||||
|
<latency>true</latency>
|
||||||
|
<timestamp>true</timestamp>
|
||||||
|
<success>true</success>
|
||||||
|
<label>true</label>
|
||||||
|
<code>true</code>
|
||||||
|
<message>true</message>
|
||||||
|
<threadName>true</threadName>
|
||||||
|
<dataType>true</dataType>
|
||||||
|
<encoding>false</encoding>
|
||||||
|
<assertions>true</assertions>
|
||||||
|
<subresults>true</subresults>
|
||||||
|
<responseData>false</responseData>
|
||||||
|
<samplerData>false</samplerData>
|
||||||
|
<xml>false</xml>
|
||||||
|
<fieldNames>true</fieldNames>
|
||||||
|
<responseHeaders>false</responseHeaders>
|
||||||
|
<requestHeaders>false</requestHeaders>
|
||||||
|
<responseDataOnError>false</responseDataOnError>
|
||||||
|
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
|
||||||
|
<assertionsResultsToSave>0</assertionsResultsToSave>
|
||||||
|
<bytes>true</bytes>
|
||||||
|
<sentBytes>true</sentBytes>
|
||||||
|
<url>true</url>
|
||||||
|
<threadCounts>true</threadCounts>
|
||||||
|
<idleTime>true</idleTime>
|
||||||
|
<connectTime>true</connectTime>
|
||||||
|
</value>
|
||||||
|
</objProp>
|
||||||
|
<stringProp name="filename"></stringProp>
|
||||||
|
</ResultCollector>
|
||||||
|
<hashTree/>
|
||||||
|
<DebugSampler guiclass="TestBeanGUI" testclass="DebugSampler" testname="Debug Sampler" enabled="true">
|
||||||
|
<boolProp name="displayJMeterProperties">false</boolProp>
|
||||||
|
<boolProp name="displayJMeterVariables">true</boolProp>
|
||||||
|
<boolProp name="displaySystemProperties">false</boolProp>
|
||||||
|
</DebugSampler>
|
||||||
|
<hashTree/>
|
||||||
|
</hashTree>
|
||||||
|
<ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree">
|
||||||
|
<boolProp name="ResultCollector.error_logging">false</boolProp>
|
||||||
|
<objProp>
|
||||||
|
<name>saveConfig</name>
|
||||||
|
<value class="SampleSaveConfiguration">
|
||||||
|
<time>true</time>
|
||||||
|
<latency>true</latency>
|
||||||
|
<timestamp>true</timestamp>
|
||||||
|
<success>true</success>
|
||||||
|
<label>true</label>
|
||||||
|
<code>true</code>
|
||||||
|
<message>true</message>
|
||||||
|
<threadName>true</threadName>
|
||||||
|
<dataType>true</dataType>
|
||||||
|
<encoding>false</encoding>
|
||||||
|
<assertions>true</assertions>
|
||||||
|
<subresults>true</subresults>
|
||||||
|
<responseData>false</responseData>
|
||||||
|
<samplerData>false</samplerData>
|
||||||
|
<xml>false</xml>
|
||||||
|
<fieldNames>true</fieldNames>
|
||||||
|
<responseHeaders>false</responseHeaders>
|
||||||
|
<requestHeaders>false</requestHeaders>
|
||||||
|
<responseDataOnError>false</responseDataOnError>
|
||||||
|
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
|
||||||
|
<assertionsResultsToSave>0</assertionsResultsToSave>
|
||||||
|
<bytes>true</bytes>
|
||||||
|
<sentBytes>true</sentBytes>
|
||||||
|
<url>true</url>
|
||||||
|
<threadCounts>true</threadCounts>
|
||||||
|
<idleTime>true</idleTime>
|
||||||
|
<connectTime>true</connectTime>
|
||||||
|
</value>
|
||||||
|
</objProp>
|
||||||
|
<stringProp name="filename"></stringProp>
|
||||||
|
</ResultCollector>
|
||||||
|
<hashTree/>
|
||||||
|
</hashTree>
|
||||||
|
</hashTree>
|
||||||
|
</jmeterTestPlan>
|
||||||
Reference in New Issue
Block a user