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"> <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>

View File

@ -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(

View File

@ -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)}
)