feat: datatrans cleanup & tests
This commit is contained in:
parent
1281be2221
commit
a9056a008b
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Generated by Django 3.2.20 on 2023-11-26 19:47
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("shop", "0009_alter_checkoutinformation_product_price"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="checkoutinformation",
|
||||||
|
name="state",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("initialized", "Initialized"),
|
||||||
|
("paid", "Paid"),
|
||||||
|
("canceled", "Canceled"),
|
||||||
|
("failed", "Failed"),
|
||||||
|
],
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -58,6 +58,9 @@ class CheckoutState(models.TextChoices):
|
||||||
"""
|
"""
|
||||||
The state of a checkout process transaction.
|
The state of a checkout process transaction.
|
||||||
|
|
||||||
|
PAID: Datatrans transaction settled/transmitted.
|
||||||
|
Rest of the states are self-explanatory, same as in Datatrans docs.
|
||||||
|
|
||||||
1) We use the `autoSettle` feature of DataTrans! Therefore, there are less possible states:
|
1) We use the `autoSettle` feature of DataTrans! Therefore, there are less possible states:
|
||||||
-> 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
|
||||||
|
|
@ -69,8 +72,7 @@ class CheckoutState(models.TextChoices):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
INITIALIZED = "initialized"
|
INITIALIZED = "initialized"
|
||||||
SETTLED = "settled"
|
PAID = "paid"
|
||||||
TRANSMITTED = "transmitted"
|
|
||||||
CANCELED = "canceled"
|
CANCELED = "canceled"
|
||||||
FAILED = "failed"
|
FAILED = "failed"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,6 @@ def init_transaction(
|
||||||
payload = {
|
payload = {
|
||||||
# We use autoSettle=True, so that we don't have to settle the transaction:
|
# We use autoSettle=True, so that we don't have to settle the transaction:
|
||||||
# -> Be aware that autoSettle has implications of the possible transaction states
|
# -> Be aware that autoSettle has implications of the possible transaction states
|
||||||
# -> see CheckoutState docstring.
|
|
||||||
"autoSettle": True,
|
"autoSettle": True,
|
||||||
"amount": amount_chf_centimes,
|
"amount": amount_chf_centimes,
|
||||||
"currency": "CHF",
|
"currency": "CHF",
|
||||||
|
|
|
||||||
|
|
@ -147,3 +147,122 @@ class CheckoutAPITestCase(APITestCase):
|
||||||
0,
|
0,
|
||||||
CheckoutInformation.objects.count(),
|
CheckoutInformation.objects.count(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_checkout_already_paid(self):
|
||||||
|
# GIVEN
|
||||||
|
CheckoutInformation.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
product_sku=VV_PRODUCT_SKU,
|
||||||
|
product_price=0,
|
||||||
|
state=CheckoutState.PAID.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.client.post(
|
||||||
|
path=reverse("checkout-vv"),
|
||||||
|
format="json",
|
||||||
|
data={
|
||||||
|
"redirect_url": REDIRECT_URL,
|
||||||
|
"address": TEST_ADDRESS_DATA,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(
|
||||||
|
"/",
|
||||||
|
response.json()["next_step_url"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_checkout_double_checkout(self):
|
||||||
|
# GIVEN
|
||||||
|
transaction_id = "1234567890"
|
||||||
|
|
||||||
|
CheckoutInformation.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
product_sku=VV_PRODUCT_SKU,
|
||||||
|
product_price=0,
|
||||||
|
state=CheckoutState.INITIALIZED.value,
|
||||||
|
transaction_id=transaction_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.client.post(
|
||||||
|
path=reverse("checkout-vv"),
|
||||||
|
format="json",
|
||||||
|
data={
|
||||||
|
"redirect_url": REDIRECT_URL,
|
||||||
|
"address": TEST_ADDRESS_DATA,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(
|
||||||
|
f"https://pay.sandbox.datatrans.com/v1/start/{transaction_id}",
|
||||||
|
response.json()["next_step_url"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("vbv_lernwelt.shop.views.init_transaction")
|
||||||
|
def test_checkout_failed_creates_new(self, mock_init_transaction):
|
||||||
|
# GIVEN
|
||||||
|
state = CheckoutState.FAILED.value
|
||||||
|
transaction_id = "1234567890"
|
||||||
|
mock_init_transaction.return_value = transaction_id
|
||||||
|
|
||||||
|
CheckoutInformation.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
product_sku=VV_PRODUCT_SKU,
|
||||||
|
product_price=0,
|
||||||
|
state=state,
|
||||||
|
transaction_id="0000000000",
|
||||||
|
)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.client.post(
|
||||||
|
path=reverse("checkout-vv"),
|
||||||
|
format="json",
|
||||||
|
data={
|
||||||
|
"redirect_url": REDIRECT_URL,
|
||||||
|
"address": TEST_ADDRESS_DATA,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(
|
||||||
|
f"https://pay.sandbox.datatrans.com/v1/start/{transaction_id}",
|
||||||
|
response.json()["next_step_url"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("vbv_lernwelt.shop.views.init_transaction")
|
||||||
|
def test_checkout_cancelled_creates_new(self, mock_init_transaction):
|
||||||
|
# GIVEN
|
||||||
|
state = CheckoutState.CANCELED.value
|
||||||
|
transaction_id = "1234567899"
|
||||||
|
mock_init_transaction.return_value = transaction_id
|
||||||
|
|
||||||
|
CheckoutInformation.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
product_sku=VV_PRODUCT_SKU,
|
||||||
|
product_price=0,
|
||||||
|
state=state,
|
||||||
|
transaction_id="1111111111",
|
||||||
|
)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.client.post(
|
||||||
|
path=reverse("checkout-vv"),
|
||||||
|
format="json",
|
||||||
|
data={
|
||||||
|
"redirect_url": REDIRECT_URL,
|
||||||
|
"address": TEST_ADDRESS_DATA,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(
|
||||||
|
f"https://pay.sandbox.datatrans.com/v1/start/{transaction_id}",
|
||||||
|
response.json()["next_step_url"],
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,12 @@ from unittest import TestCase
|
||||||
from vbv_lernwelt.shop.services import is_signature_valid
|
from vbv_lernwelt.shop.services import is_signature_valid
|
||||||
|
|
||||||
|
|
||||||
class DatatransWebhookSigningTestCase(TestCase):
|
class DatatransSigningTestCase(TestCase):
|
||||||
# Key is from their example in the docs, not ours! :D
|
"""
|
||||||
|
Test based on the example from the docs.
|
||||||
|
Key is from their example, not ours!
|
||||||
|
"""
|
||||||
|
|
||||||
HMAC_KEY_FROM_THE_DOCS_NOT_HAZARDOUS = (
|
HMAC_KEY_FROM_THE_DOCS_NOT_HAZARDOUS = (
|
||||||
"861bbfc01e089259091927d6ad7f71c8"
|
"861bbfc01e089259091927d6ad7f71c8"
|
||||||
"b46b7ee13499574e83c633b74cdc29e3"
|
"b46b7ee13499574e83c633b74cdc29e3"
|
||||||
|
|
@ -0,0 +1,252 @@
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from vbv_lernwelt.core.admin import User
|
||||||
|
from vbv_lernwelt.course.creators.test_utils import create_course, create_course_session
|
||||||
|
from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
|
from vbv_lernwelt.shop.models import (
|
||||||
|
CheckoutInformation,
|
||||||
|
CheckoutState,
|
||||||
|
Product,
|
||||||
|
VV_PRODUCT_SKU,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_checkout_information(
|
||||||
|
user: User,
|
||||||
|
transaction_id: str,
|
||||||
|
state: CheckoutState,
|
||||||
|
) -> CheckoutInformation:
|
||||||
|
return CheckoutInformation.objects.create(
|
||||||
|
user=user,
|
||||||
|
transaction_id=transaction_id,
|
||||||
|
product_sku=VV_PRODUCT_SKU,
|
||||||
|
product_price=300_00,
|
||||||
|
state=state.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DatatransWebhookTestCase(APITestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
course, _ = create_course(title="VV")
|
||||||
|
create_course_session(course=course, title="Versicherungsvermittler/-in")
|
||||||
|
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
username="testuser",
|
||||||
|
email="test@user.com",
|
||||||
|
password="testpassword",
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.product = Product.objects.create(
|
||||||
|
sku=VV_PRODUCT_SKU,
|
||||||
|
price=300_00,
|
||||||
|
description="VV",
|
||||||
|
name="VV",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_webhook_unsigned_payload(self):
|
||||||
|
# GIVEN
|
||||||
|
payload = {
|
||||||
|
"transactionId": "1234567890",
|
||||||
|
"status": "settled",
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {"Datatrans-Signature": "t=1605697463367,s0=guessed"}
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.client.post(
|
||||||
|
path=reverse("shop-transaction-webhook"),
|
||||||
|
format="json",
|
||||||
|
data=payload,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(
|
||||||
|
response.json(),
|
||||||
|
{"status": "invalid signature"},
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("vbv_lernwelt.shop.views.is_signature_valid")
|
||||||
|
def test_webhook_creates_course_session_user(self, mock_is_signature_valid):
|
||||||
|
# GIVEN
|
||||||
|
transaction_id = "1234567890"
|
||||||
|
|
||||||
|
create_checkout_information(
|
||||||
|
user=self.user,
|
||||||
|
transaction_id=transaction_id,
|
||||||
|
state=CheckoutState.INITIALIZED,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_is_signature_valid.return_value = True
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
|
||||||
|
# ~immediately after successful payment
|
||||||
|
response_settled = self.client.post(
|
||||||
|
path=reverse("shop-transaction-webhook"),
|
||||||
|
format="json",
|
||||||
|
headers={"Datatrans-Signature": "<signature>"},
|
||||||
|
data={
|
||||||
|
"status": "settled",
|
||||||
|
"transactionId": transaction_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# ~24h later
|
||||||
|
response_transmitted = self.client.post(
|
||||||
|
path=reverse("shop-transaction-webhook"),
|
||||||
|
format="json",
|
||||||
|
headers={"Datatrans-Signature": "<signature>"},
|
||||||
|
data={
|
||||||
|
"status": "transmitted",
|
||||||
|
"transactionId": transaction_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(response_settled.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response_transmitted.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
CheckoutInformation.objects.get(transaction_id=transaction_id).state,
|
||||||
|
CheckoutState.PAID.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
1,
|
||||||
|
CourseSessionUser.objects.count(),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
CourseSessionUser.objects.first().user,
|
||||||
|
self.user,
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("vbv_lernwelt.shop.views.is_signature_valid")
|
||||||
|
def test_webhook_updates_webhook_history(self, mock_is_signature_valid):
|
||||||
|
# GIVEN
|
||||||
|
transaction_id = "1234567890"
|
||||||
|
|
||||||
|
create_checkout_information(
|
||||||
|
user=self.user,
|
||||||
|
transaction_id=transaction_id,
|
||||||
|
state=CheckoutState.INITIALIZED,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_is_signature_valid.return_value = True
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response_1 = self.client.post(
|
||||||
|
path=reverse("shop-transaction-webhook"),
|
||||||
|
format="json",
|
||||||
|
headers={"Datatrans-Signature": "<signature>"},
|
||||||
|
data={
|
||||||
|
"transactionId": transaction_id,
|
||||||
|
"status": "failed",
|
||||||
|
"whatever": "1",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
response_2 = self.client.post(
|
||||||
|
path=reverse("shop-transaction-webhook"),
|
||||||
|
format="json",
|
||||||
|
headers={"Datatrans-Signature": "<signature>"},
|
||||||
|
data={
|
||||||
|
"transactionId": transaction_id,
|
||||||
|
"status": "failed",
|
||||||
|
"whatever": "2",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response_1.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response_2.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
CheckoutInformation.objects.get(
|
||||||
|
transaction_id=transaction_id
|
||||||
|
).webhook_history,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"transactionId": transaction_id,
|
||||||
|
"status": "failed",
|
||||||
|
"whatever": "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"transactionId": transaction_id,
|
||||||
|
"status": "failed",
|
||||||
|
"whatever": "2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("vbv_lernwelt.shop.views.is_signature_valid")
|
||||||
|
def test_webhook_failed(self, mock_is_signature_valid):
|
||||||
|
# GIVEN
|
||||||
|
transaction_id = "1234567890"
|
||||||
|
state_received = "failed"
|
||||||
|
|
||||||
|
create_checkout_information(
|
||||||
|
user=self.user,
|
||||||
|
transaction_id=transaction_id,
|
||||||
|
state=CheckoutState.INITIALIZED,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_is_signature_valid.return_value = True
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.client.post(
|
||||||
|
path=reverse("shop-transaction-webhook"),
|
||||||
|
format="json",
|
||||||
|
headers={"Datatrans-Signature": "<signature>"},
|
||||||
|
data={
|
||||||
|
"transactionId": transaction_id,
|
||||||
|
"status": state_received,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(
|
||||||
|
CheckoutInformation.objects.get(transaction_id=transaction_id).state,
|
||||||
|
CheckoutState.FAILED.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("vbv_lernwelt.shop.views.is_signature_valid")
|
||||||
|
def test_webhook_cancelled(self, mock_is_signature_valid):
|
||||||
|
# GIVEN
|
||||||
|
transaction_id = "1234567890"
|
||||||
|
state_received = "canceled"
|
||||||
|
|
||||||
|
create_checkout_information( # noqa
|
||||||
|
user=self.user,
|
||||||
|
transaction_id=transaction_id,
|
||||||
|
state=CheckoutState.INITIALIZED,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_is_signature_valid.return_value = True
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.client.post(
|
||||||
|
path=reverse("shop-transaction-webhook"),
|
||||||
|
format="json",
|
||||||
|
headers={"Datatrans-Signature": "<signature>"},
|
||||||
|
data={
|
||||||
|
"transactionId": transaction_id,
|
||||||
|
"status": state_received,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(
|
||||||
|
CheckoutInformation.objects.get(transaction_id=transaction_id).state,
|
||||||
|
CheckoutState.CANCELED.value,
|
||||||
|
)
|
||||||
|
|
@ -5,7 +5,6 @@ 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.core.admin import User
|
|
||||||
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
||||||
from vbv_lernwelt.shop.models import (
|
from vbv_lernwelt.shop.models import (
|
||||||
BillingAddress,
|
BillingAddress,
|
||||||
|
|
@ -24,6 +23,8 @@ from vbv_lernwelt.shop.services import (
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
COURSE_SESSION_TITLE_VV = "Versicherungsvermittler/-in"
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
|
|
@ -60,6 +61,12 @@ def update_billing_address(request):
|
||||||
|
|
||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
def transaction_webhook(request):
|
def transaction_webhook(request):
|
||||||
|
"""
|
||||||
|
VERY IMPORTANT: Datatrans had/has to set up stuff manually on their side:
|
||||||
|
Otherwise, this webhook was/will not be called for "initialized" -> "failed" state changes
|
||||||
|
For timed out transactions (cleaned up in 15 minute intervals, after 30 minutes by them),
|
||||||
|
"""
|
||||||
|
|
||||||
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(
|
||||||
|
|
@ -73,25 +80,23 @@ def transaction_webhook(request):
|
||||||
transaction_id = transaction["transactionId"]
|
transaction_id = transaction["transactionId"]
|
||||||
transaction_status = transaction["status"]
|
transaction_status = transaction["status"]
|
||||||
|
|
||||||
|
# keep webhook history for debugging
|
||||||
checkout_info = CheckoutInformation.objects.get(transaction_id=transaction_id)
|
checkout_info = CheckoutInformation.objects.get(transaction_id=transaction_id)
|
||||||
checkout_info.state = transaction_status
|
|
||||||
checkout_info.webhook_history.append(transaction)
|
checkout_info.webhook_history.append(transaction)
|
||||||
checkout_info.save(update_fields=["state", "webhook_history"])
|
checkout_info.save(update_fields=["webhook_history"])
|
||||||
|
|
||||||
|
# be aware autoSettle has implications on possible transaction states we get!
|
||||||
|
# See https://api-reference.datatrans.ch/#tag/v1transactions/operation/status
|
||||||
if transaction_status in ["settled", "transmitted"]:
|
if transaction_status in ["settled", "transmitted"]:
|
||||||
logger.info(
|
update_checkout_state(checkout_info=checkout_info, state=CheckoutState.PAID)
|
||||||
"Webhook: Create course session user", transaction_id=transaction_id
|
create_vv_course_session_user(checkout_info=checkout_info)
|
||||||
)
|
elif transaction_status == "failed":
|
||||||
|
update_checkout_state(checkout_info=checkout_info, state=CheckoutState.FAILED)
|
||||||
# FIXME make this "better"
|
elif transaction_status == "canceled":
|
||||||
cs_vv = CourseSession.objects.filter(
|
update_checkout_state(checkout_info=checkout_info, state=CheckoutState.CANCELED)
|
||||||
title="Versicherungsvermittler/-in"
|
else:
|
||||||
).first()
|
logger.warning(
|
||||||
|
"Unhandled transaction status", transaction_status=transaction_status
|
||||||
CourseSessionUser.objects.get_or_create(
|
|
||||||
course_session=cs_vv,
|
|
||||||
user=User.objects.get(id=checkout_info.user_id),
|
|
||||||
role=CourseSessionUser.Role.MEMBER,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return JsonResponse({"status": "ok"})
|
return JsonResponse({"status": "ok"})
|
||||||
|
|
@ -113,13 +118,11 @@ def checkout_vv(request):
|
||||||
try:
|
try:
|
||||||
product = Product.objects.get(sku=sku)
|
product = Product.objects.get(sku=sku)
|
||||||
except Product.DoesNotExist:
|
except Product.DoesNotExist:
|
||||||
return JsonResponse(
|
return next_step_response(
|
||||||
{
|
url=checkout_error_url(
|
||||||
"next_step_url": checkout_error_url(
|
|
||||||
base_url=base_redirect_url,
|
base_url=base_redirect_url,
|
||||||
message="vv_product_does_not_exist_needs_to_be_created",
|
message="vv_product_does_not_exist_needs_to_be_created",
|
||||||
)
|
),
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
checkouts = CheckoutInformation.objects.filter(
|
checkouts = CheckoutInformation.objects.filter(
|
||||||
|
|
@ -127,15 +130,13 @@ def checkout_vv(request):
|
||||||
product_sku=sku,
|
product_sku=sku,
|
||||||
)
|
)
|
||||||
|
|
||||||
# already purchased (settled or transmitted)
|
# already paid (successfully)-> redirect to home
|
||||||
if checkouts.filter(
|
if checkouts.filter(state__in=[CheckoutState.PAID]).exists():
|
||||||
state__in=[CheckoutState.SETTLED, CheckoutState.TRANSMITTED]
|
return next_step_response(url="/")
|
||||||
).exists():
|
|
||||||
return JsonResponse({"next_step_url": "/"})
|
|
||||||
|
|
||||||
# already initialized -> redirect to payment page again
|
# already initialized -> redirect to payment page again
|
||||||
if checkout := checkouts.filter(state=CheckoutState.INITIALIZED).first():
|
if checkout := checkouts.filter(state=CheckoutState.INITIALIZED).first():
|
||||||
return JsonResponse({"next_step_url": get_payment_url(checkout.transaction_id)})
|
return next_step_response(url=get_payment_url(checkout.transaction_id))
|
||||||
|
|
||||||
# not yet initialized at all, or canceled/failed
|
# not yet initialized at all, or canceled/failed
|
||||||
# -> create new transaction and checkout
|
# -> create new transaction and checkout
|
||||||
|
|
@ -149,12 +150,10 @@ 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:
|
||||||
return JsonResponse(
|
return next_step_response(
|
||||||
{
|
url=checkout_error_url(
|
||||||
"next_step_url": checkout_error_url(
|
|
||||||
base_url=base_redirect_url,
|
base_url=base_redirect_url,
|
||||||
)
|
),
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
CheckoutInformation.objects.create(
|
CheckoutInformation.objects.create(
|
||||||
|
|
@ -170,7 +169,33 @@ def checkout_vv(request):
|
||||||
**request.data["address"],
|
**request.data["address"],
|
||||||
)
|
)
|
||||||
|
|
||||||
return JsonResponse({"next_step_url": get_payment_url(transaction_id)})
|
return next_step_response(url=get_payment_url(transaction_id))
|
||||||
|
|
||||||
|
|
||||||
|
def update_checkout_state(checkout_info: CheckoutInformation, state: CheckoutState):
|
||||||
|
checkout_info.state = state.value
|
||||||
|
checkout_info.save(update_fields=["state"])
|
||||||
|
|
||||||
|
|
||||||
|
def create_vv_course_session_user(checkout_info: CheckoutInformation):
|
||||||
|
logger.info("Creating VV course session user", user_id=checkout_info.user_id)
|
||||||
|
CourseSessionUser.objects.get_or_create(
|
||||||
|
user=checkout_info.user,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
course_session=CourseSession.objects.filter(
|
||||||
|
title=COURSE_SESSION_TITLE_VV
|
||||||
|
).first(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def next_step_response(
|
||||||
|
url: str,
|
||||||
|
) -> JsonResponse:
|
||||||
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"next_step_url": url,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def webhook_url(base_url: str) -> str:
|
def webhook_url(base_url: str) -> str:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue