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

View file

@ -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
)
check_paid_timer.start_polling()
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
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

View file

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

View file

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

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.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,

View file

@ -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:
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")
self.billing_data.plan_id = plan_id
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):

View file

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

View file

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

View file

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