Remove unused BillingAddress model
This commit is contained in:
parent
2646b072ee
commit
ec21238ece
|
|
@ -4,12 +4,12 @@ import { useEntities } from "@/services/entities";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: {
|
modelValue: {
|
||||||
company_name: string;
|
organisation_detail_name: string;
|
||||||
company_street: string;
|
organisation_street: string;
|
||||||
company_street_number: string;
|
organisation_street_number: string;
|
||||||
company_postal_code: string;
|
organisation_postal_code: string;
|
||||||
company_city: string;
|
organisation_city: string;
|
||||||
company_country: string;
|
organisation_country_code: string;
|
||||||
};
|
};
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ const orgAddress = computed({
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<input
|
<input
|
||||||
id="company-name"
|
id="company-name"
|
||||||
v-model="orgAddress.company_name"
|
v-model="orgAddress.organisation_detail_name"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
name="company-name"
|
name="company-name"
|
||||||
|
|
@ -57,7 +57,7 @@ const orgAddress = computed({
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<input
|
<input
|
||||||
id="company-street-address"
|
id="company-street-address"
|
||||||
v-model="orgAddress.company_street"
|
v-model="orgAddress.organisation_street"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
name="street-address"
|
name="street-address"
|
||||||
|
|
@ -77,7 +77,7 @@ const orgAddress = computed({
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<input
|
<input
|
||||||
id="company-street-number"
|
id="company-street-number"
|
||||||
v-model="orgAddress.company_street_number"
|
v-model="orgAddress.organisation_street_number"
|
||||||
name="street-number"
|
name="street-number"
|
||||||
type="text"
|
type="text"
|
||||||
autocomplete="street-number"
|
autocomplete="street-number"
|
||||||
|
|
@ -95,7 +95,7 @@ const orgAddress = computed({
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<input
|
<input
|
||||||
id="company-postal-code"
|
id="company-postal-code"
|
||||||
v-model="orgAddress.company_postal_code"
|
v-model="orgAddress.organisation_postal_code"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
name="postal-code"
|
name="postal-code"
|
||||||
|
|
@ -115,7 +115,7 @@ const orgAddress = computed({
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<input
|
<input
|
||||||
id="company-city"
|
id="company-city"
|
||||||
v-model="orgAddress.company_city"
|
v-model="orgAddress.organisation_city"
|
||||||
type="text"
|
type="text"
|
||||||
name="city"
|
name="city"
|
||||||
required
|
required
|
||||||
|
|
@ -135,7 +135,7 @@ const orgAddress = computed({
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<select
|
<select
|
||||||
id="company-country"
|
id="company-country"
|
||||||
v-model="orgAddress.company_country"
|
v-model="orgAddress.organisation_country_code"
|
||||||
required
|
required
|
||||||
name="country"
|
name="country"
|
||||||
autocomplete="country-name"
|
autocomplete="country-name"
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ const props = defineProps<{
|
||||||
street_number: string;
|
street_number: string;
|
||||||
postal_code: string;
|
postal_code: string;
|
||||||
city: string;
|
city: string;
|
||||||
country: string;
|
country_code: string;
|
||||||
};
|
};
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
@ -147,7 +147,7 @@ const address = computed({
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<select
|
<select
|
||||||
id="country"
|
id="country"
|
||||||
v-model="address.country"
|
v-model="address.country_code"
|
||||||
required
|
required
|
||||||
name="country"
|
name="country"
|
||||||
autocomplete="country-name"
|
autocomplete="country-name"
|
||||||
|
|
|
||||||
|
|
@ -2,32 +2,15 @@
|
||||||
import WizardPage from "@/components/onboarding/WizardPage.vue";
|
import WizardPage from "@/components/onboarding/WizardPage.vue";
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import { computed, ref, watch } 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 PersonalAddress from "@/components/onboarding/PersonalAddress.vue";
|
||||||
import OrganisationAddress from "@/components/onboarding/OrganisationAddress.vue";
|
import OrganisationAddress from "@/components/onboarding/OrganisationAddress.vue";
|
||||||
import { itPost, itPut } from "@/fetchHelpers";
|
import { itPost } from "@/fetchHelpers";
|
||||||
import { useEntities } from "@/services/entities";
|
import { useEntities } from "@/services/entities";
|
||||||
import { useDebounceFn, useFetch } from "@vueuse/core";
|
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { useTranslation } from "i18next-vue";
|
import { useTranslation } from "i18next-vue";
|
||||||
import { getVVCourseName } from "./composables";
|
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({
|
const props = defineProps({
|
||||||
courseType: {
|
courseType: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
@ -37,7 +20,7 @@ const props = defineProps({
|
||||||
|
|
||||||
const user = useUserStore();
|
const user = useUserStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { organisations } = useEntities();
|
const { organisations, countries } = useEntities();
|
||||||
|
|
||||||
const userOrganisationName = computed(() => {
|
const userOrganisationName = computed(() => {
|
||||||
if (!user.organisation) {
|
if (!user.organisation) {
|
||||||
|
|
@ -61,56 +44,27 @@ const paymentError = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const address = ref({
|
const address = ref({
|
||||||
first_name: "",
|
first_name: user.first_name,
|
||||||
last_name: "",
|
last_name: user.last_name,
|
||||||
street: "",
|
street: user.street,
|
||||||
street_number: "",
|
street_number: user.street_number,
|
||||||
postal_code: "",
|
postal_code: user.postal_code,
|
||||||
city: "",
|
city: user.city,
|
||||||
country: "",
|
country_code: user.country?.country_code ?? "CH",
|
||||||
company_name: "",
|
organisation_detail_name: user.organisation_detail_name,
|
||||||
company_street: "",
|
organisation_street: user.organisation_street,
|
||||||
company_street_number: "",
|
organisation_street_number: user.organisation_street_number,
|
||||||
company_postal_code: "",
|
organisation_postal_code: user.organisation_postal_code,
|
||||||
company_city: "",
|
organisation_city: user.organisation_city,
|
||||||
company_country: "",
|
organisation_country_code: user.organisation_country?.country_code ?? "CH",
|
||||||
|
invoice_address: user.invoice_address ?? "prv",
|
||||||
});
|
});
|
||||||
|
|
||||||
const useCompanyAddress = ref(false);
|
const useCompanyAddress = ref(user.invoice_address === "org");
|
||||||
const fetchBillingAddress = useFetch("/api/shop/billing-address/").json();
|
|
||||||
const billingAddressData: Ref<BillingAddressType | null> = fetchBillingAddress.data;
|
|
||||||
|
|
||||||
watch(billingAddressData, (newVal) => {
|
const setUseCompanyAddress = (value: boolean) => {
|
||||||
if (newVal) {
|
useCompanyAddress.value = value;
|
||||||
address.value = newVal;
|
address.value.invoice_address = value ? "org" : "prv";
|
||||||
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 = "";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormErrors = {
|
type FormErrors = {
|
||||||
|
|
@ -153,44 +107,59 @@ function validateAddress() {
|
||||||
formErrors.value.personal.push(t("a.Ort"));
|
formErrors.value.personal.push(t("a.Ort"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!address.value.country) {
|
if (!address.value.country_code) {
|
||||||
formErrors.value.personal.push(t("a.Land"));
|
formErrors.value.personal.push(t("a.Land"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useCompanyAddress.value) {
|
if (useCompanyAddress.value) {
|
||||||
if (!address.value.company_name) {
|
if (!address.value.organisation_detail_name) {
|
||||||
formErrors.value.company.push(t("a.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"));
|
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"));
|
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"));
|
formErrors.value.company.push(t("a.PLZ"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!address.value.company_city) {
|
if (!address.value.organisation_city) {
|
||||||
formErrors.value.company.push(t("a.Ort"));
|
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"));
|
formErrors.value.company.push(t("a.Land"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const executePayment = () => {
|
async function saveAddress() {
|
||||||
validateAddress();
|
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) {
|
if (formErrors.value.personal.length > 0 || formErrors.value.company.length > 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await saveAddress();
|
||||||
|
|
||||||
// Where the payment page will redirect to after the payment is done:
|
// 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
|
// 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).
|
// then we'd need to configure this for all environments (including Caprover).
|
||||||
|
|
@ -242,6 +211,7 @@ const executePayment = () => {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 class="mb-4 mt-10">{{ $t("a.Adresse") }}</h3>
|
<h3 class="mb-4 mt-10">{{ $t("a.Adresse") }}</h3>
|
||||||
|
<pre>{{ address }}</pre>
|
||||||
<p class="mb-2">
|
<p class="mb-2">
|
||||||
{{
|
{{
|
||||||
$t(
|
$t(
|
||||||
|
|
@ -266,7 +236,7 @@ const executePayment = () => {
|
||||||
<button
|
<button
|
||||||
v-if="!useCompanyAddress"
|
v-if="!useCompanyAddress"
|
||||||
class="underline"
|
class="underline"
|
||||||
@click="useCompanyAddress = true"
|
@click="setUseCompanyAddress(true)"
|
||||||
>
|
>
|
||||||
<template v-if="userOrganisationName">
|
<template v-if="userOrganisationName">
|
||||||
{{
|
{{
|
||||||
|
|
@ -296,7 +266,7 @@ const executePayment = () => {
|
||||||
}}
|
}}
|
||||||
</h3>
|
</h3>
|
||||||
<h3 v-else>{{ $t("a.Rechnungsadresse") }}</h3>
|
<h3 v-else>{{ $t("a.Rechnungsadresse") }}</h3>
|
||||||
<button class="underline" @click="removeCompanyAddress">
|
<button class="underline" @click="setUseCompanyAddress(false)">
|
||||||
{{ $t("a.Entfernen") }}
|
{{ $t("a.Entfernen") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -31,16 +31,16 @@ export interface User {
|
||||||
language: AvailableLanguages;
|
language: AvailableLanguages;
|
||||||
course_session_experts: string[];
|
course_session_experts: string[];
|
||||||
invoice_address: InvoiceAddress | null;
|
invoice_address: InvoiceAddress | null;
|
||||||
street: string | null;
|
street: string;
|
||||||
street_number: string | null;
|
street_number: string;
|
||||||
postal_code: string | null;
|
postal_code: string;
|
||||||
city: string | null;
|
city: string;
|
||||||
country: Country | null;
|
country: Country | null;
|
||||||
organisation_detail_name: string | null;
|
organisation_detail_name: string;
|
||||||
organisation_street: string | null;
|
organisation_street: string;
|
||||||
organisation_street_number: string | null;
|
organisation_street_number: string;
|
||||||
organisation_postal_code: string | null;
|
organisation_postal_code: string;
|
||||||
organisation_city: string | null;
|
organisation_city: string;
|
||||||
organisation_country: Country | null;
|
organisation_country: Country | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,16 +74,16 @@ const initialUserState: User = {
|
||||||
loggedIn: false,
|
loggedIn: false,
|
||||||
language: defaultLanguage,
|
language: defaultLanguage,
|
||||||
invoice_address: "prv",
|
invoice_address: "prv",
|
||||||
street: null,
|
street: "",
|
||||||
street_number: null,
|
street_number: "",
|
||||||
postal_code: null,
|
postal_code: "",
|
||||||
city: null,
|
city: "",
|
||||||
country: null,
|
country: null,
|
||||||
organisation_detail_name: null,
|
organisation_detail_name: "",
|
||||||
organisation_street: null,
|
organisation_street: "",
|
||||||
organisation_street_number: null,
|
organisation_street_number: "",
|
||||||
organisation_postal_code: null,
|
organisation_postal_code: "",
|
||||||
organisation_city: null,
|
organisation_city: "",
|
||||||
organisation_country: null,
|
organisation_country: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ class Migration(migrations.Migration):
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=models.deletion.SET_NULL,
|
on_delete=models.deletion.SET_NULL,
|
||||||
related_name="user_country",
|
related_name="+",
|
||||||
to="core.country",
|
to="core.country",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -88,7 +88,7 @@ class Migration(migrations.Migration):
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=models.deletion.SET_NULL,
|
on_delete=models.deletion.SET_NULL,
|
||||||
related_name="organisation_country",
|
related_name="+",
|
||||||
to="core.country",
|
to="core.country",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ class User(AbstractUser):
|
||||||
city = models.CharField(max_length=255, blank=True)
|
city = models.CharField(max_length=255, blank=True)
|
||||||
country = models.ForeignKey(
|
country = models.ForeignKey(
|
||||||
Country,
|
Country,
|
||||||
related_name="user_country",
|
related_name="+",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
|
@ -105,7 +105,7 @@ class User(AbstractUser):
|
||||||
organisation_city = models.CharField(max_length=255, blank=True)
|
organisation_city = models.CharField(max_length=255, blank=True)
|
||||||
organisation_country = models.ForeignKey(
|
organisation_country = models.ForeignKey(
|
||||||
Country,
|
Country,
|
||||||
related_name="organisation_country",
|
related_name="+",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,23 @@
|
||||||
from django.db import migrations, models
|
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):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("shop", "0012_delete_country"),
|
("shop", "0012_delete_country"),
|
||||||
|
("core", "0009_country_refactor"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
@ -14,4 +28,79 @@ class Migration(migrations.Migration):
|
||||||
name="abacus_order_id",
|
name="abacus_order_id",
|
||||||
field=models.BigIntegerField(blank=True, null=True, unique=True),
|
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",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,7 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Max
|
from django.db.models import Max
|
||||||
|
|
||||||
|
from vbv_lernwelt.core.models import Country
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class Product(models.Model):
|
class Product(models.Model):
|
||||||
|
|
@ -62,6 +35,14 @@ class CheckoutState(models.TextChoices):
|
||||||
|
|
||||||
|
|
||||||
class CheckoutInformation(models.Model):
|
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)
|
user = models.ForeignKey("core.User", on_delete=models.PROTECT)
|
||||||
|
|
||||||
product_sku = models.CharField(max_length=255)
|
product_sku = models.CharField(max_length=255)
|
||||||
|
|
@ -89,15 +70,31 @@ class CheckoutInformation(models.Model):
|
||||||
street_number = models.CharField(max_length=255)
|
street_number = models.CharField(max_length=255)
|
||||||
postal_code = models.CharField(max_length=255)
|
postal_code = models.CharField(max_length=255)
|
||||||
city = 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)
|
invoice_address = models.CharField(
|
||||||
company_name = models.CharField(max_length=255, blank=True)
|
max_length=3, choices=INVOICE_ADDRESS_CHOICES, default="prv"
|
||||||
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)
|
# organisation data (optional)
|
||||||
company_city = models.CharField(max_length=255, blank=True)
|
organisation_detail_name = models.CharField(max_length=255, blank=True)
|
||||||
company_country = 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 metadata
|
||||||
webhook_history = models.JSONField(default=list)
|
webhook_history = models.JSONField(default=list)
|
||||||
|
|
|
||||||
|
|
@ -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",
|
|
||||||
]
|
|
||||||
|
|
|
||||||
|
|
@ -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/")
|
|
||||||
|
|
@ -1,18 +1,9 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
|
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
|
||||||
from vbv_lernwelt.shop.views import (
|
from vbv_lernwelt.shop.views import checkout_vv, transaction_webhook
|
||||||
checkout_vv,
|
|
||||||
get_billing_address,
|
|
||||||
transaction_webhook,
|
|
||||||
update_billing_address,
|
|
||||||
)
|
|
||||||
|
|
||||||
urlpatterns = [
|
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("vv/checkout/", checkout_vv, name="checkout-vv"),
|
||||||
path(
|
path(
|
||||||
"transaction/webhook/",
|
"transaction/webhook/",
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from sentry_sdk import capture_exception
|
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.course.models import CourseSession, CourseSessionUser
|
||||||
from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email
|
from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email
|
||||||
from vbv_lernwelt.shop.const import (
|
from vbv_lernwelt.shop.const import (
|
||||||
|
|
@ -15,13 +14,7 @@ from vbv_lernwelt.shop.const import (
|
||||||
VV_FR_PRODUCT_SKU,
|
VV_FR_PRODUCT_SKU,
|
||||||
VV_IT_PRODUCT_SKU,
|
VV_IT_PRODUCT_SKU,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.shop.models import (
|
from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState, Product
|
||||||
BillingAddress,
|
|
||||||
CheckoutInformation,
|
|
||||||
CheckoutState,
|
|
||||||
Product,
|
|
||||||
)
|
|
||||||
from vbv_lernwelt.shop.serializers import BillingAddressSerializer
|
|
||||||
from vbv_lernwelt.shop.services import (
|
from vbv_lernwelt.shop.services import (
|
||||||
datatrans_state_to_checkout_state,
|
datatrans_state_to_checkout_state,
|
||||||
get_payment_url,
|
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"])
|
@api_view(["POST"])
|
||||||
def transaction_webhook(request):
|
def transaction_webhook(request):
|
||||||
"""IMPORTANT: This is not called for timed out transactions!"""
|
"""IMPORTANT: This is not called for timed out transactions!"""
|
||||||
|
|
@ -125,7 +85,7 @@ def checkout_vv(request):
|
||||||
sku = request.data["product"]
|
sku = request.data["product"]
|
||||||
base_redirect_url = request.data["redirect_url"]
|
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:
|
try:
|
||||||
product = Product.objects.get(sku=sku)
|
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(
|
checkout_info = CheckoutInformation.objects.create(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
state=CheckoutState.ONGOING,
|
state=CheckoutState.ONGOING,
|
||||||
|
|
@ -184,8 +153,6 @@ def checkout_vv(request):
|
||||||
**request.data["address"],
|
**request.data["address"],
|
||||||
)
|
)
|
||||||
|
|
||||||
update_user_address(user=request.user, checkout_info=checkout_info)
|
|
||||||
|
|
||||||
return next_step_response(url=get_payment_url(transaction_id))
|
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:
|
def checkout_success_url(product_sku: str, base_url: str = "") -> str:
|
||||||
return f"{base_url}/onboarding/{product_sku}/checkout/complete"
|
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()
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue