Improved multi-payment cycle state management feedback

This commit is contained in:
SimplifiedPrivacy 2026-10-17 11:31:07 -04:00
parent f7d792745a
commit 2cf3b60540
8 changed files with 167 additions and 43 deletions

View file

@ -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.
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)

View file

@ -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()
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)}"

View file

@ -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
)

View file

@ -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

View file

@ -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:

View file

@ -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..")

View file

@ -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()

View file

@ -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'):