Skip to content

Commit

Permalink
Handle Stripe webhook events
Browse files Browse the repository at this point in the history
  • Loading branch information
kcze committed Dec 28, 2024
1 parent 9cddffb commit 76b5259
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 20 deletions.
1 change: 1 addition & 0 deletions autogpt_platform/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 32 additions & 11 deletions autogpt_platform/backend/backend/data/credit.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from backend.util.settings import Settings

settings = Settings()
stripe.api_key = settings.secrets.stripe_api_key


class UserCreditBase(ABC):
Expand Down Expand Up @@ -66,7 +67,7 @@ async def top_up_credits(self, user_id: str, amount: int):
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.
Expand All @@ -75,15 +76,24 @@ async def top_up_intent(self, user_id: str, amount: int) -> RedirectResponse:
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


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()
Expand Down Expand Up @@ -229,7 +239,7 @@ async def top_up_credits(self, user_id: str, amount: int):
}
)

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:
Expand Down Expand Up @@ -268,7 +278,7 @@ async def top_up_intent(self, user_id: str, amount: int) -> RedirectResponse:
# 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,
Expand All @@ -277,10 +287,11 @@ async def top_up_intent(self, user_id: str, amount: int) -> RedirectResponse:
}
)

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}
Expand All @@ -295,10 +306,16 @@ async def fulfill_checkout(self, session_id):

# 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(),
Expand All @@ -317,11 +334,15 @@ async def spend_credits(self, *args, **kwargs) -> int:
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:
Expand Down
40 changes: 36 additions & 4 deletions autogpt_platform/backend/backend/server/routers/v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
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
Expand Down Expand Up @@ -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)


########################################################
Expand Down
1 change: 1 addition & 0 deletions autogpt_platform/backend/backend/util/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions autogpt_platform/backend/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion autogpt_platform/backend/test/data/test_credit.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from backend.util.test import SpinTestServer

REFILL_VALUE = 1000
user_credit = UserCredit(REFILL_VALUE)
user_credit = UserCredit()


@pytest.mark.asyncio(scope="session")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export default class BackendAPI {
}
}

requestTopUp(amount: number) {
requestTopUp(amount: number): Promise<{ checkout_url: string }> {
return this._request("POST", "/credits", { amount });
}

Expand Down Expand Up @@ -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.",
Expand Down

0 comments on commit 76b5259

Please sign in to comment.