From ea12aecf053674cac7b8952a2f9aa4ff41a3a1cd Mon Sep 17 00:00:00 2001 From: SimplifiedPrivacy Date: Tue, 20 Oct 2026 15:43:11 -0400 Subject: [PATCH] Improved error handling for billing conflicts of individual profiles at the same time. Overall flow is stable in this version --- gui/v2/actions/menu_coordinator.py | 78 +++++++--------------- gui/v2/actions/payment_actions.py | 16 +++++ gui/v2/actions/ticket_actions.py | 62 ++++++++++++++++- gui/v2/infrastructure/navigator.py | 19 +++--- gui/v2/ui/pages/currency_selection_page.py | 2 +- gui/v2/ui/pages/duration_selection_page.py | 3 +- gui/v2/ui/pages/menu_page.py | 7 +- gui/v2/ui/pages/payments2.py | 2 +- 8 files changed, 117 insertions(+), 72 deletions(-) diff --git a/gui/v2/actions/menu_coordinator.py b/gui/v2/actions/menu_coordinator.py index 4524e49..e716286 100644 --- a/gui/v2/actions/menu_coordinator.py +++ b/gui/v2/actions/menu_coordinator.py @@ -1,19 +1,21 @@ # core from core.controllers.ConfigurationController import ConfigurationController -from core.services.prepare_tickets.ticket_tracker import get_a_random_ticket +# from core.services.prepare_tickets.ticket_tracker import get_a_random_ticket from core.services.profiles.profile_connections import enable_profile #, disable_profile from core.controllers.ProfileController import ProfileController from core.models.Configuration import Configuration, ConnectionChoice -from core.services.subscriptions.subscriptions_by_protocol import get_profile_subscription, start_new_payment_cycle, wipe_subscription +from core.services.subscriptions.subscriptions_by_protocol import get_profile_subscription, start_new_payment_cycle, wipe_subscription, evaluate_repeat_cycle from core.utils.check_if_time_lapsed import check_if_time_lapsed from core.models.enum_models import PaymentCategory from core.utils.type_checker import type_checker from core.models.Result import Result, ResultError from core.errors.logger import logger from core.errors.translate_error import translate_error, get_ignored_exceptions +from core.services.payment_phase.payment_state import OverallPaymentCycle # gui -from gui.v2.actions.ticket_actions import get_ticket_data, use_a_ticket +from gui.v2.actions.payment_actions import wipe_expired_subscription +from gui.v2.actions.ticket_actions import dispatch_ticketing from gui.v2.infrastructure.action_registry import register_action from gui.v2.infrastructure.take_plumbing_action import take_action from gui.v2.infrastructure.screen_size import get_max_screensize @@ -25,6 +27,7 @@ from gui.v2.actions.error_handlers import navigate_to_new_sub_page # generic from typing import Callable, Optional +from functools import partial @type_checker def get_profile(profile_id: int): @@ -35,56 +38,21 @@ def get_profile(profile_id: int): return Result(valid=True, data=profile) -@register_action(ResultError.NO_TICKET) -def start_and_navigate_to_ticketing(): - """Starts a new ticketing cycle. (assumes the user opted in).""" - start_result = start_new_payment_cycle(category=PaymentCategory.TICKET) - if start_result.valid: - logger.info("creating a new payment cycle was valid, it worked.") - return Result(valid=False, error_type=ResultError.NAVIGATE_TO, navigate_to="plan_picker") +def dispatch_individual(profile_id: int, ui_caller: Callable) -> Result: + payment_cycle = OverallPaymentCycle() + + print(f"payment_cycle is {payment_cycle}") + + if payment_cycle.active: + same_result = evaluate_repeat_cycle(payment_cycle=payment_cycle, profile_id=profile_id) + if not same_result.valid: + return same_result + else: + error_msg = "There's an error in the dispatch_individual function" + logger.error(f"This shouldn't be going off. {error_msg}") + return Result(valid=False, error_type=ResultError.NAVIGATE_TO, navigate_to="home", message=error_msg) else: - logger.info(f"Returning the error of start_result {start_result}") - return start_result - - -@type_checker -def random_ticket_dispatch(profile_id: int, passed_ui_caller: Callable) -> Result: - """ - User explicitly wants a random ticket. - The reason this is in GUI repo, is because use_a_ticket is. - """ - - random_ticket_result = get_a_random_ticket() - - if not random_ticket_result.valid: - return random_ticket_result - - which_ticket = random_ticket_result.data - - # this is a redundant sanity check: - if not which_ticket: - Result(valid=False, error_type=ResultError.NO_TICKET, message="No tickets left. Would you like to get some?") - - # calls GUI repo function: - return use_a_ticket(profile_id=profile_id, which_ticket=which_ticket, ui_caller=passed_ui_caller) - -@type_checker -def dispatch_ticketing(profile_id: int, passed_ui_caller: Callable) -> Result: - """Evaluate if a random ticket will be used.""" - - use_random_setting = ConfigurationController.use_random() - - if use_random_setting: - return random_ticket_dispatch(profile_id=profile_id, passed_ui_caller=passed_ui_caller) - - # User does NOT want an automated selection, so they can pick from their tickets: - do_they_have_tickets = get_ticket_data() - - if do_they_have_tickets.valid: - return Result(valid=False, error_type=ResultError.NAVIGATE_TO, navigate_to="ticket_or_billing_choice") - - else: - # user can decide if they want to setup tickets or do an individual profile: + # Starting fresh. Pick a billing type: return navigate_to_new_sub_page() @@ -103,8 +71,7 @@ def filter_subscription(profile_id: int, passed_ui_caller: Callable) -> Result: # already opted into ticketing: return dispatch_ticketing(profile_id=profile_id, passed_ui_caller=passed_ui_caller) else: - # Starting fresh. Pick a billing type: - return navigate_to_new_sub_page() + return dispatch_individual(profile_id=profile_id, ui_caller=passed_ui_caller) #----------------------------- # Pending Subscription @@ -128,7 +95,8 @@ def filter_subscription(profile_id: int, passed_ui_caller: Callable) -> Result: expired = check_if_time_lapsed(expiration_date) if expired: - return Result(valid=False, error_type=ResultError.EXPIRED_SUB, message="This subscription is expired, would you like to wipe it and get a new one?") + function_choice = partial(wipe_subscription, profile_id=profile_id) + return Result(valid=False, function_choice=function_choice, error_type=ResultError.EXPIRED_SUB, message="This subscription is expired, would you like to wipe it and get a new one?") #----------------------------- # Proceed with Valid Sub diff --git a/gui/v2/actions/payment_actions.py b/gui/v2/actions/payment_actions.py index e3399b9..31f49e5 100644 --- a/gui/v2/actions/payment_actions.py +++ b/gui/v2/actions/payment_actions.py @@ -162,6 +162,22 @@ def start_polling(billing_code: str, profile_id: int, ui_caller: Callable) -> st 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!") + + # 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 + + return Result(valid=True, error_type=ResultError.NAVIGATE_TO, navigate_to="menu") + @register_action(ResultError.ACTIVE_BILLING) def stop_payment_timer() -> Result: diff --git a/gui/v2/actions/ticket_actions.py b/gui/v2/actions/ticket_actions.py index d129d88..80c70a8 100644 --- a/gui/v2/actions/ticket_actions.py +++ b/gui/v2/actions/ticket_actions.py @@ -1,13 +1,16 @@ # core +from core.controllers.ConfigurationController import ConfigurationController from core.controllers.tickets.UseTicketController import use_ticket +from core.services.prepare_tickets.ticket_tracker import get_a_random_ticket from core.models.BaseProfile import BaseProfile as Profile from core.controllers.ProfileController import ProfileController -from core.controllers.tickets.TicketPayController import initiate_payment +# from core.controllers.tickets.TicketPayController import initiate_payment from core.services.subscriptions.subscriptions_by_protocol import start_new_payment_cycle, save_subscription, get_ticket_invoice from core.services.payment_phase.ticket_cycle import wipe_ticket_cycle -from core.services.payment_phase.payment_state import OverallPaymentCycle +# from core.services.payment_phase.payment_state import OverallPaymentCycle from core.models.enum_models import PaymentCategory from core.services.prepare_tickets.ticket_tracker import get_all_unused_tickets +from core.utils.type_checker import type_checker from core.models.Result import Result, ResultError from core.errors.logger import logger @@ -15,11 +18,66 @@ from core.errors.logger import logger from gui.v2.infrastructure.action_registry import register_action from gui.v2.infrastructure.take_plumbing_action import take_action from gui.v2.infrastructure.setup_observers import connection_observer, ticket_observer +from gui.v2.actions.error_handlers import navigate_to_new_sub_page # generic: from typing import Callable +@register_action(ResultError.NO_TICKET) +def start_and_navigate_to_ticketing(): + """Starts a new ticketing cycle. (assumes the user opted in).""" + start_result = start_new_payment_cycle(category=PaymentCategory.TICKET) + if start_result.valid: + logger.info("creating a new payment cycle was valid, it worked.") + return Result(valid=False, error_type=ResultError.NAVIGATE_TO, navigate_to="plan_picker") + else: + logger.info(f"Returning the error of start_result {start_result}") + return start_result + + +@type_checker +def random_ticket_dispatch(profile_id: int, passed_ui_caller: Callable) -> Result: + """ + User explicitly wants a random ticket. + The reason this is in GUI repo, is because use_a_ticket is. + """ + + random_ticket_result = get_a_random_ticket() + + if not random_ticket_result.valid: + return random_ticket_result + + which_ticket = random_ticket_result.data + + # this is a redundant sanity check: + if not which_ticket: + Result(valid=False, error_type=ResultError.NO_TICKET, message="No tickets left. Would you like to get some?") + + # calls GUI repo function: + return use_a_ticket(profile_id=profile_id, which_ticket=which_ticket, ui_caller=passed_ui_caller) + + +@type_checker +def dispatch_ticketing(profile_id: int, passed_ui_caller: Callable) -> Result: + """Evaluate if a random ticket will be used.""" + + use_random_setting = ConfigurationController.use_random() + + if use_random_setting: + return random_ticket_dispatch(profile_id=profile_id, passed_ui_caller=passed_ui_caller) + + # User does NOT want an automated selection, so they can pick from their tickets: + do_they_have_tickets = get_ticket_data() + + if do_they_have_tickets.valid: + return Result(valid=False, error_type=ResultError.NAVIGATE_TO, navigate_to="ticket_or_billing_choice") + + else: + # user can decide if they want to setup tickets or do an individual profile: + return navigate_to_new_sub_page() + + def start_ticket(ui_caller: Callable) -> Result: logger.info("started ticketing billing state") return take_action( diff --git a/gui/v2/infrastructure/navigator.py b/gui/v2/infrastructure/navigator.py index c0357e5..54c3480 100755 --- a/gui/v2/infrastructure/navigator.py +++ b/gui/v2/infrastructure/navigator.py @@ -148,7 +148,7 @@ class Navigator(QObject): self._preload_dispatched += 1 idx = self._preload_dispatched name = self._preload_queue.pop(0) - logger.debug(f"[preload] ---- {idx}/{self._preload_total} ----") + # logger.debug(f"[preload] ---- {idx}/{self._preload_total} ----") if name in ASYNC_PREPARE: self._dispatch_async_preload(name) else: @@ -220,11 +220,12 @@ class Navigator(QObject): self._emit_preload_summary() def _emit_preload_summary(self): - total_s = time.perf_counter() - self._preload_t_start - logger.debug(f"[preload] ============================================================") - logger.debug(f"[preload] complete: {self._preload_done} loaded, " - f"{self._preload_failed} failed, " - f"{self._preload_skipped_runtime} skipped (user beat us to it) " - f"in {total_s:.2f}s") - logger.debug(f"[preload] cached pages now: {sorted(self._instances.keys())}") - logger.debug(f"[preload] ============================================================") + pass + # total_s = time.perf_counter() - self._preload_t_start + # logger.debug(f"[preload] ============================================================") + # logger.debug(f"[preload] complete: {self._preload_done} loaded, " + # f"{self._preload_failed} failed, " + # f"{self._preload_skipped_runtime} skipped (user beat us to it) " + # f"in {total_s:.2f}s") + # logger.debug(f"[preload] cached pages now: {sorted(self._instances.keys())}") + # logger.debug(f"[preload] ============================================================") diff --git a/gui/v2/ui/pages/currency_selection_page.py b/gui/v2/ui/pages/currency_selection_page.py index db0439e..e4e1345 100755 --- a/gui/v2/ui/pages/currency_selection_page.py +++ b/gui/v2/ui/pages/currency_selection_page.py @@ -76,7 +76,7 @@ class CurrencySelectionPage(Page): # get the singleton (this was created on a previous page) print("doing the new method") - data_result = get_current_billing_data() + data_result = get_current_billing_data(profile_id=profile_id) if not data_result.valid: message = data_result.message diff --git a/gui/v2/ui/pages/duration_selection_page.py b/gui/v2/ui/pages/duration_selection_page.py index 198f733..3d148ae 100755 --- a/gui/v2/ui/pages/duration_selection_page.py +++ b/gui/v2/ui/pages/duration_selection_page.py @@ -89,7 +89,8 @@ class DurationSelectionPage(Page): plan_id = self.dict_of_choices[current_choice] # save to singleton across pages: - data_result = get_current_billing_data(category=PaymentCategory.INDIVIDUAL) + profile_id = int(self.update_status.current_profile_id) + data_result = get_current_billing_data(category=PaymentCategory.INDIVIDUAL, profile_id=profile_id) if not data_result.valid: message = data_result.message diff --git a/gui/v2/ui/pages/menu_page.py b/gui/v2/ui/pages/menu_page.py index 739785c..58843e4 100755 --- a/gui/v2/ui/pages/menu_page.py +++ b/gui/v2/ui/pages/menu_page.py @@ -759,12 +759,13 @@ class MenuPage(Page): self.update_action_button_visuals(profile_id) @mainthread - def turn_off_session_visuals(self, profile_id: int): + def turn_off_session_visuals(self, profile_id: int, surpress_output: Optional[bool] = False): """ Flip off the Visual buttons for a given profile. And re-enable the edit button if its currently selected. """ - print(f"Stopping Session {profile_id}!") + if not surpress_output: + print(f"Stopping Session {profile_id}!") # If you want the border to stay when off, set is_active=True # If you want the border to disappear when off, set is_active=False self._update_profile_icon(profile_id, self.not_connected_profile, is_active=True) @@ -853,4 +854,4 @@ class MenuPage(Page): else: # DEFAULT STARTING PLACE: # If it's not system-wide and not a session, it must be off. - self.turn_off_session_visuals(pid) + self.turn_off_session_visuals(profile_id=pid, surpress_output=True) diff --git a/gui/v2/ui/pages/payments2.py b/gui/v2/ui/pages/payments2.py index ddcadb6..9bcb79a 100644 --- a/gui/v2/ui/pages/payments2.py +++ b/gui/v2/ui/pages/payments2.py @@ -415,7 +415,7 @@ class PaymentDetailsPage(Page): ) # get the singleton (this was created on a previous page) - data_result = get_current_billing_data() + data_result = get_current_billing_data(profile_id=self.profile_id) if not data_result.valid: message = data_result.message