Compare commits

..

No commits in common. "4c810efe6c7709a67a737271f79fe49b2f764077" and "298a453e192f03d90935a7733a57458bc91955b2" have entirely different histories.

11 changed files with 44 additions and 99 deletions

View file

@ -1,31 +1,20 @@
FROM python:3.12-slim-bookworm FROM python:3.12-slim-bullseye
# PYTHONFAULTHANDLER: Propagate tracebacks from all threads.
# PYTHONUNBUFFERED: Write terminal output straight to docker (to not confuse Docker Compose).
# PYTHONDONTWRITEBYTECODE: Dont write *pyc files at all, making it possible for a 100% read-only container.
# PIP_NO_CACHE_DIR: Disable PIP cache, we don't need pip's cache after building the image.
# PIP_DISABLE_PIP_VERSION_CHECK: Build the image with the available pip, do not check for updates (faster!)
# PIP_DEFAULT_TIMEOUT: Allow for longer timeouts.
ENV PYTHONFAULTHANDLER=1 \ ENV PYTHONFAULTHANDLER=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \ PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1 \ PYTHONHASHSEED=random \
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \ PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 PIP_DEFAULT_TIMEOUT=100
ARG BUILD ARG BUILD
ENV BUILD=${BUILD} ENV BUILD ${BUILD}
ARG REQUIREMENTS_FILE=requirements.txt ARG REQUIREMENTS_FILE=requirements.txt
WORKDIR /app WORKDIR /app
RUN groupadd -g 1000 www && useradd -u 1000 -ms /bin/bash -g www www RUN groupadd -g 1000 www && useradd -u 1000 -ms /bin/bash -g www www
RUN apt-get update && \
# Only copy the requirements file first to leverage Docker cache
COPY $REQUIREMENTS_FILE .
RUN mkdir -p /app/src/static && \
chown www:www /app/src/static && \
apt-get update && \
apt-get install -y \ apt-get install -y \
binutils \ binutils \
libpq-dev \ libpq-dev \
@ -37,12 +26,14 @@ RUN mkdir -p /app/src/static && \
libgdk-pixbuf2.0-0 \ libgdk-pixbuf2.0-0 \
libffi-dev \ libffi-dev \
shared-mime-info \ shared-mime-info \
gettext && \ gettext
pip install --no-cache-dir -r $REQUIREMENTS_FILE
# Copy the rest of the application COPY --chown=www:www . .
COPY . .
RUN mkdir /app/src/static && \
chown www:www /app/src/static
RUN pip install --no-cache-dir -r $REQUIREMENTS_FILE
RUN django-admin compilemessages RUN django-admin compilemessages
ENTRYPOINT ["./entrypoint.sh"] ENTRYPOINT ["./entrypoint.sh"]

View file

@ -98,13 +98,3 @@ hatch-pip-compile
# Update requirements/requirements-dev.txt: # Update requirements/requirements-dev.txt:
hatch-pip-compile dev hatch-pip-compile dev
``` ```
#### Updating requirements
If you want to update the requirements, you can run the following command:
```bash
hatch run requirements
```
This uses [hatch-pip-compile](https://juftin.com/hatch-pip-compile/) to update the requirements.

View file

@ -13,16 +13,16 @@ authors = [
] ]
dependencies = [ dependencies = [
"Django>=5.1b1,<5.2", "Django>=5.1b1,<5.2",
"django-money~=3.5", "django-money==3.5.2",
"django-allauth~=0.63", "django-allauth==0.63.6",
"psycopg[binary]~=3.2", "psycopg[binary]==3.2.1",
"environs[django]>=11,<12", "environs[django]==11.0.0",
"uvicorn~=0.30", "uvicorn==0.30.1",
"whitenoise~=6.7", "whitenoise==6.7.0",
"django-zen-queries~=2.1", "django-zen-queries==2.1.0",
"django-registries==0.0.3", "django-registries==0.0.3",
"django-view-decorator==0.0.4", "django-view-decorator==0.0.4",
"django-oauth-toolkit~=2.4", "django-oauth-toolkit==2.4.0",
"django_stubs_ext~=5.0", "django_stubs_ext~=5.0",
"stripe~=10.5", "stripe~=10.5",
] ]
@ -64,7 +64,7 @@ matrix.python.dependencies = [
{ value = "typing_extensions==4.5.0", if = ["3.10"]}, { value = "typing_extensions==4.5.0", if = ["3.10"]},
] ]
[tool.hatch.envs.default.scripts] [tool.hatch.envs.dev.scripts]
cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=src --cov=tests --cov=append {args}" cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=src --cov=tests --cov=append {args}"
no-cov = "cov --no-cov {args}" no-cov = "cov --no-cov {args}"
typecheck = "mypy --config-file=pyproject.toml ." typecheck = "mypy --config-file=pyproject.toml ."
@ -74,8 +74,6 @@ migrate = "./src/manage.py migrate"
makemigrations = "./src/manage.py makemigrations" makemigrations = "./src/manage.py makemigrations"
createsuperuser = "./src/manage.py createsuperuser" createsuperuser = "./src/manage.py createsuperuser"
shell = "./src/manage.py shell" shell = "./src/manage.py shell"
# You need to install Stripe CLI from here to run this: https://github.com/stripe/stripe-cli/releases
stripe_cli = "stripe listen --forward-to 0.0.0.0:8000/order/stripe/webhook/"
[tool.pytest.ini_options] [tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE="tests.settings" DJANGO_SETTINGS_MODULE="tests.settings"

View file

@ -16,7 +16,7 @@ charset-normalizer==3.3.2
# via requests # via requests
click==8.1.7 click==8.1.7
# via uvicorn # via uvicorn
cryptography==43.0.0 cryptography==42.0.8
# via jwcrypto # via jwcrypto
dj-database-url==2.2.0 dj-database-url==2.2.0
# via environs # via environs
@ -97,7 +97,7 @@ typing-extensions==4.12.2
# stripe # stripe
urllib3==2.2.2 urllib3==2.2.2
# via requests # via requests
uvicorn==0.30.4 uvicorn==0.30.1
# via membersystem (pyproject.toml) # via membersystem (pyproject.toml)
whitenoise==6.7.0 whitenoise==6.7.0
# via membersystem (pyproject.toml) # via membersystem (pyproject.toml)

View file

@ -11,19 +11,19 @@
# - django-debug-toolbar==4.2.0 # - django-debug-toolbar==4.2.0
# - django-browser-reload==1.7.0 # - django-browser-reload==1.7.0
# - model-bakery==1.17.0 # - model-bakery==1.17.0
# - django-allauth~=0.63 # - django-allauth==0.63.6
# - django-money~=3.5 # - django-money==3.5.2
# - django-oauth-toolkit~=2.4 # - django-oauth-toolkit==2.4.0
# - django-registries==0.0.3 # - django-registries==0.0.3
# - django-stubs-ext~=5.0 # - django-stubs-ext~=5.0
# - django-view-decorator==0.0.4 # - django-view-decorator==0.0.4
# - django-zen-queries~=2.1 # - django-zen-queries==2.1.0
# - django<5.2,>=5.1b1 # - django<5.2,>=5.1b1
# - environs[django]<12,>=11 # - environs[django]==11.0.0
# - psycopg[binary]~=3.2 # - psycopg[binary]==3.2.1
# - stripe~=10.5 # - stripe~=10.5
# - uvicorn~=0.30 # - uvicorn==0.30.1
# - whitenoise~=6.7 # - whitenoise==6.7.0
# #
asgiref==3.8.1 asgiref==3.8.1
@ -49,7 +49,7 @@ coverage==7.3.0
# hatch.envs.dev # hatch.envs.dev
# coverage # coverage
# pytest-cov # pytest-cov
cryptography==43.0.0 cryptography==42.0.8
# via jwcrypto # via jwcrypto
dj-database-url==2.2.0 dj-database-url==2.2.0
# via environs # via environs
@ -86,7 +86,7 @@ django-registries==0.0.3
# via hatch.envs.dev # via hatch.envs.dev
django-stubs==1.16.0 django-stubs==1.16.0
# via hatch.envs.dev # via hatch.envs.dev
django-stubs-ext==5.0.4 django-stubs-ext==5.0.2
# via # via
# hatch.envs.dev # hatch.envs.dev
# django-stubs # django-stubs
@ -166,7 +166,7 @@ tomli==2.0.1
# via django-stubs # via django-stubs
types-pytz==2024.1.0.20240417 types-pytz==2024.1.0.20240417
# via django-stubs # via django-stubs
types-pyyaml==6.0.12.20240724 types-pyyaml==6.0.12.20240311
# via django-stubs # via django-stubs
typing-extensions==4.12.2 typing-extensions==4.12.2
# via # via
@ -180,7 +180,7 @@ typing-extensions==4.12.2
# stripe # stripe
urllib3==2.2.2 urllib3==2.2.2
# via requests # via requests
uvicorn==0.30.3 uvicorn==0.30.1
# via hatch.envs.dev # via hatch.envs.dev
wheel==0.43.0 wheel==0.43.0
# via pip-tools # via pip-tools

View file

@ -1,21 +0,0 @@
# Generated by Django 5.1b1 on 2024-07-31 22:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('accounting', '0009_alter_orderproduct_order_alter_orderproduct_product'),
]
operations = [
migrations.AlterModelOptions(
name='orderproduct',
options={'verbose_name': 'ordered product', 'verbose_name_plural': 'ordered products'},
),
migrations.RemoveField(
model_name='payment',
name='stripe_charge_id',
),
]

View file

@ -163,6 +163,8 @@ class Payment(CreatedModifiedAbstract):
payment_type = models.ForeignKey("PaymentType", on_delete=models.PROTECT) payment_type = models.ForeignKey("PaymentType", on_delete=models.PROTECT)
external_transaction_id = models.CharField(max_length=255, default="", blank=True) external_transaction_id = models.CharField(max_length=255, default="", blank=True)
stripe_charge_id = models.CharField(max_length=255, default="", blank=True)
class Meta: class Meta:
verbose_name = _("payment") verbose_name = _("payment")
verbose_name_plural = _("payments") verbose_name_plural = _("payments")

View file

@ -30,7 +30,7 @@ def mark_order_paid(sender: models.Payment, instance: models.Payment, **kwargs:
instance.order.save() instance.order.save()
@receiver(post_save, sender=models.Order) @receiver(post_save, sender=models.Payment)
def activate_membership(sender: models.Order, instance: models.Order, **kwargs: dict) -> None: # noqa: ARG001 def activate_membership(sender: models.Order, instance: models.Order, **kwargs: dict) -> None: # noqa: ARG001
"""Mark a membership as activated when its order is marked as paid.""" """Mark a membership as activated when its order is marked as paid."""
if instance.is_paid: if instance.is_paid:

View file

@ -40,10 +40,9 @@
<h2>{% trans "Total price" %}: {{ order.total_with_vat }}</h2> <h2>{% trans "Total price" %}: {{ order.total_with_vat }}</h2>
{% if not order.is_paid %}
<p> <p>
<a href="{% url "order:pay" order_id=order.pk %}" class="button">{% trans "Pay now" %}</a> <a href="{% url "order:pay" order_id=order.pk %}" class="button">{% trans "Pay now" %}</a>
</p> </p>
{% endif %}
</div> </div>
{% endblock %} {% endblock %}

View file

@ -7,13 +7,11 @@ from django.core.mail import send_mail
from django.db import transaction from django.db import transaction
from django.http import HttpRequest from django.http import HttpRequest
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.shortcuts import redirect from django.shortcuts import redirect
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django_view_decorator import namespaced_decorator_factory from django_view_decorator import namespaced_decorator_factory
from djmoney.money import Money
from . import models from . import models
@ -54,8 +52,6 @@ def order_pay(request: HttpRequest, order_id: int) -> HttpResponse:
order = models.Order.objects.get(pk=order_id, member=user) order = models.Order.objects.get(pk=order_id, member=user)
current_site = Site.objects.get_current(request) current_site = Site.objects.get_current(request)
base_domain = f"https://{current_site.domain}" base_domain = f"https://{current_site.domain}"
if settings.DEBUG:
f"http://{current_site.domain}"
try: try:
line_items = [] line_items = []
@ -74,7 +70,6 @@ def order_pay(request: HttpRequest, order_id: int) -> HttpResponse:
) )
checkout_session = stripe.checkout.Session.create( checkout_session = stripe.checkout.Session.create(
line_items=line_items, line_items=line_items,
metadata={"order_id": order.id},
mode="payment", mode="payment",
success_url=base_domain + reverse("order:success", kwargs={"order_id": order.id}), success_url=base_domain + reverse("order:success", kwargs={"order_id": order.id}),
cancel_url=base_domain + "/cancel", cancel_url=base_domain + "/cancel",
@ -137,12 +132,12 @@ def cancel(request: HttpRequest, order_id: int) -> HttpResponse:
) )
@transaction.atomic @csrf_exempt
@order_view( @order_view(
paths="stripe/webhook/", paths="stripe/webhook/",
name="webhook", name="webhook",
login_required=True,
) )
@csrf_exempt
def stripe_webhook(request: HttpRequest) -> HttpResponse: def stripe_webhook(request: HttpRequest) -> HttpResponse:
"""Handle Stripe webhook. """Handle Stripe webhook.
@ -162,16 +157,7 @@ def stripe_webhook(request: HttpRequest) -> HttpResponse:
return HttpResponse(status=400) return HttpResponse(status=400)
if event["type"] == "checkout.session.completed" or event["type"] == "checkout.session.async_payment_succeeded": if event["type"] == "checkout.session.completed" or event["type"] == "checkout.session.async_payment_succeeded":
# Order is marked paid via signals, Membership is activated via signals. # TODO: Implement the payment
order_id = event["data"]["object"]["metadata"]["order_id"] pass
order = get_object_or_404(models.Order, pk=order_id)
if not models.Payment.objects.filter(order=order).exists():
models.Payment.objects.create(
order=order,
amount=Money(event["data"]["object"]["amount_total"], event["data"]["object"]["currency"]),
description="Paid via Stripe",
payment_type=models.PaymentType.objects.get_or_create(name="Stripe")[0],
external_transaction_id=event["id"],
)
return HttpResponse(status=200) return HttpResponse(status=200)

View file

@ -66,7 +66,7 @@ def ensure_membership_type_exists(
# Get the default account of the member. We don't really know what to do if a person owns multiple accounts. # Get the default account of the member. We don't really know what to do if a person owns multiple accounts.
account, __ = Account.objects.get_or_create(owner=member) account, __ = Account.objects.get_or_create(owner=member)
# Create an Order for the products in the membership # Create an Order for the products in the membership
order = Order.objects.create(member=member, account=account, description=membership_type.name) order = Order.objects.create(member=member, account=account)
# Add stuff to the order # Add stuff to the order
for product in membership_type.products.all(): for product in membership_type.products.all():
OrderProduct.objects.create(order=order, product=product, price=product.price, vat=product.vat) OrderProduct.objects.create(order=order, product=product, price=product.price, vat=product.vat)