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."} # =============================================================================