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:
parent
9e4fd54eb6
commit
69f4e7114f
5 changed files with 374 additions and 315 deletions
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
# if self.payment_type == PaymentType.CRYPTO:
|
||||||
else:
|
# result = check_paid(self.billing_code, self.connection_observer)
|
||||||
result = Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Invalid payment type chosen to check.")
|
# 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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.")
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue