You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

444 lines
18 KiB

import binascii
import json
import requests
from decouple import config, Csv, UndefinedValueError
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)
# load configs
OTP_SERVER = config("OTP_SERVER", "")
OTP_VERIFY_ENDPOINT = config("OTP_VERIFY_ENDPOINT", "verify/")
try:
INIT_ORDER_ID = config("INIT_ORDER_ID")
REALM_ALLOWED = config("REALM_ALLOWED", cast=Csv(str))
except UndefinedValueError as uve:
print(str(uve))
exit(1)
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=OTP_SERVER,
OTP_VERIFY_ENDPOINT=OTP_VERIFY_ENDPOINT,
),
json=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 = 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 {} (One time charge)".format(price_in_chf_cents/100)
def get_user_friendly_product(product_dict):
uf_product = {
"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"])
}
if product_dict["type"] == "recurring":
uf_product["minimum_subscription_period"] = (
product_dict["minimum_subscription_period"]
)
return uf_product
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)))
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"])
)
charge_response = stripe_utils.make_charge(
amount=product_obj['price'],
customer=stripe_customer.id
)
stripe_onetime_charge = charge_response.get('response_object')
# Check if the payment was approved
if not stripe_onetime_charge:
msg = charge_response.get('error')
logging.error("Could not make a one time payment")
logging.error("Details = {}".format(msg))
return {"message": "Error subscribing to plan. "
"Details: {}".format(msg)}, 400
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
)
logging.debug(str(order_obj))
return {"message": "Order successful",
"order_details": order_obj}, 200
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)