feat(subscription): add support for wallet overflow and downgrade group functionality in subscription plans
This commit is contained in:
+1
-1
@@ -297,7 +297,7 @@ export function SubscriptionPurchaseDialog(props: Props) {
|
||||
)}
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='text-muted-foreground text-sm'>
|
||||
{t('Received amount')}
|
||||
{t('Plan Quota')}
|
||||
</span>
|
||||
<span className='flex items-center gap-1 text-sm'>
|
||||
<Package className='h-3.5 w-3.5' />
|
||||
|
||||
+4
-1
@@ -52,6 +52,7 @@ import {
|
||||
invalidateUserSubscription,
|
||||
deleteUserSubscription,
|
||||
} from '../../api'
|
||||
import { formatQuota } from '@/lib/format'
|
||||
import { formatTimestamp } from '../../lib'
|
||||
import type { PlanRecord, UserSubscriptionRecord } from '../../types'
|
||||
|
||||
@@ -301,7 +302,9 @@ export function UserSubscriptionsDialog(props: Props) {
|
||||
const sub = record.subscription
|
||||
const total = Number(sub.amount_total || 0)
|
||||
const used = Number(sub.amount_used || 0)
|
||||
return total > 0 ? `${used}/${total}` : t('Unlimited')
|
||||
return total > 0
|
||||
? `${formatQuota(used)}/${formatQuota(total)}`
|
||||
: t('Unlimited')
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
+1
-1
@@ -160,7 +160,7 @@ export function useSubscriptionsColumns(): ColumnDef<PlanRecord>[] {
|
||||
},
|
||||
{
|
||||
id: 'total_amount',
|
||||
header: t('Received amount'),
|
||||
header: t('Plan Quota'),
|
||||
meta: { mobileHidden: true },
|
||||
cell: ({ row }) => {
|
||||
const total = Number(row.original.plan.total_amount || 0)
|
||||
|
||||
+102
-23
@@ -66,6 +66,7 @@ import {
|
||||
createWaffoPancakeSubscriptionProduct,
|
||||
listWaffoPancakeSubscriptionProductOptions,
|
||||
} from '../api'
|
||||
import { getCurrencyDisplay, getCurrencyLabel } from '@/lib/currency'
|
||||
import { getDurationUnitOptions, getResetPeriodOptions } from '../constants'
|
||||
import {
|
||||
getPlanFormSchema,
|
||||
@@ -91,6 +92,9 @@ export function SubscriptionsMutateDrawer({
|
||||
const { t } = useTranslation()
|
||||
const isEdit = !!currentRow?.plan?.id
|
||||
const { triggerRefresh } = useSubscriptions()
|
||||
const { meta: currencyMeta } = getCurrencyDisplay()
|
||||
const tokensOnly = currencyMeta.kind === 'tokens'
|
||||
const currencyLabel = getCurrencyLabel()
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [groupOptions, setGroupOptions] = useState<string[]>([])
|
||||
const [creatingPancakeProduct, setCreatingPancakeProduct] = useState(false)
|
||||
@@ -314,7 +318,7 @@ export function SubscriptionsMutateDrawer({
|
||||
name='price_amount'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('Actual Amount')}</FormLabel>
|
||||
<FormLabel>{t('Plan Price')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
@@ -336,12 +340,22 @@ export function SubscriptionsMutateDrawer({
|
||||
name='total_amount'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('Received amount')}</FormLabel>
|
||||
<FormLabel>
|
||||
{t('Quota ({{currency}})', { currency: currencyLabel })}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type='number'
|
||||
min={0}
|
||||
step={tokensOnly ? 1 : 0.01}
|
||||
placeholder={
|
||||
tokensOnly
|
||||
? t('Enter quota in tokens')
|
||||
: t('Enter quota in {{currency}}', {
|
||||
currency: currencyLabel,
|
||||
})
|
||||
}
|
||||
onChange={(e) =>
|
||||
field.onChange(parseFloat(e.target.value) || 0)
|
||||
}
|
||||
@@ -349,7 +363,7 @@ export function SubscriptionsMutateDrawer({
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
'0 means unlimited. The value is converted to quota units when saved.'
|
||||
'Total quota included in the plan, usable per billing period. 0 means unlimited.'
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
@@ -398,6 +412,53 @@ export function SubscriptionsMutateDrawer({
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='downgrade_group'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('Downgrade Group')}</FormLabel>
|
||||
<Select
|
||||
items={[
|
||||
{
|
||||
value: '__none__',
|
||||
label: t('Downgrade to pre-purchase group'),
|
||||
},
|
||||
...groupOptions.map((g) => ({ value: g, label: g })),
|
||||
]}
|
||||
onValueChange={(v) =>
|
||||
field.onChange(v === '__none__' ? '' : v)
|
||||
}
|
||||
value={field.value || ''}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t('Downgrade to pre-purchase group')}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent alignItemWithTrigger={false}>
|
||||
<SelectGroup>
|
||||
<SelectItem value='__none__'>
|
||||
{t('Downgrade to pre-purchase group')}
|
||||
</SelectItem>
|
||||
{groupOptions.map((g) => (
|
||||
<SelectItem key={g} value={g}>
|
||||
{g}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
{t('Downgrade to this group after the subscription expires')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='max_purchase_per_user'
|
||||
@@ -423,27 +484,27 @@ export function SubscriptionsMutateDrawer({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 gap-3 sm:grid-cols-2'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='sort_order'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('Sort Order')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type='number'
|
||||
onChange={(e) =>
|
||||
field.onChange(parseInt(e.target.value, 10) || 0)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='sort_order'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('Sort Order')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type='number'
|
||||
onChange={(e) =>
|
||||
field.onChange(parseInt(e.target.value, 10) || 0)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className='flex flex-col gap-3'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='enabled'
|
||||
@@ -479,6 +540,24 @@ export function SubscriptionsMutateDrawer({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='allow_wallet_overflow'
|
||||
render={({ field }) => (
|
||||
<FormItem className={sideDrawerSwitchItemClassName()}>
|
||||
<FormLabel className='!mt-0'>
|
||||
{t('Allow wallet balance after quota used up')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</SideDrawerSection>
|
||||
|
||||
|
||||
@@ -40,9 +40,11 @@ export function getPlanFormSchema(t: TFunction) {
|
||||
enabled: z.boolean(),
|
||||
sort_order: z.coerce.number(),
|
||||
allow_balance_pay: z.boolean(),
|
||||
allow_wallet_overflow: z.boolean(),
|
||||
max_purchase_per_user: z.coerce.number().min(0),
|
||||
total_amount: z.coerce.number().min(0),
|
||||
upgrade_group: z.string().optional(),
|
||||
downgrade_group: z.string().optional(),
|
||||
stripe_price_id: z.string().optional(),
|
||||
creem_product_id: z.string().optional(),
|
||||
waffo_pancake_product_id: z.string().optional(),
|
||||
@@ -63,9 +65,11 @@ export const PLAN_FORM_DEFAULTS: PlanFormValues = {
|
||||
enabled: true,
|
||||
sort_order: 0,
|
||||
allow_balance_pay: true,
|
||||
allow_wallet_overflow: true,
|
||||
max_purchase_per_user: 0,
|
||||
total_amount: 0,
|
||||
upgrade_group: '',
|
||||
downgrade_group: '',
|
||||
stripe_price_id: '',
|
||||
creem_product_id: '',
|
||||
waffo_pancake_product_id: '',
|
||||
@@ -84,9 +88,11 @@ export function planToFormValues(plan: SubscriptionPlan): PlanFormValues {
|
||||
enabled: plan.enabled !== false,
|
||||
sort_order: Number(plan.sort_order || 0),
|
||||
allow_balance_pay: plan.allow_balance_pay !== false,
|
||||
allow_wallet_overflow: plan.allow_wallet_overflow !== false,
|
||||
max_purchase_per_user: Number(plan.max_purchase_per_user || 0),
|
||||
total_amount: quotaUnitsToDollars(Number(plan.total_amount || 0)),
|
||||
upgrade_group: plan.upgrade_group || '',
|
||||
downgrade_group: plan.downgrade_group || '',
|
||||
stripe_price_id: plan.stripe_price_id || '',
|
||||
creem_product_id: plan.creem_product_id || '',
|
||||
waffo_pancake_product_id: plan.waffo_pancake_product_id || '',
|
||||
@@ -110,6 +116,7 @@ export function formValuesToPlanPayload(values: PlanFormValues): PlanPayload {
|
||||
max_purchase_per_user: Number(values.max_purchase_per_user || 0),
|
||||
total_amount: parseQuotaFromDollars(Number(values.total_amount || 0)),
|
||||
upgrade_group: values.upgrade_group || '',
|
||||
downgrade_group: values.downgrade_group || '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +36,11 @@ export const subscriptionPlanSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
sort_order: z.number(),
|
||||
allow_balance_pay: z.boolean().optional().default(true),
|
||||
allow_wallet_overflow: z.boolean().optional().default(true),
|
||||
max_purchase_per_user: z.number(),
|
||||
total_amount: z.number(),
|
||||
upgrade_group: z.string().optional(),
|
||||
downgrade_group: z.string().optional(),
|
||||
stripe_price_id: z.string().optional(),
|
||||
creem_product_id: z.string().optional(),
|
||||
waffo_pancake_product_id: z.string().optional(),
|
||||
|
||||
Reference in New Issue
Block a user