From 85cc3307110524ef9916b874342e958bb42bf942 Mon Sep 17 00:00:00 2001 From: khushal Date: Wed, 18 Dec 2024 09:59:14 +0530 Subject: [PATCH] (20241218) Getting full individual payment records is now possible. --- api/blueprints/finstitutions/payments/get.py | 223 +++++++++++++++++++ api/main.py | 2 + controllers/api/payment.py | 16 -- controllers/core/payment.py | 40 +--- models/api/finstitutions/payments/get.py | 127 +++++++++++ models/core/payment.py | 3 +- utils_v2/telegram/controllers/async_bot.py | 9 +- 7 files changed, 360 insertions(+), 60 deletions(-) create mode 100644 api/blueprints/finstitutions/payments/get.py create mode 100644 models/api/finstitutions/payments/get.py diff --git a/api/blueprints/finstitutions/payments/get.py b/api/blueprints/finstitutions/payments/get.py new file mode 100644 index 0000000..4083712 --- /dev/null +++ b/api/blueprints/finstitutions/payments/get.py @@ -0,0 +1,223 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Wednesday, 18th Dec., 2024 + + OBJECTIVE: + + To get one full payment record for any given user for any given account. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + + NOTES: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# For using Quart: +from quart import Blueprint, current_app, g, request + +# My utils: +from utils_v2.string import json +from utils_v2.database.async_mongo_v2 import AsyncMongo +from utils_v2.api.codes import StatusCodes, HttpCodes +from utils_v2.api.response import ResponseModel +from utils_v2.api.async_quart import ( + make_ordered_json, + set_api_version, + read_input, + get_session_info, + log_request_to_mongo, + log_chain_to_mongo, + should_not_be_under_maintenance, + only_whitelisted_ips, + limit_rate, + validate_input, + handle_cancelled_request +) + +# GMail-related utils: +from utils_v2.goog.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT +from utils_v2.goog.models.auth_tokens import GoogleAuthTokens + +# Common: +from shared import constants + +# Data Models: +from models.api.finstitutions.payments.get import PGGetPaymentHeaders, PGGetPaymentData +from models.core.user import CoreUserInfoModel + +# To work with datatypes: +from typing import Literal + +# For asynchronous activities: +import asyncio + +# To work with date and time: +import datetime + +# Helpers: +from api.helpers.user import token_check + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# Related to Quart: +pg_get_bp = Blueprint("pg_get", __name__) + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +@pg_get_bp.record_once +def init(blueprint_setup_state): + + # This gets called when the blueprint is registered. + # Consider this to be a one-time setup for the whole blueprint: + pass + + +# --------------------------------------------------------------------------------------------------------------------- + + +@pg_get_bp.route("", methods = ["GET"]) +@set_api_version(api_version = "1.0.0") +@read_input(sanitize_headers = False, sanitize_data = False) +@get_session_info(key = "X-Session-Token", session_coro = "get_session") +@log_request_to_mongo( + attr_name = "logs_mongo", + project = constants.PROJECT_NAME, + log_type = constants.MODULE_NAME, + operation = "pymntGetApi", + log_input = True, + log_output = True, + sensitive_keys = ["sessionToken", "X-Session-Token"] +) +@log_chain_to_mongo(attr_name = "logs_mongo") +@should_not_be_under_maintenance(attr_name = "is_under_maintenance") +@validate_input( + header_validator = lambda x: PGGetPaymentHeaders(**x).model_dump(), + data_validator = lambda x: PGGetPaymentData(**x) +) +@handle_cancelled_request() +async def get_one_payment_record( + inbound_headers: dict | PGGetPaymentHeaders = None, + inbound_data: dict | PGGetPaymentData = None, + inbound_files: dict = None, + **kwargs +): + + """ + Use this endpoint when the user wants to fetch one payment's full record. + :param inbound_headers: auto-extracted by the decorators. + :param inbound_data: auto-extracted by the decorators. + :param inbound_files: auto-extracted by the decorators. + :param kwargs: Any number of extra inputs supplied by the decorators. + :return: A standard response structure. + """ + + # ┏┓ ┓ ┏┓┓ ┓ + # ┣┫┓┏╋┣┓ ┃ ┣┓┏┓┏┃┏ + # ┛┗┗┻┗┛┗ ┗┛┛┗┗ ┗┛┗ + + # If the session token is invalid/expired: + if kwargs.get("session_info") is None: + return ResponseModel( + status_code = StatusCodes.FAILED, + http_code = HttpCodes.UNAUTHORIZED, + message = "Invalid session." + ) + + # ┏┓ ┓ ┳┓ ┓ + # ┣ ┏┓╋┏┣┓ ┣┫┏┓┏┏┓┏┓┏┫ + # ┻ ┗ ┗┗┛┗ ┛┗┗ ┗┗┛┛ ┗┻ + + # Get the payment record: + record = await current_app.payment_controller.get_payment( + mongo_conn = current_app.data_mongo, + payment_id = inbound_data.paymentId + ) + + # ┏┓ ┓ • ┏┓┓ ┓ + # ┃┃┓┏┏┏┓┏┓┏┓┏┣┓┓┏┓ ┃ ┣┓┏┓┏┃┏ + # ┗┛┗┻┛┛┗┗ ┛ ┛┛┗┗┣┛ ┗┛┛┗┗ ┗┛┗ + # ┛ + + # We check if the token that was used to fetch the mail is owned by this user: + if not await token_check.is_authorized( + mongo_conn = current_app.data_mongo, + user_info = CoreUserInfoModel(**kwargs["session_info"]), + token_ids = [record.tokenId] + ): return ResponseModel( + status_code = StatusCodes.FAILED, + http_code = HttpCodes.UNAUTHORIZED, + message = "The message does not belong to this user." + ) + + # ┳┓ + # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ + # ┛┗┗ ┛┣┛┗┛┛┗┛┗ + # ┛ + + # Done here: + return ResponseModel( + status_code = StatusCodes.OK if record else StatusCodes.FAILED, + http_code = HttpCodes.SUCCESS if record else HttpCodes.NOT_FOUND, + data = record.full + ) + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/api/main.py b/api/main.py index d03bc2c..f23a801 100644 --- a/api/main.py +++ b/api/main.py @@ -103,6 +103,7 @@ from api.blueprints.finstitutions.payments.auth import pg_auth_bp from api.blueprints.finstitutions.payments.request import pg_request_bp from api.blueprints.finstitutions.payments.callback import pg_callback_bp from api.blueprints.finstitutions.payments.list import pg_list_bp +from api.blueprints.finstitutions.payments.get import pg_get_bp # AI Blueprints: from api.blueprints.ai.llm.invoke import llm_invoke_bp @@ -162,6 +163,7 @@ app.register_blueprint(pg_auth_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/p app.register_blueprint(pg_request_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/payments") app.register_blueprint(pg_callback_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/payments") app.register_blueprint(pg_list_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/payments") +app.register_blueprint(pg_get_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/payments") # AI Blueprints: app.register_blueprint(llm_invoke_bp, url_prefix = f"/{MODULE_BASE}/ai") diff --git a/controllers/api/payment.py b/controllers/api/payment.py index 63ec6e5..4702648 100644 --- a/controllers/api/payment.py +++ b/controllers/api/payment.py @@ -351,25 +351,11 @@ class PaymentController: @staticmethod async def get_payment( mongo_conn: AsyncMongo, - token_id: ObjectId | str, payment_id: ObjectId | str ) -> CorePaymentModel | None: # Simply call the core model: return await current_app.core_payment_controller.get_payment( - mongo_conn = mongo_conn, - token_id = token_id, - payment_id = payment_id - ) - - @staticmethod - async def get_payment_internal( - mongo_conn: AsyncMongo, - payment_id: ObjectId | str - ) -> CorePaymentModel | None: - - # Simply call the core model: - return await current_app.core_payment_controller.get_payment_internal( mongo_conn = mongo_conn, payment_id = payment_id ) @@ -412,7 +398,6 @@ class PaymentController: @staticmethod async def update_tags( mongo_conn: AsyncMongo, - token_id: ObjectId | str, payment_id: ObjectId | str, unset_tags: List[str] = None, set_tags: List[str] = None @@ -421,7 +406,6 @@ class PaymentController: # Simply call the core model: return await current_app.core_payment_controller.update_tags( mongo_conn = mongo_conn, - token_id = token_id, payment_id = payment_id, unset_tags = unset_tags, set_tags = set_tags diff --git a/controllers/core/payment.py b/controllers/core/payment.py index 6cbea46..c4dbb05 100644 --- a/controllers/core/payment.py +++ b/controllers/core/payment.py @@ -228,45 +228,12 @@ class CorePaymentController(BaseModel): async def get_payment( self, mongo_conn: AsyncMongo, - token_id: ObjectId | str, payment_id: ObjectId | str, ) -> CorePaymentModel | None: """ 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 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.PAYMENTS_COLLECTION, - filter = { - "_id": ObjectId(payment_id), - "tokenId": ObjectId(token_id) - }, - raise_exception = True - ) - - # If no such message was found: - if record is None: return None - - # If a record was found, - # we return it as our data model: - return CorePaymentModel(**record) - - async def get_payment_internal( - self, - mongo_conn: AsyncMongo, - payment_id: ObjectId | str, - ) -> CorePaymentModel | None: - - """ - NOTE: DO NOT USE THIS IN USER-FACING APIS. USE THIS INTERNALLY TO FETCH RECORDS. - 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 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. """ @@ -374,7 +341,6 @@ class CorePaymentController(BaseModel): async def update_tags( self, mongo_conn: AsyncMongo, - token_id: ObjectId | str, payment_id: ObjectId | str, unset_tags: List[str] = None, set_tags: List[str] = None @@ -383,7 +349,6 @@ class CorePaymentController(BaseModel): """ 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 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. @@ -393,10 +358,7 @@ class CorePaymentController(BaseModel): # Update the tags: return await mongo_conn.update_one( collection = self.PAYMENTS_COLLECTION, - filter = { - "_id": ObjectId(payment_id), - "tokenId": ObjectId(token_id) - }, + filter = {"_id": ObjectId(payment_id)}, update = [{ "$set": { "tags": { diff --git a/models/api/finstitutions/payments/get.py b/models/api/finstitutions/payments/get.py new file mode 100644 index 0000000..b563daa --- /dev/null +++ b/models/api/finstitutions/payments/get.py @@ -0,0 +1,127 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Wednesday, 18th Dec., 2024. + + OBJECTIVE: + + To provide a structure to query the full payload of a payment record. + + 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 +from typing import Optional, Literal + +# My utils: +from utils_v2.string import regex +from utils_v2.date_time import date_time + +# To work with date and time: +import datetime + + +# ***************************************************************************************************************** +# ***** **** +# *** 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 *** +# ***** **** +# ***************************************************************************************************************** + + +class PGGetPaymentHeaders(BaseModel): + + sessionToken: str = Field( + description = "the session token of the user who is requesting the service", + pattern = REGEX_SESSION_TOKEN, + frozen = True, + alias = "X-Session-Token" + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "allow" + + def model_dump(self, *args, **kwargs): + return super().model_dump(*args, by_alias = True, **kwargs) + + +# --------------------------------------------------------------------------------------------------------------------- + + +class PGGetPaymentData(BaseModel): + + paymentId: str = Field( + description = "the identifier (Mongo ObjectId) of the document that holds the payment record", + frozen = True + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/models/core/payment.py b/models/core/payment.py index 21b07a4..cfb8197 100644 --- a/models/core/payment.py +++ b/models/core/payment.py @@ -339,7 +339,8 @@ class CorePaymentModel(BaseModel): payment_json["events"].append({ "eventTs": e.eventTs, "paymentStatus": e.paymentStatus, - "initByPG": e.initByPG + "initByPG": e.initByPG, + "message": e.message }) return payment_json diff --git a/utils_v2/telegram/controllers/async_bot.py b/utils_v2/telegram/controllers/async_bot.py index 51c4bb6..12d7741 100644 --- a/utils_v2/telegram/controllers/async_bot.py +++ b/utils_v2/telegram/controllers/async_bot.py @@ -45,8 +45,8 @@ from utils_v2.system import files from utils_v2.date_time import date_time # Data models: -from utils_v2.telegram.models.data.api_call import TelegramApiResponse -from utils_v2.telegram.models.data.update import TelegramUpdate +from utils_v2.telegram.models.api_call import TelegramApiResponse +from utils_v2.telegram.models.update import TelegramUpdate # To make API calls: import httpx @@ -628,7 +628,8 @@ if __name__ == "__main__": # Create the instance and define needed variables: my_tg = AsyncTelegramBot(bot_token = r"7003670393:AAH9qF6XGqa-u2_TM2JCUkP0Fp48kMw8Ka8") - recipient_chat_id = 1275560043 + # recipient_chat_id = 1275560043 + recipient_chat_id = 7501974519 # Test out the services: # tg_response = await my_tg.bot_info() @@ -647,7 +648,7 @@ if __name__ == "__main__": file = r"https://images.unsplash.com/photo-1543852786-1cf6624b9987", caption = f"[Image URL]({my_tg.escape_special_chars(text = r'https://images.unsplash.com/photo-1543852786-1cf6624b9987', parse_mode = 'MarkdownV2')})", parse_mode = "MarkdownV2", - hide = False + hide = True ) # Show the response: