From dd5da3b234ff5ca8c6a2d0af6e6b3a8af72a0328 Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 19 Sep 2019 16:15:39 +0530 Subject: [PATCH 01/11] Add Changelog --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0e01dac --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +# 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.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 From 3dab0efcd682043dbcdcb3f6af0a4cea3cba43ba Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 19 Sep 2019 17:13:42 +0530 Subject: [PATCH 02/11] Correct product_id It was containing the entire dir /v1/... --- ucloud_pay.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ucloud_pay.py b/ucloud_pay.py index d181e0a..858205c 100644 --- a/ucloud_pay.py +++ b/ucloud_pay.py @@ -110,7 +110,7 @@ class AddProduct(Resource): product_uuid = uuid4().hex product_key = "/v1/products/{}".format(product_uuid) product_value = { - "product_id": product_key, + "product_id": product_uuid, "name": data["product_name"], "description": data["product_description"], "type": data["product_type"], @@ -126,7 +126,7 @@ class AddProduct(Resource): client.put(product_key, product_value, value_in_json=True) return {"message": "Product {} created. Product ID = {}".format( - data['product_name'], product_key + data['product_name'], product_uuid )}, 200 except KeyError as ke: logging.error("KeyError occurred. details = {}".format(str(ke))) From ffda97ca8c469651137e38a15e1ca44ed46ffb60 Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 19 Sep 2019 17:23:12 +0530 Subject: [PATCH 03/11] Remove unwanted logging + improve the order key --- ucloud_pay.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ucloud_pay.py b/ucloud_pay.py index 858205c..dae3865 100644 --- a/ucloud_pay.py +++ b/ucloud_pay.py @@ -371,14 +371,11 @@ class OrderList(Resource): orders_dict = {} for p in orders: 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"]).strftime("%c") order_dict["product"] = get_user_friendly_product( order_dict["product"]) - orders_dict[p.key] = order_dict + orders_dict[order_dict["order_id"]] = order_dict logging.debug("Orders = {}".format(orders_dict)) return orders_dict, 200 From 4c31e40411d497b8c049b1e4af377a9e2666652f Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 19 Sep 2019 17:32:05 +0530 Subject: [PATCH 04/11] Update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e01dac..ae9b7b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [//]: <> (Unreleased) +## [0.5.1] - 2019-09-19 + +### Changed +Fix minor issues in 0.5.0 + ## [0.5.0] - 2019-09-19 ### Added From 671ec755841e58d7273d5a08da425fe7a5a06038 Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 19 Sep 2019 21:08:20 +0530 Subject: [PATCH 05/11] Handle one-time payment --- ucloud_pay.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/ucloud_pay.py b/ucloud_pay.py index dae3865..f900f1d 100644 --- a/ucloud_pay.py +++ b/ucloud_pay.py @@ -351,6 +351,38 @@ class ProductOrder(Resource): "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)) From 72d651ea768b0c6c2871e6e647f9b154e291471c Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 19 Sep 2019 21:11:42 +0530 Subject: [PATCH 06/11] Improve the price shown to user --- ucloud_pay.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ucloud_pay.py b/ucloud_pay.py index f900f1d..6bf73fd 100644 --- a/ucloud_pay.py +++ b/ucloud_pay.py @@ -56,12 +56,12 @@ def get_order_id(): def get_pricing(price_in_chf_cents, product_type, recurring_period): if product_type == "recurring": - return "CHF {}/ {}".format( + return "CHF {}/{}".format( price_in_chf_cents/100, recurring_period ) 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): return { From 8da25be8b82f03c2419a711cb7ebb93456372def Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 19 Sep 2019 21:24:33 +0530 Subject: [PATCH 07/11] Show minimum_subscription_period only for recurring products --- ucloud_pay.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ucloud_pay.py b/ucloud_pay.py index 6bf73fd..0fcbe12 100644 --- a/ucloud_pay.py +++ b/ucloud_pay.py @@ -64,15 +64,18 @@ def get_pricing(price_in_chf_cents, product_type, recurring_period): return "CHF {} (One time charge)".format(price_in_chf_cents/100) def get_user_friendly_product(product_dict): - return { + 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"]), - "minimum_subscription_period": - product_dict["minimum_subscription_period"] + product_dict["recurring_period"]) } + if product_dict["type"] == "recurring": + uf_product["minimum_subscription_period"] = ( + product_dict["minimum_subscription_period"] + ) + return uf_product From ab1714e7338db680f3c945c43129d86a26819031 Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 19 Sep 2019 22:00:54 +0530 Subject: [PATCH 08/11] Update Changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae9b7b1..4c88ac9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [//]: <> (Unreleased) +## [0.5.2] - 2019-09-19 + +### Added +Handling one time payment option for one-time products + + ## [0.5.1] - 2019-09-19 ### Changed From 1897cc087c9a6ba8ea8ef836d9b7b0da5ba58b2b Mon Sep 17 00:00:00 2001 From: PCoder Date: Sun, 13 Oct 2019 09:08:40 +0530 Subject: [PATCH 09/11] Handle config not set cases more gracefully --- config.py | 12 ++++++++---- ucloud_pay.py | 28 ++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/config.py b/config.py index 578fc82..2d7dcbc 100644 --- a/config.py +++ b/config.py @@ -1,7 +1,7 @@ import logging from etcd3_wrapper import Etcd3Wrapper -from decouple import config +from decouple import config, UndefinedValueError logging.basicConfig( level=logging.DEBUG, @@ -11,7 +11,11 @@ logging.basicConfig( datefmt='%Y-%m-%d:%H:%M:%S', ) - -etcd_client = Etcd3Wrapper(host=config("ETCD_HOST"), port=config("ETCD_PORT")) +# load configs 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) diff --git a/ucloud_pay.py b/ucloud_pay.py index 0fcbe12..f7a1c73 100644 --- a/ucloud_pay.py +++ b/ucloud_pay.py @@ -2,7 +2,7 @@ import binascii import json import requests -from decouple import config, Csv +from decouple import config, Csv, UndefinedValueError from datetime import datetime from flask import Flask, request from flask_restful import Resource, Api @@ -16,13 +16,26 @@ import time app = Flask(__name__) api = Api(app) +# load configs +AUTH_NAME = config("AUTH_NAME", "") +AUTH_TOKEN = TOTP(config("AUTH_SEED", "")).now() +AUTH_REALM = config("AUTH_REALM", "") +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", ""), + "auth_name": AUTH_NAME, + "auth_token": AUTH_TOKEN, + "auth_realm": AUTH_REALM, "name": name, "realm": realm, "token": token, @@ -32,8 +45,8 @@ def check_otp(name, realm, token): response = requests.post( "{OTP_SERVER}{OTP_VERIFY_ENDPOINT}".format( - OTP_SERVER=config("OTP_SERVER", ""), - OTP_VERIFY_ENDPOINT=config("OTP_VERIFY_ENDPOINT", "verify/"), + OTP_SERVER=OTP_SERVER, + OTP_VERIFY_ENDPOINT=OTP_VERIFY_ENDPOINT, ), data=data, ) @@ -50,7 +63,7 @@ def get_order_id(): if order_id_kv is not None: order_id = int(order_id_kv.value) + 1 else: - order_id = config("INIT_ORDER_ID") + order_id = INIT_ORDER_ID client.put("/v1/last_order_id", str(order_id)) return "OR-{}".format(order_id) @@ -97,7 +110,6 @@ class AddProduct(Resource): 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( From 4c916072c382ce871aa993dec2984f871db78f3d Mon Sep 17 00:00:00 2001 From: PCoder Date: Sun, 13 Oct 2019 09:17:38 +0530 Subject: [PATCH 10/11] Update Changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c88ac9..1888973 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [//]: <> (Unreleased) +## [0.5.3] - 2019-10-13 + +### Added +Handle errors when undefined configs more gracefully + + ## [0.5.2] - 2019-09-19 ### Added From 0ea2a681428994451c2f21b9aab71a9320ab28b7 Mon Sep 17 00:00:00 2001 From: meow Date: Tue, 3 Dec 2019 18:45:45 +0500 Subject: [PATCH 11/11] Fixed order in which AUTH Token is generated --- ucloud_pay.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ucloud_pay.py b/ucloud_pay.py index f7a1c73..1269c0d 100644 --- a/ucloud_pay.py +++ b/ucloud_pay.py @@ -17,9 +17,6 @@ app = Flask(__name__) api = Api(app) # load configs -AUTH_NAME = config("AUTH_NAME", "") -AUTH_TOKEN = TOTP(config("AUTH_SEED", "")).now() -AUTH_REALM = config("AUTH_REALM", "") OTP_SERVER = config("OTP_SERVER", "") OTP_VERIFY_ENDPOINT = config("OTP_VERIFY_ENDPOINT", "verify/") @@ -33,9 +30,9 @@ except UndefinedValueError as uve: def check_otp(name, realm, token): try: data = { - "auth_name": AUTH_NAME, - "auth_token": AUTH_TOKEN, - "auth_realm": AUTH_REALM, + "auth_name": config("AUTH_NAME", ""), + "auth_token": TOTP(config("AUTH_SEED", "")).now(), + "auth_realm": config("AUTH_REALM", ""), "name": name, "realm": realm, "token": token, @@ -48,16 +45,18 @@ def check_otp(name, realm, token): OTP_SERVER=OTP_SERVER, OTP_VERIFY_ENDPOINT=OTP_VERIFY_ENDPOINT, ), - data=data, + 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: @@ -67,6 +66,7 @@ def get_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( @@ -76,6 +76,7 @@ def get_pricing(price_in_chf_cents, product_type, 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"], @@ -91,7 +92,6 @@ def get_user_friendly_product(product_dict): return uf_product - class ListProducts(Resource): @staticmethod def get():