diff --git a/core/infrastructure/plumbing.py b/core/infrastructure/plumbing.py index e9391da..07ed29c 100644 --- a/core/infrastructure/plumbing.py +++ b/core/infrastructure/plumbing.py @@ -149,7 +149,7 @@ def resolve_prescription(result: Result, ENUM_MAP: dict, *args, **kwargs): # Priority 2: Look up the Enum map logger.info("checking enum map") if result.error_type not in ENUM_MAP: - logger.info(f"No solution in the ENUM_MAP of {ENUM_MAP}") + # logger.info(f"No solution in the ENUM_MAP of {ENUM_MAP}") return None # No solution enum_value = ENUM_MAP[result.error_type] diff --git a/core/models/pydantic_models/ServerReply.py b/core/models/pydantic_models/ServerReply.py index 477476e..02fb387 100644 --- a/core/models/pydantic_models/ServerReply.py +++ b/core/models/pydantic_models/ServerReply.py @@ -34,7 +34,7 @@ class ServerReply(): message: Optional[str] = None model_version: int = 1 data: Optional[dict] = None - conf: str = None + conf: Optional[str] = None payment_status: Optional[bool] = None diff --git a/core/services/payment_phase/check_payment_loop.py b/core/services/payment_phase/check_payment_loop.py index b3475af..b28d4d0 100644 --- a/core/services/payment_phase/check_payment_loop.py +++ b/core/services/payment_phase/check_payment_loop.py @@ -112,23 +112,11 @@ class CheckPaidTimer: print("PAID! Stopping timer") self.stop_polling() - # Reset instance: + # Since paid, reset the instance (wipe it): CheckPaidTimer._instance = None marked = mark_as_paid(profile_id=self.profile_id) - # # SAVE INDIVIDUAL: - # if self.payment_type == PaymentType.CRYPTO: - # marked = mark_as_paid(profile_id=self.profile_id) - - # # SAVE TICKETS PAID: - # elif self.payment_type == PaymentType.TICKET: - # marked = mark_global_tickets_paid() - - # else: - # logger.error("Invalid payment type picked.") - # marked = Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Unsupported protocol.", billing_code=self.billing_code) - # Either way evaluate: if not marked.valid: message = marked.message @@ -173,3 +161,17 @@ class CheckPaidTimer: CheckPaidTimer._instance = None self.view_callback(paid=False, stopped=True, message=result.message, billing_code=self.billing_code) + + + +# # SAVE INDIVIDUAL: +# if self.payment_type == PaymentType.CRYPTO: +# marked = mark_as_paid(profile_id=self.profile_id) + +# # SAVE TICKETS PAID: +# elif self.payment_type == PaymentType.TICKET: +# marked = mark_global_tickets_paid() + +# else: +# logger.error("Invalid payment type picked.") +# marked = Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Unsupported protocol.", billing_code=self.billing_code) diff --git a/core/services/subscriptions/subscriptions_by_protocol.py b/core/services/subscriptions/subscriptions_by_protocol.py index 8bfda71..6f393bc 100644 --- a/core/services/subscriptions/subscriptions_by_protocol.py +++ b/core/services/subscriptions/subscriptions_by_protocol.py @@ -8,6 +8,7 @@ from core.observers.ConnectionObserver import ConnectionObserver from core.models.pydantic_models.type_checker import validate_model from core.models.pydantic_models.ServerReply import SubscriptionInvoice from core.models.Subscription import Subscription +from core.utils.type_checker import type_checker from core.Constants import Constants from core.models.Result import Result, ResultError from core.errors.logger import logger @@ -100,8 +101,8 @@ def error_about_mismatch(category: PaymentCategory) -> Result: return Result(valid=False, error_type=ResultError.ACTIVE_BILLING, message=error_msg) - -def evaluate_cycle(payment_cycle: OverallPaymentCycle, profile_id: int) -> Result: +@type_checker +def evaluate_repeat_cycle(payment_cycle: OverallPaymentCycle, profile_id: int) -> Result: if the_same_payment_cycle(payment_cycle=payment_cycle, profile_id=profile_id): logger.info("It's the same as before, returning the data") return Result(valid=True, data=payment_cycle.billing_data) @@ -111,104 +112,80 @@ def evaluate_cycle(payment_cycle: OverallPaymentCycle, profile_id: int) -> Resul return Result(valid=False, error_type=ResultError.ACTIVE_BILLING, message=error_msg) +@type_checker def start_new_payment_cycle(category: PaymentCategory, profile_id: Optional[int] = None) -> Result: - """ - Purpose: - Starts the overall payment cycle, and the individual category's billing data. - - On Conflict: - If there's already a cycle running, this function addresses that, - by returning the cycle if it's the same, or offering to end it if different. - """ - - # PRE-REQS: - if not profile_id and category == PaymentCategory.INDIVIDUAL: + # 1. Pre-requisites + if category == PaymentCategory.INDIVIDUAL and not profile_id: error_msg = "Need a profile id for an individual profile." logger.error(error_msg) return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg) - logger.info("Got pre-reqs for individual.") - # This is the singleton: payment_cycle = OverallPaymentCycle() - logger.info(f"payment_cycle is {payment_cycle}") - # ---------------------------- - # MISMATCH IN CATEGORY FROM WHAT EXISTS: - # ---------------------------- - if category != payment_cycle.category: - return error_about_mismatch(category) - logger.info("Made it past the mismatch check.") + # 2. Exact Match Check + # If a cycle is active, we first check if it is EXACTLY what the user is requesting. + if payment_cycle.active and payment_cycle.billing_data: + is_same_category = (payment_cycle.category == category) + + if is_same_category: + if category == PaymentCategory.TICKET: + # Return active ticket cycle even if billing_code is blank + logger.info("Existing active ticket cycle found. Returning it.") + return Result(valid=True, data=payment_cycle) + + if category == PaymentCategory.INDIVIDUAL: + # Let evaluate_repeat_cycle handle the profile_id comparison + # and the specific "same user" vs "different user" Result errors. + return evaluate_repeat_cycle(payment_cycle=payment_cycle, profile_id=profile_id) - # ---------------------------- - # ALREADY ACTIVE INDIVIDUAL CYCLE: - # ---------------------------- - if all([ # all are true: - payment_cycle.billing_data, - payment_cycle.active, - payment_cycle.category == PaymentCategory.INDIVIDUAL, - category == PaymentCategory.INDIVIDUAL - ]): - profile_id_already_running = payment_cycle.billing_data.instance_id - if profile_id_already_running: - logger.info(f"Triggering the evaluate_cycle reply because of {profile_id_already_running} running already") - return evaluate_cycle(payment_cycle=payment_cycle, profile_id=profile_id) + # 3. Collision Check (The "Prompt" Logic) + # If we reached here, the active cycle (if any) is NOT a match. + if payment_cycle.active: + # We return ACTIVE_BILLING so the UI can prompt the user to call wipe_billing_cycle() + error_msg = f"An active {payment_cycle.category} cycle is already running. Stop it first?" + logger.info(error_msg) + return Result(valid=False, error_type=ResultError.ACTIVE_BILLING, message=error_msg) - # ---------------------------- - # ALREADY POLLING TIMER: - # ---------------------------- + # 4. Stale Timer Check + # This handles cases where the cycle state is gone but the background thread is still humming. try: timer_loop = CheckPaidTimer.get_instance() if timer_loop.is_running: - error_msg = "There's already a billing cycle that's actively checking if paid. Do you want to stop that?" + error_msg = "A billing timer is still running in the background. Do you want to stop it?" return Result(valid=False, error_type=ResultError.ACTIVE_BILLING, message=error_msg) except Exception as e: - logger.info("All good, the timer isn't running, and we were afraid it was.") + logger.debug(f"Timer check skipped: {e}") - - logger.info("Process is NOT yet active, starting..") - payment_cycle.begin() + # 5. Initialization payment_cycle.category = category - - # ---------------------------- - # START INDIVIDUAL: - # ---------------------------- + if category == PaymentCategory.INDIVIDUAL: payment_cycle.profile_id = profile_id payment_cycle.billing_data = PaymentData(instance_id=profile_id) - subscription = get_profile_subscription(profile_id=profile_id, save_pending=True) # save to disk that it's pending - if subscription: - billing_code = subscription.billing_code - else: - billing_code = None - logger.info("Individual profile initial setup complete.") + subscription = get_profile_subscription(profile_id=profile_id, save_pending=True) + billing_code = subscription.billing_code if subscription else None + logger.info("Individual setup complete.") - # ---------------------------- - # START TICKETS: - # ---------------------------- elif category == PaymentCategory.TICKET: billing_data = TicketPaymentData() - payment_cycle.billing_data = billing_data # saves - billing_code = billing_data.billing_code # might be empty if new - logger.info("Ticketing initial setup complete.") - + payment_cycle.billing_data = billing_data + billing_code = billing_data.billing_code + logger.info("Ticket setup complete.") + else: - error_msg = f"Invalid PaymentCategory selected of {str(category)}" - logger.error(error_msg) - return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg) + return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Invalid Category") - # ---------------------------- - # COMMON FOR BOTH CATEGORIES: - # ---------------------------- + # 6. Activation payment_cycle.billing_code = billing_code - payment_cycle.active = True + payment_cycle.begin() - logger.info(f"Returning the entire payment_cycle") - return Result(valid=True, data=payment_cycle) - -# else: -# return Result(valid=False, error_type=ResultError.UNKNOWN, message="We failed to create new billing cycle data.") + if payment_cycle.billing_data: + return Result(valid=True, data=payment_cycle) + + return Result(valid=False, error_type=ResultError.UNKNOWN, message="Failed to create billing data.") +@type_checker def create_new_invoice(connection_observer: ConnectionObserver) -> Result: """Requires an active OverallPaymentCycle, that does NOT have an invoice already.""" @@ -230,19 +207,28 @@ def create_new_invoice(connection_observer: ConnectionObserver) -> Result: return create_individual_invoice(payment_cycle=payment_cycle, connection_observer=connection_observer) if category == PaymentCategory.TICKET: - return create_ticket_invoice(payment_cycle=payment_cycle, connection_observer=connection_observer) + return get_ticket_invoice(connection_observer=connection_observer) else: return not_supported() -def return_pre_existing_invoice(billing_code: str, connection_observer: ConnectionObserver) -> Result: +@type_checker +def return_pre_existing_invoice(billing_code: str, connection_observer: ConnectionObserver, category: PaymentCategory = PaymentCategory.INDIVIDUAL) -> Result: """This billing_code already got an invoice, but needs to fetch that data from the server again.""" logger.info("Fetching a pre-existing invoice..") - payload = { - "billing_code": billing_code - } + + if category == PaymentCategory.TICKET: + payload = { + "billing_code": billing_code, + "category": "tickets" + } + else: + payload = { + "billing_code": billing_code, + "category": "individual" + } #----------------------------- # SEND TO API @@ -253,9 +239,17 @@ def return_pre_existing_invoice(billing_code: str, connection_observer: Connecti return result -def create_ticket_invoice(payment_cycle: OverallPaymentCycle, connection_observer: ConnectionObserver) -> Result: - logger.info("Creating a new TICKET invoice..") +def get_ticket_invoice(connection_observer: ConnectionObserver) -> Result: + """Evaluates if it needs a new ticket invoice, and then returns pre-existing""" + + payment_cycle = OverallPaymentCycle() + + logger.info(f"Creating a TICKET invoice for the payment_cycle: {payment_cycle}") + + logger.info(f"payment_cycle is type {type(payment_cycle)}") + logger.info(f"connection_observer is type {type(connection_observer)}") + billing_data = payment_cycle.billing_data if not billing_data: @@ -266,7 +260,7 @@ def create_ticket_invoice(payment_cycle: OverallPaymentCycle, connection_observe billing_code = billing_data.billing_code if billing_code: logger.info("We already have a billing_code id for this ticket prep.") - return return_pre_existing_invoice(billing_code=billing_code, connection_observer=connection_observer) + return return_pre_existing_invoice(billing_code=billing_code, connection_observer=connection_observer, category=PaymentCategory.TICKET) #----------------------------- # MAKE PAYLOAD. @@ -275,6 +269,7 @@ def create_ticket_invoice(payment_cycle: OverallPaymentCycle, connection_observe # which_key = billing_data.which_key # currency = billing_data.currency + logger.info(f"Evaluating if billing_data is ready to prep a payload: {billing_data}") payload = billing_data.prep_payload() if not payload: @@ -305,6 +300,7 @@ def create_ticket_invoice(payment_cycle: OverallPaymentCycle, connection_observe return Result(valid=True, data=final_invoice) + def create_individual_invoice(payment_cycle: OverallPaymentCycle, connection_observer: ConnectionObserver) -> Result: """Prepare the payment data payload and send it to the API. Returns an invoice nested inside Result's data""" @@ -390,54 +386,6 @@ def create_individual_invoice(payment_cycle: OverallPaymentCycle, connection_obs return Result(valid=True, data=final_invoice) - -# def check_paid(billing_code: str, connection_observer: ConnectionObserver) -> Result: -# """ -# Purpose: -# Evaluates Payment Status for EITHER payment category - -# Requires: -# Valid OverallPaymentCycle -# billing_code is provided by the timer for BOTH types. -# """ - -# payment_cycle = OverallPaymentCycle() -# category = payment_cycle.category - -# logger.info(f"Checking paid with category of {category}") - -# # Get endpoint: -# if category == PaymentCategory.INDIVIDUAL: -# url = f'{Constants.SP_BASE_URL}/v2/check_paid' -# elif category == PaymentCategory.TICKET: -# url = f'{Constants.TICKET_API_BASE_URL}/check_ticket_paid' -# else: -# return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="This is not a supported payment category to check") - -# submit_data = { -# "billing_code": billing_code -# } - -# result = api_call_and_extraction( -# desired_type="payment_status", -# method=ApiCall.POST, -# url=url, -# connection_observer=connection_observer, -# payload=submit_data -# ) - -# if not result.valid: -# return result - -# payment_status = result.data - -# # paid -# if payment_status: -# return Result(valid=True, message="paid") -# else: -# return Result(valid=False, error_type=ResultError.UNPAID, message="The API call went through, but this subscription is not yet paid.") - - def paying_for_tickets() -> bool: """Returns boolean for if the current billing is tickets""" @@ -491,8 +439,11 @@ def wipe_billing_cycle() -> Result: timer_loop = CheckPaidTimer.get_instance() if timer_loop.is_running: timer_loop.stop_polling() + logger.info("Wiped the CheckPaidTimer") + else: + logger.error("Timer wasn't running to begin with.") except Exception as e: - logger.info("Skipping Timer since it's not yet created.") + logger.info(f"Skipping Timer since it's not yet created. BTW error was: {str(e)}") # Overall State: payment_cycle = OverallPaymentCycle() @@ -503,6 +454,7 @@ def wipe_billing_cycle() -> Result: return Result(valid=True) +@type_checker def get_profile_subscription(profile_id: int, save_pending: Optional[bool] = False) -> Subscription: """ Main Purpose: @@ -543,61 +495,7 @@ def wipe_subscription(profile_id: int) -> Result: return Result(valid=True) -# def mark_as_paid() -> Result: -# """Conclude the payment phase""" - -# logger.info("Marking as paid..") - -# payment_cycle = OverallPaymentCycle() - -# if not payment_cycle: -# error_msg = "There is no current billing cycle." -# return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg) - -# billing_data = payment_cycle.instance - -# if not billing_data: -# error_msg = "There is no current billing data." -# return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg) - -# category = payment_cycle.category - -# if category == PaymentCategory.INDIVIDUAL: -# profile_id = billing_data.instance_id -# result = mark_profile_as_paid(profile_id) -# if not result.valid: -# return result - -# elif category == PaymentCategory.TICKET: -# # saving global ticket data: -# billing_data.billing_pending = False -# billing_data.paid = True -# billing_data.save() - -# else: -# return not_supported() - -# # finally: -# payment_cycle.wipe() -# return Result(valid=True) - - -# def mark_profile_as_paid(profile_id: int): - -# logger.info(f"Marking profile_id {str(profile_id)} as paid..") - -# # Get real profile object: -# profile = ProfileController.get(profile_id) -# if not profile: -# error_msg = f"Invalid profile id of {profile_id}" -# logger.error(error_msg) -# return Result(valid=False, message=error_msg, error_type=ResultError.INVALID_INPUT) - -# profile.subscription.billing_pending = False -# ProfileController.update(profile) -# return Result(valid=True) - - +@type_checker def save_billing_code(profile_id: int, billing_code: str, expires_at: datetime) -> Result: """Checks if the billing code is already saved, and if not, saves it.""" @@ -631,6 +529,7 @@ def save_billing_code(profile_id: int, billing_code: str, expires_at: datetime) # this can be modified to accept an id, once the UI has a cache layer: +@type_checker def save_subscription(profile: Profile, subscription: Subscription) -> bool: """Saves subscription with the code and date.""" @@ -643,8 +542,9 @@ def save_subscription(profile: Profile, subscription: Subscription) -> bool: return True +@type_checker def return_pre_existing_payment_data(profile_id: int, subscription: SubscriptionInvoice) -> Result: - """Return the pre-existing payment details (or lack of) for a profile""" + """Return the pre-existing payment details (or lack of) for an individual profile""" logger.info("Starting a fetch of pre-existing payment data") diff --git a/core/utils/type_checker.py b/core/utils/type_checker.py index 53e85a3..bb645b5 100644 --- a/core/utils/type_checker.py +++ b/core/utils/type_checker.py @@ -1,4 +1,6 @@ from core.models.Result import Result +from core.errors.logger import logger + import inspect from functools import wraps @@ -20,7 +22,9 @@ def type_checker(func): bound_args.apply_defaults() except TypeError as e: # This handles cases where the wrong number of arguments are passed - return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=str(e)) + error_msg = f"For {func}, {str(e)}" + logger.error(error_msg) + return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg) # 2. Perform type validation for name, value in bound_args.arguments.items(): @@ -31,10 +35,12 @@ def type_checker(func): continue if not isinstance(value, expected_type): + error_msg = f"For function {func}, Argument '{name}' must be {expected_type.__name__}, got type {type(value).__name__}" + logger.error(error_msg) return Result( valid=False, error_type=ResultError.INVALID_INPUT, - message=f"Argument '{name}' must be {expected_type.__name__}, got type {type(value).__name__}" + message=error_msg ) # 3. If all checks pass, call the original function