(20241216) Payments requesting started, callback noting started.

This commit is contained in:
2024-12-16 18:29:30 +05:30
parent 0b9976fadb
commit b6ba54abd4
12 changed files with 636 additions and 117 deletions
@@ -0,0 +1,194 @@
"""
AUTHOR:
Khushal P Soonderji
DATE:
Monday, 16th Dec., 2024
OBJECTIVE:
To receive payment updates from various financial institutions.
REFERENCES:
N/A
DOWNLOADS:
N/A
NOTES:
N/A
"""
# *****************************************************************************************************************
# ***** ****
# *** IMPORT ***
# ***** ****
# *****************************************************************************************************************
# To make sibling directories accessible for imports:
import sys
sys.path.append(".")
sys.path.append("..")
# For using Quart:
from quart import Blueprint, current_app, request
# My utils:
from utils_v2.string import json
from utils_v2.api.codes import StatusCodes, HttpCodes
from utils_v2.api.response import ResponseModel
from utils_v2.api.async_quart import (
set_api_version,
read_input,
get_session_info,
log_request_to_mongo,
log_chain_to_mongo,
should_not_be_under_maintenance,
only_whitelisted_ips,
limit_rate,
validate_input,
handle_cancelled_request
)
# Common:
from shared import constants
# To work with MongoDB:
from bson.objectid import ObjectId
# Data Models:
from models.api.finstitutions.payments.request import PGPaymentRequestHeaders, PGPaymentRequestData
from models.core.auth_token import CoreAuthTokenModel
from models.core.payment import CorePaymentModel, PaymentEvent
# For asynchronous activities:
import asyncio
# *****************************************************************************************************************
# ***** ****
# *** MACROS / ONE-TIME INIT ***
# ***** ****
# *****************************************************************************************************************
# Related to Quart:
pg_callback_bp = Blueprint("pg_callback", __name__)
# *****************************************************************************************************************
# ***** ****
# *** VARIABLES ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** FUNCTIONS ***
# ***** ****
# *****************************************************************************************************************
@pg_callback_bp.record_once
def init(blueprint_setup_state):
# This gets called when the blueprint is registered.
# Consider this to be a one-time setup for the whole blueprint:
pass
# ---------------------------------------------------------------------------------------------------------------------
@pg_callback_bp.route("/callback/<payment_id>", methods = ["POST"])
@set_api_version(api_version = "1.0.0")
@read_input(sanitize_headers = False, sanitize_data = False)
@log_request_to_mongo(
attr_name = "logs_mongo",
project = constants.PROJECT_NAME,
log_type = constants.MODULE_NAME,
operation = "pgClbkApi",
log_input = True,
log_output = True,
sensitive_keys = None
)
@log_chain_to_mongo(attr_name = "logs_mongo")
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
@handle_cancelled_request()
async def payment_event_callback(
payment_id: str = None,
inbound_headers: dict = None,
inbound_data: dict = None,
inbound_files: dict = None,
**kwargs
):
"""
We receive payment updates for various payment gateways here.
:param payment_id: The id of the document in MongoDB that holds the reference to the payment.
:param inbound_headers: auto-extracted by the decorators.
:param inbound_data: auto-extracted by the decorators.
:param inbound_files: auto-extracted by the decorators.
:param kwargs: Any number of extra inputs supplied by the decorators.
:return: A standard response structure.
"""
# ┏┓ ┏┓ ┳ ┏
# ┃┓┏┓╋ ┃┃┏┓┓┏┏┳┓┏┓┏┓╋ ┃┏┓╋┏┓
# ┗┛┗ ┗ ┣┛┗┻┗┫┛┗┗┗ ┛┗┗ ┻┛┗┛┗┛
# ┛
# payment_info = await current_app.payment_controller.get_payment_internal(
# mongo_conn = current_app.data_mongo,
# payment_id = ObjectId(payment_id)
# )
# print("PAYMENT INFO:", json.to_string(payment_info.model_dump(), default=str))
# For now, we just insert the event into the record:
event_note_success = await current_app.payment_controller.add_event(
mongo_conn = current_app.data_mongo,
payment_id = ObjectId(payment_id),
event = PaymentEvent(
paymentStatus = "unknown",
initByPG = True,
httpCode = None,
headers = inbound_headers,
payload = inbound_data
)
)
# ┳┓
# ┣┫┏┓┏┏┓┏┓┏┓┏┏┓
# ┛┗┗ ┛┣┛┗┛┛┗┛┗
# ┛
# Done here:
return ResponseModel(
status_code = StatusCodes.OK if event_note_success else StatusCodes.FAILED,
http_code = HttpCodes.NOT_IMPLEMENTED
)
# *****************************************************************************************************************
# ***** ****
# *** MAIN PROGRAM ***
# ***** ****
# *****************************************************************************************************************
if __name__ == "__main__":
pass
@@ -0,0 +1,211 @@
"""
AUTHOR:
Khushal P Soonderji
DATE:
Monday, 16th Dec., 2024
OBJECTIVE:
To request payments from customers from various financial institutions.
REFERENCES:
N/A
DOWNLOADS:
N/A
NOTES:
N/A
"""
# *****************************************************************************************************************
# ***** ****
# *** IMPORT ***
# ***** ****
# *****************************************************************************************************************
# To make sibling directories accessible for imports:
import sys
sys.path.append(".")
sys.path.append("..")
# For using Quart:
from quart import Blueprint, current_app, request
# My utils:
from utils_v2.string import json
from utils_v2.api.codes import StatusCodes, HttpCodes
from utils_v2.api.response import ResponseModel
from utils_v2.api.async_quart import (
set_api_version,
read_input,
get_session_info,
log_request_to_mongo,
log_chain_to_mongo,
should_not_be_under_maintenance,
only_whitelisted_ips,
limit_rate,
validate_input,
handle_cancelled_request
)
# Common:
from shared import constants
# To work with MongoDB:
from bson.objectid import ObjectId
# Data Models:
from models.core.user import CoreUserInfoModel
from models.api.finstitutions.payments.request import PGPaymentRequestHeaders, PGPaymentRequestData
from models.core.auth_token import CoreAuthTokenModel
# For asynchronous activities:
import asyncio
# *****************************************************************************************************************
# ***** ****
# *** MACROS / ONE-TIME INIT ***
# ***** ****
# *****************************************************************************************************************
# Related to Quart:
pg_request_bp = Blueprint("pg_request", __name__)
# *****************************************************************************************************************
# ***** ****
# *** VARIABLES ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** FUNCTIONS ***
# ***** ****
# *****************************************************************************************************************
@pg_request_bp.record_once
def init(blueprint_setup_state):
# This gets called when the blueprint is registered.
# Consider this to be a one-time setup for the whole blueprint:
pass
# ---------------------------------------------------------------------------------------------------------------------
@pg_request_bp.route("/request", methods = ["POST"])
@set_api_version(api_version = "1.0.0")
@read_input(sanitize_headers = False, sanitize_data = False)
@get_session_info(key = "X-Session-Token", session_coro = "get_session")
@log_request_to_mongo(
attr_name = "logs_mongo",
project = constants.PROJECT_NAME,
log_type = constants.MODULE_NAME,
operation = "pgReqApi",
log_input = True,
log_output = True,
sensitive_keys = ["sessionToken", "X-Session-Token"]
)
@log_chain_to_mongo(attr_name = "logs_mongo")
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
@validate_input(
header_validator = lambda x: PGPaymentRequestHeaders(**x).model_dump(),
data_validator = lambda x: PGPaymentRequestData(**x)
)
@handle_cancelled_request()
async def request_payment(
inbound_headers: dict | PGPaymentRequestHeaders = None,
inbound_data: dict | PGPaymentRequestData = None,
inbound_files: dict = None,
**kwargs
):
"""
To request payments from customers through payment gateways.
:param inbound_headers: auto-extracted by the decorators.
:param inbound_data: auto-extracted by the decorators.
:param inbound_files: auto-extracted by the decorators.
:param kwargs: Any number of extra inputs supplied by the decorators.
:return: A standard response structure.
"""
# ┏┓
# ┃┃┏┓┏┓┏┓┏┓┏┓┏┏┓┏┏
# ┣┛┛ ┗ ┣┛┛ ┗┛┗┗ ┛┛
# ┛
# If the session token is invalid/expired:
if kwargs.get("session_info") is None:
return ResponseModel(
status_code = StatusCodes.FAILED,
http_code = HttpCodes.UNAUTHORIZED
)
# ┳┓ ┏┓
# ┣┫┏┓┏┓┓┏┏┓┏╋ ┃┃┏┓┓┏┏┳┓┏┓┏┓╋
# ┛┗┗ ┗┫┗┻┗ ┛┗ ┣┛┗┻┗┫┛┗┗┗ ┛┗┗
# ┗ ┛
# Get the token from the token key:
auth_token = await current_app.payment_controller.get_token(
mongo_conn = current_app.data_mongo,
token_key = inbound_data.tokenKey
)
if auth_token is None: return ResponseModel(
status_code = StatusCodes.FAILED,
http_code = HttpCodes.UNAUTHORIZED,
message = f"no such token key"
)
# Make the request with the fetched token:
response = await current_app.payment_controller.request_payment(
mongo_conn = current_app.data_mongo,
http_client = current_app.http_client,
auth_token = auth_token,
user_info = CoreUserInfoModel(**kwargs["session_info"]),
payment_request = inbound_data
)
# ┳┓
# ┣┫┏┓┏┏┓┏┓┏┓┏┏┓
# ┛┗┗ ┛┣┛┗┛┛┗┛┗
# ┛
# Done here:
return ResponseModel(
status_code = StatusCodes.OK if response.success else StatusCodes.FAILED,
http_code = HttpCodes.SUCCESS if response.success else HttpCodes.INTERNAL_SERVER_ERROR,
message = response.message
)
# *****************************************************************************************************************
# ***** ****
# *** MAIN PROGRAM ***
# ***** ****
# *****************************************************************************************************************
if __name__ == "__main__":
pass
+16 -3
View File
@@ -65,7 +65,8 @@ from utils_v2.goog.gmail.gmail_client import AsyncGMailClient
# Core Controller Models: # Core Controller Models:
from controllers.core.message import CoreMessageController from controllers.core.message import CoreMessageController
from controllers.core.auth_token import CoreAuthTokenController from controllers.core.auth_token import CoreAuthTokenController
from controllers.core.ai.llm import LLMController from controllers.core.ai.llm import CoreLLMController
from controllers.core.payment import CorePaymentController
# API Controller Models: # API Controller Models:
from controllers.api.mail import MailController from controllers.api.mail import MailController
@@ -98,6 +99,8 @@ from api.blueprints.sms.send import sms_send_bp
# from api.blueprints.chat.webhook import chat_webhook_bp # from api.blueprints.chat.webhook import chat_webhook_bp
from api.blueprints.software.auth import sw_auth_bp from api.blueprints.software.auth import sw_auth_bp
from api.blueprints.finstitutions.payments.auth import pg_auth_bp from api.blueprints.finstitutions.payments.auth import pg_auth_bp
from api.blueprints.finstitutions.payments.request import pg_request_bp
from api.blueprints.finstitutions.payments.callback import pg_callback_bp
from api.blueprints.tech.chat_alerts import tech_chat_alert_bp from api.blueprints.tech.chat_alerts import tech_chat_alert_bp
from api.blueprints.test.callback import test_callback_bp from api.blueprints.test.callback import test_callback_bp
from api.blueprints.ai.llm.invoke import llm_invoke_bp from api.blueprints.ai.llm.invoke import llm_invoke_bp
@@ -140,6 +143,8 @@ app.register_blueprint(sms_send_bp, url_prefix = f"/{MODULE_BASE}/sms")
# app.register_blueprint(chat_webhook_bp, url_prefix = f"/{MODULE_BASE}/chat") # app.register_blueprint(chat_webhook_bp, url_prefix = f"/{MODULE_BASE}/chat")
app.register_blueprint(sw_auth_bp, url_prefix = f"/{MODULE_BASE}/software") app.register_blueprint(sw_auth_bp, url_prefix = f"/{MODULE_BASE}/software")
app.register_blueprint(pg_auth_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/payments") app.register_blueprint(pg_auth_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/payments")
app.register_blueprint(pg_request_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/payments")
app.register_blueprint(pg_callback_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/payments")
app.register_blueprint(tech_chat_alert_bp, url_prefix = f"/{MODULE_BASE}/tech/alert") app.register_blueprint(tech_chat_alert_bp, url_prefix = f"/{MODULE_BASE}/tech/alert")
app.register_blueprint(test_callback_bp, url_prefix = f"/{MODULE_BASE}/test") app.register_blueprint(test_callback_bp, url_prefix = f"/{MODULE_BASE}/test")
app.register_blueprint(llm_invoke_bp, url_prefix = f"/{MODULE_BASE}/ai") app.register_blueprint(llm_invoke_bp, url_prefix = f"/{MODULE_BASE}/ai")
@@ -334,7 +339,7 @@ async def app_startup(**kwargs):
alert_url = current_app.script_data["alerts"]["url"], alert_url = current_app.script_data["alerts"]["url"],
http_client = current_app.http_client, http_client = current_app.http_client,
debug = enable_debugging, debug = enable_debugging,
debug_prefix = "Message (CM) | ", debug_prefix = "AuthToken (CM) | ",
debug_only_errors = True debug_only_errors = True
) )
current_app.core_message_controller = CoreMessageController( current_app.core_message_controller = CoreMessageController(
@@ -345,6 +350,14 @@ async def app_startup(**kwargs):
debug_prefix = "Message (CM) | ", debug_prefix = "Message (CM) | ",
debug_only_errors = True debug_only_errors = True
) )
current_app.core_payment_controller = CorePaymentController(
cache = current_app.module_cache,
alert_url = current_app.script_data["alerts"]["url"],
http_client = current_app.http_client,
debug = enable_debugging,
debug_prefix = "Pymnt. (CM) | ",
debug_only_errors = True
)
# ┏┓┏┓┳ ┏┓ ┓┓ # ┏┓┏┓┳ ┏┓ ┓┓
# ┣┫┃┃┃ ┃ ┏┓┏┓╋┏┓┏┓┃┃┏┓┏┓┏ # ┣┫┃┃┃ ┃ ┏┓┏┓╋┏┓┏┓┃┃┏┓┏┓┏
@@ -377,7 +390,7 @@ async def app_startup(**kwargs):
# ┛ # ┛
# For LLMs: # For LLMs:
current_app.llm = LLMController( current_app.llm = CoreLLMController(
llm_creds = { llm_creds = {
"model": script_cred["openAi"]["model"], "model": script_cred["openAi"]["model"],
"openai_api_key": script_cred["openAi"]["openai_api_key"] "openai_api_key": script_cred["openAi"]["openai_api_key"]
+5 -5
View File
@@ -61,7 +61,7 @@ from bson import ObjectId
from pymongo import InsertOne, UpdateOne, ReplaceOne from pymongo import InsertOne, UpdateOne, ReplaceOne
# To work with LLMs: # To work with LLMs:
from controllers.core.ai.llm import LLMController from controllers.core.ai.llm import CoreLLMController
from models.core.ai.llm import LLMInput, LLMOutput from models.core.ai.llm import LLMInput, LLMOutput
# To work with datatypes: # To work with datatypes:
@@ -191,7 +191,7 @@ class MailController:
self, self,
mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
user_info: CoreUserInfoModel, user_info: CoreUserInfoModel,
llm: LLMController, llm: CoreLLMController,
message: CoreMessageModel message: CoreMessageModel
) -> LLMOutput: ) -> LLMOutput:
@@ -319,7 +319,7 @@ class MailController:
mail_client: AsyncGMailClient, mail_client: AsyncGMailClient,
google_tokens: GoogleAuthTokens, google_tokens: GoogleAuthTokens,
message_id: str, message_id: str,
llm: LLMController = None, llm: CoreLLMController = None,
force_sync: bool = False force_sync: bool = False
) -> MailSyncOneResult: ) -> MailSyncOneResult:
@@ -411,7 +411,7 @@ class MailController:
user_info: CoreUserInfoModel, user_info: CoreUserInfoModel,
auth_token: CoreAuthTokenModel, auth_token: CoreAuthTokenModel,
mail_client: AsyncGMailClient, mail_client: AsyncGMailClient,
llm: LLMController = None, llm: CoreLLMController = None,
force_sync: bool = False, force_sync: bool = False,
start_date: datetime.datetime = None, start_date: datetime.datetime = None,
end_date: datetime.datetime = None, end_date: datetime.datetime = None,
@@ -524,7 +524,7 @@ class MailController:
mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
user_info: CoreUserInfoModel, user_info: CoreUserInfoModel,
token_key: ObjectId | str, token_key: ObjectId | str,
llm: LLMController = None, llm: CoreLLMController = None,
force_sync: bool = False, force_sync: bool = False,
start_date: datetime.datetime = None, start_date: datetime.datetime = None,
end_date: datetime.datetime = None, end_date: datetime.datetime = None,
+127 -79
View File
@@ -47,9 +47,10 @@ from utils_v2.database.async_mongo_v2 import AsyncMongo, AsyncMongoStorage
# Data models: # Data models:
from models.core.user import CoreUserInfoModel from models.core.user import CoreUserInfoModel
from models.core.auth_token import CoreAuthTokenModel from models.core.auth_token import CoreAuthTokenModel
from models.core.payment import CorePaymentModel, PaymentEvent from models.core.payment import CorePaymentModel, PaymentEvent, CustomerDetails
from models.api.finstitutions.payments.request import ( from models.api.finstitutions.payments.request import (
PaymentRequestOneResult PGPaymentRequestData,
PaymentRequestOneResult,
) )
# Payment Clients: # Payment Clients:
@@ -176,83 +177,118 @@ class PaymentController:
# ┛┗┗ ┗┫┗┻┗ ┛┗ ┣┛┗┻┗┫┛┗┗┗ ┛┗┗┛ # ┛┗┗ ┗┫┗┻┗ ┛┗ ┣┛┗┻┗┫┛┗┗┗ ┛┗┗┛
# ┗ ┛ # ┗ ┛
# @staticmethod @staticmethod
# async def __request_from_safaricom_m_pesa_express( async def __request_from_safaricom_m_pesa_express(
# http_client: httpx.AsyncClient, mongo_conn: AsyncMongo,
# auth_token: CoreAuthTokenModel, http_client: httpx.AsyncClient,
# payment: Union[SafaricomMPesaExpressRequest], auth_token: CoreAuthTokenModel,
# callback_url: str payment_request: PGPaymentRequestData,
# ) -> PaymentRequestOneResult: payment_id: ObjectId,
# callback_url: str
# # Start by assuming failure: ) -> PaymentRequestOneResult:
# request_result = PaymentRequestOneResult()
# # Start by assuming failure:
# # Create the client: result = PaymentRequestOneResult()
# client = SafaricomMPesaExpress(
# auth = MPesaExpressAuthorization( # Create the client:
# consumerKey = auth_token.auth["consumerKey"], client = SafaricomMPesaExpress(
# consumerSecret = auth_token.auth["consumerSecret"], auth = MPesaExpressAuthorization(
# businessShortCode = auth_token.auth["businessShortCode"], consumerKey = auth_token.auth["consumerKey"],
# appPasskey = auth_token.auth["appPasskey"] consumerSecret = auth_token.auth["consumerSecret"],
# ), businessShortCode = auth_token.auth["businessShortCode"],
# http_client = http_client appPasskey = auth_token.auth["appPasskey"]
# ) ),
# http_client = http_client
# # Make the payment request: )
# client_response = await client.request_payment(
# amount = payment.amount, # Make the payment request:
# party_a = payment.partyA, client_response = await client.request_payment(
# type = payment.transactionType, amount = payment_request.amount,
# reference = payment.accountReference, party_a = payment_request.customerNo,
# description = payment.transactionDescription, type = "CustomerPayBillOnline",
# callback_url = callback_url, reference = payment_request.reference,
# payer_no = payment.phoneNo, description = payment_request.description,
# party_b = payment.partyB callback_url = callback_url,
# ) payer_no = payment_request.payerNo,
# party_b = auth_token.auth["businessShortCode"]
# # Construct the payment details: )
# payment_details = CorePaymentModel(
# user = None, # Add this event to the payment's document:
# lastEventTs = date_time.get_current_utc_date_time(as_string = False), event_note_success = await current_app.core_payment_controller.add_event(
# lastPaymentStatus = "initiated" if client_response.success else "initFailed", mongo_conn = mongo_conn,
# tokenId = auth_token.authTokenId, payment_id = payment_id,
# amount = payment.amount, event = PaymentEvent(
# curencyCode = "KES", paymentStatus = "initiated" if client_response.success else "initFailed",
# # metadata = payment. initByPG = False,
# ) httpCode = client_response.httpCode,
# headers = await client_response.get_headers(),
# # Done here: payload = await client_response.get_json()
# request_result.success = client_response.success )
# request_result.message = client_response.message )
# request_result.message = client_response.message
# return request_result # Done here:
# result.success = client_response.success and event_note_success
# async def request_payment( result.message = "; ".join([
# self, "payment request successfully" if client_response.success else "payment request failed",
# mongo_conn: AsyncMongo, "event noted successfully" if event_note_success else "payment event noting failed",
# http_client: httpx.AsyncClient, ])
# auth_token: CoreAuthTokenModel, return result
# payment: Union[SafaricomMPesaExpressRequest],
# callback_url: str async def request_payment(
# ) -> PaymentRequestOneResult: self,
# mongo_conn: AsyncMongo,
# # Start by assuming failure: http_client: httpx.AsyncClient,
# request_result = PaymentRequestOneResult() auth_token: CoreAuthTokenModel,
# user_info: CoreUserInfoModel,
# # Now we route the message to the appropriate client: payment_request: PGPaymentRequestData,
# match auth_token.client: ) -> PaymentRequestOneResult:
# case "safaricomMPesaExpress":
# request_result = await self.__request_from_safaricom_m_pesa_express( # Start by assuming failure:
# http_client = http_client, request_result = PaymentRequestOneResult()
# auth_token = auth_token,
# payment = payment, # Create a basic database entry to note down the intended payment:
# callback_url = callback_url payment_id = await current_app.core_payment_controller.init(
# ) mongo_conn = mongo_conn,
# case _: payment = CorePaymentModel(
# request_result.message = f"invalid client {auth_token.client}" user = user_info,
# customer = CustomerDetails(
# # Done here: name = payment_request.customerName,
# return request_result contactNo = payment_request.customerNo,
payerNo = payment_request.payerNo,
email = payment_request.email
),
lastPaymentStatus = "queued",
tokenId = auth_token.authTokenId,
amount = payment_request.amount,
currencyCode = payment_request.currencyCode,
metadata = payment_request.metadata,
tags = ["payment", "safaricom", "mPesaExpress", "kenya"],
client = auth_token.client,
clientPaymentReferenceId = None,
events = []
)
)
if payment_id is None:
request_result.message = "failed to queue the payment"
return request_result
else: request_result.message = "payment has been queued for processing"
# Now we route the request to the appropriate client:
match auth_token.client:
case "safaricomMPesaExpress":
request_result = await self.__request_from_safaricom_m_pesa_express(
http_client = http_client,
mongo_conn = mongo_conn,
auth_token = auth_token,
payment_request = payment_request,
callback_url = f"https://api.thecaoffice.com/finstitutions/payments/callback/{payment_id}",
payment_id = payment_id
)
case _:
request_result = None
# Done here:
return request_result
# ┓ • ┓ ┏┓ ┏┓ # ┓ • ┓ ┏┓ ┏┓
# ┃ ┓┏╋ ┏┓┏┓┏┫ ┃┓┏┓╋ ┃┃┏┓┓┏┏┳┓┏┓┏┓╋┏ # ┃ ┓┏╋ ┏┓┏┓┏┫ ┃┓┏┓╋ ┃┃┏┓┓┏┏┳┓┏┓┏┓╋┏
@@ -299,6 +335,18 @@ class PaymentController:
payment_id = payment_id payment_id = payment_id
) )
@staticmethod
async def get_payment_internal(
mongo_conn: AsyncMongo,
payment_id: ObjectId | str
) -> CorePaymentModel | None:
# Simply call the core model:
return await current_app.core_payment_controller.get_payment_internal(
mongo_conn = mongo_conn,
payment_id = payment_id
)
# ┳┳ ┓ # ┳┳ ┓
# ┃┃┏┓┏┫┏┓╋┏┓ # ┃┃┏┓┏┫┏┓╋┏┓
# ┗┛┣┛┗┻┗┻┗┗ # ┗┛┣┛┗┻┗┻┗┗
+1 -1
View File
@@ -85,7 +85,7 @@ from langchain_openai import ChatOpenAI
# ***************************************************************************************************************** # *****************************************************************************************************************
class LLMController(BaseModel): class CoreLLMController(BaseModel):
AI_USAGE_COLLECTION = "_aiUsage" AI_USAGE_COLLECTION = "_aiUsage"
+50 -3
View File
@@ -134,10 +134,14 @@ class CorePaymentController(BaseModel):
:return: The object id of the inserted document. :return: The object id of the inserted document.
""" """
# Receive te JSON:
payment_json = payment.model_dump()
payment_json.pop("_id")
# Simply insert the document: # Simply insert the document:
return await mongo_conn.insert_one( return await mongo_conn.insert_one(
collection = self.PAYMENTS_COLLECTION, collection = self.PAYMENTS_COLLECTION,
document = payment, document = payment_json,
raise_exception = True raise_exception = True
) )
@@ -253,6 +257,34 @@ class CorePaymentController(BaseModel):
# we return it as our data model: # we return it as our data model:
return CorePaymentModel(**record) return CorePaymentModel(**record)
async def get_payment_internal(
self,
mongo_conn: AsyncMongo,
payment_id: ObjectId | str,
) -> CorePaymentModel | None:
"""
NOTE: DO NOT USE THIS IN USER-FACING APIS. USE THIS INTERNALLY TO FETCH RECORDS.
Gets one payment detail if you know its payment id.
:param mongo_conn: The instance of the database connector to use for the operation.
:param payment_id: The id of the payment detail that needs to be read.
:return: The contents of that one payment detail in a structured format.
"""
# We fetch the whole payload of that one message:
record = await mongo_conn.find_one(
collection = self.PAYMENTS_COLLECTION,
filter = {"_id": ObjectId(payment_id)},
raise_exception = True
)
# If no such message was found:
if record is None: return None
# If a record was found,
# we return it as our data model:
return CorePaymentModel(**record)
# ┏┓┳┓┳┳┳┓ ┳┳ ┓ # ┏┓┳┓┳┳┳┓ ┳┳ ┓
# ┃ ┣┫┃┃┃┃ ━━ ┃┃┏┓┏┫┏┓╋┏┓ # ┃ ┣┫┃┃┃┃ ━━ ┃┃┏┓┏┫┏┓╋┏┓
# ┗┛┛┗┗┛┻┛ ┗┛┣┛┗┻┗┻┗┗ # ┗┛┛┗┗┛┻┛ ┗┛┣┛┗┻┗┻┗┗
@@ -265,7 +297,8 @@ class CorePaymentController(BaseModel):
self, self,
mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
payment_id: ObjectId | str, payment_id: ObjectId | str,
event: PaymentEvent event: PaymentEvent,
client_reference_id: str = None
) -> bool: ) -> bool:
""" """
@@ -273,14 +306,28 @@ class CorePaymentController(BaseModel):
:param mongo_conn: The instance of the database connector to use for the operation. :param mongo_conn: The instance of the database connector to use for the operation.
:param payment_id: The id of the payment detail that needs to be read. :param payment_id: The id of the payment detail that needs to be read.
:param event: The event that occurred. This will typically be generated by the third-party client. :param event: The event that occurred. This will typically be generated by the third-party client.
:param client_reference_id: The way the client identifies this payment. You need to pass this only on the first
event. Typically, when you initiate the payment request.
:return: True if successfully noted, else False. :return: True if successfully noted, else False.
""" """
# Prepare the update document:
update_json = {
"$push": {
"events": event.model_dump()
},
"$set": {
"lastEventTs": date_time.get_current_ist_date_time(as_string = False),
"lastPaymentStatus": event.paymentStatus
}
}
if client_reference_id: update_json["$set"]["clientPaymentReferenceId"] = client_reference_id
# Try to update the existing record: # Try to update the existing record:
return await mongo_conn.update_one( return await mongo_conn.update_one(
collection = self.PAYMENTS_COLLECTION, collection = self.PAYMENTS_COLLECTION,
filter = {"_id": ObjectId(payment_id)}, filter = {"_id": ObjectId(payment_id)},
update = {"$push": {"events": event}}, update = update_json,
upsert = False, upsert = False,
raise_exception = True raise_exception = True
) )
+10 -16
View File
@@ -110,17 +110,6 @@ class PGPaymentRequestHeaders(BaseModel):
class PGPaymentRequestData(BaseModel): class PGPaymentRequestData(BaseModel):
# {
# "customerMobileNumber": "",
# "tokenId": "",
# "description": "",
# "payerNumber": "",
# "amount": "",
# "currency": "",
# "emailAddress": "",
# "reference": "",
# }
tokenKey: ObjectId = Field( tokenKey: ObjectId = Field(
description = "the auth token to use to send this message", description = "the auth token to use to send this message",
frozen = True, frozen = True,
@@ -164,6 +153,16 @@ class PGPaymentRequestData(BaseModel):
examples = ["INR", "USD", "KES"] examples = ["INR", "USD", "KES"]
) )
reference: str = Field(
description = "some reference against which the payment was made",
frozen = True
)
description: str = Field(
description = "a short, human-readable description of the payment",
frozen = True
)
metadata: dict = Field( metadata: dict = Field(
description = "any extra information about this payment", description = "any extra information about this payment",
frozen = True frozen = True
@@ -210,11 +209,6 @@ class PaymentRequestOneResult(BaseModel):
default = None default = None
) )
paymentDetails: CorePaymentModel = Field(
description = "the actual data of the payment",
default = None
)
# ┏┓ ┏• # ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓ # ┃ ┏┓┏┓╋┓┏┓
# ┗┛┗┛┛┗┛┗┗┫ # ┗┛┗┛┛┗┛┗┗┫
+14 -7
View File
@@ -138,17 +138,20 @@ class PaymentEvent(BaseModel):
eventTs: AwareDatetime = Field( eventTs: AwareDatetime = Field(
description = "to know the date and time (utc) of this update", description = "to know the date and time (utc) of this update",
frozen = True frozen = True,
default_factory = lambda: date_time.get_current_utc_date_time(as_string = False)
) )
paymentStatus: Literal[ paymentStatus: Literal[
"initFailed", # ... When we tried to initiate the request, but the payment gateway (PG) rejected it. "queued", # ....... When the UI sends a payment request, but the payment gateway (PG) hasn't received it yet.
"initFailed", # ... When we tried to initiate the request, but the PG rejected it.
"initiated", # .... When we made a successful payment request, or the customer initiated one from the PG. "initiated", # .... When we made a successful payment request, or the customer initiated one from the PG.
"failed", # ....... When the customer tried paying, but it failed (e.g.: because of an incorrect pin). "failed", # ....... When the customer tried paying, but it failed (e.g.: because of an incorrect pin).
"rejected", # ..... When the customer explicitly rejected the payment. "rejected", # ..... When the customer explicitly rejected the payment.
"authorized", # ... When the customer made the payment (but it hasn't been settled in your account yet). "authorized", # ... When the customer made the payment (but it hasn't been settled in your account yet).
"settled", # ...... When the PG sends the money to your account. "settled", # ...... When the PG sends the money to your account.
"refunded", # ..... When the money was refunded to the client. "refunded", # ..... When the money was refunded to the client.
"unknown" # ....... When integrating a new gateway and some specific status is not known.
] = Field( ] = Field(
description = "the status of the payment request to see what stage of the process we are in", description = "the status of the payment request to see what stage of the process we are in",
frozen = False frozen = False
@@ -220,9 +223,10 @@ class CorePaymentModel(BaseModel):
default_factory = lambda: date_time.get_current_utc_date_time(as_string = False) default_factory = lambda: date_time.get_current_utc_date_time(as_string = False)
) )
lastEventTs: AwareDatetime = Field( lastEventTs: AwareDatetime | None = Field(
description = "the time (utc) at which the last event occurred", description = "the time (utc) at which the last event occurred",
frozen = True frozen = True,
default = None
) )
lastPaymentStatus: str = Field( lastPaymentStatus: str = Field(
@@ -263,7 +267,7 @@ class CorePaymentModel(BaseModel):
frozen = True frozen = True
) )
clientPaymentReferenceId: int | str = Field( clientPaymentReferenceId: int | str | None = Field(
description = "the reference id given by the third-party client", description = "the reference id given by the third-party client",
frozen = False, frozen = False,
default = None default = None
@@ -271,7 +275,8 @@ class CorePaymentModel(BaseModel):
events: List[PaymentEvent] = Field( events: List[PaymentEvent] = Field(
description = "an array of all the events that happened in the process of this payment", description = "an array of all the events that happened in the process of this payment",
frozen = False frozen = False,
default = []
) )
# ┏┓ ┏• # ┏┓ ┏•
@@ -296,6 +301,7 @@ class CorePaymentModel(BaseModel):
payment_json = { payment_json = {
"paymentId": str(self.messageId), "paymentId": str(self.messageId),
"user": self.user, "user": self.user,
"customer": self.customer,
"ts": self.ts.isoformat(), "ts": self.ts.isoformat(),
"lastEventTs": self.lastEventTs, "lastEventTs": self.lastEventTs,
"lastPaymentStatus": self.lastPaymentStatus, "lastPaymentStatus": self.lastPaymentStatus,
@@ -320,6 +326,7 @@ class CorePaymentModel(BaseModel):
return { return {
"paymentId": str(self.messageId), "paymentId": str(self.messageId),
"user": self.user, "user": self.user,
"customer": self.customer,
"ts": self.ts.isoformat(), "ts": self.ts.isoformat(),
"lastEventTs": self.lastEventTs, "lastEventTs": self.lastEventTs,
"lastPaymentStatus": self.lastPaymentStatus, "lastPaymentStatus": self.lastPaymentStatus,
@@ -335,7 +342,7 @@ class CorePaymentModel(BaseModel):
# ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
# ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗
@field_validator("lastEventTs", mode = "before") @field_validator("ts", "lastEventTs", mode = "before")
def parse_date_time(cls, value): def parse_date_time(cls, value):
return date_time.parse_date_time(input_value = value, timezone = date_time.TIMEZONE_UTC) return date_time.parse_date_time(input_value = value, timezone = date_time.TIMEZONE_UTC)
+3 -2
View File
@@ -19,11 +19,12 @@ if [[ $SELECTION -gt 0 && $SELECTION -le ${#SERVERS[@]} ]]; then
# Note down the selection in a variable: # Note down the selection in a variable:
SELECTED_SERVER=${SERVERS[$((SELECTION - 1))]} SELECTED_SERVER=${SERVERS[$((SELECTION - 1))]}
# Ask the user which directory he wants to upload and his username on the server: # Ask the username and target port no. on the server::
read -rp "Your username on the server ..... : " USER read -rp "Your username on the server ..... : " USER
read -rp "The target port no. ............. : " PORT
# Run the command: # Run the command:
ssh -p 19991 "$USER@$SELECTED_SERVER" ssh -p "$PORT" "$USER@$SELECTED_SERVER"
# Exit with success (assuming that the actual data sending went well): # Exit with success (assuming that the actual data sending went well):
echo "session ended" echo "session ended"
@@ -355,7 +355,7 @@ class SafaricomMPesaExpress:
api_json = await api_response.get_json() api_json = await api_response.get_json()
api_response.message = api_json.get("ResponseDescription", "N/A") api_response.message = api_json.get("ResponseDescription", "N/A")
api_response.data = api_json api_response.data = api_json
api_response.success = True api_response.success = True if str(api_json.get("ResponseCode")) == "0" else False
# Done here: # Done here:
return api_response return api_response
@@ -109,6 +109,10 @@ class MPesaExpressApiResponse(BaseModel):
message += f"*EXCEPTION:*\n`{self.exception.__class__.__name__}: {str(self.exception)}`\n\n" message += f"*EXCEPTION:*\n`{self.exception.__class__.__name__}: {str(self.exception)}`\n\n"
return message return message
async def get_headers(self):
try: return self.response.headers
except: return {}
async def get_json(self): async def get_json(self):
try: return self.response.json() try: return self.response.json()
except: return {} except: return {}