# 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_individual_invoice, save_billing_code, get_profile_subscription, start_billing_state, wipe_billing_cycle from core.models.enum_models import PaymentType, PaymentCategory from core.models.Result import Result, ResultError from core.errors.logger import logger # gui 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 # generic from typing import Callable, Optional 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 ) 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() # start a new individual one if not existing: if not payment_cycle: logger.info("There's no current payment cycle") if profile_id: return fetch_individual_invoice(invoice_ui_caller=invoice_ui_caller, timer_resume_caller=timer_resume_caller, profile_id=profile_id) else: return "Need a profile id for an individual profile." 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. So we're starting a new cycle.." logger.info(msg) result = new_individual_billing(profile_id=profile_id) if not result.valid: return result billing_data = result.data #---------------------------- # INVOICE DATA #---------------------------- if category == PaymentCategory.INDIVIDUAL: # already conflicting one: if not profile_id: return "Need a profile id for an individual profile." 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 return fetch_individual_invoice(invoice_ui_caller=invoice_ui_caller, timer_resume_caller=timer_resume_caller, profile_id=profile_id) 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 ) #---------------------------- # FILTER ALREADY POLLING #---------------------------- payment_cycle = OverallPaymentCycle() if payment_cycle.already_polling: logger.info("This cycle is already polling, so we don't need to start another poll") return None #---------------------------- # FILTER LACK OF DATA #---------------------------- subscription = get_profile_subscription(profile_id) if not subscription: logger.info(f"Started a New Invoice for Profile {profile_id}..") return None if not subscription.billing_pending: logger.error("This function should not have been called if the invoice is already paid.") return "Invoice already paid." if not subscription.billing_code: return "Previous subscription didn't have a billing code." #---------------------------- # CHECK PAID ON PRE-EXISTING INVOICE #---------------------------- logger.info("Resuming a pre-existing payment..") # Setup PyQt6 GUI as mainthread to bring back to: pyqt_threader = ThreadSafetyTool() # update the global singleton: payment_cycle.already_polling = True # setup the timer with the callback: # it doesn't return result here. It calls the view_callback function. check_paid_timer = CheckPaidTimer( view_callback=timer_resume_caller, billing_code=subscription.billing_code, ui_dispatcher=pyqt_threader, payment_type=PaymentType.CRYPTO, connection_observer=connection_observer, profile_id=profile_id ) return None 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 def stop_payment_timer() -> tuple: """Gets the single instance of the payment checker timer, and stops it.""" try: # Wipe the cycle, so this is not the active poller (blocking others) wipe_billing_cycle() # stop the timer: check_paid_timer = CheckPaidTimer.get_instance() if check_paid_timer: check_paid_timer.stop_polling() content_to_display = "Stopped! You can resume a different profile's invoice now" return True, content_to_display except Exception as e: error_msg = f"Unable to stop the payment timer: {str(e)}" logger.error(error_msg) return False, error_msg