""" AUTHOR: Khushal P Soonderji DATE: Monday, 16th Dec., 2024 OBJECTIVE: To handle all payments from one place. REFERENCES: N/A DOWNLOADS: N/A """ # ***************************************************************************************************************** # ***** **** # *** IMPORT *** # ***** **** # ***************************************************************************************************************** # To make sibling directories accessible for imports: import sys sys.path.append(".") sys.path.append("..") # For Quart: from quart import current_app # 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, AsyncMongoStorage # Base model: from controllers.base import BaseModel # Data models: from models.core.auth_token import CoreAuthTokenModel from models.core.payment import CorePaymentModel, PaymentEvent from models.core.user import CoreUserInfoModel # To work with MongoDB: from bson import ObjectId from pymongo import InsertOne, UpdateOne, ReplaceOne # To work with datatypes: from typing import Literal, List, Dict, Any # To make deep-copies: import copy # To work with base-64 encoding: import base64 # To work with date and time: import datetime # For asynchronous activities: import asyncio # ***************************************************************************************************************** # ***** **** # *** MACROS / ONE-TIME INIT *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** VARIABLES *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** FUNCTIONS *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** CLASSES *** # ***** **** # ***************************************************************************************************************** class CorePaymentController(BaseModel): # ┏┓┓ ┓┏ # ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏ # ┗┛┗┗┻┛┛ ┗┛┗┻┛ ┛ # For MongoDB: PAYMENTS_COLLECTION = "_payments" # ┏┓┳┓┳┳┳┓ ┏┓ # ┃ ┣┫┃┃┃┃ ━━ ┃ ┏┓┏┓┏┓╋┏┓ # ┗┛┛┗┗┛┻┛ ┗┛┛ ┗ ┗┻┗┗ async def init( self, mongo_conn: AsyncMongo, payment: CorePaymentModel ) -> ObjectId: """ Simply insert one payment document into the database. :param mongo_conn: The instance of the database connector to use for the operation. :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.PAYMENTS_COLLECTION, document = payment, raise_exception = True ) # ┏┓┳┓┳┳┳┓ ┳┓ • # ┃ ┣┫┃┃┃┃ ━━ ┣┫┏┓╋┏┓┓┏┓┓┏┏┓ # ┗┛┛┗┗┛┻┛ ┛┗┗ ┗┛ ┗┗ ┗┛┗ async def count_payments( self, mongo_conn: AsyncMongo, token_ids: List[ObjectId | str], additional_filter: dict = None ) -> int: """ 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 payment details must be fetched. :param additional_filter: Any addition filters to use. :return: The no. of payment records that match the given conditions. """ # 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 # Get the count of the documents that match the criteria: count = await mongo_conn.count( collection = self.PAYMENTS_COLLECTION, filter = filter_json, raise_exception = True ) # Done here: return count 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[CorePaymentModel] | None: """ 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 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 payments (as the payments 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.PAYMENTS_COLLECTION, filter = filter_json, projection = {"events": False}, limit = limit, skip = skip, sort = {"ts": -1}, raise_exception = True ) # Convert the fetched records to instances of the data model and return: for record in records: record["events"] = [] return [CorePaymentModel(**record) for record in records] 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) # ┏┓┳┓┳┳┳┓ ┳┳ ┓ # ┃ ┣┫┃┃┃┃ ━━ ┃┃┏┓┏┫┏┓╋┏┓ # ┗┛┛┗┗┛┻┛ ┗┛┣┛┗┻┗┻┗┗ # ┛ # 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, payment_id: ObjectId | str, unset_tags: List[str] = None, set_tags: List[str] = None ) -> bool: """ 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. :return: True if the update was successful, else False. """ # Update the tags: return await mongo_conn.update_one( collection = self.PAYMENTS_COLLECTION, filter = { "_id": ObjectId(payment_id), "tokenId": ObjectId(token_id) }, update = [{ "$set": { "tags": { "$let": { "vars": { "removed_tags": { "$setDifference": [ "$tags", unset_tags ] } }, "in": { "$setUnion": [ "$$removed_tags", set_tags ] } } } } }], raise_exception = True ) # ┏┓┳┓┳┳┳┓ ┳┓ ┓ # ┃ ┣┫┃┃┃┃ ━━ ┃┃┏┓┃┏┓╋┏┓ # ┗┛┛┗┗┛┻┛ ┻┛┗ ┗┗ ┗┗ # No support whatsoever for deleting messages. # ***************************************************************************************************************** # ***** **** # *** MAIN PROGRAM *** # ***** **** # ***************************************************************************************************************** if __name__ == "__main__": pass