From 0b9976fadbc976b6331bd9f962bcb5d77676e891 Mon Sep 17 00:00:00 2001 From: khushal Date: Mon, 16 Dec 2024 16:00:52 +0530 Subject: [PATCH] (20241216) Payment auth bug fix. --- api/blueprints/finstitutions/payments/auth.py | 38 +- api/blueprints/mail/retrieve/get.py | 2 +- api/blueprints/mail/retrieve/list.py | 2 +- api/blueprints/mail/sync/sync_v2.py | 2 +- api/blueprints/mail/tags/update.py | 2 +- api/blueprints/sms/auth.py | 4 +- api/main.py | 10 +- controllers/api/mail.py | 2 +- controllers/api/payment.py | 379 +++++++----------- controllers/api/sms.py | 26 +- controllers/core/auth_token.py | 2 +- controllers/core/message.py | 4 +- controllers/core/payment.py | 196 ++++----- models/api/chat/auth.py | 17 +- models/api/finstitutions/payments/auth.py | 17 +- models/api/finstitutions/payments/request.py | 157 ++++++-- models/api/sms/auth.py | 18 +- models/api/sms/send.py | 2 +- models/core/payment.py | 182 ++++++++- 19 files changed, 589 insertions(+), 473 deletions(-) diff --git a/api/blueprints/finstitutions/payments/auth.py b/api/blueprints/finstitutions/payments/auth.py index 974af86..9d04e81 100644 --- a/api/blueprints/finstitutions/payments/auth.py +++ b/api/blueprints/finstitutions/payments/auth.py @@ -170,33 +170,21 @@ async def authorize_payment_gateway( if inbound_data.client == "safaricomMPesaExpress": - auth_token = CoreAuthTokenModel( - serviceType = "paymentGateway", - client = inbound_data.client, - authType = "auth", - auth = inbound_data.auth.model_dump(), - user = kwargs.get("session_info"), - clientUserId = { - "businessShortCode": inbound_data.auth.businessShortCode - }, - status = "active", - syncFreq = 60 - ) - - token_id = await current_app.core_auth_token_controller.get_token_id( + success = await current_app.payment_controller.set_token_direct( 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 = {}, + auth_token = CoreAuthTokenModel( + serviceType = "paymentGateway", + client = inbound_data.client, + authType = "auth", + auth = inbound_data.auth.model_dump(), + user = kwargs.get("session_info"), + clientUserId = { + "businessShortCode": inbound_data.auth.businessShortCode + }, + status = "active", + syncFreq = 60 + ), session_token = inbound_headers["X-Session-Token"] ) diff --git a/api/blueprints/mail/retrieve/get.py b/api/blueprints/mail/retrieve/get.py index fe13d9e..a3d8722 100644 --- a/api/blueprints/mail/retrieve/get.py +++ b/api/blueprints/mail/retrieve/get.py @@ -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 diff --git a/api/blueprints/mail/retrieve/list.py b/api/blueprints/mail/retrieve/list.py index 494f458..b6ff978 100644 --- a/api/blueprints/mail/retrieve/list.py +++ b/api/blueprints/mail/retrieve/list.py @@ -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 diff --git a/api/blueprints/mail/sync/sync_v2.py b/api/blueprints/mail/sync/sync_v2.py index f126c40..e2907c4 100644 --- a/api/blueprints/mail/sync/sync_v2.py +++ b/api/blueprints/mail/sync/sync_v2.py @@ -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 diff --git a/api/blueprints/mail/tags/update.py b/api/blueprints/mail/tags/update.py index ac11391..a3b382c 100644 --- a/api/blueprints/mail/tags/update.py +++ b/api/blueprints/mail/tags/update.py @@ -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 diff --git a/api/blueprints/sms/auth.py b/api/blueprints/sms/auth.py index 3fd14b0..0fae3ac 100644 --- a/api/blueprints/sms/auth.py +++ b/api/blueprints/sms/auth.py @@ -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( diff --git a/api/main.py b/api/main.py index bb59a3c..d9f72e5 100644 --- a/api/main.py +++ b/api/main.py @@ -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() # ┏┓ ┓ ┏┓┓• # ┃ ┏┓┏┓┏┓┏┓┏╋┏┓┏┓┏ ┏┓┏┓┏┫ ┃ ┃┓┏┓┏┓╋┏ diff --git a/controllers/api/mail.py b/controllers/api/mail.py index e876dc8..52dfbbc 100644 --- a/controllers/api/mail.py +++ b/controllers/api/mail.py @@ -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 diff --git a/controllers/api/payment.py b/controllers/api/payment.py index e225858..4933416 100644 --- a/controllers/api/payment.py +++ b/controllers/api/payment.py @@ -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: + # @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 + # ) + # + # # 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 - # 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, - # ) - # - # # 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": - 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 ) diff --git a/controllers/api/sms.py b/controllers/api/sms.py index 732c777..9a5dc45 100644 --- a/controllers/api/sms.py +++ b/controllers/api/sms.py @@ -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": diff --git a/controllers/core/auth_token.py b/controllers/core/auth_token.py index a1fe07f..9646d6f 100644 --- a/controllers/core/auth_token.py +++ b/controllers/core/auth_token.py @@ -90,7 +90,7 @@ from bson import ObjectId # ***************************************************************************************************************** -class AuthTokenController(BaseModel): +class CoreAuthTokenController(BaseModel): # ┏┓┓ ┓┏ # ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏ diff --git a/controllers/core/message.py b/controllers/core/message.py index cb21c5e..b29a658 100644 --- a/controllers/core/message.py +++ b/controllers/core/message.py @@ -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): # ┏┓┓ ┓┏ # ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏ diff --git a/controllers/core/payment.py b/controllers/core/payment.py index cb21c5e..fb2503d 100644 --- a/controllers/core/payment.py +++ b/controllers/core/payment.py @@ -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 = [{ diff --git a/models/api/chat/auth.py b/models/api/chat/auth.py index 1edc15c..3dd6a26 100644 --- a/models/api/chat/auth.py +++ b/models/api/chat/auth.py @@ -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 + # ***************************************************************************************************************** # ***** **** diff --git a/models/api/finstitutions/payments/auth.py b/models/api/finstitutions/payments/auth.py index bd6c515..4c523df 100644 --- a/models/api/finstitutions/payments/auth.py +++ b/models/api/finstitutions/payments/auth.py @@ -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 + # ***************************************************************************************************************** # ***** **** diff --git a/models/api/finstitutions/payments/request.py b/models/api/finstitutions/payments/request.py index bd6c515..934d350 100644 --- a/models/api/finstitutions/payments/request.py +++ b/models/api/finstitutions/payments/request.py @@ -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 + ) # ┏┓ ┏• # ┃ ┏┓┏┓╋┓┏┓ diff --git a/models/api/sms/auth.py b/models/api/sms/auth.py index 770a0c5..89c5b2d 100644 --- a/models/api/sms/auth.py +++ b/models/api/sms/auth.py @@ -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 + # ***************************************************************************************************************** # ***** **** diff --git a/models/api/sms/send.py b/models/api/sms/send.py index f2f0c84..eb58efd 100644 --- a/models/api/sms/send.py +++ b/models/api/sms/send.py @@ -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 diff --git a/models/core/payment.py b/models/core/payment.py index a3b18ed..12c227b 100644 --- a/models/core/payment.py +++ b/models/core/payment.py @@ -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",