From 1cc64a8a7117049e3ce0fea24339811785b95fe1 Mon Sep 17 00:00:00 2001 From: Job Vonk Date: Mon, 2 Dec 2024 19:26:53 +0100 Subject: [PATCH 01/10] installed mollie-api-python and created api key env var --- requirements.txt | 1 + sample.env | 2 ++ undead_mongoose/settings.py | 5 +++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index f8a5537..e874387 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,3 +21,4 @@ sentry-sdk==2.19.0 sqlparse==0.5.2 urllib3==2.2.3 python-dotenv[cli] +mollie-api-python diff --git a/sample.env b/sample.env index e10856c..203b1d8 100644 --- a/sample.env +++ b/sample.env @@ -39,3 +39,5 @@ OIDC_OP_TOKEN_ENDPOINT=http://koala.rails.local:3000/api/oauth/token OIDC_OP_USER_ENDPOINT=http://koala.rails.local:3000/oauth/userinfo OIDC_OP_JWKS_ENDPOINT=http://koala.rails.local:3000/oauth/discovery/keys OIDC_OP_LOGOUT_ENDPOINT=http://koala.rails.local:3000/signout + +MOLLIE_API_KEY= diff --git a/undead_mongoose/settings.py b/undead_mongoose/settings.py index 3899320..6403ed8 100644 --- a/undead_mongoose/settings.py +++ b/undead_mongoose/settings.py @@ -16,7 +16,6 @@ # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent -print(BASE_DIR) # Quick-start development settings - unsuitable for production @@ -173,4 +172,6 @@ OIDC_RP_SCOPES = "openid profile email member-read" OIDC_RP_SIGN_ALGO = "RS256" -OIDC_OP_JWKS_ENDPOINT=os.environ['OIDC_OP_JWKS_ENDPOINT'] \ No newline at end of file +OIDC_OP_JWKS_ENDPOINT=os.environ['OIDC_OP_JWKS_ENDPOINT'] + +MOLLIE_API_KEY = os.getenv("MOLLIE_API_KEY") \ No newline at end of file From 76bb8f77565acf0ed778de6a7eff8f5cea622eb5 Mon Sep 17 00:00:00 2001 From: Job Vonk Date: Mon, 2 Dec 2024 21:28:49 +0100 Subject: [PATCH 02/10] Added top up form --- admin_board_view/forms.py | 8 ++++++++ admin_board_view/templates/user_home.html | 7 +++++++ admin_board_view/views.py | 11 +++++++++-- mongoose_app/urls.py | 4 +++- mongoose_app/views.py | 3 +++ 5 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 admin_board_view/forms.py diff --git a/admin_board_view/forms.py b/admin_board_view/forms.py new file mode 100644 index 0000000..9284152 --- /dev/null +++ b/admin_board_view/forms.py @@ -0,0 +1,8 @@ +from django import forms + +def create_TopUpForm(mollie_client): + issuers = [(issuer.id, issuer.name) for issuer in mollie_client.methods.get("ideal", include="issuers").issuers] + class TopUpForm(forms.Form): + amount = forms.DecimalField(min_value=0, decimal_places=2, required=True) + issuer = forms.ChoiceField(choices=issuers, label="Bank") + return TopUpForm \ No newline at end of file diff --git a/admin_board_view/templates/user_home.html b/admin_board_view/templates/user_home.html index 736ad21..3b96343 100644 --- a/admin_board_view/templates/user_home.html +++ b/admin_board_view/templates/user_home.html @@ -61,6 +61,13 @@
Top ups
{% include "pagination_footer.html" with page=top_ups page_name='top_ups' %} +
+
+ {% csrf_token %} +
Top up balance
+ {{ form }} + +
diff --git a/admin_board_view/views.py b/admin_board_view/views.py index e9b8bd4..da54289 100644 --- a/admin_board_view/views.py +++ b/admin_board_view/views.py @@ -10,7 +10,9 @@ from admin_board_view.middleware import dashboard_authenticated, dashboard_admin from admin_board_view.utils import create_paginator from .models import * - +from mollie.api.client import Client +from django.conf import settings +from .forms import create_TopUpForm @dashboard_authenticated def index(request): @@ -19,6 +21,7 @@ def index(request): total_balance = sum(user.balance for user in User.objects.all()) return render(request, "home.html", {"users": User.objects.all(), "product_amount": product_amount, "total_balance": total_balance, "top_types": top_up_types }) else: + print(request.user.email) user = User.objects.get(email=request.user.email) # Get product sales @@ -28,10 +31,14 @@ def index(request): product_sale_groups.append({ "key": designation, "values": list(member_group) }) sales_page = create_paginator(product_sale_groups, request.GET.get('sales')) + mollie_client = Client() + mollie_client.set_api_key(settings.MOLLIE_API_KEY) + form = create_TopUpForm(mollie_client) + # Get topup page top_ups = TopUpTransaction.objects.all().filter(user_id=user) top_up_page = create_paginator(top_ups, request.GET.get('top_ups')) - return render(request, "user_home.html", {"user_info": user, "top_ups": top_up_page, "sales": sales_page }) + return render(request, "user_home.html", {"user_info": user, "top_ups": top_up_page, "sales": sales_page, "form": form }) def login(request): diff --git a/mongoose_app/urls.py b/mongoose_app/urls.py index 0da3197..08aca15 100644 --- a/mongoose_app/urls.py +++ b/mongoose_app/urls.py @@ -14,5 +14,7 @@ path('transaction', views.create_transaction, name='create_transaction'), path('balance', views.update_balance, name='update_balance'), path('register', views.register_card, name='register_card'), - path('catchwebhook', views.on_webhook, name='receive_update') + path('catchwebhook', views.on_webhook, name='receive_update'), + + path('topup', views.topup, name='topup') ] diff --git a/mongoose_app/views.py b/mongoose_app/views.py index c5375f6..9a5b29f 100644 --- a/mongoose_app/views.py +++ b/mongoose_app/views.py @@ -333,3 +333,6 @@ def send_confirmation(email, card): """ } ) + +def topup(request): + return HttpResponse(status=200) \ No newline at end of file From 44b84e724c3c6edb57f0f83bcc8af0447f043d2d Mon Sep 17 00:00:00 2001 From: Job Vonk Date: Tue, 3 Dec 2024 20:59:41 +0100 Subject: [PATCH 03/10] Implemented topup api route --- mongoose_app/urls.py | 4 +++- mongoose_app/views.py | 51 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/mongoose_app/urls.py b/mongoose_app/urls.py index 08aca15..09b3aa4 100644 --- a/mongoose_app/urls.py +++ b/mongoose_app/urls.py @@ -16,5 +16,7 @@ path('register', views.register_card, name='register_card'), path('catchwebhook', views.on_webhook, name='receive_update'), - path('topup', views.topup, name='topup') + path('topup', views.topup, name='topup'), + path('payment/webhook', views.payment_webhook, name='payment_webhook'), + path('payment/success', views.payment_success, name='payment_success') ] diff --git a/mongoose_app/views.py b/mongoose_app/views.py index 9a5b29f..8a2204c 100644 --- a/mongoose_app/views.py +++ b/mongoose_app/views.py @@ -1,11 +1,13 @@ import json from django.http.response import HttpResponse -from django.shortcuts import render +from django.shortcuts import redirect, render from django.http import JsonResponse from django.views.decorators.http import require_http_methods from decimal import Decimal from django.conf import settings + +from admin_board_view.forms import create_TopUpForm from .middleware import authenticated from .models import CardConfirmation, Category, Card, Product, ProductTransactions, SaleTransaction, TopUpTransaction, User, Configuration from datetime import datetime, date @@ -14,6 +16,7 @@ import threading from constance import config from django.utils import timezone +from mollie.api.client import Client import secrets @@ -334,5 +337,51 @@ def send_confirmation(email, card): } ) +@require_http_methods(["POST"]) def topup(request): + mollie_client = Client() + mollie_client.set_api_key(settings.MOLLIE_API_KEY) + form = create_TopUpForm(mollie_client) + + bound_form = form(request.POST) + + webhook_url = request.build_absolute_uri('/api/payment/webhook') + redirect_url = request.build_absolute_uri('/api/payment/success') + + if bound_form.is_valid(): + payment = mollie_client.payments.create({ + 'amount': { + 'currency': 'EUR', + 'value': f'{bound_form.cleaned_data["amount"]:.2f}' + }, + 'description': 'Top up mongoose balance', + 'redirectUrl': redirect_url, + 'webhookUrl': webhook_url, + 'method': 'ideal', + 'issuer': bound_form.cleaned_data['issuer'] + }) + return redirect(payment.checkout_url) + else: + return HttpResponse(status=400) + +@csrf_exempt +@require_http_methods(["POST"]) +def payment_webhook(request): + mollie_client = Client() + mollie_client.set_api_key(settings.MOLLIE_API_KEY) + payment = mollie_client.payments.get(request.POST['id']) + + if payment.is_paid(): + print("Payment paid!") + elif payment.is_pending(): + print("Payment started but not completed!") + elif payment.is_open(): + print("Payment not started yet!") + else: + print("Payment cancelled!") + + return HttpResponse(status=200) + +@require_http_methods(["GET"]) +def payment_success(request): return HttpResponse(status=200) \ No newline at end of file From 78533e5a224d89a0f7bfb4e9d423af632716bad4 Mon Sep 17 00:00:00 2001 From: Job Vonk Date: Mon, 16 Dec 2024 17:43:53 +0100 Subject: [PATCH 04/10] feat: create new models and migrations --- admin_board_view/templates/user_home.html | 23 ++++++++++++--- admin_board_view/views.py | 16 +++++++--- .../migrations/0029_idealtransaction.py | 29 +++++++++++++++++++ mongoose_app/models.py | 26 +++++++++++++++++ mongoose_app/views.py | 29 ++++++++++++++----- 5 files changed, 107 insertions(+), 16 deletions(-) create mode 100644 mongoose_app/migrations/0029_idealtransaction.py diff --git a/admin_board_view/templates/user_home.html b/admin_board_view/templates/user_home.html index 3b96343..ca866bd 100644 --- a/admin_board_view/templates/user_home.html +++ b/admin_board_view/templates/user_home.html @@ -2,6 +2,21 @@ {% load static %} {% block body %}
+ {% if transaction %} + {% if transaction.status == PaymentStatus.PAID %} +
+ The transaction was successful and your balance is updated! +
+ {% elif transaction.status == PaymentStatus.CANCELLED %} +
+ The transaction failed! If you believe this is a mistake, please contact the board. +
+ {% else %} +
+ We are processing your payment. Once it succeed, your balance will be updated! +
+ {% endif %} + {% endif %}

Welcome {{ user_info.name }}

Current balance: {{ user_info.euro_balance }} @@ -51,11 +66,11 @@
Top ups
- {% for top_up in top_ups.object_list %} + {% for date, price, type in top_ups %} - {{ top_up.date }} - €{{ top_up.transaction_sum }} - {% if top_up.type == 1 %}Pin{% elif top_up.type == 2 %}Credit card{% elif top_up.type == 3 %}Mollie{% endif %} + {{ date }} + €{{ price }} + {{ type }} {% endfor %} diff --git a/admin_board_view/views.py b/admin_board_view/views.py index da54289..69def3b 100644 --- a/admin_board_view/views.py +++ b/admin_board_view/views.py @@ -21,7 +21,6 @@ def index(request): total_balance = sum(user.balance for user in User.objects.all()) return render(request, "home.html", {"users": User.objects.all(), "product_amount": product_amount, "total_balance": total_balance, "top_types": top_up_types }) else: - print(request.user.email) user = User.objects.get(email=request.user.email) # Get product sales @@ -35,10 +34,19 @@ def index(request): mollie_client.set_api_key(settings.MOLLIE_API_KEY) form = create_TopUpForm(mollie_client) + transaction_id = request.GET.dict().get("transaction_id") + transaction = IDealTransaction.objects.get(transaction_id=transaction_id) if transaction_id else None + # Get topup page - top_ups = TopUpTransaction.objects.all().filter(user_id=user) - top_up_page = create_paginator(top_ups, request.GET.get('top_ups')) - return render(request, "user_home.html", {"user_info": user, "top_ups": top_up_page, "sales": sales_page, "form": form }) + top_ups = TopUpTransaction.objects.all().filter(user_id=user).values_list("date", "transaction_sum") + ideal_transactions = IDealTransaction.objects.all().filter(user_id=user, status=PaymentStatus.PAID).values_list("date", "transaction_sum") + all_top_ups = sorted( + [(d, t, "Pin") for d, t in top_ups] + + [(d, t, "iDeal") for d, t in ideal_transactions], + key=lambda transaction: transaction[0] + ) + top_up_page = create_paginator(all_top_ups, request.GET.get('top_ups')) + return render(request, "user_home.html", {"user_info": user, "top_ups": top_up_page, "sales": sales_page, "form": form, "transaction": transaction, "PaymentStatus": PaymentStatus }) def login(request): diff --git a/mongoose_app/migrations/0029_idealtransaction.py b/mongoose_app/migrations/0029_idealtransaction.py new file mode 100644 index 0000000..725ba80 --- /dev/null +++ b/mongoose_app/migrations/0029_idealtransaction.py @@ -0,0 +1,29 @@ +# Generated by Django 4.0 on 2024-12-16 16:18 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('mongoose_app', '0028_save_email_in_user_add_mollie'), + ] + + operations = [ + migrations.CreateModel( + name='IDealTransaction', + fields=[ + ('transaction_sum', models.DecimalField(decimal_places=2, max_digits=6)), + ('date', models.DateField(auto_now=True)), + ('transaction_id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('status', models.IntegerField(choices=[(1, 'PAID'), (2, 'PENDING'), (3, 'OPEN'), (4, 'CANCELLED')], default=3)), + ('added', models.BooleanField(default=False)), + ('user_id', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='mongoose_app.user')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/mongoose_app/models.py b/mongoose_app/models.py index 384dff0..f21230f 100644 --- a/mongoose_app/models.py +++ b/mongoose_app/models.py @@ -1,4 +1,5 @@ from typing import Any, Dict, Iterable, Optional, Tuple +import uuid from django.db import models from django.utils.html import mark_safe from django.conf import settings @@ -171,6 +172,31 @@ def delete(self, using: Any = None, keep_parents: bool = False) -> Tuple[int, Di self.user_id.save() return super().delete(using=using, keep_parents=keep_parents) +class PaymentStatus(models.IntegerChoices): + PAID = 1, "PAID" + PENDING = 2, "PENDING" + OPEN = 3, "OPEN" + CANCELLED = 4, "CANCELLED" + +class IDealTransaction(Transaction): + transaction_id = models.UUIDField(primary_key=True, default=uuid.uuid4) + status = models.IntegerField(choices=PaymentStatus.choices, default=PaymentStatus.OPEN) + added = models.BooleanField(default=False) + + def save(self, force_insert: bool = False, force_update: bool = False, using: Optional[str] = None, update_fields: Optional[Iterable[str]] = None) -> None: + if not self.added and self.status == PaymentStatus.PAID: + self.user_id.balance += self.transaction_sum + self.user_id.save() + self.added = True + return super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields) + + def delete(self, using: Any = None, keep_parents: bool = False) -> Tuple[int, Dict[str, int]]: + self.user_id.balance -= self.transaction_sum + self.user_id.save() + return super().delete(using=using, keep_parents=keep_parents) + + def __str__(self): + return "iDeal transaction { status = " + str(PaymentStatus(self.status).label) + ", added = " + str(self.added) + ", id = " + str(self.id) + " }" # User needs name, age and balance to be able to make sense to BESTUUUUUR. class User(models.Model): diff --git a/mongoose_app/views.py b/mongoose_app/views.py index 8a2204c..1792096 100644 --- a/mongoose_app/views.py +++ b/mongoose_app/views.py @@ -8,8 +8,9 @@ from django.conf import settings from admin_board_view.forms import create_TopUpForm +from admin_board_view.middleware import dashboard_authenticated from .middleware import authenticated -from .models import CardConfirmation, Category, Card, Product, ProductTransactions, SaleTransaction, TopUpTransaction, User, Configuration +from .models import CardConfirmation, Category, Card, IDealTransaction, PaymentStatus, Product, ProductTransactions, SaleTransaction, TopUpTransaction, User, Configuration from datetime import datetime, date from django.views.decorators.csrf import csrf_exempt import requests @@ -337,6 +338,7 @@ def send_confirmation(email, card): } ) +@dashboard_authenticated @require_http_methods(["POST"]) def topup(request): mollie_client = Client() @@ -345,10 +347,16 @@ def topup(request): bound_form = form(request.POST) - webhook_url = request.build_absolute_uri('/api/payment/webhook') - redirect_url = request.build_absolute_uri('/api/payment/success') - if bound_form.is_valid(): + user = User.objects.get(email=request.user) + transaction = IDealTransaction.objects.create( + user_id=user, + transaction_sum=bound_form.cleaned_data["amount"] + ) + + webhook_url = request.build_absolute_uri(f'/api/payment/webhook?transaction_id={transaction.transaction_id}') + redirect_url = request.build_absolute_uri(f'/?transaction_id={transaction.transaction_id}') + payment = mollie_client.payments.create({ 'amount': { 'currency': 'EUR', @@ -370,15 +378,20 @@ def payment_webhook(request): mollie_client = Client() mollie_client.set_api_key(settings.MOLLIE_API_KEY) payment = mollie_client.payments.get(request.POST['id']) + + transaction_id = request.GET["transaction_id"] + transaction = IDealTransaction.objects.get(transaction_id=transaction_id) if payment.is_paid(): - print("Payment paid!") + transaction.status = PaymentStatus.PAID elif payment.is_pending(): - print("Payment started but not completed!") + transaction.status = PaymentStatus.PENDING elif payment.is_open(): - print("Payment not started yet!") + transaction.status = PaymentStatus.OPEN else: - print("Payment cancelled!") + transaction.status = PaymentStatus.CANCELLED + + transaction.save() return HttpResponse(status=200) From 95d195dc13fab8208c2e5ba7eea4beca43e7554b Mon Sep 17 00:00:00 2001 From: Job Vonk Date: Mon, 16 Dec 2024 20:55:01 +0100 Subject: [PATCH 05/10] feat: add ideal transactions export --- admin_board_view/views.py | 263 ++++++++++++++++++++++++++++---------- 1 file changed, 192 insertions(+), 71 deletions(-) diff --git a/admin_board_view/views.py b/admin_board_view/views.py index 69def3b..a206b78 100644 --- a/admin_board_view/views.py +++ b/admin_board_view/views.py @@ -14,39 +14,78 @@ from django.conf import settings from .forms import create_TopUpForm + @dashboard_authenticated def index(request): if request.user.is_superuser: product_amount = Product.objects.count() total_balance = sum(user.balance for user in User.objects.all()) - return render(request, "home.html", {"users": User.objects.all(), "product_amount": product_amount, "total_balance": total_balance, "top_types": top_up_types }) + return render( + request, + "home.html", + { + "users": User.objects.all(), + "product_amount": product_amount, + "total_balance": total_balance, + "top_types": top_up_types, + }, + ) else: user = User.objects.get(email=request.user.email) # Get product sales - product_sales = list(ProductTransactions.objects.all().filter(transaction_id__user_id=user)) + product_sales = list( + ProductTransactions.objects.all().filter(transaction_id__user_id=user) + ) product_sale_groups = [] - for designation, member_group in groupby(product_sales, lambda sale: sale.transaction_id): - product_sale_groups.append({ "key": designation, "values": list(member_group) }) - sales_page = create_paginator(product_sale_groups, request.GET.get('sales')) + for designation, member_group in groupby( + product_sales, lambda sale: sale.transaction_id + ): + product_sale_groups.append( + {"key": designation, "values": list(member_group)} + ) + sales_page = create_paginator(product_sale_groups, request.GET.get("sales")) mollie_client = Client() mollie_client.set_api_key(settings.MOLLIE_API_KEY) form = create_TopUpForm(mollie_client) transaction_id = request.GET.dict().get("transaction_id") - transaction = IDealTransaction.objects.get(transaction_id=transaction_id) if transaction_id else None + transaction = ( + IDealTransaction.objects.get(transaction_id=transaction_id) + if transaction_id + else None + ) # Get topup page - top_ups = TopUpTransaction.objects.all().filter(user_id=user).values_list("date", "transaction_sum") - ideal_transactions = IDealTransaction.objects.all().filter(user_id=user, status=PaymentStatus.PAID).values_list("date", "transaction_sum") + top_ups = ( + TopUpTransaction.objects.all() + .filter(user_id=user) + .values_list("date", "transaction_sum") + ) + ideal_transactions = ( + IDealTransaction.objects.all() + .filter(user_id=user, status=PaymentStatus.PAID) + .values_list("date", "transaction_sum") + ) all_top_ups = sorted( - [(d, t, "Pin") for d, t in top_ups] + - [(d, t, "iDeal") for d, t in ideal_transactions], - key=lambda transaction: transaction[0] + [(d, t, "Pin") for d, t in top_ups] + + [(d, t, "iDeal") for d, t in ideal_transactions], + key=lambda transaction: transaction[0], + ) + top_up_page = create_paginator(all_top_ups, request.GET.get("top_ups")) + return render( + request, + "user_home.html", + { + "user_info": user, + "top_ups": top_up_page, + "sales": sales_page, + "form": form, + "transaction": transaction, + "PaymentStatus": PaymentStatus, + }, ) - top_up_page = create_paginator(all_top_ups, request.GET.get('top_ups')) - return render(request, "user_home.html", {"user_info": user, "top_ups": top_up_page, "sales": sales_page, "form": form, "transaction": transaction, "PaymentStatus": PaymentStatus }) def login(request): @@ -63,7 +102,9 @@ def products(request): product = ProductForm(request.POST, request.FILES, instance=instance) if product.is_valid(): - product.category = Category.objects.get(name=product.cleaned_data["category"]) + product.category = Category.objects.get( + name=product.cleaned_data["category"] + ) product.save() return HttpResponseRedirect("/products") @@ -78,28 +119,40 @@ def products(request): transactions = ProductTransactions.objects.filter(product_id=product) product_sales = { "all": transactions, - "sum": transactions.values('product_price').annotate(sum=Sum('amount')) + "sum": transactions.values("product_price").annotate(sum=Sum("amount")), } - products = Product.objects.all().order_by('name') + products = Product.objects.all().order_by("name") categories = Category.objects.all() - return render(request, "products.html", { "products": products, "categories": categories, "product_form": pf, "current_product": product, "product_sales": product_sales }) + return render( + request, + "products.html", + { + "products": products, + "categories": categories, + "product_form": pf, + "current_product": product, + "product_sales": product_sales, + }, + ) @dashboard_admin def delete(request): - id = request.POST.dict()['id'] + id = request.POST.dict()["id"] Product.objects.get(id=id).delete() - return JsonResponse({ "msg": f"Deleted product with {id}" }) + return JsonResponse({"msg": f"Deleted product with {id}"}) @dashboard_admin def toggle(request): - id = request.POST.dict()['id'] + id = request.POST.dict()["id"] product = Product.objects.get(id=id) product.enabled = not product.enabled product.save() - return JsonResponse({ "msg": f"Set the state of product {id} to enabled={product.enabled}" }) + return JsonResponse( + {"msg": f"Set the state of product {id} to enabled={product.enabled}"} + ) @dashboard_admin @@ -108,10 +161,16 @@ def users(request, user_id=None): if user_id: user = User.objects.get(id=user_id) top_ups = TopUpTransaction.objects.all().filter(user_id=user) - product_sales = list(ProductTransactions.objects.all().filter(transaction_id__user_id=user)) + product_sales = list( + ProductTransactions.objects.all().filter(transaction_id__user_id=user) + ) product_sale_groups = [] - for designation, member_group in groupby(product_sales, lambda sale: sale.transaction_id): - product_sale_groups.append({ "key": designation, "values": list(member_group) }) + for designation, member_group in groupby( + product_sales, lambda sale: sale.transaction_id + ): + product_sale_groups.append( + {"key": designation, "values": list(member_group)} + ) cards = [] for i, card in enumerate(Card.objects.all().filter(user_id=user.id)): @@ -119,19 +178,29 @@ def users(request, user_id=None): if card.active is False: cards[i]["token"] = CardConfirmation.objects.get(card=card).token - top_up_page = create_paginator(top_ups, request.GET.get('top_ups')) - sales_page = create_paginator(product_sale_groups, request.GET.get('sales')) - - return render(request, "user.html", { "user_info": user, "cards": cards, "top_ups": top_up_page, "sales": sales_page, "top_types": top_up_types }) + top_up_page = create_paginator(top_ups, request.GET.get("top_ups")) + sales_page = create_paginator(product_sale_groups, request.GET.get("sales")) + + return render( + request, + "user.html", + { + "user_info": user, + "cards": cards, + "top_ups": top_up_page, + "sales": sales_page, + "top_types": top_up_types, + }, + ) else: users = User.objects.all() - + # Only filter on user name if a name is given - if request.GET.get('name'): - users = users.filter(name__icontains=request.GET.get('name')) + if request.GET.get("name"): + users = users.filter(name__icontains=request.GET.get("name")) - user_page = create_paginator(users, request.GET.get('users'), p_len=15) - return render(request, "user.html", { "users": users, "user_page": user_page }) + user_page = create_paginator(users, request.GET.get("users"), p_len=15) + return render(request, "user.html", {"users": users, "user_page": user_page}) @dashboard_admin @@ -139,16 +208,22 @@ def settings_page(request): vat = VAT.objects.all() categories = Category.objects.all() configuration = Configuration.objects.get(pk=1) - return render(request, "settings.html", { "vat": vat, "categories": categories, "configuration": configuration }) + return render( + request, + "settings.html", + {"vat": vat, "categories": categories, "configuration": configuration}, + ) @dashboard_admin def category(request): try: - categories = json.loads(request.POST.dict()['categories']) + categories = json.loads(request.POST.dict()["categories"]) for category in categories: - if category["id"] == '0': - cat = Category.objects.create(name=category["name"], alcoholic=category["checked"]) + if category["id"] == "0": + cat = Category.objects.create( + name=category["name"], alcoholic=category["checked"] + ) cat.save() elif "delete" in category and category["delete"] is True: cat = Category.objects.get(id=category["id"]) @@ -159,18 +234,21 @@ def category(request): cat.alcoholic = category["checked"] cat.save() - return JsonResponse({ "msg": "Updated the mongoose categories" }) + return JsonResponse({"msg": "Updated the mongoose categories"}) except Exception as e: print(e) - return JsonResponse({ "msg": "Something went wrong whilst trying to save the categories" }, status=400) + return JsonResponse( + {"msg": "Something went wrong whilst trying to save the categories"}, + status=400, + ) @dashboard_admin def vat(request): try: - vatBody = json.loads(request.POST.dict()['vat']) + vatBody = json.loads(request.POST.dict()["vat"]) for vat in vatBody: - if vat["id"] == '0': + if vat["id"] == "0": newVAT = VAT.objects.create(percentage=vat["percentage"]) newVAT.save() elif "delete" in vat and vat["delete"] is True: @@ -181,10 +259,13 @@ def vat(request): newVAT.percentage = vat["percentage"] newVAT.save() - return JsonResponse({ "msg": "Updated the mongoose VAT percentages" }) + return JsonResponse({"msg": "Updated the mongoose VAT percentages"}) except Exception as e: print(e) - return JsonResponse({ "msg": "Something went wrong whilst trying to save the VAT percentages" }, status=400) + return JsonResponse( + {"msg": "Something went wrong whilst trying to save the VAT percentages"}, + status=400, + ) @dashboard_admin @@ -200,29 +281,42 @@ def settings_update(request): """ try: configuration = Configuration.objects.get(pk=1) - settings = json.loads(request.POST.dict()['settings']) - configuration.alc_time = settings['alc_time'] + settings = json.loads(request.POST.dict()["settings"]) + configuration.alc_time = settings["alc_time"] configuration.save() - return JsonResponse({ "msg": "Updated the mongoose configuration" }) + return JsonResponse({"msg": "Updated the mongoose configuration"}) except Exception as e: print(e) - return JsonResponse({ "msg": "Something went wrong whilst trying to save the configuration" }, status=400) + return JsonResponse( + {"msg": "Something went wrong whilst trying to save the configuration"}, + status=400, + ) @dashboard_admin def transactions(request): # Get product sale groups product_sales = ProductTransactions.objects.all() - product_sales_sorted = sorted(product_sales, key=lambda sale: sale.transaction_id.date, reverse=True) + product_sales_sorted = sorted( + product_sales, key=lambda sale: sale.transaction_id.date, reverse=True + ) product_sale_groups = [] - for designation, member_group in groupby(product_sales_sorted, lambda sale: sale.transaction_id): - product_sale_groups.append({ "key": designation, "values": list(member_group) }) - + for designation, member_group in groupby( + product_sales_sorted, lambda sale: sale.transaction_id + ): + product_sale_groups.append({"key": designation, "values": list(member_group)}) + # Get paginators - top_up_page = create_paginator(TopUpTransaction.objects.all(), request.GET.get('top_ups')) - sales_page = create_paginator(product_sale_groups, request.GET.get('sales'), p_len=10) + top_up_page = create_paginator( + TopUpTransaction.objects.all(), request.GET.get("top_ups") + ) + sales_page = create_paginator( + product_sale_groups, request.GET.get("sales"), p_len=10 + ) - return render(request, "transactions.html", { "top_ups": top_up_page, "sales": sales_page }) + return render( + request, "transactions.html", {"top_ups": top_up_page, "sales": sales_page} + ) @dashboard_admin @@ -232,16 +326,16 @@ def export_sale_transactions(request): Args: request (HttpRequest): The HTTP request object containing the date range. - + Returns: HttpResponse: The csv file containing the sale transactions in the given date range. """ try: req_get = request.GET - export_type = req_get.get('type') - start_date = req_get.get('start_date') - end_date = req_get.get('end_date') - if export_type == 'mollie': + export_type = req_get.get("type") + start_date = req_get.get("start_date") + end_date = req_get.get("end_date") + if export_type == "mollie": if not start_date or not end_date: return HttpResponse("No date range given.", status=400) @@ -249,18 +343,30 @@ def export_sale_transactions(request): current_date = timezone.now().strftime("%Y-%m-%d") # Get the transactions in the date range - top_up_range = TopUpTransaction.objects.filter(date__range=[start_date, end_date], type=3).all() + top_up_range = TopUpTransaction.objects.filter( + date__range=[start_date, end_date], type=3 + ).all() + ideal_transactions = IDealTransaction.objects.filter( + date__range=[start_date, end_date] + ).all() # Setup the export "csv" - response_string = f'Factuurdatum,{current_date},ideal - {start_date} / {end_date},02,473\n' + response_string = f"Factuurdatum,{current_date},ideal - {start_date} / {end_date},02,473\n" # Add the transactions to the export "csv" for t in top_up_range: - response_string += f'"",8002,Mongoose - {t.id},9,{t.transaction_sum},""\n' + response_string += ( + f'"",8002,Mongoose - {t.id},9,{t.transaction_sum},""\n' + ) + + for t in ideal_transactions: + response_string += ( + f'"",8002,Mongoose - {t.transaction_id},9,{t.transaction_sum},""\n' + ) # Return the export "csv" - return HttpResponse(response_string, content_type='text/csv') - elif export_type == 'pin': + return HttpResponse(response_string, content_type="text/csv") + elif export_type == "pin": if not start_date or not end_date: return HttpResponse("No date range given.", status=400) @@ -269,19 +375,34 @@ def export_sale_transactions(request): # Get the transactions in the date range if start_date and end_date: - top_up_range = TopUpTransaction.objects.filter(date__range=[start_date, end_date], type=1).all() + top_up_range = TopUpTransaction.objects.filter( + date__range=[start_date, end_date], type=1 + ).all() else: - top_up_range = TopUpTransaction.objects.filter(date=current_date, type=1).all() + top_up_range = TopUpTransaction.objects.filter( + date=current_date, type=1 + ).all() # Turn transaction result into JSON - json_resp = json.dumps([{"member_id": t.user_id.id, "name": t.user_id.name, - "price": float(t.transaction_sum), "date": t.date.strftime("%Y-%m-%d")} - for t in top_up_range]) + json_resp = json.dumps( + [ + { + "member_id": t.user_id.id, + "name": t.user_id.name, + "price": float(t.transaction_sum), + "date": t.date.strftime("%Y-%m-%d"), + } + for t in top_up_range + ] + ) # Return the created json - return HttpResponse(json_resp, content_type='application/json') + return HttpResponse(json_resp, content_type="application/json") else: return HttpResponse("Export type not found", status=400) except Exception as e: print(e) - return HttpResponse("Something went wrong whilst trying to export the sale transactions.", status=400) + return HttpResponse( + "Something went wrong whilst trying to export the sale transactions.", + status=400, + ) From c5483e30e8e20621efab4faa202ddbc1d4150eed Mon Sep 17 00:00:00 2001 From: Job Vonk Date: Mon, 16 Dec 2024 21:11:49 +0100 Subject: [PATCH 06/10] Add mollie-api-python as dependency to pyproject --- pyproject.toml | 1 + uv.lock | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index b2c65c6..14c699c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dependencies = [ "sqlparse==0.5.2", "urllib3==2.2.3", "python-dotenv[cli]", + "mollie-api-python>=3.7.4", ] name = "undead-mongoose" version = "1.0.0" diff --git a/uv.lock b/uv.lock index d9371b8..39de5ed 100644 --- a/uv.lock +++ b/uv.lock @@ -199,6 +199,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8b/9b/2ba5943ca36213123cf54a21629695c9bf168ee135f06ad17e89f9f6236c/josepy-1.14.0-py3-none-any.whl", hash = "sha256:d2b36a30f316269f3242f4c2e45e15890784178af5ec54fa3e49cf9234ee22e0", size = 32180 }, ] +[[package]] +name = "mollie-api-python" +version = "3.7.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/55/0ae303a63abb6faa9e448b4c9e5188e7af30f5a71f7dc3e40442023e9740/mollie_api_python-3.7.4.tar.gz", hash = "sha256:9005f10478fd628c5745712e4f99d26120c1efebd8b59cbf720b2ed659aa4f52", size = 62462 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/06/66563dd75ea115f1fb35781bc928419aa1d6f86b3a690f0df7aeccfe747c/mollie_api_python-3.7.4-py3-none-any.whl", hash = "sha256:0d5cd4b2cc8c124c948a1fd997e8409902ff262946b8244e387f20ea972b83ab", size = 52057 }, +] + [[package]] name = "mozilla-django-oidc" version = "2.0.0" @@ -214,6 +228,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/d2/28a67d605371fcf8fe31e88926f400299653bde4256c6606ac9bc15d61a0/mozilla_django_oidc-2.0.0-py2.py3-none-any.whl", hash = "sha256:53c39755b667e8c5923b1dffc3c29673198d03aa107aa42ac86b8a38b4720c25", size = 26248 }, ] +[[package]] +name = "oauthlib" +version = "3.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918", size = 177352 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688 }, +] + [[package]] name = "packaging" version = "24.2" @@ -306,6 +329,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, ] +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179 }, +] + [[package]] name = "sentry-sdk" version = "2.19.0" @@ -363,6 +399,7 @@ dependencies = [ { name = "gunicorn" }, { name = "idna" }, { name = "josepy" }, + { name = "mollie-api-python" }, { name = "mozilla-django-oidc" }, { name = "packaging" }, { name = "pillow" }, @@ -390,6 +427,7 @@ requires-dist = [ { name = "gunicorn", specifier = "==23.0.0" }, { name = "idna", specifier = "==3.10" }, { name = "josepy", specifier = "==1.14.0" }, + { name = "mollie-api-python", specifier = ">=3.7.4" }, { name = "mozilla-django-oidc", specifier = "==2.0.0" }, { name = "packaging", specifier = "==24.2" }, { name = "pillow", specifier = "==11.0.0" }, From a6f164b4ef2902dbe86f7981ffbd98333d4d3ef8 Mon Sep 17 00:00:00 2001 From: Job Vonk Date: Tue, 17 Dec 2024 08:46:34 +0100 Subject: [PATCH 07/10] Remove index route and templates from mongoose_app --- .../admin/saletransactions/st_changelist.html | 16 -- mongoose_app/templates/index.html | 23 -- mongoose_app/urls.py | 24 +- mongoose_app/views.py | 251 +++++++++--------- 4 files changed, 137 insertions(+), 177 deletions(-) delete mode 100644 mongoose_app/templates/admin/saletransactions/st_changelist.html delete mode 100644 mongoose_app/templates/index.html diff --git a/mongoose_app/templates/admin/saletransactions/st_changelist.html b/mongoose_app/templates/admin/saletransactions/st_changelist.html deleted file mode 100644 index 5705d7b..0000000 --- a/mongoose_app/templates/admin/saletransactions/st_changelist.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends 'admin/change_list.html' %} - - -{% block object-tools %} -
-

Export sales

-
- {% csrf_token %} -
- - -
-
-
- {{ block.super }} -{% endblock %} \ No newline at end of file diff --git a/mongoose_app/templates/index.html b/mongoose_app/templates/index.html deleted file mode 100644 index 03c1482..0000000 --- a/mongoose_app/templates/index.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - Undead Mongoose - - - - - - - {% if user.is_authenticated %} -

Undead Mongoose

-

Current user: {{ user.email }}

-

Is admin: {{ user.is_superuser }}

-
- {% csrf_token %} - -
- {% else %} - Login - {% endif %} - - diff --git a/mongoose_app/urls.py b/mongoose_app/urls.py index 09b3aa4..1a0ad53 100644 --- a/mongoose_app/urls.py +++ b/mongoose_app/urls.py @@ -3,20 +3,16 @@ from . import views urlpatterns = [ - path('', views.index, name='index'), - # GET endpoints - path('card', views.get_card, name='get_card'), - path('products', views.get_products, name='get_products'), - path('confirm', views.confirm_card, name='confirm_card'), - + path("card", views.get_card, name="get_card"), + path("products", views.get_products, name="get_products"), + path("confirm", views.confirm_card, name="confirm_card"), # POST endpoints - path('transaction', views.create_transaction, name='create_transaction'), - path('balance', views.update_balance, name='update_balance'), - path('register', views.register_card, name='register_card'), - path('catchwebhook', views.on_webhook, name='receive_update'), - - path('topup', views.topup, name='topup'), - path('payment/webhook', views.payment_webhook, name='payment_webhook'), - path('payment/success', views.payment_success, name='payment_success') + path("transaction", views.create_transaction, name="create_transaction"), + path("balance", views.update_balance, name="update_balance"), + path("register", views.register_card, name="register_card"), + path("catchwebhook", views.on_webhook, name="receive_update"), + path("topup", views.topup, name="topup"), + path("payment/webhook", views.payment_webhook, name="payment_webhook"), + path("payment/success", views.payment_success, name="payment_success"), ] diff --git a/mongoose_app/views.py b/mongoose_app/views.py index 1792096..09b697b 100644 --- a/mongoose_app/views.py +++ b/mongoose_app/views.py @@ -10,7 +10,19 @@ from admin_board_view.forms import create_TopUpForm from admin_board_view.middleware import dashboard_authenticated from .middleware import authenticated -from .models import CardConfirmation, Category, Card, IDealTransaction, PaymentStatus, Product, ProductTransactions, SaleTransaction, TopUpTransaction, User, Configuration +from .models import ( + CardConfirmation, + Category, + Card, + IDealTransaction, + PaymentStatus, + Product, + ProductTransactions, + SaleTransaction, + TopUpTransaction, + User, + Configuration, +) from datetime import datetime, date from django.views.decorators.csrf import csrf_exempt import requests @@ -22,10 +34,6 @@ import secrets -def index(request): - return render(request, "index.html") - - # GET endpoints @authenticated @require_http_methods(["GET"]) @@ -35,8 +43,8 @@ def get_card(request): - Check if card exists, if so obtain user, return user. - Else, should return that student number is needed (frontend should go to register page) """ - if 'uuid' in request.GET: - card_uuid = request.GET.get('uuid') + if "uuid" in request.GET: + card_uuid = request.GET.get("uuid") card = Card.objects.filter(card_id=card_uuid, active=True).first() if card is None: return HttpResponse(status=404) @@ -53,12 +61,16 @@ def get_products(request): Should we handle here whether alcoholic products are returned? """ # Obtain user from card info - card_id = request.GET.get('uuid') + card_id = request.GET.get("uuid") card = Card.objects.filter(card_id=card_id).first() user = User.objects.filter(user_id=card.user_id.user_id).first() # Calc age of user based on birthday today = date.today() - age = today.year - user.birthday.year - ((today.month, today.day) < (user.birthday.month, user.birthday.day)) + age = ( + today.year + - user.birthday.year + - ((today.month, today.day) < (user.birthday.month, user.birthday.day)) + ) alc_time = Configuration.objects.get(pk=1).alc_time now = timezone.localtime(timezone.now()) @@ -85,51 +97,45 @@ def create_transaction(request): """ try: - body = json.loads(request.body.decode('utf-8')) + body = json.loads(request.body.decode("utf-8")) except json.decoder.JSONDecodeError: return HttpResponse(status=400) - if 'items' not in body or 'uuid' not in body: + if "items" not in body or "uuid" not in body: return HttpResponse(status=400) - items = body['items'] - card_id = body['uuid'] + items = body["items"] + card_id = body["uuid"] trans_products = [] trans_sum = 0 for product in items: - if 'id' not in product or 'amount' not in product: + if "id" not in product or "amount" not in product: return HttpResponse(status=400) - p_id = int(product['id']) - p_amount = int(product['amount']) + p_id = int(product["id"]) + p_amount = int(product["amount"]) db_product = Product.objects.filter(id=p_id).first() if not db_product: - return HttpResponse( - status=400, - content=f"Product {p_id} not found" - ) + return HttpResponse(status=400, content=f"Product {p_id} not found") trans_sum += db_product.price * p_amount trans_products.append((db_product, p_amount)) card = Card.objects.filter(card_id=card_id).first() if not card: - return HttpResponse( - status=400, - content="Card not found") + return HttpResponse(status=400, content="Card not found") user = card.user_id - if (user.balance - trans_sum < 0): + if user.balance - trans_sum < 0: return HttpResponse( - status=400, - content="Transaction failed, not enough balance" + status=400, content="Transaction failed, not enough balance" ) transaction = SaleTransaction.objects.create( - user_id=user, - transaction_sum=trans_sum) + user_id=user, transaction_sum=trans_sum + ) for product, amount in trans_products: ProductTransactions.objects.create( @@ -137,13 +143,10 @@ def create_transaction(request): transaction_id=transaction, product_price=product.price, product_vat=product.vat.percentage, - amount=amount) + amount=amount, + ) - return JsonResponse( - { - 'balance': user.balance - }, - status=201, safe=False) + return JsonResponse({"balance": user.balance}, status=201, safe=False) @require_http_methods(["POST"]) @@ -159,20 +162,24 @@ def update_balance(request): user = User.objects.get(name=body["user"]) transaction = TopUpTransaction.objects.create( - user_id=user, - transaction_sum=Decimal(body["balance"]), - type=body["type"] + user_id=user, transaction_sum=Decimal(body["balance"]), type=body["type"] ) transaction.save() - return JsonResponse({ - 'msg': f"Balance for {user.name} has been updated to {user.balance}", - 'balance': user.balance - }, status=201, safe=False) + return JsonResponse( + { + "msg": f"Balance for {user.name} has been updated to {user.balance}", + "balance": user.balance, + }, + status=201, + safe=False, + ) except Exception as e: - return JsonResponse({ - 'msg': f"Balance for {body['user']} could not be updated." - }, status=400, safe=False) + return JsonResponse( + {"msg": f"Balance for {body['user']} could not be updated."}, + status=400, + safe=False, + ) @csrf_exempt @@ -188,9 +195,9 @@ def register_card(request): - Else add card to user. """ # Obtain student number and card uuid from sloth - body = json.loads(request.body.decode('utf-8')) - student_nr = body['student'] - card_id = body['uuid'] + body = json.loads(request.body.decode("utf-8")) + student_nr = body["student"] + card_id = body["uuid"] # Check if card is already present in the database # Cards are FULLY UNIQUE OVER ALL MEMBERS @@ -201,13 +208,9 @@ def register_card(request): # Obtain user information from Koala (or any other central member base) koala_response = requests.get( - settings.USER_URL + '/api/internal/member_by_studentid', - params={ - 'student_number': student_nr - }, - headers={ - 'Authorization': settings.USER_TOKEN - } + settings.USER_URL + "/api/internal/member_by_studentid", + params={"student_number": student_nr}, + headers={"Authorization": settings.USER_TOKEN}, ) # If user is not found in database, we cannot create user here. if koala_response.status_code == 204: @@ -215,57 +218,51 @@ def register_card(request): # Get user info. koala_response = koala_response.json() - user_id = koala_response['id'] + user_id = koala_response["id"] # Check if user exists. user = User.objects.filter(user_id=user_id).first() # If so, add the card to the already existing user. if not user == None: - card = Card.objects.create( - card_id=card_id, - active=False, - user_id=user - ) - send_confirmation(koala_response['email'], card) + card = Card.objects.create(card_id=card_id, active=False, user_id=user) + send_confirmation(koala_response["email"], card) # Else, we first create the user based on the info from koala. else: - first_name = koala_response['first_name'] + first_name = koala_response["first_name"] infix = None - if 'infix' in koala_response: - infix = koala_response['infix'] - last_name = koala_response['last_name'] - born = datetime.strptime(koala_response['birth_date'], '%Y-%m-%d') + if "infix" in koala_response: + infix = koala_response["infix"] + last_name = koala_response["last_name"] + born = datetime.strptime(koala_response["birth_date"], "%Y-%m-%d") user = User.objects.create( user_id=user_id, - name=f'{first_name} {infix} {last_name}' if infix else f'{first_name} {last_name}', + name=f"{first_name} {infix} {last_name}" + if infix + else f"{first_name} {last_name}", birthday=born, - email=koala_response['email'] - ) - card = Card.objects.create( - card_id=card_id, - active=False, - user_id=user + email=koala_response["email"], ) - send_confirmation(koala_response['email'], card) + card = Card.objects.create(card_id=card_id, active=False, user_id=user) + send_confirmation(koala_response["email"], card) # If that all succeeds, we return CREATED. return HttpResponse(status=201) @require_http_methods(["GET"]) def confirm_card(request): - if 'token' in request.GET: - token = request.GET.get('token') + if "token" in request.GET: + token = request.GET.get("token") card_conf = CardConfirmation.objects.filter(token=token).first() if card_conf: card = card_conf.card card.active = True card.save() card_conf.delete() - return HttpResponse('Card confirmed!') + return HttpResponse("Card confirmed!") else: - return HttpResponse('Something went horribly wrong!') + return HttpResponse("Something went horribly wrong!") else: - return HttpResponse('You should not have requested this url') + return HttpResponse("You should not have requested this url") @csrf_exempt @@ -277,22 +274,18 @@ def on_webhook(request): def async_on_webhook(request): - koala_sent = json.loads(request.body.decode('utf-8')) + koala_sent = json.loads(request.body.decode("utf-8")) - if koala_sent['type'] == 'member': - user_id = koala_sent['id'] + if koala_sent["type"] == "member": + user_id = koala_sent["id"] print(user_id) user = User.objects.filter(user_id=user_id).first() print(user.user_id) if not user is None: koala_response = requests.get( - settings.USER_URL + '/api/internal/member_by_id', - params={ - 'id': user.user_id - }, - headers={ - 'Authorization': settings.USER_TOKEN - } + settings.USER_URL + "/api/internal/member_by_id", + params={"id": user.user_id}, + headers={"Authorization": settings.USER_TOKEN}, ) # TODO: What if this happens? if koala_response.status_code == 204: @@ -301,11 +294,13 @@ def async_on_webhook(request): if koala_response.ok: print(koala_response) koala_response = koala_response.json() - first_name = koala_response['first_name'] - infix = koala_response['infix'] if 'infix' in koala_response else "" - last_name = koala_response['last_name'] - user.name = f'{first_name} {infix} {last_name}' - user.birthday = datetime.strptime(koala_response['birth_date'], '%Y-%m-%d') + first_name = koala_response["first_name"] + infix = koala_response["infix"] if "infix" in koala_response else "" + last_name = koala_response["last_name"] + user.name = f"{first_name} {infix} {last_name}" + user.birthday = datetime.strptime( + koala_response["birth_date"], "%Y-%m-%d" + ) user.save() return HttpResponse(status=200) @@ -318,14 +313,13 @@ def send_confirmation(email, card): CardConfirmation.objects.create(card=card, token=token) requests.post( - f'https://api.mailgun.net/v3/{settings.MAILGUN_ENV}/messages', - auth=('api', settings.MAILGUN_TOKEN), + f"https://api.mailgun.net/v3/{settings.MAILGUN_ENV}/messages", + auth=("api", settings.MAILGUN_TOKEN), data={ - 'from': f'Undead Mongoose ', - 'to': email, - 'subject': 'Mongoose Card Confirmation', - 'text': - f""" + "from": f"Undead Mongoose ", + "to": email, + "subject": "Mongoose Card Confirmation", + "text": f""" Beste sticky lid, Je hebt zojuist een nieuwe kaart gekoppeld aan Mongoose. @@ -334,54 +328,61 @@ def send_confirmation(email, card): Kusjes en knuffels, Sticky bestuur - """ - } + """, + }, ) + @dashboard_authenticated @require_http_methods(["POST"]) def topup(request): mollie_client = Client() mollie_client.set_api_key(settings.MOLLIE_API_KEY) form = create_TopUpForm(mollie_client) - + bound_form = form(request.POST) - + if bound_form.is_valid(): user = User.objects.get(email=request.user) transaction = IDealTransaction.objects.create( - user_id=user, - transaction_sum=bound_form.cleaned_data["amount"] + user_id=user, transaction_sum=bound_form.cleaned_data["amount"] ) - webhook_url = request.build_absolute_uri(f'/api/payment/webhook?transaction_id={transaction.transaction_id}') - redirect_url = request.build_absolute_uri(f'/?transaction_id={transaction.transaction_id}') + webhook_url = request.build_absolute_uri( + f"/api/payment/webhook?transaction_id={transaction.transaction_id}" + ) + redirect_url = request.build_absolute_uri( + f"/?transaction_id={transaction.transaction_id}" + ) - payment = mollie_client.payments.create({ - 'amount': { - 'currency': 'EUR', - 'value': f'{bound_form.cleaned_data["amount"]:.2f}' - }, - 'description': 'Top up mongoose balance', - 'redirectUrl': redirect_url, - 'webhookUrl': webhook_url, - 'method': 'ideal', - 'issuer': bound_form.cleaned_data['issuer'] - }) + payment = mollie_client.payments.create( + { + "amount": { + "currency": "EUR", + "value": f'{bound_form.cleaned_data["amount"]:.2f}', + }, + "description": "Top up mongoose balance", + "redirectUrl": redirect_url, + "webhookUrl": webhook_url, + "method": "ideal", + "issuer": bound_form.cleaned_data["issuer"], + } + ) return redirect(payment.checkout_url) else: return HttpResponse(status=400) + @csrf_exempt @require_http_methods(["POST"]) def payment_webhook(request): mollie_client = Client() mollie_client.set_api_key(settings.MOLLIE_API_KEY) - payment = mollie_client.payments.get(request.POST['id']) + payment = mollie_client.payments.get(request.POST["id"]) transaction_id = request.GET["transaction_id"] transaction = IDealTransaction.objects.get(transaction_id=transaction_id) - + if payment.is_paid(): transaction.status = PaymentStatus.PAID elif payment.is_pending(): @@ -392,9 +393,11 @@ def payment_webhook(request): transaction.status = PaymentStatus.CANCELLED transaction.save() - + return HttpResponse(status=200) + @require_http_methods(["GET"]) def payment_success(request): - return HttpResponse(status=200) \ No newline at end of file + return HttpResponse(status=200) + From 7319fd171ba80855afd504c6f69761192c9decca Mon Sep 17 00:00:00 2001 From: Job Vonk Date: Tue, 17 Dec 2024 08:46:55 +0100 Subject: [PATCH 08/10] Added CSRF_TRUSTED_ORIGINS to .env and settings --- undead_mongoose/settings.py | 143 ++++++++++++++++-------------------- 1 file changed, 64 insertions(+), 79 deletions(-) diff --git a/undead_mongoose/settings.py b/undead_mongoose/settings.py index 6403ed8..76bbab9 100644 --- a/undead_mongoose/settings.py +++ b/undead_mongoose/settings.py @@ -1,18 +1,5 @@ -""" -Django settings for undead_mongoose project. - -Generated by 'django-admin startproject' using Django 3.2.9. - -For more information on this file, see -https://docs.djangoproject.com/en/3.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/3.2/ref/settings/ -""" - from pathlib import Path import os -from datetime import time # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -28,65 +15,65 @@ DEBUG = os.getenv("DEBUG") ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "").split(" ") - +CSRF_TRUSTED_ORIGINS = os.getenv("CSRF_TRUSTED_ORIGINS", "").split(" ") # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'mozilla_django_oidc', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'mongoose_app', - 'admin_board_view', - 'mongoose_app.apps.CustomConstance', - 'constance.backends.database' + "django.contrib.admin", + "django.contrib.auth", + "mozilla_django_oidc", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "mongoose_app", + "admin_board_view", + "mongoose_app.apps.CustomConstance", + "constance.backends.database", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'undead_mongoose.urls' +ROOT_URLCONF = "undead_mongoose.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', + "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 = 'undead_mongoose.wsgi.application' +WSGI_APPLICATION = "undead_mongoose.wsgi.application" # Database # https://docs.djangoproject.com/en/3.2/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'mongoose', - 'USER': os.getenv("DB_USER"), - 'PASSWORD': os.getenv("DB_PASSWORD"), - 'HOST': os.getenv("DB_HOST"), - 'PORT': os.getenv("DB_PORT"), + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": "mongoose", + "USER": os.getenv("DB_USER"), + "PASSWORD": os.getenv("DB_PASSWORD"), + "HOST": os.getenv("DB_HOST"), + "PORT": os.getenv("DB_PORT"), } } @@ -96,30 +83,28 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] -AUTHENTICATION_BACKENDS = [ - 'undead_mongoose.oidc.UndeadMongooseOIDC' -] +AUTHENTICATION_BACKENDS = ["undead_mongoose.oidc.UndeadMongooseOIDC"] # Internationalization # https://docs.djangoproject.com/en/3.2/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = os.getenv('TIMEZONE') +TIME_ZONE = os.getenv("TIMEZONE") USE_I18N = True @@ -131,47 +116,47 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" # Default primary key field type # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -MEDIA_URL = '/images/' +MEDIA_URL = "/images/" -MEDIA_ROOT = os.path.join(BASE_DIR, 'images') +MEDIA_ROOT = os.path.join(BASE_DIR, "images") -API_TOKEN = os.getenv('API_TOKEN') +API_TOKEN = os.getenv("API_TOKEN") -BASE_URL = os.getenv('BASE_URL') +BASE_URL = os.getenv("BASE_URL") -USER_URL = os.getenv('USER_URL') -USER_TOKEN = os.getenv('USER_TOKEN') +USER_URL = os.getenv("USER_URL") +USER_TOKEN = os.getenv("USER_TOKEN") -MAILGUN_ENV = os.getenv('MAILGUN_ENV') -MAILGUN_TOKEN = os.getenv('MAILGUN_TOKEN') +MAILGUN_ENV = os.getenv("MAILGUN_ENV") +MAILGUN_TOKEN = os.getenv("MAILGUN_TOKEN") -CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend' +CONSTANCE_BACKEND = "constance.backends.database.DatabaseBackend" CONSTANCE_CONFIG = {} -CONSTANCE_DATABASE_PREFIX = 'constance:settings:' +CONSTANCE_DATABASE_PREFIX = "constance:settings:" -STATIC_ROOT = './static/' +STATIC_ROOT = "./static/" -OIDC_RP_CLIENT_ID = os.environ['OIDC_RP_CLIENT_ID'] -OIDC_RP_CLIENT_SECRET = os.environ['OIDC_RP_CLIENT_SECRET'] +OIDC_RP_CLIENT_ID = os.environ["OIDC_RP_CLIENT_ID"] +OIDC_RP_CLIENT_SECRET = os.environ["OIDC_RP_CLIENT_SECRET"] -OIDC_OP_AUTHORIZATION_ENDPOINT = os.environ['OIDC_OP_AUTHORIZATION_ENDPOINT'] -OIDC_OP_TOKEN_ENDPOINT = os.environ['OIDC_OP_TOKEN_ENDPOINT'] -OIDC_OP_USER_ENDPOINT = os.environ['OIDC_OP_USER_ENDPOINT'] +OIDC_OP_AUTHORIZATION_ENDPOINT = os.environ["OIDC_OP_AUTHORIZATION_ENDPOINT"] +OIDC_OP_TOKEN_ENDPOINT = os.environ["OIDC_OP_TOKEN_ENDPOINT"] +OIDC_OP_USER_ENDPOINT = os.environ["OIDC_OP_USER_ENDPOINT"] LOGIN_REDIRECT_URL = "/" LOGOUT_REDIRECT_URL = "/" OIDC_RP_SCOPES = "openid profile email member-read" OIDC_RP_SIGN_ALGO = "RS256" -OIDC_OP_JWKS_ENDPOINT=os.environ['OIDC_OP_JWKS_ENDPOINT'] +OIDC_OP_JWKS_ENDPOINT = os.environ["OIDC_OP_JWKS_ENDPOINT"] -MOLLIE_API_KEY = os.getenv("MOLLIE_API_KEY") \ No newline at end of file +MOLLIE_API_KEY = os.getenv("MOLLIE_API_KEY") From 1dc8d858ca0d0ae6b49847405a39132ae1e38dca Mon Sep 17 00:00:00 2001 From: Job Vonk Date: Tue, 17 Dec 2024 13:50:36 +0100 Subject: [PATCH 09/10] Remove issuers from top up form --- admin_board_view/forms.py | 9 +++------ admin_board_view/views.py | 5 ++--- mongoose_app/urls.py | 1 - mongoose_app/views.py | 13 ++----------- 4 files changed, 7 insertions(+), 21 deletions(-) diff --git a/admin_board_view/forms.py b/admin_board_view/forms.py index 9284152..2537cd0 100644 --- a/admin_board_view/forms.py +++ b/admin_board_view/forms.py @@ -1,8 +1,5 @@ from django import forms -def create_TopUpForm(mollie_client): - issuers = [(issuer.id, issuer.name) for issuer in mollie_client.methods.get("ideal", include="issuers").issuers] - class TopUpForm(forms.Form): - amount = forms.DecimalField(min_value=0, decimal_places=2, required=True) - issuer = forms.ChoiceField(choices=issuers, label="Bank") - return TopUpForm \ No newline at end of file + +class TopUpForm(forms.Form): + amount = forms.DecimalField(min_value=0, decimal_places=2, required=True) diff --git a/admin_board_view/views.py b/admin_board_view/views.py index a206b78..8959a5f 100644 --- a/admin_board_view/views.py +++ b/admin_board_view/views.py @@ -12,7 +12,7 @@ from .models import * from mollie.api.client import Client from django.conf import settings -from .forms import create_TopUpForm +from .forms import TopUpForm @dashboard_authenticated @@ -48,7 +48,6 @@ def index(request): mollie_client = Client() mollie_client.set_api_key(settings.MOLLIE_API_KEY) - form = create_TopUpForm(mollie_client) transaction_id = request.GET.dict().get("transaction_id") transaction = ( @@ -81,7 +80,7 @@ def index(request): "user_info": user, "top_ups": top_up_page, "sales": sales_page, - "form": form, + "form": TopUpForm, "transaction": transaction, "PaymentStatus": PaymentStatus, }, diff --git a/mongoose_app/urls.py b/mongoose_app/urls.py index 1a0ad53..ec819e8 100644 --- a/mongoose_app/urls.py +++ b/mongoose_app/urls.py @@ -14,5 +14,4 @@ path("catchwebhook", views.on_webhook, name="receive_update"), path("topup", views.topup, name="topup"), path("payment/webhook", views.payment_webhook, name="payment_webhook"), - path("payment/success", views.payment_success, name="payment_success"), ] diff --git a/mongoose_app/views.py b/mongoose_app/views.py index 09b697b..03903b6 100644 --- a/mongoose_app/views.py +++ b/mongoose_app/views.py @@ -7,7 +7,7 @@ from decimal import Decimal from django.conf import settings -from admin_board_view.forms import create_TopUpForm +from admin_board_view.forms import TopUpForm from admin_board_view.middleware import dashboard_authenticated from .middleware import authenticated from .models import ( @@ -338,9 +338,7 @@ def send_confirmation(email, card): def topup(request): mollie_client = Client() mollie_client.set_api_key(settings.MOLLIE_API_KEY) - form = create_TopUpForm(mollie_client) - - bound_form = form(request.POST) + bound_form = TopUpForm(request.POST) if bound_form.is_valid(): user = User.objects.get(email=request.user) @@ -365,7 +363,6 @@ def topup(request): "redirectUrl": redirect_url, "webhookUrl": webhook_url, "method": "ideal", - "issuer": bound_form.cleaned_data["issuer"], } ) return redirect(payment.checkout_url) @@ -395,9 +392,3 @@ def payment_webhook(request): transaction.save() return HttpResponse(status=200) - - -@require_http_methods(["GET"]) -def payment_success(request): - return HttpResponse(status=200) - From 9095c3097cd3d4018e2b9b8d34d88ac7662b27a0 Mon Sep 17 00:00:00 2001 From: Job Vonk Date: Tue, 17 Dec 2024 13:54:28 +0100 Subject: [PATCH 10/10] Completed README.md and allowed MOLLIE_API_TOKEN to be empty --- README.md | 34 +++++++++++++++++++++++++--------- admin_board_view/views.py | 3 --- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index fcea321..ce55a9b 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,7 @@ Copy `sample.env` to `.env` and make sure the database options are correct. By d ```bash docker compose up -d -uv run --env-file .env ./manage.py migrate -``` - -In development, create an admin superuser - -```bash -uv run --env-file .env ./manage.py createsuperuser +uv run --env-file .env manage.py migrate ``` Then depending on whether you want to use a local version of koala, you need to do some additional setup: @@ -78,12 +72,34 @@ Then depending on whether you want to use a local version of koala, you need to Copy the application id and secret into the `.env` file and make sure you update the oauth urls to point to koala.dev.svsticky.nl. + Then complete the `.env` file by filling out the following values: + + ```env + USER_URL=https://koala.dev.svsticky.nl + + ALLOWED_HOSTS=localhost + OIDC_RP_CLIENT_ID= + OIDC_RP_CLIENT_SECRET= + + OIDC_OP_AUTHORIZATION_ENDPOINT=https://koala.dev.svsticky.nl/api/oauth/authorize + OIDC_OP_TOKEN_ENDPOINT=https://koala.dev.svsticky.nl/api/oauth/token + OIDC_OP_USER_ENDPOINT=https://koala.dev.svsticky.nl/oauth/userinfo + OIDC_OP_JWKS_ENDPOINT=https://koala.dev.svsticky.nl/oauth/discovery/keys + OIDC_OP_LOGOUT_ENDPOINT=https://koala.dev.svsticky.nl/signout + ``` + +Lastly, make sure you have the mollie api key if you want to work with the iDeal payment system. If you leave it blank, mongoose will still work, except for submitting the top up form. For development you want to use a test token, which can be found in the IT Crowd bitwarden. + +```env +MOLLIE_API_KEY=test_ +``` + ## Running ``` bash -# Database +# Start the database, if it wasn't already running docker compose up -d # Server -uv run --env-file .env ./manage.py runserver +uv run --env-file .env manage.py runserver ``` diff --git a/admin_board_view/views.py b/admin_board_view/views.py index 8959a5f..fb3113e 100644 --- a/admin_board_view/views.py +++ b/admin_board_view/views.py @@ -46,9 +46,6 @@ def index(request): ) sales_page = create_paginator(product_sale_groups, request.GET.get("sales")) - mollie_client = Client() - mollie_client.set_api_key(settings.MOLLIE_API_KEY) - transaction_id = request.GET.dict().get("transaction_id") transaction = ( IDealTransaction.objects.get(transaction_id=transaction_id)