diff --git a/README.md b/README.md index 5369054fdcf5a9ebea49bbdfaabcc0773e4c54d5..12bbc96e3e3852c433bcac731e771bfd94960d6d 100644 --- a/README.md +++ b/README.md @@ -15,65 +15,4 @@ service acts as an in-between layer and does support these features When visitors are registering to TNC2025 in Visit cloud, they are redirected to this service in the final step - -# Development - -## Config -This service needs configuation data in the form of a config json file. By default, django look -a file called `config.json` in the current directory. It is possble to override this by setting -the environment variable `CONFIG_FILENAME` to point to a different location. A sample config is -given in `config-example.json`. Make a copy named `config.json` and fill in the required data -such as the `STRIPE_API_KEY` and the `VISIT.api-key` variables. - -## Database -Connecting to a database can be done by setting the DATABASE_URL environment variable to a valid -value: -``` -export DATABASE_URL=postgresql://<username>:<password>@<host_address>/<database> -``` - -Altenatively, you can set up a local (sqlite) database. This can be done easily from the -root of this repository - -``` -python manage-dev.py migrate -``` - -This will create a database file 'db.sqlite' in the current directory. You can then create a -superuser account - -``` -python manage-dev.py createsuperuser -``` - -And finally you need to populate the database with price information. Sample price information -is given in the `prices.json` file, but this file may not be completely up to date - -``` -python manage-dev.py createprices --file prices.json -``` - -## Development server -You can then run the development server by running - -``` -python manage-dev.py runserver -``` - -## Admin interface -The django admin interface is enabled, so when running the development server you can browse to -`http://127.0.0.1:5000/admin` to login to the admin interface using the credentials you created - using the createsuperuser (or other valid credentials if connected to an existing database). Here - you can manage administrative users, priced items and other things. - -## Migrate price information to deployments - -with the correct enviroment settings setup, it is possble to dump the latest price information by running from the source database: -``` -django-admin dumpdata stripe_checkout.PricedItem |json_pp > prices.json -``` - -to load price information into the target database, run: -``` -django-admin loaddata prices.json -``` +See the full documentation on [SWD Documentation](https://swd-documentation.geant.org/stripe-checkout/develop/) diff --git a/docs/source/_images/visit-bridgeserviceurl.png b/docs/source/_images/visit-bridgeserviceurl.png new file mode 100644 index 0000000000000000000000000000000000000000..333b7021b83b91237dae2db566eeccfa61389ab7 Binary files /dev/null and b/docs/source/_images/visit-bridgeserviceurl.png differ diff --git a/docs/source/_images/visit-form-redirect.png b/docs/source/_images/visit-form-redirect.png new file mode 100644 index 0000000000000000000000000000000000000000..13ca791c62211c557d7aac9a229bc2b74b5cebad Binary files /dev/null and b/docs/source/_images/visit-form-redirect.png differ diff --git a/docs/source/_static/.gitkeep b/docs/source/_static/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/docs/source/administration.rst b/docs/source/administration.rst new file mode 100644 index 0000000000000000000000000000000000000000..ab74dcda3d3508e212a081b91c17810ea2999c4c --- /dev/null +++ b/docs/source/administration.rst @@ -0,0 +1,104 @@ +.. _administation: + +Administration +============== + +For administration, log in from the root url ``/`` in the stripe checkout server. Credentials are +available in Lastpass. The following pages are available when logged in: + +.. _django-admin: + +Django Admin portal (Admin) +--------------------------- + +.. _manage-users: + +Manage Users +############ + +You can add new users that can login and manage certain aspects of the website. Based on what the +user should be able to do, they need the following permissions: + +* Log in: Staff status (Basically every user should have this) +* Access to django admin: Superuser status +* Manage visitors: ``stripe_checkout | order | Manage visitors`` +* Download Stripe Report: ``stripe_checkout | report | Can view report`` + +.. _adding-product-info: + +Adding new products or price information to the database +######################################################## + +Log in to the django admin console and add or update a ``PricedItem``. There are two different kinds +of ``PricedItem``: ``Registration Type`` and ``Extra`` + +``Registration Type`` items are used to link a visitor's ``registrationType.id`` to a Stripe product +id. Required fields are + +* ``Stripe Product id``: The stripe product for this item. Stripe product ids start with ``prod_`` +* ``Visit registration id``: The visit ``registrationType.id`` for this registration type. See also + "New registration types" below + +``Extra`` items are tied to specific questions and answers in the Visit. A visitor may opt in +(purchase) zero or more extras during their registration, and possibly after. Every extra requires: + +* ``Visit checkout question id`` and ``Visit checkout answer id``: a combination of a question and + answer in visit that indicates that the visitor wants to purchase this extra. + +* ``Visit paid question id`` and ``Visit paid answer id``: a combination of a question and + answer in visit to indicdate that the visitor has paid for this extra. Upon receiving confirmation + of payment, stripe checkout will set this combination in the visitor's profile. + +Aside from the fields described above, the following fields are required for all ``PricedItem`` +objects: + +* ``Name``: a snake case name that helps with idenfication. This name is not shown to visitors +* ``Display Name``: a user friendly name. This name is shown to visitors in their checkout page and + should match the user facing name in Visit and Stripe +* ``Kind``: See above + +Migrate price information between deployments +############################################# + +With the correct enviroment settings setup, it is possble to dump the latest price information by +running from the source database:: + + django-admin dumpdata stripe_checkout.PricedItem | json_pp > prices.json + +To load price information into the target database, change your database settings and run:: + + django-admin loaddata prices.json + + +GBP Vat +####### + +GBP VAT rates are stored in the Exchange rates section. This is done automatically by the +:ref:`getexchangerate<getexchangerate>` management command that is run periodically. + +.. _manage_visitors: + + +Manage Visitors +--------------- + +A special page is made to quickly manage visitors (``/visitors``). Here it is possible to change +what a Visitor has checked out and paid for. it is also possible to clear any ``Order``\s that have +been created for the visitor. This is useful during development and testing, but should not be used +in production. Due to limitations of the Visit API, this page only shows the first 100 registered +visitors. + +.. warning:: + + Be careful, it is very easy to accidentally make a mistake when editing data through the + ``/visitors`` endpoint. This may cause visitors to be registered incorrectly and to have an + incorrect payment status + +.. _download_stripe_report: + +Download Stripe Report +---------------------- + +Finance has requested a (periodic) report of all invoices and credit notes in Stripe. Stripe does +not offer a reporting functionality sufficient for Finance, so every day a report is generated +in a csv format using the ``generatereport`` management command. \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index d1d36296d264a460a5dea85adedb26954d2c0d1b..ae7f6ae6c7593732fac9eeb125de7d302493955c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -58,7 +58,7 @@ def setup(app): # TODO: give this a better project name project = "stripe-checkout" -copyright = "2022, GÉANT Vereniging" +copyright = "2025, GÉANT Vereniging" author = "swd@geant.org" # The full version, including alpha/beta/rc tags diff --git a/docs/source/development.rst b/docs/source/development.rst new file mode 100644 index 0000000000000000000000000000000000000000..78efeab1cd0425fea4e24a81e432e70a92cee0ca --- /dev/null +++ b/docs/source/development.rst @@ -0,0 +1,194 @@ +Development +=========== + +.. _installation: + +Installation +------------ + +Install the package and dependencies:: + + pip install -r requirements.txt + pipe install -e . + +Create a ``config.json`` file:: + + cp config-example.json config.json + +Fill out the ``STRIPE_API_KEY`` and ``VISIT_API_KEY`` settings. + +Setup a local (sqlite) database:: + + python manage-dev.py migrate + python manage-dev.py createsuperuser + python manage-dev.py loaddata prices-test.json + +This will create a file ``db.sqlite`` and initialize it with the necessary tables and data. You +can now run a development server by running:: + + python manage-dev.py runserver + +.. _invoking-management-commands: + +Invoking management commands +---------------------------- +The ``manage-dev.py`` file is an entrypoint to running django management commands. The generic CLI +tool for running +`django-admin<https://docs.djangoproject.com/en/4.2/ref/django-admin/>` +This command requires at least one enviroment variable to be set ``DJANGO_SETTINGS_MODULE`` to +point to a python module for the project's settings as a dotted path. For local development, the +path should be ``stripe_checkout.settings.dev``. Inside the root of the ``stripe_checkout`` repository, +The ``manage-dev.py`` file is preconfigured with this enviroment variable, and a ``CONFIG_FILENAME`` +environment variable pointing to the ``config.json`` file created during :ref:`installation`. + +When invoking management commands in the deployed VMs you should use the ``/home/tnc/cmd.sh`` +script instead of ``django-admin``. This script is configured with the correct environment variables +for running django commands against the deployed instance. + + +Settings +-------- + +There are three ways of supplying settings. These are described below + + +Settings file +############# + +For development, use the ``stripe_checkout.settings.dev`` settings module. Deployed environments +(test, uat, prod) use the ``stripe_checkout.settings.prod`` settings module. These files contain +settings that are static between the environments, and contain some logic for reading and parsing +environment variables that are set for specific environments. + + + +.. _config_json: + + +Config file +########### + +The config file is a way to supply settings that are non-static, and therefore cannot be placed +in a settings file. The config file must be a json file containing a top level dictionary, where +the keys are settings (are therefore must be ``UPPER_CASE``) and are directly interpreted as django +settings. This allows the values to have an arbitrary structure other than a single string, such +as an array. The config file is validated to a json schema, located in ``stripe_checkout.config``. +In order to use a specific config file, set the ``CONFIG_FILENAME`` environment variable to a file +path ``path/to/config.json`` that points to a valid config file. This can be an absolute path or +a relative path. For discoverability, it is better to use an absolute path. + + +Environment Variables +##################### + +We use the ``DJANGO_SETTINGS_MODULE`` and ``CONFIG_FILENAME`` to point to the settings file +``dotted.path`` and config file ``path/to/config.json``. There are also other environment variables +for settings that cannot be static in a settings file, because they either vary between environments, +or contain sensitive information. The settings files are set up in a way to read certain enviroment +variables and use them for specific settings. + +.. note:: + Both the ``config.json`` and environment variables can be used for variable settings. When to use one + over the other is somewhat arbitrary, but there are guidelines. Settings that are relevant to the + application specifics should be placed in the ``config.json``, such as api keys and specific + stripe or visit ids. Also settings that have a more complex structure than a single string (such + as an array) are more easily set using a ``config.json`` Settings that have a more + infrastructure-like nature, or have to do with django internals, such as ``DJANGO_FRONTEND_URL`` + or ``DJANGO_SECRET_KEY`` are supplied using environment variables. Another reason to use an + environment variable is when a value needs to be processed before being assigned to the actual + setting, as is the case with the ``DATABASE_URL`` environment variable. + + +Database +-------- + +Stripe checkout needs a database to function properly. In development this is a sqlite database. +You can also connect to the test database by setting the ``DATABASE_URL`` environment variable +(substitute the username and password with the data in Lastpass):: + + export DATABASE_URL=postgres://<username>:<password>@test-postgres.geant.org/stripe_checkout + + +If you now run ``django-admin`` or ``manage-dev.py`` this will connect to the database for the +test deployment. + + +Email +----- +``stripe-checkout`` sends an email when it sees a succeeded payment intent that doesn't match an +``Order`` in the database, and therefore can't link it to a visitor's purchase. It uses Django's +`send_mail` functionality. Django uses the following settings: + +- `EMAIL_HOST` (default: `localhost`) +- `EMAIL_PORT` (default: `25`) + +It so happens postfix is configured on the ``stripe-checkout`` vm's to forward to the geant smtp +servers. So we don't need much additional configuration. We only configure the `DEFAULT_FROM_EMAIL` +setting. The recipients for these emails can be set using the ``UNPROCESSED_PAYMENT_EMAIL_TO`` +setting in the :ref:`config_json` + +.. _stripe_webhook_endpoint: + +Stripe webhook endpoint +----------------------- + +Stripe can be configured to call a `webhook endpoint <https://docs.stripe.com/webhooks>`_ for certain +Stripe events. These webhooks can be configured in the `Stripe Workbench <https://dashboard.stripe.com/workbench/webhooks>`_. +Stripe is configured to call the ``/stripe-event-webhook/`` endpoint for ``payment_intent.succeeded`` +and ``payment_intent.canceled`` webhooks (see also: :ref:`design`). There may be other events +configured as well, but these are ignored when :ref:`processing events <processevents>`. + +During development it is possible to register your locally running development server as an event +webhook using the `stripe cli <https://docs.stripe.com/stripe-cli>`_ and register a +`local listener <https://docs.stripe.com/webhooks#local-listener>`_. After setting up stripe cli and +logging in, you can hook up the local developer server with the following command:: + + stripe listen --events payment_intent.succeeded,payment_intent.canceled \ + --forward-to 127.0.0.1:8000/stripe-event-webhook/ + +Upon running this command, the stripe cli will output a stripe signing secret. This is a secret key +that stripe will use to sign the event payload so that we can be certain the events are actually +coming from Stripe. Add this secret to you ``config.json`` as the ``STRIPE_SIGNING_SECRET`` setting +and restart the development server. + +``Events``\s will now be registered in the local database. Run the following command to process these +events:: + + python manage-dev.py processevents + +.. note:: + One way the ``/stripe-event-webhook`` is secured is through the signing secret. The second way + this endpoint is secured, is that only IP addresses from stripe can actually call this endpoint. + The full list of stripe webhook IP addresses can be found + `here <https://docs.stripe.com/ips#webhook-notifications>`_. These IP addresses are placed in the + ``STRIPE_WEBHOOK_ALLOWED_IPS`` setting in the ``stripe_checkout.settings.prod`` settings module. + +.. _visit-redirect: + +Visit redirect +-------------- +The redirect to the stripe checkout server in Visit is done using a custom javascript snippet: + +1. Click the final page "Complete" +2. Click "current page settings" +3. Clikc "Edit Javascript" + +.. image:: _images/visit-form-redirect.png + :alt: Visit form redirect instructions + +Redirect URL +############ + +The redirect url is stored in the Visit custom field ``bridgeserviceurl``. To edit this, login to +Visit, open the TNC 2025 event and follow these steps: + +1. Click on Event +2. Click on Setup +3. Click on Custom fields + + +.. image:: _images/visit-bridgeserviceurl.png + :width: 700 + :alt: Visit Redirect URL + +For more information on custom fields, see the Visit `documentation <https://help.visitcloud.com/create/docs/user-guide/organisation/edit-the-organisation/#add-and-edit-custom-fields>`_ \ No newline at end of file diff --git a/docs/source/endpoints.rst b/docs/source/endpoints.rst new file mode 100644 index 0000000000000000000000000000000000000000..2333bb6e56a42c65dde8d4897a522a02917ae4d4 --- /dev/null +++ b/docs/source/endpoints.rst @@ -0,0 +1,48 @@ +Endpoints +========= + +Visitor endpoints +----------------- +The following endpoints are configured in this service + + +``/checkout/<visitor_id>/`` +########################### + +The main entry point for users. Here they can configure a PO and VAT number and confirm their +order. Upon confirmation they get redirected to Stripe. + + + +``/checkout/stripe-event-webhook/`` +################################### + +This endpoint is configured as a stripe event webhook, so that we get updated whenever +a payment (credit card or bank transfer) has succeeded and we can update the visitors Paid +status in Visit. It is secured using `Stripes signing method +<https://docs.stripe.com/webhooks#verify-official-libraries>`_. See also +:ref:`stripe_webhook_endpoint` + + + +Administrative endpoints +------------------------ +``/admin/`` +########### + +Access to the Django admin interface. Here you can add new users, and manage PricedItems and +Orders. See :ref:`django-admin` + + +``/visitors/`` +############## + +Allow for quickly setting a visitor's registration type, extras and paid status in Visit. See +:ref:`manage_visitors` + + +``/report/`` +############## + +Download a daily generated report with all invoices and credit notes in Stripe. See +:ref:`download_stripe_report` diff --git a/docs/source/gateway.rst b/docs/source/gateway.rst deleted file mode 100644 index a6c2d43ba1b716ad62e1af013a6413dc1fe84794..0000000000000000000000000000000000000000 --- a/docs/source/gateway.rst +++ /dev/null @@ -1,84 +0,0 @@ -.. gateway intro - -Visit Stripe Gateway -==================== - -This service acts as a gateway between Visit(cloud) and Stripe and its main purpose is to be able -to customize the checkout process for people registering for TNC2025 and enable the following -features: - - * Payment through bank transfer - * Addition of PO and VAT number in the invoice - -These features are not supported by Visit's implementation of the Stripe integration. This -service acts as an in-between layer and does support these features - -When visitors are registering to TNC2025 in Visit cloud, they are redirected to this service in the -final step of the registration form. Here they get the opportunity to fill out a purchase order -and/or VAT number and select wether to pay using credit card or bank transfer. They then get -redirected to the relevant stripe web page - -Design ------- -This gateway service is designed in two parts. The first part is a Django app that takes care -of user interaction, setting up the Stripe payment objects and redirecting the users. When it -creates a payment object (either a checkout Session or a PaymentIntent), it also creates a database -entry (name ``Order``) that links the stripe payment object back to the Visit visitor (ie. the -registrant). It does not directly update the visitor's paid status in Visit. Whenever the visitor -has fulfilled its payment, Stripe will call a webhook in our service (see below) to notify us of -this fact. We store this ``Event`` in our database and process it asynchronously. Stripe requires -us to handle the webhook request fast, so we cannot directly update the status in Visit. - -The second part of the design is a worker script that checks the databases for any new ``Event`` -entries, and performs the necessary updates in our database and in Visit through the Visit API. -This worker is configured as a cronjob - - - -Endpoints ---------- -The following endpoints are configured in this service - - -``/checkout/<visitor_id>/`` -########################### - -The main entry point for users. Here they can configure a PO and VAT number and confirm their -order. Upon confirmation they get redirected to Stripe. - -``/checkout/<visitor_id>/success/`` -################################### - -Used as a redirect by stripe on a successful payment. Because this endpoint not being secure, we -do not update the Visit paid status. - -``/checkout/<visitor_id>/cancel/`` -################################## - -Used as a redirect by stripe on a canceled payment - -``/checkout/stripe-event`` -########################## - -This endpoint is configured as a stripe event webhook, so that we get updated whenever -a payment (credit card or bank transfer) has succeeded and we can update the visitors Paid -status in Visit. It is secured using Stripes signing secret method (cf. -`https://docs.stripe.com/webhooks#verify-official-libraries`_ -) - - - -Administrative endpoints ------------------------- - -/visitors/ -########## - -Allow for quickly setting a visitor's shopping cart and paid items in Visit. - -/admin/ -####### - -Access to the Django admin interface. Here you can add new users, and manage PricedItems and -Orders - diff --git a/docs/source/index.rst b/docs/source/index.rst index 18e0e51d284e9b1758eae7c82a4e9cb1e0c7c65a..e51ecc4bf0227698e0a09a1b8b598439dbea964d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,9 +1,11 @@ stripe_checkout ======================================== -Placeholder for docs .. toctree:: :maxdepth: 2 :caption: Contents: - gateway + introduction + endpoints + administration + development \ No newline at end of file diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst new file mode 100644 index 0000000000000000000000000000000000000000..e87e523e78b6b8ecdbbf40bd4fac6d6fa597319e --- /dev/null +++ b/docs/source/introduction.rst @@ -0,0 +1,150 @@ +.. _introduction: + +Introduction +============ + +This service acts as a gateway between Visit(cloud) and Stripe and its main purpose is to be able +to customize the checkout process for people registering for TNC2025 and enable the following +features: + +* Payment through bank transfer or credit card +* Customization of the invoice + + * allow the visitors to set a PO number and VAT number + * add the VAT in GBP as well as EUR + * Use a specific invoice template + +These features are not supported by Visit's implementation of the Stripe integration. This +service acts as an in-between layer and does support these features + +When visitors are registering to TNC2025 in Visit cloud, they are redirected to this service in the +final step of the registration form. Here they get the opportunity to fill out a purchase order +and/or VAT number. Upon clicking a confirmation button an invoice is created for them and they are +redirected to Stripe where they can view the invoice, pay directly by credit card and/or see +payment instructions for paying by bank transfer. They also receive an e-mail with all payment +information +`` + + +.. note:: + Throughout this documentation, there are instructions to invoke django management commands. These + instructions use the ``django-admin`` cli tool to invoke these commands. Depending on the + situation you may want to use another script to invoke these commands, such as ``manage-dev.py`` + or ``cmd.sh``. See also :ref:`invoking-management-commands` + + +.. _design: + +Design +------ + +The design of the stripe-checkout service is roughly as following: + +Whenever a visitor registers for TNC, they fill out a form in Visit. This form sets their + ``registrationType``. While filling out the form, the visitor may also want to purchase some extras, + such as access to side meetings or social events. The information for extras is stored in the + visitors profile as answers to certain questions. For example, there is a question for "Side + meetings" with answers "Friday" and "Sunday" + +* As the final page in the registration form, the visitor is redirected to the stripe checkout + service. Here they see what they've checked out and how much they need to pay. + + * The checkout service reads the visitor's profile through the Visit API (the visitor is + identified by their visitor id in the url). + * From the visitor's profile, the Stripe products that the visitor needs to pay for are + determined. A visitor can have a single Registration Type item and multiple extras. Every + category of extras has two questions: One that the visitor answers with the extras they want, + and one that is goverened by the stripe checkout service when the visitor's payment has been + processed. Every answer in the paid-questions represents an extra that the visitor has paid for. + + These are defined as ``PricedItem`` objects in the database. See also: :ref:`adding-product-info` + * All items that the visitor has not previously purchased (see ``Order`` below) are added to a + ``ShoppingCart`` and this collection is shown to the user in their checkout page + +* In their checkout page, the visitor can add some additional information that will be shown on + the invoice. These are a Purchase Order (PO) number and/or a VAT id. These fields are optional. + Then the visitor clicks Confirm to finalize the purchase +* The stripe checkout service now creates an invoice in Stripe for the items that they need to pay + for: + + * All ``PricedItem`` objects that the visitor needs to pay for are collected in an ``Order`` and + stored in the database. Any items for which the visitor already has an ``Order`` are excluded from + the new ``Order``, so that the visitor is charged only once for an item. + * The visitor's billing address is read from their Visit profile. ``contact.addresses`` should + contain an address object of ``"type": "billing"``. + + .. note:: + + In order for any billing information to be + shown in the Invoice, it is required that the ``country`` field is set to a valid country + code in the billing address. + + * The visitor is identified in stripe by their email address. If the visitor's email address + is already bound to a customer in Stripe, we use that customer, but update the billing details + using their Visit profile + * Additional fields are added to the the invoice as ``custom_fields``. These are: + + * Purchase Order: Supplied by the visitor (optional) + * VAT number: Supplied by the visitor (optional) + * GBP VAT: The VAT amount in GBP. The exchange rate read from the database and is periodically + updated by management command ``getexchangerate`` + + * We store the ``visitor_id`` and ``order_id`` as metadata fields to the invoice. That way we can + always find back the order if we have the Stripe invoice. + * We tell Stripe to finalize and send the invoice by email to the visitor. Stripe also generates + a url where the visitor can directly pay their invoice. We redirect the user to this url. The + visitor facing part of the stripe checkout service is now finished. + * When creating and finalizing the Invoice, Stripe also creates a payment intent. Before + redirecting the visitor to their Stripe payment page, we store the payment intent's id + (starting with ``pi_``) along with the ``Order`` so that we can reference it later. + +* We now wait for the visitor to pay. The visitor may pay by credit card or by bank transfer. While + credit card payments are processed almost instantly, bank transfers may take time to be + processed. First, the visitor needs to actually pay, and then Stripe needs to process the + payment. The total time between placing the order and strip processing the payment may take weeks + or longer. +* When the visitor's payment has been succesfully processed by Stripe, Stripe calls our webhook at + the ``stripe-event-webhook/`` endpoint. This endpoint does not process the event, but merely stores + it in the database as an Event. Stripe requires that this endpoint returns quickly so processing + the events happens asynchronously by periodically running the django managemement command + ``processevents`` + +.. _processevents: + +Processing stripe events +######################## + +* The ``processevents`` command looks in the database for Events that are not processed yet. It + looks at two types of events: ``payment_intent.succeeded`` and ``payment_intent.canceled``. + + * ``payment_intent.succeeded`` events indicate that the visitor has paid. We look in the ``Order`` + table for a matching ``Order``, and update the visitor's Profile in Visit. We add the ``PAID`` + tag to the visitor to indicate that they've paid for their main registration, and add the relevant + answers to the "paid"-question for extra's. In certain cases there is no ``Order`` that is + linked to the payment_intent. This can happen in the following circumstances and we handle + these differently + + * If during processing of the invoice, the invoice (and payment intent) gets created, but for + some reason we are unable to write back the payment intent id to the ``Order``, we end up + with an ``Order`` without a payment intent reference. In this case we do a reverse lookup. We + retrieve the invoice from the payment intent and look in the metadata for a ``visitor_id`` and + and ``order_id``. If we can find an order that match those parameters we use it and continue + as normal + * If the original invoice was credited and another invoice was created manually, this has a new + payment intent attached to it. The ``Order`` now has an incorrect payment intent reference. + We try the reverse lookup using the invoice's metadata, but if that fails, we send an email + with a notification that we encountered a payment intent that we cannot process. The + payment_intent / invocie must be processed manually + + * ``payment_intent.canceled`` events happen when an invoice was credited. We add the + ``CANCELLED`` tag to the visitor, to indicate that we now no longer expect them to pay + +.. _getexchangerate: + +Fetching exchange rates +####################### +* Another job that runs periodically is the ``getexchangerate`` command. Every day it looks for a + new GBP/EUR exchange rate on + `https://www.trade-tariff.service.gov.uk/exchange_rates <https://www.trade-tariff.service.gov.uk/exchange_rates>`_ + and if it finds one, adds it to the database to be used from now on. The trade-tariff service + publishes the exchange rates every month. diff --git a/prices-live.json b/prices-live.json index 111c7b5a90f39152f76bd964e9bfae4ab6919f6f..a5652dc1b3e3e35c341d81e50e9ae3db6cd295f7 100644 --- a/prices-live.json +++ b/prices-live.json @@ -49,7 +49,7 @@ "display_name" : "Side meeting (Friday)", "kind" : "EXTRA", "name" : "side_meeting_pass_friday", - "stripe_product_id" : "prod_RZw4eSzXLC1A2N", + "stripe_product_id" : "prod_RZw4cMh6B1tL3T", "visit_checkout_answer_id" : "1zeb3k6yy", "visit_checkout_question_id" : "1zeb2vc8p", "visit_paid_answer_id" : "1zen12x8z", @@ -64,7 +64,7 @@ "display_name" : "Side meeting (Monday)", "kind" : "EXTRA", "name" : "side_meeting_pass_monday", - "stripe_product_id" : "prod_RPrt4MzT2m98d2", + "stripe_product_id" : "prod_RZw4eSzXLC1A2N", "visit_checkout_answer_id" : "1zeb3k6yx", "visit_checkout_question_id" : "1zeb2vc8p", "visit_paid_answer_id" : "1zen12x8y", @@ -118,5 +118,20 @@ }, "model" : "stripe_checkout.priceditem", "pk" : 9 + }, + { + "fields" : { + "display_name" : "PC Pass", + "kind" : "REGISTRATION_TYPE", + "name" : "pc_pass", + "stripe_product_id" : "prod_Ria6hn9u7weTad", + "visit_checkout_answer_id" : "", + "visit_checkout_question_id" : "", + "visit_paid_answer_id" : "", + "visit_paid_question_id" : "", + "visit_registration_id" : "1zeurvsr3" + }, + "model" : "stripe_checkout.priceditem", + "pk" : 10 } ] diff --git a/setup.py b/setup.py index 9caed8a00265d611494318df2fe077f549b6aecd..081cced6759ff1beacb6c2241045174aec6838b2 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="stripe-checkout", - version="0.9", + version="0.10", author="GEANT", author_email="swd@geant.org", description="Stripe custom checkout support service", diff --git a/stripe_checkout/config.py b/stripe_checkout/config.py index 94400c3a28f28cbfd11689945dd9acc614b53fc1..bda88c5a1e815a5632721af44862a354c4d25974 100644 --- a/stripe_checkout/config.py +++ b/stripe_checkout/config.py @@ -13,6 +13,10 @@ CONFIG_SCHEMA = { "STRIPE_TAX_RATE_ID": {"type": "string"}, "VISIT_API_KEY": {"type": "string"}, "VISIT_EXPO_ID": {"type": "string"}, + "UNPROCESSED_PAYMENT_EMAIL_TO": { + "type": "array", + "items": {"type": "string"}, + }, }, "required": [ "STRIPE_API_KEY", diff --git a/stripe_checkout/stripe_checkout/management_views.py b/stripe_checkout/stripe_checkout/management_views.py index 5329256c8126df989e0a46051de017c58cdd8eb2..9639f3b7aaf77ae47a70c8702cb0a9c7bfff1dc2 100644 --- a/stripe_checkout/stripe_checkout/management_views.py +++ b/stripe_checkout/stripe_checkout/management_views.py @@ -1,7 +1,7 @@ from typing import Sequence from django.http import Http404, HttpResponse from django.shortcuts import redirect, render -from django.contrib.auth.decorators import login_required +from django.contrib.auth.decorators import login_required, permission_required from stripe_checkout.stripe_checkout.models import ItemKind, Order, PricedItem, Report from . import visit @@ -14,6 +14,7 @@ def index(request): @login_required +@permission_required("stripe_checkout.manage_visitors") @require_GET def list_visitors(request): api = visit.VisitorAPI() @@ -37,6 +38,7 @@ def list_visitors(request): @login_required +@permission_required("stripe_checkout.manage_visitors") @require_POST def update_visitor(request, visitor_id): all_extras: Sequence[PricedItem] = ( @@ -65,6 +67,7 @@ def update_visitor(request, visitor_id): @login_required +@permission_required("stripe_checkout.manage_visitors") @require_POST def delete_orders(request, visitor_id): Order.objects.filter(visitor_id=visitor_id).delete() @@ -72,6 +75,7 @@ def delete_orders(request, visitor_id): @login_required +@permission_required("stripe_checkout.view_report") @require_GET def get_report(request): report = Report.objects.first() diff --git a/stripe_checkout/stripe_checkout/migrations/0009_alter_order_options.py b/stripe_checkout/stripe_checkout/migrations/0009_alter_order_options.py new file mode 100644 index 0000000000000000000000000000000000000000..e27d1b20bdb17a8326694b06688d2781e2c83afa --- /dev/null +++ b/stripe_checkout/stripe_checkout/migrations/0009_alter_order_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.18 on 2025-02-13 14:27 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("stripe_checkout", "0008_order_created_alter_order_stripe_id"), + ] + + operations = [ + migrations.AlterModelOptions( + name="order", + options={"permissions": (("manage_visitors", "Manage visitors"),)}, + ), + ] diff --git a/stripe_checkout/stripe_checkout/models.py b/stripe_checkout/stripe_checkout/models.py index 3bbd90adda9732c33d0eda4981157ace681331cf..2276ef9c773ef157b066570d286ecf4bfdadf2da 100644 --- a/stripe_checkout/stripe_checkout/models.py +++ b/stripe_checkout/stripe_checkout/models.py @@ -83,6 +83,9 @@ class Order(models.Model): ) created = models.DateTimeField(auto_now_add=True) + class Meta: + permissions = (("manage_visitors", "Manage visitors"),) + class Event(models.Model): payload = models.JSONField(blank=False) diff --git a/stripe_checkout/stripe_checkout/templates/index.html b/stripe_checkout/stripe_checkout/templates/index.html index d5baaf75745acc9ee62f95e6c9d7275352ed59c7..69b22e3ba73a00b0ad8fbbc68a40b12903f52e9f 100644 --- a/stripe_checkout/stripe_checkout/templates/index.html +++ b/stripe_checkout/stripe_checkout/templates/index.html @@ -4,14 +4,20 @@ {% endblock title %} {% block body %} <ul> - <li> - <a href="{% url 'admin:index' %}">Admin</a> - </li> - <li> - <a href="{% url 'stripe_checkout:list-visitors' %}">Manage visitors</a> - </li> - <li> - <a href="{% url 'stripe_checkout:get-report' %}">Download Stripe Report</a> - </li> + {% if user.is_superuser %} + <li> + <a href="{% url 'admin:index' %}">Admin</a> + </li> + {% endif %} + {% if perms.stripe_checkout.manage_visitors %} + <li> + <a href="{% url 'stripe_checkout:list-visitors' %}">Manage visitors</a> + </li> + {% endif %} + {% if perms.stripe_checkout.view_report %} + <li> + <a href="{% url 'stripe_checkout:get-report' %}">Download Stripe Report</a> + </li> + {% endif %} </ul> {% endblock body %} diff --git a/stripe_checkout/stripe_checkout/templates/invoice-created.html b/stripe_checkout/stripe_checkout/templates/invoice-created.html deleted file mode 100644 index 6fd681a95162a8a6dee33a432e70621cccf090a3..0000000000000000000000000000000000000000 --- a/stripe_checkout/stripe_checkout/templates/invoice-created.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends "tnc-base.html" %} -{% block title %} - Invoice sent -{% endblock title %} -{% block content %} - <div class="checkout flex center"> - <div class="card flex center column"> - <p> - Thank you for registering for TNC 2025! You should soon receive an email with an - invoice for your registration as well as payment instructions. If not, please check - your junk mail. After you have successfully paid the invoice, your registration - will be completed. - </p> - </div> - </div> -{% endblock content %} diff --git a/stripe_checkout/stripe_checkout/templates/visitors.html b/stripe_checkout/stripe_checkout/templates/visitors.html index 2376d6a6d1fb4f013b858646cbc104a57799257d..9985eba966070cdb7a0a9c948450630ee8779af5 100644 --- a/stripe_checkout/stripe_checkout/templates/visitors.html +++ b/stripe_checkout/stripe_checkout/templates/visitors.html @@ -21,7 +21,7 @@ {% for visitor in visitors %} <tr> <td> - <a href="{% url 'stripe_checkout:checkout-entrypoint' visitor.id %}" + <a href="{% url 'stripe_checkout:checkout' visitor.id %}" target="_blank">{{ visitor.id }}</a> </td> <td>{{ visitor.contact.firstName }} {{ visitor.contact.lastName }}</td> diff --git a/stripe_checkout/stripe_checkout/urls.py b/stripe_checkout/stripe_checkout/urls.py index dffd24314a944f020440b36aee085211df144525..373752eea232b71d6ed69d911decfef943ec8818 100644 --- a/stripe_checkout/stripe_checkout/urls.py +++ b/stripe_checkout/stripe_checkout/urls.py @@ -7,7 +7,7 @@ from .management_views import ( list_visitors, update_visitor, ) -from .visit_views import checkout, checkout_success, stripe_event +from .visit_views import checkout, stripe_event visitor_id = r"(?P<visitor_id>[a-z0-9]+)" @@ -26,15 +26,7 @@ urlpatterns = [ ), ), path("report/", get_report, name="get-report"), - re_path( - f"checkout/{visitor_id}/", - include( - [ - path("success/", checkout_success, name="checkout-success"), - path("", checkout, name="checkout-entrypoint"), - ] - ), - ), + re_path(f"checkout/{visitor_id}/", checkout, name="checkout"), path("stripe-event-webhook/", stripe_event, name="stripe-event"), path("", index, name="index"), ] diff --git a/stripe_checkout/stripe_checkout/visit_views.py b/stripe_checkout/stripe_checkout/visit_views.py index 05a908699f4cb121908e87c470b76d3aa099ac7f..85d7c3960dc9cb25e361b4928bd3de5ac38fb12f 100644 --- a/stripe_checkout/stripe_checkout/visit_views.py +++ b/stripe_checkout/stripe_checkout/visit_views.py @@ -6,7 +6,7 @@ from django import forms from django.http import Http404, HttpResponse from django.shortcuts import redirect, render from django.views.decorators.csrf import csrf_exempt -from django.views.decorators.http import require_GET, require_http_methods, require_POST +from django.views.decorators.http import require_http_methods, require_POST from . import stripe_ as stripe from .models import Event, ExchangeRate @@ -31,9 +31,7 @@ def checkout(request, visitor_id): status_code = 200 if has_outstanding_order(visitor_id): if request.POST: - response = redirect( - "stripe_checkout:checkout-entrypoint", visitor_id=visitor_id - ) + response = redirect("stripe_checkout:checkout", visitor_id=visitor_id) return response else: return render( @@ -90,21 +88,6 @@ def create_invoice(visitor, data, shopping_cart, order): ) -@require_GET -def checkout_success(request, visitor_id): - # Here we just show a success message. Updating visit will be handled - # asynchronously by the stripe event webhook handler. This endpoint is not - # sufficiently secure to update payment status - return render( - request, - "invoice-created.html", - context={ - "visitor_id": visitor_id, - "body_classes": "tnc-background", - }, - ) - - @csrf_exempt @require_POST @whitelist_ips(by_setting="STRIPE_WEBHOOK_ALLOWED_IPS") diff --git a/test/test_visit.py b/test/test_visit.py index 56635ce47abb0f995494cd416112414de478f4c0..6ed9f552262f696919e39b5a0b9f51dcf69cb56e 100644 --- a/test/test_visit.py +++ b/test/test_visit.py @@ -3,13 +3,14 @@ from unittest.mock import patch import pytest import responses import stripe - +from django.contrib.auth.models import Permission from stripe_checkout.stripe_checkout.models import Event, Order @responses.activate @pytest.mark.django_db def test_list_visitors(client, user): + user.user_permissions.add(Permission.objects.get(codename="manage_visitors")) client.force_login(user) rv = client.get("/visitors/") assert rv.status_code == 200