Merged in feature/abacus-export (pull request #335)

Feature/abacus export

Approved-by: Christian Cueni
This commit is contained in:
Daniel Egger 2024-06-20 06:46:15 +00:00 committed by Christian Cueni
commit ceb7d07f07
60 changed files with 2258 additions and 1058 deletions

3
.gitignore vendored
View File

@ -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/

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -8,7 +8,8 @@ export type Organisation = {
};
export type Country = {
id: number;
country_code: string;
vbv_country_id: number;
name: string;
};

View File

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

Binary file not shown.

Binary file not shown.

View File

@ -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

View File

@ -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

View File

@ -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",
]

View File

@ -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):

View File

@ -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

View File

@ -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

29
server/conftest.py Normal file
View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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`

View File

@ -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,
},
)

View File

@ -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

View File

@ -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",

View File

@ -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

View File

@ -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)

View File

@ -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(

View File

@ -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),
]

View File

@ -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),
),
]

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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
)

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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),
]

View File

@ -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",
),
]

View File

@ -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),
),
]

View File

@ -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

View File

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

View File

@ -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(

View File

@ -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"

View File

@ -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)

View File

@ -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)

View File

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

View File

@ -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

View File

@ -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",

View File

@ -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,

View File

@ -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

View File

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

View File

@ -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()

10
start_sftpserver.sh Executable file
View File

@ -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)