summaryrefslogtreecommitdiff
path: root/talermerchantdemos/donations/donations.py
diff options
context:
space:
mode:
authorMS <ms@taler.net>2020-07-22 16:11:30 +0200
committerMS <ms@taler.net>2020-07-22 16:11:30 +0200
commit3ca29bf465d0419c0915924910b4e81a73ec8cef (patch)
tree66a221ee0116abceb0179ddabc03040b0b3c7c1c /talermerchantdemos/donations/donations.py
parent147658b30dfbce61f6f4e087a0c81e85899dda15 (diff)
downloadtaler-merchant-demos-3ca29bf465d0419c0915924910b4e81a73ec8cef.tar.gz
taler-merchant-demos-3ca29bf465d0419c0915924910b4e81a73ec8cef.tar.bz2
taler-merchant-demos-3ca29bf465d0419c0915924910b4e81a73ec8cef.zip
import static files
Diffstat (limited to 'talermerchantdemos/donations/donations.py')
-rw-r--r--talermerchantdemos/donations/donations.py314
1 files changed, 314 insertions, 0 deletions
diff --git a/talermerchantdemos/donations/donations.py b/talermerchantdemos/donations/donations.py
new file mode 100644
index 0000000..c6c03e5
--- /dev/null
+++ b/talermerchantdemos/donations/donations.py
@@ -0,0 +1,314 @@
+##
+# This file is part of GNU TALER.
+# Copyright (C) 2014-2016 INRIA
+#
+# TALER is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free Software
+# Foundation; either version 2.1, or (at your option) any later version.
+#
+# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along with
+# GNU TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+# @author Florian Dold
+# @author Marcello Stanisci
+# @brief Implementation of a donations site.
+
+import base64
+import flask
+import logging
+import lxml.etree
+import os
+import qrcode
+import qrcode.image.svg
+import random
+import requests
+import time
+import traceback
+import urllib
+import uuid
+from taler.util.talerconfig import TalerConfig
+from urllib.parse import urljoin, parse_qsl
+
+LOGGER = logging.getLogger(__name__)
+
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+
+app = flask.Flask(__name__, template_folder=BASE_DIR)
+app.debug = True
+app.secret_key = base64.b64encode(os.urandom(64)).decode('utf-8')
+
+TC = TalerConfig.from_env()
+BACKEND_BASE_URL = TC["frontends"]["backend"].value_string(required=True)
+CURRENCY = TC["taler"]["currency"].value_string(required=True)
+APIKEY = TC["frontends"]["backend_apikey"].value_string(required=True)
+
+app.config.from_object(__name__)
+
+
+##
+# Extend the templating language with a function
+# that fetches values from the environment.
+#
+# @return the environment-reading function.
+@app.context_processor
+def utility_processor():
+ def env(name, default=None):
+ return os.environ.get(name, default)
+
+ return dict(env=env)
+
+
+##
+# Return a error response to the client.
+#
+# @param abort_status_code status code to return along the response.
+# @param params _kw_ arguments to passed verbatim to the templating engine.
+def err_abort(abort_status_code, **params):
+ t = flask.render_template("templates/error.html", **params)
+ flask.abort(flask.make_response(t, abort_status_code))
+
+
+##
+# Issue a GET request to the backend.
+#
+# @param endpoint the backend endpoint where to issue the request.
+# @param params (dict type of) URL parameters to append to the request.
+# @return the JSON response from the backend, or a error response
+# if something unexpected happens.
+def backend_instanced_get(instance, endpoint, params):
+ backend_url = urljoin(BACKEND_BASE_URL, f"instances/{instance}/")
+ headers = {"Authorization": "ApiKey " + APIKEY}
+ try:
+ resp = requests.get(
+ urljoin(backend_url, endpoint), params=params, headers=headers
+ )
+ except requests.ConnectionError:
+ err_abort(500, message="Could not establish connection to backend")
+ try:
+ response_json = resp.json()
+ except ValueError:
+ err_abort(500, message="Could not parse response from backend")
+ if resp.status_code != 200:
+ err_abort(
+ 500,
+ message="Backend returned error status",
+ json=response_json,
+ status_code=resp.status_code
+ )
+ return response_json
+
+
+##
+# POST a request to the backend, and return a error
+# response if any error occurs.
+#
+# @param endpoint the backend endpoint where to POST
+# this request.
+# @param json the POST's body.
+# @return the backend response (JSON format).
+def backend_instanced_post(instance, endpoint, json):
+ backend_url = urljoin(BACKEND_BASE_URL, f"instances/{instance}/")
+ headers = {"Authorization": "ApiKey " + APIKEY}
+ try:
+ resp = requests.post(
+ urljoin(backend_url, endpoint), json=json, headers=headers
+ )
+ except requests.ConnectionError:
+ err_abort(500, message=f"Could not establish connection to backend (url={url})")
+ try:
+ response_json = resp.json()
+ except ValueError:
+ err_abort(
+ 500,
+ message="Could not parse response from backend",
+ status_code=resp.status_code
+ )
+ if resp.status_code != 200:
+ err_abort(
+ 500,
+ message="Backend returned error status",
+ json=response_json,
+ status_code=resp.status_code
+ )
+ return response_json
+
+
+##
+# Inspect GET arguments in the look for a parameter.
+#
+# @param name the parameter name to lookup.
+# @return the parameter value, or a error page if not found.
+def expect_parameter(name):
+ val = flask.request.args.get(name)
+ if not val:
+ return err_abort(400, message="parameter '{}' required".format(name))
+ return val
+
+
+##
+# "Fallback" exception handler to capture all the unmanaged errors.
+#
+# @param e the Exception object, currently unused.
+# @return flask-native response object carrying the error message
+# (and execution stack!).
+@app.errorhandler(Exception)
+def internal_error(e):
+ return flask.render_template(
+ "templates/error.html",
+ message="Internal error",
+ stack=traceback.format_exc()
+ )
+
+
+##
+# Serve the main index page.
+#
+# @return response object of the index page.
+@app.route("/")
+def index():
+ return flask.render_template(
+ "templates/index.html", merchant_currency=CURRENCY
+ )
+
+
+##
+# Serve the "/javascript" page.
+#
+# @return response object for the /javascript page.
+@app.route("/javascript")
+def javascript_licensing():
+ return flask.render_template("templates/javascript.html")
+
+
+##
+# Serve the "/checkout" page. This page lets the
+# user pick the payment method they want to use,
+# and finally confirm the donation.
+#
+# @return response object for the /checkout page.
+@app.route("/checkout", methods=["GET"])
+def checkout():
+ amount = expect_parameter("donation_amount")
+ donation_receiver = expect_parameter("donation_receiver")
+ donation_donor = expect_parameter("donation_donor")
+ return flask.render_template(
+ "templates/checkout.html",
+ donation_amount=amount,
+ donation_receiver=donation_receiver,
+ donation_donor=donation_donor,
+ merchant_currency=CURRENCY
+ )
+
+
+##
+# Serve the page advising the user about the impossibility
+# of further processing the payment method they chose.
+#
+# @return response object about the mentioned impossibility.
+@app.route("/provider-not-supported")
+def provider_not_supported():
+ return flask.render_template("templates/provider-not-supported.html")
+
+
+##
+# POST the donation request to the backend. In particular,
+# it uses the "POST /order" API.
+#
+# @return response object that will redirect the browser to
+# the fulfillment URL, where all the pay-logic will
+# happen.
+@app.route("/donate")
+def donate():
+ donation_receiver = expect_parameter("donation_receiver")
+ donation_amount = expect_parameter("donation_amount")
+ donation_donor = expect_parameter("donation_donor")
+ payment_system = expect_parameter("payment_system")
+ if payment_system != "taler":
+ return flask.redirect(flask.url_for("provider_not_supported"))
+ fulfillment_url = flask.url_for(
+ "fulfillment",
+ timestamp=str(time.time()),
+ receiver=donation_receiver,
+ _external=True
+ )
+ order = dict(
+ amount=donation_amount,
+ extra=dict(
+ donor=donation_donor,
+ receiver=donation_receiver,
+ amount=donation_amount
+ ),
+ fulfillment_url=fulfillment_url,
+ summary="Donation to {}".format(donation_receiver),
+ )
+ order_resp = backend_instanced_post(donation_receiver, "order", dict(order=order))
+ order_id = order_resp["order_id"]
+ return flask.redirect(
+ flask.url_for(
+ "fulfillment", receiver=donation_receiver, order_id=order_id
+ )
+ )
+
+
+##
+# This endpoint is used by the payment request page
+# to check if the payment has been completed via the QR code.
+@app.route("/check-status/<instance>/<order_id>")
+def check_status(instance, order_id):
+ pay_params = dict(order_id=order_id)
+ pay_status = backend_instanced_get(instance, "check-payment", pay_params)
+ return flask.jsonify(paid=pay_status["paid"])
+
+
+def get_qrcode_svg(data):
+ factory = qrcode.image.svg.SvgImage
+ img = qrcode.make(data, image_factory=factory)
+ return lxml.etree.tostring(img.get_image()).decode("utf-8")
+
+
+##
+# Serve the fulfillment page.
+#
+# @param receiver the donation receiver name, that should
+# correspond to a merchant instance.
+# @return after the wallet sent the payment, the final HTML "congrats"
+# page is returned; otherwise, the browser will be redirected
+# to a page that accepts the payment.
+@app.route("/donation/<receiver>")
+def fulfillment(receiver):
+ order_id = expect_parameter("order_id")
+ pay_params = dict(order_id=order_id)
+ pay_status = backend_instanced_get(receiver, "check-payment", pay_params)
+
+ if pay_status.get("paid"):
+ extra = pay_status["contract_terms"]["extra"]
+ return flask.render_template(
+ "templates/fulfillment.html",
+ donation_receiver=extra["receiver"],
+ donation_amount=extra["amount"],
+ donation_donor=extra["donor"],
+ order_id=order_id,
+ currency=CURRENCY
+ )
+ else:
+ taler_pay_uri = pay_status["taler_pay_uri"]
+ qrcode_svg = get_qrcode_svg(taler_pay_uri)
+ check_status_url_enc = urllib.parse.quote(
+ flask.url_for("check_status", instance=receiver, order_id=order_id)
+ )
+ content = flask.render_template(
+ "templates/request_payment.html",
+ taler_pay_uri=taler_pay_uri,
+ qrcode_svg=qrcode_svg,
+ check_status_url_enc=check_status_url_enc
+ )
+ headers = {"Taler": taler_pay_uri}
+ resp = flask.Response(content, status=402, headers=headers)
+ return resp
+
+ # no pay_redirect but article not paid, this should never happen!
+ err_abort(500, message="Internal error, invariant failed", json=pay_status)