Client-side `/credits` PATCH

This commit is contained in:
Krzysztof Czerwinski 2025-01-03 15:43:38 +01:00
parent 2caf498b2e
commit deddcddb62
4 changed files with 42 additions and 8 deletions

View File

@ -80,12 +80,13 @@ class UserCreditBase(ABC):
pass
@abstractmethod
async def fulfill_checkout(self, session_id):
async def fulfill_checkout(self, *, session_id: str | None = None, user_id: str | None = None):
"""
Fulfill the Stripe checkout session.
Args:
session_id (str): The checkout session ID.
session_id (str | None): The checkout session ID. Will try to fulfill most recent if None.
user_id (str | None): The user ID must be provided if session_id is None.
"""
pass
@ -296,10 +297,21 @@ class UserCredit(UserCreditBase):
return checkout_session.url or ""
# https://docs.stripe.com/checkout/fulfillment
async def fulfill_checkout(self, session_id):
async def fulfill_checkout(self, *, session_id: str | None = None, user_id: str | None = None):
if (not session_id and not user_id) or (session_id and user_id):
raise ValueError("Either session_id or user_id must be provided")
# Retrieve CreditTransaction
credit_transaction = await CreditTransaction.prisma().find_first_or_raise(
where={"transactionKey": session_id}
where={
"OR": [
{"transactionKey": session_id} if session_id is not None else {"transactionKey": ""},
{"userId": user_id} if user_id is not None else {"userId": ""}
]
},
order={
"createdAt": "desc"
}
)
# This can be called multiple times for one id, so ignore if already fulfilled
@ -307,7 +319,7 @@ class UserCredit(UserCreditBase):
return
# Retrieve the Checkout Session from the API
checkout_session = stripe.checkout.Session.retrieve(session_id)
checkout_session = stripe.checkout.Session.retrieve(credit_transaction.transactionKey)
# Check the Checkout Session's payment_status property
# to determine if fulfillment should be peformed
@ -316,7 +328,7 @@ class UserCredit(UserCreditBase):
await CreditTransaction.prisma().update(
where={
"creditTransactionIdentifier": {
"transactionKey": session_id,
"transactionKey": credit_transaction.transactionKey,
"userId": credit_transaction.userId,
}
},

View File

@ -149,6 +149,14 @@ async def request_top_up(
return {"checkout_url": checkout_url}
@v1_router.patch(
path="/credits", tags=["credits"], dependencies=[Depends(auth_middleware)]
)
async def fulfill_checkout(user_id: Annotated[str, Depends(get_user_id)]):
await _user_credit_model.fulfill_checkout(user_id=user_id)
return Response(status_code=200)
@v1_router.post(path="/credits/stripe_webhook", tags=["credits"])
async def stripe_webhook(request: Request):
# Get the raw request body
@ -171,7 +179,7 @@ async def stripe_webhook(request: Request):
event["type"] == "checkout.session.completed"
or event["type"] == "checkout.session.async_payment_succeeded"
):
await _user_credit_model.fulfill_checkout(event["data"]["object"]["id"])
await _user_credit_model.fulfill_checkout(session_id=event["data"]["object"]["id"])
return Response(status_code=200)

View File

@ -1,14 +1,24 @@
"use client";
import { Button } from "@/components/agptui/Button";
import useCredits from "@/hooks/useCredits";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { useSearchParams } from "next/navigation";
import { useState } from "react";
import { useEffect, useState } from "react";
export default function CreditsPage() {
const { credits, requestTopUp } = useCredits();
const [amount, setAmount] = useState(5);
const [patched, setPatched] = useState(false);
const searchParams = useSearchParams();
const topupStatus = searchParams.get("topup");
const api = useBackendAPI();
useEffect(() => {
if (!patched && topupStatus === "success") {
api.fulfillCheckout();
setPatched(true);
}
}, [topupStatus]);
return (
<div className="w-full min-w-[800px] px-4 sm:px-8">

View File

@ -89,6 +89,10 @@ export default class BackendAPI {
return this._request("POST", "/credits", { amount });
}
fulfillCheckout(): Promise<void> {
return this._request("PATCH", "/credits");
}
getBlocks(): Promise<Block[]> {
return this._get("/blocks");
}