Handle Stripe webhook events
This commit is contained in:
parent
9cddffb7f0
commit
76b5259f8d
|
@ -16,6 +16,7 @@ REDIS_PASSWORD=password
|
|||
|
||||
ENABLE_CREDIT=false
|
||||
STRIPE_API_KEY=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
|
||||
# What environment things should be logged under: local dev or prod
|
||||
APP_ENV=local
|
||||
|
|
|
@ -15,6 +15,7 @@ from backend.data.user import get_user_by_id
|
|||
from backend.util.settings import Settings
|
||||
|
||||
settings = Settings()
|
||||
stripe.api_key = settings.secrets.stripe_api_key
|
||||
|
||||
|
||||
class UserCreditBase(ABC):
|
||||
|
@ -66,7 +67,7 @@ class UserCreditBase(ABC):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def top_up_intent(self, user_id: str, amount: int) -> RedirectResponse:
|
||||
async def top_up_intent(self, user_id: str, amount: int) -> str:
|
||||
"""
|
||||
Create a payment intent to top up the credits for the user.
|
||||
|
||||
|
@ -75,7 +76,17 @@ class UserCreditBase(ABC):
|
|||
amount (int): The amount to top up.
|
||||
|
||||
Returns:
|
||||
RedirectResponse: The redirect response to the payment page.
|
||||
str: The redirect url to the payment page.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def fulfill_checkout(self, session_id):
|
||||
"""
|
||||
Fulfill the Stripe checkout session.
|
||||
|
||||
Args:
|
||||
session_id (str): The checkout session ID.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
@ -83,7 +94,6 @@ class UserCreditBase(ABC):
|
|||
class UserCredit(UserCreditBase):
|
||||
def __init__(self):
|
||||
self.num_user_credits_refill = settings.config.num_user_credits_refill
|
||||
stripe.api_key = settings.secrets.stripe_api_key
|
||||
|
||||
async def get_or_refill_credit(self, user_id: str) -> int:
|
||||
cur_time = self.time_now()
|
||||
|
@ -229,7 +239,7 @@ class UserCredit(UserCreditBase):
|
|||
}
|
||||
)
|
||||
|
||||
async def top_up_intent(self, user_id: str, amount: int) -> RedirectResponse:
|
||||
async def top_up_intent(self, user_id: str, amount: int) -> str:
|
||||
user = await get_user_by_id(user_id)
|
||||
|
||||
if not user:
|
||||
|
@ -268,7 +278,7 @@ class UserCredit(UserCreditBase):
|
|||
# Create pending transaction
|
||||
await CreditTransaction.prisma().create(
|
||||
data={
|
||||
"transactionKey": checkout_session.id,# TODO kcze add new model field?
|
||||
"transactionKey": checkout_session.id, # TODO kcze add new model field?
|
||||
"userId": user_id,
|
||||
"amount": amount,
|
||||
"type": CreditTransactionType.TOP_UP,
|
||||
|
@ -277,10 +287,11 @@ class UserCredit(UserCreditBase):
|
|||
}
|
||||
)
|
||||
|
||||
return RedirectResponse(checkout_session.url or "", 303)
|
||||
|
||||
return checkout_session.url or ""
|
||||
|
||||
# https://docs.stripe.com/checkout/fulfillment
|
||||
async def fulfill_checkout(self, session_id):
|
||||
print("fulfill_checkout", session_id)
|
||||
# Retrieve CreditTransaction
|
||||
credit_transaction = await CreditTransaction.prisma().find_first_or_raise(
|
||||
where={"transactionKey": session_id}
|
||||
|
@ -295,10 +306,16 @@ class UserCredit(UserCreditBase):
|
|||
|
||||
# Check the Checkout Session's payment_status property
|
||||
# to determine if fulfillment should be peformed
|
||||
if checkout_session.payment_status != 'unpaid':
|
||||
if checkout_session.payment_status != "unpaid":
|
||||
print("Payment status is not unpaid!")
|
||||
# Activate the CreditTransaction
|
||||
await CreditTransaction.prisma().update(
|
||||
where={"transactionKey": session_id},
|
||||
where={
|
||||
"creditTransactionIdentifier": {
|
||||
"transactionKey": session_id,
|
||||
"userId": credit_transaction.userId,
|
||||
}
|
||||
},
|
||||
data={
|
||||
"isActive": True,
|
||||
"createdAt": self.time_now(),
|
||||
|
@ -317,11 +334,15 @@ class DisabledUserCredit(UserCreditBase):
|
|||
async def top_up_credits(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
async def top_up_intent(self, *args, **kwargs) -> RedirectResponse:
|
||||
return RedirectResponse("")
|
||||
async def top_up_intent(self, *args, **kwargs) -> str:
|
||||
return ""
|
||||
|
||||
async def fulfill_checkout(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
def get_user_credit_model() -> UserCreditBase:
|
||||
# return UserCredit()
|
||||
if settings.config.enable_credit.lower() == "true":
|
||||
return UserCredit()
|
||||
else:
|
||||
|
|
|
@ -3,11 +3,13 @@ import logging
|
|||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Annotated, Any, Sequence
|
||||
|
||||
from fastapi.responses import RedirectResponse
|
||||
import pydantic
|
||||
import stripe
|
||||
from autogpt_libs.auth.middleware import auth_middleware
|
||||
from autogpt_libs.feature_flag.client import feature_flag
|
||||
from autogpt_libs.utils.cache import thread_cached
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||
from typing_extensions import Optional, TypedDict
|
||||
|
||||
import backend.data.block
|
||||
|
@ -138,11 +140,41 @@ async def get_user_credits(
|
|||
return {"credits": max(await _user_credit_model.get_or_refill_credit(user_id), 0)}
|
||||
|
||||
|
||||
@v1_router.post(path="/credits", dependencies=[Depends(auth_middleware)])
|
||||
@v1_router.post(path="/credits", tags=["credits"], dependencies=[Depends(auth_middleware)])
|
||||
async def request_top_up(
|
||||
user_id: Annotated[str, Depends(get_user_id)], request: RequestTopUp
|
||||
request: RequestTopUp, user_id: Annotated[str, Depends(get_user_id)]
|
||||
):
|
||||
return _user_credit_model.top_up_intent(user_id, request.amount)
|
||||
checkout_url = await _user_credit_model.top_up_intent(user_id, request.amount)
|
||||
return {"checkout_url": checkout_url}
|
||||
|
||||
|
||||
@v1_router.post(path="/credits/stripe_webhook", tags=["credits"])
|
||||
async def stripe_webhook(request: Request):
|
||||
# Get the raw request body
|
||||
payload = await request.body()
|
||||
# Get the signature header
|
||||
sig_header = request.headers.get('stripe-signature')
|
||||
|
||||
try:
|
||||
event = stripe.Webhook.construct_event(
|
||||
payload, sig_header, settings.secrets.stripe_webhook_secret
|
||||
)
|
||||
except ValueError:
|
||||
# Invalid payload
|
||||
raise HTTPException(status_code=400)
|
||||
except stripe.SignatureVerificationError:
|
||||
# Invalid signature
|
||||
raise HTTPException(status_code=400)
|
||||
|
||||
print(event)
|
||||
|
||||
if (
|
||||
event['type'] == 'checkout.session.completed'
|
||||
or event['type'] == 'checkout.session.async_payment_succeeded'
|
||||
):
|
||||
await _user_credit_model.fulfill_checkout(event['data']['object']['id'])
|
||||
|
||||
return Response(status_code=200)
|
||||
|
||||
|
||||
########################################################
|
||||
|
|
|
@ -298,6 +298,7 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
|
|||
fal_key: str = Field(default="", description="FAL API key")
|
||||
|
||||
stripe_api_key: str = Field(default="", description="Stripe API Key")
|
||||
stripe_webhook_secret: str = Field(default="", description="Stripe Webhook Secret")
|
||||
|
||||
# Add more secret fields as needed
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aio-pika"
|
||||
|
@ -4339,4 +4339,4 @@ type = ["pytest-mypy"]
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.10,<3.13"
|
||||
content-hash = "180e010fe51c228a973a76984523deedda9ebc9b6059ec4a5c5adbe76ce4509e"
|
||||
content-hash = "1f7b98944844245f1574339d622e8f1ecddf08aa1e9efa84682fd51ab0556a75"
|
||||
|
|
|
@ -10,7 +10,7 @@ from backend.integrations.credentials_store import openai_credentials
|
|||
from backend.util.test import SpinTestServer
|
||||
|
||||
REFILL_VALUE = 1000
|
||||
user_credit = UserCredit(REFILL_VALUE)
|
||||
user_credit = UserCredit()
|
||||
|
||||
|
||||
@pytest.mark.asyncio(scope="session")
|
||||
|
|
|
@ -85,7 +85,7 @@ export default class BackendAPI {
|
|||
}
|
||||
}
|
||||
|
||||
requestTopUp(amount: number) {
|
||||
requestTopUp(amount: number): Promise<{ checkout_url: string }> {
|
||||
return this._request("POST", "/credits", { amount });
|
||||
}
|
||||
|
||||
|
@ -446,7 +446,7 @@ export default class BackendAPI {
|
|||
await new Promise((resolve) => setTimeout(resolve, 100 * retryCount));
|
||||
}
|
||||
}
|
||||
console.log("Request: ", method, path, "from: ", page);
|
||||
console.log("Request: ", method, this.baseUrl, path, "from: ", page);
|
||||
if (token === "no-token-found") {
|
||||
console.warn(
|
||||
"No auth token found after retries. This may indicate a session sync issue between client and server.",
|
||||
|
|
Loading…
Reference in New Issue