fexed stripe webhook

This commit is contained in:
ValueOn AG 2026-03-31 01:12:25 +02:00
parent b53a7f363d
commit 350c699473
2 changed files with 41 additions and 12 deletions

View file

@ -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
# =========================================================================

View file

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