(20241218) Getting full individual payment records is now possible.

This commit is contained in:
2024-12-18 09:59:14 +05:30
parent bfe7b1a78b
commit 85cc330711
7 changed files with 360 additions and 60 deletions
@@ -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
+2
View File
@@ -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.request import pg_request_bp
from api.blueprints.finstitutions.payments.callback import pg_callback_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.list import pg_list_bp
from api.blueprints.finstitutions.payments.get import pg_get_bp
# AI Blueprints: # AI Blueprints:
from api.blueprints.ai.llm.invoke import llm_invoke_bp 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_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_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_list_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/payments")
app.register_blueprint(pg_get_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/payments")
# AI Blueprints: # AI Blueprints:
app.register_blueprint(llm_invoke_bp, url_prefix = f"/{MODULE_BASE}/ai") app.register_blueprint(llm_invoke_bp, url_prefix = f"/{MODULE_BASE}/ai")
-16
View File
@@ -351,25 +351,11 @@ class PaymentController:
@staticmethod @staticmethod
async def get_payment( async def get_payment(
mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
token_id: ObjectId | str,
payment_id: ObjectId | str payment_id: ObjectId | str
) -> CorePaymentModel | None: ) -> CorePaymentModel | None:
# Simply call the core model: # Simply call the core model:
return await current_app.core_payment_controller.get_payment( 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, mongo_conn = mongo_conn,
payment_id = payment_id payment_id = payment_id
) )
@@ -412,7 +398,6 @@ class PaymentController:
@staticmethod @staticmethod
async def update_tags( async def update_tags(
mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
token_id: ObjectId | str,
payment_id: ObjectId | str, payment_id: ObjectId | str,
unset_tags: List[str] = None, unset_tags: List[str] = None,
set_tags: List[str] = None set_tags: List[str] = None
@@ -421,7 +406,6 @@ class PaymentController:
# Simply call the core model: # Simply call the core model:
return await current_app.core_payment_controller.update_tags( return await current_app.core_payment_controller.update_tags(
mongo_conn = mongo_conn, mongo_conn = mongo_conn,
token_id = token_id,
payment_id = payment_id, payment_id = payment_id,
unset_tags = unset_tags, unset_tags = unset_tags,
set_tags = set_tags set_tags = set_tags
+1 -39
View File
@@ -228,45 +228,12 @@ class CorePaymentController(BaseModel):
async def get_payment( async def get_payment(
self, self,
mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
token_id: ObjectId | str,
payment_id: ObjectId | str, payment_id: ObjectId | str,
) -> CorePaymentModel | None: ) -> CorePaymentModel | None:
""" """
Gets one payment detail if you know its payment id. Gets one payment detail if you know its payment id.
:param mongo_conn: The instance of the database connector to use for the operation. :param mongo_conn: The instance of the database connector to use for the operation.
:param token_id: The id of the auth-token associated with the 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. :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. :return: The contents of that one payment detail in a structured format.
""" """
@@ -374,7 +341,6 @@ class CorePaymentController(BaseModel):
async def update_tags( async def update_tags(
self, self,
mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
token_id: ObjectId | str,
payment_id: ObjectId | str, payment_id: ObjectId | str,
unset_tags: List[str] = None, unset_tags: List[str] = None,
set_tags: List[str] = None set_tags: List[str] = None
@@ -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. Updates the tags on one payment. The tags to remove are processed first, the ones to add are processed later.
:param mongo_conn: The instance of the database connector to use for the operation. :param mongo_conn: The instance of the database connector to use for the operation.
:param token_id: The id of the auth-token associated with the payment. Needed for security.
:param payment_id: The id of the payment detail that needs to be read. :param payment_id: The id of the payment detail that needs to be read.
:param unset_tags: The tags to remove from the payment record. :param unset_tags: The tags to remove from the payment record.
:param set_tags: The tags to add to the payment record. :param set_tags: The tags to add to the payment record.
@@ -393,10 +358,7 @@ class CorePaymentController(BaseModel):
# Update the tags: # Update the tags:
return await mongo_conn.update_one( return await mongo_conn.update_one(
collection = self.PAYMENTS_COLLECTION, collection = self.PAYMENTS_COLLECTION,
filter = { filter = {"_id": ObjectId(payment_id)},
"_id": ObjectId(payment_id),
"tokenId": ObjectId(token_id)
},
update = [{ update = [{
"$set": { "$set": {
"tags": { "tags": {
+127
View File
@@ -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
+2 -1
View File
@@ -339,7 +339,8 @@ class CorePaymentModel(BaseModel):
payment_json["events"].append({ payment_json["events"].append({
"eventTs": e.eventTs, "eventTs": e.eventTs,
"paymentStatus": e.paymentStatus, "paymentStatus": e.paymentStatus,
"initByPG": e.initByPG "initByPG": e.initByPG,
"message": e.message
}) })
return payment_json return payment_json
+5 -4
View File
@@ -45,8 +45,8 @@ from utils_v2.system import files
from utils_v2.date_time import date_time from utils_v2.date_time import date_time
# Data models: # Data models:
from utils_v2.telegram.models.data.api_call import TelegramApiResponse from utils_v2.telegram.models.api_call import TelegramApiResponse
from utils_v2.telegram.models.data.update import TelegramUpdate from utils_v2.telegram.models.update import TelegramUpdate
# To make API calls: # To make API calls:
import httpx import httpx
@@ -628,7 +628,8 @@ if __name__ == "__main__":
# Create the instance and define needed variables: # Create the instance and define needed variables:
my_tg = AsyncTelegramBot(bot_token = r"7003670393:AAH9qF6XGqa-u2_TM2JCUkP0Fp48kMw8Ka8") 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: # Test out the services:
# tg_response = await my_tg.bot_info() # tg_response = await my_tg.bot_info()
@@ -647,7 +648,7 @@ if __name__ == "__main__":
file = r"https://images.unsplash.com/photo-1543852786-1cf6624b9987", 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')})", caption = f"[Image URL]({my_tg.escape_special_chars(text = r'https://images.unsplash.com/photo-1543852786-1cf6624b9987', parse_mode = 'MarkdownV2')})",
parse_mode = "MarkdownV2", parse_mode = "MarkdownV2",
hide = False hide = True
) )
# Show the response: # Show the response: