Improved Multi-payment-cycle management. Moved check_paid and mark_paid to isolated service. Setup primary configuration to track ticket setup

This commit is contained in:
SimplifiedPrivacy 2026-10-17 11:33:39 -04:00
parent e296e212db
commit 1f4b37dbe9
12 changed files with 408 additions and 122 deletions

View file

@ -152,3 +152,28 @@ class ConfigurationController:
configuration.singbox = new_value configuration.singbox = new_value
configuration.save() configuration.save()
return True return True
@staticmethod
def change_random_ticket(new_value: bool) -> bool:
configuration = ConfigurationController.get()
configuration.use_random = new_value
configuration.save()
return True
@staticmethod
def use_random() -> bool:
configuration = ConfigurationController.get()
return configuration.use_random
@staticmethod
def tickets_setup(new_value: bool) -> bool:
configuration = ConfigurationController.get()
configuration.tickets_setup = new_value
configuration.save()
return True
@staticmethod
def are_tickets_setup() -> bool:
configuration = ConfigurationController.get()
return configuration.tickets_setup

View file

@ -0,0 +1,10 @@
from core.models.Result import Result, ResultError
from core.errors.logger import logger
def not_supported():
NOT_SUPPORTED_PAYMENT = "This is not a supported payment category"
logger.error(NOT_SUPPORTED_PAYMENT)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=NOT_SUPPORTED_PAYMENT)

View file

@ -51,27 +51,35 @@ class ThreadPlumbing:
def _on_complete(self, future: Future): def _on_complete(self, future: Future):
"""Handle thread completion""" """Handle thread completion"""
try: try:
logger.info("The plumbing module started it's 'on complete' phase..") logger.info("The plumbing module started it's 'on complete' phase with this result:")
result = future.result() result = future.result()
if not isinstance(result, Result): if not isinstance(result, Result):
result = convert_to_result(result) result = convert_to_result(result)
logger.info(result)
if not result.valid: if not result.valid:
if not result.function_choice: if not result.function_choice:
result.function_choice = resolve_prescription(result, self.ENUM_MAP, *self.args, **self.kwargs) result.function_choice = resolve_prescription(result, self.ENUM_MAP, *self.args, **self.kwargs)
logger.info(f"result.function_choice is {result.function_choice}")
else: else:
logger.info("Using the pre-existing function choice for this error.") logger.info("Using the pre-existing function choice for this error.")
except Exception as e: except Exception as e:
error_msg = f"There was an error: {str(e)}" error_msg = f"There was an error: {str(e)}"
# notify(error_msg) # notify(error_msg)
logger.error(error_msg) logger.error(error_msg)
result = Result(valid=False, error_type=ResultError.UNKNOWN, message=str(e)) result = Result(valid=False, error_type=ResultError.UNKNOWN, message=str(e))
finally:
# Back to main thread # Back to main thread
print(f"result in plumbing is {result}") print(f"result in plumbing is {result}")
self.ui_dispatcher.run_on_main_thread(self._on_result_ready, result) self.ui_dispatcher.run_on_main_thread(self._on_result_ready, result)
def _on_result_ready(self, result: Result): def _on_result_ready(self, result: Result):
"""Decide: retry, show popup, or final callback""" """Decide: retry, show popup, or final callback"""
logger.info(f"We are on the main thread with result {result}")
if result.valid or self.attempt_count >= MAX_ATTEMPTS: if result.valid or self.attempt_count >= MAX_ATTEMPTS:
if self.ui_caller: if self.ui_caller:
print("calling back with result") print("calling back with result")
@ -89,11 +97,11 @@ class ThreadPlumbing:
if self.ui_caller: if self.ui_caller:
self.ui_caller(result) self.ui_caller(result)
# notify(f"Error prompt {self.attempt_count} of {MAX_ATTEMPTS}")
# Show popup and wait for user choice # Show popup and wait for user choice
choice = bool(result.function_choice) choice = bool(result.function_choice)
print(f"is there a choice? {choice}") logger.info(f"On the main thread, the choice is {choice}")
popup = self.error_implementation( popup = self.error_implementation(
choice=bool(result.function_choice), choice=bool(result.function_choice),
title=f"Error: {result.error_type.value}", title=f"Error: {result.error_type.value}",
@ -131,17 +139,21 @@ def resolve_prescription(result: Result, ENUM_MAP: dict, *args, **kwargs):
And then add the args from either the original caller's args, And then add the args from either the original caller's args,
or the defaults for that error type or the defaults for that error type
""" """
logger.info(f"resolve_prescription is being called for {result}.")
# Priority 1: If the function explicitly provided a full partial, use it. # Priority 1: If the function explicitly provided a full partial, use it.
if result.function_choice: if result.function_choice:
logger.info("The result object itself has a function choice!")
return result.function_choice return result.function_choice
# Priority 2: Look up the Enum map # Priority 2: Look up the Enum map
# logger.info(f"self.ENUM_MAP is {self.ENUM_MAP}") logger.info("checking enum map")
if result.error_type not in ENUM_MAP: if result.error_type not in ENUM_MAP:
logger.info(f"No solution in the ENUM_MAP of {ENUM_MAP}")
return None # No solution return None # No solution
enum_value = ENUM_MAP[result.error_type] enum_value = ENUM_MAP[result.error_type]
logger.info(f"got enum_value {enum_value}")
# Handle the None case (no solution) # Handle the None case (no solution)
if enum_value is None: if enum_value is None:

View file

@ -36,6 +36,7 @@ import shutil
import tempfile import tempfile
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from enum import Enum from enum import Enum
import inspect
from core.models.manage.session_management import init_session, get_session from core.models.manage.session_management import init_session, get_session

View file

@ -33,7 +33,8 @@ class Configuration(BaseModel):
dns: Optional[bool] = False dns: Optional[bool] = False
did_sudo_setup: Optional[bool] = False did_sudo_setup: Optional[bool] = False
singbox: Optional[str] = None singbox: Optional[str] = None
tickets_setup: Optional[bool] = False
use_random: Optional[bool] = False
@field_validator('last_synced_at', mode='before') @field_validator('last_synced_at', mode='before')

View file

@ -26,6 +26,8 @@ class ResultError(Enum):
STATE_CONFLICT = "state_conflict" STATE_CONFLICT = "state_conflict"
TICKET = "ticket" TICKET = "ticket"
NO_TICKET = "no_ticket" NO_TICKET = "no_ticket"
RANDOM_TICKETS_OFF = "random_tickets_off"
TICKET_SETUP = "ticket_setup"
ALREADY_EXISTS = "already_exists" ALREADY_EXISTS = "already_exists"
BILLING_CODE_EXISTS = "billing_code_exists" BILLING_CODE_EXISTS = "billing_code_exists"
PROCESS_GOT_KILLED = "process_got_killed" PROCESS_GOT_KILLED = "process_got_killed"

View file

@ -135,7 +135,7 @@ def api_call_and_extraction(
#----------------------------- #-----------------------------
# Layer 2: Validity of Server's Reply: # Layer 2: Validity of Server's Reply:
pre_validated_reply = api_response.data pre_validated_reply = api_response.data
logger.info("Validating the Validity of the Server's Reply..") logger.info(f"Validating the Validity of the Server's Reply {pre_validated_reply}")
worked, server_reply = validate_model(ServerReply, pre_validated_reply) worked, server_reply = validate_model(ServerReply, pre_validated_reply)
error_msg = f"Validation of the Server's Reply had issues! Pre-validate: {pre_validated_reply}. Post validate: {server_reply}" error_msg = f"Validation of the Server's Reply had issues! Pre-validate: {pre_validated_reply}. Post validate: {server_reply}"
@ -152,7 +152,7 @@ def api_call_and_extraction(
logger.info("Validation successfull. Checking if the server's reply says the operation worked..") logger.info("Validation successfull. Checking if the server's reply says the operation worked..")
if not server_reply.valid: 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 {raw_reply}" error_message = f"Server reply was correctly structured, but the actual invoice failed to be created. The raw data is {server_reply}"
logger.error(error_message) logger.error(error_message)
# translate by enum error code: # translate by enum error code:
return translate_server_failure(server_reply) return translate_server_failure(server_reply)

View file

@ -1,5 +1,5 @@
from core.infrastructure.thread_dispatch import MainThreadDispatcher from core.infrastructure.thread_dispatch import MainThreadDispatcher
from core.services.subscriptions.subscriptions_by_protocol import check_paid, mark_as_paid from core.services.payment_phase.payment_update_tools import check_paid, mark_as_paid
from core.controllers.tickets.TicketPayController import check_if_ticket_paid from core.controllers.tickets.TicketPayController import check_if_ticket_paid
from core.observers.ConnectionObserver import ConnectionObserver from core.observers.ConnectionObserver import ConnectionObserver

View file

@ -151,7 +151,7 @@ class TicketPaymentData(StatePersistenceMixin):
how_many_profiles: int = HOW_MANY_PROFILES_DEFAULT how_many_profiles: int = HOW_MANY_PROFILES_DEFAULT
use_random: bool = False use_random: bool = False
temp_billing_code: Optional[str] = None billing_code: Optional[str] = None
billing_pending: bool = True billing_pending: bool = True
paid: bool = False paid: bool = False
@ -204,7 +204,7 @@ class OverallPaymentCycle():
already_polling: bool = False already_polling: bool = False
profile_id: int = None profile_id: int = None
active: bool = False active: bool = False
active_data: Any = None billing_data: Any = None
got_invoice: bool = False got_invoice: bool = False
billing_code: str = None billing_code: str = None
@ -215,7 +215,7 @@ class OverallPaymentCycle():
def wipe(self): def wipe(self):
"""Wipe all data and start over.""" """Wipe all data and start over."""
self.active = False self.active = False
self.active_data = None self.billing_data = None
self.category = PaymentCategory.INDIVIDUAL self.category = PaymentCategory.INDIVIDUAL
self.already_polling = False self.already_polling = False
self.profile_id = None self.profile_id = None

View file

@ -0,0 +1,114 @@
from core.models.manage.server_reply_tools import api_call_and_extraction, ApiCall
from core.controllers.ProfileController import ProfileController
from core.services.payment_phase.payment_state import OverallPaymentCycle
from core.models.enum_models import PaymentCategory
from core.observers.ConnectionObserver import ConnectionObserver
from core.Constants import Constants
from core.models.Result import Result, ResultError
from core.errors.logger import logger
from core.errors.error_shortcuts import not_supported
def _mark_profile_as_paid(profile_id: int):
"""Save to disk that an individual profile is paid."""
logger.info(f"Marking profile_id {str(profile_id)} as paid..")
# Get real profile object:
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)
profile.subscription.billing_pending = False
ProfileController.update(profile)
return Result(valid=True)
def mark_as_paid() -> Result:
"""Conclude the payment phase for ALL profiles."""
logger.info("Marking as paid..")
payment_cycle = OverallPaymentCycle()
if not payment_cycle:
error_msg = "There is no current billing cycle."
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
billing_data = payment_cycle.instance
if not billing_data:
error_msg = "There is no current billing data."
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
category = payment_cycle.category
if category == PaymentCategory.INDIVIDUAL:
profile_id = billing_data.instance_id
result = _mark_profile_as_paid(profile_id)
if not result.valid:
return result
elif category == PaymentCategory.TICKET:
# saving global ticket data:
billing_data.billing_pending = False
billing_data.paid = True
billing_data.save()
else:
return not_supported()
# finally:
payment_cycle.wipe()
return Result(valid=True)
def check_paid(billing_code: str, connection_observer: ConnectionObserver) -> Result:
"""
Purpose:
Evaluates Payment Status for EITHER payment category
Requires:
Valid OverallPaymentCycle
billing_code is provided by the timer for BOTH types.
"""
payment_cycle = OverallPaymentCycle()
category = payment_cycle.category
logger.info(f"Checking paid with category of {category}")
# Get endpoint:
if category == PaymentCategory.INDIVIDUAL:
url = f'{Constants.SP_BASE_URL}/v2/check_paid'
elif category == PaymentCategory.TICKET:
url = f'{Constants.TICKET_API_BASE_URL}/check_ticket_paid'
else:
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="This is not a supported payment category to check")
submit_data = {
"billing_code": billing_code
}
result = api_call_and_extraction(
desired_type="payment_status",
method=ApiCall.POST,
url=url,
connection_observer=connection_observer,
payload=submit_data
)
if not result.valid:
return result
payment_status = result.data
# paid
if payment_status:
return Result(valid=True, message="paid")
else:
return Result(valid=False, error_type=ResultError.UNPAID, message="The API call went through, but this subscription is not yet paid.")

View file

@ -1,3 +1,4 @@
from core.controllers.ConfigurationController import ConfigurationController
from core.utils.basic_operations.write_or_read_from_json import read_entire_json, update_json, write_json_to_file from core.utils.basic_operations.write_or_read_from_json import read_entire_json, update_json, write_json_to_file
from core.utils.basic_operations.does_file_exist import does_file_exist from core.utils.basic_operations.does_file_exist import does_file_exist
from core.errors.exceptions import * from core.errors.exceptions import *
@ -11,6 +12,52 @@ from pathlib import Path
import shutil import shutil
def does_ticket_tracker_exist() -> tuple:
if os.path.exists(Constants.TICKET_TRACKER_PATH):
return (True, Constants.TICKET_TRACKER_PATH)
else:
return (False, Constants.TICKET_TRACKER_PATH)
def ticket_tracker_exists() -> bool:
return os.path.exists(Constants.TICKET_TRACKER_PATH)
def ticket_config_exists():
filepath = f"{Constants.HV_TICKETING_CONFIG_HOME}/billing_choices.json"
return does_file_exist(filepath)
def get_a_random_ticket() -> Result:
"""Get a random ticket"""
results = get_all_unused_tickets()
valid = results.get("valid")
tickets = results.get("data")
if valid and tickets:
one_random_ticket = tickets.random()
return Result(valid=True, data=one_random_ticket)
message = result.get("message")
if message == "no_tickets_left":
return Result(valid=False, error_type=ResultError.NO_TICKET, message="You do not have any tickets left, would you like to start a new phase to get them?")
elif message == "invalid_data":
return Result(valid=False, error_type=ResultError.TICKET_SETUP, message="The ticket tracker file is either missing values or corrupted.")
elif not ticket_config_exists():
return Result(valid=False, error_type=ResultError.TICKET_SETUP, message="You have not yet setup the ticketing config.")
elif not does_ticket_tracker_exist():
return Result(valid=False, error_type=ResultError.TICKET_SETUP, message="You have not yet setup the ticket tracker. The file is gone, corrupted, or missing.")
else:
return Result(valid=False, error_type=ResultError.TICKET_SETUP, message="For unknown reasons, we are unable to read the ticket tracker.")
def delete_ticket_data(): def delete_ticket_data():
ticket_folders = [Constants.HV_TICKETING_CONFIG_HOME, Constants.HV_TICKETING_DATA_HOME] ticket_folders = [Constants.HV_TICKETING_CONFIG_HOME, Constants.HV_TICKETING_DATA_HOME]
quantity_to_delete = len(ticket_folders) quantity_to_delete = len(ticket_folders)
@ -66,13 +113,6 @@ def is_given_ticket_able_to_be_read(which_ticket: int) -> bool:
return False return False
def does_ticket_tracker_exist() -> tuple:
if os.path.exists(Constants.TICKET_TRACKER_PATH):
return (True, Constants.TICKET_TRACKER_PATH)
else:
return (False, Constants.TICKET_TRACKER_PATH)
def get_data_for_a_single_ticket(which_ticket_as_int: int) -> tuple: def get_data_for_a_single_ticket(which_ticket_as_int: int) -> tuple:
data = read_entire_json(Constants.TICKET_TRACKER_PATH) data = read_entire_json(Constants.TICKET_TRACKER_PATH)

View file

@ -1,3 +1,4 @@
from core.services.payment_phase.check_payment_loop import CheckPaidTimer
from core.models.manage.server_reply_tools import api_call_and_extraction, ApiCall from core.models.manage.server_reply_tools import api_call_and_extraction, ApiCall
from core.controllers.ProfileController import ProfileController from core.controllers.ProfileController import ProfileController
from core.models.BaseProfile import BaseProfile as Profile from core.models.BaseProfile import BaseProfile as Profile
@ -10,16 +11,12 @@ from core.models.Subscription import Subscription
from core.Constants import Constants from core.Constants import Constants
from core.models.Result import Result, ResultError from core.models.Result import Result, ResultError
from core.errors.logger import logger from core.errors.logger import logger
from core.errors.error_shortcuts import not_supported
# generic # generic
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
def not_supported():
NOT_SUPPORTED_PAYMENT = "This is not a supported payment category"
logger.error(NOT_SUPPORTED_PAYMENT)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=NOT_SUPPORTED_PAYMENT)
def invalid_server_format() -> Result: def invalid_server_format() -> Result:
error_msg = "The server gave an invalid response format." error_msg = "The server gave an invalid response format."
@ -91,53 +88,125 @@ def is_payment_active_for(profile_id: int) -> tuple:
return False, False return False, False
def start_new_payment_cycle(category: PaymentCategory, profile_id: Optional[int] = None) -> Result: def error_about_mismatch(category: PaymentCategory) -> Result:
"""Display an error for the billing process already running, with a choice to end it"""
if category == PaymentCategory.INDIVIDUAL:
error_msg = "You want an individual process, but there's already a ticketing billing process active right now, do you want to stop that?"
else:
error_msg = "You want a ticketing process, but there's already an individual billing process active right now, do you want to stop that?"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.ACTIVE_BILLING, message=error_msg)
def evaluate_cycle(payment_cycle: OverallPaymentCycle, profile_id: int) -> Result:
if the_same_payment_cycle(payment_cycle=payment_cycle, profile_id=profile_id):
logger.info("It's the same as before, returning the data")
return Result(valid=True, data=payment_cycle.billing_data)
else:
error_msg = f"You activated profile {profile_id}, but profile {payment_cycle.billing_data.instance_id} has active billing, stop that?"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.ACTIVE_BILLING, message=error_msg)
def start_new_payment_cycle(category: PaymentCategory, profile_id: Optional[int] = None) -> Result:
"""
Purpose:
Starts the overall payment cycle, and the individual category's billing data.
On Conflict:
If there's already a cycle running, this function addresses that,
by returning the cycle if it's the same, or offering to end it if different.
"""
# PRE-REQS:
if not profile_id and category == PaymentCategory.INDIVIDUAL: if not profile_id and category == PaymentCategory.INDIVIDUAL:
error_msg = "Need a profile id for an individual profile." error_msg = "Need a profile id for an individual profile."
logger.error(error_msg) logger.error(error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg) return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
logger.info("Got pre-reqs for individual.")
# Is there currently a core process? # This is the singleton:
payment_cycle = OverallPaymentCycle() payment_cycle = OverallPaymentCycle()
logger.info(f"payment_cycle is {payment_cycle}")
if payment_cycle.active: # ----------------------------
if the_same_payment_cycle(payment_cycle=payment_cycle, profile_id=profile_id): # MISMATCH IN CATEGORY FROM WHAT EXISTS:
logger.info("It's the same as before, returning the code") # ----------------------------
return Result(valid=True, data=payment_cycle.billing_code) if category != payment_cycle.category:
else: return error_about_mismatch(category)
error_msg = f"There's already a billing process active, do you want to stop that?" logger.info("Made it past the mismatch check.")
logger.error(error_msg)
# ----------------------------
# ALREADY ACTIVE INDIVIDUAL CYCLE:
# ----------------------------
if all([ # all are true:
payment_cycle.billing_data,
payment_cycle.active,
payment_cycle.category == PaymentCategory.INDIVIDUAL,
category == PaymentCategory.INDIVIDUAL
]):
profile_id_already_running = payment_cycle.billing_data.instance_id
if profile_id_already_running:
logger.info(f"Triggering the evaluate_cycle reply because of {profile_id_already_running} running already")
return evaluate_cycle(payment_cycle=payment_cycle, profile_id=profile_id)
# ----------------------------
# ALREADY POLLING TIMER:
# ----------------------------
try:
timer_loop = CheckPaidTimer.get_instance()
if timer_loop.is_running:
error_msg = "There's already a billing cycle that's actively checking if paid. Do you want to stop that?"
return Result(valid=False, error_type=ResultError.ACTIVE_BILLING, message=error_msg) return Result(valid=False, error_type=ResultError.ACTIVE_BILLING, message=error_msg)
except Exception as e:
logger.info("All good, the timer isn't running, and we were afraid it was.")
logger.info("Process is NOT yet active, starting..") logger.info("Process is NOT yet active, starting..")
payment_cycle.begin() payment_cycle.begin()
payment_cycle.category = category payment_cycle.category = category
# ----------------------------
# START INDIVIDUAL:
# ----------------------------
if category == PaymentCategory.INDIVIDUAL: if category == PaymentCategory.INDIVIDUAL:
payment_cycle.profile_id = profile_id payment_cycle.profile_id = profile_id
payment_cycle.active_data = PaymentData(instance_id=profile_id) payment_cycle.billing_data = PaymentData(instance_id=profile_id)
subscription = get_profile_subscription(profile_id=profile_id, save_pending=True) # save to disk that it's pending subscription = get_profile_subscription(profile_id=profile_id, save_pending=True) # save to disk that it's pending
if subscription: if subscription:
billing_code = subscription.billing_code billing_code = subscription.billing_code
else: else:
billing_code = None billing_code = None
logger.info("Individual profile initial setup complete.")
# ----------------------------
# START TICKETS:
# ----------------------------
elif category == PaymentCategory.TICKET: elif category == PaymentCategory.TICKET:
billing_data = TicketPaymentData() billing_data = TicketPaymentData()
payment_cycle.active_data = billing_data # saves payment_cycle.billing_data = billing_data # saves
billing_code = billing_data.temp_billing_code billing_code = billing_data.billing_code # might be empty if new
logger.info("Ticketing initial setup complete.")
else: else:
error_msg = f"Invalid PaymentCategory selected of {str(category)}" error_msg = f"Invalid PaymentCategory selected of {str(category)}"
logger.error(error_msg) logger.error(error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg) return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
# save in cache: # ----------------------------
# COMMON FOR BOTH CATEGORIES:
# ----------------------------
payment_cycle.billing_code = billing_code payment_cycle.billing_code = billing_code
payment_cycle.active = True payment_cycle.active = True
return Result(valid=True, data=billing_code) logger.info(f"Returning the entire payment_cycle")
return Result(valid=True, data=payment_cycle)
# else:
# return Result(valid=False, error_type=ResultError.UNKNOWN, message="We failed to create new billing cycle data.")
def create_new_invoice(connection_observer: ConnectionObserver) -> Result: def create_new_invoice(connection_observer: ConnectionObserver) -> Result:
@ -187,16 +256,16 @@ def return_pre_existing_invoice(billing_code: str, connection_observer: Connecti
def create_ticket_invoice(payment_cycle: OverallPaymentCycle, connection_observer: ConnectionObserver) -> Result: def create_ticket_invoice(payment_cycle: OverallPaymentCycle, connection_observer: ConnectionObserver) -> Result:
logger.info("Creating a new TICKET invoice..") logger.info("Creating a new TICKET invoice..")
billing_data = payment_cycle.active_data billing_data = payment_cycle.billing_data
if not billing_data: if not billing_data:
error_msg = "You are currently missing billing data to begin an invoice" error_msg = "You are currently missing billing data to begin an invoice"
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg) return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
# already has billing id: # already has billing id:
billing_code = billing_data.temp_billing_code billing_code = billing_data.billing_code
if billing_code: if billing_code:
logger.info("We already have a temp_billing_code id for this ticket prep.") logger.info("We already have a billing_code id for this ticket prep.")
return return_pre_existing_invoice(billing_code=billing_code, connection_observer=connection_observer) return return_pre_existing_invoice(billing_code=billing_code, connection_observer=connection_observer)
#----------------------------- #-----------------------------
@ -229,7 +298,7 @@ def create_ticket_invoice(payment_cycle: OverallPaymentCycle, connection_observe
#----------------------------- #-----------------------------
billing_code = final_invoice.billing_code billing_code = final_invoice.billing_code
billing_data.temp_billing_code = billing_code billing_data.billing_code = billing_code
billing_data.save() billing_data.save()
payment_cycle.got_invoice = True payment_cycle.got_invoice = True
@ -260,7 +329,7 @@ def create_individual_invoice(payment_cycle: OverallPaymentCycle, connection_obs
# MAKE PAYLOAD. PIECE BY PIECE # MAKE PAYLOAD. PIECE BY PIECE
#----------------------------- #-----------------------------
# get the payment data singleton: # get the payment data singleton:
billing_data = payment_cycle.active_data billing_data = payment_cycle.billing_data
# make sure the protocol is saved in the singleton already, # make sure the protocol is saved in the singleton already,
if not billing_data.protocol_choice: if not billing_data.protocol_choice:
@ -322,51 +391,51 @@ def create_individual_invoice(payment_cycle: OverallPaymentCycle, connection_obs
def check_paid(billing_code: str, connection_observer: ConnectionObserver) -> Result: # def check_paid(billing_code: str, connection_observer: ConnectionObserver) -> Result:
""" # """
Purpose: # Purpose:
Evaluates Payment Status for EITHER payment category # Evaluates Payment Status for EITHER payment category
Requires: # Requires:
Valid OverallPaymentCycle # Valid OverallPaymentCycle
billing_code is provided by the timer for BOTH types. # billing_code is provided by the timer for BOTH types.
""" # """
payment_cycle = OverallPaymentCycle() # payment_cycle = OverallPaymentCycle()
category = payment_cycle.category # category = payment_cycle.category
logger.info(f"Checking paid with category of {category}") # logger.info(f"Checking paid with category of {category}")
# Get endpoint: # # Get endpoint:
if category == PaymentCategory.INDIVIDUAL: # if category == PaymentCategory.INDIVIDUAL:
url = f'{Constants.SP_BASE_URL}/v2/check_paid' # url = f'{Constants.SP_BASE_URL}/v2/check_paid'
elif category == PaymentCategory.TICKET: # elif category == PaymentCategory.TICKET:
url = f'{Constants.TICKET_API_BASE_URL}/check_ticket_paid' # url = f'{Constants.TICKET_API_BASE_URL}/check_ticket_paid'
else: # else:
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="This is not a supported payment category to check") # return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="This is not a supported payment category to check")
submit_data = { # submit_data = {
"billing_code": billing_code # "billing_code": billing_code
} # }
result = api_call_and_extraction( # result = api_call_and_extraction(
desired_type="payment_status", # desired_type="payment_status",
method=ApiCall.POST, # method=ApiCall.POST,
url=url, # url=url,
connection_observer=connection_observer, # connection_observer=connection_observer,
payload=submit_data # payload=submit_data
) # )
if not result.valid: # if not result.valid:
return result # return result
payment_status = result.data # payment_status = result.data
# paid # # paid
if payment_status: # if payment_status:
return Result(valid=True, message="paid") # return Result(valid=True, message="paid")
else: # else:
return Result(valid=False, error_type=ResultError.UNPAID, message="The API call went through, but this subscription is not yet paid.") # return Result(valid=False, error_type=ResultError.UNPAID, message="The API call went through, but this subscription is not yet paid.")
def paying_for_tickets() -> bool: def paying_for_tickets() -> bool:
@ -404,7 +473,7 @@ def get_current_billing_data(category: Optional[PaymentCategory] = None) -> Resu
# if not payment_cycle and category: # if not payment_cycle and category:
billing_data = payment_cycle.active_data billing_data = payment_cycle.billing_data
if not billing_data: if not billing_data:
error_msg = "There is no current billing data." error_msg = "There is no current billing data."
@ -416,9 +485,21 @@ def get_current_billing_data(category: Optional[PaymentCategory] = None) -> Resu
def wipe_billing_cycle() -> Result: def wipe_billing_cycle() -> Result:
"""Wipes the entire overall cycle""" """Wipes the entire overall cycle"""
logger.info("Wiping the overall cycle, so this cycle no longer blocks other new ones") logger.info("Wiping the overall cycle, so this cycle no longer blocks other new ones")
# Timer:
try:
timer_loop = CheckPaidTimer.get_instance()
if timer_loop.is_running:
timer_loop.stop_polling()
except Exception as e:
logger.info("Skipping Timer since it's not yet created.")
# Overall State:
payment_cycle = OverallPaymentCycle() payment_cycle = OverallPaymentCycle()
if payment_cycle: if payment_cycle:
payment_cycle.wipe() payment_cycle.wipe()
# This has to return a Result object, even if not checked, because of it's use in plumbing,
return Result(valid=True) return Result(valid=True)
@ -462,59 +543,59 @@ def wipe_subscription(profile_id: int) -> Result:
return Result(valid=True) return Result(valid=True)
def mark_as_paid() -> Result: # def mark_as_paid() -> Result:
"""Conclude the payment phase""" # """Conclude the payment phase"""
logger.info("Marking as paid..") # logger.info("Marking as paid..")
payment_cycle = OverallPaymentCycle() # payment_cycle = OverallPaymentCycle()
if not payment_cycle: # if not payment_cycle:
error_msg = "There is no current billing cycle." # error_msg = "There is no current billing cycle."
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg) # return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
billing_data = payment_cycle.instance # billing_data = payment_cycle.instance
if not billing_data: # if not billing_data:
error_msg = "There is no current billing data." # error_msg = "There is no current billing data."
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg) # return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
category = payment_cycle.category # category = payment_cycle.category
if category == PaymentCategory.INDIVIDUAL: # if category == PaymentCategory.INDIVIDUAL:
profile_id = billing_data.instance_id # profile_id = billing_data.instance_id
result = mark_profile_as_paid(profile_id) # result = mark_profile_as_paid(profile_id)
if not result.valid: # if not result.valid:
return result # return result
elif category == PaymentCategory.TICKET: # elif category == PaymentCategory.TICKET:
# saving global ticket data: # # saving global ticket data:
billing_data.billing_pending = False # billing_data.billing_pending = False
billing_data.paid = True # billing_data.paid = True
billing_data.save() # billing_data.save()
else: # else:
return not_supported() # return not_supported()
# finally: # # finally:
payment_cycle.wipe() # payment_cycle.wipe()
return Result(valid=True) # return Result(valid=True)
def mark_profile_as_paid(profile_id: int): # def mark_profile_as_paid(profile_id: int):
logger.info(f"Marking profile_id {str(profile_id)} as paid..") # logger.info(f"Marking profile_id {str(profile_id)} as paid..")
# Get real profile object: # # Get real profile object:
profile = ProfileController.get(profile_id) # profile = ProfileController.get(profile_id)
if not profile: # if not profile:
error_msg = f"Invalid profile id of {profile_id}" # error_msg = f"Invalid profile id of {profile_id}"
logger.error(error_msg) # logger.error(error_msg)
return Result(valid=False, message=error_msg, error_type=ResultError.INVALID_INPUT) # return Result(valid=False, message=error_msg, error_type=ResultError.INVALID_INPUT)
profile.subscription.billing_pending = False # profile.subscription.billing_pending = False
ProfileController.update(profile) # ProfileController.update(profile)
return Result(valid=True) # return Result(valid=True)
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) -> Result: