Merged develop into fix/optimize-queries

This commit is contained in:
Christian Cueni 2024-07-25 05:29:27 +00:00
commit 85f0b680b5
31 changed files with 588 additions and 217 deletions

View File

@ -97,6 +97,7 @@ js-linting: &js-linting
default-steps: &default-steps default-steps: &default-steps
- parallel: - parallel:
- step: *e2e
- step: *e2e - step: *e2e
- step: *e2e - step: *e2e
- step: *python-tests - step: *python-tests

View File

@ -93,8 +93,12 @@ def main(app_name, image_name, environment_file):
"AWS_S3_SECRET_ACCESS_KEY": env.str("AWS_S3_SECRET_ACCESS_KEY", ""), "AWS_S3_SECRET_ACCESS_KEY": env.str("AWS_S3_SECRET_ACCESS_KEY", ""),
"AWS_S3_REGION_NAME": "eu-central-1", "AWS_S3_REGION_NAME": "eu-central-1",
"AWS_STORAGE_BUCKET_NAME": "myvbv-dev.iterativ.ch", "AWS_STORAGE_BUCKET_NAME": "myvbv-dev.iterativ.ch",
"DATATRANS_HMAC_KEY": env.str("DATATRANS_HMAC_KEY", ""), "DATATRANS_HMAC_KEY": env.str("PIPELINES_DATATRANS_HMAC_KEY", ""),
"DATATRANS_BASIC_AUTH_KEY": env.str("DATATRANS_BASIC_AUTH_KEY", ""), "DATATRANS_BASIC_AUTH_KEY": env.str(
"PIPELINES_DATATRANS_BASIC_AUTH_KEY", ""
),
"DATATRANS_API_ENDPOINT": "https://api.sandbox.datatrans.com",
"DATATRANS_PAY_URL": "https://pay.sandbox.datatrans.com",
"FILE_UPLOAD_STORAGE": "s3", "FILE_UPLOAD_STORAGE": "s3",
"IT_DJANGO_DEBUG": "false", "IT_DJANGO_DEBUG": "false",
"IT_SERVE_VUE": "false", "IT_SERVE_VUE": "false",

View File

@ -34,7 +34,7 @@ const orgAddress = computed({
for="company-name" for="company-name"
class="block text-sm font-medium leading-6 text-gray-900" class="block text-sm font-medium leading-6 text-gray-900"
> >
{{ $t("a.Name") }} {{ $t("a.Firmenname") }}
</label> </label>
<div class="mt-2"> <div class="mt-2">
<input <input

View File

@ -1,10 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue"; import { computed } from "vue";
import { useEntities } from "@/services/entities"; import { useEntities } from "@/services/entities";
import VueDatePicker from "@vuepic/vue-datepicker";
import "@vuepic/vue-datepicker/dist/main.css"; import "@vuepic/vue-datepicker/dist/main.css";
import { t } from "i18next"; import { t } from "i18next";
import { useUserStore } from "@/stores/user"; import ItDatePicker from "@/components/ui/ItDatePicker.vue";
const props = defineProps<{ const props = defineProps<{
modelValue: { modelValue: {
@ -25,20 +24,12 @@ const props = defineProps<{
const emit = defineEmits(["update:modelValue"]); const emit = defineEmits(["update:modelValue"]);
const { countries } = useEntities(); const { countries } = useEntities();
const userStore = useUserStore();
const paymentMethods = [ const paymentMethods = [
{ value: "credit_card", label: t("a.Debit-/Kreditkarte/Twint") }, { value: "credit_card", label: t("a.Debit-/Kreditkarte/Twint") },
{ value: "cembra_byjuno", label: t("a.Rechnung") }, { value: "cembra_byjuno", label: t("a.Rechnung") },
]; ];
// TODO: remove after cembra is ready for production
const appEnv = import.meta.env.VITE_APP_ENVIRONMENT || "local";
if (appEnv.startsWith("prod")) {
paymentMethods.splice(1, 1);
}
// END TODO
const address = computed({ const address = computed({
get() { get() {
return props.modelValue; return props.modelValue;
@ -234,40 +225,8 @@ const address = computed({
{{ $t("a.Geburtsdatum") }} {{ $t("a.Geburtsdatum") }}
</label> </label>
<div class="mt-2"> <div class="mt-2">
<VueDatePicker <ItDatePicker v-model="address.birth_date"></ItDatePicker>
v-model="address.birth_date"
format="dd.MM.yyyy"
no-today
model-type="yyyy-MM-dd"
name="birth-date"
max-date="2007-01-01"
prevent-min-max-navigation
required
:enable-time-picker="false"
text-input
placeholder="15.06.1982"
start-date="1982-01-01"
:locale="userStore.language"
:cancel-text="$t('a.Abbrechen')"
:select-text="$t('a.Auswählen')"
></VueDatePicker>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style>
/* Theming for date picker */
.dp__theme_light {
--dp-text-color: #585f63;
--dp-primary-color: #41b5fa;
--dp-border-color-focus: #3d6dcc;
}
:root {
--dp-font-family: "Buenos Aires" sans-serif;
--dp-border-radius: none;
--dp-font-size: 0.875rem;
--dp-cell-border-radius: none;
}
</style>

View File

@ -3,6 +3,8 @@ import { useEntities } from "@/services/entities";
import AvatarImage from "@/components/ui/AvatarImage.vue"; import AvatarImage from "@/components/ui/AvatarImage.vue";
import { ref } from "vue"; import { ref } from "vue";
import { type User, useUserStore } from "@/stores/user"; import { type User, useUserStore } from "@/stores/user";
import ItDatePicker from "@/components/ui/ItDatePicker.vue";
import { normalizeSwissPhoneNumber } from "@/utils/phone";
const emit = defineEmits(["cancel", "save"]); const emit = defineEmits(["cancel", "save"]);
@ -21,7 +23,12 @@ const formData = ref({
postal_code: user.postal_code, postal_code: user.postal_code,
city: user.city, city: user.city,
country_code: user.country?.country_code, country_code: user.country?.country_code,
phone_number: user.phone_number,
birth_date: user.birth_date,
organisation: user.organisation, organisation: user.organisation,
organisation_detail_name: user.organisation_detail_name,
organisation_street: user.organisation_street, organisation_street: user.organisation_street,
organisation_street_number: user.organisation_street_number, organisation_street_number: user.organisation_street_number,
organisation_postal_code: user.organisation_postal_code, organisation_postal_code: user.organisation_postal_code,
@ -31,7 +38,8 @@ const formData = ref({
}); });
async function save() { async function save() {
const { country_code, organisation_country_code, ...profileData } = formData.value; const { country_code, organisation_country_code, phone_number, ...profileData } =
formData.value;
const typedProfileData: Partial<User> = { ...profileData }; const typedProfileData: Partial<User> = { ...profileData };
typedProfileData.country = countries.value.find( typedProfileData.country = countries.value.find(
@ -41,6 +49,10 @@ async function save() {
(c) => c.country_code === organisation_country_code (c) => c.country_code === organisation_country_code
); );
if (phone_number) {
typedProfileData.phone_number = normalizeSwissPhoneNumber(phone_number);
}
await user.updateUserProfile(typedProfileData); await user.updateUserProfile(typedProfileData);
emit("save"); emit("save");
} }
@ -126,6 +138,28 @@ async function avatarUpload(e: Event) {
disabled disabled
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" 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"
/> />
<label for="phone" class="block pb-1.5 leading-6">
{{ $t("a.Telefonnummer") }}
</label>
<div>
<input
id="phone"
v-model="formData.phone_number"
type="text"
name="phone"
autocomplete="phone-number"
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"
/>
</div>
<label for="birth-date" class="block pb-1.5 leading-6">
{{ $t("a.Geburtsdatum") }}
</label>
<div class="mb-4 block w-full py-1.5 sm:max-w-sm sm:text-sm sm:leading-6">
<ItDatePicker v-model="formData.birth_date"></ItDatePicker>
</div>
<label class="block pb-1.5 leading-6"> <label class="block pb-1.5 leading-6">
{{ $t("a.Profilbild") }} {{ $t("a.Profilbild") }}
</label> </label>
@ -264,6 +298,22 @@ async function avatarUpload(e: Event) {
{{ $t("a.Firmenanschrift") }} {{ $t("a.Firmenanschrift") }}
</h4> </h4>
<div class="flex flex-col justify-start md:flex-row md:space-x-4">
<div class="w-full md:max-w-lg">
<label for="org-street-address" class="block pb-1.5 leading-6">
{{ $t("a.Firmenname") }}
</label>
<input
id="org-detail-name"
v-model="formData.organisation_detail_name"
type="text"
name="org-detail-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:text-sm sm:leading-6"
/>
</div>
</div>
<div class="flex flex-col justify-start md:flex-row md:space-x-4"> <div class="flex flex-col justify-start md:flex-row md:space-x-4">
<div class="w-full md:max-w-sm"> <div class="w-full md:max-w-sm">
<label for="org-street-address" class="block pb-1.5 leading-6"> <label for="org-street-address" class="block pb-1.5 leading-6">
@ -389,7 +439,7 @@ async function avatarUpload(e: Event) {
<button class="btn btn-secondary" @click="emit('cancel')"> <button class="btn btn-secondary" @click="emit('cancel')">
{{ $t("general.cancel") }} {{ $t("general.cancel") }}
</button> </button>
<button class="btn btn-primary" @click="save"> <button class="btn btn-primary" data-cy="saveButton" @click="save">
{{ $t("general.save") }} {{ $t("general.save") }}
</button> </button>
</div> </div>

View File

@ -3,6 +3,8 @@ import { useUserStore } from "@/stores/user";
import { computed } from "vue"; import { computed } from "vue";
import { useEntities } from "@/services/entities"; import { useEntities } from "@/services/entities";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import dayjs from "dayjs";
import { displaySwissPhoneNumber } from "@/utils/phone";
const { t } = useTranslation(); const { t } = useTranslation();
@ -10,21 +12,20 @@ const user = useUserStore();
const { organisations } = useEntities(); const { organisations } = useEntities();
const privateAddress = computed(() => { const privateAddress = computed(() => {
let addressText = `${user.street} ${user.street_number}`.trim(); const textParts = [];
if (user.postal_code || user.city) {
if (addressText.length) { if (user.street || user.street_number) {
addressText += ", "; textParts.push(`${user.street} ${user.street_number}`.trim());
}
addressText += `${user.postal_code} ${user.city}`;
}
if (user.country) {
if (addressText.length) {
addressText += ", ";
}
addressText += user.country.name;
} }
return addressText.trim(); if (user.postal_code || user.city) {
textParts.push(`${user.postal_code} ${user.city}`);
}
if (textParts.length && user.country) {
textParts.push(user.country.name);
}
return textParts;
}); });
const organisationName = computed(() => { const organisationName = computed(() => {
@ -36,22 +37,25 @@ const organisationName = computed(() => {
}); });
const orgAddress = computed(() => { const orgAddress = computed(() => {
let addressText = const textParts = [];
`${user.organisation_street} ${user.organisation_street_number}`.trim();
if (user.organisation_postal_code || user.organisation_city) { if (user.organisation_detail_name) {
if (addressText.length) { textParts.push(user.organisation_detail_name);
addressText += ", ";
}
addressText += `${user.organisation_postal_code} ${user.organisation_city}`;
}
if (user.organisation_country) {
if (addressText.length) {
addressText += ", ";
}
addressText += user.organisation_country.name;
} }
return addressText.trim(); if (user.organisation_street || user.organisation_street_number) {
textParts.push(
`${user.organisation_street} ${user.organisation_street_number}`.trim()
);
}
if (user.organisation_postal_code || user.organisation_city) {
textParts.push(`${user.organisation_postal_code} ${user.organisation_city}`);
}
if (textParts.length && user.organisation_country) {
textParts.push(user.organisation_country.name);
}
return textParts;
}); });
const invoiceAddress = computed(() => { const invoiceAddress = computed(() => {
@ -67,20 +71,45 @@ const invoiceAddress = computed(() => {
<h3 class="mb-2">{{ $t("a.Persönliche Informationen") }}</h3> <h3 class="mb-2">{{ $t("a.Persönliche Informationen") }}</h3>
<div class="sm:grid sm:grid-cols-3 sm:items-start sm:gap-8 sm:py-6"> <div class="sm:grid sm:grid-cols-3 sm:items-start sm:gap-8 sm:py-6">
<label class="block font-semibold leading-6">{{ $t("a.Vorname") }}</label> <label class="block font-semibold leading-6">{{ $t("a.Vorname") }}</label>
<div class="mb-3 sm:col-span-2 sm:mb-0">{{ user.first_name }}</div> <div class="mb-3 sm:col-span-2 sm:mb-0" data-cy="firstName">
{{ user.first_name }}
</div>
<label class="block font-semibold leading-6">{{ $t("a.Name") }}</label> <label class="block font-semibold leading-6">{{ $t("a.Name") }}</label>
<div class="mb-3 sm:col-span-2 sm:mb-0">{{ user.last_name }}</div> <div class="mb-3 sm:col-span-2 sm:mb-0" data-cy="lastName">
{{ user.last_name }}
</div>
<label class="block font-semibold leading-6"> <label class="block font-semibold leading-6">
{{ $t("a.E-Mail Adresse") }} {{ $t("a.E-Mail Adresse") }}
</label> </label>
<div class="mb-3 sm:col-span-2 sm:mb-0">{{ user.email }}</div> <div class="mb-3 sm:col-span-2 sm:mb-0" data-cy="email">{{ user.email }}</div>
<label class="block font-semibold leading-6">
{{ $t("a.Telefonnummer") }}
</label>
<div class="mb-3 sm:col-span-2 sm:mb-0" data-cy="phone">
<span v-if="user.phone_number">
{{ displaySwissPhoneNumber(user.phone_number) }}
</span>
<span v-else class="text-gray-800">{{ $t("a.Keine Angabe") }}</span>
</div>
<label class="block font-semibold leading-6">
{{ $t("a.Geburtsdatum") }}
</label>
<div class="mb-3 sm:col-span-2 sm:mb-0" data-cy="birthDate">
<span v-if="user.birth_date">
{{ dayjs(user.birth_date).format("DD.MM.YYYY") }}
</span>
<span v-else class="text-gray-800">{{ $t("a.Keine Angabe") }}</span>
</div>
<label class="block font-semibold leading-6"> <label class="block font-semibold leading-6">
{{ $t("a.Privatadresse") }} {{ $t("a.Privatadresse") }}
</label> </label>
<div class="mb-3 sm:col-span-2 sm:mb-0"> <div class="mb-3 sm:col-span-2 sm:mb-0" data-cy="privateAddress">
<template v-if="privateAddress"> <div v-if="privateAddress.length">
{{ privateAddress }} <span v-for="(line, index) in privateAddress" :key="index">
</template> {{ line }}
<br />
</span>
</div>
<span v-else class="text-gray-800">{{ $t("a.Keine Angabe") }}</span> <span v-else class="text-gray-800">{{ $t("a.Keine Angabe") }}</span>
</div> </div>
</div> </div>
@ -89,14 +118,19 @@ const invoiceAddress = computed(() => {
<h3 class="my-2">{{ $t("a.Geschäftsdaten") }}</h3> <h3 class="my-2">{{ $t("a.Geschäftsdaten") }}</h3>
<div class="sm:grid sm:grid-cols-3 sm:items-start sm:gap-8 sm:py-6"> <div class="sm:grid sm:grid-cols-3 sm:items-start sm:gap-8 sm:py-6">
<label class="block font-semibold leading-6">{{ $t("a.Unternehmen") }}</label> <label class="block font-semibold leading-6">{{ $t("a.Unternehmen") }}</label>
<div class="mb-3 sm:col-span-2 sm:mb-0">{{ organisationName }}</div> <div class="mb-3 sm:col-span-2 sm:mb-0" data-cy="organisationDetailName">
{{ organisationName }}
</div>
<label class="block font-semibold leading-6"> <label class="block font-semibold leading-6">
{{ $t("a.Firmenanschrift") }} {{ $t("a.Firmenanschrift") }}
</label> </label>
<div class="sm:col-span-2"> <div class="sm:col-span-2">
<template v-if="orgAddress"> <div v-if="orgAddress" data-cy="organisationAddress">
{{ orgAddress }} <span v-for="(line, index) in orgAddress" :key="index">
</template> {{ line }}
<br />
</span>
</div>
<span v-else class="text-gray-800">{{ $t("a.Keine Angabe") }}</span> <span v-else class="text-gray-800">{{ $t("a.Keine Angabe") }}</span>
</div> </div>
</div> </div>

View File

@ -0,0 +1,60 @@
<script setup lang="ts">
import { useUserStore } from "@/stores/user";
import VueDatePicker from "@vuepic/vue-datepicker";
import "@vuepic/vue-datepicker/dist/main.css";
import { defineProps, withDefaults, defineModel } from "vue";
const model = defineModel<string>();
export interface Props {
noToday?: boolean;
required?: boolean;
placeholder?: string;
startDate?: string;
maxDate?: string;
}
const props = withDefaults(defineProps<Props>(), {
required: false,
placeholder: "15.06.1982",
startDate: "1982-01-01",
maxDate: "2007-01-01",
});
const userStore = useUserStore();
</script>
<template>
<VueDatePicker
v-model="model"
format="dd.MM.yyyy"
model-type="yyyy-MM-dd"
name="date"
prevent-min-max-navigation
:required="props.required"
:enable-time-picker="false"
text-input
:no-today="props.noToday"
:max-date="props.maxDate"
:placeholder="props.placeholder"
:start-date="props.startDate"
:locale="userStore.language"
:cancel-text="$t('a.Abbrechen')"
:select-text="$t('a.Auswählen')"
></VueDatePicker>
</template>
<style>
.dp__theme_light {
--dp-text-color: #585f63;
--dp-primary-color: #41b5fa;
--dp-border-color-focus: #3d6dcc;
}
:root {
--dp-font-family: "Buenos Aires" sans-serif;
--dp-border-radius: none;
--dp-font-size: 0.875rem;
--dp-cell-border-radius: none;
}
</style>

19
client/src/consts.ts Normal file
View File

@ -0,0 +1,19 @@
// Course IDs
export const COURSE_TEST_ID = -1;
export const COURSE_UK = -3;
export const COURSE_VERSICHERUNGSVERMITTLERIN_ID = -4;
export const COURSE_UK_FR = -5;
export const COURSE_UK_TRAINING = -6;
export const COURSE_UK_TRAINING_FR = -7;
export const COURSE_UK_IT = -8;
export const COURSE_UK_TRAINING_IT = -9;
export const COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID = -10;
export const COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID = -11;
export const COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID = -12;
export const COURSE_MOTORFAHRZEUG_PRUEFUNG_ID = -13;
// Organization IDs
export const ORGANISATION_OTHER_BROKER_ID = 1;
export const ORGANISATION_OTHER_HEALTH_INSURANCE_ID = 2;
export const ORGANISATION_OTHER_PRIVATE_INSURANCE_ID = 3;
export const ORGANISATION_NO_COMPANY_ID = 31;

View File

@ -13,6 +13,7 @@ import VerticalBarChart from "@/components/ui/VerticalBarChart.vue";
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue"; import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
import logger from "loglevel"; import logger from "loglevel";
import { reactive, ref } from "vue"; import { reactive, ref } from "vue";
import "@vuepic/vue-datepicker/dist/main.css";
const state = reactive({ const state = reactive({
checkboxValue: true, checkboxValue: true,

View File

@ -51,10 +51,6 @@ const submissionDeadline = computed(() => {
?.submission_deadline; ?.submission_deadline;
}); });
// FIXME daniel: `useRouteQuery` from usevue is currently the reason that we have to
// fix the version of @vueuse/router and @vueuse/core to 10.1.0
// it fails with version 10.2.0. I have a reminder to check out the situation
// at the end of July 2023
// 0 = introduction, 1 - n = tasks, n+1 = submission // 0 = introduction, 1 - n = tasks, n+1 = submission
const stepIndex = useRouteQuery("step", "0", { transform: Number, mode: "push" }); const stepIndex = useRouteQuery("step", "0", { transform: Number, mode: "push" });

View File

@ -3,9 +3,9 @@ import WizardPage from "@/components/onboarding/WizardPage.vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { computed, ref, watch } from "vue"; import { computed, ref, watch } from "vue";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import { useRoute } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import { profileNextRoute } from "@/services/onboarding"; import { isOtherOrganisation, profileNextRoute } from "@/services/onboarding";
import { useEntities } from "@/services/entities"; import { useEntities } from "@/services/entities";
import AvatarImage from "@/components/ui/AvatarImage.vue"; import AvatarImage from "@/components/ui/AvatarImage.vue";
@ -13,9 +13,12 @@ const { t } = useTranslation();
const user = useUserStore(); const user = useUserStore();
const route = useRoute(); const route = useRoute();
const router = useRouter();
const { organisations } = useEntities(); const { organisations } = useEntities();
const organisationDetailName = ref<string>("");
const selectedOrganisation = ref({ const selectedOrganisation = ref({
id: 0, id: 0,
name: t("a.Auswählen"), name: t("a.Auswählen"),
@ -35,7 +38,11 @@ watch(
); );
const validOrganisation = computed(() => { const validOrganisation = computed(() => {
return selectedOrganisation.value.id !== 0; const organisationSelected = selectedOrganisation.value.id !== 0;
const organisationNameSet =
!isOtherOrganisation(selectedOrganisation.value.id) ||
!!organisationDetailName.value.trim();
return organisationSelected && organisationNameSet;
}); });
const avatarError = ref(false); const avatarError = ref(false);
@ -56,15 +63,21 @@ async function avatarUpload(e: Event) {
} }
} }
watch(selectedOrganisation, async (organisation) => { async function updateUserProfile() {
await user.updateUserProfile({ await user.updateUserProfile({
organisation: organisation.id, organisation: selectedOrganisation.value.id,
organisation_detail_name: organisationDetailName.value.trim(),
}); });
}); }
const nextRoute = computed(() => { const nextRoute = computed(() => {
return profileNextRoute(route.params.courseType); return profileNextRoute(route.params.courseType);
}); });
async function navigateNextRoute() {
await updateUserProfile();
await router.push({ name: nextRoute.value });
}
</script> </script>
<template> <template>
@ -86,6 +99,19 @@ const nextRoute = computed(() => {
<ItDropdownSelect v-model="selectedOrganisation" :items="organisations" /> <ItDropdownSelect v-model="selectedOrganisation" :items="organisations" />
<div v-if="isOtherOrganisation(selectedOrganisation.id)" class="my-8">
<label for="organisationDetailName" class="heading-3 block pb-1.5">
{{ $t("a.Firmenname") }}
</label>
<input
id="organisationDetailName"
v-model="organisationDetailName"
type="text"
name="phone"
class="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"
/>
</div>
<div class="mt-16 flex flex-col justify-between gap-12 lg:flex-row lg:gap-24"> <div class="mt-16 flex flex-col justify-between gap-12 lg:flex-row lg:gap-24">
<div> <div>
<h3 class="mb-3">{{ $t("a.Profilbild") }}</h3> <h3 class="mb-3">{{ $t("a.Profilbild") }}</h3>
@ -117,18 +143,16 @@ const nextRoute = computed(() => {
</template> </template>
<template #footer> <template #footer>
<router-link v-slot="{ navigate }" :to="{ name: nextRoute }" custom>
<button <button
:disabled="!validOrganisation" :disabled="!validOrganisation"
class="btn-blue flex items-center" class="btn-blue flex items-center"
role="link" role="link"
data-cy="continue-button" data-cy="continue-button"
@click="navigate" @click="navigateNextRoute"
> >
{{ $t("general.next") }} {{ $t("general.next") }}
<it-icon-arrow-right class="it-icon ml-2 h-6 w-6" /> <it-icon-arrow-right class="it-icon ml-2 h-6 w-6" />
</button> </button>
</router-link>
</template> </template>
</WizardPage> </WizardPage>
</template> </template>

View File

@ -13,6 +13,12 @@ import DatatransCembraDeviceFingerprint from "@/components/onboarding/DatatransC
import { getLocalSessionKey } from "@/statistics"; import { getLocalSessionKey } from "@/statistics";
import log from "loglevel"; import log from "loglevel";
import { normalizeSwissPhoneNumber, validatePhoneNumber } from "@/utils/phone"; import { normalizeSwissPhoneNumber, validatePhoneNumber } from "@/utils/phone";
import {
ORGANISATION_NO_COMPANY_ID,
ORGANISATION_OTHER_BROKER_ID,
ORGANISATION_OTHER_HEALTH_INSURANCE_ID,
ORGANISATION_OTHER_PRIVATE_INSURANCE_ID,
} from "@/consts";
const props = defineProps({ const props = defineProps({
courseType: { courseType: {
@ -31,11 +37,14 @@ const userOrganisationName = computed(() => {
} }
// Those IDs do not represent a company // Those IDs do not represent a company
// 1: Other broker if (
// 2: Other insurance [
// 3: Other private insurance ORGANISATION_OTHER_BROKER_ID,
// 31: No company relation ORGANISATION_OTHER_HEALTH_INSURANCE_ID,
if ([1, 2, 3, 31].includes(user.organisation)) { ORGANISATION_OTHER_PRIVATE_INSURANCE_ID,
ORGANISATION_NO_COMPANY_ID,
].includes(user.organisation)
) {
return null; return null;
} }

View File

@ -45,7 +45,11 @@ function startEditMode() {
<div class="flex flex-grow flex-col space-y-4 px-8 py-8 md:px-16"> <div class="flex flex-grow flex-col space-y-4 px-8 py-8 md:px-16">
<div v-if="!editMode" class="flex justify-end space-x-4"> <div v-if="!editMode" class="flex justify-end space-x-4">
<button class="btn btn-secondary" @click="startEditMode"> <button
class="btn btn-secondary"
data-cy="editProfileButton"
@click="startEditMode"
>
{{ $t("a.Profil bearbeiten") }} {{ $t("a.Profil bearbeiten") }}
</button> </button>
</div> </div>

View File

@ -1,4 +1,9 @@
import { isString, startsWith } from "lodash"; import { isString, startsWith } from "lodash";
import {
ORGANISATION_OTHER_BROKER_ID,
ORGANISATION_OTHER_HEALTH_INSURANCE_ID,
ORGANISATION_OTHER_PRIVATE_INSURANCE_ID,
} from "@/consts";
export function profileNextRoute(courseType: string | string[]) { export function profileNextRoute(courseType: string | string[]) {
if (courseType === "uk") { if (courseType === "uk") {
@ -10,3 +15,11 @@ export function profileNextRoute(courseType: string | string[]) {
} }
return ""; return "";
} }
export function isOtherOrganisation(orgId: number) {
return [
ORGANISATION_OTHER_BROKER_ID,
ORGANISATION_OTHER_HEALTH_INSURANCE_ID,
ORGANISATION_OTHER_PRIVATE_INSURANCE_ID,
].includes(orgId);
}

View File

@ -17,22 +17,31 @@ describe("checkout.cy.js", () => {
cy.get('[data-cy="account-confirm-title"]').should( cy.get('[data-cy="account-confirm-title"]').should(
"contain", "contain",
"Konto erstellen" "Konto erstellen",
); );
cy.get('[data-cy="continue-button"]').click(); cy.get('[data-cy="continue-button"]').click();
cy.get('[data-cy="account-profile-title"]').should( cy.get('[data-cy="account-profile-title"]').should(
"contain", "contain",
"Profil ergänzen" "Profil ergänzen",
); );
cy.get('[data-cy="dropdown-select"]').click(); cy.get('[data-cy="dropdown-select"]').click();
cy.get('[data-cy="dropdown-select-option-Baloise"]').click(); cy.get(
'[data-cy="dropdown-select-option-andere Krankenversicherer"]',
).click();
cy.get("#organisationDetailName").type("FdH GmbH");
cy.get('[data-cy="continue-button"]').click(); cy.get('[data-cy="continue-button"]').click();
cy.loadUser("id", TEST_USER_EMPTY_ID).then((u) => {
expect(u.organisation_detail_name).to.equal("FdH GmbH");
// 2 -> andere Krankenversicherer
expect(u.organisation).to.equal(2);
});
// Adressdaten ausfüllen // Adressdaten ausfüllen
cy.get('[data-cy="account-checkout-title"]').should( cy.get('[data-cy="account-checkout-title"]').should(
"contain", "contain",
"Lehrgang kaufen" "Lehrgang kaufen",
); );
cy.get("#street-address").type("Eggersmatt"); cy.get("#street-address").type("Eggersmatt");
cy.get("#street-number").type("32"); cy.get("#street-number").type("32");
@ -40,7 +49,7 @@ describe("checkout.cy.js", () => {
cy.get("#city").type("Zumholz"); cy.get("#city").type("Zumholz");
cy.get('[data-cy="add-company-address"]').click(); cy.get('[data-cy="add-company-address"]').click();
cy.get("#company-name").type("Iterativ GmbH"); // cy.get("#company-name").clear().type("Iterativ GmbH");
cy.get("#company-street-address").type("Brückfeldstrasse"); cy.get("#company-street-address").type("Brückfeldstrasse");
cy.get("#company-street-number").type("16"); cy.get("#company-street-number").type("16");
cy.get("#company-postal-code").type("3012"); cy.get("#company-postal-code").type("3012");
@ -60,7 +69,7 @@ describe("checkout.cy.js", () => {
expect(ci.country).to.equal("CH"); expect(ci.country).to.equal("CH");
expect(ci.invoice_address).to.equal("org"); expect(ci.invoice_address).to.equal("org");
expect(ci.organisation_detail_name).to.equal("Iterativ GmbH"); expect(ci.organisation_detail_name).to.equal("FdH GmbH");
expect(ci.organisation_street).to.equal("Brückfeldstrasse"); expect(ci.organisation_street).to.equal("Brückfeldstrasse");
expect(ci.organisation_street_number).to.equal("16"); expect(ci.organisation_street_number).to.equal("16");
expect(ci.organisation_postal_code).to.equal("3012"); expect(ci.organisation_postal_code).to.equal("3012");
@ -72,12 +81,32 @@ describe("checkout.cy.js", () => {
expect(ci.state).to.equal("ongoing"); expect(ci.state).to.equal("ongoing");
}); });
cy.loadUser("id", TEST_USER_EMPTY_ID).then((u) => {
expect(u.first_name).to.equal("Flasche");
expect(u.last_name).to.equal("Leer");
expect(u.street).to.equal("Eggersmatt");
expect(u.street_number).to.equal("32");
expect(u.postal_code).to.equal("1719");
expect(u.city).to.equal("Zumholz");
expect(u.country).to.equal("CH");
expect(u.invoice_address).to.equal("org");
expect(u.organisation_detail_name).to.equal("FdH GmbH");
expect(u.organisation_street).to.equal("Brückfeldstrasse");
expect(u.organisation_street_number).to.equal("16");
expect(u.organisation_postal_code).to.equal("3012");
expect(u.organisation_city).to.equal("Bern");
// 2 -> andere Krankenversicherer
expect(u.organisation).to.equal(2);
});
// pay // pay
cy.get('[data-cy="pay-button"]').click(); cy.get('[data-cy="pay-button"]').click();
cy.get('[data-cy="checkout-success-title"]').should( cy.get('[data-cy="checkout-success-title"]').should(
"contain", "contain",
"Gratuliere" "Gratuliere",
); );
// wait for payment callback // wait for payment callback
cy.wait(3000); cy.wait(3000);
@ -86,7 +115,7 @@ describe("checkout.cy.js", () => {
// back on dashboard page // back on dashboard page
cy.get('[data-cy="db-course-title"]').should( cy.get('[data-cy="db-course-title"]').should(
"contain", "contain",
"Versicherungsvermittler" "Versicherungsvermittler",
); );
cy.loadCheckoutInformation("user_id", TEST_USER_EMPTY_ID).then((ci) => { cy.loadCheckoutInformation("user_id", TEST_USER_EMPTY_ID).then((ci) => {
@ -102,13 +131,13 @@ describe("checkout.cy.js", () => {
cy.get('[data-cy="account-confirm-title"]').should( cy.get('[data-cy="account-confirm-title"]').should(
"contain", "contain",
"Konto erstellen" "Konto erstellen",
); );
cy.get('[data-cy="continue-button"]').click(); cy.get('[data-cy="continue-button"]').click();
cy.get('[data-cy="account-profile-title"]').should( cy.get('[data-cy="account-profile-title"]').should(
"contain", "contain",
"Profil ergänzen" "Profil ergänzen",
); );
cy.get('[data-cy="dropdown-select"]').click(); cy.get('[data-cy="dropdown-select"]').click();
cy.get('[data-cy="dropdown-select-option-Baloise"]').click(); cy.get('[data-cy="dropdown-select-option-Baloise"]').click();
@ -117,10 +146,10 @@ describe("checkout.cy.js", () => {
// Adressdaten ausfüllen // Adressdaten ausfüllen
cy.get('[data-cy="account-checkout-title"]').should( cy.get('[data-cy="account-checkout-title"]').should(
"contain", "contain",
"Lehrgang kaufen" "Lehrgang kaufen",
); );
cy.get('#paymentMethod').select('cembra_byjuno'); cy.get("#paymentMethod").select("cembra_byjuno");
cy.get("#street-address").type("Eggersmatt"); cy.get("#street-address").type("Eggersmatt");
cy.get("#street-number").type("32"); cy.get("#street-number").type("32");
@ -132,7 +161,8 @@ describe("checkout.cy.js", () => {
cy.get('[data-cy="continue-pay"]').click(); cy.get('[data-cy="continue-pay"]').click();
cy.loadExternalApiRequestLog("request_username", "empty@example.com").then((entry) => { cy.loadExternalApiRequestLog("request_username", "empty@example.com").then(
(entry) => {
// ends with "/v1/transactions"" // ends with "/v1/transactions""
expect(entry.api_url).to.contain("/v1/transactions"); expect(entry.api_url).to.contain("/v1/transactions");
expect(entry.request_username).to.contain("empty@example.com"); expect(entry.request_username).to.contain("empty@example.com");
@ -143,13 +173,17 @@ describe("checkout.cy.js", () => {
expect(entry.api_request_data.customer.firstName).to.equal("Flasche"); expect(entry.api_request_data.customer.firstName).to.equal("Flasche");
expect(entry.api_request_data.customer.lastName).to.equal("Leer"); expect(entry.api_request_data.customer.lastName).to.equal("Leer");
expect(entry.api_request_data.customer.street).to.equal("Eggersmatt 32"); expect(entry.api_request_data.customer.street).to.equal(
"Eggersmatt 32",
);
expect(entry.api_request_data.customer.zipCode).to.equal("1719"); expect(entry.api_request_data.customer.zipCode).to.equal("1719");
expect(entry.api_request_data.customer.city).to.equal("Zumholz"); expect(entry.api_request_data.customer.city).to.equal("Zumholz");
expect(entry.api_request_data.customer.country).to.equal("CH"); expect(entry.api_request_data.customer.country).to.equal("CH");
expect(entry.api_request_data.customer.type).to.equal("P"); expect(entry.api_request_data.customer.type).to.equal("P");
expect(entry.api_request_data.customer.phone).to.equal("+41792018586"); expect(entry.api_request_data.customer.phone).to.equal("+41792018586");
expect(entry.api_request_data.customer.birthDate).to.equal("1982-06-09"); expect(entry.api_request_data.customer.birthDate).to.equal(
"1982-06-09",
);
expect(entry.api_request_data.INT.repaymentType).to.equal(3); expect(entry.api_request_data.INT.repaymentType).to.equal(3);
expect(entry.api_request_data.INT.riskOwner).to.equal("IJ"); expect(entry.api_request_data.INT.riskOwner).to.equal("IJ");
@ -157,7 +191,8 @@ describe("checkout.cy.js", () => {
expect(entry.api_request_data.INT.deviceFingerprintId).to.not.be.empty; expect(entry.api_request_data.INT.deviceFingerprintId).to.not.be.empty;
expect(true).to.be.true; expect(true).to.be.true;
}); },
);
// check that results are stored on server // check that results are stored on server
cy.loadCheckoutInformation("user_id", TEST_USER_EMPTY_ID).then((ci) => { cy.loadCheckoutInformation("user_id", TEST_USER_EMPTY_ID).then((ci) => {
@ -183,5 +218,23 @@ describe("checkout.cy.js", () => {
expect(ci.ip_address).to.not.be.empty; expect(ci.ip_address).to.not.be.empty;
expect(ci.device_fingerprint_session_key).to.not.be.empty; expect(ci.device_fingerprint_session_key).to.not.be.empty;
}); });
cy.loadUser("id", TEST_USER_EMPTY_ID).then((u) => {
expect(u.first_name).to.equal("Flasche");
expect(u.last_name).to.equal("Leer");
expect(u.street).to.equal("Eggersmatt");
expect(u.street_number).to.equal("32");
expect(u.postal_code).to.equal("1719");
expect(u.city).to.equal("Zumholz");
expect(u.country).to.equal("CH");
expect(u.phone_number).to.equal("+41792018586");
expect(u.birth_date).to.equal("1982-06-09");
expect(u.invoice_address).to.equal("prv");
// 7 -> Baloise
expect(u.organisation).to.equal(7);
});
}); });
}); });

View File

@ -0,0 +1,85 @@
import { TEST_USER_EMPTY_ID } from "../../consts";
import { login } from "../helpers";
describe("personalProfile.cy.js", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset");
login("empty@example.com", "test");
cy.visit("/profile");
});
it("can edit all profile fields", () => {
cy.get('[data-cy="editProfileButton"]').click();
cy.get("#phone").type("079 201 85 86");
cy.get('[data-test="dp-input"]').type("09.06.1982{enter}");
cy.get("#street-address").type("Hafen");
cy.get("#street-number").type("123");
cy.get("#postal-code").type("DE-20095");
cy.get("#city").type("Hamburg");
cy.get("#country").select("DE");
// andere broker
cy.get("#organisation").select("1");
cy.get("#org-detail-name").type("Judihui GmbH");
cy.get("#org-street-address").type("Auf der Alm");
cy.get("#org-street-number").type("17");
cy.get("#org-postal-code").type("AT-6020");
cy.get("#org-city").type("Innsbruck");
cy.get("#org-country").select("AT");
cy.get("#invoice-address-organisation").click();
cy.get('[data-cy="saveButton"]').click();
// check displayed data
cy.get('[data-cy="firstName"]').should("contain", "Flasche");
cy.get('[data-cy="lastName"]').should("contain", "Leer");
cy.get('[data-cy="email"]').should("contain", "empty@example.com");
cy.get('[data-cy="phone"]').should("contain", "079 201 85 86");
cy.get('[data-cy="birthDate"]').should("contain", "09.06.1982");
cy.get('[data-cy="privateAddress"]').should("contain", "Hafen 123");
cy.get('[data-cy="privateAddress"]').should("contain", "DE-20095 Hamburg");
cy.get('[data-cy="privateAddress"]').should("contain", "Deutschland");
cy.get('[data-cy="organisationDetailName"]').should(
"contain",
"andere Broker",
);
cy.get('[data-cy="organisationAddress"]').should("contain", "Judihui GmbH");
cy.get('[data-cy="organisationAddress"]').should(
"contain",
"Auf der Alm 17",
);
cy.get('[data-cy="organisationAddress"]').should(
"contain",
"AT-6020 Innsbruck",
);
cy.get('[data-cy="organisationAddress"]').should("contain", "Österreich");
// check stored data
cy.loadUser("id", TEST_USER_EMPTY_ID).then((u) => {
expect(u.first_name).to.equal("Flasche");
expect(u.last_name).to.equal("Leer");
expect(u.street).to.equal("Hafen");
expect(u.street_number).to.equal("123");
expect(u.postal_code).to.equal("DE-20095");
expect(u.city).to.equal("Hamburg");
expect(u.country).to.equal("DE");
expect(u.invoice_address).to.equal("org");
expect(u.organisation_detail_name).to.equal("Judihui GmbH");
expect(u.organisation_street).to.equal("Auf der Alm");
expect(u.organisation_street_number).to.equal("17");
expect(u.organisation_postal_code).to.equal("AT-6020");
expect(u.organisation_city).to.equal("Innsbruck");
// 1 -> andere Broker
expect(u.organisation).to.equal(1);
});
});
});

View File

@ -178,6 +178,17 @@ Cypress.Commands.add("loadCheckoutInformation", (key, value) => {
); );
}); });
Cypress.Commands.add("loadUser", (key, value) => {
return loadObjectJson(
key,
value,
"vbv_lernwelt.core.models.User",
"vbv_lernwelt.core.serializers.CypressUserSerializer",
true
);
});
Cypress.Commands.add("makeSelfEvaluation", (answers) => { Cypress.Commands.add("makeSelfEvaluation", (answers) => {
for (let i = 0; i < answers.length; i++) { for (let i = 0; i < answers.length; i++) {
const answer = answers[i]; const answer = answers[i];

Binary file not shown.

Binary file not shown.

View File

@ -333,7 +333,6 @@ X_FRAME_OPTIONS = "DENY"
# EMAIL # EMAIL
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
# FIXME how to send emails?
EMAIL_BACKEND = env( EMAIL_BACKEND = env(
"DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend" "DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend"
) )
@ -683,10 +682,12 @@ if APP_ENVIRONMENT.startswith("prod"):
DATATRANS_PAY_URL = "https://pay.datatrans.com" DATATRANS_PAY_URL = "https://pay.datatrans.com"
else: else:
DATATRANS_API_ENDPOINT = env( DATATRANS_API_ENDPOINT = env(
"DATATRANS_API_ENDPOINT", default="https://api.sandbox.datatrans.com" "DATATRANS_API_ENDPOINT",
default="http://localhost:8000/server/fakeapi/datatrans/api",
) )
DATATRANS_PAY_URL = env( DATATRANS_PAY_URL = env(
"DATATRANS_PAY_URL", default="https://pay.sandbox.datatrans.com" "DATATRANS_PAY_URL",
default="http://localhost:8000/server/fakeapi/datatrans/pay",
) )
# default settings for python sftpserver test-server # default settings for python sftpserver test-server

View File

@ -28,6 +28,7 @@ TEST_MENTOR1_USER_ID = "d1f5f5a9-5b0a-4e1a-9e1a-9e9b5b5e1b1b"
TEST_STUDENT1_VV_USER_ID = "5ff59857-8de5-415e-a387-4449f9a0337a" TEST_STUDENT1_VV_USER_ID = "5ff59857-8de5-415e-a387-4449f9a0337a"
TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID = "7e8ebf0b-e6e2-4022-88f4-6e663ba0a9db" TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID = "7e8ebf0b-e6e2-4022-88f4-6e663ba0a9db"
TEST_USER_EMPTY_ID = "daecbabe-4ab9-4edf-a71f-4119042ccb02" TEST_USER_EMPTY_ID = "daecbabe-4ab9-4edf-a71f-4119042ccb02"
TEST_USER_DATATRANS_HANNA_ID = "6bec1a0d-f852-47aa-a4de-072df6e07ad1"
TEST_COURSE_SESSION_BERN_ID = -1 TEST_COURSE_SESSION_BERN_ID = -1
TEST_COURSE_SESSION_ZURICH_ID = -2 TEST_COURSE_SESSION_ZURICH_ID = -2

View File

@ -4,6 +4,7 @@ from django.contrib.auth.models import Group, Permission
from django.core.files import File from django.core.files import File
from environs import Env from environs import Env
from vbv_lernwelt.core.model_utils import add_countries
from vbv_lernwelt.media_files.models import UserImage from vbv_lernwelt.media_files.models import UserImage
env = Env() env = Env()
@ -20,6 +21,7 @@ from vbv_lernwelt.core.constants import (
TEST_SUPERVISOR1_USER_ID, TEST_SUPERVISOR1_USER_ID,
TEST_TRAINER1_USER_ID, TEST_TRAINER1_USER_ID,
TEST_TRAINER2_USER_ID, TEST_TRAINER2_USER_ID,
TEST_USER_DATATRANS_HANNA_ID,
TEST_USER_EMPTY_ID, TEST_USER_EMPTY_ID,
) )
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
@ -78,7 +80,30 @@ default_users = [
AVATAR_DIR = settings.APPS_DIR / "static" / "avatars" AVATAR_DIR = settings.APPS_DIR / "static" / "avatars"
def create_datatrans_hanna_user():
hanna, _ = User.objects.get_or_create(
id=TEST_USER_DATATRANS_HANNA_ID,
)
hanna.username = "datatrans.hanna.vbv@example.com"
hanna.email = "datatrans.hanna.vbv@example.com"
hanna.language = "de"
hanna.first_name = "Hanna"
hanna.last_name = "Vbv"
hanna.street = "Bahnstrasse"
hanna.street_number = "2"
hanna.postal_code = "8603"
hanna.city = "Schwerzenbach"
hanna.country_id = "CH"
hanna.birth_date = "1970-01-01"
hanna.phone_number = "+41792018586"
hanna.password = make_password("test")
hanna.save()
return hanna
def create_default_users(default_password="test", set_avatar=False): def create_default_users(default_password="test", set_avatar=False):
add_countries(small_set=True)
admin_group, created = Group.objects.get_or_create(name="admin_group") admin_group, created = Group.objects.get_or_create(name="admin_group")
_content_creator_group, _created = Group.objects.get_or_create( _content_creator_group, _created = Group.objects.get_or_create(
name="content_creator_grop" name="content_creator_grop"
@ -202,6 +227,8 @@ def create_default_users(default_password="test", set_avatar=False):
language="de", language="de",
) )
hanna = create_datatrans_hanna_user()
for user_data in default_users: for user_data in default_users:
_create_student_user(**user_data) _create_student_user(**user_data)

View File

@ -17,8 +17,10 @@ from vbv_lernwelt.core.constants import (
TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID, TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID,
TEST_STUDENT3_USER_ID, TEST_STUDENT3_USER_ID,
TEST_TRAINER1_USER_ID, TEST_TRAINER1_USER_ID,
TEST_USER_DATATRANS_HANNA_ID,
TEST_USER_EMPTY_ID, TEST_USER_EMPTY_ID,
) )
from vbv_lernwelt.core.create_default_users import create_datatrans_hanna_user
from vbv_lernwelt.core.models import Organisation, User from vbv_lernwelt.core.models import Organisation, User
from vbv_lernwelt.course.consts import ( from vbv_lernwelt.course.consts import (
COURSE_TEST_ID, COURSE_TEST_ID,
@ -159,6 +161,9 @@ def command(
password=make_password("test"), password=make_password("test"),
) )
User.objects.filter(id=TEST_USER_DATATRANS_HANNA_ID).delete()
create_datatrans_hanna_user()
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute("truncate core_securityrequestresponselog;") cursor.execute("truncate core_securityrequestresponselog;")
cursor.execute("truncate core_externalapirequestlog;") cursor.execute("truncate core_externalapirequestlog;")

View File

@ -114,8 +114,9 @@ class User(AbstractUser):
blank=True, blank=True,
) )
# fields gathered from cembra pay form
birth_date = models.DateField(null=True, blank=True) birth_date = models.DateField(null=True, blank=True)
# phone number should be stored in the format +41792018586 (not validated)
phone_number = models.CharField(max_length=255, blank=True, default="") phone_number = models.CharField(max_length=255, blank=True, default="")
# is only set by abacus invoice export code # is only set by abacus invoice export code

View File

@ -131,6 +131,12 @@ class UserSerializer(serializers.ModelSerializer):
return instance return instance
class CypressUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = "__all__"
class OrganisationSerializer(serializers.ModelSerializer): class OrganisationSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(source="organisation_id", read_only=True) id = serializers.IntegerField(source="organisation_id", read_only=True)
name = serializers.SerializerMethodField() name = serializers.SerializerMethodField()

View File

@ -28,3 +28,10 @@ UK_COURSE_IDS = [
COURSE_UK_TRAINING_FR, COURSE_UK_TRAINING_FR,
COURSE_UK_TRAINING_IT, COURSE_UK_TRAINING_IT,
] ]
# Organization IDs
ORGANISATION_OTHER_BROKER_ID = 1
ORGANISATION_OTHER_HEALTH_INSURANCE_ID = 2
ORGANISATION_OTHER_PRIVATE_INSURANCE_ID = 3
ORGANISATION_NO_COMPANY_ID = 31

View File

@ -765,7 +765,7 @@ def create_or_update_course_session_assignment(
csa.submission_deadline.save() csa.submission_deadline.save()
csa.evaluation_deadline.start = timezone.make_aware( csa.evaluation_deadline.start = timezone.make_aware(
start start
) + timezone.timedelta(days=45) ) + timezone.timedelta(days=60)
csa.evaluation_deadline.end = None csa.evaluation_deadline.end = None
csa.evaluation_deadline.save() csa.evaluation_deadline.save()
else: else:

View File

@ -1,13 +1,11 @@
import hashlib import hashlib
import hmac import hmac
import uuid
import requests import requests
import structlog import structlog
from django.conf import settings from django.conf import settings
from vbv_lernwelt.core.admin import User from vbv_lernwelt.core.admin import User
from vbv_lernwelt.shop.const import VV_PRODUCT_NUMBER
from vbv_lernwelt.shop.datatrans.datatrans_api_client import DatatransApiClient from vbv_lernwelt.shop.datatrans.datatrans_api_client import DatatransApiClient
from vbv_lernwelt.shop.models import CheckoutState from vbv_lernwelt.shop.models import CheckoutState
@ -73,6 +71,7 @@ def is_signature_valid(
def init_datatrans_transaction( def init_datatrans_transaction(
user: User, user: User,
refno: str,
amount_chf_centimes: int, amount_chf_centimes: int,
redirect_url_success: str, redirect_url_success: str,
redirect_url_error: str, redirect_url_error: str,
@ -91,8 +90,8 @@ def init_datatrans_transaction(
"amount": amount_chf_centimes, "amount": amount_chf_centimes,
"currency": "CHF", "currency": "CHF",
"language": user.language, "language": user.language,
"refno": str(uuid.uuid4()), "refno": str(refno),
"refno2": refno2, "refno2": str(refno2),
"webhook": {"url": webhook_url}, "webhook": {"url": webhook_url},
"redirect": { "redirect": {
"successUrl": redirect_url_success, "successUrl": redirect_url_success,
@ -101,9 +100,8 @@ def init_datatrans_transaction(
}, },
} }
# FIXME: test with working cembra byjuno invoice customer? if with_cembra_byjuno_invoice:
# if with_cembra_byjuno_invoice: payload["paymentMethods"] = ["INT"]
# payload["paymentMethods"] = ["INT"]
if datatrans_customer_data: if datatrans_customer_data:
payload["customer"] = datatrans_customer_data payload["customer"] = datatrans_customer_data
if datatrans_int_data: if datatrans_int_data:

View File

@ -71,9 +71,8 @@ class CheckoutAPITestCase(APITestCase):
# THEN # THEN
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertTrue(
f"https://pay.sandbox.datatrans.com/v1/start/1234567890", response.json()["next_step_url"].endswith("v1/start/1234567890")
response.json()["next_step_url"],
) )
ci = CheckoutInformation.objects.first() ci = CheckoutInformation.objects.first()
@ -154,9 +153,11 @@ class CheckoutAPITestCase(APITestCase):
) )
self.assertEqual( self.assertEqual(
0, 1,
CheckoutInformation.objects.count(), CheckoutInformation.objects.count(),
) )
ci = CheckoutInformation.objects.first()
self.assertEqual(ci.state, CheckoutState.FAILED)
def test_checkout_already_paid(self): def test_checkout_already_paid(self):
# GIVEN # GIVEN
@ -217,9 +218,8 @@ class CheckoutAPITestCase(APITestCase):
# THEN # THEN
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertTrue(
f"https://pay.sandbox.datatrans.com/v1/start/{transaction_id_next}", response.json()["next_step_url"].endswith(f"v1/start/{transaction_id_next}")
response.json()["next_step_url"],
) )
# check that we have two checkouts # check that we have two checkouts
@ -277,9 +277,8 @@ class CheckoutAPITestCase(APITestCase):
# THEN # THEN
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertTrue(
f"https://pay.sandbox.datatrans.com/v1/start/{transaction_id}", response.json()["next_step_url"].endswith(f"v1/start/{transaction_id}")
response.json()["next_step_url"],
) )
@patch("vbv_lernwelt.shop.views.init_datatrans_transaction") @patch("vbv_lernwelt.shop.views.init_datatrans_transaction")
@ -310,7 +309,6 @@ class CheckoutAPITestCase(APITestCase):
# THEN # THEN
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertTrue(
f"https://pay.sandbox.datatrans.com/v1/start/{transaction_id}", response.json()["next_step_url"].endswith(f"v1/start/{transaction_id}")
response.json()["next_step_url"],
) )

View File

@ -24,10 +24,8 @@ class DatatransServiceTest(TestCase):
@override_settings(DATATRANS_BASIC_AUTH_KEY="BASIC_AUTH_KEY") @override_settings(DATATRANS_BASIC_AUTH_KEY="BASIC_AUTH_KEY")
@patch("vbv_lernwelt.shop.services.requests.post") @patch("vbv_lernwelt.shop.services.requests.post")
@patch("vbv_lernwelt.shop.services.uuid.uuid4") def test_init_transaction_201(self, mock_post):
def test_init_transaction_201(self, mock_uuid, mock_post):
# GIVEN # GIVEN
mock_uuid.return_value = uuid.uuid4()
mock_post.return_value.status_code = 201 mock_post.return_value.status_code = 201
mock_post.return_value.json.return_value = { mock_post.return_value.json.return_value = {
"transactionId": 1234567890, "transactionId": 1234567890,
@ -38,6 +36,7 @@ class DatatransServiceTest(TestCase):
# WHEN # WHEN
transaction_id = init_datatrans_transaction( transaction_id = init_datatrans_transaction(
user=self.user, user=self.user,
refno="123321",
amount_chf_centimes=324_30, amount_chf_centimes=324_30,
redirect_url_success=f"{REDIRECT_URL}/success", redirect_url_success=f"{REDIRECT_URL}/success",
redirect_url_error=f"{REDIRECT_URL}/error", redirect_url_error=f"{REDIRECT_URL}/error",
@ -64,11 +63,12 @@ class DatatransServiceTest(TestCase):
with self.assertRaises(InitTransactionException): with self.assertRaises(InitTransactionException):
init_datatrans_transaction( init_datatrans_transaction(
user=self.user, user=self.user,
refno="123321",
amount_chf_centimes=324_30, amount_chf_centimes=324_30,
redirect_url_success=f"/success", redirect_url_success="/success",
redirect_url_error=f"/error", redirect_url_error="/error",
redirect_url_cancel=f"/cancel", redirect_url_cancel="/cancel",
webhook_url=f"/webhook", webhook_url="/webhook",
refno2="", refno2="",
) )
@ -80,7 +80,4 @@ class DatatransServiceTest(TestCase):
url = get_payment_url(transaction_id) url = get_payment_url(transaction_id)
# THEN # THEN
self.assertEqual( self.assertTrue(url.endswith(f"v1/start/{transaction_id}"))
url,
f"https://pay.sandbox.datatrans.com/v1/start/{transaction_id}",
)

View File

@ -93,7 +93,7 @@ def checkout_vv(request):
sku = request.data["product"] sku = request.data["product"]
base_redirect_url = request.data["redirect_url"] base_redirect_url = request.data["redirect_url"]
log.info(f"Checkout requested: sku", user_id=request.user.id, sku=sku) log.info("Checkout requested: sku", user_id=request.user.id, sku=sku)
try: try:
product = Product.objects.get(sku=sku) product = Product.objects.get(sku=sku)
@ -124,6 +124,38 @@ def checkout_vv(request):
disable_save="fakeapi" in settings.DATATRANS_API_ENDPOINT disable_save="fakeapi" in settings.DATATRANS_API_ENDPOINT
) )
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
if "birth_date" in address_data and address_data["birth_date"]:
address_data["birth_date"] = date.fromisoformat(address_data["birth_date"])
checkout_info = CheckoutInformation.objects.create(
user=request.user,
state=CheckoutState.ONGOING,
# product
product_sku=sku,
product_price=product.price,
product_name=product.name,
product_description=product.description,
email=email,
ip_address=ip_address,
cembra_byjuno_invoice=with_cembra_byjuno_invoice,
device_fingerprint_session_key=request.data.get(
"device_fingerprint_session_key", ""
),
# address
**request.data["address"],
)
checkout_info.set_increment_abacus_order_id()
refno2 = f"{request.user.abacus_debitor_number}_{VV_PRODUCT_NUMBER}" refno2 = f"{request.user.abacus_debitor_number}_{VV_PRODUCT_NUMBER}"
try: try:
@ -138,10 +170,10 @@ def checkout_vv(request):
"street": f'{request.data["address"]["street"]} {request.data["address"]["street_number"]}', "street": f'{request.data["address"]["street"]} {request.data["address"]["street_number"]}',
"city": request.data["address"]["city"], "city": request.data["address"]["city"],
"zipCode": request.data["address"]["postal_code"], "zipCode": request.data["address"]["postal_code"],
"country": request.data["address"]["country_code"], "country": request.data["address"]["country_id"],
"phone": request.data["address"]["phone_number"], "phone": request.data["address"]["phone_number"],
"email": email, "email": email,
"birthDate": request.data["address"]["birth_date"], "birthDate": str(request.data["address"]["birth_date"]),
"language": request.user.language, "language": request.user.language,
"ipAddress": ip_address, "ipAddress": ip_address,
"type": "P", "type": "P",
@ -154,6 +186,7 @@ def checkout_vv(request):
} }
transaction_id = init_datatrans_transaction( transaction_id = init_datatrans_transaction(
user=request.user, user=request.user,
refno=str(checkout_info.abacus_order_id),
amount_chf_centimes=product.price, amount_chf_centimes=product.price,
redirect_url_success=checkout_success_url( redirect_url_success=checkout_success_url(
base_url=base_redirect_url, product_sku=sku base_url=base_redirect_url, product_sku=sku
@ -170,6 +203,8 @@ def checkout_vv(request):
with_cembra_byjuno_invoice=with_cembra_byjuno_invoice, with_cembra_byjuno_invoice=with_cembra_byjuno_invoice,
) )
except InitTransactionException as e: except InitTransactionException as e:
checkout_info.state = CheckoutState.FAILED.value
checkout_info.save()
if not settings.DEBUG: if not settings.DEBUG:
log.error("Transaction initiation failed", exc_info=True, error=str(e)) log.error("Transaction initiation failed", exc_info=True, error=str(e))
capture_exception(e) capture_exception(e)
@ -180,36 +215,8 @@ def checkout_vv(request):
), ),
) )
address_data = request.data["address"] checkout_info.transaction_id = transaction_id
country_code = address_data.pop("country_code") checkout_info.save()
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
if "birth_date" in address_data and address_data["birth_date"]:
address_data["birth_date"] = date.fromisoformat(address_data["birth_date"])
checkout_info = CheckoutInformation.objects.create(
user=request.user,
state=CheckoutState.ONGOING,
transaction_id=transaction_id,
# product
product_sku=sku,
product_price=product.price,
product_name=product.name,
product_description=product.description,
email=email,
ip_address=ip_address,
cembra_byjuno_invoice=with_cembra_byjuno_invoice,
device_fingerprint_session_key=request.data.get(
"device_fingerprint_session_key", ""
),
# address
**request.data["address"],
)
return next_step_response(url=get_payment_url(transaction_id)) return next_step_response(url=get_payment_url(transaction_id))