feat: shop app; billing address apis
This commit is contained in:
parent
3644a0d77d
commit
ab3dcd378e
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/",
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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]
|
||||
|
|
@ -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"
|
||||
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
@ -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)
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -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/")
|
||||
|
|
@ -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)
|
||||
Loading…
Reference in New Issue