import binascii import json import requests from decouple import config, Csv 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) class ListProducts(Resource): @staticmethod def get(): products = client.get_prefix("/v1/products/", value_in_json=False) prod_dict = {} for p in products: p_json = json.loads(p.value) product_id = p.key[p.key.rindex("/")+1:] actual_product = { "name": p_json["name"], "description": p_json["description"], "product_id": product_id, "pricing": get_pricing(p_json["price"], p_json["type"], p_json["recurring_period"]), "minimum_subscription_period": p_json["minimum_subscription_period"] } prod_dict[product_id] = actual_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 = { "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 } logging.debug("Adding product data: {}".format(str(product_value))) client.put(product_key, product_value, value_in_json=True) return {"message": "Product {} created".format(data['product_name'])}, 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["email"]) # Does customer already exist ? if stripe_customer is not None: logging.debug( "Customer {} exists already".format(data['email']) ) # 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['email']) ) 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["email"] ) 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["email"])) 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["email"] ) if not stripe_customer or len(stripe_customer.sources) == 0: logging.error("{} does not exist in Stripe => no cards".format( data["email"]) ) 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['email']), json.dumps(order_obj), value_in_json=True) 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['email']), value_in_json=True) order_dict = {} for p in orders: order_dict[p.key] = p.value logging.debug("Orders = {}".format(order_dict)) return order_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)