mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: add user choosen custom prices
This commit is contained in:
parent
bf38fee68b
commit
3f96f1ec9f
3 changed files with 84 additions and 47 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue