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:
parent
e296e212db
commit
1f4b37dbe9
12 changed files with 408 additions and 122 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
10
core/errors/error_shortcuts.py
Normal file
10
core/errors/error_shortcuts.py
Normal 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)
|
||||||
|
|
||||||
|
|
@ -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))
|
||||||
|
|
||||||
# Back to main thread
|
finally:
|
||||||
print(f"result in plumbing is {result}")
|
# Back to main thread
|
||||||
self.ui_dispatcher.run_on_main_thread(self._on_result_ready, result)
|
print(f"result in plumbing is {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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
114
core/services/payment_phase/payment_update_tools.py
Normal file
114
core/services/payment_phase/payment_update_tools.py
Normal 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.")
|
||||||
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue