Refactored billing to have core doing more, and GUI doing less. Also heavily relying upon the singleton pattern to transfer data between UI pages and core functions

This commit is contained in:
SimplifiedPrivacy 2026-10-12 20:24:04 -04:00
parent 9e4fd54eb6
commit 69f4e7114f
5 changed files with 374 additions and 315 deletions

View file

@ -41,6 +41,7 @@ class ResultError(Enum):
SERVER_SIDE = "server_side" SERVER_SIDE = "server_side"
UNPAID = "unpaid" UNPAID = "unpaid"
MTU = "mtu" MTU = "mtu"
ACTIVE_BILLING = "active_billing"
NAVIGATE_TO = "navigate_to" NAVIGATE_TO = "navigate_to"
UNKNOWN = "unknown" UNKNOWN = "unknown"

View file

@ -2,7 +2,7 @@ from core.Errors import InvalidSubscriptionError
from core.models.manage.validate_dataclass import validate_model from core.models.manage.validate_dataclass import validate_model
from core.models.pydantic_models.ServerReply import ServerReply, ServerError from core.models.pydantic_models.ServerReply import ServerReply, ServerError
from core.models.Result import Result, ResultError from core.models.Result import Result, ResultError
from core.services.networking.api_requests.ApiResponseModel import ApiResponse from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.observers.ConnectionObserver import ConnectionObserver from core.observers.ConnectionObserver import ConnectionObserver
from core.services.networking.httpx import connect from core.services.networking.httpx import connect
from core.errors.logger import logger from core.errors.logger import logger
@ -121,7 +121,8 @@ def api_call_and_extraction(
if not api_response.valid: if not api_response.valid:
# legacy system # legacy system
if api_result.error_type == ErrorType.AUTHENTICATION_ERROR: if api_response.error_type == ErrorType.AUTHENTICATION_ERROR:
logger.error(f"api_response is {api_response}")
logger.error(f"Server is giving an Authentication error for billing ID: {billing_code}") logger.error(f"Server is giving an Authentication error for billing ID: {billing_code}")
raise InvalidSubscriptionError(f"Invalid Subscription for {billing_code}") raise InvalidSubscriptionError(f"Invalid Subscription for {billing_code}")

View file

@ -1,6 +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, mark_global_tickets_paid from core.services.subscriptions.subscriptions_by_protocol 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
@ -91,12 +90,15 @@ class CheckPaidTimer:
# in the background thread: # in the background thread:
print("checking if paid..") print("checking if paid..")
if self.payment_type == PaymentType.CRYPTO:
result = check_paid(self.billing_code, self.connection_observer) result = check_paid(self.billing_code, self.connection_observer)
elif self.payment_type == PaymentType.TICKET:
result = check_if_ticket_paid(self.billing_code, self.connection_observer)
else: # if self.payment_type == PaymentType.CRYPTO:
result = Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Invalid payment type chosen to check.") # result = check_paid(self.billing_code, self.connection_observer)
# elif self.payment_type == PaymentType.TICKET:
# result = check_if_ticket_paid(self.billing_code, self.connection_observer)
# else:
# result = Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Invalid payment type chosen to check.")
print("checked") print("checked")
# Bring result to main thread: # Bring result to main thread:
@ -132,7 +134,7 @@ class CheckPaidTimer:
message = "Paid! This invoice is paid" message = "Paid! This invoice is paid"
# UPDATE UI: # UPDATE UI:
self.view_callback(paid=True, stopped=True, message=message) self.view_callback(paid=True, stopped=True, message=message, billing_code=self.billing_code)
# not yet paid: # not yet paid:
elif result.error_type == ResultError.UNPAID: elif result.error_type == ResultError.UNPAID:

View file

@ -153,6 +153,7 @@ class TicketPaymentData(StatePersistenceMixin):
use_random: bool = False use_random: bool = False
temp_billing_code: Optional[str] = None temp_billing_code: Optional[str] = None
billing_pending: bool = True billing_pending: bool = True
paid: bool = False
def __post_init__(self): def __post_init__(self):
# Global mode, assuming tickets here: # Global mode, assuming tickets here:
@ -196,23 +197,28 @@ class PaymentData(StatePersistenceMixin):
@singleton @singleton
@dataclass @dataclass
class OverallPaymentCycle(StatePersistenceMixin): class OverallPaymentCycle():
"""For ALL payment states, this just keeps track of which one.""" """For ALL payment states, this just keeps track of which one is active."""
PAYLOAD_FIELDS = {}
# metadata on it's own GLOBAL instance:
parent_folder = None
folder_path: str = None
_data_file: str = None
category: PaymentCategory = PaymentCategory.INDIVIDUAL category: PaymentCategory = PaymentCategory.INDIVIDUAL
already_polling: bool = False already_polling: bool = False
profile_id: int = None profile_id: int = None
instance: Any = None active: bool = False
active_data: Any = None
got_invoice: bool = False
billing_code: str = None
def begin(self):
if not self.active:
self.active = True
def wipe(self): def wipe(self):
"""Wipe all data and start over.""" """Wipe all data and start over."""
self.active = False
self.active_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
self.instance = None self.got_invoice = None
self.billing_code = None

View file

@ -1,209 +1,57 @@
from core.models.manage.server_reply_tools import api_call_and_extraction, ApiCall
from core.models.BaseProfile import BaseProfile as Profile from core.models.BaseProfile import BaseProfile as Profile
from core.services.payment_phase.payment_state import OverallPaymentCycle, PaymentData, TicketPaymentData from core.services.payment_phase.payment_state import OverallPaymentCycle, PaymentData, TicketPaymentData
from core.services.networking.httpx import connect
from core.models.enum_models import ProtocolChoice, PaymentCategory from core.models.enum_models import ProtocolChoice, PaymentCategory
from core.models.Result import Result, ResultError
from core.models.manage.server_reply_tools import translate_server_failure, invalid_server_format
from core.errors.logger import logger
from core.observers.ConnectionObserver import ConnectionObserver 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 ServerReply, SubscriptionInvoice from core.models.pydantic_models.ServerReply import SubscriptionInvoice
from core.models.Subscription import Subscription 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.errors.logger import logger
# generic
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
HOW_MANY_PROFILES_DEFAULT = 6 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 paying_for_tickets() -> bool: def invalid_server_format() -> Result:
"""Returns boolean for if the current billing is tickets""" error_msg = "The server gave an invalid response format."
return Result(valid=False, error_type=ResultError.INVALID_API_REPLY, message=error_msg)
payment_cycle = OverallPaymentCycle()
if not payment_cycle:
return False
category = payment_cycle.category
if category == PaymentCategory.TICKET:
return True
else:
return False
def get_current_billing_data() -> Result: def _api_call_for_invoice(url: str, payload: dict, connection_observer: ConnectionObserver) -> Result:
""" """
Goal: Cross-Category Helper function.
Get whatever current billing data is relevant by the overall cycle, for either billing category
Note: 1) Does API call
The reason it's broken into a cycle and the actual data, is to keep track of if it's individual or tickets. 2) Filters out bad server replies
And to make sure that only one set of data is polling at once. 3) Validates the SubscriptionInvoice object
4) Returns SubscriptionInvoice wrapped in a Result object.
""" """
# Is there currently a process?
payment_cycle = OverallPaymentCycle()
if not payment_cycle: logger.info(f"Doing the API call with this payload: {payload}")
error_msg = "There is no current billing cycle."
return Result(valid=False, error_type=ResultError.INVALID, message=error_msg)
billing_data = payment_cycle.instance result = api_call_and_extraction(
desired_type="data",
if not billing_data: method=ApiCall.POST,
error_msg = "There is no current billing data."
return Result(valid=False, error_type=ResultError.INVALID, message=error_msg)
return Result(valid=True, data=billing_data)
def wipe_billing_cycle():
"""Wipes the entire overall cycle"""
logger.info("Wiping the overall cycle, so this cycle no longer blocks other new ones")
payment_cycle = OverallPaymentCycle()
if payment_cycle:
payment_cycle.wipe()
def start_billing_state(category: PaymentCategory, profile_id: Optional[int] = None, wipe_existing: Optional[bool] = False) -> Result:
"""Start a new billing process. Wipe the existing one. And never save these 'overall' states to disk."""
if not category:
error_msg = "Must pick a payment category to start a billing cycle."
return Result(valid=False, error_type=ResultError.INVALID, message=error_msg)
if category == PaymentCategory.INDIVIDUAL and not profile_id:
error_msg = "Starting an Individual Profile Billing requires a profile ID."
return Result(valid=False, error_type=ResultError.INVALID, message=error_msg)
# Is there currently a process?
payment_cycle = OverallPaymentCycle()
# WIPE IF REQUESTED:
if wipe_existing and payment_cycle:
payment_cycle.wipe()
if payment_cycle and payment_cycle.already_polling:
error_msg = "Please stop the payment checking already going on before starting another."
return Result(valid=False, error_type=ResultError.INVALID, message=error_msg)
if payment_cycle:
logger.info("Wiping the current payment state already saved.")
payment_cycle.category = category
if category == PaymentCategory.INDIVIDUAL:
payment_cycle.profile_id = profile_id
billing_data = PaymentData(instance_id=profile_id)
payment_cycle.instance = billing_data
return Result(valid=True, data=billing_data)
elif category == PaymentCategory.TICKET:
billing_data = TicketPaymentData()
# billing_data = TicketPaymentData(
# which_key=None,
# how_many_profiles=HOW_MANY_PROFILES_DEFAULT,
# payment_type=None
# )
payment_cycle.instance = billing_data
return Result(valid=True, data=billing_data)
else:
error_msg = "This is a currently unsupported payment category."
return Result(valid=False, error_type=ResultError.INVALID, message=error_msg)
def check_paid(billing_code: str, connection_observer: ConnectionObserver) -> Result:
"""Evaluates Payment Status from the Server"""
submit_data = {
"billing_code": billing_code
}
url = f'{Constants.SP_BASE_URL}/v2/check_paid'
api_response = connect.single_endpoint(
method="post",
url=url, url=url,
observer=connection_observer, connection_observer=connection_observer,
payload=submit_data, payload=payload
) )
# Layer 1: API failure: if not result.valid:
if not api_response.valid: return result
error_msg = f"API Error: {api_response.error_type} {api_response.message}"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.CONNECTION, message=error_msg)
# Layer 2: Server's Reply:
raw_reply = api_response.data
server_reply = validate_model(ServerReply, raw_reply)
if not server_reply:
logger.error(f"Invalid structure for ServerReply, which had a raw reply of {raw_reply}")
return invalid_server_format()
# paid
if server_reply.payment_status:
return Result(valid=True, message="paid")
# NOT paid (but valid server reply),
if server_reply.valid and not server_reply.payment_status:
return Result(valid=False, error_type=ResultError.UNPAID, message="The API call went through, but this subscription is not yet paid.")
# invalid server reply,
return translate_server_failure(server_reply)
def return_pre_existing_invoice(billing_code: str, connection_observer: ConnectionObserver) -> Result:
"""This billing_code already got an invoice, but needs to fetch that data from the server again."""
payload = {
"billing_code": billing_code
}
#-----------------------------
# SEND TO API
#-----------------------------
url = f'{Constants.SP_BASE_URL}/v2/get_invoice'
api_response = connect.single_endpoint(
method="post",
url=url,
observer=connection_observer,
payload=payload,
)
#-----------------------------
# EVALUATE CONNECTION FAILURE
#-----------------------------
# Layer 1: API failure:
if not api_response.valid:
error_msg = f"Connection/500s Error prevented the server from replying: {api_response.error_type} {api_response.message}"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.CONNECTION, message=error_msg)
#-----------------------------
# EVALUATE SERVER-SIDE ISSUES (assuming it replied)
#-----------------------------
# Layer 2: Validity of Server's Reply:
raw_reply = api_response.data
logger.info("Validating the Validity of the Server's Reply..")
server_reply = validate_model(ServerReply, raw_reply)
if not server_reply:
logger.error(f"Invalid Structure for ServerReply! The raw data is {raw_reply}")
return invalid_server_format()
if not server_reply.valid:
logger.error(f"Server reply was correctly structured, but the actual invoice failed to be created. The raw data is {raw_reply}")
# translate by enum error code:
return translate_server_failure(server_reply)
#----------------------------- #-----------------------------
# EVALUATE SERVER'S ACTUAL DATA # EVALUATE SERVER'S ACTUAL DATA
#----------------------------- #-----------------------------
# Layer 3: Payload Data payload_data = result.data
payload_data = server_reply.data
logger.info("Validating the final invoice") logger.info("Validating the final invoice")
final_invoice = validate_model(SubscriptionInvoice, payload_data) final_invoice = validate_model(SubscriptionInvoice, payload_data)
@ -214,9 +62,169 @@ def return_pre_existing_invoice(billing_code: str, connection_observer: Connecti
return Result(valid=True, data=final_invoice) return Result(valid=True, data=final_invoice)
def create_individual_invoice(profile_id: int, connection_observer: ConnectionObserver) -> Result: def the_same_payment_cycle(payment_cycle: OverallPaymentCycle, profile_id: Optional[int] = None) -> bool:
"""Evaluate if its the same cycle."""
if profile_id is None:
return False
if profile_id == payment_cycle.profile_id:
return True
else:
return False
def start_new_payment_cycle(category: PaymentCategory, profile_id: Optional[int] = None) -> Result:
if not profile_id and category == PaymentCategory.INDIVIDUAL:
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)
# Is there currently a core process?
payment_cycle = OverallPaymentCycle()
if payment_cycle.active:
if the_same_payment_cycle(payment_cycle=payment_cycle, profile_id=profile_id):
logger.info("It's the same as before, returning the code")
return Result(valid=True, data=payment_cycle.billing_code)
else:
error_msg = "There's already a billing process active, do you want to stop that?"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.ACTIVE_BILLING, message=error_msg)
logger.info("Process is NOT yet active, starting..")
payment_cycle.begin()
payment_cycle.category = category
if category == PaymentCategory.INDIVIDUAL:
payment_cycle.profile_id = profile_id
payment_cycle.active_data = PaymentData(instance_id=profile_id)
subscription = get_profile_subscription(profile_id)
if subscription:
billing_code = subscription.billing_code
else:
billing_code = None
elif category == PaymentCategory.TICKET:
billing_data = TicketPaymentData()
payment_cycle.active_data = billing_data # saves
billing_code = billing_data.temp_billing_code
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)
# save in cache:
payment_cycle.billing_code = billing_code
payment_cycle.active = True
return Result(valid=True, data=billing_code)
def create_new_invoice(connection_observer: ConnectionObserver) -> Result:
"""Requires an active OverallPaymentCycle, that does NOT have an invoice already."""
payment_cycle = OverallPaymentCycle()
if not payment_cycle.active:
error_msg = "Process is not yet active"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
if payment_cycle.got_invoice:
error_msg = "Process already got an invoice"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
category = payment_cycle.category
if category == PaymentCategory.INDIVIDUAL:
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)
else:
return not_supported()
def return_pre_existing_invoice(billing_code: str, connection_observer: ConnectionObserver) -> 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
}
#-----------------------------
# SEND TO API
#-----------------------------
url = f'{Constants.SP_BASE_URL}/v2/get_invoice'
result = _api_call_for_invoice(url=url, payload=payload, connection_observer=connection_observer)
return result
def create_ticket_invoice(payment_cycle: OverallPaymentCycle, connection_observer: ConnectionObserver) -> Result:
logger.info("Creating a new TICKET invoice..")
billing_data = payment_cycle.active_data
if not billing_data:
error_msg = "You are currently missing billing data to begin an invoice"
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
# already has billing id:
billing_code = billing_data.temp_billing_code
if billing_code:
logger.info("We already have a temp_billing_code id for this ticket prep.")
return return_pre_existing_invoice(billing_code=billing_code, connection_observer=connection_observer)
#-----------------------------
# MAKE PAYLOAD.
#-----------------------------
# how_many_profiles = billing_data.how_many_profiles # default is always there
# which_key = billing_data.which_key
# currency = billing_data.currency
payload = billing_data.prep_payload()
if not payload:
error_msg = "Unable to save the payload, or missing fields. Check the error log."
logger.error(error_msg)
return Result(valid=False, message=error_msg, error_type=ResultError.FILE_SYSTEM)
#-----------------------------
# SEND TO API
#-----------------------------
url = f'{Constants.TICKET_API_BASE_URL}/start_payment'
result = _api_call_for_invoice(url=url, payload=payload, connection_observer=connection_observer)
if not result.valid:
return result
final_invoice = result.data
#-----------------------------
# SAVE DATA
#-----------------------------
billing_code = final_invoice.billing_code
billing_data.temp_billing_code = billing_code
billing_data.save()
payment_cycle.got_invoice = True
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""" """Prepare the payment data payload and send it to the API. Returns an invoice nested inside Result's data"""
profile_id = payment_cycle.profile_id
logger.info(f"creating an individual invoice for profile {profile_id}")
# Get profile object: # Get profile object:
profile = Profile.find_by_id(profile_id) profile = Profile.find_by_id(profile_id)
if not profile: if not profile:
@ -235,7 +243,7 @@ def create_individual_invoice(profile_id: int, connection_observer: ConnectionOb
# MAKE PAYLOAD. PIECE BY PIECE # MAKE PAYLOAD. PIECE BY PIECE
#----------------------------- #-----------------------------
# get the payment data singleton: # get the payment data singleton:
billing_data = PaymentData(instance_id=profile_id) billing_data = payment_cycle.active_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:
@ -270,52 +278,128 @@ def create_individual_invoice(profile_id: int, connection_observer: ConnectionOb
#----------------------------- #-----------------------------
url = f'{Constants.SP_BASE_URL}/v2/start_billing' url = f'{Constants.SP_BASE_URL}/v2/start_billing'
api_response = connect.single_endpoint( result = _api_call_for_invoice(url=url, payload=payload, connection_observer=connection_observer)
method="post", if not result.valid:
return result
final_invoice = result.data
#-----------------------------
# SAVE DATA
#-----------------------------
billing_code = final_invoice.billing_code
expires_at = final_invoice.expires_at
# overall payment cycle:
payment_cycle.billing_code = billing_code
# individual profile:
profile.subscription = Subscription(
billing_code=billing_code,
expires_at=expires_at,
billing_pending=True
)
profile.save()
payment_cycle.got_invoice = True
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, url=url,
observer=connection_observer, connection_observer=connection_observer,
payload=payload, payload=submit_data
) )
#----------------------------- if not result.valid:
# EVALUATE CONNECTION FAILURE return result
#-----------------------------
# Layer 1: API failure:
if not api_response.valid:
error_msg = f"Connection/500s Error prevented the server from replying: {api_response.error_type} {api_response.message}"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.CONNECTION, message=error_msg)
#----------------------------- payment_status = result.payment_status
# EVALUATE SERVER-SIDE ISSUES (assuming it replied)
#-----------------------------
# Layer 2: Validity of Server's Reply:
raw_reply = api_response.data
logger.info("Validating the Validity of the Server's Reply..")
server_reply = validate_model(ServerReply, raw_reply)
if not server_reply: # paid
logger.error(f"Invalid Structure for ServerReply! The raw data is {raw_reply}") if payment_status:
return invalid_server_format() 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.")
if not server_reply.valid:
logger.error(f"Server reply was correctly structured, but the actual invoice failed to be created. The raw data is {raw_reply}")
# translate by enum error code:
return translate_server_failure(server_reply)
#----------------------------- def paying_for_tickets() -> bool:
# EVALUATE SERVER'S ACTUAL DATA """Returns boolean for if the current billing is tickets"""
#-----------------------------
# Layer 3: Payload Data
payload_data = server_reply.data
logger.info("Validating the final invoice")
final_invoice = validate_model(SubscriptionInvoice, payload_data)
if not final_invoice: payment_cycle = OverallPaymentCycle()
logger.error(f"Failed to create an invoice object with this data: {payload_data}")
return invalid_server_format() if not payment_cycle:
return False
category = payment_cycle.category
if category == PaymentCategory.TICKET:
return True
else:
return False
def get_current_billing_data() -> Result:
"""
Goal:
Get whatever current billing data is relevant by the overall cycle, for either billing category
Note:
The reason it's broken into a cycle and the actual data, is to keep track of if it's individual or tickets.
And to make sure that only one set of data is polling at once.
"""
# Is there currently a process?
payment_cycle = OverallPaymentCycle()
if not payment_cycle:
error_msg = "There is no current billing cycle."
return Result(valid=False, error_type=ResultError.INVALID, message=error_msg)
billing_data = payment_cycle.active_data
if not billing_data:
error_msg = "There is no current billing data."
return Result(valid=False, error_type=ResultError.INVALID, message=error_msg)
return Result(valid=True, data=billing_data)
def wipe_billing_cycle():
"""Wipes the entire overall cycle"""
logger.info("Wiping the overall cycle, so this cycle no longer blocks other new ones")
payment_cycle = OverallPaymentCycle()
if payment_cycle:
payment_cycle.wipe()
return Result(valid=True, data=final_invoice)
def get_profile_subscription(profile_id: int) -> Subscription: def get_profile_subscription(profile_id: int) -> Subscription:
@ -331,23 +415,50 @@ def get_profile_subscription(profile_id: int) -> Subscription:
return profile.subscription return profile.subscription
def mark_global_tickets_paid() -> Result: def mark_as_paid() -> Result:
"""Conclude the payment phase, by marking the global tickets as paid""" """Conclude the payment phase"""
try:
ticket_payments = TicketPaymentData()
ticket_payments.billing_pending = False
ticket_payments.save()
return Result(valid=True)
except Exception as e:
return Result(valid=False, error_type=ResultError.UNKNOWN, message=str(e))
def mark_as_paid(profile_id: int) -> Result:
"""Conclude the payment phase, by marking an individual subscription as paid"""
logger.info("Marking as paid..") logger.info("Marking as paid..")
# Get profile object: payment_cycle = OverallPaymentCycle()
if not payment_cycle:
error_msg = "There is no current billing cycle."
return Result(valid=False, error_type=ResultError.INVALID, 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, 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 = Profile.find_by_id(profile_id) profile = Profile.find_by_id(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}"
@ -433,65 +544,3 @@ def return_pre_existing_payment_data(profile_id: int, subscription: Subscription
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)
# # Translate Server-side to client-side errors in THIS CONTEXT,
# TRANSLATION_DICT = {
# ServerError.WRONG_LOCATION: ResultError.WRONG_LOCATION,
# ServerError.NODE_ERROR: ResultError.WRONG_LOCATION,
# ServerError.BTCPAY: ResultError.SERVER_SIDE,
# ServerError.INVALID: ResultError.INVALID_INPUT,
# ServerError.MISSING_DATA: ResultError.INVALID_INPUT,
# ServerError.UNPAID: ResultError.UNPAID,
# ServerError.EXPIRED: ResultError.EXPIRED
# }
# def translate_server_failure(server_reply: ServerReply) -> Result:
# """Translate the server's failure to the UI"""
# logger.error(f"Server-side failure occured. The server claims the reason is of type {server_reply.error_type} with {server_reply.message}")
# client_error = TRANSLATION_DICT[server_reply.error_type]
# return Result(valid=False, error_type=client_error, message=server_reply.message)
# def invalid_server_format() -> Result:
# error_msg = "The server gave an invalid response format."
# return Result(valid=False, error_type=ResultError.INVALID_API_REPLY, message=error_msg)
# def create_invoice(
# subscription_plan_id: int,
# location_id: int,
# protocol_choice: ProtocolChoice,
# currency: Currency,
# connection_observer: ConnectionObserver
# ) -> Result:
# submit_data = {
# "subscription_plan_id": subscription_plan_id,
# "location_id": location_id,
# "protocol_choice": protocol_choice.value,
# "currency": currency.value,
# "payment_type": "crypto"
# }
# Was cut from the end of save a billing code:
# # get the payment data singleton:
# billing_data = PaymentData(instance_id=profile_id)
# # new
# if billing_data.billing_code is None:
# billing_data.billing_code = billing_code
# billing_data.save()
# logger.info("saved billing_code")
# return Result(valid=True)
# # Already exists and is different:
# if regular_billing.billing_code != billing_code:
# error_msg = "Critical Error! Billing codes do NOT match"
# logger.error(error_msg)
# return Result(valid=False, error_type=ResultError.UNKNOWN, message=error_msg)
# # already saved:
# return Result(valid=True, message="Already saved.")