feat: shop app; billing address apis

This commit is contained in:
Livio Bieri 2023-11-14 12:38:53 +01:00 committed by Christian Cueni
parent 3644a0d77d
commit ab3dcd378e
17 changed files with 356 additions and 11 deletions

View File

@ -131,6 +131,7 @@ LOCAL_APPS = [
"vbv_lernwelt.importer",
"vbv_lernwelt.edoniq_test",
"vbv_lernwelt.course_session_group",
"vbv_lernwelt.shop",
]
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS

View File

@ -57,6 +57,7 @@ from vbv_lernwelt.importer.views import (
t2l_sync,
)
from vbv_lernwelt.notify.views import email_notification_settings
from vbv_lernwelt.shop.views import get_billing_address, update_billing_address
class SignedIntConverter(IntConverter):
@ -170,6 +171,10 @@ urlpatterns = [
path(r'api/core/edoniq-test/export-users-trainers/', export_students_and_trainers,
name='edoniq_export_students_and_trainers'),
# shop
path('api/shop/billing-address/', get_billing_address, name='get-billing-address'),
path('api/shop/billing-address/update', update_billing_address, name='update-billing-address'),
# importer
path(
r"server/importer/coursesession-trainer-import/",

View File

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View File

@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

View File

@ -150,6 +150,8 @@ def webhook():
Checks the Datatrans-Signature header of the incoming request and validates the signature:
https://api-reference.datatrans.ch/#section/Webhook/Webhook-signing
"""
# TODO Check the state here too!
hmac_key = HMAC_KEY

View File

@ -0,0 +1,22 @@
from django.contrib import admin
from vbv_lernwelt.shop.models import CheckoutInformation
@admin.action(description="Create invoice for selected checkouts")
def generate_invoice(modeladmin, request, queryset):
pass
@admin.register(CheckoutInformation)
class CheckoutInformationAdmin(admin.ModelAdmin):
list_display = (
"product_sku",
"user",
"product_name",
"product_price",
"updated_at",
"state",
"invoice_transmitted_at",
)
actions = [generate_invoice]

View File

@ -1,6 +1,6 @@
from django.apps import AppConfig
class PaymentConfig(AppConfig):
class ShopConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "payment"
name = "vbv_lernwelt.shop"

View File

@ -0,0 +1,76 @@
# Generated by Django 3.2.20 on 2023-11-14 10:46
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Product',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('price', models.IntegerField()),
('sku', models.CharField(choices=[('1', 'VV')], max_length=255, unique=True)),
('name', models.CharField(max_length=255)),
('description', models.CharField(max_length=255)),
],
),
migrations.CreateModel(
name='CheckoutInformation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('product_sku', models.CharField(choices=[('1', 'VV')], max_length=255)),
('product_price', models.IntegerField()),
('product_name', models.CharField(max_length=255)),
('product_description', models.CharField(max_length=255)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('state', models.CharField(choices=[('initialized', 'initialized'), ('settled', 'settled'), ('canceled', 'canceled'), ('failed', 'failed')], max_length=255)),
('invoice_transmitted_at', models.DateTimeField(blank=True, null=True)),
('transaction_id', models.CharField(max_length=255)),
('first_name', models.CharField(max_length=255)),
('last_name', models.CharField(max_length=255)),
('street_address', models.CharField(max_length=255)),
('street_number_address', models.CharField(max_length=255)),
('postal_code', models.CharField(max_length=255)),
('city', models.CharField(max_length=255)),
('country', models.CharField(max_length=255)),
('company_name', models.CharField(blank=True, max_length=255)),
('company_street_address', models.CharField(blank=True, max_length=255)),
('company_street_number_address', models.CharField(blank=True, max_length=255)),
('company_postal_code', models.CharField(blank=True, max_length=255)),
('company_city', models.CharField(blank=True, max_length=255)),
('company_country', models.CharField(blank=True, max_length=255)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='BillingAddress',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(blank=True, max_length=255)),
('last_name', models.CharField(blank=True, max_length=255)),
('street_address', models.CharField(blank=True, max_length=255)),
('street_number_address', models.CharField(blank=True, max_length=255)),
('postal_code', models.CharField(blank=True, max_length=255)),
('city', models.CharField(blank=True, max_length=255)),
('country', models.CharField(blank=True, max_length=255)),
('company_name', models.CharField(blank=True, max_length=255)),
('company_street_address', models.CharField(blank=True, max_length=255)),
('company_street_number_address', models.CharField(blank=True, max_length=255)),
('company_postal_code', models.CharField(blank=True, max_length=255)),
('company_city', models.CharField(blank=True, max_length=255)),
('company_country', models.CharField(blank=True, max_length=255)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,83 @@
from django.db import models
class BillingAddress(models.Model):
"""
Draft of a billing address for a purchase from the shop.
"""
user = models.ForeignKey("core.User", on_delete=models.CASCADE)
# user
first_name = models.CharField(max_length=255, blank=True)
last_name = models.CharField(max_length=255, blank=True)
street_address = models.CharField(max_length=255, blank=True)
street_number_address = models.CharField(max_length=255, blank=True)
postal_code = models.CharField(max_length=255, blank=True)
city = models.CharField(max_length=255, blank=True)
country = models.CharField(max_length=255, blank=True)
# company (optional)
company_name = models.CharField(max_length=255, blank=True)
company_street_address = models.CharField(max_length=255, blank=True)
company_street_number_address = models.CharField(max_length=255, blank=True)
company_postal_code = models.CharField(max_length=255, blank=True)
company_city = models.CharField(max_length=255, blank=True)
company_country = models.CharField(max_length=255, blank=True)
SKUS = [("1", "VV")]
class Product(models.Model):
price = models.IntegerField() # 10_00 = 10.00 CHF
sku = models.CharField(max_length=255, unique=True, choices=SKUS)
name = models.CharField(max_length=255)
description = models.CharField(max_length=255)
class CheckoutInformation(models.Model):
"""
Immutable checkout information for a purchase from the shop.
"""
user = models.ForeignKey("core.User", on_delete=models.PROTECT)
# immutable product information at time of purchase
product_sku = models.CharField(max_length=255, choices=SKUS)
product_price = models.IntegerField() # 10_00 = 10.00 CHF
product_name = models.CharField(max_length=255)
product_description = models.CharField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
state = models.CharField(
max_length=255,
choices=[
("initialized", "initialized"),
("settled", "settled"),
("canceled", "canceled"),
("failed", "failed"),
],
)
invoice_transmitted_at = models.DateTimeField(blank=True, null=True)
transaction_id = models.CharField(max_length=255)
# end user (required)
first_name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255)
street_address = models.CharField(max_length=255)
street_number_address = models.CharField(max_length=255)
postal_code = models.CharField(max_length=255)
city = models.CharField(max_length=255)
country = models.CharField(max_length=255)
# company (optional)
company_name = models.CharField(max_length=255, blank=True)
company_street_address = models.CharField(max_length=255, blank=True)
company_street_number_address = models.CharField(max_length=255, blank=True)
company_postal_code = models.CharField(max_length=255, blank=True)
company_city = models.CharField(max_length=255, blank=True)
company_country = models.CharField(max_length=255, blank=True)

View File

@ -0,0 +1,23 @@
from rest_framework import serializers
from .models import BillingAddress
class BillingAddressSerializer(serializers.ModelSerializer):
class Meta:
model = BillingAddress
fields = [
"first_name",
"last_name",
"street_address",
"street_number_address",
"postal_code",
"city",
"country",
"company_name",
"company_street_address",
"company_street_number_address",
"company_postal_code",
"company_city",
"company_country",
]

View File

@ -0,0 +1,106 @@
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from vbv_lernwelt.core.admin import User
from vbv_lernwelt.shop.models import BillingAddress
class BillingAddressViewTest(APITestCase):
def setUp(self) -> None:
self.user = User.objects.create_user(
"testuser", "test@example.com", "testpassword"
)
self.client.login(username="testuser", password="testpassword")
self.billing_address = BillingAddress.objects.create(
user=self.user,
first_name="John",
last_name="Doe",
street_address="123 Main St",
street_number_address="45A",
postal_code="12345",
city="Test City",
country="Test Country",
company_name="Test Company",
company_street_address="456 Company St",
company_street_number_address="67B",
company_postal_code="67890",
company_city="Company City",
company_country="Company Country",
)
def test_get_billing_address(self) -> None:
# GIVEN
# user is logged in and has a billing address
# WHEN
url = reverse("get-billing-address")
response = self.client.get(url)
# THEN
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["first_name"], "John")
self.assertEqual(response.data["last_name"], "Doe")
self.assertEqual(response.data["street_address"], "123 Main St")
self.assertEqual(response.data["street_number_address"], "45A")
self.assertEqual(response.data["postal_code"], "12345")
self.assertEqual(response.data["city"], "Test City")
self.assertEqual(response.data["country"], "Test Country")
self.assertEqual(response.data["company_name"], "Test Company")
self.assertEqual(response.data["company_street_address"], "456 Company St")
self.assertEqual(response.data["company_street_number_address"], "67B")
self.assertEqual(response.data["company_postal_code"], "67890")
self.assertEqual(response.data["company_city"], "Company City")
self.assertEqual(response.data["company_country"], "Company Country")
def test_update_billing_address(self) -> None:
# GIVEN
new_data = {
"first_name": "Jane",
"last_name": "Smith",
"street_address": "789 New St",
"street_number_address": "101C",
"postal_code": "54321",
"city": "New City",
"country": "New Country",
"company_name": "New Company",
"company_street_address": "789 Company St",
"company_street_number_address": "102D",
"company_postal_code": "98765",
"company_city": "New Company City",
"company_country": "New Company Country",
}
# WHEN
url = reverse("update-billing-address")
response = self.client.put(url, new_data)
# THEN
self.assertEqual(response.status_code, status.HTTP_200_OK)
updated_address = BillingAddress.objects.get(user=self.user)
self.assertEqual(updated_address.first_name, "Jane")
self.assertEqual(updated_address.last_name, "Smith")
self.assertEqual(updated_address.street_address, "789 New St")
self.assertEqual(updated_address.street_number_address, "101C")
self.assertEqual(updated_address.postal_code, "54321")
self.assertEqual(updated_address.city, "New City")
self.assertEqual(updated_address.country, "New Country")
self.assertEqual(updated_address.company_name, "New Company")
self.assertEqual(updated_address.company_street_address, "789 Company St")
self.assertEqual(updated_address.company_street_number_address, "102D")
self.assertEqual(updated_address.company_postal_code, "98765")
self.assertEqual(updated_address.company_city, "New Company City")
self.assertEqual(updated_address.company_country, "New Company Country")
def test_unauthenticated_access(self) -> None:
# GIVEN
self.client.logout()
# WHEN
get_response = self.client.get(reverse("get-billing-address"))
put_response = self.client.put(reverse("update-billing-address"), {})
# THEN
self.assertTrue(get_response["Location"], "/login/")
self.assertTrue(put_response["Location"], "/login/")

View File

@ -0,0 +1,36 @@
from django.shortcuts import get_object_or_404
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from vbv_lernwelt.shop.models import BillingAddress
from vbv_lernwelt.shop.serializers import BillingAddressSerializer
@api_view(["GET"])
@permission_classes([IsAuthenticated])
def get_billing_address(request):
billing_address = get_object_or_404(BillingAddress, user=request.user)
serializer = BillingAddressSerializer(billing_address)
return Response(serializer.data)
@api_view(["PUT"])
@permission_classes([IsAuthenticated])
def update_billing_address(request):
try:
billing_address = BillingAddress.objects.get(user=request.user)
except BillingAddress.DoesNotExist:
billing_address = None
serializer = BillingAddressSerializer(
billing_address, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save(user=request.user)
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)