From 350c6994738480665eac0e3d6bfb510a059148f4 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Tue, 31 Mar 2026 01:12:25 +0200
Subject: [PATCH] fexed stripe webhook
---
modules/interfaces/interfaceDbBilling.py | 19 +++++++++++++
modules/routes/routeSubscription.py | 34 +++++++++++++++---------
2 files changed, 41 insertions(+), 12 deletions(-)
diff --git a/modules/interfaces/interfaceDbBilling.py b/modules/interfaces/interfaceDbBilling.py
index 948f8918..d8c052c9 100644
--- a/modules/interfaces/interfaceDbBilling.py
+++ b/modules/interfaces/interfaceDbBilling.py
@@ -994,6 +994,25 @@ class BillingObjects:
)
return created
+ def ensureActivationBudget(self, mandateId: str, planKey: str) -> Optional[Dict[str, Any]]:
+ """Idempotent: credit the activation budget only if no SUBSCRIPTION credit exists yet."""
+ poolAccount = self.getMandateAccount(mandateId)
+ if not poolAccount:
+ return self.creditSubscriptionBudget(mandateId, planKey, periodLabel="Erstaktivierung")
+
+ existing = self.db.getRecordset(
+ BillingTransaction,
+ recordFilter={
+ "accountId": poolAccount["id"],
+ "transactionType": TransactionTypeEnum.CREDIT.value,
+ "referenceType": ReferenceTypeEnum.SUBSCRIPTION.value,
+ },
+ )
+ if existing:
+ return None
+
+ return self.creditSubscriptionBudget(mandateId, planKey, periodLabel="Erstaktivierung")
+
# =========================================================================
# Workflow Cost Query
# =========================================================================
diff --git a/modules/routes/routeSubscription.py b/modules/routes/routeSubscription.py
index 3193292c..9f1f0bf8 100644
--- a/modules/routes/routeSubscription.py
+++ b/modules/routes/routeSubscription.py
@@ -273,10 +273,8 @@ def verifyCheckout(
):
"""Verify a Stripe Checkout Session and activate the subscription if paid.
- This is the synchronous counterpart to the checkout.session.completed webhook.
- It's called by the frontend immediately after returning from Stripe to handle
- environments where webhooks may be delayed or unavailable (e.g. localhost dev).
- The logic is idempotent — if the webhook already processed the session, this is a no-op.
+ Idempotent: if the webhook already processed the session, returns success.
+ Called by the frontend immediately after returning from Stripe.
"""
mandateId = _resolveMandateId(context)
if not mandateId:
@@ -294,7 +292,6 @@ def verifyCheckout(
payStatus = session.get("payment_status")
if session.get("status") != "complete":
return {"status": "pending", "message": "Checkout not yet completed"}
- # Subscription checkouts with trial / $0 first period use no_payment_required, not paid.
if payStatus not in ("paid", "no_payment_required"):
return {"status": "pending", "message": "Checkout not yet completed"}
@@ -306,18 +303,31 @@ def verifyCheckout(
try:
_handleSubscriptionCheckoutCompleted(session, f"verify-{data.sessionId}")
except Exception as e:
- logger.exception(
- "verifyCheckout: handler failed for session %s mandate %s: %s",
+ logger.warning(
+ "verifyCheckout: handler raised for session %s mandate %s: %s",
data.sessionId,
mandateId,
e,
)
- raise HTTPException(
- status_code=500,
- detail="Subscription-Aktivierung nach Checkout fehlgeschlagen. Bitte erneut versuchen oder Support informieren.",
- ) from e
- return {"status": "activated", "message": "Subscription activated"}
+ from modules.serviceCenter.services.serviceSubscription.mainServiceSubscription import (
+ getService as getSubscriptionService,
+ )
+ from modules.datamodels.datamodelSubscription import OPERATIVE_STATUSES
+
+ subService = getSubscriptionService(context.user, mandateId)
+ operative = subService.getOperativeSubscription(mandateId)
+ if operative and operative.get("status") in [s.value for s in OPERATIVE_STATUSES]:
+ planKey = operative.get("planKey", "")
+ if planKey:
+ try:
+ from modules.interfaces.interfaceDbBilling import _getRootInterface as _getBillingRoot
+ _getBillingRoot().ensureActivationBudget(mandateId, planKey)
+ except Exception as ex:
+ logger.warning("verifyCheckout: ensureActivationBudget failed: %s", ex)
+ return {"status": "activated", "message": "Subscription activated"}
+
+ return {"status": "pending", "message": "Subscription activation pending — webhook may still be processing."}
# =============================================================================