397 lines
16 KiB
Python
397 lines
16 KiB
Python
import binascii
|
|
import json
|
|
|
|
import requests
|
|
from decouple import config, Csv
|
|
from datetime import datetime
|
|
from flask import Flask, request
|
|
from flask_restful import Resource, Api
|
|
from pyotp import TOTP
|
|
|
|
from config import etcd_client as client, logging, APP_PORT
|
|
from stripe_utils import StripeUtils
|
|
from uuid import uuid4
|
|
import time
|
|
|
|
app = Flask(__name__)
|
|
api = Api(app)
|
|
|
|
|
|
def check_otp(name, realm, token):
|
|
try:
|
|
data = {
|
|
"auth_name": config("AUTH_NAME", ""),
|
|
"auth_token": TOTP(config("AUTH_SEED", "")).now(),
|
|
"auth_realm": config("AUTH_REALM", ""),
|
|
"name": name,
|
|
"realm": realm,
|
|
"token": token,
|
|
}
|
|
except binascii.Error:
|
|
return 400
|
|
|
|
response = requests.post(
|
|
"{OTP_SERVER}{OTP_VERIFY_ENDPOINT}".format(
|
|
OTP_SERVER=config("OTP_SERVER", ""),
|
|
OTP_VERIFY_ENDPOINT=config("OTP_VERIFY_ENDPOINT", "verify/"),
|
|
),
|
|
data=data,
|
|
)
|
|
return response.status_code
|
|
|
|
def get_plan_id_from_product(product):
|
|
plan_id = "ucloud-v1-"
|
|
plan_id += product["name"].strip().replace(' ', '-') + "-"
|
|
plan_id += product["type"]
|
|
return plan_id
|
|
|
|
def get_order_id():
|
|
order_id_kv = client.get("/v1/last_order_id")
|
|
if order_id_kv is not None:
|
|
order_id = int(order_id_kv.value) + 1
|
|
else:
|
|
order_id = config("INIT_ORDER_ID")
|
|
client.put("/v1/last_order_id", str(order_id))
|
|
return "OR-{}".format(order_id)
|
|
|
|
def get_pricing(price_in_chf_cents, product_type, recurring_period):
|
|
if product_type == "recurring":
|
|
return "CHF {}/ {}".format(
|
|
price_in_chf_cents/100,
|
|
recurring_period
|
|
)
|
|
elif product_type == "one-time":
|
|
return "CHF {}".format(price_in_chf_cents/100)
|
|
|
|
def get_user_friendly_product(product_dict):
|
|
return {
|
|
"name": product_dict["name"],
|
|
"description": product_dict["description"],
|
|
"product_id": product_dict["product_id"],
|
|
"pricing": get_pricing(product_dict["price"], product_dict["type"],
|
|
product_dict["recurring_period"]),
|
|
"minimum_subscription_period":
|
|
product_dict["minimum_subscription_period"]
|
|
}
|
|
|
|
|
|
|
|
class ListProducts(Resource):
|
|
@staticmethod
|
|
def get():
|
|
products = client.get_prefix("/v1/products/", value_in_json=False)
|
|
prod_dict = {}
|
|
for p in products:
|
|
p_dict = json.loads(p.value)
|
|
uf_product = get_user_friendly_product(p_dict)
|
|
prod_dict[uf_product["product_id"]] = uf_product
|
|
logging.debug("Products = {}".format(prod_dict))
|
|
return prod_dict, 200
|
|
|
|
|
|
class AddProduct(Resource):
|
|
@staticmethod
|
|
def post():
|
|
data = request.json
|
|
logging.debug("Got data: {}".format(str(data)))
|
|
REALM_ALLOWED = config("REALM_ALLOWED", cast=Csv(str))
|
|
logging.debug("REALM_ALLOWED = {}".format(REALM_ALLOWED))
|
|
if data["realm"] not in REALM_ALLOWED:
|
|
logging.error(
|
|
"The given realm {} is not "
|
|
"allowed to do add product".format(data["realm"]))
|
|
return {"message": "Forbidden"}, 403
|
|
otp_response = check_otp(data["name"], data["realm"],
|
|
data["token"])
|
|
if otp_response != 200:
|
|
return {"message": "Wrong Credentials"}, 403
|
|
|
|
try:
|
|
product_uuid = uuid4().hex
|
|
product_key = "/v1/products/{}".format(product_uuid)
|
|
product_value = {
|
|
"product_id": product_uuid,
|
|
"name": data["product_name"],
|
|
"description": data["product_description"],
|
|
"type": data["product_type"],
|
|
"price": data["product_price"],
|
|
"recurring_period": data["product_recurring_period"],
|
|
"minimum_subscription_period":
|
|
data["product_minimum_subscription_period"] if
|
|
data["product_type"] == "recurring" else 0,
|
|
"created_at": int(time.time()),
|
|
"created_by": data["name"]
|
|
}
|
|
logging.debug("Adding product data: {}".format(str(product_value)))
|
|
client.put(product_key, product_value, value_in_json=True)
|
|
return {"message":
|
|
"Product {} created. Product ID = {}".format(
|
|
data['product_name'], product_uuid
|
|
)}, 200
|
|
except KeyError as ke:
|
|
logging.error("KeyError occurred. details = {}".format(str(ke)))
|
|
return {"message":
|
|
"Missing or wrong parameters"}, 400
|
|
|
|
|
|
class UserRegisterPayment(Resource):
|
|
|
|
@staticmethod
|
|
def get_token(card_number, cvc, exp_month, exp_year):
|
|
stripe_utils = StripeUtils()
|
|
token_response = stripe_utils.get_token_from_card(
|
|
card_number, cvc, exp_month, exp_year
|
|
)
|
|
if token_response["response_object"]:
|
|
return token_response["response_object"].id
|
|
else:
|
|
return None
|
|
|
|
@staticmethod
|
|
def post():
|
|
try:
|
|
data = request.json
|
|
logging.debug("Got data: {}".format(str(data)))
|
|
otp_response = check_otp(data["name"], data["realm"],
|
|
data["token"])
|
|
last4 = data['card_number'].strip()[-4:]
|
|
if otp_response != 200:
|
|
return {"message": "Wrong Credentials"}, 403
|
|
|
|
stripe_utils = StripeUtils()
|
|
|
|
# Does customer already exist ?
|
|
stripe_customer = stripe_utils.get_stripe_customer_from_email(
|
|
data["name"])
|
|
|
|
# Does customer already exist ?
|
|
if stripe_customer is not None:
|
|
logging.debug(
|
|
"Customer {} exists already".format(data['name'])
|
|
)
|
|
# Check if the card already exists
|
|
ce_response = stripe_utils.card_exists(
|
|
stripe_customer.id, cc_number=data["card_number"],
|
|
exp_month=int(data["expiry_month"]),
|
|
exp_year=int(data["expiry_year"]),
|
|
cvc=data["cvc"])
|
|
if ce_response["response_object"]:
|
|
message = ("The given card ending in "
|
|
"{} exists already.").format(last4)
|
|
logging.debug(message)
|
|
return { "message": message }, 400
|
|
elif ce_response["response_object"] is False:
|
|
# Associate card with user
|
|
logging.debug("Adding card ending in {}".format(last4))
|
|
token_response = stripe_utils.get_token_from_card(
|
|
data["card_number"], data["cvc"], data["expiry_month"],
|
|
data["expiry_year"]
|
|
)
|
|
if token_response["response_object"]:
|
|
logging.debug(
|
|
"Token {}".format(
|
|
token_response["response_object"].id
|
|
)
|
|
)
|
|
resp = stripe_utils.associate_customer_card(
|
|
stripe_customer.id,
|
|
token_response["response_object"].id
|
|
)
|
|
if resp["response_object"]:
|
|
return {"message":
|
|
"Card ending in {} registered as your payment "
|
|
"source".format(last4)
|
|
}, 200
|
|
else:
|
|
logging.error("Could not obtain token")
|
|
return {"message": "Error with payment gateway. "
|
|
"Contact support"}, 400
|
|
else:
|
|
logging.error(
|
|
"Error occurred {}".format(ce_response["error"])
|
|
)
|
|
return {"message": "Error: {}".format(
|
|
ce_response["error"]
|
|
)}, 400
|
|
else:
|
|
# Stripe customer does not exist, create a new one
|
|
logging.debug(
|
|
"Customer {} does not exist, "
|
|
"creating new".format(data['name'])
|
|
)
|
|
token_response = stripe_utils.get_token_from_card(
|
|
data["card_number"], data["cvc"], data["expiry_month"],
|
|
data["expiry_year"]
|
|
)
|
|
if token_response["response_object"]:
|
|
logging.debug(
|
|
"Token {}".format(
|
|
token_response["response_object"].id))
|
|
|
|
#Create stripe customer
|
|
stripe_customer_resp = stripe_utils.create_customer(
|
|
name=data["card_holder_name"],
|
|
token=token_response["response_object"].id,
|
|
email=data["name"]
|
|
)
|
|
if stripe_customer_resp["response_object"]:
|
|
logging.debug(
|
|
"Created stripe customer {}".format(
|
|
stripe_customer_resp["response_object"].id
|
|
)
|
|
)
|
|
stripe_customer = stripe_customer_resp[
|
|
"response_object"]
|
|
return {"message":
|
|
"Card ending in {} registered as your "
|
|
"payment source".format(last4)
|
|
}, 200
|
|
else:
|
|
logging.error("Could not get/create stripe_customer "
|
|
"for {}".format(data["name"]))
|
|
return {"message":
|
|
"Error with card. Contact support"}, 400
|
|
else:
|
|
logging.error("Could not obtain token")
|
|
return {"message": "Error with payment gateway. "
|
|
"Contact support"}, 400
|
|
except KeyError as key_error:
|
|
logging.error("Key error occurred")
|
|
logging.error(str(key_error))
|
|
return {"message": "Missing or wrong parameters"}, 400
|
|
|
|
|
|
class ProductOrder(Resource):
|
|
@staticmethod
|
|
def post():
|
|
data = request.json
|
|
try:
|
|
otp_response = check_otp(data["name"], data["realm"],
|
|
data["token"])
|
|
if otp_response != 200:
|
|
return {"message": "Wrong Credentials"}, 403
|
|
|
|
stripe_utils = StripeUtils()
|
|
|
|
# Validate the given product is ok
|
|
product_id = data["product_id"]
|
|
product = client.get(
|
|
"/v1/products/{}".format(product_id), value_in_json=True
|
|
)
|
|
if not product:
|
|
logging.debug("User chose invalid product {}".format(product_id))
|
|
return {"message": "Invalid product"}, 400
|
|
|
|
logging.debug("Got product {}: {}".format(product.key, product.value))
|
|
|
|
# Check the user has a payment source added
|
|
stripe_customer = stripe_utils.get_stripe_customer_from_email(
|
|
data["name"]
|
|
)
|
|
|
|
if not stripe_customer or len(stripe_customer.sources) == 0:
|
|
logging.error("{} does not exist in Stripe => no cards".format(
|
|
data["name"])
|
|
)
|
|
return {"message": "Please register a payment source"}, 400
|
|
|
|
# Initiate a one-time/subscription based on product type
|
|
product_obj = product.value
|
|
if product_obj['type'] == "recurring":
|
|
logging.debug("Product {} is recurring payment".format(
|
|
product_obj["name"])
|
|
)
|
|
plan_id = get_plan_id_from_product(product_obj)
|
|
res = stripe_utils.get_or_create_stripe_plan(
|
|
stripe_plan_id=plan_id, amount=product_obj["price"],
|
|
name=plan_id, interval=product_obj["recurring_period"],
|
|
)
|
|
if res["response_object"]:
|
|
logging.debug("Obtained plan {}".format(plan_id))
|
|
subscription_res = stripe_utils.subscribe_customer_to_plan(
|
|
stripe_customer.id,
|
|
[{"plan": plan_id}]
|
|
)
|
|
subscription_obj = subscription_res["response_object"]
|
|
if (subscription_obj is None
|
|
or subscription_obj.status != 'active'):
|
|
logging.error("Could not create subscription")
|
|
if subscription_obj is None:
|
|
logging.error("subscription_obj is None")
|
|
else:
|
|
logging.error("subscription status is NOT active")
|
|
logging.error("Detail = {}".format(
|
|
subscription_res["error"]
|
|
))
|
|
return { "message": "Error subscribing to plan. "
|
|
"Details: {}".format(
|
|
subscription_res["error"]) }, 400
|
|
else:
|
|
logging.debug("Created subscription successfully")
|
|
order_obj = {
|
|
"order_id": get_order_id(),
|
|
"ordered_at": int(time.time()),
|
|
"product": product_obj,
|
|
}
|
|
client.put("/v1/user/{}/orders".format(
|
|
data['name']), json.dumps(order_obj),
|
|
value_in_json=True)
|
|
order_obj["ordered_at"] = datetime.fromtimestamp(
|
|
order_obj["ordered_at"]).strftime("%c")
|
|
order_obj["product"] = get_user_friendly_product(
|
|
product_obj
|
|
)
|
|
return {"message": "Order successful",
|
|
"order_details": order_obj}, 200
|
|
else:
|
|
logging.error("Could not create plan {}".format(plan_id))
|
|
|
|
elif product_obj['type'] == "one-time":
|
|
logging.debug(
|
|
"Product {} is one-time "
|
|
"payment".format(product_obj["type"])
|
|
)
|
|
except KeyError as key_error:
|
|
logging.error("Key error occurred")
|
|
logging.error(str(key_error))
|
|
return {"message": "Missing or wrong parameters"}, 400
|
|
|
|
|
|
class OrderList(Resource):
|
|
@staticmethod
|
|
def get():
|
|
data = request.json
|
|
try:
|
|
otp_response = check_otp(data["name"], data["realm"],
|
|
data["token"])
|
|
if otp_response != 200:
|
|
return {"message": "Wrong Credentials"}, 403
|
|
|
|
orders = client.get_prefix("/v1/user/{}/orders".format(data['name']), value_in_json=True)
|
|
orders_dict = {}
|
|
for p in orders:
|
|
order_dict = json.loads(p.value)
|
|
order_dict["ordered_at"] = datetime.fromtimestamp(
|
|
order_dict["ordered_at"]).strftime("%c")
|
|
order_dict["product"] = get_user_friendly_product(
|
|
order_dict["product"])
|
|
orders_dict[order_dict["order_id"]] = order_dict
|
|
logging.debug("Orders = {}".format(orders_dict))
|
|
return orders_dict, 200
|
|
|
|
except KeyError as kerr:
|
|
logging.error(str(kerr))
|
|
|
|
|
|
api.add_resource(ListProducts, "/product/list")
|
|
api.add_resource(AddProduct, "/product/add")
|
|
api.add_resource(ProductOrder, "/product/order")
|
|
api.add_resource(UserRegisterPayment, "/user/register_payment")
|
|
api.add_resource(OrderList, "/order/list")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
if APP_PORT == 5000:
|
|
app.run(host="::", debug=True)
|
|
else:
|
|
app.run(host="::", port=APP_PORT, debug=True)
|