wip: walking skeleton for datatrans
This commit is contained in:
parent
4faa034609
commit
be160f5fa7
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue