vbv/server/vbv_lernwelt/shop/views.py

312 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from datetime import date
import structlog
from django.conf import settings
from django.http import JsonResponse
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from sentry_sdk import capture_exception
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
from vbv_lernwelt.learnpath.consts import COURSE_PROFILE_ALL_ID
from vbv_lernwelt.learnpath.models import CourseProfile
from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email
from vbv_lernwelt.shop.const import (
VV_DE_PRODUCT_SKU,
VV_FR_PRODUCT_SKU,
VV_IT_PRODUCT_SKU,
VV_PRODUCT_NUMBER,
)
from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState, Product
from vbv_lernwelt.shop.services import (
create_context_data_log,
datatrans_state_to_checkout_state,
get_payment_url,
init_datatrans_transaction,
InitTransactionException,
is_signature_valid,
)
logger = structlog.get_logger(__name__)
PRODUCT_SKU_TO_COURSE_SESSION_ID = {
# CourseSession IDs PROD/STAGING
VV_DE_PRODUCT_SKU: 1, # vv-de
VV_FR_PRODUCT_SKU: 2, # vv-fr
VV_IT_PRODUCT_SKU: 3, # vv-it
}
@api_view(["POST"])
def transaction_webhook(request):
"""IMPORTANT: This is not called for timed out transactions!"""
logger.info("Webhook: Datatrans called transaction webhook", body=request.body)
request._request.security_request_logging = "datatrans_webhook"
request._request.type = "datatrans"
request._request.log_additional_json_data = request.data
if not is_signature_valid(
signature=request.headers.get("Datatrans-Signature", ""),
payload=request.body,
):
logger.warning("Datatrans Transaction Webhook: Invalid Signature -> Ignored")
return JsonResponse({"status": "invalid signature"}, status=400)
transaction = request.data
transaction_id = transaction["transactionId"]
# keep webhook history (for debugging)
checkout_info = CheckoutInformation.objects.get(transaction_id=transaction_id)
checkout_info.webhook_history.append(transaction)
checkout_info.save(update_fields=["webhook_history"])
# update checkout state
checkout_state = datatrans_state_to_checkout_state(transaction["status"])
update_checkout_state(checkout_info=checkout_info, state=checkout_state)
# handle paid
if checkout_state == CheckoutState.PAID:
create_vv_course_session_user(checkout_info=checkout_info)
return JsonResponse({"status": "ok"})
@api_view(["POST"])
@permission_classes([IsAuthenticated])
def checkout_vv(request):
"""
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."
"""
context_data, log = create_context_data_log(request, "checkout_vv")
sku = request.data["product"]
base_redirect_url = request.data["redirect_url"]
chosen_profile_id = request.data.get("chosen_profile", COURSE_PROFILE_ALL_ID)
log.info("Checkout requested: sku", user_id=request.user.id, sku=sku)
try:
product = Product.objects.get(sku=sku)
except Product.DoesNotExist:
return next_step_response(
url=checkout_error_url(
base_url=base_redirect_url,
product_sku=sku,
message=f"{sku}_product_sku_does_not_exist_needs_to_be_created_first",
),
)
try:
chosen_profile = CourseProfile.objects.get(id=chosen_profile_id)
except CourseProfile.DoesNotExist:
chosen_profile = CourseProfile.objects.get(id=COURSE_PROFILE_ALL_ID)
checkouts = CheckoutInformation.objects.filter(
user=request.user,
product_sku=sku,
)
# already paid successfully -> redirect to home
# any other case create a new checkout (see doc above)
if checkouts.filter(state=CheckoutState.PAID).exists():
return next_step_response(url="/")
with_cembra_byjuno_invoice = request.data.get("with_cembra_byjuno_invoice", False)
ip_address = request.META.get("REMOTE_ADDR")
email = request.user.email
request.user.set_increment_abacus_debitor_number(
disable_save="fakeapi" in settings.DATATRANS_API_ENDPOINT
)
address_data = request.data["address"]
country_code = address_data.pop("country_code")
address_data["country_id"] = country_code
organisation_country_code = "CH"
if "organisation_country_code" in address_data:
organisation_country_code = address_data.pop("organisation_country_code")
address_data["organisation_country_id"] = organisation_country_code
if "birth_date" in address_data and address_data["birth_date"]:
address_data["birth_date"] = date.fromisoformat(address_data["birth_date"])
checkout_info = CheckoutInformation.objects.create(
user=request.user,
state=CheckoutState.ONGOING,
# product
product_sku=sku,
product_price=product.price,
product_name=product.name,
product_description=product.description,
email=email,
ip_address=ip_address,
cembra_byjuno_invoice=with_cembra_byjuno_invoice,
device_fingerprint_session_key=request.data.get(
"device_fingerprint_session_key", ""
),
# address
chosen_profile=chosen_profile,
**request.data["address"],
)
checkout_info.set_increment_abacus_order_id()
refno2 = f"{request.user.abacus_debitor_number}_{VV_PRODUCT_NUMBER}"
try:
datatrans_customer_data = None
datatrans_int_data = None
if with_cembra_byjuno_invoice:
# see https://api-reference.datatrans.ch/#tag/v1transactions as reference
datatrans_customer_data = {
"firstName": request.user.first_name,
"lastName": request.user.last_name,
"id": request.user.abacus_debitor_number,
"street": f'{request.data["address"]["street"]} {request.data["address"]["street_number"]}',
"city": request.data["address"]["city"],
"zipCode": request.data["address"]["postal_code"],
"country": request.data["address"]["country_id"],
"phone": request.data["address"]["phone_number"],
"email": email,
"birthDate": str(request.data["address"]["birth_date"]),
"language": request.user.language,
"ipAddress": ip_address,
"type": "P",
}
datatrans_int_data = {
"subtype": "INVOICE",
"riskOwner": "IJ",
"repaymentType": 3,
"deviceFingerprintId": request.data["device_fingerprint_session_key"],
}
transaction_id = init_datatrans_transaction(
user=request.user,
refno=str(checkout_info.abacus_order_id),
amount_chf_centimes=product.price,
redirect_url_success=checkout_success_url(
base_url=base_redirect_url, product_sku=sku
),
redirect_url_error=checkout_error_url(
base_url=base_redirect_url, product_sku=sku
),
redirect_url_cancel=checkout_cancel_url(base_redirect_url),
webhook_url=webhook_url(base_redirect_url),
refno2=refno2,
datatrans_customer_data=datatrans_customer_data,
datatrans_int_data=datatrans_int_data,
context_data=context_data,
with_cembra_byjuno_invoice=with_cembra_byjuno_invoice,
)
except InitTransactionException as e:
checkout_info.state = CheckoutState.FAILED.value
checkout_info.save()
if not settings.DEBUG:
log.error("Transaction initiation failed", exc_info=True, error=str(e))
capture_exception(e)
return next_step_response(
url=checkout_error_url(
base_url=base_redirect_url,
product_sku=sku,
),
)
checkout_info.transaction_id = transaction_id
checkout_info.save()
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 send_vv_welcome_email(checkout_info: CheckoutInformation):
course_names = {
VV_DE_PRODUCT_SKU: "Versicherungsvermittler/-in VBV (Deutsch)",
VV_FR_PRODUCT_SKU: "Intermédiaire dassurance AFA (Français)",
VV_IT_PRODUCT_SKU: "Intermediario/a assicurativo/a AFA (Italiano)",
}
send_email(
recipient_email=checkout_info.user.email,
template=EmailTemplate.WELCOME_MAIL_VV,
template_data={
"course": course_names[checkout_info.product_sku],
"target_url": "https://my.vbv-afa.ch/",
"name": f"{checkout_info.first_name} {checkout_info.last_name}",
"private_street": f"{checkout_info.street} {checkout_info.street_number}",
"private_city": f"{checkout_info.country_id}-{checkout_info.postal_code} {checkout_info.city}",
"company_name": checkout_info.organisation_detail_name,
"company_street": f"{checkout_info.organisation_street} {checkout_info.organisation_street_number}",
"company_city": f"{checkout_info.organisation_country_id}-{checkout_info.organisation_postal_code} {checkout_info.organisation_city}",
},
template_language=checkout_info.user.language,
fail_silently=True,
)
def create_vv_course_session_user(checkout_info: CheckoutInformation):
logger.info("Creating VV course session user", user_id=checkout_info.user_id)
_, created = CourseSessionUser.objects.get_or_create(
user=checkout_info.user,
role=CourseSessionUser.Role.MEMBER,
chosen_profile=checkout_info.chosen_profile,
course_session=CourseSession.objects.get(
id=PRODUCT_SKU_TO_COURSE_SESSION_ID[checkout_info.product_sku]
),
# chosen_profile=bla,
)
if created:
logger.info("VV course session user created", user_id=checkout_info.user_id)
send_vv_welcome_email(checkout_info)
def next_step_response(
url: str,
) -> JsonResponse:
return JsonResponse(
{
"next_step_url": url,
},
)
def webhook_url(base_url: str) -> str:
return f"{base_url}/api/shop/transaction/webhook/"
def checkout_error_url(
base_url: str, product_sku: str, message: str | None = None
) -> str:
url = f"{base_url}/onboarding/{product_sku}/checkout/address?error"
if message:
url += f"&message={message}"
return url
def checkout_cancel_url(base_url: str) -> str:
return f"{base_url}/"
def checkout_success_url(product_sku: str, base_url: str = "") -> str:
return f"{base_url}/onboarding/{product_sku}/checkout/complete"