diff --git a/gui/v2/actions/menu_coordinator.py b/gui/v2/actions/menu_coordinator.py index f860828..6c91cfe 100644 --- a/gui/v2/actions/menu_coordinator.py +++ b/gui/v2/actions/menu_coordinator.py @@ -1,8 +1,10 @@ # core +from core.controllers.ConfigurationController import ConfigurationController +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, wipe_billing_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 @@ -15,6 +17,7 @@ from core.Errors import ( ) # gui +from gui.v2.actions.ticket_actions import get_ticket_data, use_a_ticket 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 @@ -43,6 +46,7 @@ def get_profile(profile_id: int): else: return Result(valid=True, data=profile) + def get_ignored_exceptions(ignore_state_conflict: Optional[bool] = False): ignored_as_list = [] config = Configuration.get() @@ -73,25 +77,91 @@ def wipe_expired_subscription(profile_id: int) -> Result: return wiped -def filter_subscription(profile_id: int) -> Result: +@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 + + +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) + + +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 pick between tickets or individual: + return navigate_to_new_sub_page() + + +def filter_subscription(profile_id: int, passed_ui_caller: Callable) -> Result: """Evaluate if this profile has a subscription that's ready to connect.""" logger.info(f"Checking the subscription on profile id {profile_id}") subscription = get_profile_subscription(profile_id=profile_id) + #----------------------------- + # No Subscription + #----------------------------- if not subscription or not subscription.billing_code: - # auto check tickets would be here. - return navigate_to_new_sub_page() + tickets_setup = ConfigurationController.are_tickets_setup() + if tickets_setup: + # 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() + #----------------------------- + # Pending Subscription + #----------------------------- if subscription.billing_pending: start_result = start_new_payment_cycle(category=PaymentCategory.INDIVIDUAL, profile_id=profile_id) 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="payment_details", message="Finish the subscription payment") + return Result(valid=False, error_type=ResultError.NAVIGATE_TO, navigate_to="payment_details", message="Please finish your payment.") else: logger.info(f"Returning the error of start_result {start_result}") return start_result + #----------------------------- + # Expired Subscription + #----------------------------- expiration_date = subscription.expires_at if not expiration_date: # auto check tickets would be here. @@ -101,7 +171,9 @@ def filter_subscription(profile_id: int) -> Result: 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?") - # has a valid sub: + #----------------------------- + # Proceed with Valid Sub + #----------------------------- logger.info(f"Profile id {profile_id} has a valid subscription ready to go..") return Result(valid=True) @@ -183,10 +255,10 @@ def literally_connect( else: return Result(valid=True) except Exception as error: - return _enable_error(error=error, profile_id=profile_id) + return enable_error(error=error, profile_id=profile_id) -def _enable_error(error: Type[Exception], profile_id: int) -> Result: +def enable_error(error: Type[Exception], profile_id: int) -> Result: """Translate core errors into actions and/or messages via Result Objects.""" logger.error(str(error)) @@ -234,7 +306,7 @@ def _enable_error(error: Type[Exception], profile_id: int) -> Result: return Result(valid=False, error_type=ResultError.UNKNOWN, message="An unknown error occured.") -def _connect_profile(profile_id: int) -> Result: +def _connect_profile(profile_id: int, passed_ui_caller: Callable) -> Result: """Background Threaded Orchestration to Start a Connection""" # # Step 1: Get the Profile @@ -245,7 +317,7 @@ def _connect_profile(profile_id: int) -> Result: # profile = profile_result.data # Step 2: Subscription filter: - sub_result = filter_subscription(profile_id=profile_id) + sub_result = filter_subscription(profile_id=profile_id, passed_ui_caller=passed_ui_caller) if not sub_result.valid: return sub_result @@ -259,13 +331,28 @@ def _connect_profile(profile_id: int) -> Result: return literally_connect(profile_id=profile_id) +# def basic_test(): +# print("returning the basic test") +# return Result(valid=False, error_type=ResultError.ACTIVE_BILLING, message="hello") + +# @type_checker +# def connect_profile(profile_id: int, ui_caller: Callable) -> Result: +# """Type check before we thread""" +# take_action( +# action=basic_test, +# ui_caller=ui_caller +# ) +# return Result(valid=True) + + @type_checker def connect_profile(profile_id: int, ui_caller: Callable) -> Result: """Type check before we thread""" take_action( action=_connect_profile, ui_caller=ui_caller, - profile_id=profile_id + profile_id=profile_id, + passed_ui_caller=ui_caller ) return Result(valid=True) diff --git a/gui/v2/actions/payment_actions.py b/gui/v2/actions/payment_actions.py index 314dd4b..6d284dc 100644 --- a/gui/v2/actions/payment_actions.py +++ b/gui/v2/actions/payment_actions.py @@ -3,7 +3,8 @@ from core.services.payment_phase.payment_state import OverallPaymentCycle, Payme 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 + get_profile_subscription, wipe_billing_cycle, + start_new_payment_cycle ) from core.models.enum_models import PaymentType, PaymentCategory from core.models.Result import Result, ResultError @@ -15,10 +16,20 @@ 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 core.utils.type_checker import type_checker # generic from typing import Callable, Optional +@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: @@ -109,16 +120,21 @@ 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, so this is not the active poller (blocking others) - wipe_billing_cycle() + # 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() + # 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 Result(valid=True, message=content_to_display) + if wiped_result.valid: + content_to_display = "Stopped! You can resume a different profile's invoice now" + 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)}" diff --git a/gui/v2/actions/ticket_actions.py b/gui/v2/actions/ticket_actions.py index 2fa7a69..07b4168 100644 --- a/gui/v2/actions/ticket_actions.py +++ b/gui/v2/actions/ticket_actions.py @@ -22,8 +22,8 @@ def start_ticket(ui_caller: Callable) -> Result: logger.info("started ticketing billing state") return take_action( action=start_new_payment_cycle, - category=PaymentCategory.TICKET - # ui_caller=ui_caller, + category=PaymentCategory.TICKET, + ui_caller=ui_caller ) diff --git a/gui/v2/infrastructure/action_registry.py b/gui/v2/infrastructure/action_registry.py index 7d4ede8..4136b65 100644 --- a/gui/v2/infrastructure/action_registry.py +++ b/gui/v2/infrastructure/action_registry.py @@ -1,16 +1,17 @@ from core.models.Result import Result, ResultError from core.controllers.SyncController import new_sync - +from core.services.subscriptions.subscriptions_by_protocol import wipe_billing_cycle # from gui.v2.actions.payment_actions import stop_payment_timer ENUM_MAP = { ResultError.UNKNOWN: None, ResultError.NEED_SYNC: (new_sync), + ResultError.ACTIVE_BILLING: (wipe_billing_cycle), ResultError.INVALID_API_REPLY: None, - ResultError.ENCRYPTION: None, - # ResultError.ACTIVE_BILLING: (stop_payment_timer), + ResultError.ENCRYPTION: None } + def register_action(error_code): def wrapper(func): ENUM_MAP[error_code] = (func) @@ -20,8 +21,13 @@ def register_action(error_code): + + + + # Add later potentially: +# ResultError.ACTIVE_BILLING: (stop_payment_timer), # ResultError.NO_TICKET: (start_billing), # ResultError.NO_PROFILE: (go_to_new_profile_page), # ResultError.ALREADY_EXISTS: (get_invoice), # bypass_existing: True diff --git a/gui/v2/ui/pages/id_page.py b/gui/v2/ui/pages/id_page.py index 7b8e5ba..24106f2 100755 --- a/gui/v2/ui/pages/id_page.py +++ b/gui/v2/ui/pages/id_page.py @@ -8,14 +8,16 @@ from PyQt6.QtGui import QIcon, QPixmap from PyQt6.QtCore import Qt from PyQt6 import QtCore -from core.services.subscriptions.subscriptions_by_protocol import start_new_payment_cycle from core.models.enum_models import PaymentCategory # from core.services.payment_phase.ticket_config_tools import do_we_have_billing_id from core.services.prepare_tickets.ticket_tracker import delete_ticket_data +from core.models.Result import Result, ResultError + from gui.v2.ui.pages.Page import Page from gui.v2.ui.popups.message_box import style_message_box, mark_confirm_button from gui.v2.actions.menu_tasks import enable_profile +from gui.v2.actions.payment_actions import start_individual_payment HOW_MANY_PROFILES_DEFAULT = 6 @@ -158,17 +160,21 @@ class IdPage(Page): def go_single_profile(self): self.text_edit.clear() print("starting a single profile payment cycle") - result = start_new_payment_cycle( - category=PaymentCategory.INDIVIDUAL, - profile_id=int(self.update_status.current_profile_id) + start_individual_payment( + profile_id=int(self.update_status.current_profile_id), + ui_caller=self.single_profile_callback ) + + + def single_profile_callback(self, result: Result): if not result: print("no result.") return if result.valid: - billing_code = result.data - print(f"billing_code is {billing_code}") + # billing_data = result.data + # billing_code = billing_data.billing_code + # print(f"billing_code is {billing_code}") self.custom_window.navigator.navigate("duration_selection") else: self.show_a_popup(str(result.message)) @@ -180,7 +186,9 @@ class IdPage(Page): if not result.valid: self.show_a_popup(str(result.message)) - billing_code = result.billing_code + payment_cycle = result.data + billing_code = payment_cycle.billing_code + if billing_code: self._prompt_existing_tickets(billing_code) else: diff --git a/gui/v2/ui/pages/plan_picker_page.py b/gui/v2/ui/pages/plan_picker_page.py index 5f83ff4..16899be 100755 --- a/gui/v2/ui/pages/plan_picker_page.py +++ b/gui/v2/ui/pages/plan_picker_page.py @@ -94,6 +94,10 @@ class PlanPickerPage(Page): def recieve_initial_billing_data(self, result: Result): """Callback to recieve initial billing data instance""" + + if not isinstance(result, Result): + self.show_a_popup("Developer Error: recieve_initial_billing_data should not have gotten a non-result object.") + if not result.valid: error_msg = result.message if error_msg: @@ -104,7 +108,7 @@ class PlanPickerPage(Page): # cache data to save the choice to it: self.billing_data = result.data if self.billing_data: - print("got data") + print("got pre-existing data") def on_plan_clicked(self, item): plan_key = item.data(QtCore.Qt.ItemDataRole.UserRole) @@ -114,6 +118,9 @@ class PlanPickerPage(Page): print(f"plan_key is {plan_key}") # in temp cache save choice: + if not self.billing_data: + self.show_a_popup("Developer Error that ticket flow didn't produce a billing_data object in plan picker page.") + self.billing_data.which_key = plan_key print("saved the which_key. navigating to currency selection..") diff --git a/gui/v2/ui/pages/ticket_crypto_picker_page.py b/gui/v2/ui/pages/ticket_crypto_picker_page.py index 4194bd1..5873c4f 100755 --- a/gui/v2/ui/pages/ticket_crypto_picker_page.py +++ b/gui/v2/ui/pages/ticket_crypto_picker_page.py @@ -66,9 +66,9 @@ class TicketCryptoPickerPage(Page): currency = selected_button.property('currency') self.start_initiate_payment(currency) - def check_if_paid_for_existing(self, temp_billing_code): + def check_if_paid_for_existing(self, billing_code): self.update_status.update_status("Checking if paid...") - result = check_if_paid(temp_billing_code, ticket_observer, connection_observer) + result = check_if_paid(billing_code, ticket_observer, connection_observer) if isinstance(result, dict) and result.get('valid') and result.get('payment_status') == 'paid': self.update_status.update_status("Already Paid!") self.custom_window.navigator.navigate("ticket_prep") @@ -78,8 +78,8 @@ class TicketCryptoPickerPage(Page): else: self.update_status.update_status("Not yet paid.") clipboard = QApplication.clipboard() - clipboard.setText(temp_billing_code) - not_paid_msg = f"The billing code is not yet showing paid for {temp_billing_code}. Right now, your clipboard has the billing code, to paste it in any text editor. If you did pay, either wait longer for blockchain confirmation, or contact customer support with the code in your clipboard now." + clipboard.setText(billing_code) + not_paid_msg = f"The billing code is not yet showing paid for {billing_code}. Right now, your clipboard has the billing code, to paste it in any text editor. If you did pay, either wait longer for blockchain confirmation, or contact customer support with the code in your clipboard now." info = QMessageBox(self) info.setWindowTitle("Not Paid") info.setText(not_paid_msg) @@ -120,10 +120,10 @@ class TicketCryptoPickerPage(Page): self._prompt_wipe_existing(invoice) return elif error_code == ResultError.BILLING_CODE_EXISTS and not self.bypass_existing: - temp_billing_code = getattr(invoice, 'temp_billing_code', None) - print(f"temp_billing_code is {temp_billing_code}") - if temp_billing_code: - self._prompt_wipe_billingcode(temp_billing_code) + billing_code = getattr(invoice, 'billing_code', None) + print(f"billing_code is {billing_code}") + if billing_code: + self._prompt_wipe_billingcode(billing_code) return else: self._prompt_wipe_billingcode("NONE") @@ -151,7 +151,7 @@ class TicketCryptoPickerPage(Page): else: self.update_status.update_status("Cancelled.") - def _prompt_wipe_billingcode(self, temp_billing_code): + def _prompt_wipe_billingcode(self, billing_code): msg = QMessageBox(self) msg.setWindowTitle("Existing billing code found") msg.setText("You already have a ticket billing code. Do you want to use it? Only hit YES if you ALREADY paid.") @@ -161,7 +161,7 @@ class TicketCryptoPickerPage(Page): result = msg.exec() if result == QMessageBox.StandardButton.Yes: self.update_status.update_status("Reusing same code") - self.check_if_paid_for_existing(temp_billing_code) + self.check_if_paid_for_existing(billing_code) else: self.bypass_existing = True delete_ticket_data() diff --git a/gui/v2/workers/ticketing_worker_thread.py b/gui/v2/workers/ticketing_worker_thread.py index 1ea0d74..1ff4407 100755 --- a/gui/v2/workers/ticketing_worker_thread.py +++ b/gui/v2/workers/ticketing_worker_thread.py @@ -48,7 +48,7 @@ class TicketingWorkerThread(QThread): ) self.invoice_ready.emit(result) elif self.action == 'CHECK_PAID': - result = check_if_paid(self.params['temp_billing_code'], ticket_observer, connection_observer) + result = check_if_paid(self.params['billing_code'], ticket_observer, connection_observer) if isinstance(result, dict) and result.get('valid') and result.get('payment_status') == 'paid': self.paid.emit() elif isinstance(result, dict) and result.get('valid'):