(20241216) Payment auth bug fix.

This commit is contained in:
2024-12-16 16:00:52 +05:30
parent 064495163f
commit 0b9976fadb
19 changed files with 589 additions and 473 deletions
+4 -16
View File
@@ -170,6 +170,9 @@ async def authorize_payment_gateway(
if inbound_data.client == "safaricomMPesaExpress":
success = await current_app.payment_controller.set_token_direct(
db_conn = current_app.sql_writer,
mongo_conn = current_app.data_mongo,
auth_token = CoreAuthTokenModel(
serviceType = "paymentGateway",
client = inbound_data.client,
@@ -181,22 +184,7 @@ async def authorize_payment_gateway(
},
status = "active",
syncFreq = 60
)
token_id = await current_app.core_auth_token_controller.get_token_id(
db_conn = current_app.sql_writer,
mongo_conn = current_app.data_mongo,
auth_token = auth_token,
token_notes = {},
session_token = inbound_headers["X-Session-Token"]
)
success = await current_app.core_auth_token_controller.set_token(
db_conn = current_app.sql_writer,
mongo_conn = current_app.data_mongo,
token_id = token_id,
auth_token = auth_token,
token_notes = {},
),
session_token = inbound_headers["X-Session-Token"]
)
+1 -1
View File
@@ -63,7 +63,7 @@ from utils_v2.api.async_quart import (
# GMail-related utils:
from utils_v2.goog.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT
from utils_v2.goog.models.data.auth_tokens import GoogleAuthTokens
from utils_v2.goog.models.auth_tokens import GoogleAuthTokens
# Common:
from shared import constants
+1 -1
View File
@@ -64,7 +64,7 @@ from utils_v2.api.async_quart import (
# GMail-related utils:
from utils_v2.goog.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT
from utils_v2.goog.models.data.auth_tokens import GoogleAuthTokens
from utils_v2.goog.models.auth_tokens import GoogleAuthTokens
# Common:
from shared import constants
+1 -1
View File
@@ -64,7 +64,7 @@ from utils_v2.api.async_quart import (
# GMail-related utils:
from utils_v2.goog.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT
from utils_v2.goog.models.data.auth_tokens import GoogleAuthTokens
from utils_v2.goog.models.auth_tokens import GoogleAuthTokens
# Common:
from shared import constants
+1 -1
View File
@@ -63,7 +63,7 @@ from utils_v2.api.async_quart import (
# GMail-related utils:
from utils_v2.goog.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT
from utils_v2.goog.models.data.auth_tokens import GoogleAuthTokens
from utils_v2.goog.models.auth_tokens import GoogleAuthTokens
# Common:
from shared import constants
+2 -2
View File
@@ -166,7 +166,7 @@ async def authorize_sms_client(
if inbound_data.smsClient == "nimbusSmsIndia":
success = await current_app.sms_controller.set_token(
success = await current_app.sms_controller.set_token_direct(
db_conn = current_app.sql_writer,
mongo_conn = current_app.data_mongo,
auth_token = CoreAuthTokenModel(
@@ -193,7 +193,7 @@ async def authorize_sms_client(
elif inbound_data.smsClient == "savvyBulkSmsKenya":
success = await current_app.sms_controller.set_token(
success = await current_app.sms_controller.set_token_direct(
db_conn = current_app.sql_writer,
mongo_conn = current_app.data_mongo,
auth_token = CoreAuthTokenModel(
+6 -4
View File
@@ -63,13 +63,14 @@ from utils_v2.api.async_quart import (
from utils_v2.goog.gmail.gmail_client import AsyncGMailClient
# Core Controller Models:
from controllers.core.message import MessageController
from controllers.core.auth_token import AuthTokenController
from controllers.core.message import CoreMessageController
from controllers.core.auth_token import CoreAuthTokenController
from controllers.core.ai.llm import LLMController
# API Controller Models:
from controllers.api.mail import MailController
from controllers.api.sms import SMSController
from controllers.api.payment import PaymentController
# # Old Behaviour Models:
# from controllers.mail.oauth_v3 import MailOAuthModel
@@ -328,7 +329,7 @@ async def app_startup(**kwargs):
# ┃ ┏┓┏┓┏┓ ┃┃┃┏┓┏┫┏┓┃┏
# ┗┛┗┛┛ ┗ ┛ ┗┗┛┗┻┗ ┗┛
current_app.core_auth_token_controller = AuthTokenController(
current_app.core_auth_token_controller = CoreAuthTokenController(
cache = current_app.module_cache,
alert_url = current_app.script_data["alerts"]["url"],
http_client = current_app.http_client,
@@ -336,7 +337,7 @@ async def app_startup(**kwargs):
debug_prefix = "Message (CM) | ",
debug_only_errors = True
)
current_app.core_message_controller = MessageController(
current_app.core_message_controller = CoreMessageController(
cache = current_app.module_cache,
alert_url = current_app.script_data["alerts"]["url"],
http_client = current_app.http_client,
@@ -351,6 +352,7 @@ async def app_startup(**kwargs):
current_app.mail_controller = MailController()
current_app.sms_controller = SMSController()
current_app.payment_controller = PaymentController()
# ┏┓ ┓ ┏┓┓•
# ┃ ┏┓┏┓┏┓┏┓┏╋┏┓┏┓┏ ┏┓┏┓┏┫ ┃ ┃┓┏┓┏┓╋┏
+1 -1
View File
@@ -54,7 +54,7 @@ from models.api.mail.sync import MailSyncOneResult, MailSyncManyResults
# Mail Clients:
from utils_v2.goog.gmail.gmail_client import AsyncGMailClient
from utils_v2.goog.models.data.auth_tokens import GoogleAuthTokens
from utils_v2.goog.models.auth_tokens import GoogleAuthTokens
# To work with MongoDB:
from bson import ObjectId
+149 -224
View File
@@ -21,7 +21,8 @@
N/A
"""
import asyncio
# *****************************************************************************************************************
# ***** ****
# *** IMPORT ***
@@ -43,35 +44,31 @@ 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
# Base model:
from controllers.base import BaseModel
# Data models:
from models.core.user import CoreUserInfoModel
from models.core.auth_token import CoreAuthTokenModel
from models.core.message import CoreMessageModel
from models.api.sms.send import (
SMSSendRequestData,
NimbusSMSIndiaMessage,
SavvyBulkSMSKenyaMessage,
SMSSendManyResults
from models.core.payment import CorePaymentModel, PaymentEvent
from models.api.finstitutions.payments.request import (
PaymentRequestOneResult
)
# SMS Clients:
from utils_v2.sms.models.behaviour.nimbus.async_nimbus import AsyncNimbusSMS
from utils_v2.sms.models.behaviour.savvy_bulk_sms.async_savvy_bulk_sms import AsyncSavvyBulkSMS
from utils_v2.sms.models.data.sms_message import SentSMSMessageModel
# 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
from typing import Literal, List, Dict, Any, Union
# To make API calls:
import httpx
# For asynchronous activities:
import asyncio
# *****************************************************************************************************************
# ***** ****
@@ -110,7 +107,7 @@ import httpx
# *****************************************************************************************************************
class SMSController:
class PaymentController:
# ┏┓┓ ┓┏
# ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏
@@ -174,239 +171,167 @@ class SMSController:
token_key = token_key
)
# ┏┓
# ┗┓┏┓┏┓┏
# ┗┛┗ ┛┗┗┻
# ┳┓ ┏
# ┣┫┏┓┏┓┓┏┏┓┏╋ ┃┃┏┓┓┏┏┳┓┏┓┏┓╋┏
# ┛┗┗ ┗┫┗┻┗ ┛┗ ┣┛┗┻┗┫┛┗┗┗ ┛┗┗┛
# ┗ ┛
@staticmethod
async def __send_from_nimbus_sms_india(
http_client: httpx.AsyncClient,
auth_token: CoreAuthTokenModel,
messages: List[NimbusSMSIndiaMessage],
) -> SMSSendManyResults:
# Start with a blank variable:
send_results = SMSSendManyResults()
# Initialize the third-party client:
client = AsyncNimbusSMS(
entity_id = auth_token.auth["entityId"],
sender_id = auth_token.auth["senderId"],
user_id = auth_token.auth["userId"],
api_key = auth_token.auth["apiKey"],
http_client = http_client
)
# Iterate over all the messages you need to send:
for message in messages:
# Send the SMS and return the response:
client_response = await client.send_sms(
recipient_number = message.recipientNo,
message = message.text,
template_id = message.templateId
)
# Note down the results:
send_results.totalCount += 1
if client_response.success: send_results.successCount += 1
else: send_results.failureCount += 1
send_results.smsMessages.append(CoreMessageModel(
ts = client_response.ts,
syncTs = date_time.get_current_utc_date_time(as_string = False),
tokenId = auth_token.authTokenId,
serviceType = auth_token.serviceType,
client = auth_token.client,
clientMessageId = client_response.messageId,
clientThreadId = message.recipientNo,
isSent = True,
isBroadcast = False,
sentSuccessfully = client_response.success,
sender = None,
recipient = message.recipientNo,
chat = None,
message = client_response.model_dump(),
snippet = message.text,
aiSnippet = None,
tags = ["sms", "nimbusSmsIndia"]
))
# Done here:
return send_results
@staticmethod
async def __send_from_savvy_bulk_sms_kenya(
http_client: httpx.AsyncClient,
auth_token: CoreAuthTokenModel,
messages: List[SavvyBulkSMSKenyaMessage],
) -> SMSSendManyResults:
# Start with a blank variable:
send_results = SMSSendManyResults()
# Initialize the third-party client:
client = AsyncSavvyBulkSMS(
partner_id = auth_token.auth["partnerId"],
short_code = auth_token.auth["shortCode"],
api_key = auth_token.auth["apiKey"],
http_client = http_client
)
# Iterate over all the messages you need to send:
for message in messages:
# Send the SMS and return the response:
client_response = await client.send_sms(
recipient_number = message.recipientNo,
message = message.text
)
# Note down the results:
send_results.totalCount += 1
if client_response.success: send_results.successCount += 1
else: send_results.failureCount += 1
send_results.smsMessages.append(CoreMessageModel(
ts = client_response.ts,
syncTs = date_time.get_current_utc_date_time(as_string = False),
tokenId = auth_token.authTokenId,
serviceType = auth_token.serviceType,
client = auth_token.client,
clientMessageId = client_response.messageId,
clientThreadId = message.recipientNo,
isSent = True,
isBroadcast = False,
sentSuccessfully = client_response.success,
sender = None,
recipient = message.recipientNo,
chat = None,
message = client_response.model_dump(),
snippet = message.text,
aiSnippet = None,
tags = ["sms", "savvyBulkSmsKenya"]
))
# Done here:
return send_results
async def send(
self,
mongo_conn: AsyncMongo,
http_client: httpx.AsyncClient,
# token_id: ObjectId | str,
auth_token: CoreAuthTokenModel,
messages: List[NimbusSMSIndiaMessage | SavvyBulkSMSKenyaMessage]
) -> SMSSendManyResults:
# Start by assuming failure:
send_results = SMSSendManyResults()
# # We first load the authorization tokens:
# auth_token = await self.get_token(
# mongo_conn = mongo_conn,
# token_id = token_id,
# @staticmethod
# async def __request_from_safaricom_m_pesa_express(
# http_client: httpx.AsyncClient,
# auth_token: CoreAuthTokenModel,
# payment: Union[SafaricomMPesaExpressRequest],
# callback_url: str
# ) -> PaymentRequestOneResult:
#
# # Start by assuming failure:
# request_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
# )
#
# # If we failed to load the authorization tokens:
# if not auth_token:
# send_results.message = f"no such token id '{token_id}'"
# return send_results
# # Make the payment request:
# client_response = await client.request_payment(
# amount = payment.amount,
# party_a = payment.partyA,
# type = payment.transactionType,
# reference = payment.accountReference,
# description = payment.transactionDescription,
# callback_url = callback_url,
# payer_no = payment.phoneNo,
# party_b = payment.partyB
# )
#
# # Construct the payment details:
# payment_details = CorePaymentModel(
# user = None,
# lastEventTs = date_time.get_current_utc_date_time(as_string = False),
# lastPaymentStatus = "initiated" if client_response.success else "initFailed",
# tokenId = auth_token.authTokenId,
# amount = payment.amount,
# curencyCode = "KES",
# # metadata = payment.
# )
#
# # Done here:
# request_result.success = client_response.success
# request_result.message = client_response.message
# request_result.message = client_response.message
# return request_result
#
# async def request_payment(
# self,
# mongo_conn: AsyncMongo,
# http_client: httpx.AsyncClient,
# auth_token: CoreAuthTokenModel,
# payment: Union[SafaricomMPesaExpressRequest],
# callback_url: str
# ) -> PaymentRequestOneResult:
#
# # Start by assuming failure:
# request_result = PaymentRequestOneResult()
#
# # Now we route the message to the appropriate client:
# match auth_token.client:
# case "safaricomMPesaExpress":
# request_result = await self.__request_from_safaricom_m_pesa_express(
# http_client = http_client,
# auth_token = auth_token,
# payment = payment,
# callback_url = callback_url
# )
# case _:
# request_result.message = f"invalid client {auth_token.client}"
#
# # Done here:
# return request_result
# Now we route the message to the appropriate client:
match auth_token.client:
case "nimbusSmsIndia":
send_results = await self.__send_from_nimbus_sms_india(
http_client = http_client,
auth_token = auth_token,
messages = messages
)
case "savvyBulkSmsKenya":
send_results = await self.__send_from_savvy_bulk_sms_kenya(
http_client = http_client,
auth_token = auth_token,
messages = messages
)
case _:
send_results.message = f"invalid client {auth_token.client}"
# Save the results to MongoDB:
tasks = []
for sms in send_results.smsMessages:
message_json = sms.model_dump()
message_json.pop("_id", None)
tasks.append(current_app.core_message_controller.insert(
mongo_conn = mongo_conn,
message = message_json
))
results = await asyncio.gather(*tasks)
# Done here:
send_results.message = f"{send_results.successCount}/{send_results.totalCount} message(s) sent"
return send_results
# ┓ • ┏┓ ┏┓ ┳┳┓
# ┃ ┓┏╋ ┣╋ ┃┓┏┓╋ ┃┃┃┏┓┏┏┏┓┏┓┏┓┏
# ┗┛┗┛┗ ┗┻ ┗┛┗ ┗ ┛ ┗┗ ┛┛┗┻┗┫┗ ┛
# ┓ • ┓ ┏┓ ┏┓
# ┃ ┓┏╋ ┏┓┏┓┏┫ ┃┓┏┓╋ ┃┃┏┓┓┏┏┳┓┏┓┏┓╋┏
# ┗┛┗┛┗ ┗┻┛┗┗┻ ┗┛┗ ┗ ┣┛┗┻┗┫┛┗┗┗ ┛┗┗┛
# ┛
# These are simply for retrieving sms messages.
# These are simply for retrieving payment records.
# You need to already have them saved to the database.
# @staticmethod
# async def list_messages(
# mongo_conn: AsyncMongo,
# token_ids: List[ObjectId | str],
# limit: int = 100,
# skip: int = 0,
# additional_filter: dict = None
# ) -> List[CoreMessageModel] | None:
#
# # regardless of what additional filter is provided from outside,
# # we add a mail-selecting filter here:
# if additional_filter is None: additional_filter = {}
# additional_filter["serviceType"] = "sms"
#
# # Simply call the core model:
# return await current_app.core_message_controller.get_message(
# mongo_conn = mongo_conn,
# token_ids = token_ids,
# limit = limit,
# skip = skip,
# additional_filter = additional_filter
# )
#
# @staticmethod
# async def get_one_mail(
# mongo_conn: AsyncMongo,
# token_id: ObjectId | str,
# message_id: ObjectId | str
# ) -> CoreMessageModel | None:
#
# # Simply call the core model:
# return await current_app.core_message_controller.get_message(
# mongo_conn = mongo_conn,
# token_id = token_id,
# message_id = message_id
# )
@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 add_event(
mongo_conn: AsyncMongo,
payment_id: ObjectId | str,
event: PaymentEvent
) -> bool:
# Simply call the core model:
return await current_app.core_payment_controller.add_event(
mongo_conn = mongo_conn,
payment_id = payment_id,
event = event
)
@staticmethod
async def update_tags(
mongo_conn: AsyncMongo,
token_id: ObjectId | str,
message_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_message_controller.update_tags(
return await current_app.core_payment_controller.update_tags(
mongo_conn = mongo_conn,
token_id = token_id,
message_id = message_id,
payment_id = payment_id,
unset_tags = unset_tags,
set_tags = set_tags
)
+9 -17
View File
@@ -21,7 +21,8 @@
N/A
"""
import asyncio
# *****************************************************************************************************************
# ***** ****
# *** IMPORT ***
@@ -58,9 +59,9 @@ from models.api.sms.send import (
)
# SMS Clients:
from utils_v2.sms.models.behaviour.nimbus.async_nimbus import AsyncNimbusSMS
from utils_v2.sms.models.behaviour.savvy_bulk_sms.async_savvy_bulk_sms import AsyncSavvyBulkSMS
from utils_v2.sms.models.data.sms_message import SentSMSMessageModel
from utils_v2.sms.india.nimbus.controllers.async_nimbus import AsyncNimbusSMS
from utils_v2.sms.kenya.savvy_bulk_sms.controllers.async_savvy_bulk_sms import AsyncSavvyBulkSMS
from utils_v2.sms.models.sms_message import SentSMSMessageModel
# To work with MongoDB:
from bson import ObjectId
@@ -72,6 +73,9 @@ from typing import Literal, List, Dict, Any
# To make API calls:
import httpx
# For asynchronous activities:
import asyncio
# *****************************************************************************************************************
# ***** ****
@@ -130,7 +134,7 @@ class SMSController:
# ┛┗┗┻┗┛┗
@staticmethod
async def set_token(
async def set_token_direct(
db_conn: AsyncMySQL,
mongo_conn: AsyncMongo,
auth_token: CoreAuthTokenModel,
@@ -292,7 +296,6 @@ class SMSController:
self,
mongo_conn: AsyncMongo,
http_client: httpx.AsyncClient,
# token_id: ObjectId | str,
auth_token: CoreAuthTokenModel,
messages: List[NimbusSMSIndiaMessage | SavvyBulkSMSKenyaMessage]
) -> SMSSendManyResults:
@@ -300,17 +303,6 @@ class SMSController:
# Start by assuming failure:
send_results = SMSSendManyResults()
# # We first load the authorization tokens:
# auth_token = await self.get_token(
# mongo_conn = mongo_conn,
# token_id = token_id,
# )
#
# # If we failed to load the authorization tokens:
# if not auth_token:
# send_results.message = f"no such token id '{token_id}'"
# return send_results
# Now we route the message to the appropriate client:
match auth_token.client:
case "nimbusSmsIndia":
+1 -1
View File
@@ -90,7 +90,7 @@ from bson import ObjectId
# *****************************************************************************************************************
class AuthTokenController(BaseModel):
class CoreAuthTokenController(BaseModel):
# ┏┓┓ ┓┏
# ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏
+1 -3
View File
@@ -31,8 +31,6 @@
# To make sibling directories accessible for imports:
import sys
from pyexpat.errors import messages
sys.path.append(".")
sys.path.append("..")
@@ -110,7 +108,7 @@ import asyncio
# *****************************************************************************************************************
class MessageController(BaseModel):
class CoreMessageController(BaseModel):
# ┏┓┓ ┓┏
# ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏
+70 -126
View File
@@ -6,11 +6,11 @@
DATE:
Thursday, 12th Dec., 2024
Monday, 16th Dec., 2024
OBJECTIVE:
To handle all messages from one place.
To handle all payments from one place.
REFERENCES:
@@ -31,8 +31,6 @@
# To make sibling directories accessible for imports:
import sys
from pyexpat.errors import messages
sys.path.append(".")
sys.path.append("..")
@@ -50,7 +48,7 @@ from controllers.base import BaseModel
# Data models:
from models.core.auth_token import CoreAuthTokenModel
from models.core.message import CoreMessageModel
from models.core.payment import CorePaymentModel, PaymentEvent
from models.core.user import CoreUserInfoModel
# To work with MongoDB:
@@ -110,56 +108,36 @@ import asyncio
# *****************************************************************************************************************
class MessageController(BaseModel):
class CorePaymentController(BaseModel):
# ┏┓┓ ┓┏
# ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏
# ┗┛┗┗┻┛┛ ┗┛┗┻┛ ┛
# For MongoDB:
MESSAGES_COLLECTION = "_messages"
PAYMENTS_COLLECTION = "_payments"
# ┏┓┳┓┳┳┳┓ ┏┓
# ┃ ┣┫┃┃┃┃ ━━ ┃ ┏┓┏┓┏┓╋┏┓
# ┗┛┛┗┗┛┻┛ ┗┛┛ ┗ ┗┻┗┗
async def insert(
async def init(
self,
mongo_conn: AsyncMongo,
message: CoreMessageModel
payment: CorePaymentModel
) -> ObjectId:
"""
Simply insert one message document into the database.
Simply insert one payment document into the database.
:param mongo_conn: The instance of the database connector to use for the operation.
:param message: The message to save into the database.
:param payment: The payment whose record needs to be saved in the database.
:return: The object id of the inserted document.
"""
# Simply insert the document:
return await mongo_conn.insert_one(
collection = self.MESSAGES_COLLECTION,
document = message,
raise_exception = True
)
async def bulk_write(
self,
mongo_conn: AsyncMongo,
mongo_operations: list
) -> int:
"""
Needed in cases like forcing re-sync of mails where you need to perform actions like bulk replacements of
existing documents. Not recommended to use. Please use very carefully to ensure document integrity.
:param mongo_conn: The instance of the database connector to use for the operation.
:param mongo_operations: The list operations that are supported by MongoDB's Bulk Write system.
:return: The no. of documents affected.
"""
return await mongo_conn.bulk_write(
collection = self.MESSAGES_COLLECTION,
requests = mongo_operations,
collection = self.PAYMENTS_COLLECTION,
document = payment,
raise_exception = True
)
@@ -167,7 +145,7 @@ class MessageController(BaseModel):
# ┃ ┣┫┃┃┃┃ ━━ ┣┫┏┓╋┏┓┓┏┓┓┏┏┓
# ┗┛┛┗┗┛┻┛ ┛┗┗ ┗┛ ┗┗ ┗┛┗
async def count_messages(
async def count_payments(
self,
mongo_conn: AsyncMongo,
token_ids: List[ObjectId | str],
@@ -175,11 +153,11 @@ class MessageController(BaseModel):
) -> int:
"""
Just counts the no. of messages that match a given set of conditions.
Just counts the no. of payment records that match a given set of conditions.
:param mongo_conn: The instance of the database connector to use for the operation.
:param token_ids: The token ids of the accounts from which these messages must be fetched.
:param token_ids: The token ids of the accounts from which these payment details must be fetched.
:param additional_filter: Any addition filters to use.
:return: The no. of messages that match the given conditions.
:return: The no. of payment records that match the given conditions.
"""
# Prepare the filter:
@@ -192,7 +170,7 @@ class MessageController(BaseModel):
# Get the count of the documents that match the criteria:
count = await mongo_conn.count(
collection = self.MESSAGES_COLLECTION,
collection = self.PAYMENTS_COLLECTION,
filter = filter_json,
raise_exception = True
)
@@ -200,23 +178,23 @@ class MessageController(BaseModel):
# Done here:
return count
async def get_previews(
async def get_payment_previews(
self,
mongo_conn: AsyncMongo,
token_ids: List[ObjectId | str],
limit: int = 100,
skip: int = 0,
additional_filter: dict = None
) -> List[CoreMessageModel] | None:
) -> List[CorePaymentModel] | None:
"""
Fetches many messages in one call, but leaves out the full payloads.
Fetches many payment details in one call, but just their previews.
:param mongo_conn: The instance of the database connector to use for the operation.
:param token_ids: The token ids of the accounts from which these messages must be fetched.
:param limit: The max. no. of messages to retrieve in this call.
:param skip: The no. of initial messages to skip. Useful for pagination.
:param limit: The max. no. of payment details to retrieve in this call.
:param skip: The no. of initial payment details to skip. Useful for pagination.
:param additional_filter: Any addition filters to use.
:return: The list of messages (as the message model). This list can be empty.
:return: The list of payments (as the payments model). This list can be empty.
"""
# Prepare the filter:
@@ -230,68 +208,9 @@ class MessageController(BaseModel):
# We fetch the messages that are identified by a specific token id,
# with the specified fetching limits, while enforcing the sorting condition:
records = await mongo_conn.find_many(
collection = self.MESSAGES_COLLECTION,
filter = filter_json,
limit = limit,
skip = skip,
sort = {"ts": -1},
projection = {
"_id": True,
"ts": True,
"syncTs": True,
"tokenId": True,
"serviceType": True,
"client": True,
"clientMessageId": True,
"clientThreadId": True,
"isSent": True,
"isBroadcast": True,
"sentSuccessfully": True,
"sender": True,
"chat": True,
"snippet": True,
"aiSnippet": True,
"tags": True
},
raise_exception = True
)
# Convert the fetched records to instances of the data model and return:
for record in records: record["message"] = {}
return [CoreMessageModel(**record) for record in records]
async def get_messages(
self,
mongo_conn: AsyncMongo,
token_ids: List[ObjectId | str],
limit: int = 100,
skip: int = 0,
additional_filter: dict = None
) -> List[CoreMessageModel] | None:
"""
Fetches many full messages in one call.
:param mongo_conn: The instance of the database connector to use for the operation.
:param token_ids: The token ids of the accounts from which these messages must be fetched.
:param limit: The max. no. of messages to retrieve in this call.
:param skip: The no. of initial messages to skip. Useful for pagination.
:param additional_filter: Any addition filters to use.
:return: The list of messages (as the message model). This list can be empty.
"""
# Prepare the filter:
if not isinstance(token_ids, list): token_ids = [token_ids]
token_ids = [ObjectId(t) for t in token_ids]
filter_json = {"tokenId": {"$in": token_ids}}
if additional_filter:
for k, v in additional_filter.items():
filter_json[k] = v
# We fetch the messages that are identified by a specific token id,
# with the specified fetching limits, while enforcing the sorting condition:
records = await mongo_conn.find_many(
collection = self.MESSAGES_COLLECTION,
collection = self.PAYMENTS_COLLECTION,
filter = filter_json,
projection = {"events": False},
limit = limit,
skip = skip,
sort = {"ts": -1},
@@ -299,28 +218,29 @@ class MessageController(BaseModel):
)
# Convert the fetched records to instances of the data model and return:
return [CoreMessageModel(**record) for record in records]
for record in records: record["events"] = []
return [CorePaymentModel(**record) for record in records]
async def get_message(
async def get_payment(
self,
mongo_conn: AsyncMongo,
token_id: ObjectId | str,
message_id: ObjectId | str,
) -> CoreMessageModel | None:
payment_id: ObjectId | str,
) -> CorePaymentModel | None:
"""
Gets one message if you know its message id.
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 token_id: The id of the auth-token associated with the message. Needed for security.
:param message_id: The id of the message that needs to be read.
:return: The contents of that one message in a structured format.
:param token_id: The id of the auth-token associated with the payment. Needed for security.
: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.MESSAGES_COLLECTION,
collection = self.PAYMENTS_COLLECTION,
filter = {
"_id": ObjectId(message_id),
"_id": ObjectId(payment_id),
"tokenId": ObjectId(token_id)
},
raise_exception = True
@@ -331,40 +251,64 @@ class MessageController(BaseModel):
# If a record was found,
# we return it as our data model:
return CoreMessageModel(**record)
return CorePaymentModel(**record)
# ┏┓┳┓┳┳┳┓ ┳┳ ┓
# ┃ ┣┫┃┃┃┃ ━━ ┃┃┏┓┏┫┏┓╋┏┓
# ┗┛┛┗┗┛┻┛ ┗┛┣┛┗┻┗┻┗┗
# ┛
# We don't support updating messages themselves,
# but we will allow updating fields like tags, marking as read or unread, etc.
# We don't support updating payments themselves,
# but we will allow updating fields like tags, adding events, etc.
async def add_event(
self,
mongo_conn: AsyncMongo,
payment_id: ObjectId | str,
event: PaymentEvent
) -> bool:
"""
Add an event to an existing record of a payment detail.
: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 event: The event that occurred. This will typically be generated by the third-party client.
:return: True if successfully noted, else False.
"""
# Try to update the existing record:
return await mongo_conn.update_one(
collection = self.PAYMENTS_COLLECTION,
filter = {"_id": ObjectId(payment_id)},
update = {"$push": {"events": event}},
upsert = False,
raise_exception = True
)
async def update_tags(
self,
mongo_conn: AsyncMongo,
token_id: ObjectId | str,
message_id: ObjectId | str,
payment_id: ObjectId | str,
unset_tags: List[str] = None,
set_tags: List[str] = None
) -> bool:
"""
Updates the tags on one message. The tags to remove are processed first, the ones to add are processed later.
Updates the tags on one payment. The tags to remove are processed first, the ones to add are processed later.
:param mongo_conn: The instance of the database connector to use for the operation.
:param token_id: The id of the auth-token associated with the message. Needed for security.
:param message_id: The id of the message that needs to be read.
:param unset_tags: The tags to remove from the message.
:param set_tags: The tags to add to the message.
:param token_id: The id of the auth-token associated with the payment. Needed for security.
:param payment_id: The id of the payment detail that needs to be read.
:param unset_tags: The tags to remove from the payment record.
:param set_tags: The tags to add to the payment record.
:return: True if the update was successful, else False.
"""
# Update the tags:
return await mongo_conn.update_one(
collection = self.MESSAGES_COLLECTION,
collection = self.PAYMENTS_COLLECTION,
filter = {
"_id": ObjectId(message_id),
"_id": ObjectId(payment_id),
"tokenId": ObjectId(token_id)
},
update = [{
+16 -1
View File
@@ -36,7 +36,7 @@ sys.path.append(".")
sys.path.append("..")
# For making data behaviour_models:
from pydantic import BaseModel, Field, field_validator, PastDatetime
from pydantic import BaseModel, Field, field_validator, PastDatetime, model_validator
from typing import Optional, Literal, Union
# My utils:
@@ -132,6 +132,21 @@ class ChatAuthRequestData(BaseModel):
class Config:
extra = "forbid"
# ┓┏ ┓• ┓ •
# ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
# ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗
@model_validator(mode = "after")
def ensure_harmony(cls, values):
client = values.client
auth = values.auth
harmony_map = {
"telegram": TelegramAuth
}
if not isinstance(auth, harmony_map[client]):
raise ValueError(f"incorrect 'auth' for selected client '{client}'")
return values
# *****************************************************************************************************************
# ***** ****
+16 -1
View File
@@ -36,7 +36,7 @@ sys.path.append(".")
sys.path.append("..")
# For making data behaviour_models:
from pydantic import BaseModel, Field, field_validator, PastDatetime
from pydantic import BaseModel, Field, field_validator, PastDatetime, model_validator
from typing import Optional, Literal, Union
# My utils:
@@ -146,6 +146,21 @@ class PGAuthRequestData(BaseModel):
class Config:
extra = "forbid"
# ┓┏ ┓• ┓ •
# ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
# ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗
@model_validator(mode = "after")
def ensure_harmony(cls, values):
client = values.client
auth = values.auth
harmony_map = {
"safaricomMPesaExpress": SafaricomMPesaExpressAuth
}
if not isinstance(auth, harmony_map[client]):
raise ValueError(f"incorrect 'auth' for selected client '{client}'")
return values
# *****************************************************************************************************************
# ***** ****
+117 -40
View File
@@ -6,11 +6,11 @@
DATE:
Friday, 13th Dec., 2024.
Monday, 16th Dec., 2024.
OBJECTIVE:
To provide a structure to receive auth details of various software.
To provide a structure to receive payment request details.
REFERENCES:
@@ -43,9 +43,18 @@ from typing import Optional, Literal, Union
from utils_v2.string import regex
from utils_v2.date_time import date_time
# Models:
from models.core.payment import CorePaymentModel
# To work with date and time:
import datetime
# To work with MongoDB:
from bson.objectid import ObjectId
# To work with currencies:
import pycountry
# *****************************************************************************************************************
# ***** ****
@@ -75,41 +84,7 @@ REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]
# *****************************************************************************************************************
class SafaricomMPesaExpressAuth(BaseModel):
consumerKey: str = Field(
description = "the app's consumer key given by safaricom; found in 'my apps'",
frozen = True
)
consumerSecret: str = Field(
description = "the app's consumer secret given by safaricom; found in 'my apps'",
frozen = True
)
businessShortCode: str = Field(
description = "your app's business short code; found in 'my apps'",
frozen = True
)
appPasskey: str = Field(
description = "your app's passkey; taken from human representative",
frozen = True
)
# ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓
# ┗┛┗┛┛┗┛┗┗┫
# ┛
class Config:
extra = "forbid"
# ---------------------------------------------------------------------------------------------------------------------
class PGAuthRequestHeaders(BaseModel):
class PGPaymentRequestHeaders(BaseModel):
sessionToken: str = Field(
description = "the session token of the user who is requesting the service",
@@ -133,10 +108,112 @@ class PGAuthRequestHeaders(BaseModel):
# ---------------------------------------------------------------------------------------------------------------------
class PGAuthRequestData(BaseModel):
class PGPaymentRequestData(BaseModel):
client: Literal["safaricomMPesaExpress"] = Field(alias = "client")
auth: Union[SafaricomMPesaExpressAuth]
# {
# "customerMobileNumber": "",
# "tokenId": "",
# "description": "",
# "payerNumber": "",
# "amount": "",
# "currency": "",
# "emailAddress": "",
# "reference": "",
# }
tokenKey: ObjectId = Field(
description = "the auth token to use to send this message",
frozen = True,
)
customerName: str | None = Field(
description = "the name of the registered customer who must make the payment",
frozen = True,
default = None
)
customerNo: str = Field(
description = "the contact no. of the registered customer who must make the payment",
frozen = True
)
payerNo: str | None = Field(
description = (
"the phone no. to which the payment request will go;"
"if not specified, the value of 'customerNo' should be used"
),
frozen = True,
default = None
)
email: str | None = Field(
description = "the e-mail id of the registered customer",
pattern = regex.REGEX_EMAIL_ID,
frozen = True,
default = None
)
amount: float | int = Field(
description = "the amount of money to be requested",
frozen = True
)
currencyCode: str = Field(
description = "the three-letter iso 4217 code to identify the currency",
frozen = True,
examples = ["INR", "USD", "KES"]
)
metadata: dict = Field(
description = "any extra information about this payment",
frozen = True
)
# ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓
# ┗┛┗┛┛┗┛┗┗┫
# ┛
class Config:
extra = "forbid"
arbitrary_types_allowed = True
# ┓┏ ┓• ┓ •
# ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
# ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗
@field_validator("tokenKey", mode = "before")
def parse_oid(cls, value):
try: value = ObjectId(value)
except: pass
return value
@field_validator("currencyCode", mode = "before")
def validate_currency(cls, value):
currency = pycountry.currencies.get(alpha_3 = value)
if currency is None: raise ValueError("invalid currency code, please use iso 4217 standard")
return value
# ---------------------------------------------------------------------------------------------------------------------
class PaymentRequestOneResult(BaseModel):
success: bool = Field(
description = "whether, or not, the sms was successfully sent",
default = False
)
message: str | None = Field(
description = "a brief message to summarize the result of the process",
default = None
)
paymentDetails: CorePaymentModel = Field(
description = "the actual data of the payment",
default = None
)
# ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓
+17 -1
View File
@@ -36,7 +36,7 @@ sys.path.append(".")
sys.path.append("..")
# For making data behaviour_models:
from pydantic import BaseModel, Field, field_validator, PastDatetime
from pydantic import BaseModel, Field, field_validator, PastDatetime, model_validator
from typing import Optional, Literal, Union
# My utils:
@@ -183,6 +183,22 @@ class SMSAuthRequestData(BaseModel):
class Config:
extra = "forbid"
# ┓┏ ┓• ┓ •
# ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
# ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗
@model_validator(mode = "after")
def ensure_harmony(cls, values):
client = values.smsClient
auth = values.auth
harmony_map = {
"nimbusSmsIndia": NimbusSMSIndiaAuth,
"savvyBulkSmsKenya": SavvyBulkSMSKenyaAuth
}
if not isinstance(auth, harmony_map[client]):
raise ValueError(f"incorrect 'auth' for selected client '{client}'")
return values
# *****************************************************************************************************************
# ***** ****
+1 -1
View File
@@ -45,7 +45,7 @@ from utils_v2.date_time import date_time
# Data models:
from models.core.message import CoreMessageModel
from utils_v2.sms.models.data.sms_message import SentSMSMessageModel
from utils_v2.sms.models.sms_message import SentSMSMessageModel
# To work with date and time:
import datetime
+163 -19
View File
@@ -36,13 +36,16 @@ sys.path.append(".")
sys.path.append("..")
# For making data behaviour_models:
from pydantic import BaseModel, Field, field_validator, PastDatetime, AwareDatetime
from pydantic import BaseModel, Field, field_validator, PastDatetime, AwareDatetime, model_validator
from typing import Optional, Literal, Union, List, Any
# My utils:
from utils_v2.string import regex
from utils_v2.date_time import date_time
# Other core models:
from models.core.user import CoreUserInfoModel
# To work with MongoDB:
from bson.objectid import ObjectId
@@ -80,6 +83,57 @@ import pycountry
# *****************************************************************************************************************
class CustomerDetails(BaseModel):
name: str | None = Field(
description = "the name of the registered customer who must make the payment",
frozen = True,
default = None
)
contactNo: str = Field(
description = "the contact no. of the registered customer who must make the payment",
frozen = True
)
payerNo: str | None = Field(
description = (
"the phone no. to which the payment request will go;"
"if not specified, the value of 'contactNo' will be used"
),
frozen = False,
default = None
)
email: str | None = Field(
description = "the e-mail id of the registered customer",
pattern = regex.REGEX_EMAIL_ID,
frozen = True,
default = None
)
# ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓
# ┗┛┗┛┛┗┛┗┗┫
# ┛
class Config:
extra = "allow"
arbitrary_types_allowed = True
# ┓┏ ┓• ┓ •
# ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
# ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗
@model_validator(mode = "after")
def validate_model(cls, values):
if values.payerNo is None: values.payerNo = values.contactNo
return values
# ---------------------------------------------------------------------------------------------------------------------
class PaymentEvent(BaseModel):
eventTs: AwareDatetime = Field(
@@ -87,6 +141,19 @@ class PaymentEvent(BaseModel):
frozen = True
)
paymentStatus: Literal[
"initFailed", # ... When we tried to initiate the request, but the payment gateway (PG) rejected it.
"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).
"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).
"settled", # ...... When the PG sends the money to your account.
"refunded", # ..... When the money was refunded to the client.
] = Field(
description = "the status of the payment request to see what stage of the process we are in",
frozen = False
)
initByPG: bool = Field(
description = "to figure out whether the payment gateway initiated this event or we did",
frozen = True
@@ -98,6 +165,10 @@ class PaymentEvent(BaseModel):
examples = [200, 400, 401]
)
headers: dict = Field(
description = "the headers that came in with the event; useful when trying to decode who generated the event"
)
payload: dict = Field(
description = "the json payload or set of query params received from an event from the payment gateway",
frozen = True
@@ -133,35 +204,44 @@ class CorePaymentModel(BaseModel):
alias = "_id"
)
paymentStatus: Literal[
"initFailed", # ... When we tried to initiate the request, but the payment gateway (PG) rejected it.
"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).
"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).
"settled", # ...... When the PG sends the money to your account.
"refunded", # ..... When the money was refunded to the client.
] = Field(
description = "the status of the payment request to see what stage of the process we are in",
frozen = False
user: CoreUserInfoModel = Field(
description = "how you identify your user",
frozen = True
)
customer: CustomerDetails = Field(
description = "how you identify your user's customer",
frozen = True
)
ts: AwareDatetime = Field(
description = "the time (utc) at which the payment was first initiated",
frozen = True,
default_factory = lambda: date_time.get_current_utc_date_time(as_string = False)
)
lastEventTs: AwareDatetime = Field(
description = "the time (utc) at which the latest payment event occurred",
description = "the time (utc) at which the last event occurred",
frozen = True
)
lastPaymentStatus: str = Field(
description = "the status of the payment request to see what stage of the process we are in",
frozen = False
)
tokenId: ObjectId = Field(
description = "the id of the auth token that is associated with this payment",
frozen = True
)
amount: float | int = Field(
description = "the amount of money being requested"
description = "the amount of money being requested",
frozen = True
)
currencyCode: str = Field(
description = "the three-letter ISO 4217 code to identify the currency",
description = "the three-letter iso 4217 code to identify the currency",
frozen = True,
examples = ["INR", "USD", "KES"]
)
@@ -206,6 +286,51 @@ class CorePaymentModel(BaseModel):
def model_dump(self, *args, **kwargs):
return super().model_dump(*args, by_alias = True, **kwargs)
# ┏┓ •
# ┃┃┏┓┏┓┏┓┏┓┏┓╋┓┏┓┏
# ┣┛┛ ┗┛┣┛┗ ┛ ┗┗┗ ┛
# ┛
@property
def full(self):
payment_json = {
"paymentId": str(self.messageId),
"user": self.user,
"ts": self.ts.isoformat(),
"lastEventTs": self.lastEventTs,
"lastPaymentStatus": self.lastPaymentStatus,
"amount": self.amount,
"currencyCode": self.currencyCode,
"client": self.client,
"clientPaymentReferenceId": self.clientPaymentReferenceId,
"tags": self.tags,
"metadata": self.metadata,
"events": []
}
for e in self.events:
payment_json["events"].append({
"eventTs": e.eventTs,
"paymentStatus": e.paymentStatus,
"initByPG": e.initByPG
})
return payment_json
@property
def preview(self):
return {
"paymentId": str(self.messageId),
"user": self.user,
"ts": self.ts.isoformat(),
"lastEventTs": self.lastEventTs,
"lastPaymentStatus": self.lastPaymentStatus,
"amount": self.amount,
"currencyCode": self.currencyCode,
"client": self.client,
"clientPaymentReferenceId": self.clientPaymentReferenceId,
"tags": self.tags,
"metadata": self.metadata
}
# ┓┏ ┓• ┓ •
# ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
# ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗
@@ -248,22 +373,37 @@ if __name__ == "__main__":
now = date_time.get_current_utc_date_time(as_string = False)
payment = CorePaymentModel(
paymentStatus = "authorized",
tokenId = "67519cf3a7804fcbc6f12452",
user = CoreUserInfoModel(
userId = 1
),
customer = CustomerDetails(
name = "Bhopli Narangi",
contactNo = "9876543210",
email = "bhopli@orange.com"
),
amount = 1.00,
currencyCode = "INR",
metadata = {
"userId": 1,
"name": "Bhopli"
"key": "value"
},
tags = [
"some",
"tags"
],
client = "razorpay",
clientPaymentReferenceId = "txn_123_abc",
lastPaymentStatus = "settled",
lastEventTs = now,
events = [
PaymentEvent(
eventTs = now - datetime.timedelta(minutes = 1, seconds = 12),
initByPG = True,
paymentStatus = "initiated",
initByPG = False,
httpCode = None,
headers = {
"some": "header"
},
payload = {
"status": "captured",
"from": "Barfi",
@@ -271,8 +411,12 @@ if __name__ == "__main__":
),
PaymentEvent(
eventTs = now,
paymentStatus = "settled",
initByPG = True,
httpCode = None,
headers = {
"some": "header"
},
payload = {
"status": "authorized",
"from": "Barfi",