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
logger.info("checking 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
enum_value = ENUM_MAP[result.error_type]

View file

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

View file

@ -112,23 +112,11 @@ class CheckPaidTimer:
print("PAID! Stopping timer")
self.stop_polling()
# Reset instance:
# Since paid, reset the instance (wipe it):
CheckPaidTimer._instance = None
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:
if not marked.valid:
message = marked.message
@ -173,3 +161,17 @@ class CheckPaidTimer:
CheckPaidTimer._instance = None
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.ServerReply import SubscriptionInvoice
from core.models.Subscription import Subscription
from core.utils.type_checker import type_checker
from core.Constants import Constants
from core.models.Result import Result, ResultError
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)
def evaluate_cycle(payment_cycle: OverallPaymentCycle, profile_id: int) -> Result:
@type_checker
def evaluate_repeat_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)
@ -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)
@type_checker
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:
# 1. Pre-requisites
if category == PaymentCategory.INDIVIDUAL and not profile_id:
error_msg = "Need a profile id for an individual profile."
logger.error(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()
logger.info(f"payment_cycle is {payment_cycle}")
# ----------------------------
# MISMATCH IN CATEGORY FROM WHAT EXISTS:
# ----------------------------
if category != payment_cycle.category:
return error_about_mismatch(category)
logger.info("Made it past the mismatch check.")
# 2. Exact Match Check
# 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:
is_same_category = (payment_cycle.category == category)
# ----------------------------
# 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)
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)
# ----------------------------
# ALREADY POLLING TIMER:
# ----------------------------
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)
# If we reached here, the active cycle (if any) is NOT a match.
if payment_cycle.active:
# We return ACTIVE_BILLING so the UI can prompt the user to call wipe_billing_cycle()
error_msg = f"An active {payment_cycle.category} cycle is already running. Stop it first?"
logger.info(error_msg)
return Result(valid=False, error_type=ResultError.ACTIVE_BILLING, message=error_msg)
# 4. Stale Timer Check
# This handles cases where the cycle state is gone but the background thread is still humming.
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?"
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)
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}")
logger.info("Process is NOT yet active, starting..")
payment_cycle.begin()
# 5. Initialization
payment_cycle.category = category
# ----------------------------
# START INDIVIDUAL:
# ----------------------------
if category == PaymentCategory.INDIVIDUAL:
payment_cycle.profile_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
if subscription:
billing_code = subscription.billing_code
else:
billing_code = None
logger.info("Individual profile initial setup complete.")
subscription = get_profile_subscription(profile_id=profile_id, save_pending=True)
billing_code = subscription.billing_code if subscription else None
logger.info("Individual setup complete.")
# ----------------------------
# START TICKETS:
# ----------------------------
elif category == PaymentCategory.TICKET:
billing_data = TicketPaymentData()
payment_cycle.billing_data = billing_data # saves
billing_code = billing_data.billing_code # might be empty if new
logger.info("Ticketing initial setup complete.")
payment_cycle.billing_data = billing_data
billing_code = billing_data.billing_code
logger.info("Ticket setup complete.")
else:
error_msg = f"Invalid PaymentCategory selected of {str(category)}"
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="Invalid Category")
# ----------------------------
# COMMON FOR BOTH CATEGORIES:
# ----------------------------
# 6. Activation
payment_cycle.billing_code = billing_code
payment_cycle.active = True
payment_cycle.begin()
logger.info(f"Returning the entire payment_cycle")
return Result(valid=True, data=payment_cycle)
if payment_cycle.billing_data:
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.")
return Result(valid=False, error_type=ResultError.UNKNOWN, message="Failed to create billing data.")
@type_checker
def create_new_invoice(connection_observer: ConnectionObserver) -> Result:
"""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)
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:
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."""
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
@ -253,9 +239,17 @@ def return_pre_existing_invoice(billing_code: str, connection_observer: Connecti
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
if not billing_data:
@ -266,7 +260,7 @@ def create_ticket_invoice(payment_cycle: OverallPaymentCycle, connection_observe
billing_code = billing_data.billing_code
if billing_code:
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.
@ -275,6 +269,7 @@ def create_ticket_invoice(payment_cycle: OverallPaymentCycle, connection_observe
# which_key = billing_data.which_key
# currency = billing_data.currency
logger.info(f"Evaluating if billing_data is ready to prep a payload: {billing_data}")
payload = billing_data.prep_payload()
if not payload:
@ -305,6 +300,7 @@ def create_ticket_invoice(payment_cycle: OverallPaymentCycle, connection_observe
return Result(valid=True, data=final_invoice)
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"""
@ -390,54 +386,6 @@ def create_individual_invoice(payment_cycle: OverallPaymentCycle, connection_obs
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:
"""Returns boolean for if the current billing is tickets"""
@ -491,8 +439,11 @@ def wipe_billing_cycle() -> Result:
timer_loop = CheckPaidTimer.get_instance()
if timer_loop.is_running:
timer_loop.stop_polling()
logger.info("Wiped the CheckPaidTimer")
else:
logger.error("Timer wasn't running to begin with.")
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:
payment_cycle = OverallPaymentCycle()
@ -503,6 +454,7 @@ def wipe_billing_cycle() -> Result:
return Result(valid=True)
@type_checker
def get_profile_subscription(profile_id: int, save_pending: Optional[bool] = False) -> Subscription:
"""
Main Purpose:
@ -543,61 +495,7 @@ def wipe_subscription(profile_id: int) -> Result:
return Result(valid=True)
# def mark_as_paid() -> Result:
# """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)
@type_checker
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."""
@ -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:
@type_checker
def save_subscription(profile: Profile, subscription: Subscription) -> bool:
"""Saves subscription with the code and date."""
@ -643,8 +542,9 @@ def save_subscription(profile: Profile, subscription: Subscription) -> bool:
return True
@type_checker
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")

View file

@ -1,4 +1,6 @@
from core.models.Result import Result
from core.errors.logger import logger
import inspect
from functools import wraps
@ -20,7 +22,9 @@ def type_checker(func):
bound_args.apply_defaults()
except TypeError as e:
# 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
for name, value in bound_args.arguments.items():
@ -31,10 +35,12 @@ def type_checker(func):
continue
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(
valid=False,
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