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
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)
|
|
|