Skip to content
Snippets Groups Projects
Commit 73e9c60e authored by geant-release-service's avatar geant-release-service
Browse files

Finished release 0.1.

parents 1d3db9d6 e3af6f95
Branches
Tags 0.1
No related merge requests found
Showing with 732 additions and 1 deletion
*.egg-info
__pycache__
coverage.xml
.coverage
htmlcov
.tox
dist
node_modules
docs/build
.idea
.vscode
config.json
db.sqlite
# Changelog
All notable changes to this project will be documented in this file.
## [0.1] - 2025-01-09
- first build
recursive-include stripe_checkout/**/templates/ *
recursive-include stripe_checkout/**/static/ *
......@@ -2,4 +2,78 @@
Service for adding custom steps for Stipe checkout.
Used to support vizitorz
This service acts as glue 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
# 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
```
{
"STRIPE_API_KEY": "sk_test_Gx4mWEgHtCMr4DYMUIqfIrsz",
"STRIPE_SIGNING_SECRET": "<retrieve this from the stripe workbench webhook destination details>",
"STRIPE_INVOICE_TEMPLATE_ID":"inrtem_1QfJ5aDSpyjzuj5pVVTuINiG",
"STRIPE_TAX_RATE_ID": "txr_1QeddlDSpyjzuj5pPwUcMwTd",
"VISIT_API_KEY": "<visit api key>",
"VISIT_EXPO_ID":"18lm2fafttito"
}
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
from importlib import import_module
from docutils.parsers.rst import Directive
from docutils import nodes
from sphinx import addnodes
import json
import os
import sys
sys.path.insert(
0,
os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "..", "stripe_checkout")
),
)
class RenderAsJSON(Directive):
# cf. https://stackoverflow.com/a/59883833
required_arguments = 1
def run(self):
module_path, member_name = self.arguments[0].rsplit(".", 1)
member_data = getattr(import_module(module_path), member_name)
code = json.dumps(member_data, indent=2)
literal = nodes.literal_block(code, code)
literal["language"] = "json"
return [
addnodes.desc_name(text=member_name),
addnodes.desc_content("", literal),
]
def setup(app):
app.add_directive("asjson", RenderAsJSON)
# -- Project information -----------------------------------------------------
# TODO: give this a better project name
project = "stripe-checkout"
copyright = "2022, GÉANT Vereniging"
author = "swd@geant.org"
# The full version, including alpha/beta/rc tags
release = "0.0"
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ["sphinx_rtd_theme", "sphinx.ext.autodoc", "sphinx.ext.coverage"]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "sphinx_rtd_theme"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
# Both the class’ and the __init__ method’s docstring
# are concatenated and inserted.
autoclass_content = "both"
autodoc_typehints = "none"
.. 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
stripe_checkout
========================================
Placeholder for docs
.. toctree::
:maxdepth: 2
:caption: Contents:
gateway
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "stripe_checkout.settings.dev")
os.environ.setdefault("CONFIG_FILENAME", "config.json")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()
[
{
"fields" : {
"display_name" : "TNC25 Regular Pass",
"kind" : "REGISTRATION_TYPE",
"name" : "regular_pass",
"stripe_product_id" : "prod_RPrkkdagTlupTx",
"visit_checkout_answer_id" : "",
"visit_checkout_question_id" : "",
"visit_paid_answer_id" : "",
"visit_paid_question_id" : "",
"visit_registration_id" : "1zdvtaxrc"
},
"model" : "stripe_checkout.priceditem",
"pk" : 1
},
{
"fields" : {
"display_name" : "TNC25 Speaker Pass",
"kind" : "REGISTRATION_TYPE",
"name" : "speaker_pass",
"stripe_product_id" : "prod_RPrlxnse8ue5xL",
"visit_checkout_answer_id" : "",
"visit_checkout_question_id" : "",
"visit_paid_answer_id" : "",
"visit_paid_question_id" : "",
"visit_registration_id" : "1zdvtaxrf"
},
"model" : "stripe_checkout.priceditem",
"pk" : 2
},
{
"fields" : {
"display_name" : "TNC25 Sponsor Pass",
"kind" : "REGISTRATION_TYPE",
"name" : "sponsor_pass",
"stripe_product_id" : "prod_RPrynekYplHMGI",
"visit_checkout_answer_id" : "",
"visit_checkout_question_id" : "",
"visit_paid_answer_id" : "",
"visit_paid_question_id" : "",
"visit_registration_id" : "1zegwg39v"
},
"model" : "stripe_checkout.priceditem",
"pk" : 4
},
{
"fields" : {
"display_name" : "Side meeting (Friday)",
"kind" : "EXTRA",
"name" : "side_meeting_pass_friday",
"stripe_product_id" : "prod_RPrtXeSF2SGZKL",
"visit_checkout_answer_id" : "1zeb3k6yy",
"visit_checkout_question_id" : "1zeb2vc8p",
"visit_paid_answer_id" : "1zen12x8z",
"visit_paid_question_id" : "1zen0dy45",
"visit_registration_id" : ""
},
"model" : "stripe_checkout.priceditem",
"pk" : 5
},
{
"fields" : {
"display_name" : "Side meeting (Monday)",
"kind" : "EXTRA",
"name" : "side_meeting_pass_monday",
"stripe_product_id" : "prod_RPrt4MzT2m98d2",
"visit_checkout_answer_id" : "1zeb3k6yx",
"visit_checkout_question_id" : "1zeb2vc8p",
"visit_paid_answer_id" : "1zen12x8y",
"visit_paid_question_id" : "1zen0dy45",
"visit_registration_id" : ""
},
"model" : "stripe_checkout.priceditem",
"pk" : 6
},
{
"fields" : {
"display_name" : "Social Pass (Tuesday)",
"kind" : "EXTRA",
"name" : "social_pass_tuesday",
"stripe_product_id" : "price_1QX2JCDSpyjzuj5pMEiQwFuO",
"visit_checkout_answer_id" : "",
"visit_checkout_question_id" : "",
"visit_paid_answer_id" : "",
"visit_paid_question_id" : "",
"visit_registration_id" : ""
},
"model" : "stripe_checkout.priceditem",
"pk" : 7
},
{
"fields" : {
"display_name" : "Social Pass (Wednesday)",
"kind" : "EXTRA",
"name" : "social_pass_wednesday",
"stripe_product_id" : "price_1QX2KNDSpyjzuj5pDNrkoH0x",
"visit_checkout_answer_id" : "",
"visit_checkout_question_id" : "",
"visit_paid_answer_id" : "",
"visit_paid_question_id" : "",
"visit_registration_id" : ""
},
"model" : "stripe_checkout.priceditem",
"pk" : 8
},
{
"fields" : {
"display_name" : "Social Pass (Combined)",
"kind" : "EXTRA",
"name" : "social_pass_all",
"stripe_product_id" : "price_1QX2MJDSpyjzuj5pFUbQUyV4",
"visit_checkout_answer_id" : "",
"visit_checkout_question_id" : "",
"visit_paid_answer_id" : "",
"visit_paid_question_id" : "",
"visit_registration_id" : ""
},
"model" : "stripe_checkout.priceditem",
"pk" : 9
}
]
setup.py 0 → 100644
from setuptools import setup, find_packages
setup(
name="stripe-checkout",
version="0.1",
author="GEANT",
author_email="swd@geant.org",
description="Stripe custom checkout support service",
url=("TBD"),
packages=find_packages(),
install_requires=[
"django>=5.1",
"dj_database_url==2.3.0", # pin for security
"jsonschema",
"stripe",
"requests",
"click",
],
extras_require={
"prod": [
"gunicorn",
"uvicorn",
"psycopg2",
],
},
entry_points={
"console_scripts": [
"visit=stripe_checkout.visit.cli:cli",
]
},
include_package_data=True,
)
"""
ASGI config for stripe_checkout project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
"""
from django.core.asgi import get_asgi_application
application = get_asgi_application()
import json
import pathlib
import jsonschema
CONFIG_SCHEMA = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"STRIPE_API_KEY": {"type": "string"},
"STRIPE_SIGNING_SECRET": {"type": "string"},
"STRIPE_INVOICE_TEMPLATE_ID": {"type": "string"},
"STRIPE_TAX_RATE_ID": {"type": "string"},
"VISIT_API_KEY": {"type": "string"},
"VISIT_EXPO_ID": {"type": "string"},
},
"required": [
"STRIPE_API_KEY",
"STRIPE_INVOICE_TEMPLATE_ID",
"STRIPE_TAX_RATE_ID",
"VISIT_API_KEY",
"VISIT_EXPO_ID",
],
}
def load_config(filename, settings=None):
config = json.loads(pathlib.Path(filename).read_text())
jsonschema.validate(config, CONFIG_SCHEMA)
if settings is None:
return config
if hasattr(type(settings), "update"):
settings.update(config)
else:
for k, v in config.items():
setattr(settings, k, v)
"""
Django settings for stripe_checkout project.
Generated by 'django-admin startproject' using Django 5.1.4.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
import os
from pathlib import Path
import dj_database_url
from stripe_checkout.config import load_config
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parents[-2]
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.messages",
"django.contrib.sessions",
"django.contrib.staticfiles",
"stripe_checkout.stripe_checkout",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "stripe_checkout.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "stripe_checkout.wsgi.application"
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", # noqa E501
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = "static/"
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# DATABASE_URL should be something like
# postgresql://<username>:<password>@test-postgres.geant.org:5432/<database>
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///db.sqlite")
if DATABASE_URL:
DATABASES = {
"default": dj_database_url.parse(DATABASE_URL),
}
del DATABASE_URL
CONFIG_FILENAME = os.getenv("CONFIG_FILENAME")
if CONFIG_FILENAME:
load_config(CONFIG_FILENAME, globals())
del CONFIG_FILENAME
LOGIN_URL = "/admin/login/"
LOGIN_REDIRECT_URL = "/"
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
},
"root": {
"handlers": ["console"],
"level": "INFO",
},
}
from .base import * # noqa: F401, F403
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
SECRET_KEY = "django-insecure-&_5fsv$%$hkv)$sjnt$+01vzzyur)$so*kjc&6vzm6rv%l%r+2"
from .base import * # noqa: F401, F403
import os
import dj_database_url
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
},
}
# Django settings for ensuring that we correctly identify https being used
USE_X_FORWARDED_HOST = True
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY")
ALLOWED_HOSTS = [os.getenv("DJANGO_ALLOWED_HOST")]
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
# DATABASE_URL must be something like
# postgresql://<username>:<password>@test-postgres.geant.org:5432/<database>
DATABASE_URL = os.environ["DATABASE_URL"]
DATABASES = {
"default": dj_database_url.parse(DATABASE_URL),
}
STATIC_URL = os.getenv("STATIC_URL", "/static/") # noqa: F405
STATIC_ROOT = os.getenv("STATIC_ROOT", "staticfiles/") # noqa: F405
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment