diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index 909e9bdb..0dcb4cc1 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -97,6 +97,7 @@ js-linting: &js-linting default-steps: &default-steps - parallel: + - step: *e2e - step: *e2e - step: *e2e - step: *python-tests diff --git a/caprover_create_app.py b/caprover_create_app.py index 24dba123..e137bcf1 100644 --- a/caprover_create_app.py +++ b/caprover_create_app.py @@ -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_REGION_NAME": "eu-central-1", "AWS_STORAGE_BUCKET_NAME": "myvbv-dev.iterativ.ch", - "DATATRANS_HMAC_KEY": env.str("DATATRANS_HMAC_KEY", ""), - "DATATRANS_BASIC_AUTH_KEY": env.str("DATATRANS_BASIC_AUTH_KEY", ""), + "DATATRANS_HMAC_KEY": env.str("PIPELINES_DATATRANS_HMAC_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", "IT_DJANGO_DEBUG": "false", "IT_SERVE_VUE": "false", diff --git a/client/src/components/onboarding/OrganisationAddress.vue b/client/src/components/onboarding/OrganisationAddress.vue index b8b59889..e56e33cd 100644 --- a/client/src/components/onboarding/OrganisationAddress.vue +++ b/client/src/components/onboarding/OrganisationAddress.vue @@ -34,7 +34,7 @@ const orgAddress = computed({ for="company-name" class="block text-sm font-medium leading-6 text-gray-900" > - {{ $t("a.Name") }} + {{ $t("a.Firmenname") }}
import { computed } from "vue"; import { useEntities } from "@/services/entities"; -import VueDatePicker from "@vuepic/vue-datepicker"; import "@vuepic/vue-datepicker/dist/main.css"; import { t } from "i18next"; -import { useUserStore } from "@/stores/user"; +import ItDatePicker from "@/components/ui/ItDatePicker.vue"; const props = defineProps<{ modelValue: { @@ -25,20 +24,12 @@ const props = defineProps<{ const emit = defineEmits(["update:modelValue"]); const { countries } = useEntities(); -const userStore = useUserStore(); const paymentMethods = [ { value: "credit_card", label: t("a.Debit-/Kreditkarte/Twint") }, { 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({ get() { return props.modelValue; @@ -234,40 +225,8 @@ const address = computed({ {{ $t("a.Geburtsdatum") }}
- +
- - diff --git a/client/src/components/personalProfile/ProfileEdit.vue b/client/src/components/personalProfile/ProfileEdit.vue index 77d0da8d..398e363a 100644 --- a/client/src/components/personalProfile/ProfileEdit.vue +++ b/client/src/components/personalProfile/ProfileEdit.vue @@ -3,6 +3,8 @@ import { useEntities } from "@/services/entities"; import AvatarImage from "@/components/ui/AvatarImage.vue"; import { ref } from "vue"; import { type User, useUserStore } from "@/stores/user"; +import ItDatePicker from "@/components/ui/ItDatePicker.vue"; +import { normalizeSwissPhoneNumber } from "@/utils/phone"; const emit = defineEmits(["cancel", "save"]); @@ -21,7 +23,12 @@ const formData = ref({ postal_code: user.postal_code, city: user.city, country_code: user.country?.country_code, + + phone_number: user.phone_number, + birth_date: user.birth_date, + organisation: user.organisation, + 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, @@ -31,7 +38,8 @@ const formData = ref({ }); 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 = { ...profileData }; typedProfileData.country = countries.value.find( @@ -41,6 +49,10 @@ async function save() { (c) => c.country_code === organisation_country_code ); + if (phone_number) { + typedProfileData.phone_number = normalizeSwissPhoneNumber(phone_number); + } + await user.updateUserProfile(typedProfileData); emit("save"); } @@ -126,6 +138,28 @@ async function avatarUpload(e: Event) { 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" /> + + +
+ +
+ + +
+ +
+ @@ -264,6 +298,22 @@ async function avatarUpload(e: Event) { {{ $t("a.Firmenanschrift") }} +
+
+ + + +
+
+
diff --git a/client/src/components/personalProfile/ProfileView.vue b/client/src/components/personalProfile/ProfileView.vue index b5d2d06e..ac2e494c 100644 --- a/client/src/components/personalProfile/ProfileView.vue +++ b/client/src/components/personalProfile/ProfileView.vue @@ -3,6 +3,8 @@ import { useUserStore } from "@/stores/user"; import { computed } from "vue"; import { useEntities } from "@/services/entities"; import { useTranslation } from "i18next-vue"; +import dayjs from "dayjs"; +import { displaySwissPhoneNumber } from "@/utils/phone"; const { t } = useTranslation(); @@ -10,21 +12,20 @@ const user = useUserStore(); const { organisations } = useEntities(); const privateAddress = computed(() => { - let addressText = `${user.street} ${user.street_number}`.trim(); - if (user.postal_code || user.city) { - if (addressText.length) { - addressText += ", "; - } - addressText += `${user.postal_code} ${user.city}`; - } - if (user.country) { - if (addressText.length) { - addressText += ", "; - } - addressText += user.country.name; + const textParts = []; + + if (user.street || user.street_number) { + textParts.push(`${user.street} ${user.street_number}`.trim()); } - 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(() => { @@ -36,22 +37,25 @@ const organisationName = computed(() => { }); const orgAddress = computed(() => { - let addressText = - `${user.organisation_street} ${user.organisation_street_number}`.trim(); - if (user.organisation_postal_code || user.organisation_city) { - if (addressText.length) { - addressText += ", "; - } - addressText += `${user.organisation_postal_code} ${user.organisation_city}`; - } - if (user.organisation_country) { - if (addressText.length) { - addressText += ", "; - } - addressText += user.organisation_country.name; + const textParts = []; + + if (user.organisation_detail_name) { + textParts.push(user.organisation_detail_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(() => { @@ -67,20 +71,45 @@ const invoiceAddress = computed(() => {

{{ $t("a.Persönliche Informationen") }}

-
{{ user.first_name }}
+
+ {{ user.first_name }} +
-
{{ user.last_name }}
+
+ {{ user.last_name }} +
-
{{ user.email }}
+
{{ user.email }}
+ +
+ + {{ displaySwissPhoneNumber(user.phone_number) }} + + {{ $t("a.Keine Angabe") }} +
+ +
+ + {{ dayjs(user.birth_date).format("DD.MM.YYYY") }} + + {{ $t("a.Keine Angabe") }} +
-
- +
+
+ + {{ line }} +
+
+
{{ $t("a.Keine Angabe") }}
@@ -89,14 +118,19 @@ const invoiceAddress = computed(() => {

{{ $t("a.Geschäftsdaten") }}

-
{{ organisationName }}
+
+ {{ organisationName }} +
- +
+ + {{ line }} +
+
+
{{ $t("a.Keine Angabe") }}
diff --git a/client/src/components/ui/ItDatePicker.vue b/client/src/components/ui/ItDatePicker.vue new file mode 100644 index 00000000..4c44f46d --- /dev/null +++ b/client/src/components/ui/ItDatePicker.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/client/src/consts.ts b/client/src/consts.ts new file mode 100644 index 00000000..656714c7 --- /dev/null +++ b/client/src/consts.ts @@ -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; diff --git a/client/src/pages/StyleGuidePage.vue b/client/src/pages/StyleGuidePage.vue index 1cb4a1e3..25ff5db5 100644 --- a/client/src/pages/StyleGuidePage.vue +++ b/client/src/pages/StyleGuidePage.vue @@ -13,6 +13,7 @@ import VerticalBarChart from "@/components/ui/VerticalBarChart.vue"; import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue"; import logger from "loglevel"; import { reactive, ref } from "vue"; +import "@vuepic/vue-datepicker/dist/main.css"; const state = reactive({ checkboxValue: true, diff --git a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue index d6d79516..48ae62a3 100644 --- a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue +++ b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue @@ -51,10 +51,6 @@ const submissionDeadline = computed(() => { ?.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 const stepIndex = useRouteQuery("step", "0", { transform: Number, mode: "push" }); diff --git a/client/src/pages/onboarding/AccountProfile.vue b/client/src/pages/onboarding/AccountProfile.vue index 4a0d780b..727dd96e 100644 --- a/client/src/pages/onboarding/AccountProfile.vue +++ b/client/src/pages/onboarding/AccountProfile.vue @@ -3,9 +3,9 @@ import WizardPage from "@/components/onboarding/WizardPage.vue"; import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; import { computed, ref, watch } from "vue"; import { useUserStore } from "@/stores/user"; -import { useRoute } from "vue-router"; +import { useRoute, useRouter } from "vue-router"; import { useTranslation } from "i18next-vue"; -import { profileNextRoute } from "@/services/onboarding"; +import { isOtherOrganisation, profileNextRoute } from "@/services/onboarding"; import { useEntities } from "@/services/entities"; import AvatarImage from "@/components/ui/AvatarImage.vue"; @@ -13,9 +13,12 @@ const { t } = useTranslation(); const user = useUserStore(); const route = useRoute(); +const router = useRouter(); const { organisations } = useEntities(); +const organisationDetailName = ref(""); + const selectedOrganisation = ref({ id: 0, name: t("a.Auswählen"), @@ -35,7 +38,11 @@ watch( ); 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); @@ -56,15 +63,21 @@ async function avatarUpload(e: Event) { } } -watch(selectedOrganisation, async (organisation) => { +async function updateUserProfile() { await user.updateUserProfile({ - organisation: organisation.id, + organisation: selectedOrganisation.value.id, + organisation_detail_name: organisationDetailName.value.trim(), }); -}); +} const nextRoute = computed(() => { return profileNextRoute(route.params.courseType); }); + +async function navigateNextRoute() { + await updateUserProfile(); + await router.push({ name: nextRoute.value }); +} diff --git a/client/src/pages/onboarding/vv/CheckoutAddress.vue b/client/src/pages/onboarding/vv/CheckoutAddress.vue index f8ff17ef..76e5a6ed 100644 --- a/client/src/pages/onboarding/vv/CheckoutAddress.vue +++ b/client/src/pages/onboarding/vv/CheckoutAddress.vue @@ -13,6 +13,12 @@ import DatatransCembraDeviceFingerprint from "@/components/onboarding/DatatransC import { getLocalSessionKey } from "@/statistics"; import log from "loglevel"; 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({ courseType: { @@ -31,11 +37,14 @@ const userOrganisationName = computed(() => { } // Those IDs do not represent a company - // 1: Other broker - // 2: Other insurance - // 3: Other private insurance - // 31: No company relation - if ([1, 2, 3, 31].includes(user.organisation)) { + if ( + [ + ORGANISATION_OTHER_BROKER_ID, + ORGANISATION_OTHER_HEALTH_INSURANCE_ID, + ORGANISATION_OTHER_PRIVATE_INSURANCE_ID, + ORGANISATION_NO_COMPANY_ID, + ].includes(user.organisation) + ) { return null; } diff --git a/client/src/pages/personalProfile/PersonalProfilePage.vue b/client/src/pages/personalProfile/PersonalProfilePage.vue index 01903add..99312b07 100644 --- a/client/src/pages/personalProfile/PersonalProfilePage.vue +++ b/client/src/pages/personalProfile/PersonalProfilePage.vue @@ -45,7 +45,11 @@ function startEditMode() {
-
diff --git a/client/src/services/onboarding.ts b/client/src/services/onboarding.ts index 24b59a48..9c59ef48 100644 --- a/client/src/services/onboarding.ts +++ b/client/src/services/onboarding.ts @@ -1,4 +1,9 @@ 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[]) { if (courseType === "uk") { @@ -10,3 +15,11 @@ export function profileNextRoute(courseType: string | string[]) { } return ""; } + +export function isOtherOrganisation(orgId: number) { + return [ + ORGANISATION_OTHER_BROKER_ID, + ORGANISATION_OTHER_HEALTH_INSURANCE_ID, + ORGANISATION_OTHER_PRIVATE_INSURANCE_ID, + ].includes(orgId); +} diff --git a/cypress/e2e/checkout-vv/checkout.cy.js b/cypress/e2e/checkout-vv/checkout.cy.js index 00f39ac7..e6007b44 100644 --- a/cypress/e2e/checkout-vv/checkout.cy.js +++ b/cypress/e2e/checkout-vv/checkout.cy.js @@ -17,22 +17,31 @@ describe("checkout.cy.js", () => { cy.get('[data-cy="account-confirm-title"]').should( "contain", - "Konto erstellen" + "Konto erstellen", ); cy.get('[data-cy="continue-button"]').click(); cy.get('[data-cy="account-profile-title"]').should( "contain", - "Profil ergänzen" + "Profil ergänzen", ); 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.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 cy.get('[data-cy="account-checkout-title"]').should( "contain", - "Lehrgang kaufen" + "Lehrgang kaufen", ); cy.get("#street-address").type("Eggersmatt"); cy.get("#street-number").type("32"); @@ -40,7 +49,7 @@ describe("checkout.cy.js", () => { cy.get("#city").type("Zumholz"); 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-number").type("16"); cy.get("#company-postal-code").type("3012"); @@ -60,7 +69,7 @@ describe("checkout.cy.js", () => { expect(ci.country).to.equal("CH"); 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_number).to.equal("16"); expect(ci.organisation_postal_code).to.equal("3012"); @@ -72,12 +81,32 @@ describe("checkout.cy.js", () => { 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 cy.get('[data-cy="pay-button"]').click(); cy.get('[data-cy="checkout-success-title"]').should( "contain", - "Gratuliere" + "Gratuliere", ); // wait for payment callback cy.wait(3000); @@ -86,7 +115,7 @@ describe("checkout.cy.js", () => { // back on dashboard page cy.get('[data-cy="db-course-title"]').should( "contain", - "Versicherungsvermittler" + "Versicherungsvermittler", ); 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( "contain", - "Konto erstellen" + "Konto erstellen", ); cy.get('[data-cy="continue-button"]').click(); cy.get('[data-cy="account-profile-title"]').should( "contain", - "Profil ergänzen" + "Profil ergänzen", ); cy.get('[data-cy="dropdown-select"]').click(); cy.get('[data-cy="dropdown-select-option-Baloise"]').click(); @@ -117,10 +146,10 @@ describe("checkout.cy.js", () => { // Adressdaten ausfüllen cy.get('[data-cy="account-checkout-title"]').should( "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-number").type("32"); @@ -132,32 +161,38 @@ describe("checkout.cy.js", () => { cy.get('[data-cy="continue-pay"]').click(); - cy.loadExternalApiRequestLog("request_username", "empty@example.com").then((entry) => { - // ends with "/v1/transactions"" - expect(entry.api_url).to.contain("/v1/transactions"); - expect(entry.request_username).to.contain("empty@example.com"); + cy.loadExternalApiRequestLog("request_username", "empty@example.com").then( + (entry) => { + // ends with "/v1/transactions"" + expect(entry.api_url).to.contain("/v1/transactions"); + expect(entry.request_username).to.contain("empty@example.com"); - expect(entry.api_request_data.amount).to.equal(32400); - expect(entry.api_request_data.currency).to.equal("CHF"); - expect(entry.api_request_data.autoSettle).to.equal(true); + expect(entry.api_request_data.amount).to.equal(32400); + expect(entry.api_request_data.currency).to.equal("CHF"); + expect(entry.api_request_data.autoSettle).to.equal(true); - 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.street).to.equal("Eggersmatt 32"); - 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.country).to.equal("CH"); - 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.birthDate).to.equal("1982-06-09"); + 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.street).to.equal( + "Eggersmatt 32", + ); + 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.country).to.equal("CH"); + 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.birthDate).to.equal( + "1982-06-09", + ); - 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.subtype).to.equal("INVOICE"); - expect(entry.api_request_data.INT.deviceFingerprintId).to.not.be.empty; + 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.subtype).to.equal("INVOICE"); + 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 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.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); + }); }); }); diff --git a/cypress/e2e/personalProfile/personalProfile.cy.js b/cypress/e2e/personalProfile/personalProfile.cy.js new file mode 100644 index 00000000..2608f53b --- /dev/null +++ b/cypress/e2e/personalProfile/personalProfile.cy.js @@ -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); + }); + }); +}); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 50dfdf30..3564a74c 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -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) => { for (let i = 0; i < answers.length; i++) { const answer = answers[i]; diff --git a/env_secrets/caprover_vbv-develop.env b/env_secrets/caprover_vbv-develop.env index c633f417..5432d3c8 100644 Binary files a/env_secrets/caprover_vbv-develop.env and b/env_secrets/caprover_vbv-develop.env differ diff --git a/env_secrets/local_daniel.env b/env_secrets/local_daniel.env index 7ae4f656..703bd5e9 100644 Binary files a/env_secrets/local_daniel.env and b/env_secrets/local_daniel.env differ diff --git a/server/config/settings/base.py b/server/config/settings/base.py index 8251e138..de7744aa 100644 --- a/server/config/settings/base.py +++ b/server/config/settings/base.py @@ -333,7 +333,6 @@ X_FRAME_OPTIONS = "DENY" # EMAIL # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend -# FIXME how to send emails? EMAIL_BACKEND = env( "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" else: 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", default="https://pay.sandbox.datatrans.com" + "DATATRANS_PAY_URL", + default="http://localhost:8000/server/fakeapi/datatrans/pay", ) # default settings for python sftpserver test-server diff --git a/server/vbv_lernwelt/core/constants.py b/server/vbv_lernwelt/core/constants.py index 75f24848..e36702b8 100644 --- a/server/vbv_lernwelt/core/constants.py +++ b/server/vbv_lernwelt/core/constants.py @@ -28,6 +28,7 @@ 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_USER_DATATRANS_HANNA_ID = "6bec1a0d-f852-47aa-a4de-072df6e07ad1" TEST_COURSE_SESSION_BERN_ID = -1 TEST_COURSE_SESSION_ZURICH_ID = -2 diff --git a/server/vbv_lernwelt/core/create_default_users.py b/server/vbv_lernwelt/core/create_default_users.py index 17acd036..beeaca53 100644 --- a/server/vbv_lernwelt/core/create_default_users.py +++ b/server/vbv_lernwelt/core/create_default_users.py @@ -4,6 +4,7 @@ from django.contrib.auth.models import Group, Permission from django.core.files import File from environs import Env +from vbv_lernwelt.core.model_utils import add_countries from vbv_lernwelt.media_files.models import UserImage env = Env() @@ -20,6 +21,7 @@ from vbv_lernwelt.core.constants import ( TEST_SUPERVISOR1_USER_ID, TEST_TRAINER1_USER_ID, TEST_TRAINER2_USER_ID, + TEST_USER_DATATRANS_HANNA_ID, TEST_USER_EMPTY_ID, ) from vbv_lernwelt.core.models import User @@ -78,7 +80,30 @@ default_users = [ 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): + add_countries(small_set=True) + admin_group, created = Group.objects.get_or_create(name="admin_group") _content_creator_group, _created = Group.objects.get_or_create( name="content_creator_grop" @@ -202,6 +227,8 @@ def create_default_users(default_password="test", set_avatar=False): language="de", ) + hanna = create_datatrans_hanna_user() + for user_data in default_users: _create_student_user(**user_data) diff --git a/server/vbv_lernwelt/core/management/commands/cypress_reset.py b/server/vbv_lernwelt/core/management/commands/cypress_reset.py index abfae404..1a02a343 100644 --- a/server/vbv_lernwelt/core/management/commands/cypress_reset.py +++ b/server/vbv_lernwelt/core/management/commands/cypress_reset.py @@ -17,8 +17,10 @@ from vbv_lernwelt.core.constants import ( TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID, TEST_STUDENT3_USER_ID, TEST_TRAINER1_USER_ID, + TEST_USER_DATATRANS_HANNA_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.course.consts import ( COURSE_TEST_ID, @@ -159,6 +161,9 @@ def command( password=make_password("test"), ) + User.objects.filter(id=TEST_USER_DATATRANS_HANNA_ID).delete() + create_datatrans_hanna_user() + cursor = connection.cursor() cursor.execute("truncate core_securityrequestresponselog;") cursor.execute("truncate core_externalapirequestlog;") diff --git a/server/vbv_lernwelt/core/models.py b/server/vbv_lernwelt/core/models.py index a1b09008..9d0ca936 100644 --- a/server/vbv_lernwelt/core/models.py +++ b/server/vbv_lernwelt/core/models.py @@ -114,8 +114,9 @@ class User(AbstractUser): blank=True, ) - # fields gathered from cembra pay form 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="") # is only set by abacus invoice export code diff --git a/server/vbv_lernwelt/core/serializers.py b/server/vbv_lernwelt/core/serializers.py index 3a656c71..31965017 100644 --- a/server/vbv_lernwelt/core/serializers.py +++ b/server/vbv_lernwelt/core/serializers.py @@ -131,6 +131,12 @@ class UserSerializer(serializers.ModelSerializer): return instance +class CypressUserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = "__all__" + + class OrganisationSerializer(serializers.ModelSerializer): id = serializers.IntegerField(source="organisation_id", read_only=True) name = serializers.SerializerMethodField() diff --git a/server/vbv_lernwelt/course/consts.py b/server/vbv_lernwelt/course/consts.py index a0dd7b27..a9949458 100644 --- a/server/vbv_lernwelt/course/consts.py +++ b/server/vbv_lernwelt/course/consts.py @@ -28,3 +28,10 @@ UK_COURSE_IDS = [ COURSE_UK_TRAINING_FR, 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 diff --git a/server/vbv_lernwelt/importer/services.py b/server/vbv_lernwelt/importer/services.py index f4594a24..20de544f 100644 --- a/server/vbv_lernwelt/importer/services.py +++ b/server/vbv_lernwelt/importer/services.py @@ -765,7 +765,7 @@ def create_or_update_course_session_assignment( csa.submission_deadline.save() csa.evaluation_deadline.start = timezone.make_aware( start - ) + timezone.timedelta(days=45) + ) + timezone.timedelta(days=60) csa.evaluation_deadline.end = None csa.evaluation_deadline.save() else: diff --git a/server/vbv_lernwelt/shop/services.py b/server/vbv_lernwelt/shop/services.py index f5998d38..0e7bd7b3 100644 --- a/server/vbv_lernwelt/shop/services.py +++ b/server/vbv_lernwelt/shop/services.py @@ -1,13 +1,11 @@ import hashlib import hmac -import uuid import requests import structlog from django.conf import settings 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.models import CheckoutState @@ -73,6 +71,7 @@ def is_signature_valid( def init_datatrans_transaction( user: User, + refno: str, amount_chf_centimes: int, redirect_url_success: str, redirect_url_error: str, @@ -91,8 +90,8 @@ def init_datatrans_transaction( "amount": amount_chf_centimes, "currency": "CHF", "language": user.language, - "refno": str(uuid.uuid4()), - "refno2": refno2, + "refno": str(refno), + "refno2": str(refno2), "webhook": {"url": webhook_url}, "redirect": { "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: - # payload["paymentMethods"] = ["INT"] + if with_cembra_byjuno_invoice: + payload["paymentMethods"] = ["INT"] if datatrans_customer_data: payload["customer"] = datatrans_customer_data if datatrans_int_data: diff --git a/server/vbv_lernwelt/shop/tests/test_checkout_api.py b/server/vbv_lernwelt/shop/tests/test_checkout_api.py index efbee5da..f5f46b37 100644 --- a/server/vbv_lernwelt/shop/tests/test_checkout_api.py +++ b/server/vbv_lernwelt/shop/tests/test_checkout_api.py @@ -71,9 +71,8 @@ class CheckoutAPITestCase(APITestCase): # THEN self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual( - f"https://pay.sandbox.datatrans.com/v1/start/1234567890", - response.json()["next_step_url"], + self.assertTrue( + response.json()["next_step_url"].endswith("v1/start/1234567890") ) ci = CheckoutInformation.objects.first() @@ -154,9 +153,11 @@ class CheckoutAPITestCase(APITestCase): ) self.assertEqual( - 0, + 1, CheckoutInformation.objects.count(), ) + ci = CheckoutInformation.objects.first() + self.assertEqual(ci.state, CheckoutState.FAILED) def test_checkout_already_paid(self): # GIVEN @@ -217,9 +218,8 @@ class CheckoutAPITestCase(APITestCase): # THEN self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual( - f"https://pay.sandbox.datatrans.com/v1/start/{transaction_id_next}", - response.json()["next_step_url"], + self.assertTrue( + response.json()["next_step_url"].endswith(f"v1/start/{transaction_id_next}") ) # check that we have two checkouts @@ -277,9 +277,8 @@ class CheckoutAPITestCase(APITestCase): # THEN self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual( - f"https://pay.sandbox.datatrans.com/v1/start/{transaction_id}", - response.json()["next_step_url"], + self.assertTrue( + response.json()["next_step_url"].endswith(f"v1/start/{transaction_id}") ) @patch("vbv_lernwelt.shop.views.init_datatrans_transaction") @@ -310,7 +309,6 @@ class CheckoutAPITestCase(APITestCase): # THEN self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual( - f"https://pay.sandbox.datatrans.com/v1/start/{transaction_id}", - response.json()["next_step_url"], + self.assertTrue( + response.json()["next_step_url"].endswith(f"v1/start/{transaction_id}") ) diff --git a/server/vbv_lernwelt/shop/tests/test_datatrans_service.py b/server/vbv_lernwelt/shop/tests/test_datatrans_service.py index 6dc907fe..7f22c8ee 100644 --- a/server/vbv_lernwelt/shop/tests/test_datatrans_service.py +++ b/server/vbv_lernwelt/shop/tests/test_datatrans_service.py @@ -24,10 +24,8 @@ class DatatransServiceTest(TestCase): @override_settings(DATATRANS_BASIC_AUTH_KEY="BASIC_AUTH_KEY") @patch("vbv_lernwelt.shop.services.requests.post") - @patch("vbv_lernwelt.shop.services.uuid.uuid4") - def test_init_transaction_201(self, mock_uuid, mock_post): + def test_init_transaction_201(self, mock_post): # GIVEN - mock_uuid.return_value = uuid.uuid4() mock_post.return_value.status_code = 201 mock_post.return_value.json.return_value = { "transactionId": 1234567890, @@ -38,6 +36,7 @@ class DatatransServiceTest(TestCase): # WHEN transaction_id = init_datatrans_transaction( user=self.user, + refno="123321", amount_chf_centimes=324_30, redirect_url_success=f"{REDIRECT_URL}/success", redirect_url_error=f"{REDIRECT_URL}/error", @@ -64,11 +63,12 @@ class DatatransServiceTest(TestCase): with self.assertRaises(InitTransactionException): init_datatrans_transaction( user=self.user, + refno="123321", amount_chf_centimes=324_30, - redirect_url_success=f"/success", - redirect_url_error=f"/error", - redirect_url_cancel=f"/cancel", - webhook_url=f"/webhook", + redirect_url_success="/success", + redirect_url_error="/error", + redirect_url_cancel="/cancel", + webhook_url="/webhook", refno2="", ) @@ -80,7 +80,4 @@ class DatatransServiceTest(TestCase): url = get_payment_url(transaction_id) # THEN - self.assertEqual( - url, - f"https://pay.sandbox.datatrans.com/v1/start/{transaction_id}", - ) + self.assertTrue(url.endswith(f"v1/start/{transaction_id}")) diff --git a/server/vbv_lernwelt/shop/views.py b/server/vbv_lernwelt/shop/views.py index 1ce904a3..a245520a 100644 --- a/server/vbv_lernwelt/shop/views.py +++ b/server/vbv_lernwelt/shop/views.py @@ -93,7 +93,7 @@ def checkout_vv(request): sku = request.data["product"] 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: product = Product.objects.get(sku=sku) @@ -124,6 +124,38 @@ def checkout_vv(request): 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}" try: @@ -138,10 +170,10 @@ def checkout_vv(request): "street": f'{request.data["address"]["street"]} {request.data["address"]["street_number"]}', "city": request.data["address"]["city"], "zipCode": request.data["address"]["postal_code"], - "country": request.data["address"]["country_code"], + "country": request.data["address"]["country_id"], "phone": request.data["address"]["phone_number"], "email": email, - "birthDate": request.data["address"]["birth_date"], + "birthDate": str(request.data["address"]["birth_date"]), "language": request.user.language, "ipAddress": ip_address, "type": "P", @@ -154,6 +186,7 @@ def checkout_vv(request): } transaction_id = init_datatrans_transaction( user=request.user, + refno=str(checkout_info.abacus_order_id), amount_chf_centimes=product.price, redirect_url_success=checkout_success_url( base_url=base_redirect_url, product_sku=sku @@ -170,6 +203,8 @@ def checkout_vv(request): with_cembra_byjuno_invoice=with_cembra_byjuno_invoice, ) except InitTransactionException as e: + checkout_info.state = CheckoutState.FAILED.value + checkout_info.save() if not settings.DEBUG: log.error("Transaction initiation failed", exc_info=True, error=str(e)) capture_exception(e) @@ -180,36 +215,8 @@ 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 - - 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"], - ) + checkout_info.transaction_id = transaction_id + checkout_info.save() return next_step_response(url=get_payment_url(transaction_id))