chore: validate signature & cleanup
This commit is contained in:
parent
c6cb6ba80c
commit
b1439122e1
|
|
@ -648,6 +648,26 @@ NOTIFICATIONS_NOTIFICATION_MODEL = "notify.Notification"
|
||||||
# sendgrid (email notifications)
|
# sendgrid (email notifications)
|
||||||
SENDGRID_API_KEY = env("IT_SENDGRID_API_KEY", default="")
|
SENDGRID_API_KEY = env("IT_SENDGRID_API_KEY", default="")
|
||||||
|
|
||||||
|
# Datatrans (payment)
|
||||||
|
# See https://admin.sandbox.datatrans.com/MerchSecurAdmin.jsp)
|
||||||
|
DATATRANS_HMAC_KEY = env("DATATRANS_HMAC_KEY")
|
||||||
|
|
||||||
|
# See https://admin.sandbox.datatrans.com/MenuDispatch.jsp?main=1&sub=4)
|
||||||
|
# => echo -n "Username:Password" | base64
|
||||||
|
DATATRANS_BASIC_AUTH_KEY = env("DATATRANS_BASIC_AUTH_KEY")
|
||||||
|
|
||||||
|
if APP_ENVIRONMENT.startswith("prod"):
|
||||||
|
DATATRANS_API_ENDPOINT = "https://api.datatrans.com"
|
||||||
|
DATATRANS_PAY_URL = "https://pay.datatrans.com"
|
||||||
|
else:
|
||||||
|
DATATRANS_API_ENDPOINT = "https://api.sandbox.datatrans.com"
|
||||||
|
DATATRANS_PAY_URL = "https://pay.sandbox.datatrans.com"
|
||||||
|
|
||||||
|
# Only for debugging the webhook (locally)
|
||||||
|
DATATRANS_DEBUG_WEBHOOK_OVERWRITE = env(
|
||||||
|
"DATATRANS_DEBUG_WEBHOOK_OVERWRITE", default=None
|
||||||
|
)
|
||||||
|
|
||||||
# S3 BUCKET CONFIGURATION
|
# S3 BUCKET CONFIGURATION
|
||||||
FILE_UPLOAD_STORAGE = env("FILE_UPLOAD_STORAGE", default="s3") # local | s3
|
FILE_UPLOAD_STORAGE = env("FILE_UPLOAD_STORAGE", default="s3") # local | s3
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from vbv_lernwelt.shop.models import CheckoutInformation, Country, Product
|
from vbv_lernwelt.shop.models import CheckoutInformation, Country, Product
|
||||||
from vbv_lernwelt.shop.services import DataTransPaymentProvider
|
from vbv_lernwelt.shop.services import get_transaction_state
|
||||||
|
|
||||||
|
|
||||||
@admin.action(description="ABACUS: Create invoices")
|
@admin.action(description="ABACUS: Create invoices")
|
||||||
|
|
@ -12,9 +12,7 @@ def generate_invoice(modeladmin, request, queryset):
|
||||||
@admin.action(description="DATATRANS: Sync transaction states")
|
@admin.action(description="DATATRANS: Sync transaction states")
|
||||||
def sync_transaction_state(modeladmin, request, queryset):
|
def sync_transaction_state(modeladmin, request, queryset):
|
||||||
for checkout in queryset:
|
for checkout in queryset:
|
||||||
state = DataTransPaymentProvider().get_transaction_state(
|
state = get_transaction_state(transaction_id=checkout.transaction_id)
|
||||||
transaction_id=checkout.transaction_id
|
|
||||||
)
|
|
||||||
print(state)
|
print(state)
|
||||||
checkout.state = state.value
|
checkout.state = state.value
|
||||||
checkout.save(
|
checkout.save(
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
import os
|
|
||||||
import uuid
|
import uuid
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import structlog
|
import structlog
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from vbv_lernwelt.core.admin import User
|
from vbv_lernwelt.core.admin import User
|
||||||
from vbv_lernwelt.shop.models import CheckoutState
|
from vbv_lernwelt.shop.models import CheckoutState
|
||||||
|
|
@ -17,210 +16,117 @@ class PaymentException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PaymentProvider(ABC):
|
def is_signature_valid(
|
||||||
@abstractmethod
|
signature: str,
|
||||||
def init_transaction(
|
payload: bytes,
|
||||||
self,
|
hmac_key: str = settings.DATATRANS_HMAC_KEY,
|
||||||
user: User,
|
):
|
||||||
amount_chf_centimes: int,
|
|
||||||
redirect_url_success: str,
|
|
||||||
redirect_url_error: str,
|
|
||||||
redirect_url_cancel: str,
|
|
||||||
webhook_url: str,
|
|
||||||
):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_transaction_state(
|
|
||||||
self,
|
|
||||||
transaction_id: str,
|
|
||||||
) -> CheckoutState | None:
|
|
||||||
"""
|
|
||||||
Get the state of a transaction.
|
|
||||||
:param transaction_id: Transaction ID
|
|
||||||
:return: Transaction state or None if transaction does not exist
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_payment_url(self, transaction_id: str):
|
|
||||||
"""
|
|
||||||
Get the URL to redirect to for payment.
|
|
||||||
:param transaction_id: Transaction ID
|
|
||||||
:return: URL to redirect to for payment
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
|
|
||||||
class DataTransPaymentProvider(PaymentProvider):
|
|
||||||
"""
|
"""
|
||||||
Configuration:
|
See the docs:
|
||||||
|
https://docs.datatrans.ch/docs/additional-security
|
||||||
- 1. USE_DATATRANS_PRODUCTION, e.g. "False" for sandbox, "True" for production. Default: False
|
|
||||||
|
|
||||||
- 2. DATATRANS_BASIC_AUTH_KEY (see https://admin.sandbox.datatrans.com/MenuDispatch.jsp?main=1&sub=4)
|
|
||||||
- echo -n "Username:Password" | base64
|
|
||||||
|
|
||||||
- 3. DATATRANS_HMAC_KEY (see https://admin.sandbox.datatrans.com/MerchSecurAdmin.jsp)
|
|
||||||
|
|
||||||
- 4. DATATRANS_DEBUG_WEBHOOK_OVERWRITE (optional webhook URL overwrite for debugging purposes)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
try:
|
||||||
is_production_environment = (
|
timestamp = signature.split(",")[0].split("=")[1]
|
||||||
os.getenv("USE_DATATRANS_PRODUCTION", "False").lower() == "true"
|
s0_expected = signature.split(",")[1].split("=")[1]
|
||||||
|
|
||||||
|
key_hex_bytes = bytes.fromhex(hmac_key)
|
||||||
|
timestamp_bytes = bytes(timestamp, "utf-8")
|
||||||
|
|
||||||
|
s0_actual = hmac.new(
|
||||||
|
key_hex_bytes, timestamp_bytes + payload, hashlib.sha256
|
||||||
|
).hexdigest()
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
logger.warning(
|
||||||
|
"Invalid signature format Expected format: t=TIMESTAMP,s0=XXXX",
|
||||||
|
signature=signature,
|
||||||
)
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
self._api_endpoint = (
|
return s0_actual == s0_expected
|
||||||
"https://api.sandbox.datatrans.com"
|
|
||||||
if not is_production_environment
|
|
||||||
else "https://api.datatrans.com"
|
def init_transaction(
|
||||||
|
user: User,
|
||||||
|
amount_chf_centimes: int,
|
||||||
|
redirect_url_success: str,
|
||||||
|
redirect_url_error: str,
|
||||||
|
redirect_url_cancel: str,
|
||||||
|
webhook_url: str,
|
||||||
|
):
|
||||||
|
if overwrite := settings.DATATRANS_DEBUG_WEBHOOK_OVERWRITE:
|
||||||
|
logger.warning(
|
||||||
|
"APPLYING DEBUG DATATRANS WEBHOOK OVERWRITE!",
|
||||||
|
webhook_url=overwrite,
|
||||||
)
|
)
|
||||||
|
webhook_url = overwrite
|
||||||
|
|
||||||
self._pay_endpoint = (
|
payload = {
|
||||||
"https://pay.sandbox.datatrans.com"
|
# See https://api-reference.datatrans.ch/#tag/v1transactions:
|
||||||
if not is_production_environment
|
# -> It _should_ be unique for each transaction (according to docs)
|
||||||
else "https://pay.datatrans.com"
|
# -> We use transaction id for checking transaction state
|
||||||
)
|
"refno": str(uuid.uuid4()),
|
||||||
|
# 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
|
||||||
|
# -> see CheckoutState docstring.
|
||||||
|
"autoSettle": True,
|
||||||
|
"amount": amount_chf_centimes,
|
||||||
|
"currency": "CHF",
|
||||||
|
"language": user.language,
|
||||||
|
"webhook": {"url": webhook_url},
|
||||||
|
"redirect": {
|
||||||
|
"successUrl": redirect_url_success,
|
||||||
|
"errorUrl": redirect_url_error,
|
||||||
|
"cancelUrl": redirect_url_cancel,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
self._basic_auth_key = os.getenv("DATATRANS_BASIC_AUTH_KEY")
|
logger.info("Initiating transaction", payload=payload)
|
||||||
self._hmac_key = os.getenv("DATATRANS_HMAC_KEY")
|
|
||||||
|
|
||||||
if not self._basic_auth_key or not self._hmac_key:
|
response = requests.post(
|
||||||
raise ValueError(
|
f"{settings.DATATRANS_API_ENDPOINT}/v1/transactions",
|
||||||
"Environment variables for DataTrans API are not properly set."
|
json=payload,
|
||||||
"Required: DATATRANS_BASIC_AUTH_KEY, DATATRANS_HMAC_KEY"
|
headers={
|
||||||
)
|
"Authorization": f"Basic {settings.DATATRANS_BASIC_AUTH_KEY}",
|
||||||
|
|
||||||
@property
|
|
||||||
def _headers(self):
|
|
||||||
return {
|
|
||||||
"Authorization": f"Basic {self._basic_auth_key}",
|
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def init_transaction(
|
if response.status_code == 201:
|
||||||
self,
|
transaction_id = response.json()["transactionId"]
|
||||||
user: User,
|
logger.info("Transaction initiated", transaction_id=transaction_id)
|
||||||
amount_chf_centimes: int,
|
return transaction_id
|
||||||
redirect_url_success: str,
|
else:
|
||||||
redirect_url_error: str,
|
raise PaymentException(
|
||||||
redirect_url_cancel: str,
|
"Transaction initiation failed:",
|
||||||
webhook_url: str,
|
response.json().get("error"),
|
||||||
):
|
|
||||||
if "DATATRANS_DEBUG_WEBHOOK_OVERWRITE" in os.environ:
|
|
||||||
webhook_url = os.environ["DATATRANS_DEBUG_WEBHOOK_OVERWRITE"]
|
|
||||||
logger.warning(
|
|
||||||
"Using debug webhook URL. This should only be used for testing.",
|
|
||||||
webhook_url=webhook_url,
|
|
||||||
)
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
# See https://api-reference.datatrans.ch/#tag/v1transactions:
|
|
||||||
# -> It _should_ be unique for each transaction (according to docs)
|
|
||||||
# -> We use transaction id for checking transaction state
|
|
||||||
"refno": str(uuid.uuid4()),
|
|
||||||
# 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
|
|
||||||
# -> see CheckoutState docstring.
|
|
||||||
"autoSettle": True,
|
|
||||||
"amount": amount_chf_centimes,
|
|
||||||
"currency": "CHF",
|
|
||||||
"language": user.language,
|
|
||||||
"webhook": {"url": webhook_url},
|
|
||||||
"redirect": {
|
|
||||||
"successUrl": redirect_url_success,
|
|
||||||
"errorUrl": redirect_url_error,
|
|
||||||
"cancelUrl": redirect_url_cancel,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
"Initiating transaction", payload=payload, api_endpoint=self._api_endpoint
|
|
||||||
)
|
)
|
||||||
|
|
||||||
response = requests.post(
|
|
||||||
f"{self._api_endpoint}/v1/transactions",
|
|
||||||
json=payload,
|
|
||||||
headers=self._headers,
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 201:
|
def get_transaction_state(
|
||||||
transaction_id = response.json()["transactionId"]
|
transaction_id: str,
|
||||||
logger.info("Transaction initiated", transaction_id=transaction_id)
|
) -> CheckoutState | None:
|
||||||
return transaction_id
|
response = requests.get(
|
||||||
else:
|
f"{settings.DATATRANS_API_ENDPOINT}/v1/transactions/{transaction_id}",
|
||||||
raise PaymentException(
|
headers={
|
||||||
"Transaction initiation failed:",
|
"Authorization": f"Basic {settings.DATATRANS_BASIC_AUTH_KEY}",
|
||||||
response.json().get("error"),
|
"Content-Type": "application/json",
|
||||||
)
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def get_transaction_state(
|
if response.status_code != 200:
|
||||||
self,
|
return None
|
||||||
transaction_id: str,
|
|
||||||
) -> CheckoutState | None:
|
|
||||||
response = requests.get(
|
|
||||||
f"{self._api_endpoint}/v1/transactions/{transaction_id}",
|
|
||||||
headers=self._headers,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(
|
transaction_state = response.json()["status"]
|
||||||
"Transaction status retrieved",
|
|
||||||
status_code=response.status_code,
|
|
||||||
response=response.json(),
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
logger.info(
|
||||||
return None
|
"Transaction status retrieved",
|
||||||
|
status_code=response.status_code,
|
||||||
|
response=transaction_state,
|
||||||
|
)
|
||||||
|
|
||||||
# IMPORTANT: CheckoutState docstring
|
return CheckoutState(transaction_state)
|
||||||
# for more information about the states!
|
|
||||||
transaction_state = response.json()["status"]
|
|
||||||
|
|
||||||
if transaction_state in [
|
|
||||||
"initialized",
|
|
||||||
"settled",
|
|
||||||
"canceled",
|
|
||||||
"transmitted",
|
|
||||||
"failed",
|
|
||||||
]:
|
|
||||||
return CheckoutState(transaction_state)
|
|
||||||
else:
|
|
||||||
raise PaymentException(
|
|
||||||
f"Transaction state {transaction_state} should either not happen"
|
|
||||||
"due to autoSettle=true or we forgot to handle it. -> See also CheckoutState docstring."
|
|
||||||
)
|
|
||||||
|
|
||||||
def is_webhook_request_signature_valid(self, request) -> bool:
|
def get_payment_url(transaction_id: str):
|
||||||
"""
|
return f"{settings.DATATRANS_PAY_URL}/v1/start/{transaction_id}"
|
||||||
Check if the Datatrans-Signature header is valid.
|
|
||||||
https://docs.datatrans.ch/docs/additional-security
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Datatrans-Signature: t={{timestamp}},s0={{signature}}
|
|
||||||
datatrans_signature = request.headers.get("Datatrans-Signature", "")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# received signature
|
|
||||||
parts = datatrans_signature.split(",")
|
|
||||||
timestamp = parts[0].split("=")[1]
|
|
||||||
received_signature = parts[1].split("=")[1]
|
|
||||||
|
|
||||||
# payload signature
|
|
||||||
payload = request.body
|
|
||||||
key_hex_bytes = bytes.fromhex(self._hmac_key)
|
|
||||||
calculated_signature = hmac.new(
|
|
||||||
key_hex_bytes, bytes(str(timestamp), "utf-8") + payload, hashlib.sha256
|
|
||||||
)
|
|
||||||
|
|
||||||
return calculated_signature == received_signature
|
|
||||||
|
|
||||||
except (IndexError, ValueError):
|
|
||||||
logger.warning(
|
|
||||||
"Invalid Datatrans-Signature header format",
|
|
||||||
datatrans_signature=datatrans_signature,
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_payment_url(self, transaction_id: str):
|
|
||||||
return f"{self._pay_endpoint}/v1/start/{transaction_id}"
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from vbv_lernwelt.shop.services import is_signature_valid
|
||||||
|
|
||||||
|
|
||||||
|
class DatatransWebhookSigning(TestCase):
|
||||||
|
# Key is from their example in the docs, not ours! :D
|
||||||
|
HMAC_KEY_FROM_THE_DOCS_NOT_HAZARDOUS = (
|
||||||
|
"861bbfc01e089259091927d6ad7f71c8"
|
||||||
|
"b46b7ee13499574e83c633b74cdc29e3"
|
||||||
|
"b7e262e41318c8425c520f146986675f"
|
||||||
|
"dd58a4531a01c99f06da378fdab0414a"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_signature_happy_ala_docs(self):
|
||||||
|
# GIVEN
|
||||||
|
payload = b"HELLO"
|
||||||
|
signature = "t=1605697463367,s0=82ef9a8178dcb4df0b71540fa06d7da826ecb26e1977e230bdc8c9d6f9f1af84"
|
||||||
|
|
||||||
|
# WHEN / THEN
|
||||||
|
self.assertTrue(
|
||||||
|
is_signature_valid(
|
||||||
|
hmac_key=self.HMAC_KEY_FROM_THE_DOCS_NOT_HAZARDOUS,
|
||||||
|
payload=payload,
|
||||||
|
signature=signature,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_signature_not_happy(self):
|
||||||
|
# GIVEN
|
||||||
|
tampered_payload = b"HELLO=I=TAMPERED=WITH=PAYLOAD=HIHI=I=AM=EVIL"
|
||||||
|
signature = "t=1605697463367,s0=82ef9a8178dcb4df0b71540fa06d7da826ecb26e1977e230bdc8c9d6f9f1af84"
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertFalse(
|
||||||
|
is_signature_valid(
|
||||||
|
hmac_key=self.HMAC_KEY_FROM_THE_DOCS_NOT_HAZARDOUS,
|
||||||
|
payload=tampered_payload,
|
||||||
|
signature=signature,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
@ -13,29 +13,14 @@ from vbv_lernwelt.shop.models import (
|
||||||
VV_PRODUCT_SKU,
|
VV_PRODUCT_SKU,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.shop.serializers import BillingAddressSerializer
|
from vbv_lernwelt.shop.serializers import BillingAddressSerializer
|
||||||
from vbv_lernwelt.shop.services import DataTransPaymentProvider
|
from vbv_lernwelt.shop.services import (
|
||||||
|
get_payment_url,
|
||||||
|
init_transaction,
|
||||||
|
is_signature_valid,
|
||||||
|
)
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
URL_CHECKOUT_COMPLETE = "/onboarding/vv/checkout/complete"
|
|
||||||
URL_CHECKOUT_ADDRESS = "/onboarding/vv/checkout/address"
|
|
||||||
|
|
||||||
|
|
||||||
def checkout_error_url(base_url):
|
|
||||||
return f"{base_url}/onboarding/vv/checkout/address?error=true"
|
|
||||||
|
|
||||||
|
|
||||||
def checkout_cancel_url(base_url):
|
|
||||||
return f"{base_url}/"
|
|
||||||
|
|
||||||
|
|
||||||
def checkout_success_url(base_url):
|
|
||||||
return f"{base_url}/onboarding/vv/checkout/complete"
|
|
||||||
|
|
||||||
|
|
||||||
def webhook_url(base_url):
|
|
||||||
return f"{base_url}/api/shop/transaction/webhook"
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
|
|
@ -72,23 +57,19 @@ def update_billing_address(request):
|
||||||
|
|
||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
def transaction_webhook(request):
|
def transaction_webhook(request):
|
||||||
"""Webhook endpoint for Datatrans to notify about transaction state changes."""
|
logger.info("Webhook: Datatrans called transaction webhook", body=request.body)
|
||||||
logger.info("Transaction Webhook Received", body=request.body)
|
|
||||||
|
|
||||||
datatrans = DataTransPaymentProvider()
|
if not is_signature_valid(
|
||||||
|
signature=request.headers.get("Datatrans-Signature", ""),
|
||||||
# FIXME verify signature, not working yet
|
payload=request.body,
|
||||||
# if not datatrans.is_webhook_request_signature_valid(
|
):
|
||||||
# request=request,
|
logger.warning("Invalid signature")
|
||||||
# ):
|
return JsonResponse({"status": "invalid signature"}, status=400)
|
||||||
# logger.error("Invalid webhook signature")
|
|
||||||
# return JsonResponse({"status": "error"}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
transaction = request.data
|
transaction = request.data
|
||||||
transaction_id = transaction["transactionId"]
|
transaction_id = transaction["transactionId"]
|
||||||
transaction_status = transaction["status"]
|
transaction_status = transaction["status"]
|
||||||
|
|
||||||
# FIXME just pass the checkout_id as sequence number?
|
|
||||||
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.state = transaction_status
|
||||||
checkout_info.webhook_history.append(transaction)
|
checkout_info.webhook_history.append(transaction)
|
||||||
|
|
@ -106,13 +87,12 @@ def transaction_webhook(request):
|
||||||
def checkout_vv(request):
|
def checkout_vv(request):
|
||||||
"""
|
"""
|
||||||
Checkout for the Versicherungsvermittler product (VV).
|
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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# The one and only product, thus hardcoded
|
|
||||||
# Expected to be created in the admin interface first.
|
|
||||||
sku = VV_PRODUCT_SKU
|
sku = VV_PRODUCT_SKU
|
||||||
|
logger.info(f"Checkout requested: sku={sku}", user_id=request.user.id)
|
||||||
logger.info("Checkout requested", sku=sku, user_id=request.user.id)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
product = Product.objects.get(sku=sku)
|
product = Product.objects.get(sku=sku)
|
||||||
|
|
@ -126,31 +106,21 @@ def checkout_vv(request):
|
||||||
product_sku=sku,
|
product_sku=sku,
|
||||||
)
|
)
|
||||||
|
|
||||||
datatrans = DataTransPaymentProvider()
|
|
||||||
|
|
||||||
# already purchased (settled or transmitted)
|
# already purchased (settled or transmitted)
|
||||||
if checkouts.filter(
|
if checkouts.filter(
|
||||||
state__in=[CheckoutState.SETTLED, CheckoutState.TRANSMITTED]
|
state__in=[CheckoutState.SETTLED, CheckoutState.TRANSMITTED]
|
||||||
).exists():
|
).exists():
|
||||||
return JsonResponse({"next_step_url": URL_CHECKOUT_COMPLETE})
|
return JsonResponse({"next_step_url": checkout_success_url()})
|
||||||
|
|
||||||
# FIXME: Re-using seems not to work -> just create a new one for now
|
|
||||||
# Also check if failed transactions get notified via webhook
|
|
||||||
# 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(
|
return JsonResponse(
|
||||||
# {
|
{"next_step_url": get_payment_url(transaction_id=checkout.transaction_id)}
|
||||||
# "next_step_url": datatrans.get_payment_url(
|
)
|
||||||
# transaction_id=checkout.transaction_id
|
|
||||||
# )
|
|
||||||
# }
|
|
||||||
# )
|
|
||||||
|
|
||||||
address = request.data["address"]
|
# not yet initialized at all, or canceled/failed
|
||||||
base_redirect_url = request.data["redirect_url"]
|
base_redirect_url = request.data["redirect_url"]
|
||||||
|
transaction_id = init_transaction(
|
||||||
# not yet initialized at all or canceled/failed
|
|
||||||
transaction_id = datatrans.init_transaction(
|
|
||||||
user=request.user,
|
user=request.user,
|
||||||
amount_chf_centimes=product.price,
|
amount_chf_centimes=product.price,
|
||||||
redirect_url_success=checkout_success_url(base_redirect_url),
|
redirect_url_success=checkout_success_url(base_redirect_url),
|
||||||
|
|
@ -169,9 +139,26 @@ def checkout_vv(request):
|
||||||
product_price=product.price,
|
product_price=product.price,
|
||||||
product_name=product.name,
|
product_name=product.name,
|
||||||
product_description=product.description,
|
product_description=product.description,
|
||||||
**address,
|
# address
|
||||||
|
**request.data["address"],
|
||||||
)
|
)
|
||||||
|
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
{"next_step_url": datatrans.get_payment_url(transaction_id=transaction_id)}
|
{"next_step_url": get_payment_url(transaction_id=transaction_id)}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def webhook_url(base_url: str) -> str:
|
||||||
|
return f"{base_url}/api/shop/transaction/webhook"
|
||||||
|
|
||||||
|
|
||||||
|
def checkout_error_url(base_url: str) -> str:
|
||||||
|
return f"{base_url}/onboarding/vv/checkout/address?error=true"
|
||||||
|
|
||||||
|
|
||||||
|
def checkout_cancel_url(base_url: str) -> str:
|
||||||
|
return f"{base_url}/"
|
||||||
|
|
||||||
|
|
||||||
|
def checkout_success_url(base_url: str = "") -> str:
|
||||||
|
return f"{base_url}/onboarding/vv/checkout/complete"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue