Ticket Refactor to accept ServerReply objects, and keep the internal flow returning Result objects

This commit is contained in:
SimplifiedPrivacy 2026-10-22 03:31:33 -04:00
parent 2208cedb7e
commit af368db8b7
8 changed files with 642 additions and 342 deletions

View file

@ -5,13 +5,14 @@ if TYPE_CHECKING:
from core.essentials.observers.ConnectionObserver import ConnectionObserver
from core.observers.TicketObserver import TicketObserver
from core.utils.type_checker import type_checker
from core.services.prepare_tickets.ticket_prep_orchestrator import ticket_prep_orchestrator
from core.services.prepare_tickets.make_sure_pub_key_exists import make_sure_pub_key_exists
from core.services.prepare_tickets import ticket_tracker
from core.services.helpers.get_how_many_profiles_were_ordered import (
get_how_many_profiles_were_ordered,
)
from core.observers.BaseObserver import BaseObserver
# from core.observers.BaseObserver import BaseObserver
from core.models.Result import Result, ResultError
from core.errors.logger import logger
@ -33,13 +34,15 @@ If it doesn't have a config file, then it uses the temp billing id to get the pu
"""
# Bulk Prep:
@type_checker
def prepare_tickets(
how_many_profiles: int,
ticket_observer: TicketObserver,
connection_observer: ConnectionObserver,
which_ticket: Optional[int] = None,
billing_code: Optional[int] = None
) -> dict:
) -> Result:
# these two are for preparation of a single ticket to a profile.
# can't have one blank without the other. (profile or ticket)
@ -82,35 +85,48 @@ def prepare_tickets(
billing_code=billing_code # only relevant for single ticket prep
)
# rest of this function is evaluating the results:
if prep_results == False or prep_results == None:
return {"valid": False, "message": "error"}
if "valid" not in prep_results:
return {"valid": False, "message": "error"}
if prep_results["valid"] == True:
notification = f"Done! All Tickets Ready!"
if prep_results.valid:
notification = "Done! All Tickets Ready!"
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
else:
notification = "Ticket Preparation Failed"
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
return prep_results
if "how_many_failed" in prep_results:
how_many_failed = prep_results.get("how_many_failed", 0)
failed_validations = prep_results.get("failed_validations", None)
if failed_validations:
notification = f"Error with Ticket Preparation or Verification!"
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
return prep_results
notification = f"Error with Ticket Preparation or Verification!"
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
return prep_results
# if prep_results == False or prep_results == None:
# return {"valid": False, "message": "error"}
# if "valid" not in prep_results:
# return {"valid": False, "message": "error"}
# if prep_results["valid"] == True:
# notification = f"Done! All Tickets Ready!"
# if ticket_observer is not None:
# ticket_observer.notify("preparing", subject=notification)
# return prep_results
# if "how_many_failed" in prep_results:
# how_many_failed = prep_results.get("how_many_failed", 0)
# failed_validations = prep_results.get("failed_validations", None)
# if failed_validations:
# notification = f"Error with Ticket Preparation or Verification!"
# if ticket_observer is not None:
# ticket_observer.notify("preparing", subject=notification)
# return prep_results
# notification = f"Error with Ticket Preparation or Verification!"
# if ticket_observer is not None:
# ticket_observer.notify("preparing", subject=notification)
# return prep_results
# No profile required
def respawn_billing_code_into_ticket(
billing_code: str,
@ -167,7 +183,7 @@ def respawn_billing_code_into_ticket(
valid = ticket_result.get('valid', False)
if not valid:
notification = f"Error in Respawn Prep!"
notification = "Error in Respawn Prep!"
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
return prep_error_result(ticket_result)

View file

@ -18,7 +18,7 @@ class ApiCall(Enum):
TRANSLATION_DICT = {
ServerError.WRONG_LOCATION: ResultError.WRONG_LOCATION,
ServerError.NODE_ERROR: ResultError.WRONG_LOCATION,
ServerError.BTCPAY: ResultError.SERVER_SIDE,
ServerError.CRYPTO_NODE: ResultError.SERVER_SIDE,
ServerError.INVALID: ResultError.INVALID_INPUT,
ServerError.MISSING_DATA: ResultError.INVALID_INPUT,
ServerError.UNPAID: ResultError.SUBSCRIPTION,
@ -71,7 +71,6 @@ def format_server_response(api_response: ApiResponse|dict, desired_type: str) ->
return Result(valid=False, error_type=ResultError.MISSING_DATA, message="Server Replied it was valid, but failed to provide the data.")
# return Result(valid=False, error_type=error_type, message=server_reply.message)
def translate_server_failure(server_reply: ServerReply) -> Result:
@ -89,12 +88,13 @@ def invalid_server_format(error_msg: Optional[str] = None) -> Result:
def api_call_and_extraction(
desired_type: str,
desired_type: Optional[str],
method: ApiCall,
url: str,
connection_observer: ConnectionObserver,
payload: dict,
billing_code: Optional[str] = None
billing_code: Optional[str] = None,
return_server_reply: bool = False,
) -> Result:
"""Do an API call, extract the data, and if there's an error, then translate the server's reply to a Result Object"""
@ -152,6 +152,11 @@ def api_call_and_extraction(
logger.error(error_msg)
return invalid_server_format()
# Opt into getting the ServerReply object (instead of Result object):
if return_server_reply and desired_type is None:
logger.info("Validation successfull. Returning a raw ServerReply object")
return server_reply
logger.info("Validation successfull. Checking if the server's reply says the operation worked..")
if not server_reply.valid:
error_message = f"Server reply was correctly structured, but the actual invoice failed to be created. The raw data is {server_reply}"
@ -169,12 +174,3 @@ def api_call_and_extraction(
data = getattr(server_reply, desired_type)
logger.info(f"Successfully returning the data of {data}")
return Result(valid=True, data=data)
# else:
# error_msg = f"Server Replied it was valid, but failed to provide the data for the desired type of {desired_type}."
# logger.error(error_msg)
# return Result(valid=False, error_type=ResultError.MISSING_DATA, message=error_msg)

View file

@ -16,10 +16,11 @@ class ServerError(str, Enum):
QUANTITY = "quantity"
UNPAID = "unpaid"
INVALID = "invalid"
ALREADY_USED = "already_used"
MISSING_DATA = "missing_data"
WRONG_LOCATION = "wrong_location"
NODE_ERROR = "node_error"
BTCPAY = "btcpay"
CRYPTO_NODE = "crypto_node"
SERVER_SIDE = "server_side"
@ -37,6 +38,8 @@ class ServerReply():
boolean_result: Optional[bool] = None
conf: Optional[str] = None
payment_status: Optional[bool] = None
expires_at: Optional[datetime] = None
@dataclass_json

View file

@ -4,22 +4,25 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
from essentials.observers.ConnectionObserver import ConnectionObserver
from core.observers.TicketObserver import TicketObserver
# services
from core.services.networking.httpx import connect
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
# from core.services.networking.httpx import connect
# from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.services.networking.make_url import make_url
# from core.services.payment_phase.ticket_config_tools import get_which_billing_key
# models & their controllers
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
from core.models.Subscription import Subscription # not sure if needed
from core.models.manage.server_reply_tools import api_call_and_extraction, ApiCall, translate_server_failure
from core.models.pydantic_models.ServerReply import ServerReply, ServerError
# from core.models.session.SessionProfile import SessionProfile
# from core.models.system.SystemProfile import SystemProfile
# utils
from core.utils.basic_operations.write_or_read_from_json import get_value_from_json_file
# constants & errors
from core.Constants import Constants
from core.errors.exceptions import InvalidData, ServerSideError
# from core.errors.exceptions import InvalidData, ServerSideError
from core.models.Result import Result, ResultError
from core.errors.logger import logger
from typing import Optional
@ -76,12 +79,24 @@ def prep_single_ticket_payload(billing_code: str, list_of_all_blinded_data: list
return payload
def confirmed_data_result(blind_signatures: list) -> Result:
if (
blind_signatures == "error"
or blind_signatures == None
or blind_signatures == False
):
error_msg = f"The server's blind signature was blank or invalid. It said: {blind_signatures}"
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
return Result(valid=True, data=blind_signatures)
def send_blind_commitments(
list_of_all_blinded_data: list,
ticket_observer: TicketObserver,
connection_observer: ConnectionObserver,
billing_code: Optional[int] = None
) -> list | None:
) -> Result:
"""
Purpose:
1) Oversee payload prep
@ -105,61 +120,121 @@ def send_blind_commitments(
# send it:
url = make_url(which_endpoint)
try:
api_reply_object = connect.single_endpoint(
method="post",
url=url,
observer=connection_observer,
payload=payload
)
# # did API call work?
if not api_reply_object.valid:
logger.error(f"[SEND BLIND COMMITMENTS] Second Post request failed. {api_reply_object.error_type} {api_reply_object.message}")
ticket_observer.notify("error", subject=api_reply_object.message)
return None
# assuming it worked, extract the data:
reply = api_reply_object.data
server_reply = api_call_and_extraction(
desired_type=None, # get it all
method=ApiCall.POST,
url=url,
connection_observer=connection_observer,
payload=payload,
return_server_reply=True # get it all
)
# is it in valid format?
if not isinstance(reply, dict):
final_error_msg = f"API returned an invalid format {reply}"
logger.error(final_error_msg)
ticket_observer.notify("error", subject=final_error_msg)
return None
# this is a connection error or invalid format itself:
if isinstance(server_reply, Result) or not isinstance(server_reply, ServerReply):
ticket_observer.notify("error", subject=server_reply.message)
return server_reply
if reply.get("valid") == True:
# this 'signed_data' variable is a list,
signed_data = reply.get("signed_data")
# it's then sending it to the other functions,
# to iterate through, when they save and unblind,
return signed_data
# Worked:
if server_reply.valid:
return confirmed_data_result(blind_signatures=server_reply.signed_data)
# but what if it didn't work?
elif reply.get("valid") == False:
error_msg = reply.get("message")
error_msg_with_context = (
f"Issues with Signing. Server's error message said: {error_msg}"
error_type = server_reply.error_type
message = server_reply.message
ticket_observer.notify("error", subject=message)
logger.error(f"Send blind commitments failed. {str(error_type)} {message}")
# WRONG QUANTITY:
if error_type == ServerError.QUANTITY:
quantity_required = server_reply.quantity
if not quantity_required or quantity_required < 1:
error_msg = f"Server claims it got an invalid quantity, but yet it suggested to give it just {quantity_required} items."
ticket_observer.notify("error", subject=error_msg)
return Result(
valid=False,
error_type=ResultError.INVALID_INPUT,
message=error_msg
)
ticket_observer.notify("error", subject=error_msg_with_context)
return None
if reply == False:
error_msg = f"It did not send. There's an error with sending the data to server. The payload either was or was going to be: {payload}"
ticket_observer.notify("error", subject=error_msg)
return None
else:
error_msg = "The JSON returned from the server is not formatted correctly to even be able to figure why it did not work. This is a critical error. Contact customer support"
ticket_observer.notify("error", subject=error_msg)
return None
logger.info(f"Retrying with a different size of {quantity_required}.")
resized_list = list_of_all_blinded_data[:quantity_required]
resized_payload = prep_payload_for_bulk(resized_list)
second_server_reply = api_call_and_extraction(
desired_type="signed_data",
method=ApiCall.POST,
url=url,
connection_observer=connection_observer,
payload=resized_payload,
return_server_reply=False
)
if not second_server_reply.valid:
return second_server_reply
return confirmed_data_result(blind_signatures=server_reply.signed_data)
except InvalidData as e:
logger.error(f"InvalidData error: {e}", exc_info=True)
return None
except ServerSideError as e:
logger.error(f"ServerSideError: {e}", exc_info=True)
return None
except Exception as e:
logger.error(f"Generic Python Error {e}", exc_info=True)
return None
return translate_server_failure(server_reply=server_reply)
# # # did API call work?
# if not api_reply_object.valid:
# ticket_observer.notify("error", subject=api_reply_object.message)
# return None
# # assuming it worked, extract the data:
# reply = api_reply_object.data
# # is it in valid format?
# if not isinstance(reply, dict):
# final_error_msg = f"API returned an invalid format {reply}"
# logger.error(final_error_msg)
# ticket_observer.notify("error", subject=final_error_msg)
# return None
# if reply.get("valid") == True:
# # this 'signed_data' variable is a list,
# signed_data = reply.get("signed_data")
# # it's then sending it to the other functions,
# # to iterate through, when they save and unblind,
# return signed_data
# # but what if it didn't work?
# elif reply.get("valid") == False:
# error_msg = reply.get("message")
# error_msg_with_context = (
# f"Issues with Signing. Server's error message said: {error_msg}"
# )
# ticket_observer.notify("error", subject=error_msg_with_context)
# return None
# if reply == False:
# error_msg = f"It did not send. There's an error with sending the data to server. The payload either was or was going to be: {payload}"
# ticket_observer.notify("error", subject=error_msg)
# return None
# else:
# error_msg = "The JSON returned from the server is not formatted correctly to even be able to figure why it did not work. This is a critical error. Contact customer support"
# ticket_observer.notify("error", subject=error_msg)
# return None
# except InvalidData as e:
# logger.error(f"InvalidData error: {e}", exc_info=True)
# return None
# except ServerSideError as e:
# logger.error(f"ServerSideError: {e}", exc_info=True)
# return None
# except Exception as e:
# logger.error(f"Generic Python Error {e}", exc_info=True)
# return None
# api_reply_object = connect.single_endpoint(
# method="post",
# url=url,
# observer=connection_observer,
# payload=payload
# )

View file

@ -4,20 +4,20 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.observers.TicketObserver import TicketObserver
from essentials.observers.ConnectionObserver import ConnectionObserver
# services
from core.services.crypto.make_commitments import make_ALL_commitments
from core.services.prepare_tickets.unblind_all_tickets import unblind_ALL_tickets
from core.services.prepare_tickets.setup_ticket_tracker import setup_ticket_tracker
from core.services.prepare_tickets import ticket_tracker
# from core.services.prepare_tickets import ticket_tracker
from core.services.prepare_tickets.send_blind_commitments import send_blind_commitments
from core.services.prepare_tickets.validate_blind_signatures import validate_blind_signatures
from core.services.prepare_tickets.save_ALL_blind_sigs import save_ALL_blind_sigs
from core.utils.basic_operations.does_file_exist import does_file_exist
from core.controllers.ConfigurationController import ConfigurationController
# errors & constants
from core.utils.type_checker import type_checker
from core.models.Result import Result, ResultError
from core.Constants import Constants
from core.errors.exceptions import MissingData, CriticalFailure
from core.errors.logger import logger
from typing import Optional
@ -34,156 +34,159 @@ TicketPrepController's respawn_billing_code_into_ticket
with which_ticket and billing_code. (and how_many_profiles is one)
"""
REJECTED_INPUTS = [False, None, "error"]
"""
Called by:
TicketPrepController's prepare_tickets = For Bulk Tickets
TicketPrepController's respawn_billing_code_into_ticket = For a Single Ticket
"""
@type_checker
def ticket_prep_orchestrator(
how_many_profiles: int,
ticket_observer: TicketObserver,
connection_observer: ConnectionObserver,
which_ticket: Optional[int] = None,
billing_code: Optional[int] = None
) -> dict:
"""
Purpose:
Orchestrate the entire ticket preparation operation.
) -> Result:
"""Orchestrate the entire ticket preparation operation."""
Rank:
Feature's Primary Cross-Module Orchestrator
Called by:
TicketPrepController's prepare_tickets = For Bulk Tickets
TicketPrepController's respawn_billing_code_into_ticket = For a Single Ticket
Returns:
Dicts. Shouldn't raise errors on it's own.
"""
if not billing_code and which_ticket:
logger.error("Can't have no billing_code, if you have a ticket to replace")
return {
"valid": False,
"message": "missing_data",
}
error_msg = "Can't have no billing_code, if you have a ticket to replace"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
try:
#####################################################
# MAKE BLIND COMMITMENTS
#####################################################
# make all commitments, but get the blinded ones:
list_of_all_blinded_commitments = make_ALL_commitments(how_many_profiles, which_ticket)
#####################################################
# MAKE BLIND COMMITMENTS
#####################################################
# make all commitments, but get the blinded ones:
list_of_all_blinded_commitments = make_ALL_commitments(how_many_profiles, which_ticket)
# did we actually save them? if not, try again,
if list_of_all_blinded_commitments is None:
list_of_all_blinded_commitments = make_ALL_commitments(how_many_profiles, which_ticket)
# still failed to save?!
if list_of_all_blinded_commitments is None:
return {
"valid": False,
"message": "cant_save",
}
if not list_of_all_blinded_commitments:
error_msg = "We failed to make and save all the blinded commitments"
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
#####################################################
# SEND BLIND TO GET SIGNED
#####################################################
# assuming we actually saved the unblinding factors,
notification = "Sending Blinded Package to the Server.."
#####################################################
# SEND BLIND TO GET SIGNED
#####################################################
# assuming we actually saved the unblinding factors,
notification = "Sending Blinded Package to the Server.."
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
# then send the entire blinded list to the server to sign:
bling_sig_result = send_blind_commitments(
list_of_all_blinded_commitments, ticket_observer, connection_observer, billing_code
)
if not bling_sig_result.valid:
return bling_sig_result
# Recieve signatures:
blind_signatures = bling_sig_result.data
#####################################################
# EVALUATE AND SAVE THE SERVER'S REPLIES
#####################################################
# regardless of the outcome of the verification, save all blind sigs, just in case, because the user can't get them again,
notification = "Saving the Server's Blind Replies"
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
did_they_ALL_save = save_ALL_blind_sigs(blind_signatures, which_ticket)
if not did_they_ALL_save:
error_msg = "Failed to save the blind signatures locally! We continue with it in memory.."
logger.error(error_msg)
ticket_observer.notify("preparing", subject=error_msg)
#####################################################
# VERIFY BLIND SIGS
#####################################################
# verify the server's blind signatures against the public key,
notification = "Evaluating the Server's Blind Replies"
ticket_observer.notify("preparing", subject=notification)
failed_validations = validate_blind_signatures(
blind_signatures, ticket_observer, connection_observer, which_ticket
)
logger.debug(f"failed_validations is {failed_validations}")
#####################################################
# FAILED BLIND SIG VERIFICATION
#####################################################
# did verification of any of the blind sigs fail?
how_many_failed = len(failed_validations)
if how_many_failed >= 1:
notification = f"Verification failed for {how_many_failed} blind signatures. You still have to unblind them but its paused for the error."
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
# then send the entire blinded list to the server to sign:
blind_signatures = send_blind_commitments(
list_of_all_blinded_commitments, ticket_observer, connection_observer, billing_code
logger.error(notification)
return Result(
valid=False,
error_type=ResultError.MATH,
data=failed_validations,
message=notification
)
# Recieve signatures:
if (
blind_signatures == "error"
or blind_signatures == None
or blind_signatures == False
):
return {
"valid": False,
"message": f"The server's blind signature was blank or invalid. It said: {blind_signatures}",
"error_code": "invalid_reply"
}
#####################################################
# PAID
#####################################################
# saving on config that it's paid:
config_marked = ConfigurationController.mark_as_paid(True)
if not config_marked:
return Result(valid=False, error_type=ResultError.FILE_SYSTEM, message="Failed to mark as paid in the config. You still have the tickets locally, just can't use them till this file system error is resolved.")
else:
#####################################################
# EVALUATE AND SAVE THE SERVER'S REPLIES
#####################################################
# regardless of the outcome of the verification, save all blind sigs, just in case, because the user can't get them again,
notification = "Saving the Server's Blind Replies"
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
did_they_ALL_save = save_ALL_blind_sigs(blind_signatures, which_ticket)
#####################################################
# UNBLIND
#####################################################
# Unblind the signatures & combine with unblinded commitment:
notification = "Unblinding Signatures & Preparing Tickets..."
ticket_observer.notify("preparing", subject=notification)
did_prep_work = unblind_ALL_tickets(
ALL_signed_blind_signatures=blind_signatures,
ticket_observer=ticket_observer,
connection_observer=connection_observer,
which_ticket=which_ticket
)
# verify the server's blind signatures against the public key,
notification = "Evaluating the Server's Blind Replies"
ticket_observer.notify("preparing", subject=notification)
failed_validations = validate_blind_signatures(
blind_signatures, ticket_observer, connection_observer, which_ticket
)
#####################################################
# TICKET TRACKER & APP-WIDE CONFIG
#####################################################
if not does_file_exist(Constants.TICKET_TRACKER_PATH):
setup_tracker = setup_ticket_tracker(how_many_profiles)
if not setup_tracker:
return Result(valid=False, error_type=ResultError.FILE_SYSTEM, message="Failed to setup the ticket tracker. You still have the tickets locally, just can't use them till this file system error is resolved.")
logger.debug(f"failed_validations is {failed_validations}")
#####################################################
# FINAL EVALUATION
#####################################################
if did_prep_work:
# this means it unblinded, & setup the tracker without erroring out,
# but it does NOT mean that verification of the blind sigs worked.
ConfigurationController.tickets_setup(True)
return Result(valid=True, message="Setup worked.")
# did verification of any of the blind sigs fail?
how_many_failed = len(failed_validations)
if how_many_failed >= 1:
notification = f"Verification failed for {how_many_failed} blind signatures."
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
logger.debug(
f"Verification failed for {how_many_failed} blind signatures."
)
return {
"valid": False,
"message": "verification_failed",
"how_many_failed": how_many_failed,
"failed_validations": failed_validations,
}
#####################################################
# UNBLIND
#####################################################
# Unblind the signatures & combine with unblinded commitment:
notification = f"Unblinding Signatures & Preparing Tickets..."
ticket_observer.notify("preparing", subject=notification)
did_prep_work = unblind_ALL_tickets(
ALL_signed_blind_signatures=blind_signatures,
ticket_observer=ticket_observer,
connection_observer=connection_observer,
which_ticket=which_ticket
)
else:
error_msg = "You have the blinded data, but we were unable to unblind it."
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.MATH, message=error_msg)
#####################################################
# TICKET TRACKER & APP-WIDE CONFIG
#####################################################
if not does_file_exist(Constants.TICKET_TRACKER_PATH):
setup_tracker = setup_ticket_tracker(how_many_profiles)
if not setup_tracker:
return {"valid": False, "message": "failed"}
# saving on config that it's setup:
config_marked = ConfigurationController.mark_as_paid(True)
if not config_marked:
return {'valid': False, 'message': 'file_system'}
#####################################################
# FINAL EVALUATION
#####################################################
if did_prep_work:
# this means it unblinded, & setup the tracker without erroring out,
# but it does NOT mean that verification of the blind sigs worked.
return {"valid": True, "message": "worked"}
else:
return {"valid": False, "message": "failed"}
except CriticalFailure as e:
error_msg = f"CriticalFailure error: {e}"
logger.error(error_msg, exc_info=True)
return {"valid": False, "message": error_msg}
except MissingData as e:
error_msg = f"MissingData: {e}"
logger.error(error_msg, exc_info=True)
return {"valid": False, "message": error_msg}
except:
error_msg = f"Generic Python Error"
logger.error(error_msg, exc_info=True)
return {"valid": False, "message": error_msg}
# except CriticalFailure as e:
# error_msg = f"CriticalFailure error: {e}"
# logger.error(error_msg, exc_info=True)
# return {"valid": False, "message": error_msg}
# except MissingData as e:
# error_msg = f"MissingData: {e}"
# logger.error(error_msg, exc_info=True)
# return {"valid": False, "message": error_msg}
# except:
# error_msg = f"Generic Python Error"
# logger.error(error_msg, exc_info=True)
# return {"valid": False, "message": error_msg}

View file

@ -314,13 +314,14 @@ def destroy_profile(
# RESPAWN TICKET
####################################
which_ticket = profile.ticket
respawn_result = respawn_profile(
profile=profile,
ticket_observer=ticket_observer,
connection_observer=connection_observer
)
if which_ticket and not respawn_result.valid:
logger.error(f"Unable to respawn the ticket. {respawn_result.message} {respawn_result.error_type}")
if which_ticket:
respawn_result = respawn_profile(
profile=profile,
ticket_observer=ticket_observer,
connection_observer=connection_observer
)
if which_ticket and not respawn_result.valid:
logger.error(f"Unable to respawn the ticket. {respawn_result.message} {respawn_result.error_type}")
####################################
# DESTROY PROFILE

View file

@ -573,7 +573,7 @@ def wipe_subscription(profile_id: int) -> Result:
@type_checker
def save_billing_code(profile_id: int, billing_code: str, expires_at: datetime) -> Result:
def save_billing_code(profile_id: int, billing_code: str, expires_at: datetime, which_ticket: Optional[int] = None) -> Result:
"""Checks if the billing code is already saved, and if not, saves it."""
logger.info("Saving billing code..")
@ -597,6 +597,8 @@ def save_billing_code(profile_id: int, billing_code: str, expires_at: datetime)
expires_at=expires_at,
billing_pending=True
)
if which_ticket:
profile.ticket = which_ticket
ProfileController.update(profile)
return Result(valid=True)
except Exception as e:

View file

@ -3,135 +3,339 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
from essentials.observers.ConnectionObserver import ConnectionObserver
##############
from core.services.using_tickets.send_unblinded import send_unblinded_ticket_to_server
from core.utils.basic_operations.write_string_to_text_file import write_string_to_text_file
from core.services.prepare_tickets.ticket_tracker import update_ticket_tracker
from core.models.Subscription import Subscription
from core.services.subscriptions.subscriptions_by_protocol import save_billing_code
from core.utils.get_raw_string import get_raw_string
from core.observers.TicketObserver import TicketObserver
from core.services.helpers.does_ticket_file_exist import does_ticket_file_exist
from core.services.prepare_tickets.ticket_tracker import does_ticket_tracker_exist
from core.models.pydantic_models.ServerReply import ServerReply, ServerError
from core.services.payment_phase.payment_state import TicketPaymentData
from core.services.networking.make_url import make_url
from core.models.manage.server_reply_tools import api_call_and_extraction, ApiCall, translate_server_failure
from core.models.Result import Result, ResultError
from core.errors.logger import logger
from core.Constants import Constants
from core.models.enum_models import ProtocolChoice
from core.utils.type_checker import type_checker
from core.services.prepare_tickets.ticket_tracker import update_ticket_tracker
from core.controllers.ProfileController import ProfileController
# from core.models.BaseProfile import BaseProfile as Profile
# from core.services.using_tickets.send_unblinded import send_unblinded_ticket_to_server
# from core.models.Subscription import Subscription
# from core.utils.basic_operations.write_string_to_text_file import write_string_to_text_file
from datetime import datetime
CUSTOMER_SERVICE_MSG = "Please speak with customer service."
@type_checker
def save_ticket_use(
profile_id: int,
billing_code: str,
expires_at: datetime,
location_id: int,
which_ticket: int
) -> Result:
saved_for_profile = save_billing_code(
profile_id=profile_id,
billing_code=billing_code,
expires_at=expires_at,
which_ticket=which_ticket
)
# update the ticket tracker:
saved_ticket_tracker = update_ticket_tracker(
which_ticket=which_ticket,
status="used",
location=location_id,
subscription=billing_code
)
if not saved_for_profile.valid:
return saved_for_profile # result object
if saved_ticket_tracker:
return Result(valid=True)
else:
return Result(valid=False, error_type=ResultError.FILE_SYSTEM, message="Unable to update the ticket tracker that the ticket is used.")
def make_payload(which_ticket: int, which_location: int, protocol_choice: ProtocolChoice) -> Result:
"""Prepare the JSON to send to the billing server"""
# ticket
ticket_data = get_raw_string(str(which_ticket), "unblinded_final_ticket")
# key
which_key = TicketPaymentData().which_key()
if not which_key:
return Result(valid=False, error_type=ResultError.MISSING_DATA, message="Which ticket billing key is missing from your config file. Did you wipe it?")
# prep the JSON payload:
payload = {
"which_key": which_key,
"which_location": which_location,
"protocol_choice": protocol_choice.value,
"ticket_data": ticket_data,
}
return Result(valid=True, data=payload)
@type_checker
def use_ticket_orchestrator(
profile_id: int,
which_ticket: int,
which_location: int, # Location.id
protocol_choice: ProtocolChoice,
ticket_observer: TicketObserver,
connection_observer: ConnectionObserver,
) -> Result:
"""
Purpose:
Coordinates using a ticket.
Rank:
Orchestrator
Method:
1) Send to the server
2) Evaluate the response
3) updates the 'ticker_tracker' JSON with new data
4) Formats error messages for UI
Called by:
UseTicketController
Returns:
Return Objects
do NOT raise errors
"""
###################################
# SEND TO THE SERVER
# PROFILE DATA
###################################
api_reply_object = send_unblinded_ticket_to_server(
which_ticket, which_location, protocol_choice, connection_observer
profile = ProfileController.get(profile_id)
if not profile:
error_msg = f"Invalid profile id of {profile_id}"
logger.error(error_msg)
return Result(valid=False, message=error_msg, error_type=ResultError.INVALID_INPUT)
if profile.ticket:
error_msg = f"This profile already has a ticket of {profile.ticket}."
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
location_id = profile.location.id
protocol_choice = ProtocolChoice(profile.connection.code)
PRE_REQS = [profile_id, location_id, protocol_choice, which_ticket]
if None in PRE_REQS:
return Result(valid=False, error_type=ResultError.MISSING_DATA, message="You are missing the profile data to use a ticket with.")
###################################
# FILTER TICKET DATA
###################################
# does the ticket's file exist:
ticket_exists = does_ticket_file_exist(which_ticket)
if ticket_exists == False:
error_msg = "The ticket file does not exist in the correct folder."
ticket_observer.notify("failed_input", subject=error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
# does the file keeping track of ALL tickets exist:
does_the_file_exist, path_of_file = does_ticket_tracker_exist()
if does_the_file_exist == False:
error_msg = f"The ticket tracker organizer file does not exist. Check the folder {path_of_file}"
ticket_observer.notify("failed_input", subject=error_msg)
return Result(valid=False, error_type=ResultError.MISSING_FILE, message=error_msg)
###################################
# PREP PAYLOAD
###################################
payload_result = make_payload(which_ticket=which_ticket, protocol_choice=protocol_choice)
if not payload_result.valid:
return payload_result
payload = payload_result.data
logger.info("Payload ready.")
###################################
# SEND TO API
###################################
which_endpoint = "validate"
url = make_url(which_endpoint)
ticket_observer.notify("connecting", "Connecting..")
server_reply = api_call_and_extraction(
desired_type=None,
method=ApiCall.POST,
url=url,
connection_observer=connection_observer,
payload=payload,
return_server_reply=True
)
# NETWORK ERROR.
if not api_reply_object.valid:
return Result(valid=False, error_type=ResultError.CONNECTION, message=f"Could not establish a connection to Ticketing Server. {api_reply_object.message}.")
# this is a connection error or invalid format itself:
if isinstance(server_reply, Result) or not isinstance(server_reply, ServerReply):
ticket_observer.notify("error", subject=server_reply.message)
return server_reply
###################################
# VERIFY DATA STRUCTURE
# SUCCESS
###################################
raw_data_dict = api_reply_object.data
if server_reply.valid or server_reply.error_type == ServerError.ALREADY_USED:
billing_code = server_reply.billing_code
expires_at = server_reply.expires_at
# INVALID STRUCTURE:
if not isinstance(raw_data_dict, dict):
error_msg = f"The Ticketing Server gave an invalid data format {type(raw_data_dict)} in it's reply."
logger.error(f"{error_msg} with raw value of {raw_data_dict}.")
return Result(valid=False, error_type=ResultError.INVALID_API_REPLY, message=f"{error_msg} {CUSTOMER_SERVICE_MSG}")
if not billing_code or not expires_at:
return Result(valid=False, error_type=ResultError.SERVER_SIDE, message="Server Replied it worked, but failed to provide a valid code.")
if "valid" not in raw_data_dict:
error_msg = "The Ticketing Server gave an invalid data format that won't even tell us if it's valid or not."
logger.error(f"{error_msg} with a raw value of {raw_data_dict}")
return Result(valid=False, error_type=ResultError.INVALID_API_REPLY, message=f"{error_msg} {CUSTOMER_SERVICE_MSG}")
logger.debug("If we made it to this point, the API server connected and gave a correctly structured reply.")
###################################
# EXTRACT & ACT
###################################
valid = raw_data_dict.get("valid", None)
billing_code = raw_data_dict.get("billing_code", None)
expires_at = raw_data_dict.get("expires_at", None)
message = raw_data_dict.get("message", None)
logger.info(f"Was the ticket use valid? {valid}")
# GOOD OR ALREADY USED
if valid or message == "already_used":
logger.info("This is a valid use, or a previously valid use that's still good.")
# update the ticket tracker:
saved = update_ticket_tracker(
which_ticket=which_ticket,
status="used",
location=which_location,
subscription=billing_code
)
# Emergency:
if not saved:
emergency_save_billing(
which_ticket=which_ticket,
billing_code=billing_code
)
subscription = Subscription(
return save_ticket_use(
profile_id=profile_id,
billing_code=billing_code,
expires_at=expires_at,
location_id=location_id,
which_ticket=which_ticket
)
# even if not saved return it back, so it can be saved with the profile by other functions,
return Result(valid=True, data=subscription)
###################################
# EXPIRED
if not valid and message == "expired":
logger.error(f"Expired Ticket! Wiping slot {which_ticket}")
saved = update_ticket_tracker(
which_ticket=which_ticket,
status="unused",
location=None,
subscription=None
)
if not saved:
logger.error(f"Failed to save {which_ticket} is expired! That's okay the data is worthless to save.")
return Result(valid=False, error_type=ResultError.SUBSCRIPTION, message=f"This ticket {which_ticket}'s subscription is expired. Do you want to wipe it?")
###################################
if server_reply.error_type == ServerError.EXPIRED:
error_msg = "This ticket is expired. Do you want to wipe ALL your tickets? To start over."
return Result(valid=False, error_type=ResultError.TICKET_EXPIRED, message=error_msg)
# UNKNOWN
return Result(valid=False, error_type=ResultError.UNKNOWN, message=f"Unknown error with this data: {raw_data_dict}. {CUSTOMER_SERVICE_MSG}")
###################################
# WRONG LOCATION
###################################
if server_reply.error_type == ServerError.WRONG_LOCATION:
error_msg = "The location entered is invalid. Do you want to sync location data now?"
return Result(valid=False, error_type=ResultError.NEED_SYNC, message=error_msg)
###################################
# NODE DOWN
###################################
if server_reply.error_type == ServerError.NODE_ERROR:
error_msg = f"{server_reply.message}. Do you want to switch locations for this profile?"
return Result(valid=False, error_type=ResultError.NODE_ERROR, message=error_msg) # Each GUI will navigate to edit.
###################################
# NO SOLUTION:
###################################
return translate_server_failure(server_reply=server_reply)
def emergency_save_billing(
which_ticket: int,
billing_code: str
) -> None:
docstring = """
If ticket tracker fails to save it to the JSON. Emergency save to a random text file.
"""
logger.error(docstring)
which_ticket_as_str = str(which_ticket)
write_string_to_text_file(
billing_code,
f"{Constants.HV_TICKETING_CONFIG_HOME}/emergency_code_for_profile_{which_ticket_as_str}.txt",
)
# # # the actual work here, everything else is just handling:
# # ticket_observer.notify("connecting", "Connecting..")
# # reply_object = use_ticket_orchestrator(
# # which_ticket=which_ticket,
# # which_location=which_location,
# # protocol_choice=protocol_choice,
# # connection_observer=connection_observer
# # )
# # return reply_object
# @type_checker
# def use_ticket_orchestrator(
# which_ticket: int,
# which_location: int, # Location.id
# protocol_choice: ProtocolChoice,
# connection_observer: ConnectionObserver,
# ) -> Result:
# pass
# """
# Purpose:
# Coordinates using a ticket.
# Method:
# 1) Send to the server
# 2) Evaluate the response
# 3) updates the 'ticker_tracker' JSON with new data
# 4) Formats error messages for UI
# """
# save_billing_code(profile_id: int, billing_code: str, expires_at: datetime)
# ###################################
# # SEND TO THE SERVER
# ###################################
# api_reply_object = send_unblinded_ticket_to_server(
# which_ticket, which_location, protocol_choice, connection_observer
# )
# # NETWORK ERROR.
# if not api_reply_object.valid:
# return Result(valid=False, error_type=ResultError.CONNECTION, message=f"Could not establish a connection to Ticketing Server. {api_reply_object.message}.")
# ###################################
# # VERIFY DATA STRUCTURE
# ###################################
# raw_data_dict = api_reply_object.data
# # INVALID STRUCTURE:
# if not isinstance(raw_data_dict, dict):
# error_msg = f"The Ticketing Server gave an invalid data format {type(raw_data_dict)} in it's reply."
# logger.error(f"{error_msg} with raw value of {raw_data_dict}.")
# return Result(valid=False, error_type=ResultError.INVALID_API_REPLY, message=f"{error_msg} {CUSTOMER_SERVICE_MSG}")
# if "valid" not in raw_data_dict:
# error_msg = "The Ticketing Server gave an invalid data format that won't even tell us if it's valid or not."
# logger.error(f"{error_msg} with a raw value of {raw_data_dict}")
# return Result(valid=False, error_type=ResultError.INVALID_API_REPLY, message=f"{error_msg} {CUSTOMER_SERVICE_MSG}")
# logger.debug("If we made it to this point, the API server connected and gave a correctly structured reply.")
# ###################################
# # EXTRACT & ACT
# ###################################
# valid = raw_data_dict.get("valid", None)
# billing_code = raw_data_dict.get("billing_code", None)
# expires_at = raw_data_dict.get("expires_at", None)
# message = raw_data_dict.get("message", None)
# logger.info(f"Was the ticket use valid? {valid}")
# # GOOD OR ALREADY USED
# if valid or message == "already_used":
# logger.info("This is a valid use, or a previously valid use that's still good.")
# def save_billing(billing_code: str):
# # Emergency:
# if not saved:
# emergency_save_billing(
# which_ticket=which_ticket,
# billing_code=billing_code
# )
# subscription = Subscription(
# billing_code=billing_code,
# expires_at=expires_at,
# )
# # even if not saved return it back, so it can be saved with the profile by other functions,
# return Result(valid=True, data=subscription)
# # EXPIRED
# if not valid and message == "expired":
# logger.error(f"Expired Ticket! Wiping slot {which_ticket}")
# saved = update_ticket_tracker(
# which_ticket=which_ticket,
# status="unused",
# location=None,
# subscription=None
# )
# if not saved:
# logger.error(f"Failed to save {which_ticket} is expired! That's okay the data is worthless to save.")
# return Result(valid=False, error_type=ResultError.SUBSCRIPTION, message=f"This ticket {which_ticket}'s subscription is expired. Do you want to wipe it?")
# # UNKNOWN
# return Result(valid=False, error_type=ResultError.UNKNOWN, message=f"Unknown error with this data: {raw_data_dict}. {CUSTOMER_SERVICE_MSG}")
# def emergency_save_billing(
# which_ticket: int,
# billing_code: str
# ) -> None:
# docstring = """
# If ticket tracker fails to save it to the JSON. Emergency save to a random text file.
# """
# logger.error(docstring)
# which_ticket_as_str = str(which_ticket)
# write_string_to_text_file(
# billing_code,
# f"{Constants.HV_TICKETING_CONFIG_HOME}/emergency_code_for_profile_{which_ticket_as_str}.txt",
# )