Top-Up intent logic

This commit is contained in:
Krzysztof Czerwinski 2024-12-13 10:55:23 +01:00
parent 2481d3d88f
commit 1596671ad5
11 changed files with 106 additions and 19 deletions

View File

@ -15,6 +15,8 @@ REDIS_PORT=6379
REDIS_PASSWORD=password
ENABLE_CREDIT=false
STRIPE_API_KEY=
# What environment things should be logged under: local dev or prod
APP_ENV=local
# What environment to behave as: "local" or "cloud"

View File

@ -4,20 +4,20 @@ from datetime import datetime, timezone
from prisma import Json
from prisma.enums import CreditTransactionType
from prisma.errors import UniqueViolationError
from prisma.models import CreditTransaction
from prisma.models import CreditTransaction, User
import stripe
from backend.data.block import Block, BlockInput, get_block
from backend.data.block_cost_config import BLOCK_COSTS
from backend.data.cost import BlockCost, BlockCostType
from backend.util.settings import Config
from backend.data.user import get_user_by_id
from backend.server.model import RequestTopUpResponse
from backend.util.settings import Settings
config = Config()
settings = Settings()
class UserCreditBase(ABC):
def __init__(self, num_user_credits_refill: int):
self.num_user_credits_refill = num_user_credits_refill
@abstractmethod
async def get_or_refill_credit(self, user_id: str) -> int:
"""
@ -65,8 +65,26 @@ class UserCreditBase(ABC):
"""
pass
@abstractmethod
async def top_up_intent(self, user_id: str, amount: int) -> RequestTopUpResponse:
"""
Create a payment intent to top up the credits for the user.
Args:
user_id (str): The user ID.
amount (int): The amount to top up.
Returns:
RequestTopUpResponse: The response containing the transaction ID and client secret.
"""
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()
cur_month = cur_time.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
@ -211,6 +229,43 @@ class UserCredit(UserCreditBase):
}
)
async def top_up_intent(self, user_id: str, amount: int) -> RequestTopUpResponse:
user = await get_user_by_id(user_id)
if not user:
raise ValueError(f"User not found: {user_id}")
# Create customer if not exists
if not user.stripeCustomerId:
customer = stripe.Customer.create(name=user.name or "", email=user.email)
await User.prisma().update(
where={"id": user_id}, data={"stripeCustomerId": customer.id}
)
user.stripeCustomerId = customer.id
# Create payment intent
# amount param is always in the smallest currency unit (so cents for usd)
# https://docs.stripe.com/api/payment_intents/create
intent = stripe.PaymentIntent.create(
amount=amount * 100,
currency="usd",
customer=user.stripeCustomerId,
)
# Create pending transaction
await CreditTransaction.prisma().create(
data={
"transactionKey": intent.id,
"userId": user_id,
"amount": amount,
"type": CreditTransactionType.TOP_UP,
"isActive": False,
"metadata": Json({"paymentIntent": intent}),
}
)
return RequestTopUpResponse(transaction_id=intent.id, client_secret=intent.client_secret or "")
class DisabledUserCredit(UserCreditBase):
async def get_or_refill_credit(self, *args, **kwargs) -> int:
@ -222,12 +277,15 @@ class DisabledUserCredit(UserCreditBase):
async def top_up_credits(self, *args, **kwargs):
pass
async def top_up_intent(self, *args, **kwargs) -> RequestTopUpResponse:
return RequestTopUpResponse(transaction_id="", client_secret="")
def get_user_credit_model() -> UserCreditBase:
if config.enable_credit.lower() == "true":
return UserCredit(config.num_user_credits_refill)
if settings.config.enable_credit.lower() == "true":
return UserCredit()
else:
return DisabledUserCredit(0)
return DisabledUserCredit()
def get_block_costs() -> dict[str, list[BlockCost]]:

View File

@ -60,3 +60,7 @@ class UpdatePermissionsRequest(pydantic.BaseModel):
class RequestTopUp(pydantic.BaseModel):
amount: int
class RequestTopUpResponse(pydantic.BaseModel):
transaction_id: str
client_secret: str

View File

@ -143,7 +143,7 @@ async def get_user_credits(
async def request_top_up(
user_id: Annotated[str, Depends(get_user_id)], request: RequestTopUp
):
pass
return _user_credit_model.top_up_intent(user_id, request.amount)
########################################################

View File

@ -292,6 +292,8 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
fal_key: str = Field(default="", description="FAL API key")
stripe_api_key: str = Field(default="", description="Stripe API Key")
# Add more secret fields as needed
model_config = SettingsConfigDict(

View File

@ -3350,6 +3350,21 @@ docs = ["myst-parser[linkify]", "sphinx", "sphinx-rtd-theme"]
release = ["twine"]
test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"]
[[package]]
name = "stripe"
version = "11.3.0"
description = "Python bindings for the Stripe API"
optional = false
python-versions = ">=3.6"
files = [
{file = "stripe-11.3.0-py2.py3-none-any.whl", hash = "sha256:9d2e86943e1e4f325835d3860c4f58aa98d49229c9caf67588f9f9b2451e8e56"},
{file = "stripe-11.3.0.tar.gz", hash = "sha256:98e625d9ddbabcecf02666867169696e113d9eaba27979fb310a7a8dfd44097c"},
]
[package.dependencies]
requests = {version = ">=2.20", markers = "python_version >= \"3.0\""}
typing-extensions = {version = ">=4.5.0", markers = "python_version >= \"3.7\""}
[[package]]
name = "supabase"
version = "2.10.0"
@ -4064,4 +4079,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "94dbe280c8215cd4ceef47c14b681092cb03964aa081b6cdb978da7bbf818593"
content-hash = "784cf3114ccba9472e7f28816faa95de5b555b06b94bd90fe41e79993290501b"

View File

@ -40,6 +40,7 @@ python-dotenv = "^1.0.1"
redis = "^5.2.0"
sentry-sdk = "2.19.0"
strenum = "^0.4.9"
stripe = "^11.3.0"
supabase = "^2.10.0"
tenacity = "^9.0.0"
uvicorn = { extras = ["standard"], version = "^0.32.1" }

View File

@ -40,7 +40,7 @@ export default function PrivatePage() {
const router = useRouter();
const providers = useContext(CredentialsProvidersContext);
const { toast } = useToast();
const { credits } = useCredits();
const { credits, requestTopUp } = useCredits();
const [amount, setAmount] = useState(5);
const [confirmationDialogState, setConfirmationDialogState] = useState<
@ -119,10 +119,6 @@ export default function PrivatePage() {
[],
);
const handleTopUp = useCallback(() => {
}, []);
if (isLoading || !providers) {
return (
<div className="flex h-[80vh] items-center justify-center">
@ -160,7 +156,7 @@ export default function PrivatePage() {
</Button>
</div>
<Separator className="my-6" />
<h2 className="mb-4 text-lg">Top-up Credits</h2>
<h2 className="mb-4 text-lg">Top-Up Credits</h2>
<div className="w-full max-w-md space-y-4">
<div className="text-md">
Current credits: <b>{credits}</b> <br />1 USD = 100 credits, 5 USD is
@ -176,7 +172,7 @@ export default function PrivatePage() {
min="5"
step="1"
/>
<Button onClick={handleTopUp} className="whitespace-nowrap">
<Button onClick={() => requestTopUp(amount)} className="whitespace-nowrap">
Top-Up
</Button>
</div>

View File

@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
export default function useCredits(): {
credits: number | null;
fetchCredits: () => void;
requestTopUp: (amount: number) => Promise<void>;
} {
const [credits, setCredits] = useState<number | null>(null);
const api = useMemo(() => new AutoGPTServerAPI(), []);
@ -20,5 +21,6 @@ export default function useCredits(): {
return {
credits,
fetchCredits,
requestTopUp: api.requestTopUp,
};
}

View File

@ -18,6 +18,7 @@ import {
Schedule,
ScheduleCreatable,
User,
RequestTopUpResponse,
} from "./types";
export default class BaseAutoGPTServerAPI {
@ -60,7 +61,7 @@ export default class BaseAutoGPTServerAPI {
return this._get(`/credits`);
}
requestTopUp(amount: number): Promise<void> {
requestTopUp(amount: number): Promise<RequestTopUpResponse> {
return this._request("POST", "/credits", { amount });
}

View File

@ -368,3 +368,9 @@ export type ScheduleCreatable = {
graph_id: string;
input_data: { [key: string]: any };
};
/* Mirror of backend/server/model.py:RequestTopUpResponse */
export type RequestTopUpResponse = {
transaction_id: string;
client_secret: string;
};