(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": 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( auth_token = CoreAuthTokenModel(
serviceType = "paymentGateway", serviceType = "paymentGateway",
client = inbound_data.client, client = inbound_data.client,
@@ -181,22 +184,7 @@ async def authorize_payment_gateway(
}, },
status = "active", status = "active",
syncFreq = 60 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"] 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: # GMail-related utils:
from utils_v2.goog.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT 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: # Common:
from shared import constants from shared import constants
+1 -1
View File
@@ -64,7 +64,7 @@ from utils_v2.api.async_quart import (
# GMail-related utils: # GMail-related utils:
from utils_v2.goog.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT 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: # Common:
from shared import constants from shared import constants
+1 -1
View File
@@ -64,7 +64,7 @@ from utils_v2.api.async_quart import (
# GMail-related utils: # GMail-related utils:
from utils_v2.goog.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT 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: # Common:
from shared import constants from shared import constants
+1 -1
View File
@@ -63,7 +63,7 @@ from utils_v2.api.async_quart import (
# GMail-related utils: # GMail-related utils:
from utils_v2.goog.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT 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: # Common:
from shared import constants from shared import constants
+2 -2
View File
@@ -166,7 +166,7 @@ async def authorize_sms_client(
if inbound_data.smsClient == "nimbusSmsIndia": 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, db_conn = current_app.sql_writer,
mongo_conn = current_app.data_mongo, mongo_conn = current_app.data_mongo,
auth_token = CoreAuthTokenModel( auth_token = CoreAuthTokenModel(
@@ -193,7 +193,7 @@ async def authorize_sms_client(
elif inbound_data.smsClient == "savvyBulkSmsKenya": 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, db_conn = current_app.sql_writer,
mongo_conn = current_app.data_mongo, mongo_conn = current_app.data_mongo,
auth_token = CoreAuthTokenModel( 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 from utils_v2.goog.gmail.gmail_client import AsyncGMailClient
# Core Controller Models: # Core Controller Models:
from controllers.core.message import MessageController from controllers.core.message import CoreMessageController
from controllers.core.auth_token import AuthTokenController from controllers.core.auth_token import CoreAuthTokenController
from controllers.core.ai.llm import LLMController from controllers.core.ai.llm import LLMController
# API Controller Models: # API Controller Models:
from controllers.api.mail import MailController from controllers.api.mail import MailController
from controllers.api.sms import SMSController from controllers.api.sms import SMSController
from controllers.api.payment import PaymentController
# # Old Behaviour Models: # # Old Behaviour Models:
# from controllers.mail.oauth_v3 import MailOAuthModel # 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, cache = current_app.module_cache,
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,
@@ -336,7 +337,7 @@ async def app_startup(**kwargs):
debug_prefix = "Message (CM) | ", debug_prefix = "Message (CM) | ",
debug_only_errors = True debug_only_errors = True
) )
current_app.core_message_controller = MessageController( current_app.core_message_controller = CoreMessageController(
cache = current_app.module_cache, cache = current_app.module_cache,
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,
@@ -351,6 +352,7 @@ async def app_startup(**kwargs):
current_app.mail_controller = MailController() current_app.mail_controller = MailController()
current_app.sms_controller = SMSController() 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: # Mail Clients:
from utils_v2.goog.gmail.gmail_client import AsyncGMailClient 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: # To work with MongoDB:
from bson import ObjectId from bson import ObjectId
+149 -224
View File
@@ -21,7 +21,8 @@
N/A N/A
""" """
import asyncio
# ***************************************************************************************************************** # *****************************************************************************************************************
# ***** **** # ***** ****
# *** IMPORT *** # *** 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_mysql_v2 import AsyncMySQL
from utils_v2.database.async_mongo_v2 import AsyncMongo, AsyncMongoStorage from utils_v2.database.async_mongo_v2 import AsyncMongo, AsyncMongoStorage
# Base model:
from controllers.base import BaseModel
# 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.message import CoreMessageModel from models.core.payment import CorePaymentModel, PaymentEvent
from models.api.sms.send import ( from models.api.finstitutions.payments.request import (
SMSSendRequestData, PaymentRequestOneResult
NimbusSMSIndiaMessage,
SavvyBulkSMSKenyaMessage,
SMSSendManyResults
) )
# SMS Clients: # Payment Clients:
from utils_v2.sms.models.behaviour.nimbus.async_nimbus import AsyncNimbusSMS from utils_v2.payments.safaricom.models.auth import MPesaExpressAuthorization
from utils_v2.sms.models.behaviour.savvy_bulk_sms.async_savvy_bulk_sms import AsyncSavvyBulkSMS from utils_v2.payments.safaricom.controllers.m_pesa_express import SafaricomMPesaExpress
from utils_v2.sms.models.data.sms_message import SentSMSMessageModel
# To work with MongoDB: # To work with MongoDB:
from bson import ObjectId from bson import ObjectId
from pymongo import InsertOne from pymongo import InsertOne
# To work with datatypes: # To work with datatypes:
from typing import Literal, List, Dict, Any from typing import Literal, List, Dict, Any, Union
# To make API calls: # To make API calls:
import httpx 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 token_key = token_key
) )
# ┏┓ # ┳┓ ┏
# ┗┓┏┓┏┓┏ # ┣┫┏┓┏┓┓┏┏┓┏╋ ┃┃┏┓┓┏┏┳┓┏┓┏┓╋┏
# ┗┛┗ ┛┗┗┻ # ┛┗┗ ┗┫┗┻┗ ┛┗ ┣┛┗┻┗┫┛┗┗┗ ┛┗┗┛
# ┗ ┛
@staticmethod # @staticmethod
async def __send_from_nimbus_sms_india( # async def __request_from_safaricom_m_pesa_express(
http_client: httpx.AsyncClient, # http_client: httpx.AsyncClient,
auth_token: CoreAuthTokenModel, # auth_token: CoreAuthTokenModel,
messages: List[NimbusSMSIndiaMessage], # payment: Union[SafaricomMPesaExpressRequest],
) -> SMSSendManyResults: # callback_url: str
# ) -> PaymentRequestOneResult:
# Start with a blank variable: #
send_results = SMSSendManyResults() # # Start by assuming failure:
# request_result = PaymentRequestOneResult()
# Initialize the third-party client: #
client = AsyncNimbusSMS( # # Create the client:
entity_id = auth_token.auth["entityId"], # client = SafaricomMPesaExpress(
sender_id = auth_token.auth["senderId"], # auth = MPesaExpressAuthorization(
user_id = auth_token.auth["userId"], # consumerKey = auth_token.auth["consumerKey"],
api_key = auth_token.auth["apiKey"], # consumerSecret = auth_token.auth["consumerSecret"],
http_client = http_client # businessShortCode = auth_token.auth["businessShortCode"],
) # appPasskey = auth_token.auth["appPasskey"]
# ),
# Iterate over all the messages you need to send: # http_client = http_client
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,
# ) # )
# #
# # If we failed to load the authorization tokens: # # Make the payment request:
# if not auth_token: # client_response = await client.request_payment(
# send_results.message = f"no such token id '{token_id}'" # amount = payment.amount,
# return send_results # 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. # You need to already have them saved to the database.
# @staticmethod @staticmethod
# async def list_messages( async def list_payments(
# mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
# token_ids: List[ObjectId | str], token_ids: List[ObjectId | str],
# limit: int = 100, limit: int = 100,
# skip: int = 0, skip: int = 0,
# additional_filter: dict = None additional_filter: dict = None
# ) -> List[CoreMessageModel] | None: ) -> List[CorePaymentModel] | None:
#
# # regardless of what additional filter is provided from outside, # Regardless of what additional filter is provided from outside,
# # we add a mail-selecting filter here: # we add a payment-selecting filter here:
# if additional_filter is None: additional_filter = {} if additional_filter is None: additional_filter = {}
# additional_filter["serviceType"] = "sms" additional_filter["serviceType"] = "paymentGateway"
#
# # Simply call the core model: # Simply call the core model:
# return await current_app.core_message_controller.get_message( return await current_app.core_payment_controller.get_payment_previews(
# mongo_conn = mongo_conn, mongo_conn = mongo_conn,
# token_ids = token_ids, token_ids = token_ids,
# limit = limit, limit = limit,
# skip = skip, skip = skip,
# additional_filter = additional_filter additional_filter = additional_filter
# ) )
#
# @staticmethod @staticmethod
# async def get_one_mail( async def get_payment(
# mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
# token_id: ObjectId | str, token_id: ObjectId | str,
# message_id: ObjectId | str payment_id: ObjectId | str
# ) -> CoreMessageModel | None: ) -> CorePaymentModel | None:
#
# # Simply call the core model: # Simply call the core model:
# return await current_app.core_message_controller.get_message( return await current_app.core_payment_controller.get_payment(
# mongo_conn = mongo_conn, mongo_conn = mongo_conn,
# token_id = token_id, token_id = token_id,
# message_id = message_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 @staticmethod
async def update_tags( async def update_tags(
mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
token_id: ObjectId | str, token_id: ObjectId | str,
message_id: ObjectId | str, payment_id: ObjectId | str,
unset_tags: List[str] = None, unset_tags: List[str] = None,
set_tags: List[str] = None set_tags: List[str] = None
) -> bool: ) -> bool:
# Simply call the core model: # 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, mongo_conn = mongo_conn,
token_id = token_id, token_id = token_id,
message_id = message_id, payment_id = payment_id,
unset_tags = unset_tags, unset_tags = unset_tags,
set_tags = set_tags set_tags = set_tags
) )
+9 -17
View File
@@ -21,7 +21,8 @@
N/A N/A
""" """
import asyncio
# ***************************************************************************************************************** # *****************************************************************************************************************
# ***** **** # ***** ****
# *** IMPORT *** # *** IMPORT ***
@@ -58,9 +59,9 @@ from models.api.sms.send import (
) )
# SMS Clients: # SMS Clients:
from utils_v2.sms.models.behaviour.nimbus.async_nimbus import AsyncNimbusSMS from utils_v2.sms.india.nimbus.controllers.async_nimbus import AsyncNimbusSMS
from utils_v2.sms.models.behaviour.savvy_bulk_sms.async_savvy_bulk_sms import AsyncSavvyBulkSMS from utils_v2.sms.kenya.savvy_bulk_sms.controllers.async_savvy_bulk_sms import AsyncSavvyBulkSMS
from utils_v2.sms.models.data.sms_message import SentSMSMessageModel from utils_v2.sms.models.sms_message import SentSMSMessageModel
# To work with MongoDB: # To work with MongoDB:
from bson import ObjectId from bson import ObjectId
@@ -72,6 +73,9 @@ from typing import Literal, List, Dict, Any
# To make API calls: # To make API calls:
import httpx import httpx
# For asynchronous activities:
import asyncio
# ***************************************************************************************************************** # *****************************************************************************************************************
# ***** **** # ***** ****
@@ -130,7 +134,7 @@ class SMSController:
# ┛┗┗┻┗┛┗ # ┛┗┗┻┗┛┗
@staticmethod @staticmethod
async def set_token( async def set_token_direct(
db_conn: AsyncMySQL, db_conn: AsyncMySQL,
mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
auth_token: CoreAuthTokenModel, auth_token: CoreAuthTokenModel,
@@ -292,7 +296,6 @@ class SMSController:
self, self,
mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
http_client: httpx.AsyncClient, http_client: httpx.AsyncClient,
# token_id: ObjectId | str,
auth_token: CoreAuthTokenModel, auth_token: CoreAuthTokenModel,
messages: List[NimbusSMSIndiaMessage | SavvyBulkSMSKenyaMessage] messages: List[NimbusSMSIndiaMessage | SavvyBulkSMSKenyaMessage]
) -> SMSSendManyResults: ) -> SMSSendManyResults:
@@ -300,17 +303,6 @@ class SMSController:
# Start by assuming failure: # Start by assuming failure:
send_results = SMSSendManyResults() 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: # Now we route the message to the appropriate client:
match auth_token.client: match auth_token.client:
case "nimbusSmsIndia": 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: # To make sibling directories accessible for imports:
import sys import sys
from pyexpat.errors import messages
sys.path.append(".") sys.path.append(".")
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: DATE:
Thursday, 12th Dec., 2024 Monday, 16th Dec., 2024
OBJECTIVE: OBJECTIVE:
To handle all messages from one place. To handle all payments from one place.
REFERENCES: REFERENCES:
@@ -31,8 +31,6 @@
# To make sibling directories accessible for imports: # To make sibling directories accessible for imports:
import sys import sys
from pyexpat.errors import messages
sys.path.append(".") sys.path.append(".")
sys.path.append("..") sys.path.append("..")
@@ -50,7 +48,7 @@ from controllers.base import BaseModel
# Data models: # Data models:
from models.core.auth_token import CoreAuthTokenModel 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 from models.core.user import CoreUserInfoModel
# To work with MongoDB: # To work with MongoDB:
@@ -110,56 +108,36 @@ import asyncio
# ***************************************************************************************************************** # *****************************************************************************************************************
class MessageController(BaseModel): class CorePaymentController(BaseModel):
# ┏┓┓ ┓┏ # ┏┓┓ ┓┏
# ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏ # ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏
# ┗┛┗┗┻┛┛ ┗┛┗┻┛ ┛ # ┗┛┗┗┻┛┛ ┗┛┗┻┛ ┛
# For MongoDB: # For MongoDB:
MESSAGES_COLLECTION = "_messages" PAYMENTS_COLLECTION = "_payments"
# ┏┓┳┓┳┳┳┓ ┏┓ # ┏┓┳┓┳┳┳┓ ┏┓
# ┃ ┣┫┃┃┃┃ ━━ ┃ ┏┓┏┓┏┓╋┏┓ # ┃ ┣┫┃┃┃┃ ━━ ┃ ┏┓┏┓┏┓╋┏┓
# ┗┛┛┗┗┛┻┛ ┗┛┛ ┗ ┗┻┗┗ # ┗┛┛┗┗┛┻┛ ┗┛┛ ┗ ┗┻┗┗
async def insert( async def init(
self, self,
mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
message: CoreMessageModel payment: CorePaymentModel
) -> ObjectId: ) -> 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 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. :return: The object id of the inserted document.
""" """
# Simply insert the document: # Simply insert the document:
return await mongo_conn.insert_one( return await mongo_conn.insert_one(
collection = self.MESSAGES_COLLECTION, collection = self.PAYMENTS_COLLECTION,
document = message, document = payment,
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,
raise_exception = True raise_exception = True
) )
@@ -167,7 +145,7 @@ class MessageController(BaseModel):
# ┃ ┣┫┃┃┃┃ ━━ ┣┫┏┓╋┏┓┓┏┓┓┏┏┓ # ┃ ┣┫┃┃┃┃ ━━ ┣┫┏┓╋┏┓┓┏┓┓┏┏┓
# ┗┛┛┗┗┛┻┛ ┛┗┗ ┗┛ ┗┗ ┗┛┗ # ┗┛┛┗┗┛┻┛ ┛┗┗ ┗┛ ┗┗ ┗┛┗
async def count_messages( async def count_payments(
self, self,
mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
token_ids: List[ObjectId | str], token_ids: List[ObjectId | str],
@@ -175,11 +153,11 @@ class MessageController(BaseModel):
) -> int: ) -> 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 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. :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: # Prepare the filter:
@@ -192,7 +170,7 @@ class MessageController(BaseModel):
# Get the count of the documents that match the criteria: # Get the count of the documents that match the criteria:
count = await mongo_conn.count( count = await mongo_conn.count(
collection = self.MESSAGES_COLLECTION, collection = self.PAYMENTS_COLLECTION,
filter = filter_json, filter = filter_json,
raise_exception = True raise_exception = True
) )
@@ -200,23 +178,23 @@ class MessageController(BaseModel):
# Done here: # Done here:
return count return count
async def get_previews( async def get_payment_previews(
self, self,
mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
token_ids: List[ObjectId | str], token_ids: List[ObjectId | str],
limit: int = 100, limit: int = 100,
skip: int = 0, skip: int = 0,
additional_filter: dict = None 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 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 messages must be fetched.
:param limit: The max. no. of messages to retrieve in this call. :param limit: The max. no. of payment details to retrieve in this call.
:param skip: The no. of initial messages to skip. Useful for pagination. :param skip: The no. of initial payment details to skip. Useful for pagination.
:param additional_filter: Any addition filters to use. :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: # Prepare the filter:
@@ -230,68 +208,9 @@ class MessageController(BaseModel):
# We fetch the messages that are identified by a specific token id, # We fetch the messages that are identified by a specific token id,
# with the specified fetching limits, while enforcing the sorting condition: # with the specified fetching limits, while enforcing the sorting condition:
records = await mongo_conn.find_many( records = await mongo_conn.find_many(
collection = self.MESSAGES_COLLECTION, collection = self.PAYMENTS_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,
filter = filter_json, filter = filter_json,
projection = {"events": False},
limit = limit, limit = limit,
skip = skip, skip = skip,
sort = {"ts": -1}, sort = {"ts": -1},
@@ -299,28 +218,29 @@ class MessageController(BaseModel):
) )
# Convert the fetched records to instances of the data model and return: # 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, self,
mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
token_id: ObjectId | str, token_id: ObjectId | str,
message_id: ObjectId | str, payment_id: ObjectId | str,
) -> CoreMessageModel | None: ) -> 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 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 token_id: The id of the auth-token associated with the payment. Needed for security.
:param message_id: The id of the message that needs to be read. :param payment_id: The id of the payment detail that needs to be read.
:return: The contents of that one message in a structured format. :return: The contents of that one payment detail in a structured format.
""" """
# We fetch the whole payload of that one message: # We fetch the whole payload of that one message:
record = await mongo_conn.find_one( record = await mongo_conn.find_one(
collection = self.MESSAGES_COLLECTION, collection = self.PAYMENTS_COLLECTION,
filter = { filter = {
"_id": ObjectId(message_id), "_id": ObjectId(payment_id),
"tokenId": ObjectId(token_id) "tokenId": ObjectId(token_id)
}, },
raise_exception = True raise_exception = True
@@ -331,40 +251,64 @@ class MessageController(BaseModel):
# If a record was found, # If a record was found,
# we return it as our data model: # we return it as our data model:
return CoreMessageModel(**record) return CorePaymentModel(**record)
# ┏┓┳┓┳┳┳┓ ┳┳ ┓ # ┏┓┳┓┳┳┳┓ ┳┳ ┓
# ┃ ┣┫┃┃┃┃ ━━ ┃┃┏┓┏┫┏┓╋┏┓ # ┃ ┣┫┃┃┃┃ ━━ ┃┃┏┓┏┫┏┓╋┏┓
# ┗┛┛┗┗┛┻┛ ┗┛┣┛┗┻┗┻┗┗ # ┗┛┛┗┗┛┻┛ ┗┛┣┛┗┻┗┻┗┗
# ┛ # ┛
# We don't support updating messages themselves, # We don't support updating payments themselves,
# but we will allow updating fields like tags, marking as read or unread, etc. # 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( async def update_tags(
self, self,
mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
token_id: ObjectId | str, token_id: ObjectId | str,
message_id: ObjectId | str, payment_id: ObjectId | str,
unset_tags: List[str] = None, unset_tags: List[str] = None,
set_tags: List[str] = None set_tags: List[str] = None
) -> bool: ) -> 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 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 token_id: The id of the auth-token associated with the payment. Needed for security.
:param message_id: The id of the message that needs to be read. :param payment_id: The id of the payment detail that needs to be read.
:param unset_tags: The tags to remove from the message. :param unset_tags: The tags to remove from the payment record.
:param set_tags: The tags to add to the message. :param set_tags: The tags to add to the payment record.
:return: True if the update was successful, else False. :return: True if the update was successful, else False.
""" """
# Update the tags: # Update the tags:
return await mongo_conn.update_one( return await mongo_conn.update_one(
collection = self.MESSAGES_COLLECTION, collection = self.PAYMENTS_COLLECTION,
filter = { filter = {
"_id": ObjectId(message_id), "_id": ObjectId(payment_id),
"tokenId": ObjectId(token_id) "tokenId": ObjectId(token_id)
}, },
update = [{ update = [{
+16 -1
View File
@@ -36,7 +36,7 @@ sys.path.append(".")
sys.path.append("..") sys.path.append("..")
# For making data behaviour_models: # 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 from typing import Optional, Literal, Union
# My utils: # My utils:
@@ -132,6 +132,21 @@ class ChatAuthRequestData(BaseModel):
class Config: class Config:
extra = "forbid" 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("..") sys.path.append("..")
# For making data behaviour_models: # 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 from typing import Optional, Literal, Union
# My utils: # My utils:
@@ -146,6 +146,21 @@ class PGAuthRequestData(BaseModel):
class Config: class Config:
extra = "forbid" 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: DATE:
Friday, 13th Dec., 2024. Monday, 16th Dec., 2024.
OBJECTIVE: OBJECTIVE:
To provide a structure to receive auth details of various software. To provide a structure to receive payment request details.
REFERENCES: REFERENCES:
@@ -43,9 +43,18 @@ from typing import Optional, Literal, Union
from utils_v2.string import regex from utils_v2.string import regex
from utils_v2.date_time import date_time from utils_v2.date_time import date_time
# Models:
from models.core.payment import CorePaymentModel
# To work with date and time: # To work with date and time:
import datetime 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): class PGPaymentRequestHeaders(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):
sessionToken: str = Field( sessionToken: str = Field(
description = "the session token of the user who is requesting the service", 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("..") sys.path.append("..")
# For making data behaviour_models: # 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 from typing import Optional, Literal, Union
# My utils: # My utils:
@@ -183,6 +183,22 @@ class SMSAuthRequestData(BaseModel):
class Config: class Config:
extra = "forbid" 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: # Data models:
from models.core.message import CoreMessageModel 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: # To work with date and time:
import datetime import datetime
+163 -19
View File
@@ -36,13 +36,16 @@ sys.path.append(".")
sys.path.append("..") sys.path.append("..")
# For making data behaviour_models: # 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 from typing import Optional, Literal, Union, List, Any
# My utils: # My utils:
from utils_v2.string import regex from utils_v2.string import regex
from utils_v2.date_time import date_time from utils_v2.date_time import date_time
# Other core models:
from models.core.user import CoreUserInfoModel
# To work with MongoDB: # To work with MongoDB:
from bson.objectid import ObjectId 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): class PaymentEvent(BaseModel):
eventTs: AwareDatetime = Field( eventTs: AwareDatetime = Field(
@@ -87,6 +141,19 @@ class PaymentEvent(BaseModel):
frozen = True 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( initByPG: bool = Field(
description = "to figure out whether the payment gateway initiated this event or we did", description = "to figure out whether the payment gateway initiated this event or we did",
frozen = True frozen = True
@@ -98,6 +165,10 @@ class PaymentEvent(BaseModel):
examples = [200, 400, 401] 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( payload: dict = Field(
description = "the json payload or set of query params received from an event from the payment gateway", description = "the json payload or set of query params received from an event from the payment gateway",
frozen = True frozen = True
@@ -133,35 +204,44 @@ class CorePaymentModel(BaseModel):
alias = "_id" alias = "_id"
) )
paymentStatus: Literal[ user: CoreUserInfoModel = Field(
"initFailed", # ... When we tried to initiate the request, but the payment gateway (PG) rejected it. description = "how you identify your user",
"initiated", # .... When we made a successful payment request, or the customer initiated one from the PG. frozen = True
"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). customer: CustomerDetails = Field(
"settled", # ...... When the PG sends the money to your account. description = "how you identify your user's customer",
"refunded", # ..... When the money was refunded to the client. frozen = True
] = Field( )
description = "the status of the payment request to see what stage of the process we are in",
frozen = False 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( 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 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( tokenId: ObjectId = Field(
description = "the id of the auth token that is associated with this payment", description = "the id of the auth token that is associated with this payment",
frozen = True frozen = True
) )
amount: float | int = Field( amount: float | int = Field(
description = "the amount of money being requested" description = "the amount of money being requested",
frozen = True
) )
currencyCode: str = Field( 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, frozen = True,
examples = ["INR", "USD", "KES"] examples = ["INR", "USD", "KES"]
) )
@@ -206,6 +286,51 @@ class CorePaymentModel(BaseModel):
def model_dump(self, *args, **kwargs): def model_dump(self, *args, **kwargs):
return super().model_dump(*args, by_alias = True, **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) now = date_time.get_current_utc_date_time(as_string = False)
payment = CorePaymentModel( payment = CorePaymentModel(
paymentStatus = "authorized",
tokenId = "67519cf3a7804fcbc6f12452", tokenId = "67519cf3a7804fcbc6f12452",
user = CoreUserInfoModel(
userId = 1
),
customer = CustomerDetails(
name = "Bhopli Narangi",
contactNo = "9876543210",
email = "bhopli@orange.com"
),
amount = 1.00, amount = 1.00,
currencyCode = "INR", currencyCode = "INR",
metadata = { metadata = {
"userId": 1, "key": "value"
"name": "Bhopli"
}, },
tags = [
"some",
"tags"
],
client = "razorpay", client = "razorpay",
clientPaymentReferenceId = "txn_123_abc", clientPaymentReferenceId = "txn_123_abc",
lastPaymentStatus = "settled",
lastEventTs = now, lastEventTs = now,
events = [ events = [
PaymentEvent( PaymentEvent(
eventTs = now - datetime.timedelta(minutes = 1, seconds = 12), eventTs = now - datetime.timedelta(minutes = 1, seconds = 12),
initByPG = True, paymentStatus = "initiated",
initByPG = False,
httpCode = None, httpCode = None,
headers = {
"some": "header"
},
payload = { payload = {
"status": "captured", "status": "captured",
"from": "Barfi", "from": "Barfi",
@@ -271,8 +411,12 @@ if __name__ == "__main__":
), ),
PaymentEvent( PaymentEvent(
eventTs = now, eventTs = now,
paymentStatus = "settled",
initByPG = True, initByPG = True,
httpCode = None, httpCode = None,
headers = {
"some": "header"
},
payload = { payload = {
"status": "authorized", "status": "authorized",
"from": "Barfi", "from": "Barfi",