Compare commits
116 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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,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);
|
||||||
|
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@ def create_app():
|
|||||||
CORS(app)
|
CORS(app)
|
||||||
|
|
||||||
JWTManager(app)
|
JWTManager(app)
|
||||||
|
CORS(app, supports_credentials=True)
|
||||||
|
|
||||||
# Swagger Doc
|
# Swagger Doc
|
||||||
SWAGGER_URL = app.config.get("SWAGGER_URL")
|
SWAGGER_URL = app.config.get("SWAGGER_URL")
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
from .transaction_type import TransactionType
|
from .transaction_type import TransactionType
|
||||||
|
from .loan_status import LoanStatus
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class LoanStatus(str, Enum):
|
||||||
|
PENDING = "pending"
|
||||||
|
ACTIVE = "active"
|
||||||
|
REPAID = "repaid"
|
||||||
@@ -1 +1,2 @@
|
|||||||
from .simbrella import SimbrellaClient
|
from .simbrella import SimbrellaIntegration
|
||||||
|
from .kafka import KafkaIntegration
|
||||||
@@ -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,8 +1,11 @@
|
|||||||
import requests
|
import httpx
|
||||||
|
import json
|
||||||
from app.utils.logger import logger
|
from app.utils.logger import logger
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
|
import logging
|
||||||
|
|
||||||
class SimbrellaClient:
|
|
||||||
|
class SimbrellaIntegration:
|
||||||
BASE_URL = settings.SIMBRELLA_BASE_URL
|
BASE_URL = settings.SIMBRELLA_BASE_URL
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -10,37 +13,44 @@ class SimbrellaClient:
|
|||||||
"""
|
"""
|
||||||
Calls the RACCheck endpoit
|
Calls the RACCheck endpoit
|
||||||
"""
|
"""
|
||||||
url = f"{SimbrellaClient.BASE_URL}/RACCheck"
|
url = f"{SimbrellaIntegration.BASE_URL}/RACCheck"
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"customerId": customer_id,
|
"customerId": customer_id,
|
||||||
"accountId": account_id,
|
"accountId": account_id,
|
||||||
"transactionId": transaction_id,
|
"transactionId": str(transaction_id),
|
||||||
|
"fbnTransactionId": f"FBN{transaction_id}",
|
||||||
"RAC_Array": [
|
"RAC_Array": [
|
||||||
{
|
"SalaryAccount",
|
||||||
"salaryAccount": True,
|
"BVN",
|
||||||
"bvn": "12345678901",
|
"BVNAttachedtoAccount",
|
||||||
"crc": False,
|
"CRC",
|
||||||
"crms": True,
|
"CRMS",
|
||||||
"accountStatus": "active",
|
"AccountStatus",
|
||||||
"lien": False,
|
"Lien",
|
||||||
"noBouncedCheck": True,
|
"NoBouncedCheck",
|
||||||
"existingLoan": False,
|
"Whitelist",
|
||||||
"whitelist": True,
|
"NoPastDueSalaryLoan",
|
||||||
"noPastDueSalaryLoan": True,
|
"NoPastDueOtherLoan",
|
||||||
"noPastDueOtherLoans": False
|
],
|
||||||
}
|
}
|
||||||
]
|
|
||||||
|
logger.error(f"This is PayLoad: {str(payload)}", exc_info=True)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-api-key": f"{settings.VALID_API_KEY}",
|
||||||
|
"App-Id": f"{settings.VALID_APP_ID}",
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(url, json=payload, timeout=10)
|
response = httpx.post(url, json=payload, headers=headers, timeout=10.0)
|
||||||
|
|
||||||
# Raise an error for non-200 responses
|
logger.error(f"This is Response: {str(response)}", exc_info=True)
|
||||||
# response.raise_for_status()
|
|
||||||
|
|
||||||
|
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)}")
|
||||||
|
|
||||||
return response.json()
|
|
||||||
except requests.exceptions.RequestException as err:
|
|
||||||
logger.error(f"RACCheck API call failed: {str(err)}", exc_info=True)
|
|
||||||
return {"error": "RACCheck API error", "details": str(err)}
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -7,5 +7,6 @@ class RepaymentSchema(Schema):
|
|||||||
debtId = fields.Str(required=True)
|
debtId = fields.Str(required=True)
|
||||||
productId = 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)
|
channel = fields.Str(required=True)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -48,8 +49,102 @@ class BaseService:
|
|||||||
Create a new transaction.
|
Create a new transaction.
|
||||||
"""
|
"""
|
||||||
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 = validated_data.get("channel"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def async_send_to_kafka(cls, loan_data, request_id, topic):
|
||||||
|
KafkaIntegration.send_loan_request(loan_data = loan_data, request_id = request_id, topic = topic)
|
||||||
|
KafkaIntegration.flush()
|
||||||
|
|
||||||
|
|
||||||
|
@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.tenor // 30 # Convert to months
|
||||||
|
interest = cls.get_charge_detail(charges = loan_charges, code = "INTEREST", amount = amount)
|
||||||
|
management = cls.get_charge_detail(charges = loan_charges, code = "MGTFEE", amount = amount)
|
||||||
|
insurance = cls.get_charge_detail(charges = loan_charges, code = "INSURANCE", amount = amount)
|
||||||
|
vat = cls.get_charge_detail(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
|
||||||
|
]
|
||||||
|
|
||||||
|
# Up-front payment: (only those fees due immediately i.e due_days == 0)
|
||||||
|
upfront_payment = sum(upfront_fees)
|
||||||
|
|
||||||
|
# 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 / tenor
|
||||||
|
|
||||||
|
return {
|
||||||
|
"interest": interest,
|
||||||
|
"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, charges, code, amount, management_fee=None):
|
||||||
|
"""
|
||||||
|
Get details for a specific charge code from a list of loan charges.
|
||||||
|
|
||||||
|
Returns default values if not found.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
for charge in charges:
|
||||||
|
if charge.code == code:
|
||||||
|
fee = (
|
||||||
|
management_fee * charge.percent / 100
|
||||||
|
if code == "VAT" and management_fee is not None
|
||||||
|
else amount * charge.percent / 100
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"rate": charge.percent,
|
||||||
|
"fee": round(fee, 2),
|
||||||
|
"due_days": charge.due
|
||||||
|
}
|
||||||
|
|
||||||
|
return {"rate": 0, "fee": 0, "due_days": 0}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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,36 +23,39 @@ 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({
|
||||||
|
"message": "Failed to log transaction."
|
||||||
|
}), 400
|
||||||
|
else:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"message": "Failed to log transaction."
|
"message": "Invalid Customer or Account"
|
||||||
}), 400
|
}), 400
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
"message": "Invalid Customer or Account"
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
|
|
||||||
# Simulated processing logic
|
# Simulated processing logic
|
||||||
response_data = {
|
response_data = {
|
||||||
"resultCode": "00",
|
"resultCode": "00",
|
||||||
"resultDescription": "Request is received"
|
"resultDescription": "Request is received"
|
||||||
}
|
}
|
||||||
|
|
||||||
return response_data
|
db.session.commit()
|
||||||
|
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 jsonify({
|
||||||
"message": "Validation exception"
|
"message": "Validation exception"
|
||||||
@@ -59,6 +63,7 @@ class CustomerConsentService(BaseService):
|
|||||||
|
|
||||||
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 jsonify({
|
||||||
"message": str(err)
|
"message": str(err)
|
||||||
@@ -66,6 +71,7 @@ class CustomerConsentService(BaseService):
|
|||||||
|
|
||||||
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)
|
||||||
|
db.session.rollback()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"message": "Internal Server Error"
|
"message": "Internal Server Error"
|
||||||
}) , 500
|
}) , 500
|
||||||
@@ -4,7 +4,9 @@ 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
|
||||||
|
|
||||||
class EligibilityCheckService(BaseService):
|
class EligibilityCheckService(BaseService):
|
||||||
TRANSACTION_TYPE = TransactionType.ELIGIBILITY_CHECK
|
TRANSACTION_TYPE = TransactionType.ELIGIBILITY_CHECK
|
||||||
@@ -21,68 +23,60 @@ 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")
|
|
||||||
|
if not transaction:
|
||||||
|
logger.error(f"Failed to log transaction")
|
||||||
|
return jsonify({
|
||||||
|
"message": "Failed to log transaction."
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
else:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"message": "Failed to log transaction."
|
"message": "Invalid Customer or Account"
|
||||||
}), 400
|
}), 400
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
"message": "Invalid Customer or Account"
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
# Call RACCheck
|
db.session.flush()
|
||||||
response = SimbrellaClient.rac_check(
|
|
||||||
customer_id = customer_id,
|
|
||||||
account_id = account_id,
|
|
||||||
transaction_id = transaction.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
if "error" in response or response.get("status") != 200:
|
# Call RACCheck
|
||||||
return jsonify({"message": "RACCheck failed", "error": response.get("message", response)}), 400
|
response = SimbrellaIntegration.rac_check(
|
||||||
|
customer_id = customer_id,
|
||||||
|
account_id = account_id,
|
||||||
|
transaction_id = transaction.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# this chck for error is not valid
|
||||||
|
if response.status_code != 200:
|
||||||
|
return jsonify({"message": "RACCheck failed"}), 400
|
||||||
|
|
||||||
|
offers = [offer.to_dict() for offer in Offer.get_all_offers()]
|
||||||
|
|
||||||
offers = [
|
# Simulate processing
|
||||||
{
|
response_data = {
|
||||||
"offerId": "Offer1",
|
"customerId": customer_id,
|
||||||
"productId": "Product1",
|
"transactionId": transactionId,
|
||||||
"minAamount": 100,
|
"countryCode": "NG",
|
||||||
"maxAamount": 1000,
|
"msisdn": msisdn,
|
||||||
"tenor": 12
|
"eligibleOffers": offers,
|
||||||
},
|
"resultDescription": "Successful",
|
||||||
{
|
"resultCode": "00",
|
||||||
"offerId": "Offer2",
|
"accountId": account_id
|
||||||
"productId": "Product2",
|
}
|
||||||
"minAamount": 200,
|
|
||||||
"maxAamount": 2000,
|
|
||||||
"tenor": 24
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Simulate processing
|
return response_data
|
||||||
response_data = {
|
|
||||||
"customerId": "CN621868",
|
|
||||||
"transactionId": "TX12345",
|
|
||||||
"countryCode": "NG",
|
|
||||||
"msisdn": "3451342",
|
|
||||||
"eligibleOffers": offers,
|
|
||||||
"resultDescription": "Successful",
|
|
||||||
"resultCode": "00",
|
|
||||||
"accountId": "ACN8263457"
|
|
||||||
}
|
|
||||||
|
|
||||||
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))}")
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
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
|
||||||
|
|
||||||
|
|
||||||
class LoanStatusService(BaseService):
|
class LoanStatusService(BaseService):
|
||||||
@@ -21,53 +24,69 @@ 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)):
|
|
||||||
transaction = LoanStatusService.log_transaction(validated_data = validated_data)
|
|
||||||
|
|
||||||
if not transaction:
|
customer_id = validated_data.get('customerId')
|
||||||
logger.error(f"Failed to log transaction")
|
customer = Customer.get_customer(customer_id)
|
||||||
|
transactionId = validated_data.get('transactionId')
|
||||||
|
account_id = validated_data.get('accountId')
|
||||||
|
|
||||||
|
if(LoanStatusService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
||||||
|
|
||||||
|
# Get loans
|
||||||
|
loans = [loan.to_dict() for loan in customer.loans if loan.status == LoanStatus.ACTIVE]
|
||||||
|
|
||||||
|
transaction = LoanStatusService.log_transaction(validated_data = validated_data)
|
||||||
|
|
||||||
|
if not transaction:
|
||||||
|
logger.error(f"Failed to log transaction")
|
||||||
|
return jsonify({
|
||||||
|
"message": "Failed to log transaction."
|
||||||
|
}), 400
|
||||||
|
else:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"message": "Failed to log transaction."
|
"message": "Invalid Customer or Account"
|
||||||
}), 400
|
}), 400
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
"message": "Invalid Customer or Account"
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
|
|
||||||
loans = [
|
# loans = [
|
||||||
{
|
# {
|
||||||
"debtId": "123456789",
|
# "debtId": "123456789",
|
||||||
"loanDate": "2019-10-18 14:26:21.063",
|
# "loanDate": "2019-10-18 14:26:21.063",
|
||||||
"dueDate": "2019-11-20 14:26:21.063",
|
# "dueDate": "2019-11-20 14:26:21.063",
|
||||||
"currentLoanAmount": 8500,
|
# "currentLoanAmount": 8500,
|
||||||
"initialLoanAmount": 10000,
|
# "initialLoanAmount": 10000,
|
||||||
"defaultPenaltyFee": 0,
|
# "defaultPenaltyFee": 0,
|
||||||
"continuousFee": 0,
|
# "continuousFee": 0,
|
||||||
"productId": "101"
|
# "productId": "101"
|
||||||
|
# }
|
||||||
|
# ]
|
||||||
|
|
||||||
|
total_debt_amount = sum(
|
||||||
|
loan.get("currentLoanAmount") or 0
|
||||||
|
for loan in loans
|
||||||
|
)
|
||||||
|
|
||||||
|
# Simulated processing logic
|
||||||
|
response_data = {
|
||||||
|
"customerId": customer_id,
|
||||||
|
"transactionId": transactionId,
|
||||||
|
"loans": loans,
|
||||||
|
"totalDebtAmount": total_debt_amount,
|
||||||
|
"resultCode": "00",
|
||||||
|
"resultDescription": "Successful"
|
||||||
}
|
}
|
||||||
]
|
|
||||||
|
|
||||||
# Simulated processing logic
|
db.session.commit()
|
||||||
response_data = {
|
return 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 jsonify({
|
return jsonify({
|
||||||
"message": "Validation exception"
|
"message": "Validation exception"
|
||||||
@@ -75,6 +94,7 @@ class LoanStatusService(BaseService):
|
|||||||
|
|
||||||
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 jsonify({
|
||||||
"message": str(err)
|
"message": str(err)
|
||||||
@@ -82,6 +102,7 @@ class LoanStatusService(BaseService):
|
|||||||
|
|
||||||
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)
|
||||||
|
db.session.rollback()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"message": "Internal Server Error"
|
"message": "Internal Server Error"
|
||||||
}) , 500
|
}) , 500
|
||||||
@@ -4,6 +4,7 @@ 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
|
||||||
|
|
||||||
class NotificationCallbackService(BaseService):
|
class NotificationCallbackService(BaseService):
|
||||||
TRANSACTION_TYPE = TransactionType.NOTIFICATION_CALLBACK
|
TRANSACTION_TYPE = TransactionType.NOTIFICATION_CALLBACK
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
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
|
||||||
|
from app.api.enums import LoanStatus
|
||||||
|
from app.extensions import db
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from app.models import LoanRepaymentSchedule
|
||||||
|
|
||||||
|
|
||||||
class ProvideLoanService(BaseService):
|
class ProvideLoanService(BaseService):
|
||||||
TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN
|
TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN
|
||||||
@@ -21,41 +32,120 @@ 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")
|
||||||
|
|
||||||
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")
|
|
||||||
|
offer = Offer.is_valid_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 jsonify({
|
||||||
|
"message": "Failed to log transaction."
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
charges = ProvideLoanService.calculate_charges(offer, amount)
|
||||||
|
upfront_fee = charges["upfront_payment"]
|
||||||
|
repayment_amount = charges["repayment_amount"]
|
||||||
|
installment_amount = charges["installment_amount"]
|
||||||
|
tenor = offer.tenor // 30 # Convert to months
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 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'),
|
||||||
|
initial_loan_amount = validated_data.get('requestedAmount'),
|
||||||
|
upfront_fee = upfront_fee,
|
||||||
|
repayment_amount = repayment_amount,
|
||||||
|
installment_amount = installment_amount,
|
||||||
|
status= LoanStatus.ACTIVE
|
||||||
|
)
|
||||||
|
|
||||||
|
if not loan:
|
||||||
|
logger.error(f"Failed to save loan details")
|
||||||
|
return jsonify({
|
||||||
|
"message": "Failed to save loan details."
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
schedule = LoanRepaymentSchedule.add_repayment_schedule(loan = loan, tenor = tenor)
|
||||||
|
|
||||||
|
|
||||||
|
if not schedule:
|
||||||
|
logger.error(f"Failed to create repayment schedule for loan ID {loan.id}")
|
||||||
|
return jsonify({
|
||||||
|
"message": "Failed to generate loan repayment schedule."
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
charges = Charge.get_offer_charges(offer.id)
|
||||||
|
|
||||||
|
# logger.error(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 jsonify({
|
return jsonify({
|
||||||
"message": "Failed to log transaction."
|
"message": "Invalid Customer or Account"
|
||||||
}), 400
|
}), 400
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
"message": "Invalid Customer or Account"
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
|
|
||||||
response_data = {
|
response_data = {
|
||||||
"requestId": "202111170001371256908",
|
"requestId": request_id,
|
||||||
"transactionId": "Tr201712RK9232P115",
|
"transactionId": transaction_id,
|
||||||
"customerId": "CN621868",
|
"customerId": customer_id,
|
||||||
"accountId": "ACN8263457",
|
"accountId": account_id,
|
||||||
"msisdn": "3451342",
|
"msisdn": customer.msisdn,
|
||||||
"resultCode": "00",
|
"resultCode": "00",
|
||||||
"resultDescription": "Successful"
|
"resultDescription": "Successful"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# KafkaIntegration.send_loan_request(loan_data = response_data, request_id = request_id)
|
||||||
|
# Call Kafka in a background thread
|
||||||
|
thread = Thread(target=ProvideLoanService.async_send_to_kafka, args=(response_data, request_id, "PROCESS_PAYMENT"))
|
||||||
|
thread.start()
|
||||||
|
|
||||||
return response_data
|
db.session.commit()
|
||||||
|
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 jsonify({
|
||||||
"message": "Validation exception"
|
"message": "Validation exception"
|
||||||
@@ -63,6 +153,7 @@ class ProvideLoanService(BaseService):
|
|||||||
|
|
||||||
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 jsonify({
|
||||||
"message": str(err)
|
"message": str(err)
|
||||||
@@ -70,6 +161,7 @@ class ProvideLoanService(BaseService):
|
|||||||
|
|
||||||
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)
|
||||||
|
db.session.rollback()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"message": "Internal Server Error"
|
"message": "Internal Server Error"
|
||||||
}) , 500
|
}) , 500
|
||||||
@@ -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.enums.loan_status import LoanStatus
|
||||||
|
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
|
||||||
|
|
||||||
class RepaymentService(BaseService):
|
class RepaymentService(BaseService):
|
||||||
TRANSACTION_TYPE = TransactionType.REPAYMENT
|
TRANSACTION_TYPE = TransactionType.REPAYMENT
|
||||||
@@ -20,42 +26,77 @@ 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')
|
customer_id = validated_data.get('customerId')
|
||||||
|
request_id = validated_data.get('requestId')
|
||||||
|
loan_id = validated_data.get('debtId')
|
||||||
|
product_id = validated_data.get('productId')
|
||||||
|
account_id = validated_data.get('accountId')
|
||||||
|
customer = Customer.get_customer(customer_id)
|
||||||
|
transaction_id = validated_data.get('transactionId')
|
||||||
|
|
||||||
if (RepaymentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
if(RepaymentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
||||||
transaction = RepaymentService.log_transaction(validated_data = validated_data)
|
|
||||||
|
|
||||||
if not transaction:
|
# Save the repayment details
|
||||||
logger.error(f"Failed to log transaction")
|
repayment = Repayment.create_repayment(
|
||||||
|
customer_id = customer_id,
|
||||||
|
loan_id = loan_id,
|
||||||
|
product_id = product_id,
|
||||||
|
transaction_id=transaction_id
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
if not repayment:
|
||||||
|
logger.error(f"Failed to save repayment details")
|
||||||
|
return jsonify({
|
||||||
|
"message": "Failed to save repayment details."
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
|
||||||
|
#Update Loan status
|
||||||
|
Loan.update_status(loan_id = loan_id, status = LoanStatus.REPAID)
|
||||||
|
|
||||||
|
transaction = RepaymentService.log_transaction(validated_data = validated_data)
|
||||||
|
|
||||||
|
if not transaction:
|
||||||
|
logger.error(f"Failed to log transaction")
|
||||||
|
return jsonify({
|
||||||
|
"message": "Failed to log transaction."
|
||||||
|
}), 400
|
||||||
|
else:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"message": "Failed to log transaction."
|
"message": "Invalid Customer or Account"
|
||||||
}), 400
|
}), 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(
|
|
||||||
# data=response_data,
|
|
||||||
# message="Repayment processed successfully"
|
|
||||||
# )
|
|
||||||
|
|
||||||
return response_data
|
# Simulated processing logic
|
||||||
|
response_data = {
|
||||||
|
"transactionId": transaction_id,
|
||||||
|
"customerId": customer_id,
|
||||||
|
"productId": product_id,
|
||||||
|
"debtId": loan_id,
|
||||||
|
"resultCode": "00",
|
||||||
|
"resultDescription": "Successful"
|
||||||
|
}
|
||||||
|
|
||||||
|
# return ResponseHelper.success(
|
||||||
|
# data=response_data,
|
||||||
|
# message="Repayment processed successfully"
|
||||||
|
# )
|
||||||
|
|
||||||
|
# Call Kafka in a background thread
|
||||||
|
thread = Thread(target=RepaymentService.async_send_to_kafka, args=(response_data, request_id, "LOAN_REPAYMENT"))
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
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 jsonify({
|
||||||
"message": "Validation exception"
|
"message": "Validation exception"
|
||||||
@@ -63,6 +104,7 @@ class RepaymentService(BaseService):
|
|||||||
|
|
||||||
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 jsonify({
|
||||||
"message": str(err)
|
"message": str(err)
|
||||||
@@ -70,6 +112,7 @@ class RepaymentService(BaseService):
|
|||||||
|
|
||||||
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)
|
||||||
|
db.session.rollback()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"message": "Internal Server Error"
|
"message": "Internal Server Error"
|
||||||
}) , 500
|
}) , 500
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ 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.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 +24,106 @@ 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_id = validated_data.get("transactionId")
|
||||||
|
request_id = validated_data.get("requestId")
|
||||||
|
|
||||||
if (SelectOfferService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
if SelectOfferService.validate_account_ownership(
|
||||||
transaction = SelectOfferService.log_transaction(validated_data = validated_data)
|
account_id=account_id, customer_id=customer_id
|
||||||
|
):
|
||||||
|
transaction = SelectOfferService.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 jsonify({"message": "Failed to log transaction."}), 400
|
||||||
"message": "Failed to log transaction."
|
else:
|
||||||
}), 400
|
return jsonify({"message": "Invalid Customer or Account"}), 400
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
"message": "Invalid Customer or Account"
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
offers = [
|
# Get the offer by product ID
|
||||||
|
offer = Offer.get_offer_by_product_id(product_id)
|
||||||
|
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
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"]
|
||||||
|
|
||||||
|
|
||||||
|
# Calculate the repayment dates
|
||||||
|
tenor = offer.tenor
|
||||||
|
start_date = date.today()
|
||||||
|
|
||||||
|
# Convert tenor to months
|
||||||
|
months = tenor // 30
|
||||||
|
|
||||||
|
recommended_repayment_dates = [
|
||||||
|
(start_date + relativedelta(months=i + 1)).isoformat()
|
||||||
|
for i in range(months)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
offers = [
|
||||||
{
|
{
|
||||||
"offerId": "14451",
|
"offerId": offer.id,
|
||||||
"productId": "2030",
|
"productId": product_id,
|
||||||
"amount": 10000.0,
|
"amount": amount,
|
||||||
"upfrontPayment": 1000.0,
|
"upfrontPayment": upfront_payment,
|
||||||
"interestRate": 3.0,
|
"interestRate": interest["rate"],
|
||||||
"managementRate": 1.0,
|
"managementRate": management["rate"],
|
||||||
"managementFee": 1.0,
|
"managementFee": management["fee"],
|
||||||
"insuranceRate": 1.0,
|
"insuranceRate": insurance["rate"],
|
||||||
"insuranceFee": 100.0,
|
"insuranceFee": insurance["fee"],
|
||||||
"VATRate": 7.5,
|
"VATRate": vat["rate"],
|
||||||
"VATAmount": 100.0,
|
"VATAmount": vat["fee"],
|
||||||
"recommendedRepaymentDates": ["2022-11-30"],
|
"recommendedRepaymentDates": recommended_repayment_dates,
|
||||||
"installmentAmount": 11000.0,
|
"installmentAmount": installment_amount,
|
||||||
"totalRepaymentAmount": 11000.0
|
"totalRepaymentAmount": total_amount,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
# Business logic - selecting an offer
|
# Business logic - selecting an offer
|
||||||
response_data = {
|
response_data = {
|
||||||
"outstandingDebtAmount": 0,
|
"outstandingDebtAmount": 0,
|
||||||
"requestId": "202111170001371256908",
|
"requestId": request_id,
|
||||||
"transactionId": "1231231321232",
|
"transactionId": transaction_id,
|
||||||
"customerId": "1256907",
|
"customerId": customer_id,
|
||||||
"accountId": "5948306019",
|
"accountId": account_id,
|
||||||
"loan": offers,
|
"loan": offers,
|
||||||
"resultCode": "00",
|
"resultCode": "00",
|
||||||
"resultDescription": "Successful"
|
"resultDescription": "Successful",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
return 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({"message": "Validation exception"}), 422
|
||||||
|
|
||||||
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 jsonify({"message": str(err)}), 400
|
||||||
"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 jsonify({"message": "Internal Server Error"}), 500
|
||||||
}) , 500
|
|
||||||
@@ -24,11 +24,14 @@ class Config:
|
|||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
SIMBRELLA_BASE_URL = os.getenv("SIMBRELLA_BASE_URL", "http://127.0.0.1:6337")
|
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))
|
||||||
JWT_REFRESH_TOKEN_EXPIRES = os.getenv(
|
JWT_REFRESH_TOKEN_EXPIRES = os.getenv(
|
||||||
"JWT_REFRESH_TOKEN_EXPIRES", timedelta(days=30)
|
"JWT_REFRESH_TOKEN_EXPIRES", timedelta(days=30)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
KAFKA_BROKER = 'dev-events.simbrellang.net:9085'
|
||||||
|
|
||||||
|
|
||||||
settings = Config()
|
settings = Config()
|
||||||
|
|||||||
@@ -2,5 +2,12 @@ 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
|
||||||
|
|
||||||
__all__ = ['Customer', 'Account', 'Loan', 'Transaction']
|
|
||||||
|
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge', 'RACCheck', 'LoanRepaymentSchedule']
|
||||||
+15
-3
@@ -1,5 +1,7 @@
|
|||||||
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
|
||||||
|
|
||||||
class Account(db.Model):
|
class Account(db.Model):
|
||||||
__tablename__ = 'accounts'
|
__tablename__ = 'accounts'
|
||||||
@@ -12,6 +14,13 @@ class Account(db.Model):
|
|||||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
||||||
|
|
||||||
|
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(
|
||||||
@@ -19,8 +28,11 @@ class Account(db.Model):
|
|||||||
customer_id=customer_id,
|
customer_id=customer_id,
|
||||||
account_type=account_type
|
account_type=account_type
|
||||||
)
|
)
|
||||||
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 +42,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,95 @@
|
|||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
from app.extensions import db
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
|
||||||
|
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, default=datetime.now(timezone.utc))
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
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}>"
|
||||||
+40
-11
@@ -1,6 +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 app.models.account import Account
|
from app.models.account import Account
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
class Customer(db.Model):
|
class Customer(db.Model):
|
||||||
__tablename__ = 'customers'
|
__tablename__ = 'customers'
|
||||||
@@ -11,12 +13,26 @@ class Customer(db.Model):
|
|||||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
||||||
|
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
|
||||||
@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'):
|
||||||
@@ -25,16 +41,29 @@ class Customer(db.Model):
|
|||||||
|
|
||||||
# Create the customer
|
# Create the customer
|
||||||
customer = cls(id=id, msisdn=msisdn, country_code=country_code)
|
customer = cls(id=id, msisdn=msisdn, country_code=country_code)
|
||||||
db.session.add(customer)
|
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(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):
|
||||||
|
|||||||
+136
-5
@@ -1,19 +1,104 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
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)
|
||||||
|
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')
|
||||||
|
due_date = db.Column(db.DateTime, nullable=True)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
||||||
|
|
||||||
|
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,
|
||||||
|
upfront_fee,
|
||||||
|
repayment_amount,
|
||||||
|
installment_amount,
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
initial_loan_amount = initial_loan_amount,
|
||||||
|
current_loan_amount = initial_loan_amount,
|
||||||
|
upfront_fee = upfront_fee,
|
||||||
|
repayment_amount = repayment_amount,
|
||||||
|
installment_amount = installment_amount,
|
||||||
|
due_date=now,
|
||||||
|
status = status
|
||||||
|
)
|
||||||
|
|
||||||
|
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 +108,55 @@ 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.
|
||||||
|
"""
|
||||||
|
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 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
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""
|
||||||
|
Convert the Loan object to a dictionary format for JSON serialization.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'debtId': self.id,
|
||||||
|
'initialLoanAmount': self.initial_loan_amount,
|
||||||
|
'currentLoanAmount': self.current_loan_amount,
|
||||||
|
'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,
|
||||||
|
'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,86 @@
|
|||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
from app.extensions import db
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
|
||||||
|
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, default=datetime.now(timezone.utc))
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
for charge in charges:
|
||||||
|
due_days = getattr(charge, "due", 0)
|
||||||
|
amount = getattr(charge, "amount", 0.0)
|
||||||
|
percent = getattr(charge, "percent", 0.0)
|
||||||
|
|
||||||
|
if amount == 0.0:
|
||||||
|
amount = (percent / 100.0) * referenced_amount
|
||||||
|
|
||||||
|
charge_obj = cls(
|
||||||
|
loan_id = loan_id,
|
||||||
|
transaction_id = transaction_id,
|
||||||
|
code = getattr(charge, "code"),
|
||||||
|
amount = round(amount, 2),
|
||||||
|
percent = percent,
|
||||||
|
description = getattr(charge, "description", ""),
|
||||||
|
due = due_days,
|
||||||
|
due_date = now + timedelta(days=due_days)
|
||||||
|
)
|
||||||
|
|
||||||
|
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,68 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
from app.extensions import db
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
||||||
|
|
||||||
|
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, tenor):
|
||||||
|
"""
|
||||||
|
Add repayment schedules for a given loan.
|
||||||
|
"""
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
schedules = []
|
||||||
|
|
||||||
|
for i in range(tenor):
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
'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}>'
|
||||||
+67
-1
@@ -1,16 +1,82 @@
|
|||||||
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
|
||||||
|
|
||||||
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, primary_key=True)
|
||||||
product_id = db.Column(db.String, nullable=False)
|
product_id = db.Column(db.String, 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)
|
||||||
|
schedule = db.Column(db.Integer, nullable=True)
|
||||||
|
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)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<LoanOffer {self.id}>'
|
return f'<LoanOffer {self.id}>'
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
from app.extensions import db
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from uuid import uuid4
|
||||||
|
from sqlalchemy.types import JSON
|
||||||
|
|
||||||
|
class RACCheck(db.Model):
|
||||||
|
__tablename__ = 'rac_checks'
|
||||||
|
|
||||||
|
id = db.Column(db.String, primary_key=True)
|
||||||
|
transaction_id = db.Column(db.String(50), nullable=False)
|
||||||
|
customer_id = db.Column(db.String, nullable=False)
|
||||||
|
account_id = db.Column(db.String, nullable=False)
|
||||||
|
rac_response = db.Column(db.JSON, nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all_rac_checks(cls):
|
||||||
|
"""
|
||||||
|
Return all RAC checks in dictionary format.
|
||||||
|
"""
|
||||||
|
rac_checks = cls.query.all()
|
||||||
|
|
||||||
|
if not rac_checks:
|
||||||
|
raise ValueError("No available RAC checks")
|
||||||
|
return rac_checks
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_rac_check_by_id(cls, check_id):
|
||||||
|
"""
|
||||||
|
Return a RAC check by its ID.
|
||||||
|
"""
|
||||||
|
rac_check = cls.query.filter_by(id=check_id).first()
|
||||||
|
|
||||||
|
if not rac_check:
|
||||||
|
raise ValueError(f"RAC Check with ID {check_id} 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,
|
||||||
|
"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,55 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class Repayment(db.Model):
|
||||||
|
__tablename__ = 'repayments'
|
||||||
|
|
||||||
|
id = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
primary_key=True,
|
||||||
|
autoincrement=True,
|
||||||
|
)
|
||||||
|
loan_id = db.Column(db.String(50), nullable=False)
|
||||||
|
customer_id = db.Column(db.String(50), nullable=False)
|
||||||
|
product_id = db.Column(db.String(20), nullable=True)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
||||||
|
transaction_id = db.Column(db.String(50), nullable=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_repayment(cls, customer_id, loan_id, product_id, transaction_id):
|
||||||
|
|
||||||
|
|
||||||
|
# Check customer exists
|
||||||
|
if not Customer.is_valid_customer(customer_id):
|
||||||
|
raise ValueError("Invalid customer")
|
||||||
|
|
||||||
|
# Check loan exists
|
||||||
|
loan = Loan.get_customer_loan(loan_id = loan_id, customer_id = customer_id)
|
||||||
|
|
||||||
|
# Check that the loan is active
|
||||||
|
if loan.status != LoanStatus.ACTIVE:
|
||||||
|
raise ValueError(f"Repayment cannot be processed. Loan status: ({loan.status})")
|
||||||
|
|
||||||
|
|
||||||
|
repayment = cls(
|
||||||
|
customer_id=customer_id,
|
||||||
|
loan_id=loan_id,
|
||||||
|
product_id=product_id,
|
||||||
|
transaction_id = transaction_id
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.add(repayment)
|
||||||
|
except IntegrityError as err:
|
||||||
|
raise ValueError(f"Database integrity error: {err}")
|
||||||
|
|
||||||
|
return repayment
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Repayment {self.id}>'
|
||||||
+23
-11
@@ -1,12 +1,19 @@
|
|||||||
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_
|
||||||
|
|
||||||
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))
|
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
||||||
@@ -16,22 +23,27 @@ class Transaction(db.Model):
|
|||||||
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():
|
|
||||||
|
# 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():
|
||||||
raise ValueError("Duplicate Transaction")
|
raise ValueError("Duplicate Transaction")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
transaction = cls(
|
transaction = cls(
|
||||||
id=id,
|
transaction_id = transaction_id,
|
||||||
account_id=account_id,
|
customer_id = customer_id,
|
||||||
type=type,
|
account_id = account_id,
|
||||||
channel=channel
|
type = type,
|
||||||
|
channel = channel
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -19,9 +19,28 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "http://api.dev.simbrellang.net:4500"
|
"url": "http://api.dev.simbrellang.net:4500"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://api.dev.simbrellang.net"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
|
{
|
||||||
|
"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 +80,15 @@
|
|||||||
"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": {
|
||||||
|
"/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 +103,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": {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
"channel": {
|
"channel": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "USSD"
|
"example": "USSD"
|
||||||
|
},
|
||||||
|
"accountId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "ACN8263457"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"xml": {
|
"xml": {
|
||||||
|
|||||||
@@ -47,13 +47,26 @@
|
|||||||
"productId": {
|
"productId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "101"
|
"example": "101"
|
||||||
|
},
|
||||||
|
"installment": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"amount": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 10000.0
|
||||||
|
},
|
||||||
|
"repaymentDate": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "2025-04-24 10:31:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"totalDebtAmount": {
|
}
|
||||||
"type": "integer",
|
}
|
||||||
"example": 8500
|
|
||||||
},
|
},
|
||||||
"resultCode": {
|
"resultCode": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -21,15 +21,6 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "3451342"
|
"example": "3451342"
|
||||||
},
|
},
|
||||||
"productId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "101"
|
|
||||||
},
|
|
||||||
"lienAmount": {
|
|
||||||
"type": "number",
|
|
||||||
"format": "decimal",
|
|
||||||
"example": 400
|
|
||||||
},
|
|
||||||
"requestedAmount": {
|
"requestedAmount": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"format": "decimal",
|
"format": "decimal",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
},
|
},
|
||||||
"debtId": {
|
"debtId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "273194670"
|
"example": "10"
|
||||||
},
|
},
|
||||||
"productId": {
|
"productId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -19,11 +19,15 @@
|
|||||||
},
|
},
|
||||||
"customerId": {
|
"customerId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "CN621868"
|
"example": "CID0000025585"
|
||||||
},
|
},
|
||||||
"channel": {
|
"channel": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "USSD"
|
"example": "USSD"
|
||||||
|
},
|
||||||
|
"accountId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "ACN8263457"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"xml": {
|
"xml": {
|
||||||
|
|||||||
@@ -35,6 +35,10 @@
|
|||||||
"format": "float",
|
"format": "float",
|
||||||
"example": 10000.0
|
"example": 10000.0
|
||||||
},
|
},
|
||||||
|
"dueDate": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "2025-04-24 10:31:"
|
||||||
|
},
|
||||||
"upfrontPayment": {
|
"upfrontPayment": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"format": "float",
|
"format": "float",
|
||||||
@@ -75,7 +79,7 @@
|
|||||||
"format": "float",
|
"format": "float",
|
||||||
"example": 100.0
|
"example": 100.0
|
||||||
},
|
},
|
||||||
"recommendedRepaymentDates": {
|
"installmentRepaymentDates": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""Migration on Thu Apr 10 21:50:01 UTC 2025
|
||||||
|
|
||||||
|
Revision ID: 1340e7e578b9
|
||||||
|
Revises: b8f6fd76ead8
|
||||||
|
Create Date: 2025-04-10 21:50:32.113149
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '1340e7e578b9'
|
||||||
|
down_revision = 'b8f6fd76ead8'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('transactions', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('ref_model', sa.String(length=50), nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('transactions', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('ref_model')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""Migration on Thu Apr 24 17:42:25 UTC 2025
|
||||||
|
|
||||||
|
Revision ID: 1b2339f43824
|
||||||
|
Revises: de9ad96ba34e
|
||||||
|
Create Date: 2025-04-24 17:43:09.589626
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '1b2339f43824'
|
||||||
|
down_revision = 'de9ad96ba34e'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('rac_checks',
|
||||||
|
sa.Column('id', sa.String(), nullable=False),
|
||||||
|
sa.Column('transaction_id', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('customer_id', sa.String(), nullable=False),
|
||||||
|
sa.Column('account_id', sa.String(), nullable=False),
|
||||||
|
sa.Column('rac_response', sa.JSON(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
|
||||||
|
batch_op.alter_column('amount',
|
||||||
|
existing_type=sa.NUMERIC(precision=10, scale=2),
|
||||||
|
type_=sa.Float(),
|
||||||
|
existing_nullable=True)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('loans', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('installment_amount')
|
||||||
|
batch_op.drop_column('repayment_amount')
|
||||||
|
batch_op.drop_column('upfront_fee')
|
||||||
|
|
||||||
|
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
|
||||||
|
batch_op.alter_column('amount',
|
||||||
|
existing_type=sa.Float(),
|
||||||
|
type_=sa.NUMERIC(precision=10, scale=2),
|
||||||
|
existing_nullable=True)
|
||||||
|
|
||||||
|
op.drop_table('rac_checks')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""Migration on Wed Apr 16 18:35:18 UTC 2025
|
||||||
|
|
||||||
|
Revision ID: 287ecb02d3d7
|
||||||
|
Revises: a4847b997191
|
||||||
|
Create Date: 2025-04-16 18:36:04.632791
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '287ecb02d3d7'
|
||||||
|
down_revision = 'a4847b997191'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('transaction_id')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('transaction_id', sa.VARCHAR(length=50), autoincrement=False, nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""Migration on Fri Apr 25 15:01:00 UTC 2025
|
||||||
|
|
||||||
|
Revision ID: 2a45dd99c9cb
|
||||||
|
Revises: 2cf0c177ca02
|
||||||
|
Create Date: 2025-04-25 15:01:51.129681
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '2a45dd99c9cb'
|
||||||
|
down_revision = '2cf0c177ca02'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('product_id', sa.String(length=20), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('installment_amount', sa.Float(), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('total_repayment_amount', sa.Float(), nullable=True))
|
||||||
|
batch_op.drop_column('principal_amount')
|
||||||
|
batch_op.drop_column('interest_amount')
|
||||||
|
batch_op.drop_column('total_installment')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('total_installment', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('interest_amount', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('principal_amount', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
|
||||||
|
batch_op.drop_column('total_repayment_amount')
|
||||||
|
batch_op.drop_column('installment_amount')
|
||||||
|
batch_op.drop_column('product_id')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"""Migration on Fri Apr 25 14:02:01 UTC 2025
|
||||||
|
|
||||||
|
Revision ID: 2cf0c177ca02
|
||||||
|
Revises: 1b2339f43824
|
||||||
|
Create Date: 2025-04-25 14:02:42.244146
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '2cf0c177ca02'
|
||||||
|
down_revision = '1b2339f43824'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
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('installment_number', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('due_date', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('principal_amount', sa.Float(), nullable=True),
|
||||||
|
sa.Column('interest_amount', sa.Float(), nullable=True),
|
||||||
|
sa.Column('total_installment', 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(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('loan_repayment_schedules')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""Migration on Fri Apr 11 14:15:19 UTC 2025
|
||||||
|
|
||||||
|
Revision ID: 610b7e9d15a6
|
||||||
|
Revises: 9bb0367eb486
|
||||||
|
Create Date: 2025-04-11 14:16:12.533227
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '610b7e9d15a6'
|
||||||
|
down_revision = '9bb0367eb486'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('loans', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('transaction_id', sa.String(length=50), nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('loans', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('transaction_id')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""Migration on Mon Apr 14 15:15:05 UTC 2025
|
||||||
|
|
||||||
|
Revision ID: 783a023a477f
|
||||||
|
Revises: f6cd1bfc8832
|
||||||
|
Create Date: 2025-04-14 15:15:36.991148
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '783a023a477f'
|
||||||
|
down_revision = 'f6cd1bfc8832'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('transactions', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('customer_id', sa.String(length=50), nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('transactions', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('customer_id')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"""Migration on Sat Apr 26 12:50:46 UTC 2025
|
||||||
|
|
||||||
|
Revision ID: 89759cebb9c6
|
||||||
|
Revises: 2a45dd99c9cb
|
||||||
|
Create Date: 2025-04-26 12:50:49.771355
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '89759cebb9c6'
|
||||||
|
down_revision = '2a45dd99c9cb'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('offers', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('interest_rate', sa.Float(), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('management_rate', sa.Float(), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('insurance_rate', sa.Float(), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('vat_rate', sa.Float(), nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('offers', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('vat_rate')
|
||||||
|
batch_op.drop_column('insurance_rate')
|
||||||
|
batch_op.drop_column('management_rate')
|
||||||
|
batch_op.drop_column('interest_rate')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""Migration on Fri Apr 11 12:48:01 UTC 2025
|
||||||
|
|
||||||
|
Revision ID: 9bb0367eb486
|
||||||
|
Revises: fd447d78b161
|
||||||
|
Create Date: 2025-04-11 12:48:36.145311
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '9bb0367eb486'
|
||||||
|
down_revision = 'fd447d78b161'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('loans', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('product_id', sa.String(length=20), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('current_loan_amount', sa.Float(), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('default_penalty_fee', sa.Float(), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('continuous_fee', sa.Float(), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('due_date', sa.DateTime(), nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('loans', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('due_date')
|
||||||
|
batch_op.drop_column('continuous_fee')
|
||||||
|
batch_op.drop_column('default_penalty_fee')
|
||||||
|
batch_op.drop_column('current_loan_amount')
|
||||||
|
batch_op.drop_column('product_id')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"""Migration on Wed Apr 16 17:42:49 UTC 2025
|
||||||
|
|
||||||
|
Revision ID: a4847b997191
|
||||||
|
Revises: 783a023a477f
|
||||||
|
Create Date: 2025-04-16 17:43:22.509659
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'a4847b997191'
|
||||||
|
down_revision = '783a023a477f'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
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('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('offers',
|
||||||
|
sa.Column('id', sa.String(), nullable=False),
|
||||||
|
sa.Column('product_id', sa.String(), 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('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('loans', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('product_id', sa.String(length=20), nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('loans', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('product_id')
|
||||||
|
|
||||||
|
op.drop_table('offers')
|
||||||
|
op.drop_table('loan_charges')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
"""Migration on Thu Apr 10 16:21:45 UTC 2025
|
||||||
|
|
||||||
|
Revision ID: b8f6fd76ead8
|
||||||
|
Revises:
|
||||||
|
Create Date: 2025-04-10 16:22:15.946157
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'b8f6fd76ead8'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('repayments',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('loan_id', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('customer_id', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('product_id', sa.String(length=20), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('loans', schema=None) as batch_op:
|
||||||
|
batch_op.alter_column('id',
|
||||||
|
existing_type=sa.VARCHAR(length=50),
|
||||||
|
type_=sa.Integer(),
|
||||||
|
existing_nullable=False,
|
||||||
|
autoincrement=True,
|
||||||
|
existing_server_default=sa.text("nextval('loan_id_seq'::regclass)"))
|
||||||
|
|
||||||
|
with op.batch_alter_table('transactions', schema=None) as batch_op:
|
||||||
|
batch_op.alter_column('channel',
|
||||||
|
existing_type=sa.VARCHAR(length=8),
|
||||||
|
type_=sa.String(length=50),
|
||||||
|
existing_nullable=False)
|
||||||
|
batch_op.alter_column('created_at',
|
||||||
|
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||||
|
type_=sa.DateTime(),
|
||||||
|
existing_nullable=True,
|
||||||
|
existing_server_default=sa.text('now()'))
|
||||||
|
batch_op.alter_column('updated_at',
|
||||||
|
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||||
|
type_=sa.DateTime(),
|
||||||
|
existing_nullable=True,
|
||||||
|
existing_server_default=sa.text('now()'))
|
||||||
|
batch_op.drop_constraint('transactions_id_key', type_='unique')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('transactions', schema=None) as batch_op:
|
||||||
|
batch_op.create_unique_constraint('transactions_id_key', ['id'])
|
||||||
|
batch_op.alter_column('updated_at',
|
||||||
|
existing_type=sa.DateTime(),
|
||||||
|
type_=postgresql.TIMESTAMP(timezone=True),
|
||||||
|
existing_nullable=True,
|
||||||
|
existing_server_default=sa.text('now()'))
|
||||||
|
batch_op.alter_column('created_at',
|
||||||
|
existing_type=sa.DateTime(),
|
||||||
|
type_=postgresql.TIMESTAMP(timezone=True),
|
||||||
|
existing_nullable=True,
|
||||||
|
existing_server_default=sa.text('now()'))
|
||||||
|
batch_op.alter_column('channel',
|
||||||
|
existing_type=sa.String(length=50),
|
||||||
|
type_=sa.VARCHAR(length=8),
|
||||||
|
existing_nullable=False)
|
||||||
|
|
||||||
|
with op.batch_alter_table('loans', schema=None) as batch_op:
|
||||||
|
batch_op.alter_column('id',
|
||||||
|
existing_type=sa.Integer(),
|
||||||
|
type_=sa.VARCHAR(length=50),
|
||||||
|
existing_nullable=False,
|
||||||
|
autoincrement=True,
|
||||||
|
existing_server_default=sa.text("nextval('loan_id_seq'::regclass)"))
|
||||||
|
|
||||||
|
op.drop_table('repayments')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"""Migration on Thu Apr 17 14:15:36 UTC 2025
|
||||||
|
|
||||||
|
Revision ID: de9ad96ba34e
|
||||||
|
Revises: ec8d97f9b584
|
||||||
|
Create Date: 2025-04-17 14:16:16.537466
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'de9ad96ba34e'
|
||||||
|
down_revision = 'ec8d97f9b584'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
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(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('charges')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""Migration on Thu Apr 17 10:40:05 UTC 2025
|
||||||
|
|
||||||
|
Revision ID: ec8d97f9b584
|
||||||
|
Revises: 287ecb02d3d7
|
||||||
|
Create Date: 2025-04-17 10:40:34.751272
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'ec8d97f9b584'
|
||||||
|
down_revision = '287ecb02d3d7'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('transaction_id', sa.String(length=50), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('due_date', sa.DateTime(), nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('due_date')
|
||||||
|
batch_op.drop_column('transaction_id')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""Migration on Fri Apr 11 14:34:36 UTC 2025
|
||||||
|
|
||||||
|
Revision ID: f6cd1bfc8832
|
||||||
|
Revises: 610b7e9d15a6
|
||||||
|
Create Date: 2025-04-11 14:35:07.093967
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'f6cd1bfc8832'
|
||||||
|
down_revision = '610b7e9d15a6'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('loans', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('collection_type', sa.String(length=20), nullable=True))
|
||||||
|
batch_op.drop_column('product_id')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('loans', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('product_id', sa.VARCHAR(length=20), autoincrement=False, nullable=True))
|
||||||
|
batch_op.drop_column('collection_type')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"""Migration on Fri Apr 11 12:02:45 UTC 2025
|
||||||
|
|
||||||
|
Revision ID: fd447d78b161
|
||||||
|
Revises: 1340e7e578b9
|
||||||
|
Create Date: 2025-04-11 12:03:28.346671
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'fd447d78b161'
|
||||||
|
down_revision = '1340e7e578b9'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('transactions', schema=None) as batch_op:
|
||||||
|
batch_op.alter_column('account_id',
|
||||||
|
existing_type=sa.VARCHAR(length=50),
|
||||||
|
nullable=True)
|
||||||
|
batch_op.drop_column('ref_model')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('transactions', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('ref_model', sa.VARCHAR(length=50), autoincrement=False, nullable=True))
|
||||||
|
batch_op.alter_column('account_id',
|
||||||
|
existing_type=sa.VARCHAR(length=50),
|
||||||
|
nullable=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
+10
-1
@@ -25,8 +25,17 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -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