Remove unused BillingAddress model

This commit is contained in:
Daniel Egger 2024-05-30 18:02:46 +02:00
parent 2646b072ee
commit ec21238ece
12 changed files with 218 additions and 366 deletions

View File

@ -4,12 +4,12 @@ import { useEntities } from "@/services/entities";
const props = defineProps<{
modelValue: {
company_name: string;
company_street: string;
company_street_number: string;
company_postal_code: string;
company_city: string;
company_country: string;
organisation_detail_name: string;
organisation_street: string;
organisation_street_number: string;
organisation_postal_code: string;
organisation_city: string;
organisation_country_code: string;
};
}>();
@ -39,7 +39,7 @@ const orgAddress = computed({
<div class="mt-2">
<input
id="company-name"
v-model="orgAddress.company_name"
v-model="orgAddress.organisation_detail_name"
type="text"
required
name="company-name"
@ -57,7 +57,7 @@ const orgAddress = computed({
<div class="mt-2">
<input
id="company-street-address"
v-model="orgAddress.company_street"
v-model="orgAddress.organisation_street"
type="text"
required
name="street-address"
@ -77,7 +77,7 @@ const orgAddress = computed({
<div class="mt-2">
<input
id="company-street-number"
v-model="orgAddress.company_street_number"
v-model="orgAddress.organisation_street_number"
name="street-number"
type="text"
autocomplete="street-number"
@ -95,7 +95,7 @@ const orgAddress = computed({
<div class="mt-2">
<input
id="company-postal-code"
v-model="orgAddress.company_postal_code"
v-model="orgAddress.organisation_postal_code"
type="text"
required
name="postal-code"
@ -115,7 +115,7 @@ const orgAddress = computed({
<div class="mt-2">
<input
id="company-city"
v-model="orgAddress.company_city"
v-model="orgAddress.organisation_city"
type="text"
name="city"
required
@ -135,7 +135,7 @@ const orgAddress = computed({
<div class="mt-2">
<select
id="company-country"
v-model="orgAddress.company_country"
v-model="orgAddress.organisation_country_code"
required
name="country"
autocomplete="country-name"

View File

@ -10,7 +10,7 @@ const props = defineProps<{
street_number: string;
postal_code: string;
city: string;
country: string;
country_code: string;
};
}>();
@ -147,7 +147,7 @@ const address = computed({
<div class="mt-2">
<select
id="country"
v-model="address.country"
v-model="address.country_code"
required
name="country"
autocomplete="country-name"

View File

@ -2,32 +2,15 @@
import WizardPage from "@/components/onboarding/WizardPage.vue";
import type { Ref } from "vue";
import { computed, ref, watch } from "vue";
import { useUserStore } from "@/stores/user";
import { type User, useUserStore } from "@/stores/user";
import PersonalAddress from "@/components/onboarding/PersonalAddress.vue";
import OrganisationAddress from "@/components/onboarding/OrganisationAddress.vue";
import { itPost, itPut } from "@/fetchHelpers";
import { itPost } from "@/fetchHelpers";
import { useEntities } from "@/services/entities";
import { useDebounceFn, useFetch } from "@vueuse/core";
import { useRoute } from "vue-router";
import { useTranslation } from "i18next-vue";
import { getVVCourseName } from "./composables";
type BillingAddressType = {
first_name: string;
last_name: string;
street: string;
street_number: string;
postal_code: string;
city: string;
country: string;
company_name: string;
company_street: string;
company_street_number: string;
company_postal_code: string;
company_city: string;
company_country: string;
};
const props = defineProps({
courseType: {
type: String,
@ -37,7 +20,7 @@ const props = defineProps({
const user = useUserStore();
const route = useRoute();
const { organisations } = useEntities();
const { organisations, countries } = useEntities();
const userOrganisationName = computed(() => {
if (!user.organisation) {
@ -61,56 +44,27 @@ const paymentError = computed(() => {
});
const address = ref({
first_name: "",
last_name: "",
street: "",
street_number: "",
postal_code: "",
city: "",
country: "",
company_name: "",
company_street: "",
company_street_number: "",
company_postal_code: "",
company_city: "",
company_country: "",
first_name: user.first_name,
last_name: user.last_name,
street: user.street,
street_number: user.street_number,
postal_code: user.postal_code,
city: user.city,
country_code: user.country?.country_code ?? "CH",
organisation_detail_name: user.organisation_detail_name,
organisation_street: user.organisation_street,
organisation_street_number: user.organisation_street_number,
organisation_postal_code: user.organisation_postal_code,
organisation_city: user.organisation_city,
organisation_country_code: user.organisation_country?.country_code ?? "CH",
invoice_address: user.invoice_address ?? "prv",
});
const useCompanyAddress = ref(false);
const fetchBillingAddress = useFetch("/api/shop/billing-address/").json();
const billingAddressData: Ref<BillingAddressType | null> = fetchBillingAddress.data;
const useCompanyAddress = ref(user.invoice_address === "org");
watch(billingAddressData, (newVal) => {
if (newVal) {
address.value = newVal;
useCompanyAddress.value = !!newVal.company_name;
}
});
const updateAddress = useDebounceFn(() => {
itPut("/api/shop/billing-address/update/", address.value);
}, 500);
watch(
address,
(newVal, oldVal) => {
if (Object.values(oldVal).every((x) => x === "")) {
return;
}
updateAddress();
},
{ deep: true }
);
const removeCompanyAddress = () => {
useCompanyAddress.value = false;
address.value.company_name = "";
address.value.company_street = "";
address.value.company_street_number = "";
address.value.company_postal_code = "";
address.value.company_city = "";
address.value.company_country = "";
const setUseCompanyAddress = (value: boolean) => {
useCompanyAddress.value = value;
address.value.invoice_address = value ? "org" : "prv";
};
type FormErrors = {
@ -153,44 +107,59 @@ function validateAddress() {
formErrors.value.personal.push(t("a.Ort"));
}
if (!address.value.country) {
if (!address.value.country_code) {
formErrors.value.personal.push(t("a.Land"));
}
if (useCompanyAddress.value) {
if (!address.value.company_name) {
if (!address.value.organisation_detail_name) {
formErrors.value.company.push(t("a.Name"));
}
if (!address.value.company_street) {
if (!address.value.organisation_street) {
formErrors.value.company.push(t("a.Strasse"));
}
if (!address.value.company_street_number) {
if (!address.value.organisation_street_number) {
formErrors.value.company.push(t("a.Hausnummmer"));
}
if (!address.value.company_postal_code) {
if (!address.value.organisation_postal_code) {
formErrors.value.company.push(t("a.PLZ"));
}
if (!address.value.company_city) {
if (!address.value.organisation_city) {
formErrors.value.company.push(t("a.Ort"));
}
if (!address.value.company_country) {
if (!address.value.organisation_country_code) {
formErrors.value.company.push(t("a.Land"));
}
}
}
const executePayment = () => {
validateAddress();
async function saveAddress() {
const { country_code, organisation_country_code, ...profileData } = address.value;
const typedProfileData: Partial<User> = { ...profileData };
typedProfileData.country = countries.value.find(
(c) => c.country_code === country_code
);
typedProfileData.organisation_country = countries.value.find(
(c) => c.country_code === organisation_country_code
);
await user.updateUserProfile(typedProfileData);
}
const executePayment = async () => {
validateAddress();
if (formErrors.value.personal.length > 0 || formErrors.value.company.length > 0) {
return;
}
await saveAddress();
// Where the payment page will redirect to after the payment is done:
// The reason why this is here is convenience: We could also do this in the backend
// then we'd need to configure this for all environments (including Caprover).
@ -242,6 +211,7 @@ const executePayment = () => {
</p>
<h3 class="mb-4 mt-10">{{ $t("a.Adresse") }}</h3>
<pre>{{ address }}</pre>
<p class="mb-2">
{{
$t(
@ -266,7 +236,7 @@ const executePayment = () => {
<button
v-if="!useCompanyAddress"
class="underline"
@click="useCompanyAddress = true"
@click="setUseCompanyAddress(true)"
>
<template v-if="userOrganisationName">
{{
@ -296,7 +266,7 @@ const executePayment = () => {
}}
</h3>
<h3 v-else>{{ $t("a.Rechnungsadresse") }}</h3>
<button class="underline" @click="removeCompanyAddress">
<button class="underline" @click="setUseCompanyAddress(false)">
{{ $t("a.Entfernen") }}
</button>
</div>

View File

@ -31,16 +31,16 @@ export interface User {
language: AvailableLanguages;
course_session_experts: string[];
invoice_address: InvoiceAddress | null;
street: string | null;
street_number: string | null;
postal_code: string | null;
city: string | null;
street: string;
street_number: string;
postal_code: string;
city: string;
country: Country | null;
organisation_detail_name: string | null;
organisation_street: string | null;
organisation_street_number: string | null;
organisation_postal_code: string | null;
organisation_city: string | null;
organisation_detail_name: string;
organisation_street: string;
organisation_street_number: string;
organisation_postal_code: string;
organisation_city: string;
organisation_country: Country | null;
}
@ -74,16 +74,16 @@ const initialUserState: User = {
loggedIn: false,
language: defaultLanguage,
invoice_address: "prv",
street: null,
street_number: null,
postal_code: null,
city: null,
street: "",
street_number: "",
postal_code: "",
city: "",
country: null,
organisation_detail_name: null,
organisation_street: null,
organisation_street_number: null,
organisation_postal_code: null,
organisation_city: null,
organisation_detail_name: "",
organisation_street: "",
organisation_street_number: "",
organisation_postal_code: "",
organisation_city: "",
organisation_country: null,
};

View File

@ -77,7 +77,7 @@ class Migration(migrations.Migration):
blank=True,
null=True,
on_delete=models.deletion.SET_NULL,
related_name="user_country",
related_name="+",
to="core.country",
),
),
@ -88,7 +88,7 @@ class Migration(migrations.Migration):
blank=True,
null=True,
on_delete=models.deletion.SET_NULL,
related_name="organisation_country",
related_name="+",
to="core.country",
),
),

View File

@ -92,7 +92,7 @@ class User(AbstractUser):
city = models.CharField(max_length=255, blank=True)
country = models.ForeignKey(
Country,
related_name="user_country",
related_name="+",
on_delete=models.SET_NULL,
null=True,
blank=True,
@ -105,7 +105,7 @@ class User(AbstractUser):
organisation_city = models.CharField(max_length=255, blank=True)
organisation_country = models.ForeignKey(
Country,
related_name="organisation_country",
related_name="+",
on_delete=models.SET_NULL,
null=True,
blank=True,

View File

@ -3,9 +3,23 @@
from django.db import migrations, models
def migrate_checkout_information_country(apps, schema_editor):
CheckoutInformation = apps.get_model("shop", "CheckoutInformation")
Country = apps.get_model("core", "Country")
for info in CheckoutInformation.objects.all():
if info.old_country:
country = Country.objects.get(vbv_country_id=info.old_country)
info.country = country
if info.old_company_country:
country = Country.objects.get(vbv_country_id=info.old_company_country)
info.organisation_country = country
info.save(update_fields=["country", "organisation_country"])
class Migration(migrations.Migration):
dependencies = [
("shop", "0012_delete_country"),
("core", "0009_country_refactor"),
]
operations = [
@ -14,4 +28,79 @@ class Migration(migrations.Migration):
name="abacus_order_id",
field=models.BigIntegerField(blank=True, null=True, unique=True),
),
migrations.RenameField(
model_name="checkoutinformation",
old_name="company_name",
new_name="organisation_detail_name",
),
migrations.RenameField(
model_name="checkoutinformation",
old_name="company_street",
new_name="organisation_street",
),
migrations.RenameField(
model_name="checkoutinformation",
old_name="company_street_number",
new_name="organisation_street_number",
),
migrations.RenameField(
model_name="checkoutinformation",
old_name="company_postal_code",
new_name="organisation_postal_code",
),
migrations.RenameField(
model_name="checkoutinformation",
old_name="company_city",
new_name="organisation_city",
),
migrations.RenameField(
model_name="checkoutinformation",
old_name="country",
new_name="old_country",
),
migrations.RenameField(
model_name="checkoutinformation",
old_name="company_country",
new_name="old_company_country",
),
migrations.AddField(
model_name="checkoutinformation",
name="invoice_address",
field=models.CharField(
choices=[("prv", "Private"), ("org", "Organisation")],
default="prv",
max_length=3,
),
),
migrations.AddField(
model_name="checkoutinformation",
name="country",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=models.deletion.SET_NULL,
related_name="+",
to="core.country",
),
),
migrations.AddField(
model_name="checkoutinformation",
name="organisation_country",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=models.deletion.SET_NULL,
related_name="+",
to="core.country",
),
),
migrations.RunPython(migrate_checkout_information_country),
migrations.RemoveField(
model_name="checkoutinformation",
name="old_country",
),
migrations.RemoveField(
model_name="checkoutinformation",
name="old_company_country",
),
]

View File

@ -1,34 +1,7 @@
from django.db import models
from django.db.models import Max
class BillingAddress(models.Model):
"""
Draft of a billing address for a purchase from the shop.
"""
user = models.OneToOneField(
"core.User",
on_delete=models.CASCADE,
primary_key=True,
)
# user
first_name = models.CharField(max_length=255, blank=True)
last_name = models.CharField(max_length=255, blank=True)
street = models.CharField(max_length=255, blank=True)
street_number = 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 = models.CharField(max_length=255, blank=True)
company_street_number = 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)
from vbv_lernwelt.core.models import Country
class Product(models.Model):
@ -62,6 +35,14 @@ class CheckoutState(models.TextChoices):
class CheckoutInformation(models.Model):
INVOICE_ADDRESS_PRIVATE = "prv"
INVOICE_ADDRESS_ORGANISATION = "org"
INVOICE_ADDRESS_CHOICES = (
(INVOICE_ADDRESS_PRIVATE, "Private"),
(INVOICE_ADDRESS_ORGANISATION, "Organisation"),
)
user = models.ForeignKey("core.User", on_delete=models.PROTECT)
product_sku = models.CharField(max_length=255)
@ -89,15 +70,31 @@ class CheckoutInformation(models.Model):
street_number = models.CharField(max_length=255)
postal_code = models.CharField(max_length=255)
city = models.CharField(max_length=255)
country = models.CharField(max_length=255)
country = models.ForeignKey(
Country,
related_name="+",
on_delete=models.SET_NULL,
null=True,
blank=True,
)
# company (optional)
company_name = models.CharField(max_length=255, blank=True)
company_street = models.CharField(max_length=255, blank=True)
company_street_number = 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)
invoice_address = models.CharField(
max_length=3, choices=INVOICE_ADDRESS_CHOICES, default="prv"
)
# organisation data (optional)
organisation_detail_name = models.CharField(max_length=255, blank=True)
organisation_street = models.CharField(max_length=255, blank=True)
organisation_street_number = models.CharField(max_length=255, blank=True)
organisation_postal_code = models.CharField(max_length=255, blank=True)
organisation_city = models.CharField(max_length=255, blank=True)
organisation_country = models.ForeignKey(
Country,
related_name="+",
on_delete=models.SET_NULL,
null=True,
blank=True,
)
# webhook metadata
webhook_history = models.JSONField(default=list)

View File

@ -1,23 +1 @@
from rest_framework import serializers
from .models import BillingAddress
class BillingAddressSerializer(serializers.ModelSerializer):
class Meta:
model = BillingAddress
fields = [
"first_name",
"last_name",
"street",
"street_number",
"postal_code",
"city",
"country",
"company_name",
"company_street",
"company_street_number",
"company_postal_code",
"company_city",
"company_country",
]

View File

@ -1,106 +0,0 @@
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="123 Main St",
street_number="45A",
postal_code="12345",
city="Test City",
country="Test Country",
company_name="Test Company",
company_street="456 Company St",
company_street_number="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"], "123 Main St")
self.assertEqual(response.data["street_number"], "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"], "456 Company St")
self.assertEqual(response.data["company_street_number"], "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": "789 New St",
"street_number": "101C",
"postal_code": "54321",
"city": "New City",
"country": "New Country",
"company_name": "New Company",
"company_street": "789 Company St",
"company_street_number": "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, "789 New St")
self.assertEqual(updated_address.street_number, "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, "789 Company St")
self.assertEqual(updated_address.company_street_number, "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

@ -1,18 +1,9 @@
from django.urls import path
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
from vbv_lernwelt.shop.views import (
checkout_vv,
get_billing_address,
transaction_webhook,
update_billing_address,
)
from vbv_lernwelt.shop.views import checkout_vv, transaction_webhook
urlpatterns = [
path("billing-address/", get_billing_address, name="get-billing-address"),
path(
"billing-address/update/", update_billing_address, name="update-billing-address"
),
path("vv/checkout/", checkout_vv, name="checkout-vv"),
path(
"transaction/webhook/",

View File

@ -7,7 +7,6 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from sentry_sdk import capture_exception
from vbv_lernwelt.core.models import Country, User
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email
from vbv_lernwelt.shop.const import (
@ -15,13 +14,7 @@ from vbv_lernwelt.shop.const import (
VV_FR_PRODUCT_SKU,
VV_IT_PRODUCT_SKU,
)
from vbv_lernwelt.shop.models import (
BillingAddress,
CheckoutInformation,
CheckoutState,
Product,
)
from vbv_lernwelt.shop.serializers import BillingAddressSerializer
from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState, Product
from vbv_lernwelt.shop.services import (
datatrans_state_to_checkout_state,
get_payment_url,
@ -40,39 +33,6 @@ PRODUCT_SKU_TO_COURSE_SESSION_ID = {
}
@api_view(["GET"])
@permission_classes([IsAuthenticated])
def get_billing_address(request):
try:
billing_address = BillingAddress.objects.get(user=request.user)
data = BillingAddressSerializer(billing_address).data
except BillingAddress.DoesNotExist:
data = BillingAddressSerializer().data
data["first_name"] = request.user.first_name # noqa
data["last_name"] = request.user.last_name # noqa
return Response(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)
@api_view(["POST"])
def transaction_webhook(request):
"""IMPORTANT: This is not called for timed out transactions!"""
@ -125,7 +85,7 @@ def checkout_vv(request):
sku = request.data["product"]
base_redirect_url = request.data["redirect_url"]
logger.info(f"Checkout requested: sku={sku}", user_id=request.user.id)
logger.info(f"Checkout requested: sku", user_id=request.user.id, sku=sku)
try:
product = Product.objects.get(sku=sku)
@ -171,6 +131,15 @@ def checkout_vv(request):
),
)
address_data = request.data["address"]
country_code = address_data.pop("country_code")
address_data["country_id"] = country_code
organisation_country_code = "CH"
if "organisation_country_code" in address_data:
organisation_country_code = address_data.pop("organisation_country_code")
address_data["organisation_country_id"] = organisation_country_code
checkout_info = CheckoutInformation.objects.create(
user=request.user,
state=CheckoutState.ONGOING,
@ -184,8 +153,6 @@ def checkout_vv(request):
**request.data["address"],
)
update_user_address(user=request.user, checkout_info=checkout_info)
return next_step_response(url=get_payment_url(transaction_id))
@ -266,37 +233,3 @@ def checkout_cancel_url(base_url: str) -> str:
def checkout_success_url(product_sku: str, base_url: str = "") -> str:
return f"{base_url}/onboarding/{product_sku}/checkout/complete"
def update_user_address(user: User, checkout_info: CheckoutInformation):
user.street = checkout_info.street
user.street_number = checkout_info.street_number
user.postal_code = checkout_info.postal_code
user.city = checkout_info.city
if checkout_info.country:
user.country = Country.objects.filter(
country_code=checkout_info.country
).first()
if (
checkout_info.company_name
and checkout_info.company_street
and checkout_info.company_street_number
and checkout_info.company_postal_code
and checkout_info.company_city
and checkout_info.company_country
):
user.organisation_detail_name = checkout_info.company_name
user.organisation_street = checkout_info.company_street
user.organisation_street_number = checkout_info.company_street_number
user.organisation_postal_code = checkout_info.company_postal_code
user.organisation_city = checkout_info.company_city
user.organisation_country = Country.objects.filter(
country_code=checkout_info.company_country
).first()
user.invoice_address = User.INVOICE_ADDRESS_ORGANISATION
user.save()