diff --git a/core/Constants.py b/core/Constants.py index 5a9aadd..9ba2eb1 100644 --- a/core/Constants.py +++ b/core/Constants.py @@ -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') diff --git a/core/controllers/tickets/TicketPayController.py b/core/controllers/tickets/TicketPayController.py index 4aa1bb0..d0c289c 100644 --- a/core/controllers/tickets/TicketPayController.py +++ b/core/controllers/tickets/TicketPayController.py @@ -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") diff --git a/core/controllers/tickets/TicketSyncController.py b/core/controllers/tickets/TicketSyncController.py index 9188441..b6ba95d 100644 --- a/core/controllers/tickets/TicketSyncController.py +++ b/core/controllers/tickets/TicketSyncController.py @@ -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"} diff --git a/core/services/payment_phase/payment_state.py b/core/services/payment_phase/payment_state.py index c9694e8..8be6568 100644 --- a/core/services/payment_phase/payment_state.py +++ b/core/services/payment_phase/payment_state.py @@ -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: diff --git a/core/services/payment_phase/save_and_send_intitial_billing.py b/core/services/payment_phase/save_and_send_intitial_billing.py index aa012db..519a6da 100644 --- a/core/services/payment_phase/save_and_send_intitial_billing.py +++ b/core/services/payment_phase/save_and_send_intitial_billing.py @@ -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) diff --git a/core/services/prepare_tickets/get_pub_key.py b/core/services/prepare_tickets/get_pub_key.py index 91a9d6f..e3f8263 100644 --- a/core/services/prepare_tickets/get_pub_key.py +++ b/core/services/prepare_tickets/get_pub_key.py @@ -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) diff --git a/core/services/subscriptions/subscriptions_by_protocol.py b/core/services/subscriptions/subscriptions_by_protocol.py index dd83784..3a684dd 100644 --- a/core/services/subscriptions/subscriptions_by_protocol.py +++ b/core/services/subscriptions/subscriptions_by_protocol.py @@ -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: diff --git a/core/utils/basic_operations/enum_tools.py b/core/utils/basic_operations/enum_tools.py new file mode 100644 index 0000000..70a2dff --- /dev/null +++ b/core/utils/basic_operations/enum_tools.py @@ -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 + diff --git a/core/utils/basic_operations/write_or_read_from_json.py b/core/utils/basic_operations/write_or_read_from_json.py index b7b1d39..d671f7e 100644 --- a/core/utils/basic_operations/write_or_read_from_json.py +++ b/core/utils/basic_operations/write_or_read_from_json.py @@ -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}")