Stable repeat Ticketing flow
This commit is contained in:
parent
1f4b37dbe9
commit
9ca0809ceb
5 changed files with 110 additions and 202 deletions
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
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,18 +207,27 @@ 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..")
|
||||
|
||||
if category == PaymentCategory.TICKET:
|
||||
payload = {
|
||||
"billing_code": billing_code
|
||||
"billing_code": billing_code,
|
||||
"category": "tickets"
|
||||
}
|
||||
else:
|
||||
payload = {
|
||||
"billing_code": billing_code,
|
||||
"category": "individual"
|
||||
}
|
||||
|
||||
#-----------------------------
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue