Compare commits
11 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ea2a68142 | |||
|
|
4c916072c3 | ||
|
|
1897cc087c | ||
|
|
ab1714e733 | ||
|
|
8da25be8b8 | ||
|
|
72d651ea76 | ||
|
|
671ec75584 | ||
|
|
4c31e40411 | ||
|
|
ffda97ca8c | ||
|
|
3dab0efcd6 | ||
|
|
dd5da3b234 |
3 changed files with 107 additions and 23 deletions
36
CHANGELOG.md
Normal file
36
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# 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
|
from decouple import config, UndefinedValueError
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.DEBUG,
|
level=logging.DEBUG,
|
||||||
|
|
@ -11,7 +11,11 @@ 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)
|
||||||
STRIPE_API_PRIVATE_KEY = config("STRIPE_API_PRIVATE_KEY")
|
try:
|
||||||
|
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
|
from decouple import config, Csv, UndefinedValueError
|
||||||
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,6 +16,16 @@ 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:
|
||||||
|
|
@ -32,48 +42,54 @@ 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=config("OTP_SERVER", ""),
|
OTP_SERVER=OTP_SERVER,
|
||||||
OTP_VERIFY_ENDPOINT=config("OTP_VERIFY_ENDPOINT", "verify/"),
|
OTP_VERIFY_ENDPOINT=OTP_VERIFY_ENDPOINT,
|
||||||
),
|
),
|
||||||
data=data,
|
json=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 = config("INIT_ORDER_ID")
|
order_id = 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 {}".format(price_in_chf_cents/100)
|
return "CHF {} (One time charge)".format(price_in_chf_cents/100)
|
||||||
|
|
||||||
|
|
||||||
def get_user_friendly_product(product_dict):
|
def get_user_friendly_product(product_dict):
|
||||||
return {
|
uf_product = {
|
||||||
"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":
|
|
||||||
product_dict["minimum_subscription_period"]
|
|
||||||
}
|
}
|
||||||
|
if product_dict["type"] == "recurring":
|
||||||
|
uf_product["minimum_subscription_period"] = (
|
||||||
|
product_dict["minimum_subscription_period"]
|
||||||
|
)
|
||||||
|
return uf_product
|
||||||
|
|
||||||
|
|
||||||
class ListProducts(Resource):
|
class ListProducts(Resource):
|
||||||
|
|
@ -94,7 +110,6 @@ 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(
|
||||||
|
|
@ -110,7 +125,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_key,
|
"product_id": product_uuid,
|
||||||
"name": data["product_name"],
|
"name": data["product_name"],
|
||||||
"description": data["product_description"],
|
"description": data["product_description"],
|
||||||
"type": data["product_type"],
|
"type": data["product_type"],
|
||||||
|
|
@ -126,7 +141,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_key
|
data['product_name'], product_uuid
|
||||||
)}, 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)))
|
||||||
|
|
@ -351,6 +366,38 @@ 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))
|
||||||
|
|
@ -371,14 +418,11 @@ 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[p.key] = order_dict
|
orders_dict[order_dict["order_id"]] = 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