Compare commits
No commits in common. "master" and "0.5.0" have entirely different histories.
3 changed files with 23 additions and 107 deletions
36
CHANGELOG.md
36
CHANGELOG.md
|
|
@ -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
|
|
||||||
12
config.py
12
config.py
|
|
@ -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)
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue