From ddc29a7fe17ef29ed303d2ada115cc9006ce9dd7 Mon Sep 17 00:00:00 2001 From: yatmesh Date: Mon, 11 Aug 2025 14:19:24 +0530 Subject: [PATCH] (20250811) - Added RazorPay Integration class with auth api and requires validations and many more. --- .../finstitutions/payments/auth_v2.py | 36 ++- api/main.py | 10 + .../payments/razorpay/__init__.py | 0 .../finstitutions/payments/razorpay/base.py | 206 +++++++++++++++++ .../payments/razorpay/razorpay.py | 210 ++++++++++++++++++ models/api/finstitutions/payments/auth.py | 35 ++- models/finstitutions/payment/__init__.py | 0 models/finstitutions/payment/auth.py | 168 ++++++++++++++ playground/test_oauth_places.py | 65 ++++++ 9 files changed, 723 insertions(+), 7 deletions(-) create mode 100644 controllers_v2/finstitutions/payments/razorpay/__init__.py create mode 100644 controllers_v2/finstitutions/payments/razorpay/base.py create mode 100644 controllers_v2/finstitutions/payments/razorpay/razorpay.py create mode 100644 models/finstitutions/payment/__init__.py create mode 100644 models/finstitutions/payment/auth.py create mode 100644 playground/test_oauth_places.py diff --git a/api/blueprints/finstitutions/payments/auth_v2.py b/api/blueprints/finstitutions/payments/auth_v2.py index 4e6e271..4084b4c 100644 --- a/api/blueprints/finstitutions/payments/auth_v2.py +++ b/api/blueprints/finstitutions/payments/auth_v2.py @@ -162,6 +162,7 @@ async def authorize_payment_gateway( # Start by assuming failure: success = False + message = "" # ┏┓ ┏ • ┳┳┓ ┏┓ ┏┓ # ┗┓┏┓╋┏┓┏┓┓┏┏┓┏┳┓ ┃┃┃━━┃┃┏┓┏┏┓ ┣ ┓┏┏┓┏┓┏┓┏┏ @@ -191,6 +192,31 @@ async def authorize_payment_gateway( session_token = inbound_headers["X-Session-Token"] ) + if inbound_data.client == "razorpay": + # Make the client controller test and save the auth: + response = await current_app.razorPay_controller.save_auth( + sql_conn=current_app.sql_writer, + mongo_data_conn=current_app.data_mongo, + auth=inbound_data.auth, + user=kwargs.get("session_info"), + session_token=inbound_headers.get("X-Session-Token") + ) + + # Note down the results: + success = response.success + message = response.message + token_id = response.token_id + + auth_url_success = "" + auth_url_failed = "" + + if token_id is not None: + auth_url_success = f"https://api.thecaoffice.com/shopify/razorpay/auth/template?status=1&token={token_id}&razorPayKeyId={inbound_data.auth.razorPayKeyId}&razorPayKeyId={inbound_data.auth.razorPayKeySecret}" + # auth_url_success = f"http://192.168.2.99:5220/shopify/razorpay/auth/template?status=1&token={token_id}&razorPayKeyId={inbound_data.auth.razorPayKeyId}&razorPayKeyId={inbound_data.auth.razorPayKeySecret}" + else: + auth_url_failed = f"https://api.thecaoffice.com/shopify/razorpay/auth/template?status=0&razorPayKeyId={inbound_data.auth.razorPayKeyId}&razorPayKeyId={inbound_data.auth.razorPayKeySecret}" + # auth_url_failed = f"http://192.168.2.99:5220/shopify/razorpay/auth/template?status=0&razorPayKeyId={inbound_data.auth.razorPayKeyId}&razorPayKeyId={inbound_data.auth.razorPayKeySecret}" + # ┳┓ # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ # ┛┗┗ ┛┣┛┗┛┛┗┛┗ @@ -198,11 +224,13 @@ async def authorize_payment_gateway( # Done here: return ResponseModel( - status_code = StatusCodes.OK if success else StatusCodes.FAILED, - http_code = HttpCodes.SUCCESS if success else HttpCodes.INTERNAL_SERVER_ERROR, - data = { + status_code=StatusCodes.OK if success else StatusCodes.FAILED, + http_code=HttpCodes.SUCCESS if success else HttpCodes.INTERNAL_SERVER_ERROR, + message=message, + data={ "client": inbound_data.client, - "authorized": success + "authorized": success, + "authorizationUrl": auth_url_success if success else auth_url_failed } ) diff --git a/api/main.py b/api/main.py index 3d0f6f9..90e025b 100644 --- a/api/main.py +++ b/api/main.py @@ -99,6 +99,7 @@ from controllers_v2.finstitutions.trading.all_trading import AllTradingControlle from controllers_v2.finstitutions.trading.zerodha_kite import ZerodhaKiteTradingController #from controllers_v2.finstitutions.trading.icici_breeze import ICICIBreezeTradingController from controllers_v2.finstitutions.trading.paper_trading import PaperTradingController +from controllers_v2.finstitutions.payments.razorpay.razorpay import RazorPayAppController # --- from controllers_v2.finstitutions.payments.all_payments import AllPaymentsController from controllers_v2.finstitutions.payments.safaricom_mpesa_express import SafaricomMPesaExpressPaymentsController @@ -602,6 +603,15 @@ async def app_startup(**kwargs): ) current_app.printer("Finstitutions/Payments (C) ready.") + + current_app.razorPay_controller = RazorPayAppController( + cache=current_app.module_cache, + http_client=current_app.http_client, + alert_url=current_app.script_data["alerts"]["url"], + debug=enable_debugging + ) + current_app.printer("Finstitutions/Payments (C) ready.") + # Software / Mikrotik: current_app.mikrotik_controller = AllMikroTikController( cache = current_app.module_cache, diff --git a/controllers_v2/finstitutions/payments/razorpay/__init__.py b/controllers_v2/finstitutions/payments/razorpay/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/controllers_v2/finstitutions/payments/razorpay/base.py b/controllers_v2/finstitutions/payments/razorpay/base.py new file mode 100644 index 0000000..e505752 --- /dev/null +++ b/controllers_v2/finstitutions/payments/razorpay/base.py @@ -0,0 +1,206 @@ +""" + + AUTHOR: + + Omkar Khandare + + DATE: + + Friday, 08th Aug., 2025. + + OBJECTIVE: + + To handle razorPay authentication. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# My async utils: +from utils_v2.string import json +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 +from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache + +# Controllers: +from controllers_v2.core.software import CoreSoftwareController + +# To make very controlled API calls: +from utils_v2.rest.controllers.async_base import AsyncREST +from utils_v2.rest.models.api_call import ApiResponse + +# Models: +from models.finstitutions.payment.auth import ( + RazorPayAuth, + RazorPayAuthResponse +) +from models.core.user import CoreUserInfoModel + +# To make HTTP requests: +import httpx + +# to work with MongoDB: +from bson.objectid import ObjectId + +# To make abstract classes: +from abc import ABC, abstractmethod + + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** CLASSES *** +# ***** **** +# ***************************************************************************************************************** + + +class RayzorpayController(CoreSoftwareController, ABC): + + # ┏┓┓ ┓┏ + # ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏ + # ┗┛┗┗┻┛┛ ┗┛┗┻┛ ┛ + + SERVICE_TYPE = "paymentGateway" + + # ┏┓ + # ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓ + # ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛ + + def __init__( + self, + cache: AsyncRedisCache = None, + http_client: httpx.AsyncClient = None, + alert_url: str = None, + base_filter: dict = None, + debug: bool = True, + debug_prefix: str = "RazorPay (C) | ", + debug_only_errors: bool = True + ): + + """ + This is the foundational controller for generate an authentication key for ecommerce integration, + To Validate user from ecommerce webhooks + :param cache: The object to use for caching results from database calls. + :param http_client: The HTTP client to use to make REST-ful API calls. + :param base_filter: The basic filter that will be applied to all fetching/updating queries. WARNING: THE BASE + FILTER WILL ALWAYS BE APPLIED AUTOMATICALLY. SET THIS UP WISELY. + :param debug: Whether, or not, you would like to print debugging messages: + :param debug_prefix: The prefix to print with the debugging messages. + :param debug_only_errors: Whether you would like to print only error messages or all messages. + :return: None. + """ + + # Prepare the combined base filter: + payment_filter = {} + for k, v in (base_filter or {}).items(): payment_filter[k] = v + payment_filter["serviceType"] = self.SERVICE_TYPE + + # Invoke the parent's constructor: + CoreSoftwareController.__init__( + self, + cache = cache, + alert_url = alert_url, + http_client = http_client, + base_filter = payment_filter, + debug = debug, + debug_prefix = debug_prefix, + debug_only_errors = debug_only_errors + ) + + # Init a variable in a parent: + self._service_type = self.SERVICE_TYPE + + # For controlled REST-ful calls: + self._rest = AsyncREST( + http_client = http_client, + debug = debug, + debug_prefix = debug_prefix, + debug_only_errors = debug_only_errors + ) + + # ┏┓ ┓ + # ┣┫┓┏╋┣┓ + # ┛┗┗┻┗┛┗ + + @abstractmethod + async def save_auth( + self, + sql_conn: AsyncMySQL, + mongo_data_conn: AsyncMongo, + auth: RazorPayAuth, + user: CoreUserInfoModel, + session_token: str + ) -> RazorPayAuthResponse: + + """ + Checks if a particular set of incoming credentials give access to a valid server and then stores the + credentials. + :param sql_conn: The database connection to use to perform this task. + :param mongo_data_conn: The database connection to use to perform this task. + :param auth: The set of credentials as received from the UI/API. + :return: A structured response to indicate what happened during authorization. + """ + + pass + + + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/controllers_v2/finstitutions/payments/razorpay/razorpay.py b/controllers_v2/finstitutions/payments/razorpay/razorpay.py new file mode 100644 index 0000000..b01297b --- /dev/null +++ b/controllers_v2/finstitutions/payments/razorpay/razorpay.py @@ -0,0 +1,210 @@ +""" + + AUTHOR: + + Omkar Khandare + + DATE: + + Monday, 11th Aug., 2025. + + OBJECTIVE: + + To handle all Razorpay service from one place. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +from abc import ABC + + + +sys.path.append(".") +sys.path.append("..") + +# My async utils: +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 +from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache +from utils_v2.logging.context import AsyncLoggerContext + +# Controllers: +from controllers_v2.finstitutions.payments.razorpay.base import RayzorpayController + +# Models: +from models.core.auth_token import CoreAuthTokenModel +from models.core.user import CoreUserInfoModel +from models.core.message import CoreMessageModel + +from models.finstitutions.payment.auth import ( + RazorPayAuth, + RazorPayAuthResponse +) +# Chat clients: +from utils_v2.whatsapp.nimbus.controllers.async_nimbus_whatsapp import AsyncNimbusWhatsapp + +# To work with datatypes: +from typing import List, Any + +# To make HTTP requests: +import httpx + +# For asynchronous activities: +import asyncio + +# Common: +from shared import constants + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** CLASSES *** +# ***** **** +# ***************************************************************************************************************** + + +class RazorPayAppController(RayzorpayController): + + # ┏┓┓ ┓┏ + # ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏ + # ┗┛┗┗┻┛┛ ┗┛┗┻┛ ┛ + + CLIENT_NAME = "razorpay" + + # ┏┓ + # ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓ + # ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛ + + def __init__( + self, + cache: AsyncRedisCache = None, + http_client: httpx.AsyncClient = None, + alert_url: str = None, + debug: bool = True, + debug_prefix: str = "RazorPay (C) | ", + debug_only_errors: bool = True + ): + + """ + This is the controller for RayzorPAY service. + :param cache: The object to use for caching results from database calls. + :param http_client: The HTTP client + :param debug: Whether, or not, you would like to print debugging messages: + :param debug_prefix: The prefix to print with the debugging messages. + :param debug_only_errors: Whether you would like to print only error messages or all messages. + :return: None. + """ + + # Invoke the parent's constructor: + super().__init__( + cache = cache, + alert_url = alert_url, + http_client = http_client, + base_filter = {"client": self.CLIENT_NAME}, + debug = debug, + debug_prefix = debug_prefix, + debug_only_errors = debug_only_errors + ) + + # Init a variable in a parent: + self._client = self.CLIENT_NAME + + async def save_auth( + self, + sql_conn: AsyncMySQL, + mongo_data_conn: AsyncMongo, + auth: RazorPayAuth, + user: CoreUserInfoModel, + session_token: str + ) -> RazorPayAuthResponse: + + success, object_id = await self.set_token_direct_with_return_id( + sql_conn=sql_conn, + mongo_data_conn=mongo_data_conn, + auth_token=CoreAuthTokenModel( + serviceType=self.SERVICE_TYPE, + client=self.CLIENT_NAME, + authType="auth", + auth=auth.model_dump(), + user=user, + clientUserId={ + "razorPayKeyId": auth.razorPayKeyId, + "razorPayKeySecret": auth.razorPayKeySecret + }, + status="active", + syncFreq=60 + ), + token_notes={ + "razorPayKeyId": auth.razorPayKeyId, + "razorPayKeySecret": auth.razorPayKeySecret + }, + display_name=auth.displayName, + display_picture=None, + session_token=session_token + ) + + # Done here: + return RazorPayAuthResponse( + success=success, + token_id=str(object_id), + message="RazorPay Account Added successfully." if success else "RazorPay Account Added failed." + ) + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/models/api/finstitutions/payments/auth.py b/models/api/finstitutions/payments/auth.py index 4c523df..cd0ef99 100644 --- a/models/api/finstitutions/payments/auth.py +++ b/models/api/finstitutions/payments/auth.py @@ -105,6 +105,34 @@ class SafaricomMPesaExpressAuth(BaseModel): class Config: extra = "forbid" +# ---------------------------------------------------------------------------------------------------------------------- + + +class RazorPayAuth(BaseModel): + + razorPayKeyId: str = Field( + description = "the app's consumer key given by razorpay; found in 'my apps'", + frozen = True + ) + + razorPayKeySecret: str = Field( + description = "the app's consumer secret given by razorpay; found in 'my apps'", + frozen = True + ) + + displayName: str = Field( + description="Display Name", + frozen=True + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + # --------------------------------------------------------------------------------------------------------------------- @@ -135,8 +163,8 @@ class PGAuthRequestHeaders(BaseModel): class PGAuthRequestData(BaseModel): - client: Literal["safaricomMPesaExpress"] = Field(alias = "client") - auth: Union[SafaricomMPesaExpressAuth] + client: Literal["safaricomMPesaExpress", "razorpay"] = Field(alias = "client") + auth: Union[SafaricomMPesaExpressAuth, RazorPayAuth] # ┏┓ ┏• # ┃ ┏┓┏┓╋┓┏┓ @@ -155,7 +183,8 @@ class PGAuthRequestData(BaseModel): client = values.client auth = values.auth harmony_map = { - "safaricomMPesaExpress": SafaricomMPesaExpressAuth + "safaricomMPesaExpress": SafaricomMPesaExpressAuth, + "razorpay": RazorPayAuth } if not isinstance(auth, harmony_map[client]): raise ValueError(f"incorrect 'auth' for selected client '{client}'") diff --git a/models/finstitutions/payment/__init__.py b/models/finstitutions/payment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/finstitutions/payment/auth.py b/models/finstitutions/payment/auth.py new file mode 100644 index 0000000..a813b58 --- /dev/null +++ b/models/finstitutions/payment/auth.py @@ -0,0 +1,168 @@ +""" + + AUTHOR: + + Omkar Khandare + + DATE: + + Monday, 11th Aug., 2025. + + OBJECTIVE: + + To provide a structure to receive auth details for razorpay + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# For making data behaviour_models: +from pydantic import BaseModel, Field, field_validator, PastDatetime, model_validator +from typing import Optional, Literal, Union, Any, List + + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# RegEx Patterns: +REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + +# Not Yet -- + +# ***************************************************************************************************************** +# ***** **** +# *** CLASSES *** +# ***** **** +# ***************************************************************************************************************** + + +class RazorPayAuth(BaseModel): + + razorPayKeyId: str = Field( + description = "Razor Pay key id", + frozen = True + ) + + razorPayKeySecret: str = Field( + description = "Razor Pay Secrets", + frozen = True + ) + + displayName: str = Field( + description="Razor Pay Display Name", + default=None + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# --------------------------------------------------------------------------------------------------------------------- + + +# --------------------------------------------------------------------------------------------------------------------- + + +class RazorPayAuthResponse(BaseModel): + + success: bool = Field( + description = "To indicate whether or not, the action was a success", + frozen = False, + default = False + ) + + token_id : str = Field( + description="MongoDb object id", + frozen=False, + default=False + ) + + message: str = Field( + description = "To explain what happened in the process of handling the OAuth callback.", + frozen = False, + default = "ERR: Message not captured." + ) + + exception: Any = Field( + description = "To pass on any exception that occurred in the process.", + frozen = False, + default = None + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + # ┏┓ ┏┓ + # ┃ ┓┏┏╋┏┓┏┳┓ ┣ ┓┏┏┓┏┏ + # ┗┛┗┻┛┗┗┛┛┗┗ ┻ ┗┻┛┗┗┛ + + pass + + # ┓┏ ┓• ┓ • + # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ + # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ + + pass + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/playground/test_oauth_places.py b/playground/test_oauth_places.py new file mode 100644 index 0000000..cbfa0a6 --- /dev/null +++ b/playground/test_oauth_places.py @@ -0,0 +1,65 @@ +from utils_v2.goog.controllers.base import AsyncGoogleBase +import httpx +import asyncio +from utils_v2.string import json + + +# Create an HTTP client: +test_client = httpx.AsyncClient( + limits = httpx.Limits( + max_connections = 100, # ............ Maximum number of connections allowed in the pool. + max_keepalive_connections = 50, # ... Maximum number of connections that can be kept alive. + ), + timeout = httpx.Timeout( + connect = 2.5, # ... Shorter connection timeout. + read = 2.5, # ...... Like what EasyEcom gives. + write = 10.0, # .... Time to wait for sending data. + pool = 120.0 # ..... Time to wait for a free connection from the pool. + ) +) + +# Read the secrets that give you access to the app: +secrets_file = r"/home/python-dev-debug/Downloads/client_secret_349360248417-uuba8eudk75jg1jag212g5obhc1uostk.apps.googleusercontent.com.json" +secrets_dict = json.from_file(secrets_file) + +PLACES_SCOPES = ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/userinfo.profile"] + +async def main(): + + # Create an instance of the client: + my_places = AsyncGoogleBase( + service_name = "places", + oauth_json = secrets_dict, + http_client = test_client, + # redirect_url = r"https://api.thecaoffice.com/converse/software/callback/places", + redirect_url = r"https://wtt.ditscentre.in/shopify/test/1", + debug = True, + debug_prefix = "places (M) | ", + debug_only_errors = False + ) + + # Request Auth: + print("AUTH URL:", await my_places.get_authorization_url( + scopes = PLACES_SCOPES, + state = "Sundar", + approval_prompt = "force" + )) + + # Get tokens from callback: + test_tokens = await my_places.get_authorization_tokens( + scopes = PLACES_SCOPES, + redirect_url = input("Paste the redirect URL here: ") + ) + print("TOKENS:", test_tokens) + + # Test some feature: + # response = await my_gmail.get_user_profile(tokens = test_tokens) + # print("SUCCESS:", response.success) + # print("SUMMARY:", response.to_markdown()) + # print("\n\n---\n\n") + # print("DATA:", json.to_string(response.data, default = str)) + # if not response.success: + # print("\n\n---\n\n") + + +asyncio.run(main()) \ No newline at end of file