feat: vv-de vv-fr vv-it API support
This commit is contained in:
parent
100362b6a5
commit
fb24ec24e4
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
from django.db import models
|
||||
|
||||
VV_PRODUCT_SKU = "VV"
|
||||
|
||||
|
||||
class Country(models.Model):
|
||||
country_id = models.IntegerField(primary_key=True)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue