Files
api_utils_converse_v2/controllers/api/payment.py
T

454 lines
16 KiB
Python

"""
AUTHOR:
Khushal P Soonderji
DATE:
Friday, 13th Dec., 2024
OBJECTIVE:
To handle all SMS related behaviour from one place.
REFERENCES:
N/A
DOWNLOADS:
N/A
"""
# *****************************************************************************************************************
# ***** ****
# *** IMPORT ***
# ***** ****
# *****************************************************************************************************************
# To make sibling directories accessible for imports:
import sys
sys.path.append(".")
sys.path.append("..")
# For Quart:
from quart import current_app
# My async utils:
from utils_v2.string import json
from utils_v2.string import regex
from utils_v2.date_time import date_time
from utils_v2.database.async_mysql_v2 import AsyncMySQL
from utils_v2.database.async_mongo_v2 import AsyncMongo, AsyncMongoStorage
# Data models:
from models.core.user import CoreUserInfoModel
from models.core.auth_token import CoreAuthTokenModel
from models.core.payment import CorePaymentModel, PaymentEvent, CustomerDetails
from models.api.finstitutions.payments.request import (
PGPaymentRequestData,
PaymentRequestOneResult,
)
# Payment Clients:
from utils_v2.payments.safaricom.models.auth import MPesaExpressAuthorization
from utils_v2.payments.safaricom.controllers.m_pesa_express import SafaricomMPesaExpress
# To work with MongoDB:
from bson import ObjectId
from pymongo import InsertOne
# To work with datatypes:
from typing import Literal, List, Dict, Any, Union
# To make API calls:
import httpx
# For asynchronous activities:
import asyncio
# *****************************************************************************************************************
# ***** ****
# *** MACROS / ONE-TIME INIT ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** VARIABLES ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** FUNCTIONS ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** CLASSES ***
# ***** ****
# *****************************************************************************************************************
class PaymentController:
# ┏┓┓ ┓┏
# ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏
# ┗┛┗┗┻┛┛ ┗┛┗┻┛ ┛
pass
# ┓┏ ┓
# ┣┫┏┓┃┏┓┏┓┏┓┏
# ┛┗┗ ┗┣┛┗ ┛ ┛
# ┛
pass
# ┏┓ ┓
# ┣┫┓┏╋┣┓
# ┛┗┗┻┗┛┗
@staticmethod
async def set_token_direct(
db_conn: AsyncMySQL,
mongo_conn: AsyncMongo,
auth_token: CoreAuthTokenModel,
session_token: str = None
) -> bool:
# Start by assuming failure:
success = False
# Get a token id:
token_key = await current_app.core_auth_token_controller.get_token_key(
db_conn = db_conn,
mongo_conn = mongo_conn,
auth_token = auth_token,
token_notes = {},
session_token = session_token
)
# Immediately save the details against that token id:
success = await current_app.core_auth_token_controller.set_token(
db_conn = db_conn,
mongo_conn = mongo_conn,
token_key = token_key,
auth_token = auth_token,
token_notes = {},
session_token = session_token
)
# Done here:
return success
@staticmethod
async def get_token(
mongo_conn: AsyncMongo,
token_key: ObjectId | str = None,
) -> CoreAuthTokenModel | None:
# Simply call the core model:
return await current_app.core_auth_token_controller.get_token(
mongo_conn = mongo_conn,
token_key = token_key
)
@staticmethod
async def get_tokens(
mongo_conn: AsyncMongo,
token_keys: ObjectId | str = None,
) -> List[CoreAuthTokenModel] | None:
# Simply call the core model:
return await current_app.core_auth_token_controller.get_tokens(
mongo_conn = mongo_conn,
token_keys = token_keys
)
# ┳┓ ┏┓
# ┣┫┏┓┏┓┓┏┏┓┏╋ ┃┃┏┓┓┏┏┳┓┏┓┏┓╋┏
# ┛┗┗ ┗┫┗┻┗ ┛┗ ┣┛┗┻┗┫┛┗┗┗ ┛┗┗┛
# ┗ ┛
async def __request_from_safaricom_m_pesa_express(
self,
mongo_conn: AsyncMongo,
http_client: httpx.AsyncClient,
auth_token: CoreAuthTokenModel,
payment_request: PGPaymentRequestData,
payment_id: ObjectId,
callback_url: str
) -> PaymentRequestOneResult:
# Start by assuming failure:
result = PaymentRequestOneResult()
# Create the client:
client = SafaricomMPesaExpress(
auth = MPesaExpressAuthorization(
consumerKey = auth_token.auth["consumerKey"],
consumerSecret = auth_token.auth["consumerSecret"],
businessShortCode = auth_token.auth["businessShortCode"],
appPasskey = auth_token.auth["appPasskey"]
),
http_client = http_client
)
# Make the payment request:
client_response = await client.request_payment(
amount = payment_request.amount,
party_a = regex.replace(
text = payment_request.customerNo,
pattern = r"[^0-9]",
substitute_text = ""
) if isinstance(payment_request.customerNo, str) else payment_request.customerNo,
type = "CustomerPayBillOnline",
reference = payment_request.reference,
description = payment_request.description,
callback_url = callback_url,
payer_no = regex.replace(
text = payment_request.payerNo,
pattern = r"[^0-9]",
substitute_text = ""
) if isinstance(payment_request.payerNo, str) else payment_request.payerNo,
party_b = auth_token.auth["businessShortCode"]
)
# Add this event to the payment's document:
event_note_success = await self.add_event_by_payment_id(
mongo_conn = mongo_conn,
payment_id = payment_id,
event = PaymentEvent(
paymentStatus = "initiated" if client_response.success else "initFailed",
message = f"PG: {client_response.message}",
initByPG = False,
httpCode = client_response.httpCode,
headers = await client_response.get_headers(),
payload = await client_response.get_json()
),
client_reference_id = client_response.referenceId
)
# Done here:
result.success = client_response.success and event_note_success
result.message = " ".join([
"Payment requested successfully." if client_response.success
else f"Payment request FAILED (PG: '{client_response.message}').",
" " if event_note_success
else "Event noting FAILED.",
]).strip()
return result
async def request_payment(
self,
mongo_conn: AsyncMongo,
http_client: httpx.AsyncClient,
auth_token: CoreAuthTokenModel,
user_info: CoreUserInfoModel,
payment_request: PGPaymentRequestData,
) -> PaymentRequestOneResult:
# Start by assuming failure:
request_result = PaymentRequestOneResult()
# Create a basic database entry to note down the intended payment:
payment_id = await current_app.core_payment_controller.init(
mongo_conn = mongo_conn,
payment = CorePaymentModel(
user = user_info,
customer = CustomerDetails(
name = payment_request.customerName,
contactNo = payment_request.customerNo,
payerNo = payment_request.payerNo,
email = payment_request.email
),
lastEventMessage = "Payment Request Queued",
lastPaymentStatus = "queued",
tokenId = auth_token.authTokenId,
amount = payment_request.amount,
currencyCode = payment_request.currencyCode,
metadata = payment_request.metadata.model_dump(),
tags = ["payment", "safaricom", "mPesaExpress", "kenya"],
serviceType = "paymentGateway",
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/safaricom/mpesaexpress",
payment_id = payment_id
)
case _:
request_result = None
# Done here:
return request_result
# ┓ • ┓ ┏┓ ┏┓
# ┃ ┓┏╋ ┏┓┏┓┏┫ ┃┓┏┓╋ ┃┃┏┓┓┏┏┳┓┏┓┏┓╋┏
# ┗┛┗┛┗ ┗┻┛┗┗┻ ┗┛┗ ┗ ┣┛┗┻┗┫┛┗┗┗ ┛┗┗┛
# ┛
# These are simply for retrieving payment records.
# You need to already have them saved to the database.
@staticmethod
async def list_payments(
mongo_conn: AsyncMongo,
token_ids: List[ObjectId | str],
limit: int = 100,
skip: int = 0,
additional_filter: dict = None
) -> List[CorePaymentModel] | None:
# Regardless of what additional filter is provided from outside,
# we add a payment-selecting filter here:
if additional_filter is None: additional_filter = {}
# additional_filter["serviceType"] = "paymentGateway"
# Simply call the core model:
return await current_app.core_payment_controller.get_payment_previews(
mongo_conn = mongo_conn,
token_ids = token_ids,
limit = limit,
skip = skip,
additional_filter = additional_filter
)
@staticmethod
async def get_payment(
mongo_conn: AsyncMongo,
token_id: ObjectId | str,
payment_id: ObjectId | str
) -> CorePaymentModel | None:
# Simply call the core model:
return await current_app.core_payment_controller.get_payment(
mongo_conn = mongo_conn,
token_id = token_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
)
# ┳┳ ┓
# ┃┃┏┓┏┫┏┓╋┏┓
# ┗┛┣┛┗┻┗┻┗┗
# ┛
@staticmethod
async def add_event_by_payment_id(
mongo_conn: AsyncMongo,
payment_id: ObjectId | str,
event: PaymentEvent,
client_reference_id: str = None
) -> bool:
# Simply call the core model:
return await current_app.core_payment_controller.add_event_by_payment_id(
mongo_conn = mongo_conn,
payment_id = payment_id,
event = event,
client_reference_id = client_reference_id
)
@staticmethod
async def add_event_by_client_reference_id(
mongo_conn: AsyncMongo,
client_reference_id: str,
event: PaymentEvent,
) -> bool:
# Simply call the core model:
return await current_app.core_payment_controller.add_event_by_client_reference_id(
mongo_conn = mongo_conn,
event = event,
client_reference_id = client_reference_id
)
@staticmethod
async def update_tags(
mongo_conn: AsyncMongo,
token_id: ObjectId | str,
payment_id: ObjectId | str,
unset_tags: List[str] = None,
set_tags: List[str] = None
) -> bool:
# Simply call the core model:
return await current_app.core_payment_controller.update_tags(
mongo_conn = mongo_conn,
token_id = token_id,
payment_id = payment_id,
unset_tags = unset_tags,
set_tags = set_tags
)
# *****************************************************************************************************************
# ***** ****
# *** MAIN PROGRAM ***
# ***** ****
# *****************************************************************************************************************
if __name__ == "__main__":
pass
# from utils_v2.string import json
#
# file_options = [
# r"/home/developer/Downloads/recursive parts parse - 20241210.json",
# r"/home/developer/Downloads/recursive parts parse (no attachment) - 20241210.json",
# ]
#
# raw_mail_json = json.from_file(file_options[1])
# print("FROM FILE:", json.to_string(raw_mail_json["payload"]))
# print("\n\n---------\n\n")
# mail_controller = MailController()
# print(json.to_string(mail_controller.drop_attachments(raw_mail_json["payload"])))