Big Bang Refactor of Payments. Introduced a new paymentdata singleton that stores data across pages and on disk. Also introduced a suite of functions for handling payments
This commit is contained in:
parent
57774ae697
commit
52b3e1ba94
6 changed files with 228 additions and 52 deletions
|
|
@ -1,13 +1,15 @@
|
|||
from core.services.payment_phase.payment_state import PaymentData
|
||||
from core.models.BaseProfile import BaseProfile as Profile
|
||||
from core.models.orm_models.Plan import Plan
|
||||
from core.models.orm_calls.model_neutral_tools import get_all_for_model
|
||||
from core.models.orm_calls.location_calls import get_profile_location_data
|
||||
from core.models.enum_models import ProtocolChoice
|
||||
from core.models.enum_models import ProtocolChoice, Currency
|
||||
from core.models.orm_models.Location import Location
|
||||
from core.errors.logger import logger
|
||||
from core.models.Result import Result, ResultError
|
||||
|
||||
ALL_PROTOCOLS = [e.value for e in ProtocolChoice]
|
||||
|
||||
ALL_CURRENCIES = [e.value for e in Currency]
|
||||
|
||||
"""
|
||||
Outputs:
|
||||
|
|
@ -57,16 +59,41 @@ def _get_supported_protocols_for_location(location: Location, as_strings: bool =
|
|||
return SUPPORTED_PROTOCOLS
|
||||
|
||||
|
||||
def get_all_plans(chosen_protocol: ProtocolChoice) -> Result:
|
||||
def get_plans_for_profile(profile_id: int) -> Result:
|
||||
"""
|
||||
Get a formatted list of all Plans for a given profile.
|
||||
And save the protocol in the PaymentData singleton.
|
||||
"""
|
||||
|
||||
# Get profile:
|
||||
profile = Profile.find_by_id(profile_id)
|
||||
if not profile:
|
||||
error_msg = f"Invalid profile id of {profile_id}"
|
||||
return Result(valid=False, message=error_msg, error_type=ResultError.INVALID_INPUT)
|
||||
|
||||
# Get Protocol for that profile:
|
||||
chosen_protocol = ProtocolChoice(profile.connection.code)
|
||||
|
||||
# create or get the singleton:
|
||||
billing_data = PaymentData(instance_id=profile_id)
|
||||
|
||||
# set it's value across modules:
|
||||
billing_data.protocol_choice = chosen_protocol
|
||||
|
||||
# Return plans for that protocol:
|
||||
return get_all_plans_by_protocol(chosen_protocol)
|
||||
|
||||
|
||||
def get_all_plans_by_protocol(chosen_protocol: ProtocolChoice) -> Result:
|
||||
"""Get a formatted list of all Plans for a given protocol."""
|
||||
|
||||
all_plans = get_all_for_model(model_type=Plan)
|
||||
|
||||
if not all_plans:
|
||||
error_msg = "There are no plans in your local SQL database. Please sync or make sure there's no SQL errors."
|
||||
return Result(valid=False, error_type=ResultError.SYNC, message=error_msg)
|
||||
return Result(valid=False, error_type=ResultError.MISSING_DATA, message=error_msg)
|
||||
|
||||
PLANS_TO_RETURN = []
|
||||
PLANS_TO_RETURN = {}
|
||||
|
||||
for each_plan in all_plans:
|
||||
|
||||
|
|
@ -74,12 +101,7 @@ def get_all_plans(chosen_protocol: ProtocolChoice) -> Result:
|
|||
if chosen_protocol not in each_plan.supported_protocols:
|
||||
continue
|
||||
|
||||
each_json = {
|
||||
'id': each_plan.id,
|
||||
'cost': each_plan.cost,
|
||||
'months': each_plan.months
|
||||
}
|
||||
PLANS_TO_RETURN.append(each_json)
|
||||
|
||||
plan_format = str(f"{each_plan.months} months (€{each_plan.cost})")
|
||||
PLANS_TO_RETURN[plan_format] = each_plan.id
|
||||
|
||||
return Result(valid=True, data=PLANS_TO_RETURN)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ import tempfile
|
|||
from sqlalchemy.orm import Session
|
||||
from enum import Enum
|
||||
|
||||
|
||||
from core.models.manage.session_management import init_session, get_session
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ class Subscription:
|
|||
exclude=lambda value: value is None
|
||||
)
|
||||
)
|
||||
billing_pending: bool = False
|
||||
|
||||
def get_sanitized_billing_code(self):
|
||||
return '-'.join(['****'] * 3 + [self.billing_code[-4:]])
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ def register_wireguard_session(
|
|||
# print(f"wireguard_configuration value is {wireguard_configuration}")
|
||||
|
||||
if isinstance(wireguard_configuration, dict):
|
||||
logger.error("Server isn't returning a WG config, it's an error.")
|
||||
logger.error("Server isn't returning a WG config, it's trying to tell you a ServerReply message...")
|
||||
return handle_config_error(wireguard_configuration)
|
||||
|
||||
if wireguard_configuration is None:
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ def singleton(cls):
|
|||
# for saving.
|
||||
EXCLUDED_FIELDS = {"parent_folder", "folder_path", "_data_file"}
|
||||
|
||||
|
||||
class StatePersistenceMixin(ValidatedDataclass):
|
||||
def _serialize_value(self, value):
|
||||
"""Helper method to ensure values are JSON-serializable"""
|
||||
|
|
@ -89,6 +90,7 @@ class StatePersistenceMixin(ValidatedDataclass):
|
|||
# Add it to the dictionary
|
||||
data[f.name] = clean_value
|
||||
|
||||
print(f"saving to {self._data_file}")
|
||||
with open(self._data_file, "w") as f:
|
||||
json.dump(data, f, indent=4, sort_keys=True)
|
||||
|
||||
|
|
@ -122,6 +124,10 @@ class StatePersistenceMixin(ValidatedDataclass):
|
|||
setattr(self, field.name, value)
|
||||
|
||||
|
||||
def exists_already(self) -> bool:
|
||||
return os.path.exists(self._data_file)
|
||||
|
||||
|
||||
|
||||
@singleton
|
||||
@dataclass
|
||||
|
|
@ -134,7 +140,7 @@ class TicketPaymentData(StatePersistenceMixin):
|
|||
_data_file: str = None
|
||||
|
||||
# actual payload data:
|
||||
billing_code: str = None
|
||||
# billing_code: str = None
|
||||
currency: Currency = None
|
||||
payment_type: PaymentType = PaymentType.CRYPTO
|
||||
|
||||
|
|
@ -153,7 +159,7 @@ class TicketPaymentData(StatePersistenceMixin):
|
|||
@singleton
|
||||
@dataclass
|
||||
class PaymentData(StatePersistenceMixin):
|
||||
PAYLOAD_FIELDS = {"billing_code", "currency", "plan_id", "location_id", "protocol_choice", "payment_type"}
|
||||
PAYLOAD_FIELDS = {"currency", "plan_id", "location_id", "protocol_choice", "payment_type"}
|
||||
|
||||
# metadata on it's own instance:
|
||||
parent_folder = None
|
||||
|
|
@ -162,11 +168,8 @@ class PaymentData(StatePersistenceMixin):
|
|||
_data_file: str = None
|
||||
|
||||
# actual payload data:
|
||||
billing_code: str = None
|
||||
currency: Currency = None
|
||||
payment_type: PaymentType = PaymentType.CRYPTO
|
||||
|
||||
# for traditional plans:
|
||||
plan_id: int = None
|
||||
location_id: int = None
|
||||
protocol_choice: ProtocolChoice = None
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
|
||||
from core.models.BaseProfile import BaseProfile as Profile
|
||||
from core.services.payment_phase.payment_state import PaymentData
|
||||
from core.services.networking.httpx import connect
|
||||
from core.models.enum_models import ProtocolChoice, Currency
|
||||
from core.models.enum_models import ProtocolChoice
|
||||
from core.models.Result import Result, ResultError
|
||||
from core.errors.logger import logger
|
||||
from core.observers.ConnectionObserver import ConnectionObserver
|
||||
from core.models.pydantic_models.type_checker import validate_model
|
||||
from core.models.pydantic_models.ServerReply import ServerReply, ServerError, SubscriptionInvoice
|
||||
from core.models.Subscription import Subscription
|
||||
from core.Constants import Constants
|
||||
|
||||
|
||||
|
|
@ -32,11 +34,10 @@ def check_paid(billing_code: str, connection_observer: ConnectionObserver) -> Re
|
|||
|
||||
# Layer 2: Server's Reply:
|
||||
raw_reply = api_response.data
|
||||
print(f"validating the server reply of {raw_reply}")
|
||||
server_reply = validate_model(ServerReply, raw_reply)
|
||||
|
||||
if not server_reply:
|
||||
print("invalid structure for ServerReply")
|
||||
logger.error(f"Invalid structure for ServerReply, which had a raw reply of {raw_reply}")
|
||||
return invalid_server_format()
|
||||
|
||||
# paid
|
||||
|
|
@ -51,68 +52,200 @@ def check_paid(billing_code: str, connection_observer: ConnectionObserver) -> Re
|
|||
return translate_server_failure(server_reply)
|
||||
|
||||
|
||||
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"
|
||||
}
|
||||
def create_invoice(profile_id: int, connection_observer: ConnectionObserver) -> Result:
|
||||
"""Prepare the payment data payload and send it to the API. Returns an invoice nested inside Result's data"""
|
||||
|
||||
# Get profile object:
|
||||
profile = Profile.find_by_id(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)
|
||||
|
||||
#-----------------------------
|
||||
# MAKE PAYLOAD. PIECE BY PIECE
|
||||
#-----------------------------
|
||||
# get the payment data singleton:
|
||||
billing_data = PaymentData(instance_id=profile_id)
|
||||
|
||||
# make sure the protocol is saved in the singleton already,
|
||||
if not billing_data.protocol_choice:
|
||||
# If not, then get the Protocol for that profile:
|
||||
billing_data.protocol_choice = ProtocolChoice(profile.connection.code)
|
||||
|
||||
# location
|
||||
billing_data.location_id = profile.location.id
|
||||
|
||||
# plan id
|
||||
if not billing_data.plan_id:
|
||||
error_msg = "Missing a plan id to make an invoice"
|
||||
logger.error(error_msg)
|
||||
return Result(valid=False, message=error_msg, error_type=ResultError.INVALID_INPUT)
|
||||
|
||||
# currency
|
||||
if not billing_data.currency:
|
||||
error_msg = "Missing a currency to make an invoice"
|
||||
logger.error(error_msg)
|
||||
return Result(valid=False, message=error_msg, error_type=ResultError.INVALID_INPUT)
|
||||
|
||||
# saves it to disk (organized by profile id) & returns a json:
|
||||
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.SP_BASE_URL}/v2/start_billing'
|
||||
print(f"url is {url}")
|
||||
|
||||
api_response = connect.single_endpoint(
|
||||
method="post",
|
||||
url=url,
|
||||
observer=connection_observer,
|
||||
payload=submit_data,
|
||||
payload=payload,
|
||||
)
|
||||
|
||||
print(f"api_response is {api_response.valid}")
|
||||
print(f"api_response is {api_response.error_type}")
|
||||
|
||||
#-----------------------------
|
||||
# EVALUATE CONNECTION FAILURE
|
||||
#-----------------------------
|
||||
# Layer 1: API failure:
|
||||
if not api_response.valid:
|
||||
error_msg = f"API Error: {api_response.error_type} {api_response.message}"
|
||||
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)
|
||||
|
||||
# Layer 2: Server's Reply:
|
||||
#-----------------------------
|
||||
# EVALUATE SERVER-SIDE ISSUES (assuming it replied)
|
||||
#-----------------------------
|
||||
# Layer 2: Validity of Server's Reply:
|
||||
raw_reply = api_response.data
|
||||
print(f"validating the server reply of {raw_reply}.")
|
||||
logger.info("Validating the Validity of the Server's Reply..")
|
||||
server_reply = validate_model(ServerReply, raw_reply)
|
||||
|
||||
if not server_reply:
|
||||
print("invalid structure for ServerReply")
|
||||
logger.error(f"Invalid Structure for ServerReply! The raw data is {raw_reply}")
|
||||
return invalid_server_format()
|
||||
|
||||
if not server_reply.valid:
|
||||
print("Server reply was correctly structured, but was a failure..")
|
||||
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
|
||||
#-----------------------------
|
||||
# Layer 3: Payload Data
|
||||
payload_data = server_reply.data
|
||||
print(f"validating the final invoice with this data: {payload_data}")
|
||||
logger.info("Validating the final invoice")
|
||||
final_invoice = validate_model(SubscriptionInvoice, payload_data)
|
||||
|
||||
print(f"final_invoice is {final_invoice}")
|
||||
print(f"final_invoice type is {type(final_invoice)}")
|
||||
|
||||
if not final_invoice:
|
||||
logger.error(f"Failed to create an invoice object with this data: {payload_data}")
|
||||
return invalid_server_format()
|
||||
|
||||
print("returning True with the data")
|
||||
return Result(valid=True, data=final_invoice)
|
||||
|
||||
|
||||
def get_profile_subscription(profile_id: int) -> Subscription:
|
||||
"""Lookup the full profile object & get it's subscription"""
|
||||
print(f"starting lookup of profile id {profile_id}")
|
||||
profile = Profile.find_by_id(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)
|
||||
|
||||
print(f"returning profile {profile}")
|
||||
return profile.subscription
|
||||
|
||||
|
||||
def save_billing_code(profile_id: int, billing_code: str, expires_at: str) -> Result:
|
||||
"""Checks if the billing code is already saved, and if not, saves it."""
|
||||
|
||||
logger.info("Saving billing code..")
|
||||
|
||||
PRE_REQS = [profile_id, billing_code, expires_at]
|
||||
if None in PRE_REQS:
|
||||
error_msg = "Missing critical required data to save!"
|
||||
logger.error(error_msg)
|
||||
return Result(valid=False, message=error_msg, error_type=ResultError.MISSING_DATA)
|
||||
|
||||
# Get profile object:
|
||||
profile = Profile.find_by_id(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)
|
||||
|
||||
try:
|
||||
profile.subscription = Subscription(
|
||||
billing_code=billing_code,
|
||||
expires_at=expires_at,
|
||||
billing_pending=True
|
||||
)
|
||||
profile.save()
|
||||
except Exception as e:
|
||||
error_msg = f"Legacy system wouldn't create or save Subscription object with {str(billing_code)} and {str(expires_at)} or type {type(expires_at)}, with error: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return Result(valid=False, message=error_msg, error_type=ResultError.INVALID_INPUT)
|
||||
|
||||
return Result(valid=True)
|
||||
|
||||
|
||||
# # 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.")
|
||||
|
||||
|
||||
def return_pre_existing_payment_data(profile_id: int, subscription: SubscriptionInvoice) -> Result:
|
||||
"""Return the pre-existing payment details (or lack of) for a profile"""
|
||||
|
||||
logger.info("Starting a fetch of pre-existing payment data")
|
||||
|
||||
# singleton:
|
||||
regular_billing = PaymentData(instance_id=profile_id)
|
||||
if not regular_billing.exists_already():
|
||||
error_msg = f"You have a billing id of {subscription.billing_code}, but no related payment data."
|
||||
logger.error(error_msg)
|
||||
return Result(valid=False, message=error_msg, error_type=ResultError.MISSING_DATA)
|
||||
|
||||
# dump data:
|
||||
logger.info("Payment data already exists for this profile.")
|
||||
payload = regular_billing.prep_payload
|
||||
|
||||
# add on billing code & expires
|
||||
if payload and isinstance(payload, dict):
|
||||
payload["billing_code"] = subscription.billing_code
|
||||
payload["expires_at"] = subscription.expires_at
|
||||
|
||||
# form an invoice with Profile data & PaymentData singleton for the gui
|
||||
subscription = SubscriptionInvoice(**payload)
|
||||
return Result(valid=True, data=subscription)
|
||||
else:
|
||||
error_msg = "Invalid format for the local PaymentData. It expected a dict and the payload produced to not be blank."
|
||||
logger.error(error_msg)
|
||||
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,
|
||||
|
|
@ -133,3 +266,21 @@ def translate_server_failure(server_reply: ServerReply) -> Result:
|
|||
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"
|
||||
# }
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue