Ticket Inovices Stable with Big Bang Refactor. Still need to resolve issues with ticket billing invoice config already existing

This commit is contained in:
SimplifiedPrivacy 2026-10-10 18:27:02 -04:00
parent f9698ced68
commit 6a445eba41
9 changed files with 192 additions and 96 deletions

View file

@ -18,7 +18,7 @@ class Constants:
# ticketing group:
TICKET_API_BASE_URL: Final[str] = os.environ.get(
"TICKET_API_BASE_URL", "https://ticket.hydraveil.net"
"TICKET_API_BASE_URL", "https://api.hydraveil.net"
)
SP_API_BASE_URL: Final[str] = os.environ.get('SP_API_BASE_URL', 'https://api.hydraveil.net/api/v1')

View file

@ -177,7 +177,7 @@ def check_if_paid(
def check_if_ticket_paid(
temp_billing_code: str,
connection_observer: ConnectionObserver,
) -> dict:
) -> Result:
"""Coordinate a request with the ticket billing code, to check if an invoice is paid"""
billing_folder = Constants.HV_TICKETING_CONFIG_HOME
@ -189,46 +189,44 @@ def check_if_ticket_paid(
logger.error(f"{error_msg} inside the check_if_paid function")
return Result(valid=False, error_type=ResultError.SUBSCRIPTION, message=error_msg)
# prep the JSON payload:
payload = {"temp_billing_code": temp_billing_code}
# prep endpoint:
which_endpoint = "check_paid"
url = make_url(which_endpoint)
# literally send:
api_reply_object = connect.single_endpoint(
method="post",
url=url,
observer=connection_observer,
payload=payload
)
if not api_reply_object.valid:
error_msg = f"Connection/API Error: {api_reply_object.message}"
logger.error(f"[TICKET PayController] 2nd Post Request inside ticketpay controller had a {error_msg}")
return Result(valid=False, error_type=ResultError.CONNECTION, message=error_msg)
# return the payload with GUI/CLI to interpret results:
reply_dict = api_reply_object.data
logger.debug(f"[TICKET PayController] We have a valid reply from the API inside ticketpay controller of {reply_dict}")
print(f"reply_dict is {reply_dict}")
if isinstance(reply_dict, dict):
payment_status = reply_dict.get('payment_status', None)
message = reply_dict.get('message', None)
else:
payment_status = str(reply_dict)
# prep the JSON payload:
payload = {"temp_billing_code": temp_billing_code}
# prep endpoint:
which_endpoint = "check_paid"
url = make_url(which_endpoint)
# literally send:
api_reply_object = connect.single_endpoint(
method="post",
url=url,
observer=connection_observer,
payload=payload
)
if not api_reply_object.valid:
error_msg = f"Connection/API Error: {api_reply_object.message}"
logger.error(f"[TICKET PayController] 2nd Post Request inside ticketpay controller had a {error_msg}")
return Result(valid=False, error_type=ResultError.CONNECTION, message=error_msg)
# return the payload with GUI/CLI to interpret results:
reply_dict = api_reply_object.data
logger.debug(f"[TICKET PayController] We have a valid reply from the API inside ticketpay controller of {reply_dict}")
print(f"reply_dict is {reply_dict}")
if isinstance(reply_dict, dict):
payment_status = reply_dict.get('payment_status', None)
message = reply_dict.get('message', None)
if payment_status == "paid":
saved = update_json(filepath=filepath, key_to_add="paid", value_to_update="paid")
if saved:
return Result(valid=True, data=payment_status)
else:
payment_status = str(reply_dict)
return Result(valid=False, error_type=ResultError.FILE_SYSTEM, message="You paid, but it was unable to save that to the config")
if payment_status == "paid":
saved = update_json(filepath=filepath, key_to_add="paid", value_to_update="paid")
if saved:
return Result(valid=True, data=payment_status)
else:
return Result(valid=False, error_type=ResultError.FILE_SYSTEM, message="You paid, but it was unable to save that to the config")
elif message == "bad_code":
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="This is not a valid subscription code to check.")
elif message == "bad_code":
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="This is not a valid subscription code to check.")
return Result(valid=False, error_type=ResultError.UNPAID, message="Not yet paid")
return Result(valid=False, error_type=ResultError.UNPAID, message="Not yet paid")

View file

@ -6,12 +6,14 @@ if TYPE_CHECKING:
from core.observers.TicketObserver import TicketObserver
from core.Constants import Constants
from core.observers.BaseObserver import BaseObserver
# from core.services.networking.api_requests.step1_get_or_post import get_data_from_api
# from core.observers.BaseObserver import BaseObserver
from core.models.manage.server_reply_tools import translate_server_failure, invalid_server_format
from core.models.pydantic_models.type_checker import validate_model
from core.models.pydantic_models.ServerReply import ServerReply
from core.services.networking.httpx import connect
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.services.networking.api_requests.step5_solve_api_problems import solve_api_problems
# from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.models.Result import Result, ResultError
from core.errors.logger import logger
@ -19,8 +21,9 @@ from core.errors.logger import logger
def sync_ticket_prices(
ticket_observer: TicketObserver,
connection_observer: ConnectionObserver
) -> dict:
notification = f"Connecting to get Ticket Pricing..."
) -> Result:
notification = "Connecting to get Ticket Pricing..."
ticket_observer.notify("connecting", subject=notification)
rejected_list = [None, False, ""]
@ -33,36 +36,50 @@ def sync_ticket_prices(
return {"valid": False, "error_code": "invalid_url"}
url = f"{base_url}/sync"
try:
# api_result = get_data_from_api(url, None, connection_observer)
api_result = connect.single_endpoint(
method="get",
url=url,
observer=connection_observer,
payload=None
)
api_response = connect.single_endpoint(
method="get",
url=url,
observer=connection_observer,
payload=None
)
# 2nd try:
if not api_result.valid:
logger.error(f"Invalid API call result: {api_result.error_type}")
return {"valid": False, "error_code": "sync_failed"}
#-----------------------------
# EVALUATE CONNECTION FAILURE
#-----------------------------
# Layer 1: API failure:
if not api_response.valid:
error_msg = f"Connection/500s Error prevented the server from replying: {api_response.error_type} {api_response.message}"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.CONNECTION, message=error_msg)
# if not api_result.valid:
# error_msg = api_result.message
# logger.error(f"[TICKET SYNC Controller] There's an issue with the sync of endpoint {url} the API Reply: {error_msg}")
# api_result = solve_api_problems(
# api_reply_object=api_result,
# get_or_post="get",
# url=url,
# payload=None,
# connection_observer=connection_observer,
# client_observer=None
# )
# # 2nd try:
# if not api_result.valid:
# return {"valid": False, "error_code": "sync_failed"}
#-----------------------------
# EVALUATE SERVER-SIDE ISSUES (assuming it replied)
#-----------------------------
# Layer 2: Validity of Server's Reply:
raw_reply = api_response.data
logger.info("Validating the Validity of the Server's Reply..")
server_reply = validate_model(ServerReply, raw_reply)
return {"valid": True, "data": api_result.data}
if not server_reply:
logger.error(f"Invalid Structure for ServerReply! The raw data is {raw_reply}")
return invalid_server_format()
if not server_reply.valid:
logger.error(f"Server reply was correctly structured, but the actual invoice failed to be created. The raw data is {raw_reply}")
# translate by enum error code:
return translate_server_failure(server_reply)
#-----------------------------
# EVALUATE SERVER'S ACTUAL DATA
#-----------------------------
# Layer 3: Payload Data
payload_data = server_reply.data
if not payload_data:
logger.error(f"Failed to create an invoice object with this data: {payload_data}")
return invalid_server_format()
return Result(valid=True, data=payload_data)
# return {"valid": True, "data": payload_data}
except:
return {"valid": False, "error_code": "sync_failed"}

View file

@ -128,6 +128,7 @@ class StatePersistenceMixin(ValidatedDataclass):
return os.path.exists(self._data_file)
HOW_MANY_PROFILES_DEFAULT = 6
@singleton
@dataclass
@ -146,7 +147,7 @@ class TicketPaymentData(StatePersistenceMixin):
# for only tickets:
which_key: str = None
how_many_profiles: int = None
how_many_profiles: int = HOW_MANY_PROFILES_DEFAULT
def __post_init__(self):
# Global mode, assuming tickets here:

View file

@ -8,18 +8,23 @@ if TYPE_CHECKING:
# from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
# from core.services.networking.api_requests.step5_solve_api_problems import solve_api_problems
from core.models.Result import Result, ResultError
from core.models.invoice.TicketInvoice import TicketInvoice
# from core.models.invoice.TicketInvoice import TicketInvoice
from core.models.manage.server_reply_tools import translate_server_failure, invalid_server_format
from core.models.pydantic_models.type_checker import validate_model
from core.models.pydantic_models.ServerReply import ServerReply, SubscriptionInvoice
from core.utils.basic_operations.write_or_read_from_json import update_json
from core.Constants import Constants
from core.services.networking.httpx import connect
from core.utils.basic_operations.enum_tools import serialize_enums
from core.services.networking.make_url import make_url
# from core.services.payment_phase.extract_payment_details import extract_payment_details
from core.services.payment_phase.save_billing_choices import save_billing_choices
from core.errors.logger import logger
# generic:
from pydantic import ValidationError
# from pydantic import ValidationError
filepath = f"{Constants.HV_TICKETING_CONFIG_HOME}/billing_choices.json"
@ -39,6 +44,9 @@ def save_and_send_intitial_billing(
Errors:
No. Does NOT raise errors.
"""
# convert any enums:
payload = serialize_enums(payload)
# SAVE CHOICES:
saved_choices = save_billing_choices(payload)
if not saved_choices:
@ -50,41 +58,82 @@ def save_and_send_intitial_billing(
which_endpoint = "start_payment"
url = make_url(which_endpoint)
print(f"saved, now sending choices to {url}")
api_reply_object = connect.single_endpoint(
api_response = connect.single_endpoint(
method="post",
url=url,
observer=connection_observer,
payload=payload
)
if not api_reply_object.valid:
error_msg = f"API Error! {api_reply_object.message}"
logger.error(f"{api_reply_object.error_type} {error_msg}")
raise ConnectionError(error_msg)
# Extract from API Response into Invoice Object
server_reply = api_reply_object.data
# Validate and create the Pydantic model
try:
ticket_invoice = TicketInvoice.model_validate(server_reply)
except ValidationError as e:
error_msg = f"Server's Reply is Missing {str(e)}"
# if not api_reply_object.valid:
# error_msg = f"API Error! {api_reply_object.message}"
# logger.error(f"{api_reply_object.error_type} {error_msg}")
# raise ConnectionError(error_msg)
# # Extract from API Response into Invoice Object
# server_reply = api_reply_object.data
# # Validate and create the Pydantic model
# try:
# ticket_invoice = SubscriptionInvoice.model_validate(server_reply)
# except ValidationError as e:
# error_msg = f"Server's Reply is Missing {str(e)}"
# logger.error(error_msg)
# raise ValueError(error_msg)
#-----------------------------
# EVALUATE CONNECTION FAILURE
#-----------------------------
# Layer 1: API failure:
if not api_response.valid:
error_msg = f"Connection/500s Error prevented the server from replying: {api_response.error_type} {api_response.message}"
logger.error(error_msg)
raise ValueError(error_msg)
return Result(valid=False, error_type=ResultError.CONNECTION, message=error_msg)
#-----------------------------
# EVALUATE SERVER-SIDE ISSUES (assuming it replied)
#-----------------------------
# Layer 2: Validity of Server's Reply:
raw_reply = api_response.data
logger.info("Validating the Validity of the Server's Reply..")
server_reply = validate_model(ServerReply, raw_reply)
if not server_reply:
logger.error(f"Invalid Structure for ServerReply! The raw data is {raw_reply}")
return invalid_server_format()
if not server_reply.valid:
logger.error(f"Server reply was correctly structured, but the actual invoice failed to be created. The raw data is {raw_reply}")
# translate by enum error code:
return translate_server_failure(server_reply)
#-----------------------------
# EVALUATE SERVER'S ACTUAL DATA
#-----------------------------
# Layer 3: Payload Data
payload_data = server_reply.data
logger.info("Validating the final invoice")
final_invoice = validate_model(SubscriptionInvoice, payload_data)
if not final_invoice:
logger.error(f"Failed to create an invoice object with this data: {payload_data}")
return invalid_server_format()
# save the temp_billing_code:
print("saving to ticket tracker json..")
did_it_save = update_json(
filepath=filepath,
key_to_add="temp_billing_code",
value_to_update=ticket_invoice.temp_billing_code
value_to_update=final_invoice.billing_code
)
print(f"did_it_save is {did_it_save}")
if did_it_save == True:
return Result(valid=True, data=ticket_invoice)
return Result(valid=True, data=final_invoice)
else:
error_msg = f"Error: could NOT save the temp_billing_code {ticket_invoice.temp_billing_code}. Check your local file permissions and storage."
error_msg = f"Error: could NOT save the billing_code {final_invoice.billing_code}. Check your local file permissions and storage."
logger.error(error_msg, exc_info=True)
return Result(valid=False, error_type=ResultError.FILE_SYSTEM, message=error_msg)

View file

@ -46,6 +46,8 @@ def get_from_server_and_save(
# extract:
public_key = public_key_results.data
print(f"public_key is {public_key}")
# save it:
did_it_save = write_string_to_text_file(public_key, file_path)

View file

@ -66,7 +66,7 @@ def wipe_billing_cycle():
payment_cycle.wipe()
def start_billing_state(category: PaymentCategory, profile_id: Optional[int], wipe_existing: Optional[bool] = False) -> Result:
def start_billing_state(category: PaymentCategory, profile_id: Optional[int] = None, wipe_existing: Optional[bool] = False) -> Result:
"""Start a new billing process. Wipe the existing one. And never save these 'overall' states to disk."""
if not category:

View file

@ -0,0 +1,19 @@
from enum import Enum
from typing import Any
def serialize_enums(data: Any) -> Any:
"""
Recursively traverses a data structure and replaces Enum members with their values.
"""
if isinstance(data, Enum):
return data.value
if isinstance(data, dict):
return {k: serialize_enums(v) for k, v in data.items()}
if isinstance(data, list):
return [serialize_enums(item) for item in data]
return data

View file

@ -5,6 +5,8 @@ import os
from typing import Any
import tempfile
import shutil
from enum import Enum
def write_json_to_file(data: dict, filepath: str) -> bool:
try:
@ -24,6 +26,14 @@ def write_json_to_file(data: dict, filepath: str) -> bool:
except TypeError as e:
logger.error(f"Data is not JSON serializable: {e}")
# Attempt to sanitize Enums by extracting their .value
sanitized_data = {k: (v.value if isinstance(v, Enum) else v) for k, v in data.items()}
# Only retry if we actually modified the data to prevent infinite recursion
if sanitized_data != data:
return write_json_to_file(sanitized_data, filepath)
return False
except FileNotFoundError:
logger.error(f"File not found: {filepath}")