wip: walking skeleton for datatrans
This commit is contained in:
parent
4faa034609
commit
be160f5fa7
|
|
@ -1,11 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import WizardPage from "@/components/onboarding/WizardPage.vue";
|
import WizardPage from "@/components/onboarding/WizardPage.vue";
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import { ref, watch } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
import PersonalAddress from "@/components/onboarding/PersonalAddress.vue";
|
import PersonalAddress from "@/components/onboarding/PersonalAddress.vue";
|
||||||
import OrganisationAddress from "@/components/onboarding/OrganisationAddress.vue";
|
import OrganisationAddress from "@/components/onboarding/OrganisationAddress.vue";
|
||||||
import { itPut, useFetch } from "@/fetchHelpers";
|
import { itPost, itPut, useFetch } from "@/fetchHelpers";
|
||||||
|
import { useEntities } from "@/services/onboarding";
|
||||||
|
|
||||||
type BillingAddressType = {
|
type BillingAddressType = {
|
||||||
first_name: string;
|
first_name: string;
|
||||||
|
|
@ -23,6 +24,11 @@ type BillingAddressType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const user = useUserStore();
|
const user = useUserStore();
|
||||||
|
const { organisations } = useEntities();
|
||||||
|
|
||||||
|
const userOrganisationName = computed(
|
||||||
|
() => organisations.value?.find((c) => c.id === user.organisation)?.name
|
||||||
|
);
|
||||||
|
|
||||||
const address = ref({
|
const address = ref({
|
||||||
firstName: user.first_name,
|
firstName: user.first_name,
|
||||||
|
|
@ -37,6 +43,7 @@ const address = ref({
|
||||||
const useCompanyAddress = ref(false);
|
const useCompanyAddress = ref(false);
|
||||||
|
|
||||||
const orgAddress = ref({
|
const orgAddress = ref({
|
||||||
|
name: "",
|
||||||
street: "",
|
street: "",
|
||||||
streetNumber: "",
|
streetNumber: "",
|
||||||
postalCode: "",
|
postalCode: "",
|
||||||
|
|
@ -59,6 +66,7 @@ watch(billingAddressData, (newVal) => {
|
||||||
country: newVal.country || "",
|
country: newVal.country || "",
|
||||||
};
|
};
|
||||||
orgAddress.value = {
|
orgAddress.value = {
|
||||||
|
name: userOrganisationName.value || "",
|
||||||
street: newVal.company_street || "",
|
street: newVal.company_street || "",
|
||||||
streetNumber: newVal.company_street_number || "",
|
streetNumber: newVal.company_street_number || "",
|
||||||
postalCode: newVal.company_postal_code || "",
|
postalCode: newVal.company_postal_code || "",
|
||||||
|
|
@ -69,7 +77,9 @@ watch(billingAddressData, (newVal) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const executePayment = () => {
|
const executePayment = () => {
|
||||||
itPut("/api/shop/billing-address/update", {
|
// FIXME do this as user types, not on submit -> the is the whole point of this ;)
|
||||||
|
// FIXME same address and orgaddress too
|
||||||
|
itPut("/api/shop/billing-address/update/", {
|
||||||
first_name: address.value.firstName,
|
first_name: address.value.firstName,
|
||||||
last_name: address.value.lastName,
|
last_name: address.value.lastName,
|
||||||
street: address.value.street,
|
street: address.value.street,
|
||||||
|
|
@ -83,6 +93,22 @@ const executePayment = () => {
|
||||||
company_city: orgAddress.value.city,
|
company_city: orgAddress.value.city,
|
||||||
company_country: orgAddress.value.country,
|
company_country: orgAddress.value.country,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Where the payment page will redirect to after the payment is done:
|
||||||
|
// The reason why this is here is convenience: We could also do this in the backend
|
||||||
|
// then we'd need to configure this for all environments (including Caprover).
|
||||||
|
// /server/transactions/redirect?... will just redirect to the frontend to the right page
|
||||||
|
// anyway, so it seems fine to do it here.
|
||||||
|
const fullHost = `${window.location.protocol}//${window.location.host}`;
|
||||||
|
|
||||||
|
itPost("/api/shop/vv/checkout/", {
|
||||||
|
redirect_url: fullHost,
|
||||||
|
organisation_address: orgAddress.value,
|
||||||
|
address: address.value,
|
||||||
|
}).then((res) => {
|
||||||
|
console.log("Going to next page", res.next_step_url);
|
||||||
|
window.location.href = res.next_step_url;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -110,7 +136,7 @@ const executePayment = () => {
|
||||||
class="underline"
|
class="underline"
|
||||||
@click="useCompanyAddress = true"
|
@click="useCompanyAddress = true"
|
||||||
>
|
>
|
||||||
Rechnungsadresse von Axa hinzufügen
|
{{ `Rechnungsadresse von ${userOrganisationName} hinzufügen` }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<transition
|
<transition
|
||||||
|
|
@ -123,7 +149,7 @@ const executePayment = () => {
|
||||||
>
|
>
|
||||||
<div v-if="useCompanyAddress">
|
<div v-if="useCompanyAddress">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h3>Rechnungsaddresse von Axa</h3>
|
<h3>{{ `Rechnungsaddresse von ${userOrganisationName}` }}</h3>
|
||||||
<button class="underline" @click="useCompanyAddress = false">
|
<button class="underline" @click="useCompanyAddress = false">
|
||||||
Entfernen
|
Entfernen
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ from django.views import defaults as default_views
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django_ratelimit.exceptions import Ratelimited
|
from django_ratelimit.exceptions import Ratelimited
|
||||||
from graphene_django.views import GraphQLView
|
from graphene_django.views import GraphQLView
|
||||||
|
from wagtail import urls as wagtail_urls
|
||||||
|
from wagtail.admin import urls as wagtailadmin_urls
|
||||||
|
from wagtail.documents import urls as wagtaildocs_urls
|
||||||
|
|
||||||
from vbv_lernwelt.api.directory import list_entities
|
from vbv_lernwelt.api.directory import list_entities
|
||||||
from vbv_lernwelt.api.user import me_user_view
|
from vbv_lernwelt.api.user import me_user_view
|
||||||
|
|
@ -55,10 +58,12 @@ from vbv_lernwelt.importer.views import (
|
||||||
t2l_sync,
|
t2l_sync,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.notify.views import email_notification_settings
|
from vbv_lernwelt.notify.views import email_notification_settings
|
||||||
from vbv_lernwelt.shop.views import get_billing_address, update_billing_address
|
from vbv_lernwelt.shop.views import (
|
||||||
from wagtail import urls as wagtail_urls
|
get_billing_address,
|
||||||
from wagtail.admin import urls as wagtailadmin_urls
|
update_billing_address,
|
||||||
from wagtail.documents import urls as wagtaildocs_urls
|
checkout_vv,
|
||||||
|
transaction_webhook,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SignedIntConverter(IntConverter):
|
class SignedIntConverter(IntConverter):
|
||||||
|
|
@ -174,7 +179,9 @@ urlpatterns = [
|
||||||
|
|
||||||
# shop
|
# shop
|
||||||
path(r'api/shop/billing-address/', get_billing_address, name='get-billing-address'),
|
path(r'api/shop/billing-address/', get_billing_address, name='get-billing-address'),
|
||||||
path(r'api/shop/billing-address/update', update_billing_address, name='update-billing-address'),
|
path(r'api/shop/billing-address/update/', update_billing_address, name='update-billing-address'),
|
||||||
|
path(r'api/shop/vv/checkout/', checkout_vv, name='checkout-vv'),
|
||||||
|
path(r'api/shop/transaction/webhook/', django_view_authentication_exempt(transaction_webhook), name='shop-transaction-webhook'),
|
||||||
|
|
||||||
# importer
|
# importer
|
||||||
path(
|
path(
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,41 @@
|
||||||
|
import structlog
|
||||||
|
from django.http import JsonResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.decorators import api_view, permission_classes
|
from rest_framework.decorators import api_view, permission_classes
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from vbv_lernwelt.shop.models import BillingAddress
|
from vbv_lernwelt.shop.models import (
|
||||||
|
BillingAddress,
|
||||||
|
Product,
|
||||||
|
CheckoutInformation,
|
||||||
|
VV_PRODUCT_SKU,
|
||||||
|
CheckoutState,
|
||||||
|
)
|
||||||
from vbv_lernwelt.shop.serializers import BillingAddressSerializer
|
from vbv_lernwelt.shop.serializers import BillingAddressSerializer
|
||||||
|
from vbv_lernwelt.shop.services import DataTransPaymentProvider
|
||||||
|
|
||||||
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
URL_CHECKOUT_COMPLETE = "/onboarding/vv/checkout/complete"
|
||||||
|
URL_CHECKOUT_ADDRESS = "/onboarding/vv/checkout/address"
|
||||||
|
|
||||||
|
|
||||||
|
def checkout_error_url(base_url):
|
||||||
|
return f"{base_url}/onboarding/vv/checkout/address?error=true"
|
||||||
|
|
||||||
|
|
||||||
|
def checkout_cancel_url(base_url):
|
||||||
|
return f"{base_url}/"
|
||||||
|
|
||||||
|
|
||||||
|
def checkout_success_url(base_url):
|
||||||
|
return f"{base_url}/onboarding/vv/checkout/complete"
|
||||||
|
|
||||||
|
|
||||||
|
def webhook_url(base_url):
|
||||||
|
return f"{base_url}/api/shop/transaction/webhook"
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
|
|
@ -34,3 +64,125 @@ def update_billing_address(request):
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(["POST"])
|
||||||
|
def transaction_webhook(request):
|
||||||
|
"""Webhook endpoint for Datatrans to notify about transaction state changes."""
|
||||||
|
logger.info("Transaction Webhook Received", body=request.body)
|
||||||
|
|
||||||
|
datatrans = DataTransPaymentProvider()
|
||||||
|
|
||||||
|
# FIXME verify signature, not working yet
|
||||||
|
# if not datatrans.is_webhook_request_signature_valid(
|
||||||
|
# request=request,
|
||||||
|
# ):
|
||||||
|
# logger.error("Invalid webhook signature")
|
||||||
|
# return JsonResponse({"status": "error"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
transaction = request.data
|
||||||
|
transaction_id = transaction["transactionId"]
|
||||||
|
transaction_status = transaction["status"]
|
||||||
|
|
||||||
|
# FIXME just pass the checkout_id as sequence number?
|
||||||
|
checkout_info = CheckoutInformation.objects.get(transaction_id=transaction_id)
|
||||||
|
checkout_info.state = transaction_status
|
||||||
|
checkout_info.webhook_history.append(transaction)
|
||||||
|
checkout_info.save(update_fields=["state", "webhook_history"])
|
||||||
|
|
||||||
|
if transaction_status == ["settled", "transmitted"]:
|
||||||
|
# FIXME create CSU
|
||||||
|
...
|
||||||
|
|
||||||
|
return JsonResponse({"status": "ok"})
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(["POST"])
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
|
def checkout_vv(request):
|
||||||
|
"""
|
||||||
|
Checkout for the Versicherungsvermittler product (VV).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The one and only product, thus hardcoded
|
||||||
|
# Expected to be created in the admin interface first.
|
||||||
|
sku = VV_PRODUCT_SKU
|
||||||
|
|
||||||
|
logger.info("Checkout requested", sku=sku, user_id=request.user.id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
product = Product.objects.get(sku=sku)
|
||||||
|
except Product.DoesNotExist:
|
||||||
|
raise Exception(
|
||||||
|
f"Required Product not found: {sku} must be created in the admin interface first.",
|
||||||
|
)
|
||||||
|
|
||||||
|
checkouts = CheckoutInformation.objects.filter(
|
||||||
|
user=request.user,
|
||||||
|
product_sku=sku,
|
||||||
|
)
|
||||||
|
|
||||||
|
datatrans = DataTransPaymentProvider()
|
||||||
|
|
||||||
|
# already purchased (settled or transmitted)
|
||||||
|
if checkouts.filter(
|
||||||
|
state__in=[CheckoutState.SETTLED, CheckoutState.TRANSMITTED]
|
||||||
|
).exists():
|
||||||
|
return JsonResponse({"next_step_url": URL_CHECKOUT_COMPLETE})
|
||||||
|
|
||||||
|
# FIXME: Re-using seems not to work -> just create a new one for now
|
||||||
|
# Also check if failed transactions get notified via webhook
|
||||||
|
# already initialized -> redirect to payment page again
|
||||||
|
# if checkout := checkouts.filter(state=CheckoutState.INITIALIZED).first():
|
||||||
|
# return JsonResponse(
|
||||||
|
# {
|
||||||
|
# "next_step_url": datatrans.get_payment_url(
|
||||||
|
# transaction_id=checkout.transaction_id
|
||||||
|
# )
|
||||||
|
# }
|
||||||
|
# )
|
||||||
|
|
||||||
|
address = request.data["address"]
|
||||||
|
organization = request.data["organisation_address"]
|
||||||
|
base_redirect_url = request.data["redirect_url"]
|
||||||
|
|
||||||
|
# not yet initialized at all or canceled/failed
|
||||||
|
transaction_id = datatrans.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_cancel=checkout_cancel_url(base_redirect_url),
|
||||||
|
webhook_url=webhook_url(base_redirect_url),
|
||||||
|
)
|
||||||
|
|
||||||
|
# immutable snapshot of data at time of purchase
|
||||||
|
CheckoutInformation.objects.create(
|
||||||
|
user=request.user,
|
||||||
|
state="initialized",
|
||||||
|
transaction_id=transaction_id,
|
||||||
|
# product
|
||||||
|
product_sku=sku,
|
||||||
|
product_price=product.price,
|
||||||
|
product_name=product.name,
|
||||||
|
product_description=product.description,
|
||||||
|
# address
|
||||||
|
first_name=address["firstName"],
|
||||||
|
last_name=address["lastName"],
|
||||||
|
street=address["street"],
|
||||||
|
street_number=address["streetNumber"],
|
||||||
|
postal_code=address["postalCode"],
|
||||||
|
city=address["city"],
|
||||||
|
country=address["country"],
|
||||||
|
# organization
|
||||||
|
company_name=organization.get("name"),
|
||||||
|
company_street=organization.get("street"),
|
||||||
|
company_street_number=organization.get("streetNumber"),
|
||||||
|
company_postal_code=organization.get("postalCode"),
|
||||||
|
company_city=organization.get("city"),
|
||||||
|
company_country=organization.get("country"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return JsonResponse(
|
||||||
|
{"next_step_url": datatrans.get_payment_url(transaction_id=transaction_id)}
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue