wip: walking skeleton for datatrans

This commit is contained in:
Livio Bieri 2023-11-17 01:28:18 +01:00 committed by Christian Cueni
parent 4faa034609
commit be160f5fa7
3 changed files with 196 additions and 11 deletions

View File

@ -1,11 +1,12 @@
<script setup lang="ts">
import WizardPage from "@/components/onboarding/WizardPage.vue";
import type { Ref } from "vue";
import { ref, watch } from "vue";
import { computed, ref, watch } from "vue";
import { useUserStore } from "@/stores/user";
import PersonalAddress from "@/components/onboarding/PersonalAddress.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 = {
first_name: string;
@ -23,6 +24,11 @@ type BillingAddressType = {
};
const user = useUserStore();
const { organisations } = useEntities();
const userOrganisationName = computed(
() => organisations.value?.find((c) => c.id === user.organisation)?.name
);
const address = ref({
firstName: user.first_name,
@ -37,6 +43,7 @@ const address = ref({
const useCompanyAddress = ref(false);
const orgAddress = ref({
name: "",
street: "",
streetNumber: "",
postalCode: "",
@ -59,6 +66,7 @@ watch(billingAddressData, (newVal) => {
country: newVal.country || "",
};
orgAddress.value = {
name: userOrganisationName.value || "",
street: newVal.company_street || "",
streetNumber: newVal.company_street_number || "",
postalCode: newVal.company_postal_code || "",
@ -69,7 +77,9 @@ watch(billingAddressData, (newVal) => {
});
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,
last_name: address.value.lastName,
street: address.value.street,
@ -83,6 +93,22 @@ const executePayment = () => {
company_city: orgAddress.value.city,
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>
@ -110,7 +136,7 @@ const executePayment = () => {
class="underline"
@click="useCompanyAddress = true"
>
Rechnungsadresse von Axa hinzufügen
{{ `Rechnungsadresse von ${userOrganisationName} hinzufügen` }}
</button>
<transition
@ -123,7 +149,7 @@ const executePayment = () => {
>
<div v-if="useCompanyAddress">
<div class="flex items-center justify-between">
<h3>Rechnungsaddresse von Axa</h3>
<h3>{{ `Rechnungsaddresse von ${userOrganisationName}` }}</h3>
<button class="underline" @click="useCompanyAddress = false">
Entfernen
</button>

View File

@ -10,6 +10,9 @@ from django.views import defaults as default_views
from django.views.decorators.csrf import csrf_exempt
from django_ratelimit.exceptions import Ratelimited
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.user import me_user_view
@ -55,10 +58,12 @@ from vbv_lernwelt.importer.views import (
t2l_sync,
)
from vbv_lernwelt.notify.views import email_notification_settings
from vbv_lernwelt.shop.views import get_billing_address, update_billing_address
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.shop.views import (
get_billing_address,
update_billing_address,
checkout_vv,
transaction_webhook,
)
class SignedIntConverter(IntConverter):
@ -174,7 +179,9 @@ urlpatterns = [
# shop
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
path(

View File

@ -1,11 +1,41 @@
import structlog
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
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.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"])
@ -34,3 +64,125 @@ def update_billing_address(request):
return Response(serializer.data)
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)}
)