# core from core.services.payment_phase.payment_state import OverallPaymentCycle, PaymentData, TicketPaymentData from core.services.payment_phase.check_payment_loop import CheckPaidTimer from core.services.subscriptions.subscriptions_by_protocol import ( create_new_invoice, save_billing_code, get_profile_subscription, wipe_billing_cycle, start_new_payment_cycle, return_pre_existing_invoice, wipe_subscription, ) from core.models.enum_models import PaymentType, PaymentCategory from core.models.Result import Result, ResultError from core.errors.logger import logger from core.utils.type_checker import type_checker from core.observers.Notifications import Notifications, EventType # gui from gui.v2.infrastructure.action_registry import register_action from gui.v2.actions.ticket_actions import fetch_ticket_invoice from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool from gui.v2.infrastructure.take_plumbing_action import take_action from gui.v2.infrastructure.setup_observers import connection_observer from gui.v2.actions.error_handlers import navigate_to_new_sub_page # generic from typing import Callable, Optional def _wipe_and_navigate(profile_id: Optional[int] = None): cleared_cycle = wipe_billing_cycle(profile_id) if cleared_cycle: return navigate_to_new_sub_page() else: return cleared_cycle @register_action(ResultError.EXPIRED_INVOICE) def reset_individual_billing_cycle() -> Result: payment_cycle = OverallPaymentCycle() if not payment_cycle.category == PaymentCategory.INDIVIDUAL: return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="This function isn't for tickets.") billing_data = payment_cycle.billing_data if not billing_data: return _wipe_and_navigate() profile_id = billing_data.instance_id if profile_id: return wipe_subscription(profile_id) else: return _wipe_and_navigate() # @register_action(ResultError.EXPIRED_SUB) # def wipe_expired_subscription(profile_id: int) -> Result: # """Call core to wipe a subscription, then navigate to the billing page""" # logger.info(f"Wiping the subscription for {profile_id}") # wiped_result = wipe_subscription(profile_id) # if wiped_result.valid: # logger.info("Successfully wiped") # payment_cycle = OverallPaymentCycle() # payment_cycle.wipe_billing_cycle() # # also clear the whole billing cycle, so it's ready for an individual purchase, # return _wipe_and_navigate(profile_id) # else: # return wiped_result @type_checker def start_individual_payment(profile_id: int, ui_caller: Callable) -> Result: logger.info(f"Attempting to start individual billing state for profile {str(profile_id)}") take_action( action=start_new_payment_cycle, ui_caller=ui_caller, category=PaymentCategory.INDIVIDUAL, profile_id=profile_id ) def gui_starts_invoice(invoice_ui_caller: Callable, timer_resume_caller: Callable, profile_id: Optional[int] = None) -> str | None: logger.info(f"gui_starts_invoice is being called with profile id {profile_id}") # Is there currently a process? payment_cycle = OverallPaymentCycle() if not payment_cycle: error_msg = "There is no payment cycle yet" logger.error(error_msg) return error_msg if not payment_cycle.active: error_msg = "Process is not yet active" logger.error(error_msg) return error_msg if payment_cycle.got_invoice: print("Cycle already got an invoice") billing_code = payment_cycle.billing_code if not billing_code: error_msg = "Did not save the invoice from last time." logger.error(error_msg) # insert function here to get it via the data inside. return error_msg else: return return_pre_existing_invoice(billing_code=billing_code, connection_observer=connection_observer) notification = "Fetching Invoice.." Notifications.emit(EventType.STATUS_UPDATE, notification) take_action( action=create_new_invoice, ui_caller=invoice_ui_caller, # profile_id=profile_id, connection_observer=connection_observer ) def start_polling(billing_code: str, profile_id: int, ui_caller: Callable) -> str | None: """Kickstart an independent timer on a background thread to check if it's paid.""" payment_cycle = OverallPaymentCycle() if not payment_cycle.active: logger.error("Not yet active to start polling.") #---------------------------- # FILTER ALREADY POLLING #---------------------------- if payment_cycle.already_polling: error_msg = "This cycle is already polling, so we don't need to start another poll" logger.info(error_msg) return None #---------------------------- # FILTER LACK OF DATA #---------------------------- if not billing_code: return "Requires a billing code" #---------------------------- # CHECK PAID ON PRE-EXISTING INVOICE #---------------------------- logger.info("Starting a check paid timer..") # Setup PyQt6 GUI as mainthread to bring back to: pyqt_threader = ThreadSafetyTool() # update the global singleton: payment_cycle.already_polling = True notification = "Checking if Paid.." Notifications.emit(EventType.STATUS_UPDATE, notification) # setup the timer with the callback: # it doesn't return result here. It calls the view_callback function. check_paid_timer = CheckPaidTimer( view_callback=ui_caller, billing_code=billing_code, ui_dispatcher=pyqt_threader, payment_type=PaymentType.CRYPTO, connection_observer=connection_observer, profile_id=profile_id ) check_paid_timer.start_polling() return None @register_action(ResultError.BILLING_CONFLICT) def stop_current() -> Result: wiped_result = wipe_billing_cycle() logger.info("Stopping current billing cycle!") # if not wiped_result.valid: # return wiped_result # new_cycle_result = start_new_payment_cycle(category=PaymentCategory.INDIVIDUAL, profile_id=profile_id) # if not new_cycle_result.valid: # return new_cycle_result notification = "Stopped Checking if Paid" Notifications.emit(EventType.STATUS_UPDATE, notification) return Result(valid=True, error_type=ResultError.NAVIGATE_TO, navigate_to="menu") @register_action(ResultError.ACTIVE_BILLING) def stop_payment_timer() -> Result: """Gets the single instance of the payment checker timer, and stops it.""" logger.info("Stopping the check paid timer & wiping the cycle..") try: # Wipe the cycle via core, and stop the timer, so this is not the active poller (blocking others) wiped_result = wipe_billing_cycle() # stop the timer: # check_paid_timer = CheckPaidTimer.get_instance() # if check_paid_timer: # check_paid_timer.stop_polling() if wiped_result.valid: content_to_display = "Stopped! You can resume a different profile's invoice now" notification = "Stopped Checking if Paid" Notifications.emit(EventType.STATUS_UPDATE, notification) return Result(valid=True, message=content_to_display) else: error_msg = "Unable to stop the payment timer and wipe the profile inside stop_payment_timer. But didn't get an actual error." logger.error(error_msg) return Result(valid=False, error_type=ResultError.UNKNOWN, message=error_msg) except Exception as e: error_msg = f"Unable to stop the payment timer: {str(e)}" logger.error(error_msg) return Result(valid=False, error_type=ResultError.UNKNOWN, message=error_msg) # def new_individual_billing(profile_id: int): # """Wipes pre-existing global payment cycle. And Calls upon core to setup a new individual profile cycle & data state.""" # logger.info("starting a new individual profile state. And wiping the old one") # return start_billing_state( # category=PaymentCategory.INDIVIDUAL, # profile_id=profile_id, # wipe_existing=True # ) # billing_data = payment_cycle.instance # category = payment_cycle.category # if not billing_data: # msg = "There is a payment cycle, but no current billing data in it." # print(msg) # return msg # #---------------------------- # # INDIVIDUAL # #---------------------------- # if category == PaymentCategory.INDIVIDUAL: # # already conflicting one: # if not profile_id: # return "Need a profile id for an individual profile." # take_action( # action=create_individual_invoice, # ui_caller=invoice_ui_caller, # profile_id=profile_id, # connection_observer=connection_observer # ) # #---------------------------- # # TICKETS # #---------------------------- # elif category == PaymentCategory.TICKET: # logger.info("Starting ticketing invoicing..") # take_action( # action=create_individual_invoice, # ui_caller=invoice_ui_caller, # connection_observer=connection_observer # ) # else: # error_msg = "Unsupported Payment Category" # logger.error(error_msg) # return error_msg # # Finally: # payment_cycle.got_invoice = True # return None # return fetch_individual_invoice(invoice_ui_caller=invoice_ui_caller, timer_resume_caller=timer_resume_caller, profile_id=profile_id) # # if profile_id != payment_cycle.profile_id: # # logger.info("The new profile id request is different than the pre-existing running one. So we're wiping it and stopping the timer.") # # worked, message = stop_payment_timer() # # if not worked: # # return message # # # start over: # # payment_cycle.wipe() # # new_data = new_individual_billing(profile_id) # # if not new_data.valid: # # return new_data # result object # # category = PaymentCategory.INDIVIDUAL # # # billing_data = new_cycle.data # elif category == PaymentCategory.TICKET: # logger.info("Starting ticketing invoicing..") # return fetch_ticket_invoice(invoice_ui_caller=invoice_ui_caller, timer_resume_caller=timer_resume_caller) # def fetch_individual_invoice(invoice_ui_caller: Callable, timer_resume_caller: Callable, profile_id: int) -> str | None: # """ # Purpose: # Create a new invoice if we don't have one yet, or re-uses the saved data. # And starts polling if it's not already. # Context: # For individual profiles. # Returns: # Returns any errors as strings for UI to display. # """ # # Fetch: # take_action( # action=create_individual_invoice, # ui_caller=invoice_ui_caller, # profile_id=profile_id, # connection_observer=connection_observer # ) # def save_and_begin_timer( # ticketing: bool, # gui_callback: Callable, # billing_code: str, # profile_id: Optional[int] = None, # expires_at: Optional[str] = None, # ) -> bool: # """ # First saves the billing code & profile expiration, if it's an individual (non-ticket) crypto-based one. # Then it Starts a loop of checking if the invoice is paid and saves it. # """ # if ticketing: # payment_type = PaymentType.TICKET # logger.error("Ticketing already saves the billing code on it's own when it recieves it.") # # NON-ticketing: # elif profile_id and expires_at: # payment_type = PaymentType.CRYPTO # saved = save_billing_code(profile_id=profile_id, billing_code=billing_code, expires_at=expires_at) # if not saved.valid: # return False # else: # logger.error("You need a profile id and expires_at for individual profiles") # return False # # Setup PyQt6 GUI as mainthread to bring back to: # pyqt_threader = ThreadSafetyTool() # # setup the timer with the callback: # # it doesn't return result here. It calls the gui_callback function. # check_paid_timer = CheckPaidTimer( # view_callback=gui_callback, # billing_code=billing_code, # ui_dispatcher=pyqt_threader, # payment_type=payment_type, # connection_observer=connection_observer, # profile_id=profile_id # ) # logger.info("starting polling from GUI action..") # check_paid_timer.start_polling() # return True # this is just that it started