Add new fields for cembra pay
This commit is contained in:
parent
da5c6d07d2
commit
e776103eb7
|
|
@ -11,6 +11,9 @@ const props = defineProps<{
|
|||
postal_code: string;
|
||||
city: string;
|
||||
country_code: string;
|
||||
|
||||
phone_number: string;
|
||||
birth_date: string;
|
||||
};
|
||||
}>();
|
||||
|
||||
|
|
@ -163,5 +166,37 @@ const address = computed({
|
|||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-full">
|
||||
<label for="phone" class="block text-sm font-medium leading-6 text-gray-900">
|
||||
{{ $t("a.Telefonnummer") }}
|
||||
</label>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
id="phone"
|
||||
v-model="address.phone_number"
|
||||
type="text"
|
||||
name="phone"
|
||||
autocomplete="phone-number"
|
||||
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 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-full">
|
||||
<label for="birth-date" class="block text-sm font-medium leading-6 text-gray-900">
|
||||
{{ $t("a.Geburtsdatum") }}
|
||||
</label>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
id="birth-date"
|
||||
v-model="address.birth_date"
|
||||
type="text"
|
||||
name="phone"
|
||||
autocomplete="birth-date"
|
||||
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 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useEntities } from "@/services/entities";
|
|||
import { useRoute } from "vue-router";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import { getVVCourseName } from "./composables";
|
||||
import ItToggleSwitch from "@/components/ui/ItToggleSwitch.vue";
|
||||
|
||||
const props = defineProps({
|
||||
courseType: {
|
||||
|
|
@ -50,6 +51,10 @@ const address = ref({
|
|||
postal_code: user.postal_code,
|
||||
city: user.city,
|
||||
country_code: user.country?.country_code ?? "CH",
|
||||
|
||||
phone_number: user.phone_number,
|
||||
birth_date: user.birth_date,
|
||||
|
||||
organisation_detail_name: user.organisation_detail_name,
|
||||
organisation_street: user.organisation_street,
|
||||
organisation_street_number: user.organisation_street_number,
|
||||
|
|
@ -59,13 +64,15 @@ const address = ref({
|
|||
invoice_address: user.invoice_address ?? "prv",
|
||||
});
|
||||
|
||||
const useCompanyAddress = ref(user.invoice_address === "org");
|
||||
const withCompanyAddress = ref(user.invoice_address === "org");
|
||||
|
||||
const setUseCompanyAddress = (value: boolean) => {
|
||||
useCompanyAddress.value = value;
|
||||
const setWithCompanyAddress = (value: boolean) => {
|
||||
withCompanyAddress.value = value;
|
||||
address.value.invoice_address = value ? "org" : "prv";
|
||||
};
|
||||
|
||||
const withCembraInvoice = ref<boolean>(false);
|
||||
|
||||
type FormErrors = {
|
||||
personal: string[];
|
||||
company: string[];
|
||||
|
|
@ -110,7 +117,7 @@ function validateAddress() {
|
|||
formErrors.value.personal.push(t("a.Land"));
|
||||
}
|
||||
|
||||
if (useCompanyAddress.value) {
|
||||
if (withCompanyAddress.value) {
|
||||
if (!address.value.organisation_detail_name) {
|
||||
formErrors.value.company.push(t("a.Name"));
|
||||
}
|
||||
|
|
@ -170,6 +177,7 @@ const executePayment = async () => {
|
|||
redirect_url: fullHost,
|
||||
address: address.value,
|
||||
product: props.courseType,
|
||||
with_cembra_invoice: withCembraInvoice.value,
|
||||
}).then((res: any) => {
|
||||
console.log("Going to next page", res.next_step_url);
|
||||
window.location.href = res.next_step_url;
|
||||
|
|
@ -228,26 +236,34 @@ const executePayment = async () => {
|
|||
</p>
|
||||
<PersonalAddress v-model="address" />
|
||||
|
||||
<p v-if="formErrors.personal.length" class="mb-10 text-red-700">
|
||||
{{ $t("a.Bitte folgende Felder ausfüllen") }}:
|
||||
{{ formErrors.personal.join(", ") }}
|
||||
</p>
|
||||
<div class="flex flex-row items-center space-x-2 text-sm">
|
||||
<ItToggleSwitch
|
||||
data-cy="cembra-switch"
|
||||
:initially-switched-left="withCembraInvoice"
|
||||
@click="withCembraInvoice = !withCembraInvoice"
|
||||
></ItToggleSwitch>
|
||||
<span :class="{ 'text-gray-700': !withCembraInvoice }">
|
||||
"TODO: Zahlung auf Rechnung/Cembra"
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="!useCompanyAddress"
|
||||
class="underline"
|
||||
data-cy="add-company-address"
|
||||
@click="setUseCompanyAddress(true)"
|
||||
>
|
||||
<template v-if="userOrganisationName">
|
||||
{{
|
||||
$t("a.Rechnungsadresse von {organisation} hinzufügen", {
|
||||
organisation: userOrganisationName,
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
<template v-else>{{ $t("a.Rechnungsadresse hinzufügen") }}</template>
|
||||
</button>
|
||||
<div class="mt-4">
|
||||
<button
|
||||
v-if="!withCompanyAddress"
|
||||
class="underline"
|
||||
data-cy="add-company-address"
|
||||
@click="setWithCompanyAddress(true)"
|
||||
>
|
||||
<template v-if="userOrganisationName">
|
||||
{{
|
||||
$t("a.Rechnungsadresse von {organisation} hinzufügen", {
|
||||
organisation: userOrganisationName,
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
<template v-else>{{ $t("a.Rechnungsadresse hinzufügen") }}</template>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<transition
|
||||
enter-active-class="transition ease-out duration-100"
|
||||
|
|
@ -257,7 +273,7 @@ const executePayment = async () => {
|
|||
leave-from-class="transform opacity-100 scale-y-100"
|
||||
leave-to-class="transform opacity-0 scale-y-95"
|
||||
>
|
||||
<div v-if="useCompanyAddress">
|
||||
<div v-if="withCompanyAddress">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 v-if="userOrganisationName">
|
||||
{{
|
||||
|
|
@ -267,7 +283,7 @@ const executePayment = async () => {
|
|||
}}
|
||||
</h3>
|
||||
<h3 v-else>{{ $t("a.Rechnungsadresse") }}</h3>
|
||||
<button class="underline" @click="setUseCompanyAddress(false)">
|
||||
<button class="underline" @click="setWithCompanyAddress(false)">
|
||||
{{ $t("a.Entfernen") }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -288,7 +304,11 @@ const executePayment = async () => {
|
|||
<it-icon-arrow-left class="it-icon mr-2 h-6 w-6" />
|
||||
{{ $t("general.back") }}
|
||||
</router-link>
|
||||
<button class="btn-blue flex items-center" data-cy="continue-pay" @click="executePayment">
|
||||
<button
|
||||
class="btn-blue flex items-center"
|
||||
data-cy="continue-pay"
|
||||
@click="executePayment"
|
||||
>
|
||||
{{ $t("a.Mit Kreditkarte bezahlen") }}
|
||||
<it-icon-arrow-right class="it-icon ml-2 h-6 w-6" />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ export interface User {
|
|||
postal_code: string;
|
||||
city: string;
|
||||
country: Country | null;
|
||||
birth_date: string;
|
||||
phone_number: string;
|
||||
organisation_detail_name: string;
|
||||
organisation_street: string;
|
||||
organisation_street_number: string;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ describe("checkout.cy.js", () => {
|
|||
cy.visit("/");
|
||||
});
|
||||
|
||||
it("can register and buy Versicherungsvermittlerin with credit card", () => {
|
||||
it("can checkout and buy Versicherungsvermittlerin with credit card", () => {
|
||||
cy.get('[data-cy="start-vv"]').click();
|
||||
|
||||
// wähle "Deutsch"
|
||||
|
|
@ -66,7 +66,6 @@ describe("checkout.cy.js", () => {
|
|||
expect(ci.organisation_postal_code).to.equal("3012");
|
||||
expect(ci.organisation_city).to.equal("Bern");
|
||||
|
||||
expect(ci.product_name).to.equal("Versicherungsvermittler/-in VBV");
|
||||
expect(ci.product_name).to.equal("Versicherungsvermittler/-in VBV");
|
||||
expect(ci.product_price).to.equal(32400);
|
||||
|
||||
|
|
@ -94,4 +93,64 @@ describe("checkout.cy.js", () => {
|
|||
expect(ci.state).to.equal("paid");
|
||||
});
|
||||
});
|
||||
|
||||
it.only("can checkout and pay Versicherungsvermittlerin with Cembra invoice", () => {
|
||||
cy.get('[data-cy="start-vv"]').click();
|
||||
|
||||
// wähle "Deutsch"
|
||||
cy.get('[href="/onboarding/vv-de/account/create"]').click();
|
||||
|
||||
cy.get('[data-cy="account-confirm-title"]').should(
|
||||
"contain",
|
||||
"Konto erstellen"
|
||||
);
|
||||
cy.get('[data-cy="continue-button"]').click();
|
||||
|
||||
cy.get('[data-cy="account-profile-title"]').should(
|
||||
"contain",
|
||||
"Profil ergänzen"
|
||||
);
|
||||
cy.get('[data-cy="dropdown-select"]').click();
|
||||
cy.get('[data-cy="dropdown-select-option-Baloise"]').click();
|
||||
cy.get('[data-cy="continue-button"]').click();
|
||||
|
||||
// Adressdaten ausfüllen
|
||||
cy.get('[data-cy="account-checkout-title"]').should(
|
||||
"contain",
|
||||
"Lehrgang kaufen"
|
||||
);
|
||||
cy.get("#street-address").type("Eggersmatt");
|
||||
cy.get("#street-number").type("32");
|
||||
cy.get("#postal-code").type("1719");
|
||||
cy.get("#city").type("Zumholz");
|
||||
|
||||
cy.get("#phone").type("+41 79 201 85 86");
|
||||
cy.get("#birth-date").type("1982-06-09");
|
||||
|
||||
cy.get('[data-cy="cembra-switch"]').click();
|
||||
|
||||
cy.get('[data-cy="continue-pay"]').click();
|
||||
|
||||
// check that results are stored on server
|
||||
cy.loadCheckoutInformation("user_id", TEST_USER_EMPTY_ID).then((ci) => {
|
||||
expect(ci.first_name).to.equal("Flasche");
|
||||
expect(ci.last_name).to.equal("Leer");
|
||||
|
||||
expect(ci.street).to.equal("Eggersmatt");
|
||||
expect(ci.street_number).to.equal("32");
|
||||
expect(ci.postal_code).to.equal("1719");
|
||||
expect(ci.city).to.equal("Zumholz");
|
||||
expect(ci.country).to.equal("CH");
|
||||
expect(ci.phone_number).to.equal("+41 79 201 85 86");
|
||||
expect(ci.birth_date).to.equal("1982-06-09");
|
||||
expect(ci.cembra_invoice).to.be.true;
|
||||
|
||||
expect(ci.invoice_address).to.equal("prv");
|
||||
|
||||
expect(ci.product_name).to.equal("Versicherungsvermittler/-in VBV");
|
||||
expect(ci.product_price).to.equal(32400);
|
||||
|
||||
expect(ci.state).to.equal("ongoing");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
# pylint: disable=unused-wildcard-import,wildcard-import,wrong-import-position
|
||||
import os
|
||||
|
||||
from dotenv import dotenv_values
|
||||
|
||||
script_path = os.path.abspath(__file__)
|
||||
script_dir = os.path.dirname(script_path)
|
||||
|
||||
dev_env = dotenv_values(f"{script_dir}/../../../env_secrets/caprover_vbv-develop.env")
|
||||
|
||||
os.environ["IT_APP_ENVIRONMENT"] = "local"
|
||||
|
||||
os.environ["AWS_S3_SECRET_ACCESS_KEY"] = dev_env.get("AWS_S3_SECRET_ACCESS_KEY")
|
||||
os.environ["DATATRANS_BASIC_AUTH_KEY"] = dev_env.get("DATATRANS_BASIC_AUTH_KEY")
|
||||
os.environ["DATATRANS_HMAC_KEY"] = dev_env.get("DATATRANS_HMAC_KEY")
|
||||
|
||||
os.environ["IT_APP_ENVIRONMENT"] = "local"
|
||||
os.environ["AWS_S3_SECRET_ACCESS_KEY"] = os.environ.get(
|
||||
"AWS_S3_SECRET_ACCESS_KEY",
|
||||
"!!!default_for_quieting_cypress_within_pycharm!!!",
|
||||
)
|
||||
|
||||
from .test_cypress import * # noqa
|
||||
|
||||
DATATRANS_API_ENDPOINT = "https://api.sandbox.datatrans.com"
|
||||
DATATRANS_PAY_URL = "https://pay.sandbox.datatrans.com"
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 3.2.20 on 2024-06-20 14:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0009_country_refactor'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='birth_date',
|
||||
field=models.DateField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='phone_number',
|
||||
field=models.CharField(blank=True, default='', max_length=255),
|
||||
),
|
||||
]
|
||||
|
|
@ -111,6 +111,10 @@ class User(AbstractUser):
|
|||
blank=True,
|
||||
)
|
||||
|
||||
# fields gathered from cembra pay form
|
||||
birth_date = models.DateField(null=True, blank=True)
|
||||
phone_number = models.CharField(max_length=255, blank=True, default="")
|
||||
|
||||
# is only set by abacus invoice export code
|
||||
abacus_debitor_number = models.BigIntegerField(unique=True, null=True, blank=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ class UserSerializer(serializers.ModelSerializer):
|
|||
"postal_code",
|
||||
"city",
|
||||
"country",
|
||||
"phone_number",
|
||||
"birth_date",
|
||||
"organisation_detail_name",
|
||||
"organisation_street",
|
||||
"organisation_street_number",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import time
|
|||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
|
@ -16,12 +17,19 @@ from vbv_lernwelt.core.models import User
|
|||
|
||||
@csrf_exempt
|
||||
@django_view_authentication_exempt
|
||||
@transaction.non_atomic_requests
|
||||
def fake_datatrans_api_view(request, api_url=""):
|
||||
if api_url == "/v1/transactions" and request.method == "POST":
|
||||
data = json.loads(request.body.decode("utf-8"))
|
||||
user = User.objects.get(id=data["user_id"])
|
||||
|
||||
if "customer" in data and not user.abacus_debitor_number:
|
||||
user.set_increment_abacus_debitor_number()
|
||||
data["customer"]["id"] = user.abacus_debitor_number
|
||||
|
||||
user.additional_json_data["datatrans_transaction_payload"] = data
|
||||
user.save()
|
||||
|
||||
return JsonResponse({"transactionId": data["refno"]}, status=201)
|
||||
|
||||
return HttpResponse(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
# Generated by Django 3.2.20 on 2024-06-21 08:47
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shop', '0014_checkoutinformation_abacus_ssh_upload_done'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='checkoutinformation',
|
||||
name='birth_date',
|
||||
field=models.DateField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='checkoutinformation',
|
||||
name='cembra_invoice',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='checkoutinformation',
|
||||
name='device_fingerprint',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='checkoutinformation',
|
||||
name='email',
|
||||
field=models.CharField(blank=True, default='', max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='checkoutinformation',
|
||||
name='ip_address',
|
||||
field=models.CharField(blank=True, default='', max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='checkoutinformation',
|
||||
name='phone_number',
|
||||
field=models.CharField(blank=True, default='', max_length=255),
|
||||
),
|
||||
]
|
||||
|
|
@ -78,6 +78,14 @@ class CheckoutInformation(models.Model):
|
|||
blank=True,
|
||||
)
|
||||
|
||||
# optional fields for cembra payment
|
||||
cembra_invoice = models.BooleanField(default=False)
|
||||
birth_date = models.DateField(null=True, blank=True)
|
||||
phone_number = models.CharField(max_length=255, blank=True, default="")
|
||||
email = models.CharField(max_length=255, blank=True, default="")
|
||||
device_fingerprint = models.TextField(blank=True, default="")
|
||||
ip_address = models.CharField(max_length=255, blank=True, default="")
|
||||
|
||||
invoice_address = models.CharField(
|
||||
max_length=3, choices=INVOICE_ADDRESS_CHOICES, default="prv"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ def init_datatrans_transaction(
|
|||
redirect_url_error: str,
|
||||
redirect_url_cancel: str,
|
||||
webhook_url: str,
|
||||
datatrans_customer_data: dict = None,
|
||||
datatrans_int_data: dict = None,
|
||||
):
|
||||
payload = {
|
||||
# We use autoSettle=True, so that we don't have to settle the transaction:
|
||||
|
|
@ -69,6 +71,11 @@ def init_datatrans_transaction(
|
|||
},
|
||||
}
|
||||
|
||||
if datatrans_customer_data:
|
||||
payload["customer"] = datatrans_customer_data
|
||||
if datatrans_int_data:
|
||||
payload["INT"] = datatrans_int_data
|
||||
|
||||
# add testing configuration data
|
||||
if "fakeapi" in settings.DATATRANS_API_ENDPOINT:
|
||||
payload["user_id"] = str(user.id)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from datetime import date
|
||||
|
||||
import structlog
|
||||
from django.conf import settings
|
||||
from django.http import JsonResponse
|
||||
|
|
@ -106,7 +108,40 @@ def checkout_vv(request):
|
|||
if checkouts.filter(state=CheckoutState.PAID).exists():
|
||||
return next_step_response(url="/")
|
||||
|
||||
with_cembra_invoice = request.data.get("with_cembra_invoice", False)
|
||||
ip_address = request.META.get("REMOTE_ADDR")
|
||||
email = request.user.email
|
||||
|
||||
try:
|
||||
datatrans_customer_data = None
|
||||
datatrans_int_data = None
|
||||
if with_cembra_invoice:
|
||||
|
||||
if "fakeapi" not in settings.DATATRANS_API_ENDPOINT:
|
||||
request.user.set_increment_abacus_debitor_number()
|
||||
|
||||
# see https://api-reference.datatrans.ch/#tag/v1transactions as reference
|
||||
datatrans_customer_data = {
|
||||
"firstName": request.user.first_name,
|
||||
"lastName": request.user.last_name,
|
||||
"id": request.user.abacus_debitor_number,
|
||||
"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"],
|
||||
"phone": request.data["address"]["phone_number"],
|
||||
"email": email,
|
||||
"birthDate": request.data["address"]["birth_date"],
|
||||
"language": request.user.language,
|
||||
"ipAddress": ip_address,
|
||||
"type": "P",
|
||||
}
|
||||
datatrans_int_data = {
|
||||
"subtype": "INVOICE",
|
||||
"riskOwner": "IJ",
|
||||
"repaymentType": 3,
|
||||
"deviceFingerprintId": "TODO",
|
||||
}
|
||||
transaction_id = init_datatrans_transaction(
|
||||
user=request.user,
|
||||
amount_chf_centimes=product.price,
|
||||
|
|
@ -118,6 +153,8 @@ def checkout_vv(request):
|
|||
),
|
||||
redirect_url_cancel=checkout_cancel_url(base_redirect_url),
|
||||
webhook_url=webhook_url(base_redirect_url),
|
||||
datatrans_customer_data=datatrans_customer_data,
|
||||
datatrans_int_data=datatrans_int_data,
|
||||
)
|
||||
except InitTransactionException as e:
|
||||
if not settings.DEBUG:
|
||||
|
|
@ -138,6 +175,9 @@ def checkout_vv(request):
|
|||
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,
|
||||
|
|
@ -147,6 +187,9 @@ def checkout_vv(request):
|
|||
product_price=product.price,
|
||||
product_name=product.name,
|
||||
product_description=product.description,
|
||||
email=email,
|
||||
ip_address=ip_address,
|
||||
cembra_invoice=with_cembra_invoice,
|
||||
# address
|
||||
**request.data["address"],
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue