Improved error handling for billing conflicts of individual profiles at the same time. Overall flow is stable in this version

This commit is contained in:
SimplifiedPrivacy 2026-10-20 15:43:11 -04:00
parent 39925ad546
commit ea12aecf05
8 changed files with 117 additions and 72 deletions

View file

@ -1,19 +1,21 @@
# core # core
from core.controllers.ConfigurationController import ConfigurationController 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.services.profiles.profile_connections import enable_profile #, disable_profile
from core.controllers.ProfileController import ProfileController from core.controllers.ProfileController import ProfileController
from core.models.Configuration import Configuration, ConnectionChoice 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.utils.check_if_time_lapsed import check_if_time_lapsed
from core.models.enum_models import PaymentCategory from core.models.enum_models import PaymentCategory
from core.utils.type_checker import type_checker from core.utils.type_checker import type_checker
from core.models.Result import Result, ResultError from core.models.Result import Result, ResultError
from core.errors.logger import logger from core.errors.logger import logger
from core.errors.translate_error import translate_error, get_ignored_exceptions from core.errors.translate_error import translate_error, get_ignored_exceptions
from core.services.payment_phase.payment_state import OverallPaymentCycle
# gui # 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.action_registry import register_action
from gui.v2.infrastructure.take_plumbing_action import take_action from gui.v2.infrastructure.take_plumbing_action import take_action
from gui.v2.infrastructure.screen_size import get_max_screensize 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 # generic
from typing import Callable, Optional from typing import Callable, Optional
from functools import partial
@type_checker @type_checker
def get_profile(profile_id: int): def get_profile(profile_id: int):
@ -35,56 +38,21 @@ def get_profile(profile_id: int):
return Result(valid=True, data=profile) return Result(valid=True, data=profile)
@register_action(ResultError.NO_TICKET) def dispatch_individual(profile_id: int, ui_caller: Callable) -> Result:
def start_and_navigate_to_ticketing(): payment_cycle = OverallPaymentCycle()
"""Starts a new ticketing cycle. (assumes the user opted in)."""
start_result = start_new_payment_cycle(category=PaymentCategory.TICKET) print(f"payment_cycle is {payment_cycle}")
if start_result.valid:
logger.info("creating a new payment cycle was valid, it worked.") if payment_cycle.active:
return Result(valid=False, error_type=ResultError.NAVIGATE_TO, navigate_to="plan_picker") 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: else:
logger.info(f"Returning the error of start_result {start_result}") # Starting fresh. Pick a billing type:
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() 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: # already opted into ticketing:
return dispatch_ticketing(profile_id=profile_id, passed_ui_caller=passed_ui_caller) return dispatch_ticketing(profile_id=profile_id, passed_ui_caller=passed_ui_caller)
else: else:
# Starting fresh. Pick a billing type: return dispatch_individual(profile_id=profile_id, ui_caller=passed_ui_caller)
return navigate_to_new_sub_page()
#----------------------------- #-----------------------------
# Pending Subscription # Pending Subscription
@ -128,7 +95,8 @@ def filter_subscription(profile_id: int, passed_ui_caller: Callable) -> Result:
expired = check_if_time_lapsed(expiration_date) expired = check_if_time_lapsed(expiration_date)
if expired: 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 # Proceed with Valid Sub

View file

@ -162,6 +162,22 @@ def start_polling(billing_code: str, profile_id: int, ui_caller: Callable) -> st
check_paid_timer.start_polling() check_paid_timer.start_polling()
return None 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) @register_action(ResultError.ACTIVE_BILLING)
def stop_payment_timer() -> Result: def stop_payment_timer() -> Result:

View file

@ -1,13 +1,16 @@
# core # core
from core.controllers.ConfigurationController import ConfigurationController
from core.controllers.tickets.UseTicketController import use_ticket 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.models.BaseProfile import BaseProfile as Profile
from core.controllers.ProfileController import ProfileController 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.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.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.models.enum_models import PaymentCategory
from core.services.prepare_tickets.ticket_tracker import get_all_unused_tickets 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.models.Result import Result, ResultError
from core.errors.logger import logger 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.action_registry import register_action
from gui.v2.infrastructure.take_plumbing_action import take_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.infrastructure.setup_observers import connection_observer, ticket_observer
from gui.v2.actions.error_handlers import navigate_to_new_sub_page
# generic: # generic:
from typing import Callable 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: def start_ticket(ui_caller: Callable) -> Result:
logger.info("started ticketing billing state") logger.info("started ticketing billing state")
return take_action( return take_action(

View file

@ -148,7 +148,7 @@ class Navigator(QObject):
self._preload_dispatched += 1 self._preload_dispatched += 1
idx = self._preload_dispatched idx = self._preload_dispatched
name = self._preload_queue.pop(0) 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: if name in ASYNC_PREPARE:
self._dispatch_async_preload(name) self._dispatch_async_preload(name)
else: else:
@ -220,11 +220,12 @@ class Navigator(QObject):
self._emit_preload_summary() self._emit_preload_summary()
def _emit_preload_summary(self): def _emit_preload_summary(self):
total_s = time.perf_counter() - self._preload_t_start pass
logger.debug(f"[preload] ============================================================") # total_s = time.perf_counter() - self._preload_t_start
logger.debug(f"[preload] complete: {self._preload_done} loaded, " # logger.debug(f"[preload] ============================================================")
f"{self._preload_failed} failed, " # logger.debug(f"[preload] complete: {self._preload_done} loaded, "
f"{self._preload_skipped_runtime} skipped (user beat us to it) " # f"{self._preload_failed} failed, "
f"in {total_s:.2f}s") # f"{self._preload_skipped_runtime} skipped (user beat us to it) "
logger.debug(f"[preload] cached pages now: {sorted(self._instances.keys())}") # f"in {total_s:.2f}s")
logger.debug(f"[preload] ============================================================") # logger.debug(f"[preload] cached pages now: {sorted(self._instances.keys())}")
# logger.debug(f"[preload] ============================================================")

View file

@ -76,7 +76,7 @@ class CurrencySelectionPage(Page):
# get the singleton (this was created on a previous page) # get the singleton (this was created on a previous page)
print("doing the new method") 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: if not data_result.valid:
message = data_result.message message = data_result.message

View file

@ -89,7 +89,8 @@ class DurationSelectionPage(Page):
plan_id = self.dict_of_choices[current_choice] plan_id = self.dict_of_choices[current_choice]
# save to singleton across pages: # 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: if not data_result.valid:
message = data_result.message message = data_result.message

View file

@ -759,12 +759,13 @@ class MenuPage(Page):
self.update_action_button_visuals(profile_id) self.update_action_button_visuals(profile_id)
@mainthread @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. Flip off the Visual buttons for a given profile.
And re-enable the edit button if its currently selected. 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 stay when off, set is_active=True
# If you want the border to disappear when off, set is_active=False # 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) self._update_profile_icon(profile_id, self.not_connected_profile, is_active=True)
@ -853,4 +854,4 @@ class MenuPage(Page):
else: else:
# DEFAULT STARTING PLACE: # DEFAULT STARTING PLACE:
# If it's not system-wide and not a session, it must be off. # 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)

View file

@ -415,7 +415,7 @@ class PaymentDetailsPage(Page):
) )
# get the singleton (this was created on a previous 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: if not data_result.valid:
message = data_result.message message = data_result.message