Stable repeat Ticketing flow

This commit is contained in:
SimplifiedPrivacy 2026-10-17 14:24:06 -04:00
parent 1f4b37dbe9
commit 9ca0809ceb
5 changed files with 110 additions and 202 deletions

View file

@ -149,7 +149,7 @@ def resolve_prescription(result: Result, ENUM_MAP: dict, *args, **kwargs):
# Priority 2: Look up the Enum map # Priority 2: Look up the Enum map
logger.info("checking 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}") # 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]

View file

@ -34,7 +34,7 @@ class ServerReply():
message: Optional[str] = None message: Optional[str] = None
model_version: int = 1 model_version: int = 1
data: Optional[dict] = None data: Optional[dict] = None
conf: str = None conf: Optional[str] = None
payment_status: Optional[bool] = None payment_status: Optional[bool] = None

View file

@ -112,23 +112,11 @@ class CheckPaidTimer:
print("PAID! Stopping timer") print("PAID! Stopping timer")
self.stop_polling() self.stop_polling()
# Reset instance: # Since paid, reset the instance (wipe it):
CheckPaidTimer._instance = None CheckPaidTimer._instance = None
marked = mark_as_paid(profile_id=self.profile_id) marked = mark_as_paid(profile_id=self.profile_id)
# # SAVE INDIVIDUAL:
# if self.payment_type == PaymentType.CRYPTO:
# marked = mark_as_paid(profile_id=self.profile_id)
# # SAVE TICKETS PAID:
# elif self.payment_type == PaymentType.TICKET:
# marked = mark_global_tickets_paid()
# else:
# logger.error("Invalid payment type picked.")
# marked = Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Unsupported protocol.", billing_code=self.billing_code)
# Either way evaluate: # Either way evaluate:
if not marked.valid: if not marked.valid:
message = marked.message message = marked.message
@ -173,3 +161,17 @@ class CheckPaidTimer:
CheckPaidTimer._instance = None CheckPaidTimer._instance = None
self.view_callback(paid=False, stopped=True, message=result.message, billing_code=self.billing_code) self.view_callback(paid=False, stopped=True, message=result.message, billing_code=self.billing_code)
# # SAVE INDIVIDUAL:
# if self.payment_type == PaymentType.CRYPTO:
# marked = mark_as_paid(profile_id=self.profile_id)
# # SAVE TICKETS PAID:
# elif self.payment_type == PaymentType.TICKET:
# marked = mark_global_tickets_paid()
# else:
# logger.error("Invalid payment type picked.")
# marked = Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Unsupported protocol.", billing_code=self.billing_code)

View file

@ -8,6 +8,7 @@ from core.observers.ConnectionObserver import ConnectionObserver
from core.models.pydantic_models.type_checker import validate_model from core.models.pydantic_models.type_checker import validate_model
from core.models.pydantic_models.ServerReply import SubscriptionInvoice from core.models.pydantic_models.ServerReply import SubscriptionInvoice
from core.models.Subscription import Subscription from core.models.Subscription import Subscription
from core.utils.type_checker import type_checker
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
@ -100,8 +101,8 @@ def error_about_mismatch(category: PaymentCategory) -> Result:
return Result(valid=False, error_type=ResultError.ACTIVE_BILLING, message=error_msg) return Result(valid=False, error_type=ResultError.ACTIVE_BILLING, message=error_msg)
@type_checker
def evaluate_cycle(payment_cycle: OverallPaymentCycle, profile_id: int) -> Result: def evaluate_repeat_cycle(payment_cycle: OverallPaymentCycle, profile_id: int) -> Result:
if the_same_payment_cycle(payment_cycle=payment_cycle, profile_id=profile_id): if the_same_payment_cycle(payment_cycle=payment_cycle, profile_id=profile_id):
logger.info("It's the same as before, returning the data") logger.info("It's the same as before, returning the data")
return Result(valid=True, data=payment_cycle.billing_data) return Result(valid=True, data=payment_cycle.billing_data)
@ -111,104 +112,80 @@ def evaluate_cycle(payment_cycle: OverallPaymentCycle, profile_id: int) -> Resul
return Result(valid=False, error_type=ResultError.ACTIVE_BILLING, message=error_msg) return Result(valid=False, error_type=ResultError.ACTIVE_BILLING, message=error_msg)
@type_checker
def start_new_payment_cycle(category: PaymentCategory, profile_id: Optional[int] = None) -> Result: def start_new_payment_cycle(category: PaymentCategory, profile_id: Optional[int] = None) -> Result:
""" # 1. Pre-requisites
Purpose: if category == PaymentCategory.INDIVIDUAL and not profile_id:
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:
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.")
# This is the singleton:
payment_cycle = OverallPaymentCycle() payment_cycle = OverallPaymentCycle()
logger.info(f"payment_cycle is {payment_cycle}")
# ---------------------------- # 2. Exact Match Check
# MISMATCH IN CATEGORY FROM WHAT EXISTS: # If a cycle is active, we first check if it is EXACTLY what the user is requesting.
# ---------------------------- if payment_cycle.active and payment_cycle.billing_data:
if category != payment_cycle.category: is_same_category = (payment_cycle.category == category)
return error_about_mismatch(category)
logger.info("Made it past the mismatch check.") if is_same_category:
if category == PaymentCategory.TICKET:
# Return active ticket cycle even if billing_code is blank
logger.info("Existing active ticket cycle found. Returning it.")
return Result(valid=True, data=payment_cycle)
if category == PaymentCategory.INDIVIDUAL:
# Let evaluate_repeat_cycle handle the profile_id comparison
# and the specific "same user" vs "different user" Result errors.
return evaluate_repeat_cycle(payment_cycle=payment_cycle, profile_id=profile_id)
# ---------------------------- # 3. Collision Check (The "Prompt" Logic)
# ALREADY ACTIVE INDIVIDUAL CYCLE: # If we reached here, the active cycle (if any) is NOT a match.
# ---------------------------- if payment_cycle.active:
if all([ # all are true: # We return ACTIVE_BILLING so the UI can prompt the user to call wipe_billing_cycle()
payment_cycle.billing_data, error_msg = f"An active {payment_cycle.category} cycle is already running. Stop it first?"
payment_cycle.active, logger.info(error_msg)
payment_cycle.category == PaymentCategory.INDIVIDUAL, return Result(valid=False, error_type=ResultError.ACTIVE_BILLING, message=error_msg)
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)
# ---------------------------- # 4. Stale Timer Check
# ALREADY POLLING TIMER: # This handles cases where the cycle state is gone but the background thread is still humming.
# ----------------------------
try: try:
timer_loop = CheckPaidTimer.get_instance() timer_loop = CheckPaidTimer.get_instance()
if timer_loop.is_running: 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?" error_msg = "A billing timer is still running in the background. Do you want to stop it?"
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: except Exception as e:
logger.info("All good, the timer isn't running, and we were afraid it was.") logger.debug(f"Timer check skipped: {e}")
# 5. Initialization
logger.info("Process is NOT yet active, starting..")
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.billing_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)
if subscription: billing_code = subscription.billing_code if subscription else None
billing_code = subscription.billing_code logger.info("Individual setup complete.")
else:
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.billing_data = billing_data # saves payment_cycle.billing_data = billing_data
billing_code = billing_data.billing_code # might be empty if new billing_code = billing_data.billing_code
logger.info("Ticketing initial setup complete.") logger.info("Ticket setup complete.")
else: else:
error_msg = f"Invalid PaymentCategory selected of {str(category)}" return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Invalid Category")
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
# ---------------------------- # 6. Activation
# COMMON FOR BOTH CATEGORIES:
# ----------------------------
payment_cycle.billing_code = billing_code payment_cycle.billing_code = billing_code
payment_cycle.active = True payment_cycle.begin()
logger.info(f"Returning the entire payment_cycle") if payment_cycle.billing_data:
return Result(valid=True, data=payment_cycle) return Result(valid=True, data=payment_cycle)
# else: return Result(valid=False, error_type=ResultError.UNKNOWN, message="Failed to create billing data.")
# return Result(valid=False, error_type=ResultError.UNKNOWN, message="We failed to create new billing cycle data.")
@type_checker
def create_new_invoice(connection_observer: ConnectionObserver) -> Result: def create_new_invoice(connection_observer: ConnectionObserver) -> Result:
"""Requires an active OverallPaymentCycle, that does NOT have an invoice already.""" """Requires an active OverallPaymentCycle, that does NOT have an invoice already."""
@ -230,19 +207,28 @@ def create_new_invoice(connection_observer: ConnectionObserver) -> Result:
return create_individual_invoice(payment_cycle=payment_cycle, connection_observer=connection_observer) return create_individual_invoice(payment_cycle=payment_cycle, connection_observer=connection_observer)
if category == PaymentCategory.TICKET: if category == PaymentCategory.TICKET:
return create_ticket_invoice(payment_cycle=payment_cycle, connection_observer=connection_observer) return get_ticket_invoice(connection_observer=connection_observer)
else: else:
return not_supported() return not_supported()
def return_pre_existing_invoice(billing_code: str, connection_observer: ConnectionObserver) -> Result: @type_checker
def return_pre_existing_invoice(billing_code: str, connection_observer: ConnectionObserver, category: PaymentCategory = PaymentCategory.INDIVIDUAL) -> Result:
"""This billing_code already got an invoice, but needs to fetch that data from the server again.""" """This billing_code already got an invoice, but needs to fetch that data from the server again."""
logger.info("Fetching a pre-existing invoice..") logger.info("Fetching a pre-existing invoice..")
payload = {
"billing_code": billing_code if category == PaymentCategory.TICKET:
} payload = {
"billing_code": billing_code,
"category": "tickets"
}
else:
payload = {
"billing_code": billing_code,
"category": "individual"
}
#----------------------------- #-----------------------------
# SEND TO API # SEND TO API
@ -253,9 +239,17 @@ def return_pre_existing_invoice(billing_code: str, connection_observer: Connecti
return result return result
def create_ticket_invoice(payment_cycle: OverallPaymentCycle, connection_observer: ConnectionObserver) -> Result:
logger.info("Creating a new TICKET invoice..") def get_ticket_invoice(connection_observer: ConnectionObserver) -> Result:
"""Evaluates if it needs a new ticket invoice, and then returns pre-existing"""
payment_cycle = OverallPaymentCycle()
logger.info(f"Creating a TICKET invoice for the payment_cycle: {payment_cycle}")
logger.info(f"payment_cycle is type {type(payment_cycle)}")
logger.info(f"connection_observer is type {type(connection_observer)}")
billing_data = payment_cycle.billing_data billing_data = payment_cycle.billing_data
if not billing_data: if not billing_data:
@ -266,7 +260,7 @@ def create_ticket_invoice(payment_cycle: OverallPaymentCycle, connection_observe
billing_code = billing_data.billing_code billing_code = billing_data.billing_code
if billing_code: if billing_code:
logger.info("We already have a 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, category=PaymentCategory.TICKET)
#----------------------------- #-----------------------------
# MAKE PAYLOAD. # MAKE PAYLOAD.
@ -275,6 +269,7 @@ def create_ticket_invoice(payment_cycle: OverallPaymentCycle, connection_observe
# which_key = billing_data.which_key # which_key = billing_data.which_key
# currency = billing_data.currency # currency = billing_data.currency
logger.info(f"Evaluating if billing_data is ready to prep a payload: {billing_data}")
payload = billing_data.prep_payload() payload = billing_data.prep_payload()
if not payload: if not payload:
@ -305,6 +300,7 @@ def create_ticket_invoice(payment_cycle: OverallPaymentCycle, connection_observe
return Result(valid=True, data=final_invoice) return Result(valid=True, data=final_invoice)
def create_individual_invoice(payment_cycle: OverallPaymentCycle, connection_observer: ConnectionObserver) -> Result: def create_individual_invoice(payment_cycle: OverallPaymentCycle, connection_observer: ConnectionObserver) -> Result:
"""Prepare the payment data payload and send it to the API. Returns an invoice nested inside Result's data""" """Prepare the payment data payload and send it to the API. Returns an invoice nested inside Result's data"""
@ -390,54 +386,6 @@ def create_individual_invoice(payment_cycle: OverallPaymentCycle, connection_obs
return Result(valid=True, data=final_invoice) return Result(valid=True, data=final_invoice)
# 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.")
def paying_for_tickets() -> bool: def paying_for_tickets() -> bool:
"""Returns boolean for if the current billing is tickets""" """Returns boolean for if the current billing is tickets"""
@ -491,8 +439,11 @@ def wipe_billing_cycle() -> Result:
timer_loop = CheckPaidTimer.get_instance() timer_loop = CheckPaidTimer.get_instance()
if timer_loop.is_running: if timer_loop.is_running:
timer_loop.stop_polling() timer_loop.stop_polling()
logger.info("Wiped the CheckPaidTimer")
else:
logger.error("Timer wasn't running to begin with.")
except Exception as e: except Exception as e:
logger.info("Skipping Timer since it's not yet created.") logger.info(f"Skipping Timer since it's not yet created. BTW error was: {str(e)}")
# Overall State: # Overall State:
payment_cycle = OverallPaymentCycle() payment_cycle = OverallPaymentCycle()
@ -503,6 +454,7 @@ def wipe_billing_cycle() -> Result:
return Result(valid=True) return Result(valid=True)
@type_checker
def get_profile_subscription(profile_id: int, save_pending: Optional[bool] = False) -> Subscription: def get_profile_subscription(profile_id: int, save_pending: Optional[bool] = False) -> Subscription:
""" """
Main Purpose: Main Purpose:
@ -543,61 +495,7 @@ def wipe_subscription(profile_id: int) -> Result:
return Result(valid=True) return Result(valid=True)
# def mark_as_paid() -> Result: @type_checker
# """Conclude the payment phase"""
# 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 mark_profile_as_paid(profile_id: int):
# 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 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:
"""Checks if the billing code is already saved, and if not, saves it.""" """Checks if the billing code is already saved, and if not, saves it."""
@ -631,6 +529,7 @@ def save_billing_code(profile_id: int, billing_code: str, expires_at: datetime)
# this can be modified to accept an id, once the UI has a cache layer: # this can be modified to accept an id, once the UI has a cache layer:
@type_checker
def save_subscription(profile: Profile, subscription: Subscription) -> bool: def save_subscription(profile: Profile, subscription: Subscription) -> bool:
"""Saves subscription with the code and date.""" """Saves subscription with the code and date."""
@ -643,8 +542,9 @@ def save_subscription(profile: Profile, subscription: Subscription) -> bool:
return True return True
@type_checker
def return_pre_existing_payment_data(profile_id: int, subscription: SubscriptionInvoice) -> Result: def return_pre_existing_payment_data(profile_id: int, subscription: SubscriptionInvoice) -> Result:
"""Return the pre-existing payment details (or lack of) for a profile""" """Return the pre-existing payment details (or lack of) for an individual profile"""
logger.info("Starting a fetch of pre-existing payment data") logger.info("Starting a fetch of pre-existing payment data")

View file

@ -1,4 +1,6 @@
from core.models.Result import Result from core.models.Result import Result
from core.errors.logger import logger
import inspect import inspect
from functools import wraps from functools import wraps
@ -20,7 +22,9 @@ def type_checker(func):
bound_args.apply_defaults() bound_args.apply_defaults()
except TypeError as e: except TypeError as e:
# This handles cases where the wrong number of arguments are passed # This handles cases where the wrong number of arguments are passed
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=str(e)) error_msg = f"For {func}, {str(e)}"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
# 2. Perform type validation # 2. Perform type validation
for name, value in bound_args.arguments.items(): for name, value in bound_args.arguments.items():
@ -31,10 +35,12 @@ def type_checker(func):
continue continue
if not isinstance(value, expected_type): if not isinstance(value, expected_type):
error_msg = f"For function {func}, Argument '{name}' must be {expected_type.__name__}, got type {type(value).__name__}"
logger.error(error_msg)
return Result( return Result(
valid=False, valid=False,
error_type=ResultError.INVALID_INPUT, error_type=ResultError.INVALID_INPUT,
message=f"Argument '{name}' must be {expected_type.__name__}, got type {type(value).__name__}" message=error_msg
) )
# 3. If all checks pass, call the original function # 3. If all checks pass, call the original function