Top-Up intent logic
This commit is contained in:
parent
2481d3d88f
commit
1596671ad5
|
@ -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"
|
||||
|
|
|
@ -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]]:
|
||||
|
|
|
@ -60,3 +60,7 @@ class UpdatePermissionsRequest(pydantic.BaseModel):
|
|||
|
||||
class RequestTopUp(pydantic.BaseModel):
|
||||
amount: int
|
||||
|
||||
class RequestTopUpResponse(pydantic.BaseModel):
|
||||
transaction_id: str
|
||||
client_secret: str
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
########################################################
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue