feat: add user choosen custom prices

This commit is contained in:
swve 2024-10-31 16:12:32 +01:00
parent bf38fee68b
commit 3f96f1ec9f
3 changed files with 84 additions and 47 deletions

View file

@ -7,10 +7,15 @@ class PaymentProductTypeEnum(str, Enum):
SUBSCRIPTION = "subscription" SUBSCRIPTION = "subscription"
ONE_TIME = "one_time" ONE_TIME = "one_time"
class PaymentPriceTypeEnum(str, Enum):
CUSTOMER_CHOICE = "customer_choice"
FIXED_PRICE = "fixed_price"
class PaymentsProductBase(SQLModel): class PaymentsProductBase(SQLModel):
name: str = "" name: str = ""
description: Optional[str] = "" description: Optional[str] = ""
product_type: PaymentProductTypeEnum = PaymentProductTypeEnum.ONE_TIME product_type: PaymentProductTypeEnum = PaymentProductTypeEnum.ONE_TIME
price_type: PaymentPriceTypeEnum = PaymentPriceTypeEnum.FIXED_PRICE
benefits: str = "" benefits: str = ""
amount: float = 0.0 amount: float = 0.0
currency: str = "USD" currency: str = "USD"

View file

@ -1,7 +1,7 @@
from fastapi import HTTPException, Request from fastapi import HTTPException, Request
from sqlmodel import Session from sqlmodel import Session
import stripe import stripe
from src.db.payments.payments_products import PaymentProductTypeEnum, PaymentsProduct from src.db.payments.payments_products import PaymentPriceTypeEnum, PaymentProductTypeEnum, PaymentsProduct
from src.db.users import AnonymousUser, PublicUser from src.db.users import AnonymousUser, PublicUser
from src.services.payments.payments import get_payments_config from src.services.payments.payments import get_payments_config
@ -43,22 +43,23 @@ async def create_stripe_product(
# Set the Stripe API key using the credentials # Set the Stripe API key using the credentials
stripe.api_key = creds.get('stripe_secret_key') stripe.api_key = creds.get('stripe_secret_key')
## Create product # Prepare default_price_data based on price_type
if product_data.price_type == PaymentPriceTypeEnum.CUSTOMER_CHOICE:
# Interval or one time default_price_data = {
if product_data.product_type == PaymentProductTypeEnum.SUBSCRIPTION: "currency": product_data.currency,
interval = "month" "custom_unit_amount": {
"enabled": True,
"minimum": int(product_data.amount * 100), # Convert to cents
}
}
else: else:
interval = None default_price_data = {
"currency": product_data.currency,
# Prepare default_price_data "unit_amount": int(product_data.amount * 100) # Convert to cents
default_price_data = { }
"currency": product_data.currency,
"unit_amount": int(product_data.amount * 100) # Convert to cents if product_data.product_type == PaymentProductTypeEnum.SUBSCRIPTION:
} default_price_data["recurring"] = {"interval": "month"}
if interval:
default_price_data["recurring"] = {"interval": interval}
product = stripe.Product.create( product = stripe.Product.create(
name=product_data.name, name=product_data.name,
@ -104,13 +105,22 @@ async def update_stripe_product(
stripe.api_key = creds.get('stripe_secret_key') stripe.api_key = creds.get('stripe_secret_key')
try: try:
# Create new price based on price_type
# Always create a new price if product_data.price_type == PaymentPriceTypeEnum.CUSTOMER_CHOICE:
new_price_data = { new_price_data = {
"currency": product_data.currency, "currency": product_data.currency,
"unit_amount": int(product_data.amount * 100), # Convert to cents "product": product_id,
"product": product_id, "custom_unit_amount": {
} "enabled": True,
"minimum": int(product_data.amount * 100), # Convert to cents
}
}
else:
new_price_data = {
"currency": product_data.currency,
"unit_amount": int(product_data.amount * 100), # Convert to cents
"product": product_id,
}
if product_data.product_type == PaymentProductTypeEnum.SUBSCRIPTION: if product_data.product_type == PaymentProductTypeEnum.SUBSCRIPTION:
new_price_data["recurring"] = {"interval": "month"} new_price_data["recurring"] = {"interval": "month"}
@ -128,16 +138,12 @@ async def update_stripe_product(
# Update the product in Stripe # Update the product in Stripe
updated_product = stripe.Product.modify(product_id, **update_data) updated_product = stripe.Product.modify(product_id, **update_data)
# Archive all existing prices for the product # Archive all existing prices for the product
existing_prices = stripe.Price.list(product=product_id, active=True) existing_prices = stripe.Price.list(product=product_id, active=True)
for price in existing_prices: for price in existing_prices:
if price.id != new_price.id: if price.id != new_price.id:
stripe.Price.modify(price.id, active=False) stripe.Price.modify(price.id, active=False)
# Set the new price as the default price for the product
updated_product = stripe.Product.modify(product_id, default_price=new_price.id)
return updated_product return updated_product
except stripe.StripeError as e: except stripe.StripeError as e:

View file

@ -16,16 +16,20 @@ import currencyCodes from 'currency-codes';
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
name: Yup.string().required('Name is required'), name: Yup.string().required('Name is required'),
description: Yup.string().required('Description is required'), description: Yup.string().required('Description is required'),
amount: Yup.number().min(0, 'Amount must be positive').required('Amount is required'), amount: Yup.number()
.min(1, 'Amount must be greater than zero')
.required('Amount is required'),
benefits: Yup.string(), benefits: Yup.string(),
currency: Yup.string().required('Currency is required'), currency: Yup.string().required('Currency is required'),
product_type: Yup.string().oneOf(['one_time', 'subscription']).required('Product type is required'), product_type: Yup.string().oneOf(['one_time', 'subscription']).required('Product type is required'),
price_type: Yup.string().oneOf(['fixed_price', 'customer_choice']).required('Price type is required'),
}); });
interface ProductFormValues { interface ProductFormValues {
name: string; name: string;
description: string; description: string;
product_type: 'one_time' | 'subscription'; product_type: 'one_time' | 'subscription';
price_type: 'fixed_price' | 'customer_choice';
benefits: string; benefits: string;
amount: number; amount: number;
currency: string; currency: string;
@ -48,6 +52,7 @@ const CreateProductForm: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) =
name: '', name: '',
description: '', description: '',
product_type: 'one_time', product_type: 'one_time',
price_type: 'fixed_price',
benefits: '', benefits: '',
amount: 0, amount: 0,
currency: 'USD', currency: 'USD',
@ -92,11 +97,49 @@ const CreateProductForm: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) =
<Field name="description" as={Textarea} placeholder="Product Description" /> <Field name="description" as={Textarea} placeholder="Product Description" />
<ErrorMessage name="description" component="div" className="text-red-500 text-sm mt-1" /> <ErrorMessage name="description" component="div" className="text-red-500 text-sm mt-1" />
</div> </div>
<div>
<Label htmlFor="product_type">Product Type</Label>
<Select
value={values.product_type}
onValueChange={(value) => setFieldValue('product_type', value)}
>
<SelectTrigger>
<SelectValue placeholder="Product Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="one_time">One Time</SelectItem>
<SelectItem value="subscription">Subscription</SelectItem>
</SelectContent>
</Select>
<ErrorMessage name="product_type" component="div" className="text-red-500 text-sm mt-1" />
</div>
<div>
<Label htmlFor="price_type">Price Type</Label>
<Select
value={values.price_type}
onValueChange={(value) => setFieldValue('price_type', value)}
>
<SelectTrigger>
<SelectValue placeholder="Price Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="fixed_price">Fixed Price</SelectItem>
{values.product_type !== 'subscription' && (
<SelectItem value="customer_choice">Customer Choice</SelectItem>
)}
</SelectContent>
</Select>
<ErrorMessage name="price_type" component="div" className="text-red-500 text-sm mt-1" />
</div>
<div className="flex space-x-2"> <div className="flex space-x-2">
<div className="flex-grow"> <div className="flex-grow">
<Label htmlFor="amount">Price</Label> <Label htmlFor="amount">
<Field name="amount" as={Input} type="number" placeholder="Price" /> {values.price_type === 'fixed_price' ? 'Price' : 'Minimum Amount'}
</Label>
<Field name="amount" as={Input} type="number" placeholder={values.price_type === 'fixed_price' ? 'Price' : 'Minimum Amount'} />
<ErrorMessage name="amount" component="div" className="text-red-500 text-sm mt-1" /> <ErrorMessage name="amount" component="div" className="text-red-500 text-sm mt-1" />
</div> </div>
<div className="w-1/3"> <div className="w-1/3">
@ -120,23 +163,6 @@ const CreateProductForm: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) =
</div> </div>
</div> </div>
<div>
<Label htmlFor="product_type">Product Type</Label>
<Select
value={values.product_type}
onValueChange={(value) => setFieldValue('product_type', value)}
>
<SelectTrigger>
<SelectValue placeholder="Product Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="one_time">One Time</SelectItem>
<SelectItem value="subscription">Subscription</SelectItem>
</SelectContent>
</Select>
<ErrorMessage name="product_type" component="div" className="text-red-500 text-sm mt-1" />
</div>
<div> <div>
<Label htmlFor="benefits">Benefits</Label> <Label htmlFor="benefits">Benefits</Label>
<Field name="benefits" as={Textarea} placeholder="Product Benefits" /> <Field name="benefits" as={Textarea} placeholder="Product Benefits" />