From c340ebcba5cb8e543ea8c221b8582de302c3da46 Mon Sep 17 00:00:00 2001 From: khushal Date: Wed, 18 Dec 2024 10:24:42 +0530 Subject: [PATCH] (20241218) Can now send tags to apply with the payment request itself, and can even update tags later. --- api/blueprints/finstitutions/payments/get.py | 2 +- api/blueprints/finstitutions/payments/tags.py | 234 ++++++++++++++++++ api/blueprints/mail/tags/update.py | 2 +- api/main.py | 2 + controllers/api/payment.py | 2 +- models/api/finstitutions/payments/request.py | 11 +- models/api/finstitutions/payments/tags.py | 139 +++++++++++ 7 files changed, 388 insertions(+), 4 deletions(-) create mode 100644 api/blueprints/finstitutions/payments/tags.py create mode 100644 models/api/finstitutions/payments/tags.py diff --git a/api/blueprints/finstitutions/payments/get.py b/api/blueprints/finstitutions/payments/get.py index 4083712..4a1def2 100644 --- a/api/blueprints/finstitutions/payments/get.py +++ b/api/blueprints/finstitutions/payments/get.py @@ -195,7 +195,7 @@ async def get_one_payment_record( ): return ResponseModel( status_code = StatusCodes.FAILED, http_code = HttpCodes.UNAUTHORIZED, - message = "The message does not belong to this user." + message = "The record does not belong to this user." ) # ┳┓ diff --git a/api/blueprints/finstitutions/payments/tags.py b/api/blueprints/finstitutions/payments/tags.py new file mode 100644 index 0000000..c502c58 --- /dev/null +++ b/api/blueprints/finstitutions/payments/tags.py @@ -0,0 +1,234 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Wednesday, 18th Dec., 2024 + + OBJECTIVE: + + To update the tags on one payment record. + + 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.tags import PGPaymentUpdateTagsRequestHeaders, PGPaymentUpdateTagsRequestData +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_tags_update_bp = Blueprint("pg_tags_update", __name__) + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +@pg_tags_update_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_tags_update_bp.route("/tags", methods = ["PATCH"]) +@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 = "pgTagsUpdtApi", + 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: PGPaymentUpdateTagsRequestHeaders(**x).model_dump(), + data_validator = lambda x: PGPaymentUpdateTagsRequestData(**x) +) +@handle_cancelled_request() +async def update_payment_record_tags( + inbound_headers: dict | PGPaymentUpdateTagsRequestHeaders = None, + inbound_data: dict | PGPaymentUpdateTagsRequestData = None, + inbound_files: dict = None, + **kwargs +): + + """ + Use this endpoint to update the tags on an payment 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 + ) + + # ┏┓ ┓ ┳┓ ┓ + # ┣ ┏┓╋┏┣┓ ┣┫┏┓┏┏┓┏┓┏┫ + # ┻ ┗ ┗┗┛┗ ┛┗┗ ┗┗┛┛ ┗┻ + + # 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 record does not belong to this user." + ) + + # ┳┳ ┓ ┳┓ ┓ + # ┃┃┏┓┏┫┏┓╋┏┓ ┣┫┏┓┏┏┓┏┓┏┫ + # ┗┛┣┛┗┻┗┻┗┗ ┛┗┗ ┗┗┛┛ ┗┻ + # ┛ + + # Update the record: + success = await current_app.payment_controller.update_tags( + mongo_conn = current_app.data_mongo, + payment_id = inbound_data.paymentId, + unset_tags = inbound_data.unsetTags, + set_tags = inbound_data.setTags + ) + + # ┳┓ + # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ + # ┛┗┗ ┛┣┛┗┛┛┗┛┗ + # ┛ + + # Done here: + return ResponseModel( + status_code = StatusCodes.OK if success else StatusCodes.FAILED, + http_code = HttpCodes.SUCCESS if success else HttpCodes.INTERNAL_SERVER_ERROR + ) + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/api/blueprints/mail/tags/update.py b/api/blueprints/mail/tags/update.py index 21fb068..6e56900 100644 --- a/api/blueprints/mail/tags/update.py +++ b/api/blueprints/mail/tags/update.py @@ -36,8 +36,8 @@ # To make sibling directories accessible for imports: import sys +sys.path.append(".") sys.path.append("..") -sys.path.append("../..") # For using Quart: from quart import Blueprint, current_app, g, request diff --git a/api/main.py b/api/main.py index f23a801..1669546 100644 --- a/api/main.py +++ b/api/main.py @@ -104,6 +104,7 @@ 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 +from api.blueprints.finstitutions.payments.tags import pg_tags_update_bp # AI Blueprints: from api.blueprints.ai.llm.invoke import llm_invoke_bp @@ -164,6 +165,7 @@ app.register_blueprint(pg_request_bp, url_prefix = f"/{MODULE_BASE}/finstitution 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") +app.register_blueprint(pg_tags_update_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 4702648..68c010e 100644 --- a/controllers/api/payment.py +++ b/controllers/api/payment.py @@ -288,7 +288,7 @@ class PaymentController: amount = payment_request.amount, currencyCode = payment_request.currencyCode, metadata = payment_request.metadata.model_dump(), - tags = ["payment", "safaricom", "mPesaExpress", "kenya"], + tags = list(set(payment_request.tags + ["Payment", "Safaricom", "M-Pesa Express", "Kenya"])), serviceType = "paymentGateway", client = auth_token.client, clientPaymentReferenceId = None, diff --git a/models/api/finstitutions/payments/request.py b/models/api/finstitutions/payments/request.py index ecf4edf..8bdd1f7 100644 --- a/models/api/finstitutions/payments/request.py +++ b/models/api/finstitutions/payments/request.py @@ -37,7 +37,7 @@ sys.path.append("..") # For making data behaviour_models: from pydantic import BaseModel, Field, field_validator, PastDatetime -from typing import Optional, Literal, Union +from typing import Optional, Literal, Union, List, Any # My utils: from utils_v2.string import regex @@ -188,6 +188,11 @@ class PGPaymentRequestData(BaseModel): frozen = True ) + tags: List[Any] = Field( + description = "any no. of tags to apply to this payment record to make search and filtering easy later", + default = None + ) + # ┏┓ ┏• # ┃ ┏┓┏┓╋┓┏┓ # ┗┛┗┛┛┗┛┗┗┫ @@ -213,6 +218,10 @@ class PGPaymentRequestData(BaseModel): if currency is None: raise ValueError("invalid currency code, please use iso 4217 standard") return value + @field_validator("tags", mode = "before") + def null_to_empty_list(cls, value): + return [] if value is None else value + # --------------------------------------------------------------------------------------------------------------------- diff --git a/models/api/finstitutions/payments/tags.py b/models/api/finstitutions/payments/tags.py new file mode 100644 index 0000000..fc7dc29 --- /dev/null +++ b/models/api/finstitutions/payments/tags.py @@ -0,0 +1,139 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Wednesday, 18th Dec., 2024. + + OBJECTIVE: + + To provide a structure to work with the tags on payment records. + + 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, List, Any + +# 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 PGPaymentUpdateTagsRequestHeaders(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 PGPaymentUpdateTagsRequestData(BaseModel): + + paymentId: str = Field( + description = "the identifier (Mongo ObjectId) of the document that holds the payment record", + frozen = True + ) + + unsetTags: List[Any] | None = Field( + description = "the list of tags to remove from the mail", + frozen = True, + default = None + ) + + setTags: List[Any] | None = Field( + description = "the list of tags to add to the mail", + frozen = True, + default = None + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass