Compare commits

..

No commits in common. "master" and "0.5.0" have entirely different histories.

3 changed files with 23 additions and 107 deletions

View file

@ -1,36 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
[//]: <> (Unreleased)
## [0.5.3] - 2019-10-13
### Added
Handle errors when undefined configs more gracefully
## [0.5.2] - 2019-09-19
### Added
Handling one time payment option for one-time products
## [0.5.1] - 2019-09-19
### Changed
Fix minor issues in 0.5.0
## [0.5.0] - 2019-09-19
### Added
Features described in [\#7125](https://redmine.ungleich.ch/issues/7125)
- Users (inlcuding anonymous users) can list products
- Authenticated users (with valid credentials) can:
- register a payment method
- make an order
- list their orders
- Admin user can create products

View file

@ -1,7 +1,7 @@
import logging import logging
from etcd3_wrapper import Etcd3Wrapper from etcd3_wrapper import Etcd3Wrapper
from decouple import config, UndefinedValueError from decouple import config
logging.basicConfig( logging.basicConfig(
level=logging.DEBUG, level=logging.DEBUG,
@ -11,11 +11,7 @@ logging.basicConfig(
datefmt='%Y-%m-%d:%H:%M:%S', datefmt='%Y-%m-%d:%H:%M:%S',
) )
# load configs
etcd_client = Etcd3Wrapper(host=config("ETCD_HOST"), port=config("ETCD_PORT"))
APP_PORT = config("APP_PORT", 5000) APP_PORT = config("APP_PORT", 5000)
try: STRIPE_API_PRIVATE_KEY = config("STRIPE_API_PRIVATE_KEY")
etcd_client = Etcd3Wrapper(host=config("ETCD_HOST"), port=config("ETCD_PORT"))
STRIPE_API_PRIVATE_KEY = config("STRIPE_API_PRIVATE_KEY")
except UndefinedValueError as uve:
print(str(uve))
exit(1)

View file

@ -2,7 +2,7 @@ import binascii
import json import json
import requests import requests
from decouple import config, Csv, UndefinedValueError from decouple import config, Csv
from datetime import datetime from datetime import datetime
from flask import Flask, request from flask import Flask, request
from flask_restful import Resource, Api from flask_restful import Resource, Api
@ -16,16 +16,6 @@ import time
app = Flask(__name__) app = Flask(__name__)
api = Api(app) 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): def check_otp(name, realm, token):
try: try:
@ -42,54 +32,48 @@ def check_otp(name, realm, token):
response = requests.post( response = requests.post(
"{OTP_SERVER}{OTP_VERIFY_ENDPOINT}".format( "{OTP_SERVER}{OTP_VERIFY_ENDPOINT}".format(
OTP_SERVER=OTP_SERVER, OTP_SERVER=config("OTP_SERVER", ""),
OTP_VERIFY_ENDPOINT=OTP_VERIFY_ENDPOINT, OTP_VERIFY_ENDPOINT=config("OTP_VERIFY_ENDPOINT", "verify/"),
), ),
json=data, data=data,
) )
return response.status_code return response.status_code
def get_plan_id_from_product(product): def get_plan_id_from_product(product):
plan_id = "ucloud-v1-" plan_id = "ucloud-v1-"
plan_id += product["name"].strip().replace(' ', '-') + "-" plan_id += product["name"].strip().replace(' ', '-') + "-"
plan_id += product["type"] plan_id += product["type"]
return plan_id return plan_id
def get_order_id(): def get_order_id():
order_id_kv = client.get("/v1/last_order_id") order_id_kv = client.get("/v1/last_order_id")
if order_id_kv is not None: if order_id_kv is not None:
order_id = int(order_id_kv.value) + 1 order_id = int(order_id_kv.value) + 1
else: else:
order_id = INIT_ORDER_ID order_id = config("INIT_ORDER_ID")
client.put("/v1/last_order_id", str(order_id)) client.put("/v1/last_order_id", str(order_id))
return "OR-{}".format(order_id) return "OR-{}".format(order_id)
def get_pricing(price_in_chf_cents, product_type, recurring_period): def get_pricing(price_in_chf_cents, product_type, recurring_period):
if product_type == "recurring": if product_type == "recurring":
return "CHF {}/{}".format( return "CHF {}/ {}".format(
price_in_chf_cents/100, price_in_chf_cents/100,
recurring_period recurring_period
) )
elif product_type == "one-time": elif product_type == "one-time":
return "CHF {} (One time charge)".format(price_in_chf_cents/100) return "CHF {}".format(price_in_chf_cents/100)
def get_user_friendly_product(product_dict): def get_user_friendly_product(product_dict):
uf_product = { return {
"name": product_dict["name"], "name": product_dict["name"],
"description": product_dict["description"], "description": product_dict["description"],
"product_id": product_dict["product_id"], "product_id": product_dict["product_id"],
"pricing": get_pricing(product_dict["price"], product_dict["type"], "pricing": get_pricing(product_dict["price"], product_dict["type"],
product_dict["recurring_period"]) product_dict["recurring_period"]),
} "minimum_subscription_period":
if product_dict["type"] == "recurring":
uf_product["minimum_subscription_period"] = (
product_dict["minimum_subscription_period"] product_dict["minimum_subscription_period"]
) }
return uf_product
class ListProducts(Resource): class ListProducts(Resource):
@ -110,6 +94,7 @@ class AddProduct(Resource):
def post(): def post():
data = request.json data = request.json
logging.debug("Got data: {}".format(str(data))) logging.debug("Got data: {}".format(str(data)))
REALM_ALLOWED = config("REALM_ALLOWED", cast=Csv(str))
logging.debug("REALM_ALLOWED = {}".format(REALM_ALLOWED)) logging.debug("REALM_ALLOWED = {}".format(REALM_ALLOWED))
if data["realm"] not in REALM_ALLOWED: if data["realm"] not in REALM_ALLOWED:
logging.error( logging.error(
@ -125,7 +110,7 @@ class AddProduct(Resource):
product_uuid = uuid4().hex product_uuid = uuid4().hex
product_key = "/v1/products/{}".format(product_uuid) product_key = "/v1/products/{}".format(product_uuid)
product_value = { product_value = {
"product_id": product_uuid, "product_id": product_key,
"name": data["product_name"], "name": data["product_name"],
"description": data["product_description"], "description": data["product_description"],
"type": data["product_type"], "type": data["product_type"],
@ -141,7 +126,7 @@ class AddProduct(Resource):
client.put(product_key, product_value, value_in_json=True) client.put(product_key, product_value, value_in_json=True)
return {"message": return {"message":
"Product {} created. Product ID = {}".format( "Product {} created. Product ID = {}".format(
data['product_name'], product_uuid data['product_name'], product_key
)}, 200 )}, 200
except KeyError as ke: except KeyError as ke:
logging.error("KeyError occurred. details = {}".format(str(ke))) logging.error("KeyError occurred. details = {}".format(str(ke)))
@ -366,38 +351,6 @@ class ProductOrder(Resource):
"Product {} is one-time " "Product {} is one-time "
"payment".format(product_obj["type"]) "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: except KeyError as key_error:
logging.error("Key error occurred") logging.error("Key error occurred")
logging.error(str(key_error)) logging.error(str(key_error))
@ -418,11 +371,14 @@ class OrderList(Resource):
orders_dict = {} orders_dict = {}
for p in orders: for p in orders:
order_dict = json.loads(p.value) order_dict = json.loads(p.value)
logging.debug("order_dict = " + str(order_dict))
logging.debug("type p.value = " + str(type(p.value)))
logging.debug("p.value = " + str(p.value))
order_dict["ordered_at"] = datetime.fromtimestamp( order_dict["ordered_at"] = datetime.fromtimestamp(
order_dict["ordered_at"]).strftime("%c") order_dict["ordered_at"]).strftime("%c")
order_dict["product"] = get_user_friendly_product( order_dict["product"] = get_user_friendly_product(
order_dict["product"]) order_dict["product"])
orders_dict[order_dict["order_id"]] = order_dict orders_dict[p.key] = order_dict
logging.debug("Orders = {}".format(orders_dict)) logging.debug("Orders = {}".format(orders_dict))
return orders_dict, 200 return orders_dict, 200