From 307e6800f1fc30839d143b782e6e67cbd8234a13 Mon Sep 17 00:00:00 2001 From: SimplifiedPrivacy Date: Sat, 10 Oct 2026 18:28:13 -0400 Subject: [PATCH] Stable Ticket Invoices for the initial invoice --- gui/v2/actions/payment_actions.py | 47 +++++++------- gui/v2/actions/ticket_actions.py | 58 ++++++++++++++++- gui/v2/infrastructure/navigator.py | 11 ++-- gui/v2/ui/pages/currency_selection_page.py | 2 +- gui/v2/ui/pages/payments2.py | 73 ++++++++++++++------- gui/v2/ui/pages/plan_picker_page.py | 74 ++++++++++++---------- 6 files changed, 178 insertions(+), 87 deletions(-) diff --git a/gui/v2/actions/payment_actions.py b/gui/v2/actions/payment_actions.py index c759c19..e707f5e 100644 --- a/gui/v2/actions/payment_actions.py +++ b/gui/v2/actions/payment_actions.py @@ -7,6 +7,7 @@ from core.models.Result import Result, ResultError from core.errors.logger import logger # gui +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 @@ -51,33 +52,34 @@ def gui_starts_invoice(invoice_ui_caller: Callable, timer_resume_caller: Callabl return result billing_data = result.data - # already conflicting one: - elif profile_id and 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 - #---------------------------- # INVOICE DATA #---------------------------- if category == PaymentCategory.INDIVIDUAL: - if profile_id: - return fetch_individual_invoice(invoice_ui_caller=invoice_ui_caller, timer_resume_caller=timer_resume_caller, profile_id=profile_id) - else: + # already conflicting one: + if not profile_id: return "Need a profile id for an individual profile." - elif category == PaymentCategory.TICKET: - print("ready to start for tickets") + 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: @@ -162,9 +164,8 @@ def save_and_begin_timer( """ if ticketing: - payment_type = PaymentType.TICKETS - logger.error("Not yet setup for ticketing") - return False + 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: diff --git a/gui/v2/actions/ticket_actions.py b/gui/v2/actions/ticket_actions.py index cb805e6..2cb577e 100644 --- a/gui/v2/actions/ticket_actions.py +++ b/gui/v2/actions/ticket_actions.py @@ -1,15 +1,69 @@ # core -from core.services.payment_phase.payment_state import OverallPaymentCycle, PaymentData, TicketPaymentData -from core.models.enum_models import PaymentType +from core.controllers.tickets.TicketPayController import initiate_payment +from core.services.subscriptions.subscriptions_by_protocol import start_billing_state +from core.services.payment_phase.payment_state import OverallPaymentCycle, TicketPaymentData +from core.models.enum_models import PaymentType, PaymentCategory from core.controllers.tickets.UseTicketController import get_unused_tickets from core.models.Result import Result, ResultError # from core.controllers.ProfileController import ProfileController +from core.errors.logger import logger # gui 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, ticket_observer +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, + category=PaymentCategory.TICKET + ) + + +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.""" + + logger.info("fetching ticket invoice..") + + payment_cycle = OverallPaymentCycle() + + if not payment_cycle: + "No payment cycle, and therefore no data." + + # already existing but wrong format: + if payment_cycle.category != PaymentCategory.TICKET: + return "Wrong payment cycle category." + + billing_data = payment_cycle.instance + + if not billing_data: + return "No billing data" + + # if not isinstance(billing_data, TicketPaymentData): + # return "Billing data in the wrong format." + + 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/navigator.py b/gui/v2/infrastructure/navigator.py index f4af942..c0357e5 100755 --- a/gui/v2/infrastructure/navigator.py +++ b/gui/v2/infrastructure/navigator.py @@ -182,17 +182,18 @@ class Navigator(QObject): def _on_preload_data_ready(self, name, payload): self._preload_inflight -= 1 if name in self._instances: - logger.debug(f"[preload] {name:30s} SKIP (already cached, async result dropped)") + # logger.debug(f"[preload] {name:30s} SKIP (already cached, async result dropped)") + pass else: try: page = self._instantiate(name, prepared=payload) self.register_instance(name, page) self._preload_done += 1 - logger.debug(f"[preload] {name:30s} OK (async, cached={len(self._instances)}, " - f"stack_size={self.page_stack.count()})") + # logger.debug(f"[preload] {name:30s} OK (async, cached={len(self._instances)}, " + # f"stack_size={self.page_stack.count()})") except Exception as e: self._preload_failed += 1 - logger.debug(f"[preload] {name:30s} FAIL (async build) {type(e).__name__}: {e}") + # logger.debug(f"[preload] {name:30s} FAIL (async build) {type(e).__name__}: {e}") self._maybe_emit_preload_summary() def _on_preload_failed(self, name, error): @@ -204,7 +205,7 @@ class Navigator(QObject): self._preload_done += 1 else: self._preload_failed += 1 - self._maybe_emit_preload_summary() + # self._maybe_emit_preload_summary() def _cleanup_preload_worker(self, name): worker = self._preload_workers.pop(name, None) diff --git a/gui/v2/ui/pages/currency_selection_page.py b/gui/v2/ui/pages/currency_selection_page.py index 9821f8b..db0439e 100755 --- a/gui/v2/ui/pages/currency_selection_page.py +++ b/gui/v2/ui/pages/currency_selection_page.py @@ -5,7 +5,7 @@ from core.models.enum_models import Currency from core.services.payment_phase.payment_state import PaymentData # gui -from gui.v2.ui.pages.Page import Page +from gui.v2.ui.pages.Page import Page, DataError import os from PyQt6 import QtCore diff --git a/gui/v2/ui/pages/payments2.py b/gui/v2/ui/pages/payments2.py index 18f2d3d..29ff27b 100644 --- a/gui/v2/ui/pages/payments2.py +++ b/gui/v2/ui/pages/payments2.py @@ -1,7 +1,8 @@ # core -from core.services.subscriptions.subscriptions_by_protocol import paying_for_tickets +from core.services.subscriptions.subscriptions_by_protocol import paying_for_tickets, get_current_billing_data from core.services.payment_phase.payment_state import PaymentData from core.models.pydantic_models.ServerReply import SubscriptionInvoice +from core.models.invoice.TicketInvoice import TicketInvoice from core.models.enum_models import Currency from core.models.Result import Result @@ -40,6 +41,12 @@ from PyQt6.QtWidgets import ( ) from PyQt6.QtCore import Qt from PyQt6.QtGui import QIcon +from enum import Enum + +# For formatting errors graphically: +class DataError(Enum): + FORMAT: "format" + BLANK: "blank" class PaymentDetailsPage(Page): @@ -73,9 +80,12 @@ class PaymentDetailsPage(Page): def showEvent(self, event): + """Refresh data. This runs every time the widget is displayed""" + # Call the parent class showEvent first super().showEvent(event) - # runs every time the widget is displayed + + self.ticketing = paying_for_tickets() self.begin_invoice() @@ -172,7 +182,7 @@ class PaymentDetailsPage(Page): print(f"profile_id is {self.profile_id}") - if not self.profile_id: + if not self.ticketing and not self.profile_id: self.display_error( whats_wrong=DataError.BLANK, whats_it_called="Profile ID", @@ -281,13 +291,13 @@ class PaymentDetailsPage(Page): ) return - if not isinstance(invoice, SubscriptionInvoice): - self.display_error( - whats_wrong=DataError.FORMAT, - whats_it_called="invoice", - the_thing=invoice, - ) - return + # if not isinstance(invoice, SubscriptionInvoice): + # self.display_error( + # whats_wrong=DataError.FORMAT, + # whats_it_called="invoice", + # the_thing=invoice, + # ) + # return self.fill_in_invoice_data(invoice) self.update_status.update_status("Invoice generated. Awaiting payment...") @@ -309,6 +319,8 @@ class PaymentDetailsPage(Page): def fill_in_invoice_data(self, invoice: SubscriptionInvoice): """Extract and populate invoice data into the labels""" + print(f"filling in {invoice}") + # Extract from the already validated object: billing_code = str(invoice.billing_code) duration = str(invoice.expires_at) @@ -409,7 +421,7 @@ class PaymentDetailsPage(Page): self.custom_window.navigator.navigate("menu") def _get_currency(self): - """Get the currency from prior cached PaymentData""" + """Get the currency from prior cached Payment Data""" if not self.profile_id or not isinstance(self.profile_id, int): self.display_error( @@ -418,23 +430,36 @@ class PaymentDetailsPage(Page): the_thing=self.profile_id, ) - billing_data = PaymentData(instance_id=self.profile_id) + # get the singleton (this was created on a previous page) + data_result = get_current_billing_data() - if not billing_data: - self.display_error( - whats_wrong=DataError.BLANK, - whats_it_called="Profile's Billing Data", - the_thing=billing_data, - ) + if not data_result.valid: + message = data_result.message + if not message: + message = "Error fetching data" + self.show_a_popup(str(message)) - currency = billing_data.currency + # get the singleton (this was created on a previous page) + billing_singleton = data_result.data + + if not billing_singleton: + message = "Error, no billing data" + self.show_a_popup(str(message)) + + # billing_data = PaymentData(instance_id=self.profile_id) + + # if not billing_data: + # self.display_error( + # whats_wrong=DataError.BLANK, + # whats_it_called="Profile's Billing Data", + # the_thing=billing_data, + # ) + + currency = billing_singleton.currency if not currency: - self.display_error( - whats_wrong=DataError.BLANK, - whats_it_called="currency", - the_thing=currency, - ) + self.show_a_popup('Missing Currency') + if not isinstance(currency, Currency): self.display_error( diff --git a/gui/v2/ui/pages/plan_picker_page.py b/gui/v2/ui/pages/plan_picker_page.py index 8848be3..5f83ff4 100755 --- a/gui/v2/ui/pages/plan_picker_page.py +++ b/gui/v2/ui/pages/plan_picker_page.py @@ -1,9 +1,9 @@ # core -from core.services.subscriptions.subscriptions_by_protocol import start_billing_state from core.models.enum_models import PaymentCategory +from core.models.Result import Result # gui -# from gui.v2.actions.payment_actions import start_billing_state +from gui.v2.actions.ticket_actions import start_ticket from gui.v2.ui.pages.Page import Page from gui.v2.ui.styles.styles import TERMINAL_LIST_QSS from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread @@ -53,16 +53,7 @@ class PlanPickerPage(Page): def start_sync(self): # STATE - result = start_billing_state(category=PaymentCategory.TICKET) - if not result.valid: - error_msg = result.message - if error_msg: - self.show_a_popup(str(error_msg)) - else: - self.show_a_popup("Error with Starting Billing") - - # cache data to save the choice to it: - self.billing_data = result.data + result = start_ticket(ui_caller=self.recieve_initial_billing_data) # Begin Tickets: self.plan_list.clear() @@ -73,27 +64,18 @@ class PlanPickerPage(Page): self.worker.error.connect(self.on_error) self.worker.start() - def on_sync_done(self, pricing_data): - if isinstance(pricing_data, dict) and pricing_data.get('valid') is False: - err = pricing_data.get('error_code', 'unknown') - self.status_label.setText(f"Sync failed: {err}") - self.update_status.update_status(f"Sync failed: {err}") + def on_sync_done(self, pricing_data: Result): + if not pricing_data.valid: + self.status_label.setText("Pricing data fetch failed.") + self.show_a_popup(pricing_data.message) return - if isinstance(pricing_data, dict) and isinstance(pricing_data.get('data'), dict): - plans = pricing_data['data'] - elif isinstance(pricing_data, dict): - plans = pricing_data - else: - self.status_label.setText("Unexpected pricing data.") - return - - self.pricing_data = plans - self.populate_plans(plans) + self.pricing_data = pricing_data.data + self.populate_plans(self.pricing_data) self.status_label.setText("Pick a plan above.") self.update_status.update_status("Plans loaded.") - def populate_plans(self, pricing_data): + def populate_plans(self, pricing_data: dict): self.plan_list.clear() if not isinstance(pricing_data, dict): self.status_label.setText("Unexpected pricing data.") @@ -110,6 +92,20 @@ class PlanPickerPage(Page): item.setData(QtCore.Qt.ItemDataRole.UserRole, plan_key) self.plan_list.addItem(item) + def recieve_initial_billing_data(self, result: Result): + """Callback to recieve initial billing data instance""" + if not result.valid: + error_msg = result.message + if error_msg: + self.show_a_popup(str(error_msg)) + else: + self.show_a_popup("Error with Starting Billing") + + # cache data to save the choice to it: + self.billing_data = result.data + if self.billing_data: + print("got data") + def on_plan_clicked(self, item): plan_key = item.data(QtCore.Qt.ItemDataRole.UserRole) if not plan_key: @@ -119,12 +115,12 @@ class PlanPickerPage(Page): # in temp cache save choice: self.billing_data.which_key = plan_key + print("saved the which_key. navigating to currency selection..") # go to 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") - + # 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("ticket_crypto_picker") @@ -138,3 +134,17 @@ class PlanPickerPage(Page): def reverse(self): self.custom_window.navigator.navigate("id") + + +# LEGACY: + +# if isinstance(pricing_data, dict) and pricing_data.get('valid') is False: +# err = pricing_data.get('error_code', 'unknown') +# self.status_label.setText(f"Sync failed: {err}") +# self.update_status.update_status(f"Sync failed: {err}") +# return +# if isinstance(pricing_data, dict) and isinstance(pricing_data.get('data'), dict): +# plans = pricing_data['data'] +# elif isinstance(pricing_data, dict): +# plans = pricing_data +# else: