taler-merchant-demos

Python-based Frontends for the Demonstration Web site
Log | Files | Refs | Submodules | README | LICENSE

donations.py (10962B)


      1 ##
      2 # This file is part of GNU TALER.
      3 # Copyright (C) 2014-2016, 2020 Taler Systems SA
      4 #
      5 # TALER is free software; you can redistribute it and/or modify it under the
      6 # terms of the GNU Lesser General Public License as published by the Free Software
      7 # Foundation; either version 2.1, or (at your option) any later version.
      8 #
      9 # TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11 # A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
     12 #
     13 # You should have received a copy of the GNU Lesser General Public License along with
     14 # GNU TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 #
     16 # @author Florian Dold
     17 # @author Marcello Stanisci
     18 # @brief Implementation of a donations site.
     19 
     20 import base64
     21 import logging
     22 import flask
     23 from flask import url_for
     24 from flask_babel import Babel
     25 from flask_babel import gettext
     26 from werkzeug.middleware.proxy_fix import ProxyFix
     27 import os
     28 import time
     29 from ..httpcommon import backend_post, backend_get, make_utility_processor, get_locale
     30 from ..appconfig import load_taler_config
     31 import sys
     32 
     33 if not sys.version_info.major == 3 and sys.version_info.minor >= 6:
     34     print("Python 3.6 or higher is required.")
     35     print(
     36         "You are using Python {}.{}.".format(
     37             sys.version_info.major, sys.version_info.minor
     38         )
     39     )
     40     sys.exit(1)
     41 
     42 logger = logging.getLogger(__name__)
     43 
     44 BABEL_TRANSLATION_DIRECTORIES = "../translations"
     45 
     46 app = flask.Flask(__name__, template_folder="../templates", static_folder="../static")
     47 app.wsgi_app = ProxyFix(app.wsgi_app, x_host=1, x_prefix=1)
     48 app.debug = True
     49 app.secret_key = base64.b64encode(os.urandom(64)).decode("utf-8")
     50 
     51 app.config.from_object(__name__)
     52 
     53 babel = Babel(app, locale_selector=get_locale)
     54 
     55 config = load_taler_config()
     56 
     57 CURRENCY = config["taler"]["currency"].value_string(required=True)
     58 
     59 backend_urls = {}
     60 backend_apikeys = {}
     61 
     62 
     63 def add_backend(name):
     64     backend_urls[name] = config["frontend-demo-donations"][
     65         f"backend_url_{name}"
     66     ].value_string(required=True)
     67     backend_apikeys[name] = config["frontend-demo-donations"][
     68         f"backend_apikey_{name}"
     69     ].value_string(required=True)
     70 
     71 
     72 add_backend("tor")
     73 add_backend("taler")
     74 add_backend("gnunet")
     75 
     76 # Use donation authority if configured
     77 donau_url = config["frontend-demo-donations"][f"donau_url"].value_string()
     78 if donau_url:
     79     donau_url = donau_url.strip()
     80     if donau_url == "":
     81         donau_url = None
     82 
     83 
     84 logger.info("currency: " + CURRENCY)
     85 
     86 # Add context processor that will make additional variables
     87 # and functions available in the template.
     88 app.context_processor(
     89     make_utility_processor(
     90         "donations", os.environ.get("TALER_ENV_URL_MERCHANT_DONATIONS")
     91     )
     92 )
     93 
     94 
     95 ##
     96 # Return a error response to the client.
     97 #
     98 # @param abort_status_code status code to return along the response.
     99 # @param params _kw_ arguments to passed verbatim to the templating engine.
    100 def err_abort(abort_status_code, **params):
    101     t = flask.render_template("donations-error.html.j2", **params)
    102     flask.abort(flask.make_response(t, abort_status_code))
    103 
    104 
    105 ##
    106 # Issue a GET request to the backend.
    107 #
    108 # @param endpoint the backend endpoint where to issue the request.
    109 # @param params (dict type of) URL parameters to append to the request.
    110 # @return the JSON response from the backend, or a error response
    111 #         if something unexpected happens.
    112 def backend_instanced_get(instance, endpoint, params):
    113     return backend_get(
    114         backend_urls[instance], endpoint, params, auth_token=backend_apikeys[instance]
    115     )
    116 
    117 
    118 ##
    119 # POST a request to the backend, and return a error
    120 # response if any error occurs.
    121 #
    122 # @param endpoint the backend endpoint where to POST
    123 #        this request.
    124 # @param json the POST's body.
    125 # @return the backend response (JSON format).
    126 def backend_instanced_post(instance, endpoint, json):
    127     return backend_post(
    128         backend_urls[instance], endpoint, json, auth_token=backend_apikeys[instance]
    129     )
    130 
    131 
    132 ##
    133 # Inspect GET arguments in the look for a parameter.
    134 #
    135 # @param name the parameter name to lookup.
    136 # @return the parameter value, or a error page if not found.
    137 def expect_parameter(name):
    138     val = flask.request.args.get(name)
    139     if not val:
    140         return err_abort(400, message=gettext("parameter '{}' required").format(name))
    141     return val
    142 
    143 
    144 ##
    145 # "Fallback" exception handler to capture all the unmanaged errors.
    146 #
    147 # @param e the Exception object, currently unused.
    148 # @return flask-native response object carrying the error message
    149 #         (and execution stack!).
    150 @app.errorhandler(Exception)
    151 def internal_error(e):
    152     return flask.render_template(
    153         "donations-error.html.j2",
    154         page_title=gettext("GNU Taler Demo: Error"),
    155         message=str(e),
    156     )
    157 
    158 
    159 ##
    160 # Serve the /favicon.ico requests.
    161 #
    162 # @return the favicon.ico file.
    163 @app.route("/favicon.ico")
    164 def favicon():
    165     logger.info("will look into: " + os.path.join(app.root_path, "static"))
    166     return flask.send_from_directory(
    167         os.path.join(app.root_path, "static"),
    168         "favicon.ico",
    169         mimetype="image/vnd.microsoft.ico",
    170     )
    171 
    172 
    173 ##
    174 # Serve the main index page, redirecting to /<lang>/
    175 #
    176 # @return response object of the index page.
    177 @app.route("/")
    178 def index():
    179     default = "en"
    180     translations = [x.language for x in babel.list_translations()]
    181     target = flask.request.accept_languages.best_match(translations, default)
    182     return flask.redirect(url_for("index") + target + "/", code=302)
    183 
    184 
    185 ##
    186 # Serve the main index page.
    187 #
    188 # @return response object of the index page.
    189 @app.route("/<lang>/")
    190 def start(lang):
    191 
    192     # get_locale defaults to english, hence the
    193     # condition below happens only when lang is
    194     # wrong or unsupported, respond 404.
    195     if lang != get_locale():
    196         err_abort(
    197             404,
    198             message=f"Language {lang} not found",
    199         )
    200 
    201     return flask.render_template(
    202         "donations-index.html.j2",
    203         page_title=gettext("GNU Taler Demo: Donations"),
    204         merchant_currency=CURRENCY,
    205     )
    206 
    207 
    208 ##
    209 # Serve the "/checkout" page.  This page lets the
    210 # user pick the payment method they want to use,
    211 # and finally confirm the donation.
    212 #
    213 # @return response object for the /checkout page.
    214 @app.route("/<lang>/checkout", methods=["GET"])
    215 def checkout(lang):
    216     amount = expect_parameter("donation_amount")
    217     donation_receiver = expect_parameter("donation_receiver")
    218     donation_donor = expect_parameter("donation_donor")
    219     return flask.render_template(
    220         "donations-checkout.html.j2",
    221         page_title=gettext("GNU Taler Demo: Donations checkout"),
    222         donation_amount=amount,
    223         donation_receiver=donation_receiver,
    224         donation_donor=donation_donor,
    225         merchant_currency=CURRENCY,
    226     )
    227 
    228 
    229 ##
    230 # Serve the page advising the user about the impossibility
    231 # of further processing the payment method they chose.
    232 #
    233 # @return response object about the mentioned impossibility.
    234 @app.route("/<lang>/provider-not-supported")
    235 def provider_not_supported(lang):
    236     return flask.render_template(
    237         "donations-provider-not-supported.html.j2",
    238         page_title=gettext("GNU Taler Demo: Donations"),
    239     )
    240 
    241 
    242 ##
    243 # POST the donation request to the backend.  In particular,
    244 # it uses the "POST /order" API.
    245 #
    246 # @return response object that will redirect the browser to
    247 #         the fulfillment URL, where all the pay-logic will
    248 #         happen.
    249 @app.route("/<lang>/donate")
    250 def donate(lang):
    251     donation_receiver = expect_parameter("donation_receiver")
    252     donation_amount = expect_parameter("donation_amount")
    253     donation_donor = expect_parameter("donation_donor")
    254     payment_system = expect_parameter("payment_system")
    255     if payment_system != "taler":
    256         return flask.redirect(flask.url_for("provider_not_supported", lang=lang))
    257     fulfillment_url = flask.url_for(
    258         "fulfillment",
    259         timestamp=str(time.time()),
    260         receiver=donation_receiver,
    261         lang=lang,
    262         _external=True,
    263     )
    264     fulfillment_url = fulfillment_url + "&order_id=${ORDER_ID}"
    265     if donau_url is None:
    266         order = dict(
    267             amount=donation_amount,
    268             extra=dict(
    269                 donor=donation_donor, receiver=donation_receiver, amount=donation_amount
    270             ),
    271             fulfillment_url=fulfillment_url,
    272             summary="Donation to {}".format(donation_receiver),
    273             wire_transfer_deadline=dict(t_s=int(time.time() + 10)),
    274             minimum_age=16,
    275         )
    276     else:
    277         order = dict(
    278             version=1,
    279             extra=dict(
    280                 donor=donation_donor, receiver=donation_receiver, amount=donation_amount
    281             ),
    282             fulfillment_url=fulfillment_url,
    283             summary="Donation to {} (with receipt)".format(donation_receiver),
    284             wire_transfer_deadline=dict(t_s=int(time.time() + 10)),
    285             minimum_age=16,
    286             choices=[
    287                 dict(
    288                     amount=donation_amount,
    289                     outputs=[dict(type="tax-receipt", donau_urls=[donau_url])],
    290                 ),
    291             ],
    292         )
    293 
    294     order_resp = backend_instanced_post(
    295         donation_receiver, "private/orders", dict(order=order)
    296     )
    297 
    298     if not order_resp:
    299         return err_abort(
    300             500,  # FIXME: status code got lost in the httpcommon module.
    301             message=gettext("Backend could not create the order"),
    302         )
    303 
    304     order_id = order_resp["order_id"]
    305     return flask.redirect(
    306         flask.url_for(
    307             "fulfillment", receiver=donation_receiver, order_id=order_id, lang=lang
    308         )
    309     )
    310 
    311 
    312 ##
    313 # Serve the fulfillment page.
    314 #
    315 # @param receiver the donation receiver name, that should
    316 #        correspond to a merchant instance.
    317 # @return after the wallet sent the payment, the final HTML "congrats"
    318 #         page is returned; otherwise, the browser will be redirected
    319 #         to a page that accepts the payment.
    320 @app.route("/<lang>/donation/<receiver>")
    321 def fulfillment(lang, receiver):
    322     order_id = expect_parameter("order_id")
    323     pay_status = backend_instanced_get(
    324         receiver, f"private/orders/{order_id}", params=dict()
    325     )
    326     order_status = pay_status.get("order_status")
    327     if order_status == "paid":
    328         extra = pay_status["contract_terms"]["extra"]
    329         return flask.render_template(
    330             "donations-fulfillment.html.j2",
    331             page_title=gettext("GNU Taler Demo: Donations"),
    332             donation_receiver=extra["receiver"],
    333             donation_amount=extra["amount"],
    334             donation_donor=extra["donor"],
    335             order_id=order_id,
    336             currency=CURRENCY,
    337         )
    338     return flask.redirect(pay_status["order_status_url"])
    339 
    340 
    341 @app.errorhandler(404)
    342 def handler(e):
    343     return flask.render_template(
    344         "donations-error.html.j2",
    345         page_title=gettext("GNU Taler Demo: Error"),
    346         message=gettext("Page not found"),
    347     )