feat: datatrans re-init on double checkout
This commit is contained in:
parent
447ac05859
commit
561f9e3c96
|
|
@ -663,10 +663,10 @@ NOTIFICATIONS_NOTIFICATION_MODEL = "notify.Notification"
|
||||||
SENDGRID_API_KEY = env("IT_SENDGRID_API_KEY", default="")
|
SENDGRID_API_KEY = env("IT_SENDGRID_API_KEY", default="")
|
||||||
|
|
||||||
# Datatrans (payment)
|
# Datatrans (payment)
|
||||||
# See https://admin.sandbox.datatrans.com/MerchSecurAdmin.jsp)
|
# See https://admin.sandbox.datatrans.com/MerchSecurAdmin.jsp
|
||||||
DATATRANS_HMAC_KEY = env("DATATRANS_HMAC_KEY", default="")
|
DATATRANS_HMAC_KEY = env("DATATRANS_HMAC_KEY", default="")
|
||||||
|
|
||||||
# See https://admin.sandbox.datatrans.com/MenuDispatch.jsp?main=1&sub=4)
|
# See https://admin.sandbox.datatrans.com/MenuDispatch.jsp?main=1&sub=4
|
||||||
# => echo -n "Username:Password" | base64
|
# => echo -n "Username:Password" | base64
|
||||||
DATATRANS_BASIC_AUTH_KEY = env("DATATRANS_BASIC_AUTH_KEY", default="")
|
DATATRANS_BASIC_AUTH_KEY = env("DATATRANS_BASIC_AUTH_KEY", default="")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@
|
||||||
|
|
||||||
## Shop Product
|
## Shop Product
|
||||||
|
|
||||||
In Django Shop App, create new products (Products model) that should be available in the shop.
|
In the Django shop app, create new products that should be available in the shop:
|
||||||
Products:
|
|
||||||
|
|
||||||
- `vv-de` Price 30000 (300_00 -> 300.00 CHF), name & description can be anything.
|
- `vv-de` Price 30000 (300_00 -> 300.00 CHF), name & description can be anything.
|
||||||
- ONLY if `COURSE_VERSICHERUNGSVERMITTLERIN_ID` exists!
|
- ONLY if `COURSE_VERSICHERUNGSVERMITTLERIN_ID` exists!
|
||||||
|
|
@ -12,7 +11,7 @@ Products:
|
||||||
- `vv-it` Price 30000 (300_00 -> 300.00 CHF), name & description can be anything.
|
- `vv-it` Price 30000 (300_00 -> 300.00 CHF), name & description can be anything.
|
||||||
- ONLY if `COURSE_VERSICHERUNGSVERMITTLERIN_ID_IT` exists!
|
- ONLY if `COURSE_VERSICHERUNGSVERMITTLERIN_ID_IT` exists!
|
||||||
|
|
||||||
## Datatrans
|
## Datatrans (Payment Provider)
|
||||||
|
|
||||||
- Set `DATATRANS_BASIC_AUTH_KEY`:
|
- Set `DATATRANS_BASIC_AUTH_KEY`:
|
||||||
- https://admin.sandbox.datatrans.com/MenuDispatch.jsp?main=1&sub=4
|
- https://admin.sandbox.datatrans.com/MenuDispatch.jsp?main=1&sub=4
|
||||||
|
|
@ -21,27 +20,14 @@ Products:
|
||||||
- Set `DATATRANS_HMAC_KEY`:
|
- Set `DATATRANS_HMAC_KEY`:
|
||||||
- https://admin.sandbox.datatrans.com/MerchSecurAdmin.jsp
|
- https://admin.sandbox.datatrans.com/MerchSecurAdmin.jsp
|
||||||
|
|
||||||
- Ensure that the webhook is set up correctly by Datatrans:
|
For Production:
|
||||||
- Be default transitions from `initialized` to `failed` do not trigger the webhook.
|
|
||||||
- Edgecase: When user starts a datatrans payment and then closes the browser, the payment will be
|
|
||||||
in `initialized`
|
|
||||||
state forever. -> That's why we need the webhook for `initialized` -> `failed` transitions.
|
|
||||||
- This can and needs to be enabled by datatrans (according to Mario from datatrans).
|
|
||||||
- Livio 21.11.23: Mario promised to enable it,
|
|
||||||
- Livio 27.11.23. Not yet enabled for the sandbox. -> Followed up!
|
|
||||||
- Livio: TODO still not enabled. Follow up again!
|
|
||||||
|
|
||||||
### Production / "going live"
|
1. Coordinate with datatrans to get production account. -> TBD!
|
||||||
|
|
||||||
For Production: We use the proper production datatrans endpoint!
|
|
||||||
|
|
||||||
1. Coordinate with datatrans to get production account.
|
|
||||||
2. Set `DATATRANS_BASIC_AUTH_KEY` and `DATATRANS_HMAC_KEY` to the production values (see above).
|
2. Set `DATATRANS_BASIC_AUTH_KEY` and `DATATRANS_HMAC_KEY` to the production values (see above).
|
||||||
3. Ensure that the webhook is set up correctly by Datatrans (see above).
|
|
||||||
|
|
||||||
## OAUTH
|
## OAUTH
|
||||||
|
|
||||||
Make sure that the following env vars are set:
|
For Production: Make sure that the following env vars are set:
|
||||||
|
|
||||||
### Azure B2C
|
### Azure B2C
|
||||||
|
|
||||||
|
|
@ -49,26 +35,30 @@ Make sure that the following env vars are set:
|
||||||
- Set `OAUTH_SIGNUP_CLIENT_SECRET`
|
- Set `OAUTH_SIGNUP_CLIENT_SECRET`
|
||||||
- Set `OAUTH_SIGNUP_SERVER_METADATA_URL` (.well-known/openid-configuration)
|
- Set `OAUTH_SIGNUP_SERVER_METADATA_URL` (.well-known/openid-configuration)
|
||||||
- Set `OAUTH_SIGNUP_TENANT_ID`
|
- Set `OAUTH_SIGNUP_TENANT_ID`
|
||||||
|
- Set `OAUTH_SIGNUP_REDIRECT_URI` (`.../sso/login` e.g. `https://myvbv-stage.iterativ.ch/sso/login`)
|
||||||
|
|
||||||
### Keycloak
|
### Keycloak
|
||||||
|
|
||||||
- Set `OAUTH_SIGNIN_CLIENT_ID`
|
- Set `OAUTH_SIGNIN_CLIENT_ID`
|
||||||
- Set `OAUTH_SIGNIN_CLIENT_SECRET`
|
- Set `OAUTH_SIGNIN_CLIENT_SECRET`
|
||||||
- Set `OAUTH_SIGNIN_SERVER_METADATA_URL` (.well-known/openid-configuration)
|
- Set `OAUTH_SIGNIN_SERVER_METADATA_URL` (.well-known/openid-configuration)
|
||||||
|
|
||||||
### Redirect URIs
|
|
||||||
|
|
||||||
- Set `OAUTH_SIGNUP_REDIRECT_URI` (`.../sso/login` e.g. `https://myvbv-stage.iterativ.ch/sso/login`)
|
|
||||||
- Set `OAUTH_SIGNIN_REDIRECT_URI` (`.../sso/callback` e.g. `https://myvbv-stage.iterativ.ch/sso/callback`)
|
- Set `OAUTH_SIGNIN_REDIRECT_URI` (`.../sso/callback` e.g. `https://myvbv-stage.iterativ.ch/sso/callback`)
|
||||||
|
|
||||||
### Frontend:
|
### Caprover (VITEx)
|
||||||
|
|
||||||
- Update `VITE_OAUTH_API_BASE_URL` in `caprover_deploy.sh` for production.
|
- Set `VITE_OAUTH_API_BASE_URL` in `caprover_deploy.sh` for `prod` environment.
|
||||||
- Should be the SSO Prod one from Lernnetz.
|
- `OAUTH_SIGNIN_SERVER_METADATA_URL` should help to find the correct value.
|
||||||
|
- Should be the SSO Prod one from Lernnetz. -> TBD!
|
||||||
|
|
||||||
|
## Testing Payment Flow
|
||||||
|
|
||||||
|
- To get user into state for testing (e.g. test-student1@example.com so that he can buy the course):
|
||||||
|
- Remove all existing course session users for the user.
|
||||||
|
- Remove all existing checkout information for the user.
|
||||||
|
|
||||||
### Cleanup
|
### Cleanup
|
||||||
|
|
||||||
After everything runs fine, we should be able to remove the following env vars:
|
After everything runs fine, we should be able to remove the following deprecated env vars:
|
||||||
|
|
||||||
1. `IT_OAUTH_TENANT_ID`
|
1. `IT_OAUTH_TENANT_ID`
|
||||||
2. `IT_OAUTH_CLIENT_NAME`
|
2. `IT_OAUTH_CLIENT_NAME`
|
||||||
|
|
@ -79,3 +69,4 @@ After everything runs fine, we should be able to remove the following env vars:
|
||||||
7. `IT_OAUTH_SERVER_METADATA_URL`
|
7. `IT_OAUTH_SERVER_METADATA_URL`
|
||||||
8. `IT_OAUTH_SCOPE`
|
8. `IT_OAUTH_SCOPE`
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Generated by Django 3.2.20 on 2023-12-05 13:27
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("shop", "0010_alter_checkoutinformation_state"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="checkoutinformation",
|
||||||
|
name="state",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("ongoing", "Ongoing"),
|
||||||
|
("paid", "Paid"),
|
||||||
|
("canceled", "Canceled"),
|
||||||
|
("failed", "Failed"),
|
||||||
|
],
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -57,19 +57,19 @@ class CheckoutState(models.TextChoices):
|
||||||
The state of a checkout process transaction.
|
The state of a checkout process transaction.
|
||||||
|
|
||||||
PAID: Datatrans transaction settled/transmitted.
|
PAID: Datatrans transaction settled/transmitted.
|
||||||
Rest of the states are self-explanatory, same as in Datatrans docs.
|
ONGOING: Any state that is not final (e.g. initialized, challenge_ongoing, etc.)
|
||||||
|
|
||||||
1) We use the `autoSettle` feature of DataTrans! Therefore, there are less possible states:
|
1) We use the `autoSettle` feature of DataTrans!
|
||||||
-> https://docs.datatrans.ch/docs/after-the-payment
|
-> https://docs.datatrans.ch/docs/after-the-payment
|
||||||
-> https://api-reference.datatrans.ch/#tag/v1transactions/operation/status
|
-> https://api-reference.datatrans.ch/#tag/v1transactions/operation/status
|
||||||
|
|
||||||
2) Difference between `settled` and `transmitted`:
|
2) Difference between `settled` and `transmitted`:
|
||||||
- https://www.datatrans.ch/en/know-how/faq/#what-does-the-status-transaction-settled-or-settledtransmitted-mean
|
- https://www.datatrans.ch/en/know-how/faq/#what-does-the-status-transaction-settled-or-settledtransmitted-mean
|
||||||
|
|
||||||
3) Related relevant code:init_transaction and get_transaction_state in shop/services.py
|
3) Related code: init_transaction and get_transaction_state in shop/services.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
INITIALIZED = "initialized"
|
ONGOING = "ongoing"
|
||||||
PAID = "paid"
|
PAID = "paid"
|
||||||
CANCELED = "canceled"
|
CANCELED = "canceled"
|
||||||
FAILED = "failed"
|
FAILED = "failed"
|
||||||
|
|
|
||||||
|
|
@ -124,9 +124,10 @@ def get_payment_url(transaction_id: str):
|
||||||
return f"{settings.DATATRANS_PAY_URL}/v1/start/{transaction_id}"
|
return f"{settings.DATATRANS_PAY_URL}/v1/start/{transaction_id}"
|
||||||
|
|
||||||
|
|
||||||
def datatrans_state_to_checkout_state(
|
def datatrans_state_to_checkout_state(datatrans_transaction_state) -> CheckoutState:
|
||||||
datatrans_transaction_state: str,
|
"""
|
||||||
) -> CheckoutState:
|
https://api-reference.datatrans.ch/#tag/v1transactions/operation/status
|
||||||
|
"""
|
||||||
if datatrans_transaction_state in ["settled", "transmitted"]:
|
if datatrans_transaction_state in ["settled", "transmitted"]:
|
||||||
return CheckoutState.PAID
|
return CheckoutState.PAID
|
||||||
elif datatrans_transaction_state == "failed":
|
elif datatrans_transaction_state == "failed":
|
||||||
|
|
@ -136,4 +137,4 @@ def datatrans_state_to_checkout_state(
|
||||||
else:
|
else:
|
||||||
# An intermediate state such as "initialized", "challenge_ongoing", etc.
|
# An intermediate state such as "initialized", "challenge_ongoing", etc.
|
||||||
# -> we don't care about those states, we only care about final states here.
|
# -> we don't care about those states, we only care about final states here.
|
||||||
return CheckoutState.INITIALIZED
|
return CheckoutState.ONGOING
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ class CheckoutAPITestCase(APITestCase):
|
||||||
CheckoutInformation.objects.filter(
|
CheckoutInformation.objects.filter(
|
||||||
user=self.user,
|
user=self.user,
|
||||||
product_sku=VV_DE_PRODUCT_SKU,
|
product_sku=VV_DE_PRODUCT_SKU,
|
||||||
state=CheckoutState.INITIALIZED.value,
|
state=CheckoutState.ONGOING,
|
||||||
).exists()
|
).exists()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -153,7 +153,7 @@ class CheckoutAPITestCase(APITestCase):
|
||||||
user=self.user,
|
user=self.user,
|
||||||
product_sku=VV_DE_PRODUCT_SKU,
|
product_sku=VV_DE_PRODUCT_SKU,
|
||||||
product_price=0,
|
product_price=0,
|
||||||
state=CheckoutState.PAID.value,
|
state=CheckoutState.PAID,
|
||||||
)
|
)
|
||||||
|
|
||||||
# WHEN
|
# WHEN
|
||||||
|
|
@ -174,18 +174,25 @@ class CheckoutAPITestCase(APITestCase):
|
||||||
response.json()["next_step_url"],
|
response.json()["next_step_url"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_checkout_double_checkout(self):
|
@patch("vbv_lernwelt.shop.views.init_transaction")
|
||||||
|
def test_checkout_double_checkout(self, mock_init_transaction):
|
||||||
|
"""Advise by Datatrans: Just create a new transaction."""
|
||||||
# GIVEN
|
# GIVEN
|
||||||
transaction_id = "1234567890"
|
|
||||||
|
|
||||||
|
# existing checkout
|
||||||
|
transaction_id_previous = "1234567890"
|
||||||
CheckoutInformation.objects.create(
|
CheckoutInformation.objects.create(
|
||||||
user=self.user,
|
user=self.user,
|
||||||
product_sku=VV_DE_PRODUCT_SKU,
|
product_sku=VV_DE_PRODUCT_SKU,
|
||||||
product_price=0,
|
product_price=0,
|
||||||
state=CheckoutState.INITIALIZED.value,
|
state=CheckoutState.ONGOING,
|
||||||
transaction_id=transaction_id,
|
transaction_id=transaction_id_previous,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# new checkout / transaction
|
||||||
|
transaction_id_next = "9999999999"
|
||||||
|
mock_init_transaction.return_value = transaction_id_next
|
||||||
|
|
||||||
# WHEN
|
# WHEN
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
path=reverse("checkout-vv"),
|
path=reverse("checkout-vv"),
|
||||||
|
|
@ -200,14 +207,41 @@ class CheckoutAPITestCase(APITestCase):
|
||||||
# THEN
|
# THEN
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
f"https://pay.sandbox.datatrans.com/v1/start/{transaction_id}",
|
f"https://pay.sandbox.datatrans.com/v1/start/{transaction_id_next}",
|
||||||
response.json()["next_step_url"],
|
response.json()["next_step_url"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# check that we have two checkouts
|
||||||
|
# (one previous and one new)
|
||||||
|
self.assertEqual(
|
||||||
|
2,
|
||||||
|
CheckoutInformation.objects.count(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# previous checkout
|
||||||
|
self.assertTrue(
|
||||||
|
CheckoutInformation.objects.filter(
|
||||||
|
user=self.user,
|
||||||
|
product_sku=VV_DE_PRODUCT_SKU,
|
||||||
|
state=CheckoutState.ONGOING,
|
||||||
|
transaction_id=transaction_id_previous,
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
# new checkout
|
||||||
|
self.assertTrue(
|
||||||
|
CheckoutInformation.objects.filter(
|
||||||
|
user=self.user,
|
||||||
|
product_sku=VV_DE_PRODUCT_SKU,
|
||||||
|
state=CheckoutState.ONGOING,
|
||||||
|
transaction_id=transaction_id_next,
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
|
||||||
@patch("vbv_lernwelt.shop.views.init_transaction")
|
@patch("vbv_lernwelt.shop.views.init_transaction")
|
||||||
def test_checkout_failed_creates_new(self, mock_init_transaction):
|
def test_checkout_failed_creates_new(self, mock_init_transaction):
|
||||||
# GIVEN
|
# GIVEN
|
||||||
state = CheckoutState.FAILED.value
|
state = CheckoutState.FAILED
|
||||||
transaction_id = "1234567890"
|
transaction_id = "1234567890"
|
||||||
mock_init_transaction.return_value = transaction_id
|
mock_init_transaction.return_value = transaction_id
|
||||||
|
|
||||||
|
|
@ -240,7 +274,7 @@ class CheckoutAPITestCase(APITestCase):
|
||||||
@patch("vbv_lernwelt.shop.views.init_transaction")
|
@patch("vbv_lernwelt.shop.views.init_transaction")
|
||||||
def test_checkout_cancelled_creates_new(self, mock_init_transaction):
|
def test_checkout_cancelled_creates_new(self, mock_init_transaction):
|
||||||
# GIVEN
|
# GIVEN
|
||||||
state = CheckoutState.CANCELED.value
|
state = CheckoutState.CANCELED
|
||||||
transaction_id = "1234567899"
|
transaction_id = "1234567899"
|
||||||
mock_init_transaction.return_value = transaction_id
|
mock_init_transaction.return_value = transaction_id
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ class DatatransWebhookTestCase(APITestCase):
|
||||||
create_checkout_information(
|
create_checkout_information(
|
||||||
user=self.user,
|
user=self.user,
|
||||||
transaction_id=transaction_id,
|
transaction_id=transaction_id,
|
||||||
state=CheckoutState.INITIALIZED,
|
state=CheckoutState.ONGOING,
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_is_signature_valid.return_value = True
|
mock_is_signature_valid.return_value = True
|
||||||
|
|
@ -143,7 +143,7 @@ class DatatransWebhookTestCase(APITestCase):
|
||||||
create_checkout_information(
|
create_checkout_information(
|
||||||
user=self.user,
|
user=self.user,
|
||||||
transaction_id=transaction_id,
|
transaction_id=transaction_id,
|
||||||
state=CheckoutState.INITIALIZED,
|
state=CheckoutState.ONGOING,
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_is_signature_valid.return_value = True
|
mock_is_signature_valid.return_value = True
|
||||||
|
|
@ -203,7 +203,7 @@ class DatatransWebhookTestCase(APITestCase):
|
||||||
create_checkout_information(
|
create_checkout_information(
|
||||||
user=self.user,
|
user=self.user,
|
||||||
transaction_id=transaction_id,
|
transaction_id=transaction_id,
|
||||||
state=CheckoutState.INITIALIZED,
|
state=CheckoutState.ONGOING,
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_is_signature_valid.return_value = True
|
mock_is_signature_valid.return_value = True
|
||||||
|
|
@ -223,7 +223,7 @@ class DatatransWebhookTestCase(APITestCase):
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
CheckoutInformation.objects.get(transaction_id=transaction_id).state,
|
CheckoutInformation.objects.get(transaction_id=transaction_id).state,
|
||||||
CheckoutState.FAILED.value,
|
CheckoutState.FAILED,
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch("vbv_lernwelt.shop.views.is_signature_valid")
|
@patch("vbv_lernwelt.shop.views.is_signature_valid")
|
||||||
|
|
@ -235,7 +235,7 @@ class DatatransWebhookTestCase(APITestCase):
|
||||||
create_checkout_information( # noqa
|
create_checkout_information( # noqa
|
||||||
user=self.user,
|
user=self.user,
|
||||||
transaction_id=transaction_id,
|
transaction_id=transaction_id,
|
||||||
state=CheckoutState.INITIALIZED,
|
state=CheckoutState.ONGOING,
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_is_signature_valid.return_value = True
|
mock_is_signature_valid.return_value = True
|
||||||
|
|
@ -255,5 +255,5 @@ class DatatransWebhookTestCase(APITestCase):
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
CheckoutInformation.objects.get(transaction_id=transaction_id).state,
|
CheckoutInformation.objects.get(transaction_id=transaction_id).state,
|
||||||
CheckoutState.CANCELED.value,
|
CheckoutState.CANCELED,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import structlog
|
import structlog
|
||||||
|
from django.conf import settings
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
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 sentry_sdk import capture_exception
|
||||||
|
|
||||||
from vbv_lernwelt.course.consts import (
|
from vbv_lernwelt.course.consts import (
|
||||||
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID,
|
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID,
|
||||||
|
|
@ -75,13 +77,15 @@ def update_billing_address(request):
|
||||||
|
|
||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
def transaction_webhook(request):
|
def transaction_webhook(request):
|
||||||
|
"""IMPORTANT: This is not called for timed out transactions!"""
|
||||||
|
|
||||||
logger.info("Webhook: Datatrans called transaction webhook", body=request.body)
|
logger.info("Webhook: Datatrans called transaction webhook", body=request.body)
|
||||||
|
|
||||||
if not is_signature_valid(
|
if not is_signature_valid(
|
||||||
signature=request.headers.get("Datatrans-Signature", ""),
|
signature=request.headers.get("Datatrans-Signature", ""),
|
||||||
payload=request.body,
|
payload=request.body,
|
||||||
):
|
):
|
||||||
logger.warning("Invalid signature")
|
logger.warning("Datatrans Transaction Webhook: Invalid Signature -> Ignored")
|
||||||
return JsonResponse({"status": "invalid signature"}, status=400)
|
return JsonResponse({"status": "invalid signature"}, status=400)
|
||||||
|
|
||||||
transaction = request.data
|
transaction = request.data
|
||||||
|
|
@ -107,7 +111,18 @@ def transaction_webhook(request):
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
def checkout_vv(request):
|
def checkout_vv(request):
|
||||||
"""
|
"""
|
||||||
Checkout for the Versicherungsvermittler products (vv-de, vv-fr, vv-it)
|
Check-out for the Versicherungsvermittler products (vv-de, vv-fr, vv-it)
|
||||||
|
|
||||||
|
IMPORTANT: Even if we have an already ONGOING checkout,
|
||||||
|
we create a new one! This might seem a bit unintuitive,
|
||||||
|
but it's the advised way to handle it by Datatrans:
|
||||||
|
|
||||||
|
"Fehlverhalten des User können fast gar nicht abgefangen werden,
|
||||||
|
wichtig wäre aus eurer Sicht das ihr immer einen neuen INIT
|
||||||
|
schickt, wenn der User im Checkout ist und zum Beispiel
|
||||||
|
auf «Bezahlen» klickt. Um zum Beispiel White-screens
|
||||||
|
bei Browser Back redirections zu vermeiden."
|
||||||
|
|
||||||
"""
|
"""
|
||||||
sku = request.data["product"]
|
sku = request.data["product"]
|
||||||
base_redirect_url = request.data["redirect_url"]
|
base_redirect_url = request.data["redirect_url"]
|
||||||
|
|
@ -129,16 +144,11 @@ def checkout_vv(request):
|
||||||
product_sku=sku,
|
product_sku=sku,
|
||||||
)
|
)
|
||||||
|
|
||||||
# already paid (successfully)-> redirect to home
|
# already paid successfully -> redirect to home
|
||||||
if checkouts.filter(state__in=[CheckoutState.PAID]).exists():
|
# any other case create a new checkout (see doc above)
|
||||||
|
if checkouts.filter(state=CheckoutState.PAID).exists():
|
||||||
return next_step_response(url="/")
|
return next_step_response(url="/")
|
||||||
|
|
||||||
# already initialized -> redirect to payment page again
|
|
||||||
if checkout := checkouts.filter(state=CheckoutState.INITIALIZED).first():
|
|
||||||
return next_step_response(url=get_payment_url(checkout.transaction_id))
|
|
||||||
|
|
||||||
# not yet initialized at all, or canceled/failed
|
|
||||||
# -> create new transaction and checkout
|
|
||||||
try:
|
try:
|
||||||
transaction_id = init_transaction(
|
transaction_id = init_transaction(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
|
|
@ -149,6 +159,8 @@ def checkout_vv(request):
|
||||||
webhook_url=webhook_url(base_redirect_url),
|
webhook_url=webhook_url(base_redirect_url),
|
||||||
)
|
)
|
||||||
except InitTransactionException as e:
|
except InitTransactionException as e:
|
||||||
|
if not settings.DEBUG:
|
||||||
|
capture_exception(e)
|
||||||
return next_step_response(
|
return next_step_response(
|
||||||
url=checkout_error_url(
|
url=checkout_error_url(
|
||||||
base_url=base_redirect_url,
|
base_url=base_redirect_url,
|
||||||
|
|
@ -157,7 +169,7 @@ def checkout_vv(request):
|
||||||
|
|
||||||
CheckoutInformation.objects.create(
|
CheckoutInformation.objects.create(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
state="initialized",
|
state=CheckoutState.ONGOING,
|
||||||
transaction_id=transaction_id,
|
transaction_id=transaction_id,
|
||||||
# product
|
# product
|
||||||
product_sku=sku,
|
product_sku=sku,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue