Merged in feature/abacus-export (pull request #335)
Feature/abacus export Approved-by: Christian Cueni
This commit is contained in:
commit
ceb7d07f07
|
|
@ -277,10 +277,11 @@ cypress/test-reports
|
|||
|
||||
git-crypt-encrypted-files-check.txt
|
||||
|
||||
|
||||
/server/vbv_lernwelt/static/css/tailwind.css
|
||||
/server/vbv_lernwelt/static/vue/
|
||||
/server/vbv_lernwelt/static/storybook
|
||||
/server/vbv_lernwelt/templates/vue/index.html
|
||||
/server/vbv_lernwelt/media
|
||||
/client/src/gql/dist/minifiedSchema.json
|
||||
|
||||
/sftptest/
|
||||
|
|
|
|||
|
|
@ -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,13 +135,17 @@ 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"
|
||||
class="block w-full border-0 py-1.5 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
>
|
||||
<option v-for="country in countries" :key="country.id" :value="country.id">
|
||||
<option
|
||||
v-for="country in countries"
|
||||
:key="country.country_code"
|
||||
:value="country.country_code"
|
||||
>
|
||||
{{ country.name }}
|
||||
</option>
|
||||
</select>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const props = defineProps<{
|
|||
street_number: string;
|
||||
postal_code: string;
|
||||
city: string;
|
||||
country: string;
|
||||
country_code: string;
|
||||
};
|
||||
}>();
|
||||
|
||||
|
|
@ -147,13 +147,17 @@ 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"
|
||||
class="block w-full border-0 py-1.5 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
>
|
||||
<option v-for="country in countries" :key="country.id" :value="country.id">
|
||||
<option
|
||||
v-for="country in countries"
|
||||
:key="country.country_code"
|
||||
:value="country.country_code"
|
||||
>
|
||||
{{ country.name }}
|
||||
</option>
|
||||
</select>
|
||||
|
|
|
|||
|
|
@ -20,23 +20,25 @@ const formData = ref({
|
|||
street_number: user.street_number,
|
||||
postal_code: user.postal_code,
|
||||
city: user.city,
|
||||
country_id: user.country?.id,
|
||||
country_code: user.country?.country_code,
|
||||
organisation: user.organisation,
|
||||
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_id: user.organisation_country?.id,
|
||||
organisation_country_code: user.organisation_country?.country_code,
|
||||
invoice_address: user.invoice_address,
|
||||
});
|
||||
|
||||
async function save() {
|
||||
const { country_id, organisation_country_id, ...profileData } = formData.value;
|
||||
const { country_code, organisation_country_code, ...profileData } = formData.value;
|
||||
const typedProfileData: Partial<User> = { ...profileData };
|
||||
|
||||
typedProfileData.country = countries.value.find((c) => c.id === country_id);
|
||||
typedProfileData.country = countries.value.find(
|
||||
(c) => c.country_code === country_code
|
||||
);
|
||||
typedProfileData.organisation_country = countries.value.find(
|
||||
(c) => c.id === organisation_country_id
|
||||
(c) => c.country_code === organisation_country_code
|
||||
);
|
||||
|
||||
await user.updateUserProfile(typedProfileData);
|
||||
|
|
@ -219,12 +221,16 @@ async function avatarUpload(e: Event) {
|
|||
|
||||
<select
|
||||
id="country"
|
||||
v-model="formData.country_id"
|
||||
v-model="formData.country_code"
|
||||
name="country"
|
||||
autocomplete="country-name"
|
||||
class="disabled:bg-gray-50 mb-4 block w-full border-0 py-1.5 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 disabled:cursor-not-allowed disabled:text-gray-500 disabled:ring-gray-200 sm:max-w-sm sm:text-sm sm:leading-6"
|
||||
>
|
||||
<option v-for="country in countries" :key="country.id" :value="country.id">
|
||||
<option
|
||||
v-for="country in countries"
|
||||
:key="country.country_code"
|
||||
:value="country.country_code"
|
||||
>
|
||||
{{ country.name }}
|
||||
</option>
|
||||
</select>
|
||||
|
|
@ -325,13 +331,17 @@ async function avatarUpload(e: Event) {
|
|||
|
||||
<select
|
||||
id="org-country"
|
||||
v-model="formData.organisation_country_id"
|
||||
v-model="formData.organisation_country_code"
|
||||
required
|
||||
name="org-country"
|
||||
autocomplete="country-name"
|
||||
class="disabled:bg-gray-50 mb-4 block w-full border-0 py-1.5 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 disabled:cursor-not-allowed disabled:text-gray-500 disabled:ring-gray-200 sm:max-w-sm sm:text-sm sm:leading-6"
|
||||
>
|
||||
<option v-for="country in countries" :key="country.id" :value="country.id">
|
||||
<option
|
||||
v-for="country in countries"
|
||||
:key="country.country_code"
|
||||
:value="country.country_code"
|
||||
>
|
||||
{{ country.name }}
|
||||
</option>
|
||||
</select>
|
||||
|
|
|
|||
|
|
@ -1,33 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
import WizardPage from "@/components/onboarding/WizardPage.vue";
|
||||
import type { Ref } from "vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { computed, ref } from "vue";
|
||||
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 +19,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 +43,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 +106,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).
|
||||
|
|
@ -266,7 +234,7 @@ const executePayment = () => {
|
|||
<button
|
||||
v-if="!useCompanyAddress"
|
||||
class="underline"
|
||||
@click="useCompanyAddress = true"
|
||||
@click="setUseCompanyAddress(true)"
|
||||
>
|
||||
<template v-if="userOrganisationName">
|
||||
{{
|
||||
|
|
@ -296,7 +264,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>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ export type Organisation = {
|
|||
};
|
||||
|
||||
export type Country = {
|
||||
id: number;
|
||||
country_code: string;
|
||||
vbv_country_id: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -1,5 +1,6 @@
|
|||
encrypted: env_secrets/caprover_myvbv-prod.env
|
||||
encrypted: env_secrets/caprover_myvbv-stage.env
|
||||
encrypted: env_secrets/caprover_vbv-develop.env
|
||||
encrypted: env_secrets/local_chrigu.env
|
||||
encrypted: env_secrets/local_daniel.env
|
||||
encrypted: env_secrets/local_elia.env
|
||||
|
|
|
|||
|
|
@ -675,13 +675,20 @@ if APP_ENVIRONMENT.startswith("prod"):
|
|||
DATATRANS_API_ENDPOINT = "https://api.datatrans.com"
|
||||
DATATRANS_PAY_URL = "https://pay.datatrans.com"
|
||||
else:
|
||||
DATATRANS_API_ENDPOINT = "https://api.sandbox.datatrans.com"
|
||||
DATATRANS_PAY_URL = "https://pay.sandbox.datatrans.com"
|
||||
DATATRANS_API_ENDPOINT = env(
|
||||
"DATATRANS_API_ENDPOINT", default="https://api.sandbox.datatrans.com"
|
||||
)
|
||||
DATATRANS_PAY_URL = env(
|
||||
"DATATRANS_PAY_URL", default="https://pay.sandbox.datatrans.com"
|
||||
)
|
||||
|
||||
|
||||
# default settings for python sftpserver test-server
|
||||
ABACUS_EXPORT_SFTP_HOST = env("ABACUS_EXPORT_SFTP_HOST", default="localhost")
|
||||
ABACUS_EXPORT_SFTP_PASSWORD = env("ABACUS_EXPORT_SFTP_PASSWORD", default="admin")
|
||||
ABACUS_EXPORT_SFTP_PORT = env("ABACUS_EXPORT_SFTP_PORT", default="3373")
|
||||
ABACUS_EXPORT_SFTP_USERNAME = env("ABACUS_EXPORT_SFTP_USERNAME", default="admin")
|
||||
|
||||
# Only for debugging the webhook (locally)
|
||||
DATATRANS_DEBUG_WEBHOOK_OVERWRITE = env(
|
||||
"DATATRANS_DEBUG_WEBHOOK_OVERWRITE", default=None
|
||||
)
|
||||
|
||||
# S3 BUCKET CONFIGURATION
|
||||
FILE_UPLOAD_STORAGE = env("FILE_UPLOAD_STORAGE", default="s3") # local | s3
|
||||
|
|
@ -772,7 +779,7 @@ if APP_ENVIRONMENT == "local":
|
|||
# django-extensions
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration
|
||||
INSTALLED_APPS += ["django_extensions", "django_watchfiles"] # noqa F405
|
||||
INSTALLED_APPS += ["django_extensions"] # noqa F405
|
||||
else:
|
||||
# not local
|
||||
# SECURITY
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
# pylint: disable=unused-wildcard-import,wildcard-import,wrong-import-position
|
||||
import os
|
||||
|
||||
from dotenv import dotenv_values
|
||||
|
||||
script_path = os.path.abspath(__file__)
|
||||
script_dir = os.path.dirname(script_path)
|
||||
|
||||
dev_env = dotenv_values(f"{script_dir}/../../../env_secrets/caprover_vbv-develop.env")
|
||||
|
||||
os.environ["IT_APP_ENVIRONMENT"] = "local"
|
||||
|
||||
os.environ["AWS_S3_SECRET_ACCESS_KEY"] = dev_env.get("AWS_S3_SECRET_ACCESS_KEY")
|
||||
os.environ["DATATRANS_BASIC_AUTH_KEY"] = dev_env.get("DATATRANS_BASIC_AUTH_KEY")
|
||||
os.environ["DATATRANS_HMAC_KEY"] = dev_env.get("DATATRANS_HMAC_KEY")
|
||||
|
||||
from .base import * # noqa
|
||||
|
||||
# GENERAL
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
|
||||
DATABASES["default"]["NAME"] = "vbv_lernwelt_cypress"
|
||||
|
||||
# EMAIL
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
|
||||
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
|
||||
|
||||
CYPRESS_TEST = True
|
||||
|
||||
# Your stuff...
|
||||
# ------------------------------------------------------------------------------
|
||||
REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = {
|
||||
"anon": "10000/day",
|
||||
"hour-throttle": "40000/hour",
|
||||
"day-throttle": "2000000/day",
|
||||
}
|
||||
|
||||
RATELIMIT_ENABLE = False
|
||||
|
||||
# Select faster password hasher during tests
|
||||
PASSWORD_HASHERS = [
|
||||
"django.contrib.auth.hashers.MD5PasswordHasher",
|
||||
]
|
||||
|
|
@ -23,6 +23,8 @@ EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
|
|||
WHITENOISE_MANIFEST_STRICT = False
|
||||
AWS_S3_FILE_OVERWRITE = True
|
||||
|
||||
ABACUS_EXPORT_SFTP_PORT = 34343
|
||||
|
||||
|
||||
class DisableMigrations(dict):
|
||||
def __contains__(self, item):
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ from .base import * # noqa
|
|||
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
|
||||
DATABASES["default"]["NAME"] = "vbv_lernwelt_cypress"
|
||||
|
||||
DATATRANS_API_ENDPOINT = "http://localhost:8001/server/fakeapi/datatrans/api"
|
||||
DATATRANS_PAY_URL = "http://localhost:8001/server/fakeapi/datatrans/pay"
|
||||
|
||||
# EMAIL
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
|
||||
|
|
|
|||
|
|
@ -68,6 +68,11 @@ from vbv_lernwelt.importer.views import (
|
|||
)
|
||||
from vbv_lernwelt.media_files.views import user_image
|
||||
from vbv_lernwelt.notify.views import email_notification_settings
|
||||
|
||||
from vbv_lernwelt.shop.datatrans_fake_server import (
|
||||
fake_datatrans_api_view,
|
||||
fake_datatrans_pay_view,
|
||||
)
|
||||
from wagtail import urls as wagtail_urls
|
||||
from wagtail.admin import urls as wagtailadmin_urls
|
||||
from wagtail.documents import urls as media_library_urls
|
||||
|
|
@ -250,6 +255,16 @@ if settings.DEBUG:
|
|||
# Static file serving when using Gunicorn + Uvicorn for local web socket development
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
|
||||
if "fakeapi" in settings.DATATRANS_API_ENDPOINT:
|
||||
urlpatterns += [
|
||||
re_path(
|
||||
r"^server/fakeapi/datatrans/api(?P<api_url>.*)$", fake_datatrans_api_view
|
||||
),
|
||||
re_path(
|
||||
r"^server/fakeapi/datatrans/pay(?P<api_url>.*)$", fake_datatrans_pay_view
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
# fmt: on
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
import pytest
|
||||
from _pytest.runner import runtestprotocol
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
parallel_items = []
|
||||
serial_items = []
|
||||
|
||||
for item in items:
|
||||
if "serial" in item.keywords:
|
||||
serial_items.append(item)
|
||||
else:
|
||||
parallel_items.append(item)
|
||||
|
||||
# Modify the collection to run serial tests first
|
||||
config.serial_items = serial_items
|
||||
items[:] = parallel_items
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_sessionfinish(session, exitstatus):
|
||||
config = session.config
|
||||
if hasattr(config, "serial_items") and config.serial_items:
|
||||
serial_items = config.serial_items
|
||||
|
||||
# Run serial tests one by one
|
||||
for item in serial_items:
|
||||
runtestprotocol(item, nextitem=None)
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from io import StringIO
|
||||
from subprocess import Popen
|
||||
from time import sleep
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
|
||||
from vbv_lernwelt.core.admin import User
|
||||
from vbv_lernwelt.core.create_default_users import create_default_users
|
||||
from vbv_lernwelt.core.model_utils import add_countries
|
||||
from vbv_lernwelt.shop.invoice.abacus import abacus_ssh_upload
|
||||
from vbv_lernwelt.shop.invoice.abacus_sftp_client import AbacusSftpClient
|
||||
from vbv_lernwelt.shop.tests.factories import CheckoutInformationFactory
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def sftp_server():
|
||||
tmppath = tempfile.mkdtemp()
|
||||
print(tmppath)
|
||||
shutil.rmtree(tmppath)
|
||||
os.mkdir(tmppath)
|
||||
os.mkdir(os.path.join(tmppath, "debitor"))
|
||||
os.mkdir(os.path.join(tmppath, "order"))
|
||||
sftp_server = Popen(
|
||||
f"sftpserver -p {settings.ABACUS_EXPORT_SFTP_PORT} -l INFO",
|
||||
shell=True,
|
||||
cwd=tmppath,
|
||||
)
|
||||
sleep(3)
|
||||
yield tmppath
|
||||
if sftp_server:
|
||||
sftp_server.kill()
|
||||
|
||||
|
||||
def test_can_write_file_to_fake_sftp_server(sftp_server):
|
||||
with AbacusSftpClient() as client:
|
||||
files = client.listdir(".")
|
||||
assert set(files) == {"debitor", "order"}
|
||||
|
||||
str_file = StringIO()
|
||||
str_file.write("Hello world\n")
|
||||
str_file.seek(0)
|
||||
client.putfo(str_file, "hello.txt")
|
||||
|
||||
files = client.listdir(".")
|
||||
assert set(files) == {"debitor", "order", "hello.txt"}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def setup_abacus_env(sftp_server):
|
||||
add_countries(small_set=True)
|
||||
create_default_users()
|
||||
yield sftp_server
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_upload_abacus_xml(setup_abacus_env):
|
||||
tmppath = setup_abacus_env
|
||||
|
||||
# set abacus_number before
|
||||
_pat = User.objects.get(username="patrizia.huggel@eiger-versicherungen.ch")
|
||||
_pat.abacus_debitor_number = 60000011
|
||||
_pat.save()
|
||||
_ignore_checkout_information = CheckoutInformationFactory(
|
||||
user=_pat, abacus_order_id=6_000_000_123
|
||||
)
|
||||
|
||||
feuz = User.objects.get(username="andreas.feuz@eiger-versicherungen.ch")
|
||||
feuz_checkout_info = CheckoutInformationFactory(
|
||||
user=feuz,
|
||||
transaction_id="24021508331287484",
|
||||
first_name="Andreas",
|
||||
last_name="Feuz",
|
||||
street="Eggersmatt",
|
||||
street_number="32",
|
||||
postal_code="1719",
|
||||
city="Zumholz",
|
||||
country_id="CH",
|
||||
invoice_address="org",
|
||||
organisation_detail_name="VBV",
|
||||
organisation_street="Laupenstrasse",
|
||||
organisation_street_number="10",
|
||||
organisation_postal_code="3000",
|
||||
organisation_city="Bern",
|
||||
organisation_country_id="CH",
|
||||
)
|
||||
feuz_checkout_info.created_at = datetime(2024, 2, 15, 8, 33, 12, 0)
|
||||
|
||||
abacus_ssh_upload(feuz_checkout_info)
|
||||
|
||||
# check if files got created
|
||||
debitor_filepath = os.path.join(tmppath, "debitor/myVBV_debi_60000012.xml")
|
||||
assert os.path.exists(debitor_filepath)
|
||||
|
||||
with open(debitor_filepath) as debitor_file:
|
||||
debi_content = debitor_file.read()
|
||||
assert "<CustomerNumber>60000012</CustomerNumber>" in debi_content
|
||||
assert "<Email>andreas.feuz@eiger-versicherungen.ch</Email>" in debi_content
|
||||
|
||||
order_filepath = os.path.join(
|
||||
tmppath, "order/myVBV_orde_60000012_20240215083312_6000000124.xml"
|
||||
)
|
||||
assert os.path.exists(order_filepath)
|
||||
with open(order_filepath) as order_file:
|
||||
order_content = order_file.read()
|
||||
assert (
|
||||
"<ReferencePurchaseOrder>24021508331287484</ReferencePurchaseOrder>"
|
||||
in order_content
|
||||
)
|
||||
assert "<CustomerNumber>60000012</CustomerNumber>" in order_content
|
||||
|
||||
feuz_checkout_info.refresh_from_db()
|
||||
assert feuz_checkout_info.abacus_ssh_upload_done
|
||||
|
||||
# calling `abacus_ssh_upload` a second time will not upload files again...
|
||||
os.remove(debitor_filepath)
|
||||
os.remove(order_filepath)
|
||||
|
||||
abacus_ssh_upload(feuz_checkout_info)
|
||||
|
||||
debitor_filepath = os.path.join(tmppath, "debitor/myVBV_debi_60000012.xml")
|
||||
assert not os.path.exists(debitor_filepath)
|
||||
|
||||
order_filepath = os.path.join(
|
||||
tmppath, "order/myVBV_orde_60000012_20240215083312_6000000124.xml"
|
||||
)
|
||||
assert not os.path.exists(order_filepath)
|
||||
|
|
@ -2,3 +2,5 @@
|
|||
addopts = --ds=config.settings.test --no-migrations
|
||||
python_files = tests.py test_*.py
|
||||
norecursedirs = node_modules
|
||||
markers =
|
||||
serial: marks tests as serial (not to be run in parallel)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ django-stubs # https://github.com/typeddjango/django-stubs
|
|||
pytest # https://github.com/pytest-dev/pytest
|
||||
pytest-sugar # https://github.com/Frozenball/pytest-sugar
|
||||
pytest-xdist #
|
||||
pytest-order
|
||||
djangorestframework-stubs # https://github.com/typeddjango/djangorestframework-stubs
|
||||
|
||||
|
||||
|
|
@ -33,11 +34,11 @@ django-coverage-plugin # https://github.com/nedbat/django_coverage_plugin
|
|||
pytest-django # https://github.com/pytest-dev/pytest-django
|
||||
freezegun # https://github.com/spulec/freezegun
|
||||
|
||||
# django-watchfiles custom PR
|
||||
https://github.com/q0w/django-watchfiles/archive/issue-1.zip
|
||||
|
||||
# code checking
|
||||
truffleHog
|
||||
|
||||
# deployement and CI
|
||||
git+https://github.com/iterativ/Caprover-API.git@5013f8fc929e8e3281b9d609e968a782e8e99530
|
||||
|
||||
# sftpserver for tests
|
||||
git+https://github.com/lonetwin/sftpserver.git@1d16896d3f0f90d63d1caaf4e199f2a9dde6456f
|
||||
|
|
|
|||
|
|
@ -131,7 +131,6 @@ django==3.2.20
|
|||
# django-stubs-ext
|
||||
# django-taggit
|
||||
# django-treebeard
|
||||
# django-watchfiles
|
||||
# djangorestframework
|
||||
# drf-spectacular
|
||||
# graphene-django
|
||||
|
|
@ -186,8 +185,6 @@ django-taggit==4.0.0
|
|||
# via wagtail
|
||||
django-treebeard==4.7
|
||||
# via wagtail
|
||||
django-watchfiles @ https://github.com/q0w/django-watchfiles/archive/issue-1.zip
|
||||
# via -r requirements-dev.in
|
||||
djangorestframework==3.14.0
|
||||
# via
|
||||
# -r requirements.in
|
||||
|
|
@ -341,7 +338,9 @@ packaging==23.1
|
|||
# pytest
|
||||
# pytest-sugar
|
||||
paramiko==3.3.1
|
||||
# via -r requirements.in
|
||||
# via
|
||||
# -r requirements.in
|
||||
# sftpserver
|
||||
parso==0.8.3
|
||||
# via jedi
|
||||
pathspec==0.11.2
|
||||
|
|
@ -397,9 +396,7 @@ pyflakes==3.1.0
|
|||
pygments==2.16.1
|
||||
# via ipython
|
||||
pyjwt[crypto]==2.8.0
|
||||
# via
|
||||
# msal
|
||||
# pyjwt
|
||||
# via msal
|
||||
pylint==2.17.5
|
||||
# via
|
||||
# pylint-django
|
||||
|
|
@ -416,10 +413,13 @@ pytest==7.4.0
|
|||
# via
|
||||
# -r requirements-dev.in
|
||||
# pytest-django
|
||||
# pytest-order
|
||||
# pytest-sugar
|
||||
# pytest-xdist
|
||||
pytest-django==4.5.2
|
||||
# via -r requirements-dev.in
|
||||
pytest-order==1.2.1
|
||||
# via -r requirements-dev.in
|
||||
pytest-sugar==0.9.7
|
||||
# via -r requirements-dev.in
|
||||
pytest-xdist==3.5.0
|
||||
|
|
@ -480,6 +480,8 @@ sendgrid==6.10.0
|
|||
# via -r requirements.in
|
||||
sentry-sdk==1.29.2
|
||||
# via -r requirements.in
|
||||
sftpserver @ git+https://github.com/lonetwin/sftpserver.git@1d16896d3f0f90d63d1caaf4e199f2a9dde6456f
|
||||
# via -r requirements-dev.in
|
||||
six==1.16.0
|
||||
# via
|
||||
# asttokens
|
||||
|
|
@ -607,9 +609,7 @@ wagtail-headless-preview==0.6.0
|
|||
wagtail-localize==1.5.1
|
||||
# via -r requirements.in
|
||||
watchfiles==0.19.0
|
||||
# via
|
||||
# django-watchfiles
|
||||
# uvicorn
|
||||
# via uvicorn
|
||||
wcwidth==0.2.6
|
||||
# via prompt-toolkit
|
||||
webencodings==0.5.1
|
||||
|
|
@ -621,9 +621,7 @@ wheel==0.41.1
|
|||
whitenoise[brotli]==6.5.0
|
||||
# via -r requirements.in
|
||||
willow[heif]==1.6.1
|
||||
# via
|
||||
# wagtail
|
||||
# willow
|
||||
# via wagtail
|
||||
wrapt==1.15.0
|
||||
# via astroid
|
||||
|
||||
|
|
|
|||
|
|
@ -31,8 +31,7 @@ azure-core==1.29.1
|
|||
azure-identity==1.14.0
|
||||
# via -r requirements.in
|
||||
azure-storage-blob==12.17.0
|
||||
# via
|
||||
# -r requirements.in
|
||||
# via -r requirements.in
|
||||
bcrypt==4.0.1
|
||||
# via paramiko
|
||||
beautifulsoup4==4.11.2
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@
|
|||
cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
# limit test to 6 parallel processes, otherwise ratelimit of s3 could be hit
|
||||
pytest --numprocesses auto --maxprocesses=6 --junitxml=../test-reports/coverage.xml
|
||||
pytest --numprocesses auto --maxprocesses=6 --dist=loadscope --junitxml=../test-reports/coverage.xml $1
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
set -e
|
||||
|
||||
cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
coverage run -m pytest --numprocesses auto --maxprocesses=6 --junitxml=../test-reports/coverage.xml $1
|
||||
pytest --numprocesses auto --maxprocesses=6 --dist=loadscope --junitxml=../test-reports/coverage.xml $1
|
||||
|
||||
coverage_python=`coverage report -m | tail -n1 | awk '{print $4}'`
|
||||
commit=`git rev-parse HEAD`
|
||||
|
|
|
|||
|
|
@ -42,58 +42,32 @@ class EntitiesViewTest(APITestCase):
|
|||
},
|
||||
)
|
||||
|
||||
countries = response.data["countries"]
|
||||
|
||||
self.assertEqual(
|
||||
countries[0],
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Afghanistan",
|
||||
},
|
||||
)
|
||||
|
||||
def test_list_country_entities_ordered_by_country_id(self) -> None:
|
||||
# GIVEN
|
||||
url = reverse("list_entities")
|
||||
|
||||
first_country = Country.objects.get(country_id=1)
|
||||
|
||||
# WHEN
|
||||
response = self.client.get(url)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
countries = response.data["countries"]
|
||||
|
||||
self.assertEqual(
|
||||
countries[0],
|
||||
{
|
||||
"id": first_country.country_id,
|
||||
"name": first_country.name_de,
|
||||
},
|
||||
)
|
||||
|
||||
def test_list_country_entities_ordered_by_order_id(self) -> None:
|
||||
# GIVEN
|
||||
url = reverse("list_entities")
|
||||
|
||||
switzerland = Country.objects.get(name_de="Schweiz")
|
||||
switzerland.order_id = 1
|
||||
switzerland.save()
|
||||
|
||||
# WHEN
|
||||
response = self.client.get(url)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
countries = response.data["countries"]
|
||||
|
||||
self.assertEqual(
|
||||
countries[0],
|
||||
{
|
||||
"id": switzerland.country_id,
|
||||
"name": switzerland.name_de,
|
||||
"country_code": "CH",
|
||||
"vbv_country_id": 209,
|
||||
"name": "Schweiz",
|
||||
},
|
||||
)
|
||||
|
||||
usa = Country.objects.get(country_code="US")
|
||||
usa.order_id = 0.5
|
||||
usa.save()
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
countries = response.data["countries"]
|
||||
self.assertEqual(
|
||||
countries[0],
|
||||
{
|
||||
"country_code": "US",
|
||||
"vbv_country_id": usa.vbv_country_id,
|
||||
"name": usa.name_de,
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class MeUserViewTest(APITestCase):
|
|||
)
|
||||
self.client.login(username="testuser", password="testpassword")
|
||||
add_organisations()
|
||||
add_countries()
|
||||
add_countries(small_set=True)
|
||||
|
||||
def test_user_can_update_language(self) -> None:
|
||||
# GIVEN
|
||||
|
|
|
|||
|
|
@ -123,7 +123,8 @@ class OrganisationAdmin(admin.ModelAdmin):
|
|||
class CountryAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
"order_id",
|
||||
"country_id",
|
||||
"country_code",
|
||||
"vbv_country_id",
|
||||
"name_de",
|
||||
"name_fr",
|
||||
"name_it",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ TEST_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b"
|
|||
TEST_MENTOR1_USER_ID = "d1f5f5a9-5b0a-4e1a-9e1a-9e9b5b5e1b1b"
|
||||
TEST_STUDENT1_VV_USER_ID = "5ff59857-8de5-415e-a387-4449f9a0337a"
|
||||
TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID = "7e8ebf0b-e6e2-4022-88f4-6e663ba0a9db"
|
||||
TEST_USER_EMPTY_ID = "daecbabe-4ab9-4edf-a71f-4119042ccb02"
|
||||
|
||||
TEST_COURSE_SESSION_BERN_ID = -1
|
||||
TEST_COURSE_SESSION_ZURICH_ID = -2
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from vbv_lernwelt.core.constants import (
|
|||
TEST_SUPERVISOR1_USER_ID,
|
||||
TEST_TRAINER1_USER_ID,
|
||||
TEST_TRAINER2_USER_ID,
|
||||
TEST_USER_EMPTY_ID,
|
||||
)
|
||||
from vbv_lernwelt.core.models import User
|
||||
|
||||
|
|
@ -192,6 +193,15 @@ def create_default_users(default_password="test", set_avatar=False):
|
|||
password=env("IT_DEFAULT_ADMIN_PASSWORD", default_password),
|
||||
)
|
||||
|
||||
_create_user(
|
||||
TEST_USER_EMPTY_ID,
|
||||
"empty@example.com",
|
||||
"Flasche",
|
||||
"Leer",
|
||||
password=default_password,
|
||||
language="de",
|
||||
)
|
||||
|
||||
for user_data in default_users:
|
||||
_create_student_user(**user_data)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from datetime import datetime
|
||||
|
||||
import djclick as click
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.utils import timezone
|
||||
|
||||
from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletion
|
||||
|
|
@ -14,6 +15,7 @@ from vbv_lernwelt.core.constants import (
|
|||
TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID,
|
||||
TEST_STUDENT3_USER_ID,
|
||||
TEST_TRAINER1_USER_ID,
|
||||
TEST_USER_EMPTY_ID,
|
||||
)
|
||||
from vbv_lernwelt.core.models import Organisation, User
|
||||
from vbv_lernwelt.course.consts import (
|
||||
|
|
@ -48,6 +50,7 @@ from vbv_lernwelt.self_evaluation_feedback.models import (
|
|||
CourseCompletionFeedback,
|
||||
SelfEvaluationFeedback,
|
||||
)
|
||||
from vbv_lernwelt.shop.models import CheckoutInformation
|
||||
|
||||
|
||||
@click.command()
|
||||
|
|
@ -142,6 +145,18 @@ def command(
|
|||
User.objects.all().update(language="de")
|
||||
User.objects.all().update(additional_json_data={})
|
||||
|
||||
CheckoutInformation.objects.filter(user_id=TEST_USER_EMPTY_ID).delete()
|
||||
User.objects.filter(id=TEST_USER_EMPTY_ID).delete()
|
||||
user, _ = User.objects.get_or_create(
|
||||
id=TEST_USER_EMPTY_ID,
|
||||
username="empty@example.com",
|
||||
email="empty@example.com",
|
||||
language="de",
|
||||
first_name="Flasche",
|
||||
last_name="Leer",
|
||||
password=make_password("test"),
|
||||
)
|
||||
|
||||
if create_assignment_completion or create_assignment_evaluation:
|
||||
print("create assignment completion data for test course")
|
||||
create_test_assignment_submitted_data(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,15 @@
|
|||
|
||||
from django.db import migrations, models
|
||||
|
||||
from vbv_lernwelt.core.model_utils import countries
|
||||
|
||||
|
||||
def populate_country_order_id(apps, schema_editor):
|
||||
Country = apps.get_model("core", "Country")
|
||||
for country in Country.objects.all():
|
||||
country.order_id = countries[country.country_id].get("order_id", 20.0)
|
||||
country.save(update_fields=["order_id"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
|
|
@ -22,4 +31,5 @@ class Migration(migrations.Migration):
|
|||
name="order_id",
|
||||
field=models.FloatField(default=20),
|
||||
),
|
||||
migrations.RunPython(populate_country_order_id),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.2.20 on 2024-05-29 13:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("core", "0007_auto_20240220_1058"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="abacus_debitor_number",
|
||||
field=models.BigIntegerField(blank=True, null=True, unique=True),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
# Generated by Django 3.2.20 on 2024-05-30 10:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
from vbv_lernwelt.core.model_utils import countries
|
||||
|
||||
|
||||
def populate_country_code(apps, schema_editor):
|
||||
Country = apps.get_model("core", "Country")
|
||||
for country in Country.objects.all():
|
||||
country.country_code = countries[country.country_id]["country_code"]
|
||||
country.save(update_fields=["country_code"])
|
||||
|
||||
|
||||
def migrate_user_country(apps, schema_editor):
|
||||
User = apps.get_model("core", "User")
|
||||
Country = apps.get_model("core", "Country")
|
||||
for user in User.objects.all():
|
||||
if user.old_country:
|
||||
country = Country.objects.get(vbv_country_id=user.old_country)
|
||||
user.country = country
|
||||
if user.old_organisation_country:
|
||||
country = Country.objects.get(vbv_country_id=user.old_organisation_country)
|
||||
user.organisation_country = country
|
||||
user.save(update_fields=["country", "organisation_country"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("core", "0008_user_abacus_debitor_number"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="country",
|
||||
field=models.IntegerField(null=True, blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="organisation_country",
|
||||
field=models.IntegerField(null=True, blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="country",
|
||||
name="country_code",
|
||||
field=models.CharField(max_length=2, null=True),
|
||||
),
|
||||
migrations.RunPython(populate_country_code),
|
||||
migrations.AlterField(
|
||||
model_name="country",
|
||||
name="country_id",
|
||||
field=models.IntegerField(),
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="country", old_name="country_id", new_name="vbv_country_id"
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="country",
|
||||
name="country_code",
|
||||
field=models.CharField(max_length=2, primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="user",
|
||||
old_name="country",
|
||||
new_name="old_country",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="user",
|
||||
old_name="organisation_country",
|
||||
new_name="old_organisation_country",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="country",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to="core.country",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="organisation_country",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to="core.country",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(migrate_user_country),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="old_country",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="old_organisation_country",
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="country",
|
||||
options={
|
||||
"ordering": ["order_id", "vbv_country_id"],
|
||||
"verbose_name": "Country",
|
||||
"verbose_name_plural": "Countries",
|
||||
},
|
||||
),
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -3,7 +3,7 @@ import uuid
|
|||
import structlog
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from django.db.models import JSONField
|
||||
from django.db.models import JSONField, Max
|
||||
from django.urls import reverse
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
|
@ -25,19 +25,20 @@ class Organisation(models.Model):
|
|||
|
||||
|
||||
class Country(models.Model):
|
||||
country_id = models.IntegerField(primary_key=True)
|
||||
country_code = models.CharField(max_length=2, primary_key=True)
|
||||
vbv_country_id = models.IntegerField(primary_key=False)
|
||||
name_de = models.CharField(max_length=255)
|
||||
name_fr = models.CharField(max_length=255)
|
||||
name_it = models.CharField(max_length=255)
|
||||
order_id = models.FloatField(default=20)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name_de} ({self.country_id})"
|
||||
return f"{self.name_de} ({self.country_code}) ({self.vbv_country_id})"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Country"
|
||||
verbose_name_plural = "Countries"
|
||||
ordering = ["order_id", "country_id"]
|
||||
ordering = ["order_id", "vbv_country_id"]
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
|
|
@ -91,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,
|
||||
|
|
@ -104,12 +105,30 @@ 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,
|
||||
)
|
||||
|
||||
# is only set by abacus invoice export code
|
||||
abacus_debitor_number = models.BigIntegerField(unique=True, null=True, blank=True)
|
||||
|
||||
def set_increment_abacus_debitor_number(self):
|
||||
if self.abacus_debitor_number:
|
||||
return self
|
||||
|
||||
# Get the current maximum debitor_number and increment it by 1
|
||||
current_max = User.objects.aggregate(max_number=Max("abacus_debitor_number"))[
|
||||
"max_number"
|
||||
]
|
||||
new_debitor_number = (
|
||||
current_max if current_max is not None else 60_000_000
|
||||
) + 1
|
||||
self.abacus_debitor_number = new_debitor_number
|
||||
self.save()
|
||||
return self
|
||||
|
||||
def create_avatar_url(self, size=400):
|
||||
try:
|
||||
if self.avatar:
|
||||
|
|
|
|||
|
|
@ -14,12 +14,11 @@ def create_json_from_objects(objects, serializer_class, many=True) -> str:
|
|||
|
||||
|
||||
class CountrySerializer(serializers.ModelSerializer):
|
||||
id = serializers.IntegerField(source="country_id", read_only=True)
|
||||
name = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Country
|
||||
fields = ["id", "name"]
|
||||
fields = ["country_code", "vbv_country_id", "name"]
|
||||
|
||||
def get_name(self, obj):
|
||||
language = self.context.get("langauge")
|
||||
|
|
@ -32,11 +31,15 @@ class CountrySerializer(serializers.ModelSerializer):
|
|||
return obj.name_de
|
||||
|
||||
def to_internal_value(self, data):
|
||||
country_id = data.get("id")
|
||||
if country_id is not None:
|
||||
country_code = data.get("country_code")
|
||||
if country_code is not None:
|
||||
try:
|
||||
country = Country.objects.get(country_id=country_id)
|
||||
return {"id": country.country_id, "name": self.get_name(country)}
|
||||
country = Country.objects.get(country_code=country_code)
|
||||
return {
|
||||
"country_code": country.country_code,
|
||||
"vbv_country_id": country.vbv_country_id,
|
||||
"name": self.get_name(country),
|
||||
}
|
||||
except Country.DoesNotExist:
|
||||
raise serializers.ValidationError({"id": "Invalid country ID"})
|
||||
return super().to_internal_value(data)
|
||||
|
|
@ -105,14 +108,14 @@ class UserSerializer(serializers.ModelSerializer):
|
|||
setattr(instance, attr, value)
|
||||
|
||||
if country_data is not None:
|
||||
country_id = country_data.get("id")
|
||||
country_instance = Country.objects.filter(country_id=country_id).first()
|
||||
country_code = country_data.get("country_code")
|
||||
country_instance = Country.objects.filter(country_code=country_code).first()
|
||||
instance.country = country_instance
|
||||
|
||||
if organisation_country_data is not None:
|
||||
organisation_country_id = organisation_country_data.get("id")
|
||||
organisation_country_code = organisation_country_data.get("country_code")
|
||||
organisation_country_instance = Country.objects.filter(
|
||||
country_id=organisation_country_id
|
||||
country_code=organisation_country_code
|
||||
).first()
|
||||
instance.organisation_country = organisation_country_instance
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
from django.test import TestCase
|
||||
|
||||
from vbv_lernwelt.core.create_default_users import create_default_users
|
||||
from vbv_lernwelt.core.models import User
|
||||
|
||||
|
||||
class UserAbacusDebitorNumberTestCase(TestCase):
|
||||
def setUp(self):
|
||||
create_default_users()
|
||||
|
||||
def test_set_debitor_number(self):
|
||||
pat = User.objects.get(username="patrizia.huggel@eiger-versicherungen.ch")
|
||||
|
||||
self.assertIsNone(pat.abacus_debitor_number)
|
||||
|
||||
pat.set_increment_abacus_debitor_number()
|
||||
self.assertEqual(pat.abacus_debitor_number, 60000001)
|
||||
pat = pat.set_increment_abacus_debitor_number()
|
||||
self.assertEqual(pat.abacus_debitor_number, 60000001)
|
||||
|
||||
pat = User.objects.get(username="patrizia.huggel@eiger-versicherungen.ch")
|
||||
self.assertEqual(pat.abacus_debitor_number, 60000001)
|
||||
|
||||
feuz = User.objects.get(username="andreas.feuz@eiger-versicherungen.ch")
|
||||
self.assertIsNone(feuz.abacus_debitor_number)
|
||||
feuz = feuz.set_increment_abacus_debitor_number()
|
||||
self.assertEqual(feuz.abacus_debitor_number, 60000002)
|
||||
|
||||
feuz = User.objects.get(username="andreas.feuz@eiger-versicherungen.ch")
|
||||
self.assertEqual(feuz.abacus_debitor_number, 60000002)
|
||||
|
|
@ -74,3 +74,8 @@ After everything runs fine, we should be able to remove the following deprecated
|
|||
8. `IT_OAUTH_SCOPE`
|
||||
|
||||
|
||||
### Datatrans Test Credit Card
|
||||
|
||||
5100 0010 0000 0014
|
||||
06/25
|
||||
123
|
||||
|
|
|
|||
|
|
@ -1,14 +1,10 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from vbv_lernwelt.shop.invoice.abacus import abacus_ssh_upload
|
||||
from vbv_lernwelt.shop.models import CheckoutInformation, Product
|
||||
from vbv_lernwelt.shop.services import get_transaction_state
|
||||
|
||||
|
||||
@admin.action(description="ABACUS: Create invoices")
|
||||
def generate_invoice(modeladmin, request, queryset):
|
||||
pass
|
||||
|
||||
|
||||
@admin.action(description="DATATRANS: Sync transaction states")
|
||||
def sync_transaction_state(modeladmin, request, queryset):
|
||||
for checkout in queryset:
|
||||
|
|
@ -23,18 +19,60 @@ def sync_transaction_state(modeladmin, request, queryset):
|
|||
|
||||
@admin.register(CheckoutInformation)
|
||||
class CheckoutInformationAdmin(admin.ModelAdmin):
|
||||
@admin.action(description="ABACUS: Upload invoice to SFTP server")
|
||||
def abacus_upload_order(self, request, queryset):
|
||||
success = True
|
||||
for ci in queryset:
|
||||
if not abacus_ssh_upload(ci):
|
||||
success = False
|
||||
if not success:
|
||||
self.message_user(
|
||||
request, f"Beim SFTP upload ist ein Fehler aufgetreten", level="error"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def customer(obj):
|
||||
return f"{obj.user.first_name} {obj.user.last_name} ({obj.user.email})"
|
||||
|
||||
@staticmethod
|
||||
def debitor_number(obj):
|
||||
return obj.user.abacus_debitor_number
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
# Disable delete
|
||||
return False
|
||||
|
||||
list_display = (
|
||||
"product_sku",
|
||||
"user",
|
||||
customer,
|
||||
"product_name",
|
||||
"product_price",
|
||||
"updated_at",
|
||||
"created_at",
|
||||
"state",
|
||||
"invoice_transmitted_at",
|
||||
"abacus_order_id",
|
||||
debitor_number,
|
||||
"abacus_ssh_upload_done",
|
||||
)
|
||||
search_fields = ["user__email"]
|
||||
list_filter = ("state", "product_name")
|
||||
actions = [generate_invoice, sync_transaction_state]
|
||||
search_fields = [
|
||||
"user__email",
|
||||
"user__first_name",
|
||||
"user__last_name",
|
||||
"user__username",
|
||||
"transaction_id",
|
||||
"abacus_order_id",
|
||||
"user__abacus_debitor_number",
|
||||
]
|
||||
list_filter = ("state", "product_name", "product_sku", "abacus_ssh_upload_done")
|
||||
date_hierarchy = "created_at"
|
||||
actions = [abacus_upload_order, sync_transaction_state]
|
||||
readonly_fields = [
|
||||
"user",
|
||||
"transaction_id",
|
||||
"state",
|
||||
"product_price",
|
||||
"webhook_history",
|
||||
]
|
||||
|
||||
|
||||
@admin.register(Product)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,142 @@
|
|||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
|
||||
from vbv_lernwelt.core.models import User
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@django_view_authentication_exempt
|
||||
def fake_datatrans_api_view(request, api_url=""):
|
||||
if api_url == "/v1/transactions" and request.method == "POST":
|
||||
data = json.loads(request.body.decode("utf-8"))
|
||||
user = User.objects.get(id=data["user_id"])
|
||||
user.additional_json_data["datatrans_transaction_payload"] = data
|
||||
user.save()
|
||||
return JsonResponse({"transactionId": data["refno"]}, status=201)
|
||||
|
||||
return HttpResponse(
|
||||
content="unknown api url", content_type="application/json", status=400
|
||||
)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@django_view_authentication_exempt
|
||||
def fake_datatrans_pay_view(request, api_url=""):
|
||||
def call_transaction_complete_webhook(
|
||||
webhook_url, transaction_id, datatrans_status="settled"
|
||||
):
|
||||
time.sleep(1)
|
||||
|
||||
payload = {
|
||||
"transactionId": transaction_id,
|
||||
"status": datatrans_status,
|
||||
}
|
||||
key_hex_bytes = bytes.fromhex(settings.DATATRANS_HMAC_KEY)
|
||||
|
||||
# Create sign with timestamp and payload
|
||||
sign = hmac.new(
|
||||
key_hex_bytes, bytes(str(1) + json.dumps(payload), "utf-8"), hashlib.sha256
|
||||
)
|
||||
|
||||
response = requests.post(
|
||||
url=webhook_url,
|
||||
json=payload,
|
||||
headers={"Datatrans-Signature": f"t=1,s0={sign.hexdigest()}"},
|
||||
)
|
||||
print(response)
|
||||
|
||||
if api_url.startswith("/v1/start/"):
|
||||
transaction_id = api_url.split("/")[-1]
|
||||
transaction_user = User.objects.filter(
|
||||
additional_json_data__datatrans_transaction_payload__refno=transaction_id
|
||||
).first()
|
||||
|
||||
if transaction_user is None:
|
||||
return HttpResponse(
|
||||
content=f"""
|
||||
<h1>Fake Datatrans Payment</h1>
|
||||
<p>No active transaction found for {transaction_id}</p>
|
||||
""",
|
||||
status=404,
|
||||
)
|
||||
|
||||
if request.method == "GET":
|
||||
return HttpResponse(
|
||||
content=f"""
|
||||
<h1>Fake Datatrans Payment</h1>
|
||||
<form action="{request.build_absolute_uri()}" method="post">
|
||||
<fieldset>
|
||||
<legend>Datatrans payment result status</legend>
|
||||
<div>
|
||||
<input type="radio" name="payment" value="settled" checked/>
|
||||
<label>settled</label>
|
||||
|
||||
<input type="radio" name="payment" value="cancelled" />
|
||||
<label>cancelled</label>
|
||||
|
||||
<input type="radio" name="payment" value="failed" />
|
||||
<label>failed</label>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit">
|
||||
Pay with selected Status
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
"""
|
||||
)
|
||||
|
||||
elif request.method == "POST":
|
||||
payment_status = request.POST.get("payment", "settled")
|
||||
|
||||
if payment_status == "settled":
|
||||
success_url = transaction_user.additional_json_data[
|
||||
"datatrans_transaction_payload"
|
||||
]["redirect"]["successUrl"]
|
||||
|
||||
# start new thread which will call webhook after 2 seconds
|
||||
webhook_url = transaction_user.additional_json_data[
|
||||
"datatrans_transaction_payload"
|
||||
]["webhook"]["url"]
|
||||
thread = threading.Thread(
|
||||
target=call_transaction_complete_webhook,
|
||||
args=(
|
||||
webhook_url,
|
||||
transaction_id,
|
||||
),
|
||||
)
|
||||
thread.start()
|
||||
|
||||
# redirect to url
|
||||
return redirect(success_url + f"?datatransTrxId={transaction_id}")
|
||||
|
||||
if payment_status == "cancelled":
|
||||
cancel_url = transaction_user.additional_json_data[
|
||||
"datatrans_transaction_payload"
|
||||
]["redirect"]["cancelUrl"]
|
||||
|
||||
# redirect to url
|
||||
return redirect(cancel_url + f"?datatransTrxId={transaction_id}")
|
||||
|
||||
if payment_status == "failed":
|
||||
error_url = transaction_user.additional_json_data[
|
||||
"datatrans_transaction_payload"
|
||||
]["redirect"]["errorUrl"]
|
||||
|
||||
# redirect to url
|
||||
return redirect(error_url + f"?datatransTrxId={transaction_id}")
|
||||
|
||||
return HttpResponse(
|
||||
content="unknown api url", content_type="application/json", status=400
|
||||
)
|
||||
|
|
@ -1,146 +1,240 @@
|
|||
import datetime
|
||||
from typing import List
|
||||
from uuid import uuid4
|
||||
from io import BytesIO
|
||||
from xml.dom import minidom
|
||||
from xml.etree.ElementTree import Element, SubElement, tostring
|
||||
|
||||
from vbv_lernwelt.shop.invoice.creator import InvoiceCreator, Item
|
||||
from vbv_lernwelt.shop.invoice.repositories import InvoiceRepository
|
||||
from vbv_lernwelt.shop.models import CheckoutInformation
|
||||
import structlog
|
||||
|
||||
from vbv_lernwelt.shop.invoice.abacus_sftp_client import AbacusSftpClient
|
||||
from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class AbacusInvoiceCreator(InvoiceCreator):
|
||||
def __init__(self, repository: InvoiceRepository):
|
||||
self.repository = repository
|
||||
def abacus_ssh_upload(checkout_information: CheckoutInformation):
|
||||
if checkout_information.state != CheckoutState.PAID:
|
||||
# only upload invoice if checkout is paid
|
||||
return True
|
||||
|
||||
def create_invoice(
|
||||
self,
|
||||
checkout_information: CheckoutInformation,
|
||||
filename: str = None,
|
||||
):
|
||||
customer_number = checkout_information.transaction_id
|
||||
order_date = checkout_information.created_at.date()
|
||||
reference_purchase_order = str(checkout_information.id)
|
||||
unic_id = checkout_information.transaction_id
|
||||
|
||||
items = [
|
||||
Item(
|
||||
product_number=checkout_information.product_sku,
|
||||
quantity=1,
|
||||
description=checkout_information.product_description,
|
||||
try:
|
||||
if not checkout_information.abacus_ssh_upload_done:
|
||||
# only upload data for not yet uploaded invoices
|
||||
invoice_xml_filename, invoice_xml_content = create_invoice_xml(
|
||||
checkout_information
|
||||
)
|
||||
customer_xml_filename, customer_xml_content = create_customer_xml(
|
||||
checkout_information
|
||||
)
|
||||
]
|
||||
|
||||
invoice = self.invoice_xml(
|
||||
customer_number,
|
||||
order_date,
|
||||
reference_purchase_order,
|
||||
unic_id,
|
||||
items,
|
||||
abacus_ssh_upload_invoice(
|
||||
customer_xml_filename, customer_xml_content, folder="debitor"
|
||||
)
|
||||
abacus_ssh_upload_invoice(
|
||||
invoice_xml_filename, invoice_xml_content, folder="order"
|
||||
)
|
||||
|
||||
checkout_information.abacus_ssh_upload_done = True
|
||||
checkout_information.invoice_transmitted_at = datetime.datetime.now()
|
||||
checkout_information.save()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Error uploading invoice to Abacus SFTP",
|
||||
checkout_information_id=checkout_information.id,
|
||||
exception=str(e),
|
||||
exc_info=True,
|
||||
)
|
||||
return False
|
||||
|
||||
if filename is None:
|
||||
filename = f"vbv-vv-{uuid4().hex}.xml"
|
||||
|
||||
self.repository.upload_invoice(invoice, filename)
|
||||
def create_invoice_xml(checkout_information: CheckoutInformation):
|
||||
# set fill abacus numbers
|
||||
checkout_information = checkout_information.set_increment_abacus_order_id()
|
||||
customer = checkout_information.user.set_increment_abacus_debitor_number()
|
||||
|
||||
@staticmethod
|
||||
def invoice_xml(
|
||||
customer_number: str,
|
||||
order_date: datetime.date,
|
||||
reference_purchase_order: str,
|
||||
unic_id: str,
|
||||
items: List[Item],
|
||||
) -> str:
|
||||
container = Element("AbaConnectContainer")
|
||||
task = SubElement(container, "Task")
|
||||
parameter = SubElement(task, "Parameter")
|
||||
SubElement(parameter, "Application").text = "ORDE"
|
||||
SubElement(parameter, "Id").text = "Verkaufsauftrag"
|
||||
SubElement(parameter, "MapId").text = "AbaDefault"
|
||||
SubElement(parameter, "Version").text = "2022.00"
|
||||
invoice_xml_content = render_invoice_xml(
|
||||
abacus_debitor_number=customer.abacus_debitor_number,
|
||||
abacus_order_id=checkout_information.abacus_order_id,
|
||||
datatrans_transaction_id=checkout_information.transaction_id,
|
||||
order_date=checkout_information.created_at.date(),
|
||||
item_description=f"{checkout_information.product_name}, {checkout_information.created_at.date().isoformat()}, {checkout_information.user.last_name} {checkout_information.user.first_name}",
|
||||
)
|
||||
|
||||
transaction = SubElement(task, "Transaction")
|
||||
sales_order_header = SubElement(transaction, "SalesOrderHeader", mode="SAVE")
|
||||
sales_order_header_fields = SubElement(
|
||||
sales_order_header, "SalesOrderHeaderFields", mode="SAVE"
|
||||
)
|
||||
# YYYYMMDDhhmmss
|
||||
filename_datetime = checkout_information.created_at.strftime("%Y%m%d%H%M%S")
|
||||
invoice_xml_filename = f"myVBV_orde_{customer.abacus_debitor_number}_{filename_datetime}_{checkout_information.abacus_order_id}.xml"
|
||||
|
||||
SubElement(sales_order_header_fields, "CustomerNumber").text = customer_number
|
||||
SubElement(
|
||||
sales_order_header_fields, "PurchaseOrderDate"
|
||||
).text = order_date.isoformat()
|
||||
SubElement(
|
||||
sales_order_header_fields, "DeliveryDate"
|
||||
).text = order_date.isoformat()
|
||||
SubElement(
|
||||
sales_order_header_fields, "ReferencePurchaseOrder"
|
||||
).text = reference_purchase_order
|
||||
SubElement(sales_order_header_fields, "UnicId").text = unic_id
|
||||
return invoice_xml_filename, invoice_xml_content
|
||||
|
||||
for index, item in enumerate(items, start=1):
|
||||
item_element = SubElement(sales_order_header, "Item", mode="SAVE")
|
||||
item_fields = SubElement(item_element, "ItemFields", mode="SAVE")
|
||||
SubElement(item_fields, "ItemNumber").text = str(index)
|
||||
SubElement(item_fields, "ProductNumber").text = item.product_number
|
||||
SubElement(item_fields, "QuantityOrdered").text = str(item.quantity)
|
||||
|
||||
item_text = SubElement(item_element, "ItemText", mode="SAVE")
|
||||
item_text_fields = SubElement(item_text, "ItemTextFields", mode="SAVE")
|
||||
SubElement(item_text_fields, "Text").text = item.description
|
||||
def create_customer_xml(checkout_information: CheckoutInformation):
|
||||
customer = checkout_information.user.set_increment_abacus_debitor_number()
|
||||
|
||||
return AbacusInvoiceCreator.create_xml_string(container)
|
||||
customer_xml_content = render_customer_xml(
|
||||
abacus_debitor_number=customer.abacus_debitor_number,
|
||||
last_name=checkout_information.last_name,
|
||||
first_name=checkout_information.first_name,
|
||||
company_name=checkout_information.organisation_detail_name
|
||||
if checkout_information.invoice_address == "org"
|
||||
else "",
|
||||
street=(
|
||||
checkout_information.organisation_street
|
||||
if checkout_information.invoice_address == "org"
|
||||
else checkout_information.street
|
||||
),
|
||||
house_number=(
|
||||
checkout_information.organisation_street_number
|
||||
if checkout_information.invoice_address == "org"
|
||||
else checkout_information.street_number
|
||||
),
|
||||
zip_code=(
|
||||
checkout_information.organisation_postal_code
|
||||
if checkout_information.invoice_address == "org"
|
||||
else checkout_information.postal_code
|
||||
),
|
||||
city=(
|
||||
checkout_information.organisation_city
|
||||
if checkout_information.invoice_address == "org"
|
||||
else checkout_information.city
|
||||
),
|
||||
country=(
|
||||
checkout_information.organisation_country_id
|
||||
if checkout_information.invoice_address == "org"
|
||||
else checkout_information.country_id
|
||||
),
|
||||
language=customer.language,
|
||||
email=customer.email,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def customer_xml(
|
||||
customer_number: str,
|
||||
name: str,
|
||||
first_name: str,
|
||||
address_text: str,
|
||||
street: str,
|
||||
house_number: str,
|
||||
zip_code: str,
|
||||
city: str,
|
||||
country: str,
|
||||
language: str,
|
||||
email: str,
|
||||
):
|
||||
container = Element("AbaConnectContainer")
|
||||
task = SubElement(container, "Task")
|
||||
customer_xml_filename = f"myVBV_debi_{customer.abacus_debitor_number}.xml"
|
||||
|
||||
parameter = SubElement(task, "Parameter")
|
||||
SubElement(parameter, "Application").text = "DEBI"
|
||||
SubElement(parameter, "ID").text = "Kunden"
|
||||
SubElement(parameter, "MapID").text = "AbaDefault"
|
||||
SubElement(parameter, "Version").text = "2022.00"
|
||||
return customer_xml_filename, customer_xml_content
|
||||
|
||||
transaction = SubElement(task, "Transaction")
|
||||
customer_element = SubElement(transaction, "Customer", mode="SAVE")
|
||||
|
||||
SubElement(customer_element, "CustomerNumber").text = customer_number
|
||||
SubElement(customer_element, "DefaultCurrency").text = "CHF"
|
||||
SubElement(customer_element, "PaymentTermNumber").text = "1"
|
||||
SubElement(customer_element, "ReminderProcedure").text = "NORM"
|
||||
def render_invoice_xml(
|
||||
abacus_debitor_number: int,
|
||||
abacus_order_id: int,
|
||||
datatrans_transaction_id: str,
|
||||
order_date: datetime.date,
|
||||
item_description: str,
|
||||
) -> str:
|
||||
container = Element("AbaConnectContainer")
|
||||
task = SubElement(container, "Task")
|
||||
parameter = SubElement(task, "Parameter")
|
||||
SubElement(parameter, "Application").text = "ORDE"
|
||||
SubElement(parameter, "Id").text = "Verkaufsauftrag"
|
||||
SubElement(parameter, "MapId").text = "AbaDefault"
|
||||
SubElement(parameter, "Version").text = "2022.00"
|
||||
|
||||
address_data = SubElement(customer_element, "AddressData", mode="SAVE")
|
||||
SubElement(address_data, "AddressNumber").text = customer_number
|
||||
SubElement(address_data, "Name").text = name
|
||||
SubElement(address_data, "FirstName").text = first_name
|
||||
SubElement(address_data, "Text").text = address_text
|
||||
SubElement(address_data, "Street").text = street
|
||||
SubElement(address_data, "HouseNumber").text = house_number
|
||||
SubElement(address_data, "ZIP").text = zip_code
|
||||
SubElement(address_data, "City").text = city
|
||||
SubElement(address_data, "Country").text = country
|
||||
SubElement(address_data, "Language").text = language
|
||||
SubElement(address_data, "Email").text = email
|
||||
transaction = SubElement(task, "Transaction")
|
||||
sales_order_header = SubElement(transaction, "SalesOrderHeader", mode="SAVE")
|
||||
sales_order_header_fields = SubElement(
|
||||
sales_order_header, "SalesOrderHeaderFields", mode="SAVE"
|
||||
)
|
||||
|
||||
return AbacusInvoiceCreator.create_xml_string(container)
|
||||
# Skender: Kundennummer, erste Ziffer abhängig von der Plattform (4 = LMS, 6 = myVBV, 7 = EduManager), Plattform führt ein eigenständiges hochzählendes Mapping.
|
||||
SubElement(sales_order_header_fields, "CustomerNumber").text = str(
|
||||
abacus_debitor_number
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_xml_string(container: Element, encoding: str = "UTF-8") -> str:
|
||||
xml_bytes = tostring(container, encoding)
|
||||
xml_pretty_str = minidom.parseString(xml_bytes).toprettyxml(
|
||||
indent=" ", encoding=encoding
|
||||
)
|
||||
return xml_pretty_str.decode(encoding)
|
||||
# Skender: ePayment: Ablaufnr. für ePayment Rechnung in Abacus
|
||||
SubElement(sales_order_header_fields, "ProcessFlowNumber").text = "30"
|
||||
|
||||
# Skender: ePayment: Zahlungskondition für ePaymente in Abacus 9999 Tage Mahnungsfrist, da schon bezahlt
|
||||
SubElement(sales_order_header_fields, "PaymentCode").text = "9999"
|
||||
|
||||
# Skender: Bestellzeitpunkt
|
||||
SubElement(
|
||||
sales_order_header_fields, "PurchaseOrderDate"
|
||||
).text = order_date.isoformat()
|
||||
|
||||
# Skender: ePayment: TRANSACTION-ID von Datatrans in Bestellreferenz
|
||||
SubElement(
|
||||
sales_order_header_fields, "ReferencePurchaseOrder"
|
||||
).text = datatrans_transaction_id
|
||||
|
||||
# Skender: ePayment: OrderID. max 10 Ziffern, erste Ziffer abhängig von der Plattform (4 = LMS, 6 = myVBV, 7 = EduManager)
|
||||
SubElement(sales_order_header_fields, "GroupingNumberAscii1").text = str(
|
||||
abacus_order_id
|
||||
)
|
||||
|
||||
item_element = SubElement(sales_order_header, "Item", mode="SAVE")
|
||||
item_fields = SubElement(item_element, "ItemFields", mode="SAVE")
|
||||
SubElement(item_fields, "DeliveryDate").text = order_date.isoformat()
|
||||
SubElement(item_fields, "ItemNumber").text = "1"
|
||||
SubElement(item_fields, "ProductNumber").text = "30202"
|
||||
SubElement(item_fields, "QuantityOrdered").text = "1"
|
||||
|
||||
item_text = SubElement(item_element, "ItemText", mode="SAVE")
|
||||
item_text_fields = SubElement(item_text, "ItemTextFields", mode="SAVE")
|
||||
SubElement(item_text_fields, "Text").text = item_description
|
||||
|
||||
return create_xml_string(container)
|
||||
|
||||
|
||||
def render_customer_xml(
|
||||
abacus_debitor_number: int,
|
||||
last_name: str,
|
||||
first_name: str,
|
||||
company_name: str,
|
||||
street: str,
|
||||
house_number: str,
|
||||
zip_code: str,
|
||||
city: str,
|
||||
country: str,
|
||||
language: str,
|
||||
email: str,
|
||||
):
|
||||
container = Element("AbaConnectContainer")
|
||||
task = SubElement(container, "Task")
|
||||
|
||||
parameter = SubElement(task, "Parameter")
|
||||
SubElement(parameter, "Application").text = "DEBI"
|
||||
SubElement(parameter, "ID").text = "Kunden"
|
||||
SubElement(parameter, "MapID").text = "AbaDefault"
|
||||
SubElement(parameter, "Version").text = "2022.00"
|
||||
|
||||
transaction = SubElement(task, "Transaction")
|
||||
customer_element = SubElement(transaction, "Customer", mode="SAVE")
|
||||
|
||||
SubElement(customer_element, "CustomerNumber").text = str(abacus_debitor_number)
|
||||
SubElement(customer_element, "DefaultCurrency").text = "CHF"
|
||||
SubElement(customer_element, "PaymentTermNumber").text = "1"
|
||||
SubElement(customer_element, "ReminderProcedure").text = "NORM"
|
||||
|
||||
address_data = SubElement(customer_element, "AddressData", mode="SAVE")
|
||||
SubElement(address_data, "AddressNumber").text = str(abacus_debitor_number)
|
||||
SubElement(address_data, "Name").text = last_name
|
||||
SubElement(address_data, "FirstName").text = first_name
|
||||
if company_name:
|
||||
SubElement(address_data, "Text").text = company_name
|
||||
SubElement(address_data, "Street").text = street
|
||||
SubElement(address_data, "HouseNumber").text = house_number
|
||||
SubElement(address_data, "ZIP").text = zip_code
|
||||
SubElement(address_data, "City").text = city
|
||||
SubElement(address_data, "Country").text = country
|
||||
SubElement(address_data, "Language").text = language
|
||||
SubElement(address_data, "Email").text = email
|
||||
|
||||
return create_xml_string(container)
|
||||
|
||||
|
||||
def create_xml_string(container: Element, encoding: str = "utf-8") -> str:
|
||||
xml_bytes = tostring(container, encoding)
|
||||
xml_pretty_str = minidom.parseString(xml_bytes).toprettyxml(
|
||||
indent=" ", encoding=encoding
|
||||
)
|
||||
return xml_pretty_str.decode(encoding)
|
||||
|
||||
|
||||
def abacus_ssh_upload_invoice(
|
||||
filename: str, content_xml: str, folder: str = ""
|
||||
) -> None:
|
||||
invoice_io = BytesIO(content_xml.encode("utf-8"))
|
||||
with AbacusSftpClient() as sftp_client:
|
||||
path = filename
|
||||
if folder:
|
||||
path = f"{folder}/{filename}"
|
||||
|
||||
sftp_client.putfo(invoice_io, path)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
from django.conf import settings
|
||||
from paramiko.client import AutoAddPolicy, SSHClient
|
||||
|
||||
|
||||
def _create_abacus_sftp_client():
|
||||
ssh_client = SSHClient()
|
||||
|
||||
ssh_client.set_missing_host_key_policy(AutoAddPolicy())
|
||||
ssh_client.connect(
|
||||
hostname=settings.ABACUS_EXPORT_SFTP_HOST,
|
||||
port=settings.ABACUS_EXPORT_SFTP_PORT,
|
||||
username=settings.ABACUS_EXPORT_SFTP_USERNAME,
|
||||
password=settings.ABACUS_EXPORT_SFTP_PASSWORD,
|
||||
look_for_keys=False,
|
||||
allow_agent=False,
|
||||
)
|
||||
|
||||
return ssh_client.open_sftp(), ssh_client
|
||||
|
||||
|
||||
class AbacusSftpClient:
|
||||
def __enter__(self):
|
||||
(self.sftp_client, self.ssh_client) = _create_abacus_sftp_client()
|
||||
return self.sftp_client
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_traceback):
|
||||
# self.sftp_client.close()
|
||||
self.ssh_client.close()
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
|
||||
from vbv_lernwelt.shop.models import CheckoutInformation
|
||||
|
||||
|
||||
@dataclass
|
||||
class Item:
|
||||
product_number: str
|
||||
quantity: int
|
||||
description: str
|
||||
|
||||
|
||||
class InvoiceCreator(ABC):
|
||||
@abstractmethod
|
||||
def create_invoice(
|
||||
self,
|
||||
checkout_information: CheckoutInformation,
|
||||
filename: str = None,
|
||||
):
|
||||
pass
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
from abc import ABC, abstractmethod
|
||||
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class InvoiceRepository(ABC):
|
||||
@abstractmethod
|
||||
def upload_invoice(self, invoice: str, filename: str):
|
||||
pass
|
||||
|
||||
|
||||
class SFTPInvoiceRepository(InvoiceRepository):
|
||||
def __init__(self, hostname: str, username: str, password: str, port: int = 22):
|
||||
self.hostname = hostname
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.port = port
|
||||
|
||||
def upload_invoice(self, invoice: str, filename: str) -> None:
|
||||
from io import BytesIO
|
||||
|
||||
from paramiko import AutoAddPolicy, SSHClient
|
||||
|
||||
invoice_io = BytesIO(invoice.encode("utf-8"))
|
||||
ssh_client = SSHClient()
|
||||
|
||||
try:
|
||||
ssh_client.set_missing_host_key_policy(AutoAddPolicy())
|
||||
ssh_client.connect(
|
||||
self.hostname,
|
||||
port=self.port,
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
)
|
||||
|
||||
with ssh_client.open_sftp() as sftp_client:
|
||||
sftp_client.putfo(invoice_io, f"uploads/{filename}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Could not upload invoice", exc_info=e)
|
||||
finally:
|
||||
ssh_client.close()
|
||||
|
|
@ -3,6 +3,16 @@
|
|||
from django.db import migrations, models
|
||||
|
||||
|
||||
def add_default_shop_product(apps, schema_editor):
|
||||
Product = apps.get_model("shop", "Product")
|
||||
Product.objects.create(
|
||||
sku="vv-de",
|
||||
name="Versicherungsvermittler/-in VBV",
|
||||
description="Versicherungsvermittler/-in VBV DE",
|
||||
price=324_00,
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("shop", "0008_auto_20231117_0905"),
|
||||
|
|
@ -16,4 +26,5 @@ class Migration(migrations.Migration):
|
|||
help_text="The total price of the product in centimes -> 1000 = 10.00 CHF"
|
||||
),
|
||||
),
|
||||
migrations.RunPython(add_default_shop_product),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
# Generated by Django 3.2.20 on 2024-05-29 13:34
|
||||
|
||||
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.invoice_address = "org"
|
||||
info.save(update_fields=["country", "organisation_country", "invoice_address"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("shop", "0012_delete_country"),
|
||||
("core", "0009_country_refactor"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="checkoutinformation",
|
||||
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",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="BillingAddress",
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.2.20 on 2024-05-31 15:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("shop", "0013_checkoutinformation_abacus_order_id"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="checkoutinformation",
|
||||
name="abacus_ssh_upload_done",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,33 +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):
|
||||
|
|
@ -61,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)
|
||||
|
|
@ -88,15 +70,50 @@ 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)
|
||||
|
||||
# is only set by abacus invoice export code
|
||||
abacus_order_id = models.BigIntegerField(unique=True, null=True, blank=True)
|
||||
abacus_ssh_upload_done = models.BooleanField(default=False)
|
||||
|
||||
def set_increment_abacus_order_id(self):
|
||||
if self.abacus_order_id:
|
||||
return self
|
||||
|
||||
# Get the current maximum abacus_order_id and increment it by 1
|
||||
current_max = CheckoutInformation.objects.aggregate(
|
||||
max_number=Max("abacus_order_id")
|
||||
)["max_number"]
|
||||
new_abacus_order_id = (
|
||||
current_max if current_max is not None else 6_000_000_000
|
||||
) + 1
|
||||
self.abacus_order_id = new_abacus_order_id
|
||||
self.save()
|
||||
return self
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ def is_signature_valid(
|
|||
return s0_actual == s0_expected
|
||||
|
||||
|
||||
def init_transaction(
|
||||
def init_datatrans_transaction(
|
||||
user: User,
|
||||
amount_chf_centimes: int,
|
||||
redirect_url_success: str,
|
||||
|
|
@ -53,13 +53,6 @@ def init_transaction(
|
|||
redirect_url_cancel: str,
|
||||
webhook_url: str,
|
||||
):
|
||||
if overwrite := settings.DATATRANS_DEBUG_WEBHOOK_OVERWRITE:
|
||||
logger.warning(
|
||||
"APPLYING DEBUG DATATRANS WEBHOOK OVERWRITE!",
|
||||
webhook_url=overwrite,
|
||||
)
|
||||
webhook_url = overwrite
|
||||
|
||||
payload = {
|
||||
# We use autoSettle=True, so that we don't have to settle the transaction:
|
||||
# -> Be aware that autoSettle has implications of the possible transaction states
|
||||
|
|
@ -76,6 +69,10 @@ def init_transaction(
|
|||
},
|
||||
}
|
||||
|
||||
# add testing configuration data
|
||||
if "fakeapi" in settings.DATATRANS_API_ENDPOINT:
|
||||
payload["user_id"] = str(user.id)
|
||||
|
||||
logger.info("Initiating transaction", payload=payload)
|
||||
|
||||
response = requests.post(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
from factory.django import DjangoModelFactory
|
||||
|
||||
from vbv_lernwelt.shop.const import VV_DE_PRODUCT_SKU
|
||||
from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState
|
||||
|
||||
|
||||
class CheckoutInformationFactory(DjangoModelFactory):
|
||||
class Meta:
|
||||
model = CheckoutInformation
|
||||
|
||||
product_sku = VV_DE_PRODUCT_SKU
|
||||
product_price = 324_30
|
||||
state = CheckoutState.PAID
|
||||
product_name = "Versicherungsvermittler/-in VBV"
|
||||
product_description = "Versicherungsvermittler/-in VBV DE"
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
from datetime import date, datetime
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from vbv_lernwelt.core.admin import User
|
||||
from vbv_lernwelt.core.create_default_users import create_default_users
|
||||
from vbv_lernwelt.core.model_utils import add_countries
|
||||
from vbv_lernwelt.shop.invoice.abacus import (
|
||||
create_customer_xml,
|
||||
create_invoice_xml,
|
||||
render_customer_xml,
|
||||
render_invoice_xml,
|
||||
)
|
||||
from vbv_lernwelt.shop.tests.factories import CheckoutInformationFactory
|
||||
|
||||
USER_USERNAME = "testuser"
|
||||
USER_EMAIL = "test@example.com"
|
||||
USER_PASSWORD = "testpassword"
|
||||
|
||||
|
||||
class AbacusInvoiceTestCase(TestCase):
|
||||
def setUp(self):
|
||||
add_countries(small_set=True)
|
||||
create_default_users()
|
||||
|
||||
def test_create_invoice_xml(self):
|
||||
# set abacus_number before
|
||||
_pat = User.objects.get(username="patrizia.huggel@eiger-versicherungen.ch")
|
||||
_pat.abacus_debitor_number = 60000011
|
||||
_pat.save()
|
||||
_ignore_checkout_information = CheckoutInformationFactory(
|
||||
user=_pat, abacus_order_id=6_000_000_123
|
||||
)
|
||||
|
||||
feuz = User.objects.get(username="andreas.feuz@eiger-versicherungen.ch")
|
||||
feuz_checkout_info = CheckoutInformationFactory(
|
||||
user=feuz,
|
||||
transaction_id="24021508331287484",
|
||||
)
|
||||
feuz_checkout_info.created_at = datetime(2024, 2, 15, 8, 33, 12, 0)
|
||||
invoice_xml_filename, invoice_xml_content = create_invoice_xml(
|
||||
feuz_checkout_info
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
invoice_xml_filename, "myVBV_orde_60000012_20240215083312_6000000124.xml"
|
||||
)
|
||||
|
||||
print(invoice_xml_content)
|
||||
assert "<CustomerNumber>60000012</CustomerNumber>" in invoice_xml_content
|
||||
assert (
|
||||
"<PurchaseOrderDate>2024-02-15</PurchaseOrderDate>" in invoice_xml_content
|
||||
)
|
||||
assert (
|
||||
"<GroupingNumberAscii1>6000000124</GroupingNumberAscii1>"
|
||||
in invoice_xml_content
|
||||
)
|
||||
assert (
|
||||
"<ReferencePurchaseOrder>24021508331287484</ReferencePurchaseOrder>"
|
||||
in invoice_xml_content
|
||||
)
|
||||
assert "<DeliveryDate>2024-02-15</DeliveryDate>" in invoice_xml_content
|
||||
assert (
|
||||
"<Text>Versicherungsvermittler/-in VBV, 2024-02-15, Feuz Andreas</Text>"
|
||||
in invoice_xml_content
|
||||
)
|
||||
|
||||
def test_render_invoice_xml(self):
|
||||
invoice_xml = render_invoice_xml(
|
||||
abacus_debitor_number=60000012,
|
||||
abacus_order_id=6000000001,
|
||||
order_date=date(2024, 2, 15),
|
||||
datatrans_transaction_id="24021508331287484",
|
||||
item_description="myVBV Versicherungsvermittler - Lernpfad, 2024-02-15, Skender, Gebhart-Krasniqi",
|
||||
)
|
||||
|
||||
# example from Skender
|
||||
expected_xml = """<?xml version="1.0" encoding="utf-8"?>
|
||||
<AbaConnectContainer>
|
||||
<Task>
|
||||
<Parameter>
|
||||
<Application>ORDE</Application>
|
||||
<Id>Verkaufsauftrag</Id>
|
||||
<MapId>AbaDefault</MapId>
|
||||
<Version>2022.00</Version>
|
||||
</Parameter>
|
||||
<Transaction>
|
||||
<SalesOrderHeader mode="SAVE">
|
||||
<SalesOrderHeaderFields mode="SAVE">
|
||||
<CustomerNumber>60000012</CustomerNumber>
|
||||
<ProcessFlowNumber>30</ProcessFlowNumber>
|
||||
<PaymentCode>9999</PaymentCode>
|
||||
<PurchaseOrderDate>2024-02-15</PurchaseOrderDate>
|
||||
<ReferencePurchaseOrder>24021508331287484</ReferencePurchaseOrder>
|
||||
<GroupingNumberAscii1>6000000001</GroupingNumberAscii1>
|
||||
</SalesOrderHeaderFields>
|
||||
<Item mode="SAVE">
|
||||
<ItemFields mode="SAVE">
|
||||
<DeliveryDate>2024-02-15</DeliveryDate>
|
||||
<ItemNumber>1</ItemNumber>
|
||||
<ProductNumber>30202</ProductNumber>
|
||||
<QuantityOrdered>1</QuantityOrdered>
|
||||
</ItemFields>
|
||||
<ItemText mode="SAVE">
|
||||
<ItemTextFields mode="SAVE">
|
||||
<Text>myVBV Versicherungsvermittler - Lernpfad, 2024-02-15, Skender, Gebhart-Krasniqi</Text>
|
||||
</ItemTextFields>
|
||||
</ItemText>
|
||||
</Item>
|
||||
</SalesOrderHeader>
|
||||
</Transaction>
|
||||
</Task>
|
||||
</AbaConnectContainer>
|
||||
"""
|
||||
|
||||
self.maxDiff = None
|
||||
self.assertXMLEqual(expected_xml, invoice_xml)
|
||||
|
||||
def test_create_customer_xml_with_company_address(self):
|
||||
_pat = User.objects.get(username="patrizia.huggel@eiger-versicherungen.ch")
|
||||
_pat.abacus_debitor_number = 60000011
|
||||
_pat.save()
|
||||
_ignore_checkout_information = CheckoutInformationFactory(
|
||||
user=_pat, abacus_order_id=6_000_000_123
|
||||
)
|
||||
|
||||
feuz = User.objects.get(username="andreas.feuz@eiger-versicherungen.ch")
|
||||
feuz_checkout_info = CheckoutInformationFactory(
|
||||
user=feuz,
|
||||
transaction_id="24021508331287484",
|
||||
first_name="Andreas",
|
||||
last_name="Feuz",
|
||||
street="Eggersmatt",
|
||||
street_number="32",
|
||||
postal_code="1719",
|
||||
city="Zumholz",
|
||||
country_id="CH",
|
||||
invoice_address="org",
|
||||
organisation_detail_name="VBV",
|
||||
organisation_street="Laupenstrasse",
|
||||
organisation_street_number="10",
|
||||
organisation_postal_code="3000",
|
||||
organisation_city="Bern",
|
||||
organisation_country_id="CH",
|
||||
)
|
||||
feuz_checkout_info.created_at = datetime(2024, 2, 15, 8, 33, 12, 0)
|
||||
|
||||
customer_xml_filename, customer_xml_content = create_customer_xml(
|
||||
checkout_information=feuz_checkout_info
|
||||
)
|
||||
print(customer_xml_content)
|
||||
print(customer_xml_filename)
|
||||
|
||||
self.assertEqual("myVBV_debi_60000012.xml", customer_xml_filename)
|
||||
assert "<CustomerNumber>60000012</CustomerNumber>" in customer_xml_content
|
||||
assert (
|
||||
"<Email>andreas.feuz@eiger-versicherungen.ch</Email>"
|
||||
in customer_xml_content
|
||||
)
|
||||
assert "<AddressNumber>60000012</AddressNumber>" in customer_xml_content
|
||||
assert "<Name>Feuz</Name>" in customer_xml_content
|
||||
assert "<Text>VBV</Text>" in customer_xml_content
|
||||
assert "<Street>Laupenstrasse</Street>" in customer_xml_content
|
||||
assert "<Country>CH</Country>" in customer_xml_content
|
||||
|
||||
def test_render_customer_xml(self):
|
||||
customer_xml = render_customer_xml(
|
||||
abacus_debitor_number=60000012,
|
||||
last_name="Gebhart-Krasniqi",
|
||||
first_name="Skender",
|
||||
company_name="VBV",
|
||||
street="Laupenstrasse",
|
||||
house_number="10",
|
||||
zip_code="3000",
|
||||
city="Bern",
|
||||
country="CH",
|
||||
language="de",
|
||||
email="skender.krasniqi@vbv-afa.ch",
|
||||
)
|
||||
|
||||
# example from Skender
|
||||
expected_xml = """<?xml version="1.0" encoding="utf-8"?>
|
||||
<AbaConnectContainer>
|
||||
<Task>
|
||||
<Parameter>
|
||||
<Application>DEBI</Application>
|
||||
<ID>Kunden</ID>
|
||||
<MapID>AbaDefault</MapID>
|
||||
<Version>2022.00</Version>
|
||||
</Parameter>
|
||||
<Transaction>
|
||||
<Customer mode="SAVE">
|
||||
<CustomerNumber>60000012</CustomerNumber>
|
||||
<DefaultCurrency>CHF</DefaultCurrency>
|
||||
<PaymentTermNumber>1</PaymentTermNumber>
|
||||
<ReminderProcedure>NORM</ReminderProcedure>
|
||||
<AddressData mode="SAVE">
|
||||
<AddressNumber>60000012</AddressNumber>
|
||||
<Name>Gebhart-Krasniqi</Name>
|
||||
<FirstName>Skender</FirstName>
|
||||
<Text>VBV</Text>
|
||||
<Street>Laupenstrasse</Street>
|
||||
<HouseNumber>10</HouseNumber>
|
||||
<ZIP>3000</ZIP>
|
||||
<City>Bern</City>
|
||||
<Country>CH</Country>
|
||||
<Language>de</Language>
|
||||
<Email>skender.krasniqi@vbv-afa.ch</Email>
|
||||
</AddressData>
|
||||
</Customer>
|
||||
</Transaction>
|
||||
</Task>
|
||||
</AbaConnectContainer>
|
||||
"""
|
||||
|
||||
self.maxDiff = None
|
||||
self.assertXMLEqual(expected_xml, customer_xml)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
from django.test import TestCase
|
||||
|
||||
from vbv_lernwelt.core.create_default_users import create_default_users
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.shop.models import CheckoutInformation
|
||||
from vbv_lernwelt.shop.tests.factories import CheckoutInformationFactory
|
||||
|
||||
|
||||
class AbacusOrderIdTestCase(TestCase):
|
||||
def setUp(self):
|
||||
create_default_users()
|
||||
self.pat = User.objects.get(username="patrizia.huggel@eiger-versicherungen.ch")
|
||||
self.feuz = User.objects.get(username="andreas.feuz@eiger-versicherungen.ch")
|
||||
|
||||
def test_set_increment_abacus_order_id(self):
|
||||
checkout_info = CheckoutInformationFactory(user=self.pat)
|
||||
|
||||
self.assertIsNone(checkout_info.abacus_order_id)
|
||||
checkout_info.set_increment_abacus_order_id()
|
||||
|
||||
self.assertEqual(checkout_info.abacus_order_id, 6_000_000_001)
|
||||
checkout_info = CheckoutInformation.objects.get(id=checkout_info.id)
|
||||
self.assertEqual(checkout_info.abacus_order_id, 6_000_000_001)
|
||||
checkout_info = checkout_info.set_increment_abacus_order_id()
|
||||
self.assertEqual(checkout_info.abacus_order_id, 6_000_000_001)
|
||||
|
||||
checkout_info2 = CheckoutInformationFactory(user=self.feuz)
|
||||
checkout_info2 = checkout_info2.set_increment_abacus_order_id()
|
||||
self.assertEqual(checkout_info2.abacus_order_id, 6_000_000_002)
|
||||
checkout_info2 = CheckoutInformation.objects.get(id=checkout_info2.id)
|
||||
self.assertEqual(checkout_info2.abacus_order_id, 6_000_000_002)
|
||||
|
|
@ -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/")
|
||||
|
|
@ -21,13 +21,14 @@ TEST_ADDRESS_DATA = {
|
|||
"street_number": "1",
|
||||
"postal_code": "1234",
|
||||
"city": "Test City",
|
||||
"country": "209",
|
||||
"company_name": "Test Company",
|
||||
"company_street": "Test Company Street",
|
||||
"company_street_number": "1",
|
||||
"company_postal_code": "1234",
|
||||
"company_city": "Test Company City",
|
||||
"company_country": "209",
|
||||
"country_code": "CH",
|
||||
"invoice_address": "org",
|
||||
"organisation_detail_name": "Test Company",
|
||||
"organisation_street": "Test Company Street",
|
||||
"organisation_street_number": "1",
|
||||
"organisation_postal_code": "1234",
|
||||
"organisation_city": "Test Company City",
|
||||
"organisation_country_code": "CH",
|
||||
}
|
||||
|
||||
REDIRECT_URL = "http://testserver/redirect-url"
|
||||
|
|
@ -50,40 +51,9 @@ class CheckoutAPITestCase(APITestCase):
|
|||
)
|
||||
|
||||
self.client.login(username=USER_USERNAME, password=USER_PASSWORD)
|
||||
add_countries()
|
||||
add_countries(small_set=True)
|
||||
|
||||
@patch("vbv_lernwelt.shop.views.init_transaction")
|
||||
def test_checkout_no_company_address_updates_user(self, mock_init_transaction):
|
||||
# GIVEN
|
||||
mock_init_transaction.return_value = "1234567890"
|
||||
|
||||
# WHEN
|
||||
response = self.client.post(
|
||||
path=reverse("checkout-vv"),
|
||||
format="json",
|
||||
data={
|
||||
"redirect_url": REDIRECT_URL,
|
||||
"product": VV_DE_PRODUCT_SKU,
|
||||
"address": {
|
||||
"first_name": "Test",
|
||||
"last_name": "User",
|
||||
"street": "Test Street",
|
||||
"street_number": "1",
|
||||
"postal_code": "1234",
|
||||
"city": "Test City",
|
||||
"country": "209",
|
||||
# NO company data
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
user = User.objects.get(username=USER_USERNAME)
|
||||
self.assertEqual(user.invoice_address, User.INVOICE_ADDRESS_PRIVATE)
|
||||
|
||||
@patch("vbv_lernwelt.shop.views.init_transaction")
|
||||
@patch("vbv_lernwelt.shop.views.init_datatrans_transaction")
|
||||
def test_checkout_happy_case(self, mock_init_transaction):
|
||||
# GIVEN
|
||||
mock_init_transaction.return_value = "1234567890"
|
||||
|
|
@ -106,13 +76,12 @@ class CheckoutAPITestCase(APITestCase):
|
|||
response.json()["next_step_url"],
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
CheckoutInformation.objects.filter(
|
||||
user=self.user,
|
||||
product_sku=VV_DE_PRODUCT_SKU,
|
||||
state=CheckoutState.ONGOING,
|
||||
).exists()
|
||||
)
|
||||
ci = CheckoutInformation.objects.first()
|
||||
self.assertEqual(ci.first_name, "Test")
|
||||
self.assertEqual(ci.last_name, "User")
|
||||
self.assertEqual(ci.country_id, "CH")
|
||||
self.assertEqual(ci.state, "ongoing")
|
||||
self.assertEqual(ci.transaction_id, "1234567890")
|
||||
|
||||
mock_init_transaction.assert_called_once_with(
|
||||
user=self.user,
|
||||
|
|
@ -123,13 +92,7 @@ class CheckoutAPITestCase(APITestCase):
|
|||
webhook_url=f"{REDIRECT_URL}/api/shop/transaction/webhook/",
|
||||
)
|
||||
|
||||
user = User.objects.get(username=USER_USERNAME)
|
||||
|
||||
self.assertEqual(user.street, TEST_ADDRESS_DATA["street"])
|
||||
self.assertEqual(str(user.country.country_id), TEST_ADDRESS_DATA["country"])
|
||||
self.assertEqual(user.invoice_address, User.INVOICE_ADDRESS_ORGANISATION)
|
||||
|
||||
@patch("vbv_lernwelt.shop.views.init_transaction")
|
||||
@patch("vbv_lernwelt.shop.views.init_datatrans_transaction")
|
||||
def test_incomplete_setup(self, mock_init_transaction):
|
||||
# GIVEN
|
||||
Product.objects.all().delete()
|
||||
|
|
@ -156,7 +119,7 @@ class CheckoutAPITestCase(APITestCase):
|
|||
|
||||
self.assertEqual(expected, response.json()["next_step_url"])
|
||||
|
||||
@patch("vbv_lernwelt.shop.views.init_transaction")
|
||||
@patch("vbv_lernwelt.shop.views.init_datatrans_transaction")
|
||||
def test_checkout_init_transaction_exception(self, mock_init_transaction):
|
||||
# GIVEN
|
||||
mock_init_transaction.side_effect = InitTransactionException(
|
||||
|
|
@ -213,7 +176,7 @@ class CheckoutAPITestCase(APITestCase):
|
|||
response.json()["next_step_url"],
|
||||
)
|
||||
|
||||
@patch("vbv_lernwelt.shop.views.init_transaction")
|
||||
@patch("vbv_lernwelt.shop.views.init_datatrans_transaction")
|
||||
def test_checkout_double_checkout(self, mock_init_transaction):
|
||||
"""Advise by Datatrans: Just create a new transaction."""
|
||||
# GIVEN
|
||||
|
|
@ -277,7 +240,7 @@ class CheckoutAPITestCase(APITestCase):
|
|||
).exists()
|
||||
)
|
||||
|
||||
@patch("vbv_lernwelt.shop.views.init_transaction")
|
||||
@patch("vbv_lernwelt.shop.views.init_datatrans_transaction")
|
||||
def test_checkout_failed_creates_new(self, mock_init_transaction):
|
||||
# GIVEN
|
||||
state = CheckoutState.FAILED
|
||||
|
|
@ -310,7 +273,7 @@ class CheckoutAPITestCase(APITestCase):
|
|||
response.json()["next_step_url"],
|
||||
)
|
||||
|
||||
@patch("vbv_lernwelt.shop.views.init_transaction")
|
||||
@patch("vbv_lernwelt.shop.views.init_datatrans_transaction")
|
||||
def test_checkout_cancelled_creates_new(self, mock_init_transaction):
|
||||
# GIVEN
|
||||
state = CheckoutState.CANCELED
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from django.test import override_settings, TestCase
|
|||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.shop.services import (
|
||||
get_payment_url,
|
||||
init_transaction,
|
||||
init_datatrans_transaction,
|
||||
InitTransactionException,
|
||||
)
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ class DatatransServiceTest(TestCase):
|
|||
self.user.language = "it"
|
||||
|
||||
# WHEN
|
||||
transaction_id = init_transaction(
|
||||
transaction_id = init_datatrans_transaction(
|
||||
user=self.user,
|
||||
amount_chf_centimes=324_30,
|
||||
redirect_url_success=f"{REDIRECT_URL}/success",
|
||||
|
|
@ -76,7 +76,7 @@ class DatatransServiceTest(TestCase):
|
|||
|
||||
# WHEN / THEN
|
||||
with self.assertRaises(InitTransactionException):
|
||||
init_transaction(
|
||||
init_datatrans_transaction(
|
||||
user=self.user,
|
||||
amount_chf_centimes=324_30,
|
||||
redirect_url_success=f"/success",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from rest_framework import status
|
|||
from rest_framework.test import APITestCase
|
||||
|
||||
from vbv_lernwelt.core.admin import User
|
||||
from vbv_lernwelt.core.model_utils import add_countries
|
||||
from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN_ID
|
||||
from vbv_lernwelt.course.creators.test_utils import create_course, create_course_session
|
||||
from vbv_lernwelt.course.models import CourseSessionUser
|
||||
|
|
@ -29,6 +30,8 @@ def create_checkout_information(
|
|||
|
||||
class DatatransWebhookTestCase(APITestCase):
|
||||
def setUp(self) -> None:
|
||||
add_countries(small_set=True)
|
||||
|
||||
course, _ = create_course(
|
||||
title="VV_in_DE",
|
||||
# needed for VV_DE_PRODUCT_SKU
|
||||
|
|
@ -102,13 +105,13 @@ class DatatransWebhookTestCase(APITestCase):
|
|||
checkout_info.street_number = "1"
|
||||
checkout_info.postal_code = "1234"
|
||||
checkout_info.city = "Musterstadt"
|
||||
checkout_info.country = "Schweiz"
|
||||
checkout_info.company_name = "Musterfirma"
|
||||
checkout_info.company_street = "Firmastrasse"
|
||||
checkout_info.company_street_number = "2"
|
||||
checkout_info.company_postal_code = "5678"
|
||||
checkout_info.company_city = "Firmastadt"
|
||||
checkout_info.company_country = "Schweiz"
|
||||
checkout_info.country_id = "CH"
|
||||
checkout_info.organisation_detail_name = "Musterfirma"
|
||||
checkout_info.organisation_street = "Firmastrasse"
|
||||
checkout_info.organisation_street_number = "2"
|
||||
checkout_info.organisation_postal_code = "5678"
|
||||
checkout_info.organisation_city = "Firmastadt"
|
||||
checkout_info.organisation_country_id = "CH"
|
||||
checkout_info.save()
|
||||
|
||||
mock_is_signature_valid.return_value = True
|
||||
|
|
@ -181,10 +184,10 @@ class DatatransWebhookTestCase(APITestCase):
|
|||
"target_url": "https://my.vbv-afa.ch/",
|
||||
"name": "Max Mustermann",
|
||||
"private_street": "Musterstrasse 1",
|
||||
"private_city": "1234 Musterstadt Schweiz",
|
||||
"private_city": "CH-1234 Musterstadt",
|
||||
"company_name": "Musterfirma",
|
||||
"company_street": "Firmastrasse 2",
|
||||
"company_city": "5678 Firmastadt Schweiz",
|
||||
"company_city": "CH-5678 Firmastadt",
|
||||
},
|
||||
template_language=self.user.language,
|
||||
fail_silently=ANY,
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
from datetime import date
|
||||
from unittest.mock import create_autospec
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from vbv_lernwelt.core.admin import User
|
||||
from vbv_lernwelt.shop.invoice.abacus import AbacusInvoiceCreator
|
||||
from vbv_lernwelt.shop.invoice.creator import Item
|
||||
from vbv_lernwelt.shop.invoice.repositories import InvoiceRepository
|
||||
from vbv_lernwelt.shop.models import CheckoutInformation
|
||||
|
||||
USER_USERNAME = "testuser"
|
||||
USER_EMAIL = "test@example.com"
|
||||
USER_PASSWORD = "testpassword"
|
||||
|
||||
|
||||
class InvoiceTestCase(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.user = User.objects.create_user(
|
||||
username=USER_USERNAME,
|
||||
email=USER_EMAIL,
|
||||
password=USER_PASSWORD,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
def test_render_invoice(self):
|
||||
# GIVEN
|
||||
creator = AbacusInvoiceCreator(repository=create_autospec(InvoiceRepository))
|
||||
items = [Item(product_number="001", quantity=1, description="Test Item")]
|
||||
customer_number = "12345"
|
||||
order_date = date(2023, 1, 1)
|
||||
reference_purchase_order = "PO12345678"
|
||||
unic_id = "UNIC001"
|
||||
|
||||
# WHEN
|
||||
invoice_xml = creator.invoice_xml(
|
||||
customer_number,
|
||||
order_date,
|
||||
reference_purchase_order,
|
||||
unic_id,
|
||||
items,
|
||||
)
|
||||
|
||||
# THEN
|
||||
assert "<CustomerNumber>12345</CustomerNumber>" in invoice_xml
|
||||
assert "<ItemNumber>1</ItemNumber>" in invoice_xml
|
||||
assert "<ProductNumber>001</ProductNumber>" in invoice_xml
|
||||
assert "<QuantityOrdered>1</QuantityOrdered>" in invoice_xml
|
||||
assert "<Text>Test Item</Text>" in invoice_xml
|
||||
|
||||
def test_create_invoice_calls_upload(self):
|
||||
# GIVEN
|
||||
repository_mock = create_autospec(InvoiceRepository)
|
||||
|
||||
creator = AbacusInvoiceCreator(repository=repository_mock)
|
||||
|
||||
expected_filename = "test.xml"
|
||||
|
||||
checkout_information = CheckoutInformation.objects.create(
|
||||
user=self.user,
|
||||
transaction_id="12345",
|
||||
product_sku="001",
|
||||
product_name="Test Product",
|
||||
product_description="Test Product Description",
|
||||
product_price=1000,
|
||||
state="initialized",
|
||||
)
|
||||
|
||||
# WHEN
|
||||
creator.create_invoice(
|
||||
checkout_information=checkout_information,
|
||||
filename=expected_filename,
|
||||
)
|
||||
|
||||
# THEN
|
||||
repository_mock.upload_invoice.assert_called_once()
|
||||
uploaded_invoice, uploaded_filename = repository_mock.upload_invoice.call_args[
|
||||
0
|
||||
]
|
||||
|
||||
assert uploaded_filename == expected_filename
|
||||
assert "<CustomerNumber>12345</CustomerNumber>" in uploaded_invoice
|
||||
assert "<ItemNumber>1</ItemNumber>" in uploaded_invoice
|
||||
assert "<ProductNumber>001</ProductNumber>" in uploaded_invoice
|
||||
assert "<QuantityOrdered>1</QuantityOrdered>" in uploaded_invoice
|
||||
assert "<Text>Test Product Description</Text>" in uploaded_invoice
|
||||
|
|
@ -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/",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
import structlog
|
||||
from django.conf import settings
|
||||
from django.http import JsonResponse
|
||||
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 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,17 +12,11 @@ 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,
|
||||
init_transaction,
|
||||
init_datatrans_transaction,
|
||||
InitTransactionException,
|
||||
is_signature_valid,
|
||||
)
|
||||
|
|
@ -40,39 +31,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 +83,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)
|
||||
|
|
@ -149,7 +107,7 @@ def checkout_vv(request):
|
|||
return next_step_response(url="/")
|
||||
|
||||
try:
|
||||
transaction_id = init_transaction(
|
||||
transaction_id = init_datatrans_transaction(
|
||||
user=request.user,
|
||||
amount_chf_centimes=product.price,
|
||||
redirect_url_success=checkout_success_url(
|
||||
|
|
@ -171,6 +129,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 +151,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))
|
||||
|
||||
|
||||
|
|
@ -209,10 +174,10 @@ def send_vv_welcome_email(checkout_info: CheckoutInformation):
|
|||
"target_url": "https://my.vbv-afa.ch/",
|
||||
"name": f"{checkout_info.first_name} {checkout_info.last_name}",
|
||||
"private_street": f"{checkout_info.street} {checkout_info.street_number}",
|
||||
"private_city": f"{checkout_info.postal_code} {checkout_info.city} {checkout_info.country}",
|
||||
"company_name": checkout_info.company_name,
|
||||
"company_street": f"{checkout_info.company_street} {checkout_info.company_street_number}",
|
||||
"company_city": f"{checkout_info.company_postal_code} {checkout_info.company_city} {checkout_info.company_country}",
|
||||
"private_city": f"{checkout_info.country_id}-{checkout_info.postal_code} {checkout_info.city}",
|
||||
"company_name": checkout_info.organisation_detail_name,
|
||||
"company_street": f"{checkout_info.organisation_street} {checkout_info.organisation_street_number}",
|
||||
"company_city": f"{checkout_info.organisation_country_id}-{checkout_info.organisation_postal_code} {checkout_info.organisation_city}",
|
||||
},
|
||||
template_language=checkout_info.user.language,
|
||||
fail_silently=True,
|
||||
|
|
@ -266,35 +231,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_id=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_id=checkout_info.company_country
|
||||
).first()
|
||||
|
||||
user.invoice_address = User.INVOICE_ADDRESS_ORGANISATION
|
||||
|
||||
user.save()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
#!/bin/bash
|
||||
|
||||
# set location to script directory
|
||||
cd "${0%/*}"
|
||||
|
||||
# start python sftp test server (for abacus exports)
|
||||
rm -rf sftptest
|
||||
mkdir -p sftptest/debitor
|
||||
mkdir -p sftptest/order
|
||||
(cd sftptest && sftpserver -p 3373)
|
||||
Loading…
Reference in New Issue