diff --git a/gui/v2/actions/menu_coordinator.py b/gui/v2/actions/menu_coordinator.py new file mode 100644 index 0000000..bd273e8 --- /dev/null +++ b/gui/v2/actions/menu_coordinator.py @@ -0,0 +1,72 @@ +from core.services.subscriptions.subscriptions_by_protocol import get_profile_subscription, start_new_payment_cycle +from core.models.enum_models import PaymentCategory +from core.models.Result import Result, ResultError +from core.errors.logger import logger + +from gui.v2.infrastructure.take_plumbing_action import take_action + +# generic +from typing import Callable, Optional + + +def confirm_prereqs(PRE_REQS: list, allow_false: Optional[bool] = False) -> Result: + for each_prereq in PRE_REQS: + if allow_false: + if each_prereq == None: + return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=f"You are missing {each_prereq}") + else: + if not each_prereq: + return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=f"You are missing {each_prereq}") + return Result(valid=True) + + +def filter_subscription(profile_id: int) -> 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) + + if not subscription or not subscription.billing_code: + # auto check tickets would be here. + return Result(valid=False, error_type=ResultError.NAVIGATE_TO, navigate_to="id", message="No subscription. Pick a payment method.") + + 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") + else: + logger.info(f"Returning the error of start_result {start_result}") + return start_result + + # has a valid sub: + logger.info(f"Profile id {profile_id} has a valid subscription ready to go..") + return Result(valid=True) + + +def _connect_profile(profile_id: int) -> Result: + # Step 1: Subscription filter: + sub_result = filter_subscription(profile_id=profile_id) + if not sub_result.valid: + return sub_result + + # fake connection + logger.info(f"Profile id {profile_id} is ready to connect..") + return Result(valid=True) + + +def connect_profile(profile_id: int, ui_caller: Callable) -> Result: + PRE_REQS = [profile_id, ui_caller] + + confirmed_result = confirm_prereqs(PRE_REQS) + if not confirmed_result.valid: + return confirmed_result + + take_action( + action=_connect_profile, + ui_caller=ui_caller, + profile_id=profile_id + ) + return Result(valid=True) + + diff --git a/gui/v2/actions/menu_tasks.py b/gui/v2/actions/menu_tasks.py index 886fb1a..25536c6 100755 --- a/gui/v2/actions/menu_tasks.py +++ b/gui/v2/actions/menu_tasks.py @@ -1,8 +1,12 @@ +from core.services.subscriptions.subscriptions_by_protocol import get_profile_subscription +from core.models.Result import Result, ResultError +from core.errors.logger import logger + + from functools import partial from typing import Any, Iterable, Mapping from PyQt6.QtCore import Qt - from gui.v2.actions.menu_results import MenuCallback from gui.v2.actions.profile_operations import execute_menu_task from gui.v2.infrastructure.screen_size import get_max_screensize @@ -29,6 +33,14 @@ def _start_task(request: Mapping[str, Any], on_result: MenuCallback) -> Worker: return worker +def pre_connect_clear(profile_id: int) -> Result: + subscription = get_profile_subscription(profile_id=profile_id) + if not subscription or not subscription.billing_code: + return Result(valid=False, error_type=ResultError.SUBSCRIPTION, message="No Subscription") + + return Result(valid=True) + + def enable_profile(profile_data: Mapping[str, Any], on_result: MenuCallback, allow_uninstalled_tor: bool = False) -> Worker: """Start the complete activation workflow with billing and retry data kept in one request.""" return _start_task({**profile_data, "task": "enable", "allow_uninstalled_tor": allow_uninstalled_tor}, on_result) diff --git a/gui/v2/actions/payment_actions.py b/gui/v2/actions/payment_actions.py index e707f5e..314dd4b 100644 --- a/gui/v2/actions/payment_actions.py +++ b/gui/v2/actions/payment_actions.py @@ -1,12 +1,16 @@ # 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.services.subscriptions.subscriptions_by_protocol import ( + create_new_invoice, save_billing_code, + get_profile_subscription, 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.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 @@ -16,16 +20,6 @@ from gui.v2.infrastructure.setup_observers import connection_observer 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}") @@ -33,103 +27,62 @@ def gui_starts_invoice(invoice_ui_caller: Callable, timer_resume_caller: Callabl # 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) + 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 "Need a profile id for an individual profile." + return return_pre_existing_invoice(billing_code=billing_code, connection_observer=connection_observer) - 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, + action=create_new_invoice, ui_caller=invoice_ui_caller, - profile_id=profile_id, + # 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 #---------------------------- - payment_cycle = OverallPaymentCycle() if payment_cycle.already_polling: - logger.info("This cycle is already polling, so we don't need to start another poll") + 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 #---------------------------- - 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." + if not billing_code: + return "Requires a billing code" #---------------------------- # CHECK PAID ON PRE-EXISTING INVOICE #---------------------------- - logger.info("Resuming a pre-existing payment..") + logger.info("Starting a check paid timer..") # Setup PyQt6 GUI as mainthread to bring back to: pyqt_threader = ThreadSafetyTool() @@ -140,63 +93,21 @@ def fetch_individual_invoice(invoice_ui_caller: Callable, timer_resume_caller: C # 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, + view_callback=ui_caller, + billing_code=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 + return None -def stop_payment_timer() -> tuple: +@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, so this is not the active poller (blocking others) wipe_billing_cycle() @@ -207,12 +118,174 @@ def stop_payment_timer() -> tuple: check_paid_timer.stop_polling() content_to_display = "Stopped! You can resume a different profile's invoice now" - return True, content_to_display + return Result(valid=True, message=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 + 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 diff --git a/gui/v2/actions/profile_operations.py b/gui/v2/actions/profile_operations.py index e3340c9..188fd64 100755 --- a/gui/v2/actions/profile_operations.py +++ b/gui/v2/actions/profile_operations.py @@ -198,6 +198,12 @@ def _enable(request: dict[str, Any], emit: MenuCallback, max_resolution: str | N except Exception as e: print(f"Skipping billing resume check") + # print("checking sub") + # if not profile.subscription.billing: + # raise MissingSubscriptionError() + + + print(f"checking preparation") preparation = _prepare_subscription(profile, request) diff --git a/gui/v2/actions/ticket_actions.py b/gui/v2/actions/ticket_actions.py index 261ec5c..2fa7a69 100644 --- a/gui/v2/actions/ticket_actions.py +++ b/gui/v2/actions/ticket_actions.py @@ -3,7 +3,7 @@ from core.controllers.tickets.UseTicketController import use_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.services.subscriptions.subscriptions_by_protocol import start_billing_state, save_subscription +from core.services.subscriptions.subscriptions_by_protocol import start_new_payment_cycle, save_subscription 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 @@ -21,50 +21,54 @@ from typing import Callable def start_ticket(ui_caller: Callable) -> Result: logger.info("started ticketing billing state") return take_action( - action=start_billing_state, - ui_caller=ui_caller, + action=start_new_payment_cycle, category=PaymentCategory.TICKET + # ui_caller=ui_caller, ) def fetch_ticket_invoice(invoice_ui_caller: Callable, timer_resume_caller: Callable) -> str | None: - """Start the Ticketing process on the background thread with the relevant billing data.""" + pass + return None - logger.info("fetching ticket invoice..") +# def fetch_ticket_invoice(invoice_ui_caller: Callable, timer_resume_caller: Callable) -> str | None: +# """Start the Ticketing process on the background thread with the relevant billing data.""" - payment_cycle = OverallPaymentCycle() +# logger.info("fetching ticket invoice..") - if not payment_cycle: - "No payment cycle, and therefore no data." +# payment_cycle = OverallPaymentCycle() - # already existing but wrong format: - if payment_cycle.category != PaymentCategory.TICKET: - return "Wrong payment cycle category." +# if not payment_cycle: +# "No payment cycle, and therefore no data." - billing_data = payment_cycle.instance +# # already existing but wrong format: +# if payment_cycle.category != PaymentCategory.TICKET: +# return "Wrong payment cycle category." - if not billing_data: - return "No billing data" +# billing_data = payment_cycle.instance - # if not isinstance(billing_data, TicketPaymentData): - # return "Billing data in the wrong format." +# if not billing_data: +# return "No billing data" - which_key = billing_data.which_key - currency = billing_data.currency - how_many_profiles = billing_data.how_many_profiles +# # if not isinstance(billing_data, TicketPaymentData): +# # return "Billing data in the wrong format." - # Fetch (this function will complain if missing data): - take_action( - action=initiate_payment, - ui_caller=invoice_ui_caller, - how_many_profiles=how_many_profiles, - which_key=which_key, - which_cryptocurrency=currency, - ticket_observer=ticket_observer, - connection_observer=connection_observer, - bypass_existing=True, - ) - logger.info("Initiated payment") +# which_key = billing_data.which_key +# currency = billing_data.currency +# how_many_profiles = billing_data.how_many_profiles + +# # Fetch (this function will complain if missing data): +# take_action( +# action=initiate_payment, +# ui_caller=invoice_ui_caller, +# how_many_profiles=how_many_profiles, +# which_key=which_key, +# which_cryptocurrency=currency, +# ticket_observer=ticket_observer, +# connection_observer=connection_observer, +# bypass_existing=True, +# ) +# logger.info("Initiated payment") def get_ticket_data() -> Result: diff --git a/gui/v2/infrastructure/action_registry.py b/gui/v2/infrastructure/action_registry.py new file mode 100644 index 0000000..7d4ede8 --- /dev/null +++ b/gui/v2/infrastructure/action_registry.py @@ -0,0 +1,40 @@ +from core.models.Result import Result, ResultError +from core.controllers.SyncController import new_sync + +# from gui.v2.actions.payment_actions import stop_payment_timer + +ENUM_MAP = { + ResultError.UNKNOWN: None, + ResultError.NEED_SYNC: (new_sync), + ResultError.INVALID_API_REPLY: None, + ResultError.ENCRYPTION: None, + # ResultError.ACTIVE_BILLING: (stop_payment_timer), +} + +def register_action(error_code): + def wrapper(func): + ENUM_MAP[error_code] = (func) + return func + return wrapper + + + + +# Add later potentially: + +# ResultError.NO_TICKET: (start_billing), +# ResultError.NO_PROFILE: (go_to_new_profile_page), +# ResultError.ALREADY_EXISTS: (get_invoice), # bypass_existing: True +# ResultError.BILLING_CODE_EXISTS: (get_invoice) # bypass_existing: True + + + + +# Potential future upgrade to multiple error codes per function: + +# def register_action(*error_codes): +# def wrapper(func): +# for code in error_codes: +# ACTION_REGISTRY[code] = func +# return func +# return wrapper diff --git a/gui/v2/infrastructure/take_plumbing_action.py b/gui/v2/infrastructure/take_plumbing_action.py index 44d5bf2..42c7dcc 100644 --- a/gui/v2/infrastructure/take_plumbing_action.py +++ b/gui/v2/infrastructure/take_plumbing_action.py @@ -1,5 +1,7 @@ from core.infrastructure.plumbing import ThreadPlumbing from core.models.Result import ResultError + +from gui.v2.infrastructure.action_registry import ENUM_MAP from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool from gui.v2.ui.popups.error_popup import ErrorPopup @@ -17,6 +19,7 @@ def take_action(action: Callable, ui_caller: Callable, *args, **kwargs): action=action, ui_dispatcher=ThreadSafetyTool(), # PyQt6 as mainthread error_implementation=ErrorPopup, # PyQt6 ErrorPopup + ENUM_MAP=ENUM_MAP, auto_navigate_dict=AUTO_NAVIGATE_REGISTRY, ui_caller=ui_caller, *args, diff --git a/gui/v2/ui/pages/duration_selection_page.py b/gui/v2/ui/pages/duration_selection_page.py index e71f80c..198f733 100755 --- a/gui/v2/ui/pages/duration_selection_page.py +++ b/gui/v2/ui/pages/duration_selection_page.py @@ -2,10 +2,12 @@ from core.services.payment_phase.payment_state import PaymentData from gui.v2.infrastructure.take_plumbing_action import take_action from core.controllers.profile_state.data_filters import get_plans_for_profile -from core.models.Result import Result #, ResultError +from core.services.subscriptions.subscriptions_by_protocol import get_current_billing_data +from core.models.enum_models import PaymentCategory +from core.models.Result import Result # GUI -from gui.v2.actions.payment_actions import new_individual_billing +# from gui.v2.actions.payment_actions import new_individual_billing from gui.v2.ui.styles.payment_styles import DURATION_SELECTION_STYLE, NEXT_BUTTON_STYLE from gui.v2.ui.pages.Page import Page @@ -52,19 +54,6 @@ class DurationSelectionPage(Page): # get profile id: profile_id = int(self.update_status.current_profile_id) - # start entire payment cycle process: - result = new_individual_billing(profile_id=profile_id) - if not result.valid: - self.show_a_popup(str(result.message)) - return - - # setup singleton - self.billing_data = result.data - # PaymentData(instance_id=profile_id) - - if self.billing_data: - print(f"we have billing data: {self.billing_data}") - # Thread SQL in background: take_action( action=get_plans_for_profile, @@ -100,13 +89,26 @@ class DurationSelectionPage(Page): plan_id = self.dict_of_choices[current_choice] # save to singleton across pages: - print(f"saving plan_id to the billing data") - self.billing_data.plan_id = plan_id + data_result = get_current_billing_data(category=PaymentCategory.INDIVIDUAL) + + if not data_result.valid: + message = data_result.message + if not message: + message = "Error fetching data" + self.show_a_popup(str(message)) + + billing_data = data_result.data + + if billing_data: + print(f"we have billing data: {billing_data}") + print(f"saving plan_id to the billing data") + billing_data.plan_id = plan_id + else: + message = "No Billing Data" + self.show_a_popup(message) # go to next page: print("going to the next page") - # currency_page = self.custom_window.navigator.get_cached("currency_selection") - # if currency_page is not None: self.custom_window.navigator.navigate("currency_selection") def reverse(self): diff --git a/gui/v2/ui/pages/id_page.py b/gui/v2/ui/pages/id_page.py index e012d70..7b8e5ba 100755 --- a/gui/v2/ui/pages/id_page.py +++ b/gui/v2/ui/pages/id_page.py @@ -8,7 +8,9 @@ from PyQt6.QtGui import QIcon, QPixmap from PyQt6.QtCore import Qt from PyQt6 import QtCore -from core.services.payment_phase.ticket_config_tools import do_we_have_billing_id +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 gui.v2.ui.pages.Page import Page @@ -152,14 +154,35 @@ class IdPage(Page): self.separator_right.setGeometry(540, 60, 2, 420) self.separator_right.setStyleSheet(separator_style) + def go_single_profile(self): self.text_edit.clear() - self.custom_window.navigator.navigate("duration_selection") + print("starting a single profile payment cycle") + result = start_new_payment_cycle( + category=PaymentCategory.INDIVIDUAL, + profile_id=int(self.update_status.current_profile_id) + ) + if not result: + print("no result.") + return + + if result.valid: + billing_code = result.data + print(f"billing_code is {billing_code}") + self.custom_window.navigator.navigate("duration_selection") + else: + self.show_a_popup(str(result.message)) def go_multiple_profiles(self): - billing_id = do_we_have_billing_id() - if billing_id: - self._prompt_existing_tickets(billing_id) + result = start_new_payment_cycle( + category=PaymentCategory.TICKET, + ) + if not result.valid: + self.show_a_popup(str(result.message)) + + billing_code = result.billing_code + if billing_code: + self._prompt_existing_tickets(billing_code) else: self._continue_multiple_flow() diff --git a/gui/v2/ui/pages/menu_page.py b/gui/v2/ui/pages/menu_page.py index 011e9ab..6cc1bfd 100755 --- a/gui/v2/ui/pages/menu_page.py +++ b/gui/v2/ui/pages/menu_page.py @@ -1,3 +1,5 @@ +from core.models.Result import Result, ResultError + import functools import os from itertools import chain @@ -10,10 +12,12 @@ from PyQt6.QtWidgets import ( QMessageBox, QPushButton, QScrollArea, QTextEdit, QVBoxLayout, QWidget, ) +from gui.v2.actions.menu_coordinator import connect_profile + from gui.v2.actions.menu_profiles import find_verification_icon, get_profile_details, load_menu_profiles, menu_icons from gui.v2.actions.menu_tasks import ( continue_menu_task, create_profile, delete_profile, disable_profile, - download_update, edit_profile, enable_profile, + download_update, edit_profile, enable_profile ) from gui.v2.actions.menu_results import MenuResult from gui.v2.ui.pages.Page import Page @@ -393,7 +397,10 @@ class MenuPage(Page): def _render_profile_details(self, details): self.reverse_id = details["id"] + + # THIS IS UPDATING THE PROFILE ID self.update_status.current_profile_id = self.reverse_id + self.current_profile_location = details["location"] target_button = self.profile_button_map.get(self.reverse_id) if target_button is not None: @@ -514,12 +521,36 @@ class MenuPage(Page): self.boton_edit.setEnabled(not busy and self.reverse_id is not None and not self.connection_manager.is_profile_connected(self.reverse_id)) + + def handle_result(self, result: Result): + print(f"handle error got result {result}") + if result.error_type == ResultError.NAVIGATE_TO: + print(f"Navigating to page {result.navigate_to}") + self.custom_window.navigator.navigate(result.navigate_to) + + elif result.error_type == ResultError.SUBSCRIPTION: + print("going to sub page") + self.update_status.update_status("Select Subscription Choice") + self.custom_window.navigator.navigate("id") + else: + error_type = result.error_type.value + print(f"handle_error was called, but we do not currently have a pre-made solution for the unknown situation of {error_type}") + self.update_status.update_status(error_type) + def connect(self): print(f"hit connect to enable profile. self.reverse_id is {self.reverse_id}") + pre_thread_result = connect_profile(profile_id=self.reverse_id, ui_caller=self.handle_result) + + if not pre_thread_result.valid: + return + + # Temporary until moved to menu coordinator: + print("connecting...") self._set_task_busy(True) self.update_status.update_status("Attempting to connect...") self.worker = enable_profile({"id": self.reverse_id}, self.handle_task_result) + def disconnect(self): self._set_task_busy(True) self.update_status.update_status("Disconnecting...") diff --git a/gui/v2/ui/pages/payments2.py b/gui/v2/ui/pages/payments2.py index ef5c79f..7b66d11 100644 --- a/gui/v2/ui/pages/payments2.py +++ b/gui/v2/ui/pages/payments2.py @@ -8,7 +8,7 @@ from core.models.Result import Result # GUI from gui.v2.actions.payment_actions import ( - save_and_begin_timer, + start_polling, gui_starts_invoice, stop_payment_timer, ) @@ -76,8 +76,8 @@ class PaymentDetailsPage(Page): self.create_initial_elements() self.button_reverse.setVisible(True) - # self.begin_invoice() + # self.begin_invoice() def showEvent(self, event): """Refresh data. This runs every time the widget is displayed""" @@ -183,11 +183,7 @@ class PaymentDetailsPage(Page): print(f"profile_id is {self.profile_id}") if not self.ticketing and not self.profile_id: - self.display_error( - whats_wrong=DataError.BLANK, - whats_it_called="Profile ID", - the_thing=profile_id, - ) + self.show_a_popup(f"Missing a profile id for an individual profile.") return # this is not used here, but this is the right place to put it. Since we should have it by now: @@ -214,9 +210,11 @@ class PaymentDetailsPage(Page): """Toggle On/Off Polling""" # If it's on: if self.polling: - stopped, message = stop_payment_timer() + timer_result = stop_payment_timer() - if stopped: + message = timer_result.message + + if not timer_result.valid: self.update_status.update_status("Stopped checking!") self.stop_button.setDisabled(True) self.switch_to_background_button.setDisabled(True) @@ -225,9 +223,9 @@ class PaymentDetailsPage(Page): if message: self.show_a_popup(str(message)) - # Turn it back on if off: - else: - self.begin_invoice() + # # Turn it back on if off: + # else: + # self.begin_invoice() def background_fetch_mode(self): """Continue checking if paid, but navigate back to the menu page""" @@ -284,37 +282,22 @@ class PaymentDetailsPage(Page): invoice = invoice_result.data if not invoice: - self.display_error( - whats_wrong=DataError.BLANK, - whats_it_called="invoice", - the_thing=invoice, - ) + self.show_a_popup("Missing Invoice from API call's data") return - # if not isinstance(invoice, SubscriptionInvoice): - # self.display_error( - # whats_wrong=DataError.FORMAT, - # whats_it_called="invoice", - # the_thing=invoice, - # ) - # return - + # Fill in UI: self.fill_in_invoice_data(invoice) - self.update_status.update_status("Invoice generated. Awaiting payment...") - # kick off the timer and save the billing id, - saved_and_started = save_and_begin_timer( - ticketing=paying_for_tickets(), - gui_callback=self.timer_updates_gui, + # Start polling: + error_msg = start_polling( billing_code=invoice.billing_code, profile_id=int(self.update_status.current_profile_id), - expires_at=invoice.expires_at, + ui_caller=self.timer_updates_gui ) - if not saved_and_started: - self.show_a_popup( - "The save and start checking if paid did not even begin. Please see the error logs/" - ) - self.update_status.update_status("Could not begin") + if error_msg: + self.show_a_popup(error_msg) + else: + self.update_status.update_status("Invoice generated. Awaiting payment...") def fill_in_invoice_data(self, invoice: SubscriptionInvoice): """Extract and populate invoice data into the labels""" @@ -439,6 +422,8 @@ class PaymentDetailsPage(Page): # get the singleton (this was created on a previous page) billing_singleton = data_result.data + print(f"Inside payments screen GUI of _get_currency, the billing_singleton is {billing_singleton}") + if not billing_singleton: message = "Error, no billing data" self.show_a_popup(str(message)) @@ -477,3 +462,7 @@ class PaymentDetailsPage(Page): prep_page = self.custom_window.navigator.get_cached("ticket_prep") if prep_page is not None: prep_page.start_prep() + + + +