feat: vv-de vv-fr vv-it API support

This commit is contained in:
Livio Bieri 2023-12-04 17:05:10 +01:00 committed by Christian Cueni
parent 100362b6a5
commit fb24ec24e4
8 changed files with 89 additions and 56 deletions

View File

@ -564,7 +564,7 @@ ALLOWED_HOSTS = env.list(
"localhost",
"0.0.0.0",
"127.0.0.1",
"e124-2a02-21b4-9679-d800-9c5-c205-e72c-82f2.ngrok-free.app",
"2375-2a02-21b4-9679-d800-8151-6008-2b6c-7a32.ngrok-free.app",
],
)

View File

@ -1,28 +1,35 @@
# Setup
# Setup steps for Production
## Shop Product
- In Django Shop App, create a new product (Products model).
- `SKU` must be `VV`, Price 30000 (300_00 -> 300.00 CHF), name & description can be anything.
- Done for staging but not yet for production!
In Django Shop App, create new products (Products model) that should be available in the shop.
Products:
- `vv-de` Price 30000 (300_00 -> 300.00 CHF), name & description can be anything.
- ONLY if `COURSE_VERSICHERUNGSVERMITTLERIN_ID` exists!
- `vv-fr` Price 30000 (300_00 -> 300.00 CHF), name & description can be anything.
- ONLY if `COURSE_VERSICHERUNGSVERMITTLERIN_ID_FR` exists!
- `vv-it` Price 30000 (300_00 -> 300.00 CHF), name & description can be anything.
- ONLY if `COURSE_VERSICHERUNGSVERMITTLERIN_ID_IT` exists!
## Datatrans
- Set `DATATRANS_BASIC_AUTH_KEY`:
- https://admin.sandbox.datatrans.com/MenuDispatch.jsp?main=1&sub=4
- `echo -n "{merchantid}:{password}" | base64`
- https://admin.sandbox.datatrans.com/MenuDispatch.jsp?main=1&sub=4
- `echo -n "{merchantid}:{password}" | base64`
- 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:
- 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!
- 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"
@ -57,7 +64,7 @@ Make sure that the following env vars are set:
### Frontend:
- Update `VITE_OAUTH_API_BASE_URL` in `caprover_deploy.sh` for production.
- NEEDS to be updated! Should be the SSO Prod one from Lernnetz -> Lookup from Metadata URL
- Should be the SSO Prod one from Lernnetz.
### Cleanup

View File

@ -0,0 +1,4 @@
# available products for VV
VV_DE_PRODUCT_SKU = "vv-de"
VV_FR_PRODUCT_SKU = "vv-fr"
VV_IT_PRODUCT_SKU = "vv-it"

View File

@ -1,7 +1,5 @@
from django.db import models
VV_PRODUCT_SKU = "VV"
class Country(models.Model):
country_id = models.IntegerField(primary_key=True)

View File

@ -22,8 +22,7 @@ def is_signature_valid(
hmac_key: str = settings.DATATRANS_HMAC_KEY,
):
"""
See the docs:
https://docs.datatrans.ch/docs/additional-security
See the docs: https://docs.datatrans.ch/docs/additional-security
"""
try:
@ -126,13 +125,13 @@ def get_payment_url(transaction_id: str):
def datatrans_state_to_checkout_state(
transaction_state: str,
datatrans_transaction_state: str,
) -> CheckoutState:
if transaction_state in ["settled", "transmitted"]:
if datatrans_transaction_state in ["settled", "transmitted"]:
return CheckoutState.PAID
elif transaction_state == "failed":
elif datatrans_transaction_state == "failed":
return CheckoutState.FAILED
elif transaction_state == "canceled":
elif datatrans_transaction_state == "canceled":
return CheckoutState.CANCELED
else:
# An intermediate state such as "initialized", "challenge_ongoing", etc.

View File

@ -5,11 +5,11 @@ from rest_framework import status
from rest_framework.test import APITestCase
from vbv_lernwelt.core.admin import User
from vbv_lernwelt.shop.const import VV_DE_PRODUCT_SKU
from vbv_lernwelt.shop.models import (
CheckoutInformation,
CheckoutState,
Product,
VV_PRODUCT_SKU,
)
from vbv_lernwelt.shop.services import InitTransactionException
@ -39,7 +39,7 @@ REDIRECT_URL = "http://testserver/redirect-url"
class CheckoutAPITestCase(APITestCase):
def setUp(self) -> None:
Product.objects.create(
sku=VV_PRODUCT_SKU,
sku=VV_DE_PRODUCT_SKU,
price=300_00,
description="VV",
name="VV",
@ -65,6 +65,7 @@ class CheckoutAPITestCase(APITestCase):
format="json",
data={
"redirect_url": REDIRECT_URL,
"product": VV_DE_PRODUCT_SKU,
"address": TEST_ADDRESS_DATA,
},
)
@ -79,7 +80,7 @@ class CheckoutAPITestCase(APITestCase):
self.assertTrue(
CheckoutInformation.objects.filter(
user=self.user,
product_sku=VV_PRODUCT_SKU,
product_sku=VV_DE_PRODUCT_SKU,
state=CheckoutState.INITIALIZED.value,
).exists()
)
@ -105,6 +106,7 @@ class CheckoutAPITestCase(APITestCase):
format="json",
data={
"redirect_url": REDIRECT_URL,
"product": VV_DE_PRODUCT_SKU,
"address": TEST_ADDRESS_DATA,
},
)
@ -114,7 +116,7 @@ class CheckoutAPITestCase(APITestCase):
expected = (
f"{REDIRECT_URL}/onboarding/vv/checkout/address?error&"
f"message=vv_product_does_not_exist_needs_to_be_created"
f"message=vv-de_product_sku_does_not_exist_needs_to_be_created_first"
)
self.assertEqual(expected, response.json()["next_step_url"])
@ -132,6 +134,7 @@ class CheckoutAPITestCase(APITestCase):
format="json",
data={
"redirect_url": REDIRECT_URL,
"product": VV_DE_PRODUCT_SKU,
"address": TEST_ADDRESS_DATA,
},
)
@ -152,7 +155,7 @@ class CheckoutAPITestCase(APITestCase):
# GIVEN
CheckoutInformation.objects.create(
user=self.user,
product_sku=VV_PRODUCT_SKU,
product_sku=VV_DE_PRODUCT_SKU,
product_price=0,
state=CheckoutState.PAID.value,
)
@ -163,6 +166,7 @@ class CheckoutAPITestCase(APITestCase):
format="json",
data={
"redirect_url": REDIRECT_URL,
"product": VV_DE_PRODUCT_SKU,
"address": TEST_ADDRESS_DATA,
},
)
@ -180,7 +184,7 @@ class CheckoutAPITestCase(APITestCase):
CheckoutInformation.objects.create(
user=self.user,
product_sku=VV_PRODUCT_SKU,
product_sku=VV_DE_PRODUCT_SKU,
product_price=0,
state=CheckoutState.INITIALIZED.value,
transaction_id=transaction_id,
@ -192,6 +196,7 @@ class CheckoutAPITestCase(APITestCase):
format="json",
data={
"redirect_url": REDIRECT_URL,
"product": VV_DE_PRODUCT_SKU,
"address": TEST_ADDRESS_DATA,
},
)
@ -212,7 +217,7 @@ class CheckoutAPITestCase(APITestCase):
CheckoutInformation.objects.create(
user=self.user,
product_sku=VV_PRODUCT_SKU,
product_sku=VV_DE_PRODUCT_SKU,
product_price=0,
state=state,
transaction_id="0000000000",
@ -224,6 +229,7 @@ class CheckoutAPITestCase(APITestCase):
format="json",
data={
"redirect_url": REDIRECT_URL,
"product": VV_DE_PRODUCT_SKU,
"address": TEST_ADDRESS_DATA,
},
)
@ -244,7 +250,7 @@ class CheckoutAPITestCase(APITestCase):
CheckoutInformation.objects.create(
user=self.user,
product_sku=VV_PRODUCT_SKU,
product_sku=VV_DE_PRODUCT_SKU,
product_price=0,
state=state,
transaction_id="1111111111",
@ -256,6 +262,7 @@ class CheckoutAPITestCase(APITestCase):
format="json",
data={
"redirect_url": REDIRECT_URL,
"product": VV_DE_PRODUCT_SKU,
"address": TEST_ADDRESS_DATA,
},
)

View File

@ -5,13 +5,14 @@ from rest_framework import status
from rest_framework.test import APITestCase
from vbv_lernwelt.core.admin import User
from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN_ID
from vbv_lernwelt.course.creators.test_utils import create_course, create_course_session
from vbv_lernwelt.course.models import CourseSessionUser
from vbv_lernwelt.shop.const import VV_DE_PRODUCT_SKU
from vbv_lernwelt.shop.models import (
CheckoutInformation,
CheckoutState,
Product,
VV_PRODUCT_SKU,
)
@ -23,7 +24,7 @@ def create_checkout_information(
return CheckoutInformation.objects.create(
user=user,
transaction_id=transaction_id,
product_sku=VV_PRODUCT_SKU,
product_sku=VV_DE_PRODUCT_SKU,
product_price=300_00,
state=state.value,
)
@ -31,8 +32,13 @@ def create_checkout_information(
class DatatransWebhookTestCase(APITestCase):
def setUp(self) -> None:
course, _ = create_course(title="VV")
create_course_session(course=course, title="Versicherungsvermittler/-in")
course, _ = create_course(
title="VV_in_DE",
# needed for VV_DE_PRODUCT_SKU
_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID,
)
create_course_session(course=course, title="Versicherungsvermittler/-in DE")
self.user = User.objects.create_user(
username="testuser",
@ -42,7 +48,7 @@ class DatatransWebhookTestCase(APITestCase):
)
self.product = Product.objects.create(
sku=VV_PRODUCT_SKU,
sku=VV_DE_PRODUCT_SKU,
price=300_00,
description="VV",
name="VV",
@ -110,12 +116,12 @@ class DatatransWebhookTestCase(APITestCase):
)
# THEN
self.assertEqual(response_settled.status_code, status.HTTP_200_OK)
self.assertEqual(response_transmitted.status_code, status.HTTP_200_OK)
self.assertEqual(status.HTTP_200_OK, response_settled.status_code)
self.assertEqual(status.HTTP_200_OK, response_transmitted.status_code)
self.assertEqual(
CheckoutInformation.objects.get(transaction_id=transaction_id).state,
CheckoutState.PAID.value,
CheckoutInformation.objects.get(transaction_id=transaction_id).state,
)
self.assertEqual(
@ -124,8 +130,13 @@ class DatatransWebhookTestCase(APITestCase):
)
self.assertEqual(
CourseSessionUser.objects.first().user,
self.user,
CourseSessionUser.objects.first().user,
)
self.assertEqual(
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
CourseSessionUser.objects.first().course_session.course.id,
)
@patch("vbv_lernwelt.shop.views.is_signature_valid")

View File

@ -5,13 +5,22 @@ from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from vbv_lernwelt.course.consts import (
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID,
COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID,
)
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
from vbv_lernwelt.shop.const import (
VV_DE_PRODUCT_SKU,
VV_FR_PRODUCT_SKU,
VV_IT_PRODUCT_SKU,
)
from vbv_lernwelt.shop.models import (
BillingAddress,
CheckoutInformation,
CheckoutState,
Product,
VV_PRODUCT_SKU,
)
from vbv_lernwelt.shop.serializers import BillingAddressSerializer
from vbv_lernwelt.shop.services import (
@ -24,7 +33,11 @@ from vbv_lernwelt.shop.services import (
logger = structlog.get_logger(__name__)
COURSE_SESSION_TITLE_VV = "Versicherungsvermittler/-in"
PRODUCT_SKU_TO_COURSE = {
VV_DE_PRODUCT_SKU: COURSE_VERSICHERUNGSVERMITTLERIN_ID,
VV_FR_PRODUCT_SKU: COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID,
VV_IT_PRODUCT_SKU: COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID,
}
@api_view(["GET"])
@ -62,11 +75,6 @@ def update_billing_address(request):
@api_view(["POST"])
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)
if not is_signature_valid(
@ -99,22 +107,20 @@ def transaction_webhook(request):
@permission_classes([IsAuthenticated])
def checkout_vv(request):
"""
Checkout for the Versicherungsvermittler product (VV).
VV_PRODUCT_SKU: The one and only product, thus hardcoded
Expected to be created in the admin interface first.
Checkout for the Versicherungsvermittler products (vv-de, vv-fr, vv-it)
"""
sku = VV_PRODUCT_SKU
logger.info(f"Checkout requested: sku={sku}", user_id=request.user.id)
sku = request.data["product"]
base_redirect_url = request.data["redirect_url"]
logger.info(f"Checkout requested: sku={sku}", user_id=request.user.id)
try:
product = Product.objects.get(sku=sku)
except Product.DoesNotExist:
return next_step_response(
url=checkout_error_url(
base_url=base_redirect_url,
message="vv_product_does_not_exist_needs_to_be_created",
message=f"{sku}_product_sku_does_not_exist_needs_to_be_created_first",
),
)
@ -172,11 +178,12 @@ def update_checkout_state(checkout_info: CheckoutInformation, state: CheckoutSta
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
course_id=PRODUCT_SKU_TO_COURSE[checkout_info.product_sku]
).first(),
)