Individual Profile Payments Bigbang is now Stable. It allows for detached payment checks with proper error prompts, if users try to open a 2nd individual payments. Additionally we introduced core's threading plumbing, with a registry wrapper for actions to be routed to automatically on error prompt

This commit is contained in:
SimplifiedPrivacy 2026-10-13 21:28:37 -04:00
parent 20f405e878
commit 1308b025c2
11 changed files with 485 additions and 230 deletions

View file

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

View file

@ -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 functools import partial
from typing import Any, Iterable, Mapping from typing import Any, Iterable, Mapping
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
from gui.v2.actions.menu_results import MenuCallback from gui.v2.actions.menu_results import MenuCallback
from gui.v2.actions.profile_operations import execute_menu_task from gui.v2.actions.profile_operations import execute_menu_task
from gui.v2.infrastructure.screen_size import get_max_screensize 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 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: 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.""" """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) return _start_task({**profile_data, "task": "enable", "allow_uninstalled_tor": allow_uninstalled_tor}, on_result)

View file

@ -1,12 +1,16 @@
# core # core
from core.services.payment_phase.payment_state import OverallPaymentCycle, PaymentData, TicketPaymentData from core.services.payment_phase.payment_state import OverallPaymentCycle, PaymentData, TicketPaymentData
from core.services.payment_phase.check_payment_loop import CheckPaidTimer 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.enum_models import PaymentType, PaymentCategory
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
# gui # gui
from gui.v2.infrastructure.action_registry import register_action
from gui.v2.actions.ticket_actions import fetch_ticket_invoice from gui.v2.actions.ticket_actions import fetch_ticket_invoice
from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool
from gui.v2.infrastructure.take_plumbing_action import take_action 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 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: 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}") 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? # Is there currently a process?
payment_cycle = OverallPaymentCycle() payment_cycle = OverallPaymentCycle()
# start a new individual one if not existing:
if not payment_cycle: if not payment_cycle:
logger.info("There's no current payment cycle") error_msg = "There is no payment cycle yet"
if profile_id: logger.error(error_msg)
return fetch_individual_invoice(invoice_ui_caller=invoice_ui_caller, timer_resume_caller=timer_resume_caller, profile_id=profile_id) 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: 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( take_action(
action=create_individual_invoice, action=create_new_invoice,
ui_caller=invoice_ui_caller, ui_caller=invoice_ui_caller,
profile_id=profile_id, # profile_id=profile_id,
connection_observer=connection_observer 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 # FILTER ALREADY POLLING
#---------------------------- #----------------------------
payment_cycle = OverallPaymentCycle()
if payment_cycle.already_polling: 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 return None
#---------------------------- #----------------------------
# FILTER LACK OF DATA # FILTER LACK OF DATA
#---------------------------- #----------------------------
subscription = get_profile_subscription(profile_id) if not billing_code:
return "Requires a billing code"
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."
#---------------------------- #----------------------------
# CHECK PAID ON PRE-EXISTING INVOICE # 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: # Setup PyQt6 GUI as mainthread to bring back to:
pyqt_threader = ThreadSafetyTool() 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: # setup the timer with the callback:
# it doesn't return result here. It calls the view_callback function. # it doesn't return result here. It calls the view_callback function.
check_paid_timer = CheckPaidTimer( check_paid_timer = CheckPaidTimer(
view_callback=timer_resume_caller, view_callback=ui_caller,
billing_code=subscription.billing_code, billing_code=billing_code,
ui_dispatcher=pyqt_threader, ui_dispatcher=pyqt_threader,
payment_type=PaymentType.CRYPTO, payment_type=PaymentType.CRYPTO,
connection_observer=connection_observer, connection_observer=connection_observer,
profile_id=profile_id profile_id=profile_id
) )
check_paid_timer.start_polling()
return None return None
@register_action(ResultError.ACTIVE_BILLING)
def save_and_begin_timer( def stop_payment_timer() -> Result:
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
def stop_payment_timer() -> tuple:
"""Gets the single instance of the payment checker timer, and stops it.""" """Gets the single instance of the payment checker timer, and stops it."""
logger.info("Stopping the check paid timer & wiping the cycle..")
try: try:
# Wipe the cycle, so this is not the active poller (blocking others) # Wipe the cycle, so this is not the active poller (blocking others)
wipe_billing_cycle() wipe_billing_cycle()
@ -207,12 +118,174 @@ def stop_payment_timer() -> tuple:
check_paid_timer.stop_polling() check_paid_timer.stop_polling()
content_to_display = "Stopped! You can resume a different profile's invoice now" 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: except Exception as e:
error_msg = f"Unable to stop the payment timer: {str(e)}" error_msg = f"Unable to stop the payment timer: {str(e)}"
logger.error(error_msg) 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

View file

@ -198,6 +198,12 @@ def _enable(request: dict[str, Any], emit: MenuCallback, max_resolution: str | N
except Exception as e: except Exception as e:
print(f"Skipping billing resume check") print(f"Skipping billing resume check")
# print("checking sub")
# if not profile.subscription.billing:
# raise MissingSubscriptionError()
print(f"checking preparation") print(f"checking preparation")
preparation = _prepare_subscription(profile, request) preparation = _prepare_subscription(profile, request)

View file

@ -3,7 +3,7 @@ from core.controllers.tickets.UseTicketController import use_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_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.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
@ -21,50 +21,54 @@ from typing import Callable
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(
action=start_billing_state, action=start_new_payment_cycle,
ui_caller=ui_caller,
category=PaymentCategory.TICKET category=PaymentCategory.TICKET
# ui_caller=ui_caller,
) )
def fetch_ticket_invoice(invoice_ui_caller: Callable, timer_resume_caller: Callable) -> str | None: 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: # payment_cycle = OverallPaymentCycle()
"No payment cycle, and therefore no data."
# already existing but wrong format: # if not payment_cycle:
if payment_cycle.category != PaymentCategory.TICKET: # "No payment cycle, and therefore no data."
return "Wrong payment cycle category."
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: # billing_data = payment_cycle.instance
return "No billing data"
# if not isinstance(billing_data, TicketPaymentData): # if not billing_data:
# return "Billing data in the wrong format." # return "No billing data"
which_key = billing_data.which_key # # if not isinstance(billing_data, TicketPaymentData):
currency = billing_data.currency # # return "Billing data in the wrong format."
how_many_profiles = billing_data.how_many_profiles
# Fetch (this function will complain if missing data): # which_key = billing_data.which_key
take_action( # currency = billing_data.currency
action=initiate_payment, # how_many_profiles = billing_data.how_many_profiles
ui_caller=invoice_ui_caller,
how_many_profiles=how_many_profiles, # # Fetch (this function will complain if missing data):
which_key=which_key, # take_action(
which_cryptocurrency=currency, # action=initiate_payment,
ticket_observer=ticket_observer, # ui_caller=invoice_ui_caller,
connection_observer=connection_observer, # how_many_profiles=how_many_profiles,
bypass_existing=True, # which_key=which_key,
) # which_cryptocurrency=currency,
logger.info("Initiated payment") # ticket_observer=ticket_observer,
# connection_observer=connection_observer,
# bypass_existing=True,
# )
# logger.info("Initiated payment")
def get_ticket_data() -> Result: def get_ticket_data() -> Result:

View file

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

View file

@ -1,5 +1,7 @@
from core.infrastructure.plumbing import ThreadPlumbing from core.infrastructure.plumbing import ThreadPlumbing
from core.models.Result import ResultError 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.infrastructure.ThreadSafetyTool import ThreadSafetyTool
from gui.v2.ui.popups.error_popup import ErrorPopup 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, action=action,
ui_dispatcher=ThreadSafetyTool(), # PyQt6 as mainthread ui_dispatcher=ThreadSafetyTool(), # PyQt6 as mainthread
error_implementation=ErrorPopup, # PyQt6 ErrorPopup error_implementation=ErrorPopup, # PyQt6 ErrorPopup
ENUM_MAP=ENUM_MAP,
auto_navigate_dict=AUTO_NAVIGATE_REGISTRY, auto_navigate_dict=AUTO_NAVIGATE_REGISTRY,
ui_caller=ui_caller, ui_caller=ui_caller,
*args, *args,

View file

@ -2,10 +2,12 @@
from core.services.payment_phase.payment_state import PaymentData from core.services.payment_phase.payment_state import PaymentData
from gui.v2.infrastructure.take_plumbing_action import take_action from gui.v2.infrastructure.take_plumbing_action import take_action
from core.controllers.profile_state.data_filters import get_plans_for_profile 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 # 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.styles.payment_styles import DURATION_SELECTION_STYLE, NEXT_BUTTON_STYLE
from gui.v2.ui.pages.Page import Page from gui.v2.ui.pages.Page import Page
@ -52,19 +54,6 @@ class DurationSelectionPage(Page):
# get profile id: # get profile id:
profile_id = int(self.update_status.current_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: # Thread SQL in background:
take_action( take_action(
action=get_plans_for_profile, action=get_plans_for_profile,
@ -100,13 +89,26 @@ 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:
print(f"saving plan_id to the billing data") data_result = get_current_billing_data(category=PaymentCategory.INDIVIDUAL)
self.billing_data.plan_id = plan_id
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: # go to next page:
print("going to the 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") self.custom_window.navigator.navigate("currency_selection")
def reverse(self): def reverse(self):

View file

@ -8,7 +8,9 @@ from PyQt6.QtGui import QIcon, QPixmap
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
from PyQt6 import QtCore 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 core.services.prepare_tickets.ticket_tracker import delete_ticket_data
from gui.v2.ui.pages.Page import Page 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.setGeometry(540, 60, 2, 420)
self.separator_right.setStyleSheet(separator_style) self.separator_right.setStyleSheet(separator_style)
def go_single_profile(self): def go_single_profile(self):
self.text_edit.clear() 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): def go_multiple_profiles(self):
billing_id = do_we_have_billing_id() result = start_new_payment_cycle(
if billing_id: category=PaymentCategory.TICKET,
self._prompt_existing_tickets(billing_id) )
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: else:
self._continue_multiple_flow() self._continue_multiple_flow()

View file

@ -1,3 +1,5 @@
from core.models.Result import Result, ResultError
import functools import functools
import os import os
from itertools import chain from itertools import chain
@ -10,10 +12,12 @@ from PyQt6.QtWidgets import (
QMessageBox, QPushButton, QScrollArea, QTextEdit, QVBoxLayout, QWidget, 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_profiles import find_verification_icon, get_profile_details, load_menu_profiles, menu_icons
from gui.v2.actions.menu_tasks import ( from gui.v2.actions.menu_tasks import (
continue_menu_task, create_profile, delete_profile, disable_profile, 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.actions.menu_results import MenuResult
from gui.v2.ui.pages.Page import Page from gui.v2.ui.pages.Page import Page
@ -393,7 +397,10 @@ class MenuPage(Page):
def _render_profile_details(self, details): def _render_profile_details(self, details):
self.reverse_id = details["id"] self.reverse_id = details["id"]
# THIS IS UPDATING THE PROFILE ID
self.update_status.current_profile_id = self.reverse_id self.update_status.current_profile_id = self.reverse_id
self.current_profile_location = details["location"] self.current_profile_location = details["location"]
target_button = self.profile_button_map.get(self.reverse_id) target_button = self.profile_button_map.get(self.reverse_id)
if target_button is not None: 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 self.boton_edit.setEnabled(not busy and self.reverse_id is not None
and not self.connection_manager.is_profile_connected(self.reverse_id)) 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): def connect(self):
print(f"hit connect to enable profile. self.reverse_id is {self.reverse_id}") 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._set_task_busy(True)
self.update_status.update_status("Attempting to connect...") self.update_status.update_status("Attempting to connect...")
self.worker = enable_profile({"id": self.reverse_id}, self.handle_task_result) self.worker = enable_profile({"id": self.reverse_id}, self.handle_task_result)
def disconnect(self): def disconnect(self):
self._set_task_busy(True) self._set_task_busy(True)
self.update_status.update_status("Disconnecting...") self.update_status.update_status("Disconnecting...")

View file

@ -8,7 +8,7 @@ from core.models.Result import Result
# GUI # GUI
from gui.v2.actions.payment_actions import ( from gui.v2.actions.payment_actions import (
save_and_begin_timer, start_polling,
gui_starts_invoice, gui_starts_invoice,
stop_payment_timer, stop_payment_timer,
) )
@ -76,8 +76,8 @@ class PaymentDetailsPage(Page):
self.create_initial_elements() self.create_initial_elements()
self.button_reverse.setVisible(True) self.button_reverse.setVisible(True)
# self.begin_invoice()
# self.begin_invoice()
def showEvent(self, event): def showEvent(self, event):
"""Refresh data. This runs every time the widget is displayed""" """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}") print(f"profile_id is {self.profile_id}")
if not self.ticketing and not self.profile_id: if not self.ticketing and not self.profile_id:
self.display_error( self.show_a_popup(f"Missing a profile id for an individual profile.")
whats_wrong=DataError.BLANK,
whats_it_called="Profile ID",
the_thing=profile_id,
)
return return
# this is not used here, but this is the right place to put it. Since we should have it by now: # 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""" """Toggle On/Off Polling"""
# If it's on: # If it's on:
if self.polling: 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.update_status.update_status("Stopped checking!")
self.stop_button.setDisabled(True) self.stop_button.setDisabled(True)
self.switch_to_background_button.setDisabled(True) self.switch_to_background_button.setDisabled(True)
@ -225,9 +223,9 @@ class PaymentDetailsPage(Page):
if message: if message:
self.show_a_popup(str(message)) self.show_a_popup(str(message))
# Turn it back on if off: # # Turn it back on if off:
else: # else:
self.begin_invoice() # self.begin_invoice()
def background_fetch_mode(self): def background_fetch_mode(self):
"""Continue checking if paid, but navigate back to the menu page""" """Continue checking if paid, but navigate back to the menu page"""
@ -284,37 +282,22 @@ class PaymentDetailsPage(Page):
invoice = invoice_result.data invoice = invoice_result.data
if not invoice: if not invoice:
self.display_error( self.show_a_popup("Missing Invoice from API call's data")
whats_wrong=DataError.BLANK,
whats_it_called="invoice",
the_thing=invoice,
)
return return
# if not isinstance(invoice, SubscriptionInvoice): # Fill in UI:
# self.display_error(
# whats_wrong=DataError.FORMAT,
# whats_it_called="invoice",
# the_thing=invoice,
# )
# return
self.fill_in_invoice_data(invoice) self.fill_in_invoice_data(invoice)
self.update_status.update_status("Invoice generated. Awaiting payment...")
# kick off the timer and save the billing id, # Start polling:
saved_and_started = save_and_begin_timer( error_msg = start_polling(
ticketing=paying_for_tickets(),
gui_callback=self.timer_updates_gui,
billing_code=invoice.billing_code, billing_code=invoice.billing_code,
profile_id=int(self.update_status.current_profile_id), 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: if error_msg:
self.show_a_popup( self.show_a_popup(error_msg)
"The save and start checking if paid did not even begin. Please see the error logs/" else:
) self.update_status.update_status("Invoice generated. Awaiting payment...")
self.update_status.update_status("Could not begin")
def fill_in_invoice_data(self, invoice: SubscriptionInvoice): def fill_in_invoice_data(self, invoice: SubscriptionInvoice):
"""Extract and populate invoice data into the labels""" """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) # get the singleton (this was created on a previous page)
billing_singleton = data_result.data billing_singleton = data_result.data
print(f"Inside payments screen GUI of _get_currency, the billing_singleton is {billing_singleton}")
if not billing_singleton: if not billing_singleton:
message = "Error, no billing data" message = "Error, no billing data"
self.show_a_popup(str(message)) self.show_a_popup(str(message))
@ -477,3 +462,7 @@ class PaymentDetailsPage(Page):
prep_page = self.custom_window.navigator.get_cached("ticket_prep") prep_page = self.custom_window.navigator.get_cached("ticket_prep")
if prep_page is not None: if prep_page is not None:
prep_page.start_prep() prep_page.start_prep()