chore: shop vv-it, vv-de, vv-fr

This commit is contained in:
Livio Bieri 2023-12-07 12:50:30 +01:00 committed by Christian Cueni
parent 561f9e3c96
commit b414e4cf93
16 changed files with 105 additions and 69 deletions

View File

@ -16,12 +16,7 @@ const user = useUserStore();
Der Lehrgang und die Prüfung zum Erwerb des Verbandszertifikats als
Versicherungs-vermittler/-in.
</p>
<router-link
:to="{ name: 'accountProfile', params: { courseType: 'vv' } }"
class="btn-primary"
>
Jetzt mit Lehrgang starten
</router-link>
<a href="/start/vv" class="btn-primary">Mehr erfahren</a>
</div>
<div>

View File

@ -3,12 +3,27 @@
<template>
<main class="bg-gray-200 lg:px-12 lg:py-12">
<div class="container-medium">
<h2 class="mb-8 text-blue-900">{{ $t("a.Versicherungsvermittler/-in") }}</h2>
<router-link
class="btn-primary"
:to="{ name: 'accountCreate', params: { courseType: 'vv' } }"
:to="{ name: 'accountCreate', params: { courseType: 'vv-de' } }"
>
{{ $t("a.Jetzt mit Lehrgang starten") }}
VV in Deutsch
</router-link>
</div>
<div class="container-medium">
<router-link
class="btn-primary"
:to="{ name: 'accountCreate', params: { courseType: 'vv-fr' } }"
>
VV in Französisch
</router-link>
</div>
<div class="container-medium">
<router-link
class="btn-primary"
:to="{ name: 'accountCreate', params: { courseType: 'vv-it' } }"
>
VV in Italienisch
</router-link>
</div>
</main>

View File

@ -3,8 +3,6 @@ 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 AvatarImage from "@/components/ui/AvatarImage.vue";
import { useFileUpload } from "@/composables";
import { useRoute } from "vue-router";
import { useTranslation } from "i18next-vue";
import { profileNextRoute, useEntities } from "@/services/onboarding";
@ -38,6 +36,7 @@ const validOrganisation = computed(() => {
return selectedOrganisation.value.id !== 0;
});
/* TODO: We do this later (not in the first release)
const {
upload: avatarUpload,
loading: avatarLoading,
@ -47,7 +46,7 @@ const {
watch(avatarFileInfo, (info) => {
console.log("fileInfo changed", info);
});
})*/
watch(selectedOrganisation, async (organisation) => {
await user.setUserOrganisation(organisation.id);
@ -75,6 +74,7 @@ const nextRoute = computed(() => {
<ItDropdownSelect v-model="selectedOrganisation" :items="organisations" />
<!--- TODO: We do this later (not in the first release)
<div class="mt-16 flex flex-col justify-between gap-12 lg:flex-row lg:gap-24">
<div>
<h3 class="mb-3">{{ $t("a.Profilbild") }}</h3>
@ -103,6 +103,7 @@ const nextRoute = computed(() => {
</div>
<AvatarImage :loading="avatarLoading" :image-url="user.avatar_url" />
</div>
-->
</template>
<template #footer>

View File

@ -28,7 +28,13 @@ const user = useUserStore();
<p class="mb-4 mt-12">{{ $t("a.Hast du schon ein Konto?") }}</p>
<a
:href="getLoginURL({ course: props.courseType, lang: user.language })"
:href="
getLoginURL({
course: props.courseType,
lang: user.language,
next: `/onboarding/${courseType}/account/confirm`,
})
"
class="btn-secondary"
>
{{ $t("a.Anmelden") }}

View File

@ -1,20 +0,0 @@
# Onboarding flow
## ÜK
### Guard
1. Call /me
- User logged in -> redirect to /
- Anonymous user -> Set to /onboarding/uk/step1
## VV
### Guard
1. Call /shop/vv/state
- User logged in
- If already bought -> redirect to /
- If not bought -> redirect to /onboarding/vv/step1?state="success"
- Anonymous user -> Set to /onboarding/vv/step1

View File

@ -4,6 +4,8 @@ import { computed } from "vue";
import mood_uk from "@/assets/images/mood_uk.jpg";
import mood_vv from "@/assets/images/mood_vv.jpg";
import { useTranslation } from "i18next-vue";
import { startsWith } from "lodash";
import { getVVCourseName } from "@/pages/onboarding/vv/helpers";
const props = defineProps({
courseType: {
@ -21,17 +23,17 @@ const courseData = computed(() => {
imageUrl: mood_uk,
};
}
if (props.courseType === "vv") {
if (startsWith(props.courseType, "vv")) {
return {
name: t("a.Versicherungsvermittler/-in"),
name: getVVCourseName(props.courseType),
imageUrl: mood_vv,
};
} else {
return {
name: "",
imageUrl: "",
};
}
return {
name: "",
imageUrl: "",
};
});
</script>

View File

@ -10,6 +10,7 @@ import { useEntities } from "@/services/onboarding";
import { useDebounceFn, useFetch } from "@vueuse/core";
import { useRoute } from "vue-router";
import { useTranslation } from "i18next-vue";
import { getVVCourseName } from "./helpers";
type BillingAddressType = {
first_name: string;
@ -27,6 +28,13 @@ type BillingAddressType = {
company_country: string;
};
const props = defineProps({
courseType: {
type: String,
required: true,
},
});
const user = useUserStore();
const route = useRoute();
const { organisations } = useEntities();
@ -193,7 +201,7 @@ const executePayment = () => {
itPost("/api/shop/vv/checkout/", {
redirect_url: fullHost,
address: address.value,
product: `vv-${user.language}`,
product: props.courseType,
}).then((res) => {
console.log("Going to next page", res.next_step_url);
window.location.href = res.next_step_url;
@ -210,7 +218,7 @@ const executePayment = () => {
:translation="$t('a.Der Preis für den Lehrgang {course} beträgt {price}.')"
>
<template #course>
<b>"{{ $t("a.Versicherungsvermittler/-in") }} ({{ user.languageName }})"</b>
<b>"{{ getVVCourseName(props.courseType) }}"</b>
</template>
<template #price>
<b class="whitespace-nowrap">300 CHF</b>

View File

@ -28,7 +28,7 @@ const user = useUserStore();
"
>
<template #course>
{{ $t("a.Versicherungsvermittler/-in") }} ({{ user.languageName }})
{{ $t("a.Versicherungsvermittler/-in") }}
</template>
</i18next>
</p>

View File

@ -0,0 +1,16 @@
import i18next from "i18next";
export const getVVCourseName = (courseType: string) => {
if (!["vv-de", "vv-it", "vv-fr"].includes(courseType)) {
return "";
}
const lookup: { [key: string]: string } = {
"vv-de": i18next.t("a.Deutsch"),
"vv-fr": i18next.t("a.Franzosisch"),
"vv-it": i18next.t("a.Italienisch"),
};
const vv = i18next.t("a.Versicherungsvermittler/-in");
return `${vv} (${lookup[courseType]})`;
};

View File

@ -280,6 +280,7 @@ const router = createRouter({
path: "checkout/address",
component: () => import("@/pages/onboarding/vv/CheckoutAddress.vue"),
name: "checkoutAddress",
props: true,
},
{
path: "checkout/complete",

View File

@ -1,5 +1,6 @@
import { itGetCached } from "@/fetchHelpers";
import { useUserStore } from "@/stores/user";
import { isString, startsWith } from "lodash";
import type { Ref } from "vue";
import { computed, ref } from "vue";
@ -7,7 +8,8 @@ export function profileNextRoute(courseType: string | string[]) {
if (courseType === "uk") {
return "setupComplete";
}
if (courseType === "vv") {
// vv- -> vv-de, vv-fr or vv-it
if (isString(courseType) && startsWith(courseType, "vv-")) {
return "checkoutAddress";
}
return "";

View File

@ -559,12 +559,10 @@ else:
ALLOWED_HOSTS = env.list(
"IT_DJANGO_ALLOWED_HOSTS",
# FIXME @livioso: Remove ngrok -> Datatrans Testing (hope I don't commit this)
default=[
"localhost",
"0.0.0.0",
"127.0.0.1",
"2375-2a02-21b4-9679-d800-8151-6008-2b6c-7a32.ngrok-free.app",
],
)

View File

@ -84,8 +84,8 @@ class CheckoutAPITestCase(APITestCase):
mock_init_transaction.assert_called_once_with(
user=self.user,
amount_chf_centimes=300_00,
redirect_url_success=f"{REDIRECT_URL}/onboarding/vv/checkout/complete",
redirect_url_error=f"{REDIRECT_URL}/onboarding/vv/checkout/address?error",
redirect_url_success=f"{REDIRECT_URL}/onboarding/{VV_DE_PRODUCT_SKU}/checkout/complete",
redirect_url_error=f"{REDIRECT_URL}/onboarding/{VV_DE_PRODUCT_SKU}/checkout/address?error",
redirect_url_cancel=f"{REDIRECT_URL}/",
webhook_url=f"{REDIRECT_URL}/api/shop/transaction/webhook/",
)
@ -111,7 +111,7 @@ class CheckoutAPITestCase(APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
expected = (
f"{REDIRECT_URL}/onboarding/vv/checkout/address?error&"
f"{REDIRECT_URL}/onboarding/{VV_DE_PRODUCT_SKU}/checkout/address?error&"
f"message=vv-de_product_sku_does_not_exist_needs_to_be_created_first"
)
@ -138,7 +138,7 @@ class CheckoutAPITestCase(APITestCase):
# THEN
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
f"{REDIRECT_URL}/onboarding/vv/checkout/address?error",
f"{REDIRECT_URL}/onboarding/{VV_DE_PRODUCT_SKU}/checkout/address?error",
response.json()["next_step_url"],
)

View File

@ -135,6 +135,7 @@ def checkout_vv(request):
return next_step_response(
url=checkout_error_url(
base_url=base_redirect_url,
product_sku=sku,
message=f"{sku}_product_sku_does_not_exist_needs_to_be_created_first",
),
)
@ -153,8 +154,12 @@ def checkout_vv(request):
transaction_id = init_transaction(
user=request.user,
amount_chf_centimes=product.price,
redirect_url_success=checkout_success_url(base_redirect_url),
redirect_url_error=checkout_error_url(base_redirect_url),
redirect_url_success=checkout_success_url(
base_url=base_redirect_url, product_sku=sku
),
redirect_url_error=checkout_error_url(
base_url=base_redirect_url, product_sku=sku
),
redirect_url_cancel=checkout_cancel_url(base_redirect_url),
webhook_url=webhook_url(base_redirect_url),
)
@ -164,6 +169,7 @@ def checkout_vv(request):
return next_step_response(
url=checkout_error_url(
base_url=base_redirect_url,
product_sku=sku,
),
)
@ -214,8 +220,10 @@ def webhook_url(base_url: str) -> str:
return f"{base_url}/api/shop/transaction/webhook/"
def checkout_error_url(base_url: str, message: str | None = None) -> str:
url = f"{base_url}/onboarding/vv/checkout/address?error"
def checkout_error_url(
base_url: str, product_sku: str, message: str | None = None
) -> str:
url = f"{base_url}/onboarding/{product_sku}/checkout/address?error"
if message:
url += f"&message={message}"
@ -227,5 +235,5 @@ def checkout_cancel_url(base_url: str) -> str:
return f"{base_url}/"
def checkout_success_url(base_url: str = "") -> str:
return f"{base_url}/onboarding/vv/checkout/complete"
def checkout_success_url(product_sku: str, base_url: str = "") -> str:
return f"{base_url}/onboarding/{product_sku}/checkout/complete"

View File

@ -79,7 +79,7 @@ class TestSignInAuthorizeSSO(TestCase):
}
state = base64.urlsafe_b64encode(
json.dumps({"course": "vv", "next": "/shall-be-ignored"}).encode()
json.dumps({"course": "vv-de", "next": "/shall-be-ignored"}).encode()
).decode()
# WHEN
@ -87,7 +87,7 @@ class TestSignInAuthorizeSSO(TestCase):
# THEN
self.assertEqual(302, response.status_code)
self.assertEqual("/onboarding/vv/account/create", response.url) # noqa
self.assertEqual("/onboarding/vv-de/account/create", response.url) # noqa
user = User.objects.get(email=email) # noqa
self.assertIsNotNone(user)
@ -206,7 +206,7 @@ class TestSignIn(TestCase):
@patch("vbv_lernwelt.sso.views.oauth")
def test_signin_with_course_param(self, mock_oauth):
# GIVEN
course_param = "vv"
course_param = "vv-de"
expected_state = {"course": course_param, "next": None}
expected_state_encoded = base64.urlsafe_b64encode(
@ -229,14 +229,14 @@ class TestSignIn(TestCase):
ANY,
"/sso/callback",
state=expected_state_encoded,
lang="de",
ui_locales="de",
)
@override_settings(OAUTH_SIGNIN_REDIRECT_URI="/sso/callback")
@patch("vbv_lernwelt.sso.views.oauth")
def test_signin_with_state_param(self, mock_oauth):
# GIVEN
state_param = "vv"
state_param = "vv-de"
expected_state = {"course": state_param, "next": None}
expected_state_encoded = base64.urlsafe_b64encode(
@ -259,7 +259,7 @@ class TestSignIn(TestCase):
ANY,
"/sso/callback",
state=expected_state_encoded,
lang="de",
ui_locales="de",
)
@override_settings(OAUTH_SIGNIN_REDIRECT_URI="/sso/callback")
@ -289,7 +289,7 @@ class TestSignIn(TestCase):
ANY,
"/sso/callback",
state=expected_state_encoded,
lang=ANY,
ui_locales=ANY,
)
@override_settings(OAUTH_SIGNIN_REDIRECT_URI="/sso/callback")
@ -314,7 +314,7 @@ class TestSignIn(TestCase):
ANY,
"/sso/callback",
state=ANY,
lang=language,
ui_locales=language,
)
@ -323,7 +323,7 @@ class TestSignUp(TestCase):
@patch("vbv_lernwelt.sso.views.oauth")
def test_signup_with_course_param(self, mock_oauth):
# GIVEN
course_param = "vv"
course_param = "vv-de"
language = "fr"
expected_state = {"course": course_param, "next": None}

View File

@ -43,14 +43,18 @@ def signin(request):
f"SSO Login (course={course_param}, next={next_param})",
sso_login_redirect_uri=redirect_uri,
)
return oauth.signin.authorize_redirect(
request, redirect_uri, state=state_encoded, lang=request.GET.get("lang", "de")
request,
redirect_uri,
state=state_encoded,
ui_locales=request.GET.get("lang", "de"),
)
def get_redirect_uri(user: User, course: str, next_url: str):
if course == "vv" and not has_course_session_user_vv(user):
return redirect("/onboarding/vv/account/create")
def get_redirect_uri(user: User, course: str | None, next_url: str | None):
if course and course.startswith("vv") and not has_course_session_user_vv(user):
return redirect(f"/onboarding/{course}/account/create")
elif (
course == "uk"
and not CourseSession.objects.filter(coursesessionuser__user=user).exists()