/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import * as z from 'zod' import type { Resolver } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { useTranslation } from 'react-i18next' import { DEFAULT_CURRENCY_CONFIG } from '@/stores/system-config-store' import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Switch } from '@/components/ui/switch' import { FormDirtyIndicator } from '../components/form-dirty-indicator' import { FormNavigationGuard } from '../components/form-navigation-guard' import { SettingsForm, SettingsSwitchContent, SettingsSwitchItem, } from '../components/settings-form-layout' import { SettingsPageFormActions } from '../components/settings-page-context' import { SettingsSection } from '../components/settings-section' import { useSettingsForm } from '../hooks/use-settings-form' import { useUpdateOption } from '../hooks/use-update-option' import { safeNumberFieldProps } from '../utils/numeric-field' const createPricingSchema = (t: (key: string) => string) => z .object({ QuotaPerUnit: z.coerce.number().min(0, t('Value must be at least 0')), USDExchangeRate: z.coerce .number() .min(0.0001, t('Exchange rate must be greater than 0')), DisplayInCurrencyEnabled: z.boolean(), DisplayTokenStatEnabled: z.boolean(), general_setting: z.object({ quota_display_type: z.enum(['USD', 'CNY', 'TOKENS', 'CUSTOM']), custom_currency_symbol: z.string().max(8).optional(), custom_currency_exchange_rate: z.coerce .number() .min(0.0001, t('Exchange rate must be greater than 0')) .optional(), }), }) .superRefine((data, ctx) => { const displayType = data.general_setting.quota_display_type if (displayType === 'CUSTOM') { if (!data.general_setting.custom_currency_symbol?.trim()) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['general_setting', 'custom_currency_symbol'], message: t('Custom currency symbol is required'), }) } if (data.general_setting.custom_currency_exchange_rate == null) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['general_setting', 'custom_currency_exchange_rate'], message: t('Exchange rate is required'), }) } } }) type PricingFormValues = z.infer> type PricingSectionProps = { defaultValues: PricingFormValues } export function PricingSection({ defaultValues }: PricingSectionProps) { const { t } = useTranslation() const updateOption = useUpdateOption() const pricingSchema = createPricingSchema(t) const { form, handleSubmit, handleReset, isDirty, isSubmitting } = useSettingsForm({ resolver: zodResolver(pricingSchema) as Resolver< PricingFormValues, unknown, PricingFormValues >, defaultValues, onSubmit: async (_data, changedFields) => { for (const [key, value] of Object.entries(changedFields)) { if (value === undefined || value === null) continue if (typeof value === 'object') continue let serialized: string | boolean = value as string | boolean if (typeof value === 'boolean') { serialized = String(value) } else if (typeof value === 'number') { serialized = Number.isFinite(value) ? String(value) : '0' } await updateOption.mutateAsync({ key, value: serialized, }) } }, }) const displayType = form.watch('general_setting.quota_display_type') ?? 'USD' const displayInCurrencyEnabled = form.watch('DisplayInCurrencyEnabled') const showTokensOnlyOption = displayType === 'TOKENS' const showQuotaPerUnit = displayType === 'TOKENS' || defaultValues.QuotaPerUnit !== DEFAULT_CURRENCY_CONFIG.quotaPerUnit const showDisplayInCurrencyOption = displayInCurrencyEnabled === false return ( <>
{showQuotaPerUnit && ( ( {t('Quota Per Unit')} {t('Number of tokens per unit quota')} )} /> )} ( {t('Display Mode')} {t('Choose how quota values are shown to users')} )} /> {displayType !== 'TOKENS' && ( ( {displayType === 'CNY' ? t('CNY per USD') : displayType === 'USD' ? t('USD Exchange Rate') : t('USD Exchange Rate')} {t( 'Real exchange rate between USD and your payment gateway currency' )} )} /> )} {displayType === 'CUSTOM' && (
( {t('Custom Currency Symbol')} {t('Prefix used when displaying prices')} )} /> ( {t('Units per USD')} field.onChange( e.target.value === '' ? undefined : e.target.valueAsNumber ) } name={field.name} onBlur={field.onBlur} ref={field.ref} placeholder={t('e.g. 8 means 1 USD = 8 units')} /> {t('Conversion rate from USD to your custom currency')} )} />
)} {showDisplayInCurrencyOption && ( ( {t('Display in Currency')} {displayType === 'TOKENS' ? t( 'Tokens-only mode will show raw quota values regardless of this toggle.' ) : t('Show prices in currency instead of quota.')} )} /> )} ( {t('Display Token Statistics')} {t('Show token usage statistics in the UI')} )} />
) }