Stable Ticket Invoices for the initial invoice

This commit is contained in:
SimplifiedPrivacy 2026-10-10 18:28:13 -04:00
parent e071b0f502
commit 307e6800f1
6 changed files with 178 additions and 87 deletions

View file

@ -7,6 +7,7 @@ from core.models.Result import Result, ResultError
from core.errors.logger import logger from core.errors.logger import logger
# gui # gui
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
from gui.v2.infrastructure.setup_observers import connection_observer 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 return result
billing_data = result.data 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 # INVOICE DATA
#---------------------------- #----------------------------
if category == PaymentCategory.INDIVIDUAL: if category == PaymentCategory.INDIVIDUAL:
if profile_id: # already conflicting one:
return fetch_individual_invoice(invoice_ui_caller=invoice_ui_caller, timer_resume_caller=timer_resume_caller, profile_id=profile_id) if not profile_id:
else:
return "Need a profile id for an individual profile." return "Need a profile id for an individual profile."
elif category == PaymentCategory.TICKET: if profile_id != payment_cycle.profile_id:
print("ready to start for tickets") 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: 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: if ticketing:
payment_type = PaymentType.TICKETS payment_type = PaymentType.TICKET
logger.error("Not yet setup for ticketing") logger.error("Ticketing already saves the billing code on it's own when it recieves it.")
return False
# NON-ticketing: # NON-ticketing:
elif profile_id and expires_at: elif profile_id and expires_at:

View file

@ -1,15 +1,69 @@
# core # core
from core.services.payment_phase.payment_state import OverallPaymentCycle, PaymentData, TicketPaymentData from core.controllers.tickets.TicketPayController import initiate_payment
from core.models.enum_models import PaymentType 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.controllers.tickets.UseTicketController import get_unused_tickets
from core.models.Result import Result, ResultError from core.models.Result import Result, ResultError
# from core.controllers.ProfileController import ProfileController # from core.controllers.ProfileController import ProfileController
from core.errors.logger import logger
# gui # gui
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
from gui.v2.infrastructure.setup_observers import connection_observer, ticket_observer 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: def get_ticket_data() -> Result:

View file

@ -182,17 +182,18 @@ class Navigator(QObject):
def _on_preload_data_ready(self, name, payload): def _on_preload_data_ready(self, name, payload):
self._preload_inflight -= 1 self._preload_inflight -= 1
if name in self._instances: 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: else:
try: try:
page = self._instantiate(name, prepared=payload) page = self._instantiate(name, prepared=payload)
self.register_instance(name, page) self.register_instance(name, page)
self._preload_done += 1 self._preload_done += 1
logger.debug(f"[preload] {name:30s} OK (async, cached={len(self._instances)}, " # logger.debug(f"[preload] {name:30s} OK (async, cached={len(self._instances)}, "
f"stack_size={self.page_stack.count()})") # f"stack_size={self.page_stack.count()})")
except Exception as e: except Exception as e:
self._preload_failed += 1 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() self._maybe_emit_preload_summary()
def _on_preload_failed(self, name, error): def _on_preload_failed(self, name, error):
@ -204,7 +205,7 @@ class Navigator(QObject):
self._preload_done += 1 self._preload_done += 1
else: else:
self._preload_failed += 1 self._preload_failed += 1
self._maybe_emit_preload_summary() # self._maybe_emit_preload_summary()
def _cleanup_preload_worker(self, name): def _cleanup_preload_worker(self, name):
worker = self._preload_workers.pop(name, None) worker = self._preload_workers.pop(name, None)

View file

@ -5,7 +5,7 @@ from core.models.enum_models import Currency
from core.services.payment_phase.payment_state import PaymentData from core.services.payment_phase.payment_state import PaymentData
# gui # gui
from gui.v2.ui.pages.Page import Page from gui.v2.ui.pages.Page import Page, DataError
import os import os
from PyQt6 import QtCore from PyQt6 import QtCore

View file

@ -1,7 +1,8 @@
# core # 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.services.payment_phase.payment_state import PaymentData
from core.models.pydantic_models.ServerReply import SubscriptionInvoice 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.enum_models import Currency
from core.models.Result import Result from core.models.Result import Result
@ -40,6 +41,12 @@ from PyQt6.QtWidgets import (
) )
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
from PyQt6.QtGui import QIcon from PyQt6.QtGui import QIcon
from enum import Enum
# For formatting errors graphically:
class DataError(Enum):
FORMAT: "format"
BLANK: "blank"
class PaymentDetailsPage(Page): class PaymentDetailsPage(Page):
@ -73,9 +80,12 @@ class PaymentDetailsPage(Page):
def showEvent(self, event): def showEvent(self, event):
"""Refresh data. This runs every time the widget is displayed"""
# Call the parent class showEvent first # Call the parent class showEvent first
super().showEvent(event) super().showEvent(event)
# runs every time the widget is displayed
self.ticketing = paying_for_tickets()
self.begin_invoice() self.begin_invoice()
@ -172,7 +182,7 @@ class PaymentDetailsPage(Page):
print(f"profile_id is {self.profile_id}") 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( self.display_error(
whats_wrong=DataError.BLANK, whats_wrong=DataError.BLANK,
whats_it_called="Profile ID", whats_it_called="Profile ID",
@ -281,13 +291,13 @@ class PaymentDetailsPage(Page):
) )
return return
if not isinstance(invoice, SubscriptionInvoice): # if not isinstance(invoice, SubscriptionInvoice):
self.display_error( # self.display_error(
whats_wrong=DataError.FORMAT, # whats_wrong=DataError.FORMAT,
whats_it_called="invoice", # whats_it_called="invoice",
the_thing=invoice, # the_thing=invoice,
) # )
return # return
self.fill_in_invoice_data(invoice) self.fill_in_invoice_data(invoice)
self.update_status.update_status("Invoice generated. Awaiting payment...") self.update_status.update_status("Invoice generated. Awaiting payment...")
@ -309,6 +319,8 @@ class PaymentDetailsPage(Page):
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"""
print(f"filling in {invoice}")
# Extract from the already validated object: # Extract from the already validated object:
billing_code = str(invoice.billing_code) billing_code = str(invoice.billing_code)
duration = str(invoice.expires_at) duration = str(invoice.expires_at)
@ -409,7 +421,7 @@ class PaymentDetailsPage(Page):
self.custom_window.navigator.navigate("menu") self.custom_window.navigator.navigate("menu")
def _get_currency(self): 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): if not self.profile_id or not isinstance(self.profile_id, int):
self.display_error( self.display_error(
@ -418,23 +430,36 @@ class PaymentDetailsPage(Page):
the_thing=self.profile_id, 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: if not data_result.valid:
self.display_error( message = data_result.message
whats_wrong=DataError.BLANK, if not message:
whats_it_called="Profile's Billing Data", message = "Error fetching data"
the_thing=billing_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: if not currency:
self.display_error( self.show_a_popup('Missing Currency')
whats_wrong=DataError.BLANK,
whats_it_called="currency",
the_thing=currency,
)
if not isinstance(currency, Currency): if not isinstance(currency, Currency):
self.display_error( self.display_error(

View file

@ -1,9 +1,9 @@
# core # core
from core.services.subscriptions.subscriptions_by_protocol import start_billing_state
from core.models.enum_models import PaymentCategory from core.models.enum_models import PaymentCategory
from core.models.Result import Result
# gui # 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.pages.Page import Page
from gui.v2.ui.styles.styles import TERMINAL_LIST_QSS from gui.v2.ui.styles.styles import TERMINAL_LIST_QSS
from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread
@ -53,16 +53,7 @@ class PlanPickerPage(Page):
def start_sync(self): def start_sync(self):
# STATE # STATE
result = start_billing_state(category=PaymentCategory.TICKET) result = start_ticket(ui_caller=self.recieve_initial_billing_data)
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
# Begin Tickets: # Begin Tickets:
self.plan_list.clear() self.plan_list.clear()
@ -73,27 +64,18 @@ class PlanPickerPage(Page):
self.worker.error.connect(self.on_error) self.worker.error.connect(self.on_error)
self.worker.start() self.worker.start()
def on_sync_done(self, pricing_data): def on_sync_done(self, pricing_data: Result):
if isinstance(pricing_data, dict) and pricing_data.get('valid') is False: if not pricing_data.valid:
err = pricing_data.get('error_code', 'unknown') self.status_label.setText("Pricing data fetch failed.")
self.status_label.setText(f"Sync failed: {err}") self.show_a_popup(pricing_data.message)
self.update_status.update_status(f"Sync failed: {err}")
return return
if isinstance(pricing_data, dict) and isinstance(pricing_data.get('data'), dict): self.pricing_data = pricing_data.data
plans = pricing_data['data'] self.populate_plans(self.pricing_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.status_label.setText("Pick a plan above.") self.status_label.setText("Pick a plan above.")
self.update_status.update_status("Plans loaded.") self.update_status.update_status("Plans loaded.")
def populate_plans(self, pricing_data): def populate_plans(self, pricing_data: dict):
self.plan_list.clear() self.plan_list.clear()
if not isinstance(pricing_data, dict): if not isinstance(pricing_data, dict):
self.status_label.setText("Unexpected pricing data.") self.status_label.setText("Unexpected pricing data.")
@ -110,6 +92,20 @@ class PlanPickerPage(Page):
item.setData(QtCore.Qt.ItemDataRole.UserRole, plan_key) item.setData(QtCore.Qt.ItemDataRole.UserRole, plan_key)
self.plan_list.addItem(item) 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): def on_plan_clicked(self, item):
plan_key = item.data(QtCore.Qt.ItemDataRole.UserRole) plan_key = item.data(QtCore.Qt.ItemDataRole.UserRole)
if not plan_key: if not plan_key:
@ -119,12 +115,12 @@ class PlanPickerPage(Page):
# in temp cache save choice: # in temp cache save choice:
self.billing_data.which_key = plan_key self.billing_data.which_key = plan_key
print("saved the which_key. navigating to currency selection..")
# go to next page: # go to next page:
currency_page = self.custom_window.navigator.get_cached("currency_selection") # currency_page = self.custom_window.navigator.get_cached("currency_selection")
if currency_page is not None: # if currency_page is not None:
self.custom_window.navigator.navigate("currency_selection") self.custom_window.navigator.navigate("currency_selection")
# self.custom_window.navigator.navigate("ticket_crypto_picker") # self.custom_window.navigator.navigate("ticket_crypto_picker")
@ -138,3 +134,17 @@ class PlanPickerPage(Page):
def reverse(self): def reverse(self):
self.custom_window.navigator.navigate("id") 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: