feat(subscription): support balance purchases

Refs #3071.
This commit is contained in:
CaIon
2026-05-26 12:03:02 +08:00
parent 1011934987
commit 6b6c9904ac
14 changed files with 222 additions and 9 deletions
+23
View File
@@ -22,6 +22,10 @@ type BillingPreferenceRequest struct {
BillingPreference string `json:"billing_preference"` BillingPreference string `json:"billing_preference"`
} }
type SubscriptionBalancePayRequest struct {
PlanId int `json:"plan_id"`
}
// ---- User APIs ---- // ---- User APIs ----
func GetSubscriptionPlans(c *gin.Context) { func GetSubscriptionPlans(c *gin.Context) {
@@ -92,6 +96,25 @@ func UpdateSubscriptionPreference(c *gin.Context) {
common.ApiSuccess(c, gin.H{"billing_preference": pref}) common.ApiSuccess(c, gin.H{"billing_preference": pref})
} }
func SubscriptionRequestBalancePay(c *gin.Context) {
if !requirePaymentCompliance(c) {
return
}
userId := c.GetInt("id")
var req SubscriptionBalancePayRequest
if err := c.ShouldBindJSON(&req); err != nil || req.PlanId <= 0 {
common.ApiErrorMsg(c, "参数错误")
return
}
if err := model.PurchaseSubscriptionWithBalance(userId, req.PlanId); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, nil)
}
// ---- Admin APIs ---- // ---- Admin APIs ----
func AdminListSubscriptionPlans(c *gin.Context) { func AdminListSubscriptionPlans(c *gin.Context) {
+101
View File
@@ -11,6 +11,7 @@ import (
"github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/pkg/cachex" "github.com/QuantumNous/new-api/pkg/cachex"
"github.com/samber/hot" "github.com/samber/hot"
"github.com/shopspring/decimal"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -665,6 +666,106 @@ func AdminBindSubscription(userId int, planId int, sourceNote string) (string, e
return "", nil return "", nil
} }
func calcSubscriptionBalanceQuota(priceAmount float64) (int, error) {
if priceAmount <= 0 {
return 0, nil
}
if common.QuotaPerUnit <= 0 {
return 0, errors.New("额度单位配置错误")
}
quota := decimal.NewFromFloat(priceAmount).
Mul(decimal.NewFromFloat(common.QuotaPerUnit)).
Ceil().
IntPart()
return int(quota), nil
}
// PurchaseSubscriptionWithBalance creates a subscription by deducting the user's wallet quota.
func PurchaseSubscriptionWithBalance(userId int, planId int) error {
if userId <= 0 || planId <= 0 {
return errors.New("invalid userId or planId")
}
var logPlanTitle string
var logMoney float64
var chargedQuota int
var upgradeGroup string
err := DB.Transaction(func(tx *gorm.DB) error {
plan, err := getSubscriptionPlanByIdTx(tx, planId)
if err != nil {
return err
}
if !plan.Enabled {
return errors.New("套餐未启用")
}
if plan.PriceAmount < 0 {
return errors.New("套餐价格不能为负数")
}
requiredQuota, err := calcSubscriptionBalanceQuota(plan.PriceAmount)
if err != nil {
return err
}
var user User
if err := tx.Set("gorm:query_option", "FOR UPDATE").Where("id = ?", userId).First(&user).Error; err != nil {
return err
}
if requiredQuota > 0 && user.Quota < requiredQuota {
return errors.New("余额不足")
}
if requiredQuota > 0 {
if err := tx.Model(&User{}).Where("id = ?", userId).
Update("quota", gorm.Expr("quota - ?", requiredQuota)).Error; err != nil {
return err
}
}
if _, err := CreateUserSubscriptionFromPlanTx(tx, userId, plan, PaymentMethodBalance); err != nil {
return err
}
now := common.GetTimestamp()
tradeNo := fmt.Sprintf("SUBBALUSR%dNO%s%d", userId, common.GetRandomString(6), time.Now().UnixNano())
order := &SubscriptionOrder{
UserId: userId,
PlanId: plan.Id,
Money: plan.PriceAmount,
TradeNo: tradeNo,
PaymentMethod: PaymentMethodBalance,
PaymentProvider: PaymentProviderBalance,
Status: common.TopUpStatusSuccess,
CreateTime: now,
CompleteTime: now,
ProviderPayload: fmt.Sprintf("charged_quota=%d", requiredQuota),
}
if err := tx.Create(order).Error; err != nil {
return err
}
logPlanTitle = plan.Title
logMoney = plan.PriceAmount
chargedQuota = requiredQuota
upgradeGroup = strings.TrimSpace(plan.UpgradeGroup)
return nil
})
if err != nil {
return err
}
if chargedQuota > 0 {
if err := cacheDecrUserQuota(userId, int64(chargedQuota)); err != nil {
common.SysLog("failed to decrease user quota cache after subscription balance purchase: " + err.Error())
}
}
if upgradeGroup != "" {
_ = UpdateUserGroupCache(userId, upgradeGroup)
}
msg := fmt.Sprintf("使用余额购买订阅成功,套餐: %s,支付金额: %.2f,扣除额度: %d", logPlanTitle, logMoney, chargedQuota)
RecordLog(userId, LogTypeTopup, msg)
return nil
}
// GetAllActiveUserSubscriptions returns all active subscriptions for a user. // GetAllActiveUserSubscriptions returns all active subscriptions for a user.
func GetAllActiveUserSubscriptions(userId int) ([]SubscriptionSummary, error) { func GetAllActiveUserSubscriptions(userId int) ([]SubscriptionSummary, error) {
if userId <= 0 { if userId <= 0 {
+2
View File
@@ -29,6 +29,7 @@ const (
PaymentMethodCreem = "creem" PaymentMethodCreem = "creem"
PaymentMethodWaffo = "waffo" PaymentMethodWaffo = "waffo"
PaymentMethodWaffoPancake = "waffo_pancake" PaymentMethodWaffoPancake = "waffo_pancake"
PaymentMethodBalance = "balance"
) )
const ( const (
@@ -37,6 +38,7 @@ const (
PaymentProviderCreem = "creem" PaymentProviderCreem = "creem"
PaymentProviderWaffo = "waffo" PaymentProviderWaffo = "waffo"
PaymentProviderWaffoPancake = "waffo_pancake" PaymentProviderWaffoPancake = "waffo_pancake"
PaymentProviderBalance = "balance"
) )
var ( var (
+1
View File
@@ -153,6 +153,7 @@ func SetApiRouter(router *gin.Engine) {
subscriptionRoute.GET("/plans", controller.GetSubscriptionPlans) subscriptionRoute.GET("/plans", controller.GetSubscriptionPlans)
subscriptionRoute.GET("/self", controller.GetSubscriptionSelf) subscriptionRoute.GET("/self", controller.GetSubscriptionSelf)
subscriptionRoute.PUT("/self/preference", controller.UpdateSubscriptionPreference) subscriptionRoute.PUT("/self/preference", controller.UpdateSubscriptionPreference)
subscriptionRoute.POST("/balance/pay", middleware.CriticalRateLimit(), controller.SubscriptionRequestBalancePay)
subscriptionRoute.POST("/epay/pay", middleware.CriticalRateLimit(), controller.SubscriptionRequestEpay) subscriptionRoute.POST("/epay/pay", middleware.CriticalRateLimit(), controller.SubscriptionRequestEpay)
subscriptionRoute.POST("/stripe/pay", middleware.CriticalRateLimit(), controller.SubscriptionRequestStripePay) subscriptionRoute.POST("/stripe/pay", middleware.CriticalRateLimit(), controller.SubscriptionRequestStripePay)
subscriptionRoute.POST("/creem/pay", middleware.CriticalRateLimit(), controller.SubscriptionRequestCreemPay) subscriptionRoute.POST("/creem/pay", middleware.CriticalRateLimit(), controller.SubscriptionRequestCreemPay)
+7
View File
@@ -129,6 +129,13 @@ export async function paySubscriptionWaffoPancake(
return res.data return res.data
} }
export async function paySubscriptionBalance(
data: SubscriptionPayRequest
): Promise<SubscriptionPayResponse> {
const res = await api.post('/api/subscription/balance/pay', data)
return res.data
}
// Mints a Pancake OnetimeProduct (see controller for the OnetimeProduct vs // Mints a Pancake OnetimeProduct (see controller for the OnetimeProduct vs
// SubscriptionProduct rationale) using persisted creds + StoreID. // SubscriptionProduct rationale) using persisted creds + StoreID.
export async function createWaffoPancakeSubscriptionProduct(data: { export async function createWaffoPancakeSubscriptionProduct(data: {
@@ -20,7 +20,9 @@ import { useState, useEffect } from 'react'
import { Crown, CalendarClock, Package } from 'lucide-react' import { Crown, CalendarClock, Package } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { DEFAULT_CURRENCY_CONFIG } from '@/stores/system-config-store'
import { formatQuota } from '@/lib/format' import { formatQuota } from '@/lib/format'
import { useSystemConfig } from '@/hooks/use-system-config'
import { Alert, AlertDescription } from '@/components/ui/alert' import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
@@ -44,6 +46,7 @@ import {
paySubscriptionCreem, paySubscriptionCreem,
paySubscriptionEpay, paySubscriptionEpay,
paySubscriptionWaffoPancake, paySubscriptionWaffoPancake,
paySubscriptionBalance,
} from '../../api' } from '../../api'
import { formatDuration, formatResetPeriod } from '../../lib' import { formatDuration, formatResetPeriod } from '../../lib'
import type { PlanRecord } from '../../types' import type { PlanRecord } from '../../types'
@@ -64,10 +67,13 @@ interface Props {
epayMethods?: PaymentMethod[] epayMethods?: PaymentMethod[]
purchaseLimit?: number purchaseLimit?: number
purchaseCount?: number purchaseCount?: number
userQuota?: number
onPurchaseSuccess?: () => void | Promise<void>
} }
export function SubscriptionPurchaseDialog(props: Props) { export function SubscriptionPurchaseDialog(props: Props) {
const { t } = useTranslation() const { t } = useTranslation()
const { currency } = useSystemConfig()
const [paying, setPaying] = useState(false) const [paying, setPaying] = useState(false)
const [selectedEpayMethod, setSelectedEpayMethod] = useState('') const [selectedEpayMethod, setSelectedEpayMethod] = useState('')
@@ -96,6 +102,16 @@ export function SubscriptionPurchaseDialog(props: Props) {
t('Select payment method') t('Select payment method')
const totalAmount = Number(plan.total_amount || 0) const totalAmount = Number(plan.total_amount || 0)
const price = Number(plan.price_amount || 0).toFixed(2) const price = Number(plan.price_amount || 0).toFixed(2)
const quotaPerUnit =
currency?.quotaPerUnit && currency.quotaPerUnit > 0
? currency.quotaPerUnit
: DEFAULT_CURRENCY_CONFIG.quotaPerUnit
const balanceCost = Math.max(
0,
Math.ceil(Number(plan.price_amount || 0) * quotaPerUnit)
)
const userQuota = Math.max(0, Number(props.userQuota || 0))
const insufficientBalance = userQuota < balanceCost
const limitReached = const limitReached =
(props.purchaseLimit || 0) > 0 && (props.purchaseLimit || 0) > 0 &&
(props.purchaseCount || 0) >= (props.purchaseLimit || 0) (props.purchaseCount || 0) >= (props.purchaseLimit || 0)
@@ -215,6 +231,28 @@ export function SubscriptionPurchaseDialog(props: Props) {
} }
} }
const handlePayBalance = async () => {
setPaying(true)
try {
const res = await paySubscriptionBalance({ plan_id: plan.id })
if (res.success) {
toast.success(t('Subscription purchased successfully'))
void props.onPurchaseSuccess?.()
props.onOpenChange(false)
} else {
toast.error(
res.message && res.message !== 'success'
? res.message
: t('Payment request failed')
)
}
} catch {
toast.error(t('Payment request failed'))
} finally {
setPaying(false)
}
}
return ( return (
<Dialog open={props.open} onOpenChange={props.onOpenChange}> <Dialog open={props.open} onOpenChange={props.onOpenChange}>
<DialogContent className='max-sm:w-[calc(100vw-1.5rem)] sm:max-w-md'> <DialogContent className='max-sm:w-[calc(100vw-1.5rem)] sm:max-w-md'>
@@ -285,7 +323,30 @@ export function SubscriptionPurchaseDialog(props: Props) {
</Alert> </Alert>
)} )}
{hasAnyPayment ? ( <div className='flex flex-col gap-2 rounded-md border p-3'>
<div className='flex items-center justify-between gap-2 text-xs'>
<span className='text-muted-foreground'>{t('Required')}</span>
<span>{formatQuota(balanceCost)}</span>
</div>
<div className='flex items-center justify-between gap-2 text-xs'>
<span className='text-muted-foreground'>{t('Available')}</span>
<span>{formatQuota(userQuota)}</span>
</div>
{insufficientBalance && (
<Alert variant='destructive'>
<AlertDescription>{t('Insufficient balance')}</AlertDescription>
</Alert>
)}
<Button
variant='outline'
onClick={handlePayBalance}
disabled={paying || limitReached || insufficientBalance}
>
{t('Pay with Balance')}
</Button>
</div>
{hasAnyPayment && (
<div className='space-y-3'> <div className='space-y-3'>
<p className='text-muted-foreground text-xs'> <p className='text-muted-foreground text-xs'>
{t('Select payment method')} {t('Select payment method')}
@@ -361,14 +422,6 @@ export function SubscriptionPurchaseDialog(props: Props) {
</div> </div>
)} )}
</div> </div>
) : (
<Alert>
<AlertDescription>
{t(
'Online payment is not enabled. Please contact the administrator.'
)}
</AlertDescription>
</Alert>
)} )}
</div> </div>
</DialogContent> </DialogContent>
@@ -62,6 +62,8 @@ import type { PaymentMethod, TopupInfo } from '../types'
interface SubscriptionPlansCardProps { interface SubscriptionPlansCardProps {
topupInfo: TopupInfo | null topupInfo: TopupInfo | null
onAvailabilityChange?: (available: boolean) => void onAvailabilityChange?: (available: boolean) => void
userQuota?: number
onPurchaseSuccess?: () => void | Promise<void>
} }
function getEpayMethods(payMethods: PaymentMethod[] = []): PaymentMethod[] { function getEpayMethods(payMethods: PaymentMethod[] = []): PaymentMethod[] {
@@ -91,6 +93,8 @@ function getBillingPreferenceLabel(
export function SubscriptionPlansCard({ export function SubscriptionPlansCard({
topupInfo, topupInfo,
onAvailabilityChange, onAvailabilityChange,
userQuota,
onPurchaseSuccess,
}: SubscriptionPlansCardProps) { }: SubscriptionPlansCardProps) {
const { t } = useTranslation() const { t } = useTranslation()
@@ -633,6 +637,8 @@ export function SubscriptionPlansCard({
enableWaffoPancake={enableWaffoPancake} enableWaffoPancake={enableWaffoPancake}
enableOnlineTopUp={enableOnlineTopUp} enableOnlineTopUp={enableOnlineTopUp}
epayMethods={epayMethods} epayMethods={epayMethods}
userQuota={userQuota}
onPurchaseSuccess={onPurchaseSuccess}
purchaseLimit={ purchaseLimit={
selectedPlan?.plan?.max_purchase_per_user selectedPlan?.plan?.max_purchase_per_user
? Number(selectedPlan.plan.max_purchase_per_user) ? Number(selectedPlan.plan.max_purchase_per_user)
+2
View File
@@ -309,6 +309,8 @@ export function Wallet(props: WalletProps) {
<SubscriptionPlansCard <SubscriptionPlansCard
topupInfo={topupInfo} topupInfo={topupInfo}
onAvailabilityChange={handleSubscriptionAvailabilityChange} onAvailabilityChange={handleSubscriptionAvailabilityChange}
userQuota={user?.quota}
onPurchaseSuccess={fetchUser}
/> />
</div> </div>
+3
View File
@@ -2035,6 +2035,7 @@
"Inspect requests, errors, and billing details": "Inspect requests, errors, and billing details", "Inspect requests, errors, and billing details": "Inspect requests, errors, and billing details",
"Inspect user prompts": "Inspect user prompts", "Inspect user prompts": "Inspect user prompts",
"Instance": "Instance", "Instance": "Instance",
"Insufficient balance": "Insufficient balance",
"Integrations": "Integrations", "Integrations": "Integrations",
"Inter-group overrides": "Inter-group overrides", "Inter-group overrides": "Inter-group overrides",
"Inter-group ratio overrides": "Inter-group ratio overrides", "Inter-group ratio overrides": "Inter-group ratio overrides",
@@ -2852,6 +2853,7 @@
"Path:": "Path:", "Path:": "Path:",
"Pay": "Pay", "Pay": "Pay",
"Pay-as-you-go with real-time usage monitoring": "Pay-as-you-go with real-time usage monitoring", "Pay-as-you-go with real-time usage monitoring": "Pay-as-you-go with real-time usage monitoring",
"Pay with Balance": "Pay with Balance",
"Payment": "Payment", "Payment": "Payment",
"Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.", "Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.",
"Payment Channel": "Payment Channel", "Payment Channel": "Payment Channel",
@@ -3763,6 +3765,7 @@
"Subscription First": "Subscription First", "Subscription First": "Subscription First",
"Subscription Management": "Subscription Management", "Subscription Management": "Subscription Management",
"Subscription Only": "Subscription Only", "Subscription Only": "Subscription Only",
"Subscription purchased successfully": "Subscription purchased successfully",
"Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.", "Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.",
"Subscription Plans": "Subscription Plans", "Subscription Plans": "Subscription Plans",
"Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).", "Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).",
+3
View File
@@ -2035,6 +2035,7 @@
"Inspect requests, errors, and billing details": "Inspecter les requêtes, les erreurs et les détails de facturation", "Inspect requests, errors, and billing details": "Inspecter les requêtes, les erreurs et les détails de facturation",
"Inspect user prompts": "Inspecter les invites utilisateur", "Inspect user prompts": "Inspecter les invites utilisateur",
"Instance": "Instance", "Instance": "Instance",
"Insufficient balance": "Solde insuffisant",
"Integrations": "Intégrations", "Integrations": "Intégrations",
"Inter-group overrides": "Dérogations inter-groupes", "Inter-group overrides": "Dérogations inter-groupes",
"Inter-group ratio overrides": "Dérogations de ratio inter-groupes", "Inter-group ratio overrides": "Dérogations de ratio inter-groupes",
@@ -2852,6 +2853,7 @@
"Path:": "Chemin :", "Path:": "Chemin :",
"Pay": "Pay", "Pay": "Pay",
"Pay-as-you-go with real-time usage monitoring": "Paiement à l'usage avec suivi de la consommation en temps réel", "Pay-as-you-go with real-time usage monitoring": "Paiement à l'usage avec suivi de la consommation en temps réel",
"Pay with Balance": "Payer avec le solde",
"Payment": "Paiement", "Payment": "Paiement",
"Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "Mode agrégateur de paiement — embarquez avec votre propre société enregistrée (entité offshore). Conçu pour les entreprises.", "Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "Mode agrégateur de paiement — embarquez avec votre propre société enregistrée (entité offshore). Conçu pour les entreprises.",
"Payment Channel": "Canal de paiement", "Payment Channel": "Canal de paiement",
@@ -3763,6 +3765,7 @@
"Subscription First": "Abonnement en priorité", "Subscription First": "Abonnement en priorité",
"Subscription Management": "Gestion des abonnements", "Subscription Management": "Gestion des abonnements",
"Subscription Only": "Abonnement uniquement", "Subscription Only": "Abonnement uniquement",
"Subscription purchased successfully": "Abonnement acheté avec succès",
"Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "La création et la modification des forfaits dabonnement sont verrouillées jusqu’à ce que ladministrateur confirme les conditions de conformité dans les paramètres de la passerelle de paiement.", "Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "La création et la modification des forfaits dabonnement sont verrouillées jusqu’à ce que ladministrateur confirme les conditions de conformité dans les paramètres de la passerelle de paiement.",
"Subscription Plans": "Plans d'abonnement", "Subscription Plans": "Plans d'abonnement",
"Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "Les forfaits dabonnement nutilisent PAS le produit associé : chaque forfait dispose de son propre produit Pancake dédié, défini dans ladministration des abonnements (ou créé automatiquement via le bouton « + Créer »).", "Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "Les forfaits dabonnement nutilisent PAS le produit associé : chaque forfait dispose de son propre produit Pancake dédié, défini dans ladministration des abonnements (ou créé automatiquement via le bouton « + Créer »).",
+3
View File
@@ -2035,6 +2035,7 @@
"Inspect requests, errors, and billing details": "リクエスト、エラー、請求詳細を確認", "Inspect requests, errors, and billing details": "リクエスト、エラー、請求詳細を確認",
"Inspect user prompts": "ユーザープロンプトの検査", "Inspect user prompts": "ユーザープロンプトの検査",
"Instance": "インスタンス", "Instance": "インスタンス",
"Insufficient balance": "残高が不足しています",
"Integrations": "統合", "Integrations": "統合",
"Inter-group overrides": "グループ間上書き", "Inter-group overrides": "グループ間上書き",
"Inter-group ratio overrides": "グループ間比率上書き", "Inter-group ratio overrides": "グループ間比率上書き",
@@ -2852,6 +2853,7 @@
"Path:": "パス:", "Path:": "パス:",
"Pay": "Pay", "Pay": "Pay",
"Pay-as-you-go with real-time usage monitoring": "リアルタイム使用量監視付き従量課金制", "Pay-as-you-go with real-time usage monitoring": "リアルタイム使用量監視付き従量課金制",
"Pay with Balance": "残高で支払う",
"Payment": "支払い", "Payment": "支払い",
"Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "決済アグリゲーターモード — 自社の登録済み法人(オフショア法人)でオンボーディングします。エンタープライズ向けです。", "Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "決済アグリゲーターモード — 自社の登録済み法人(オフショア法人)でオンボーディングします。エンタープライズ向けです。",
"Payment Channel": "決済チャネル", "Payment Channel": "決済チャネル",
@@ -3763,6 +3765,7 @@
"Subscription First": "サブスクリプション優先", "Subscription First": "サブスクリプション優先",
"Subscription Management": "サブスクリプション管理", "Subscription Management": "サブスクリプション管理",
"Subscription Only": "サブスクリプションのみ", "Subscription Only": "サブスクリプションのみ",
"Subscription purchased successfully": "サブスクリプションを購入しました",
"Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "管理者が支払いゲートウェイ設定でコンプライアンス条件を確認するまで、サブスクリプションプランの作成と変更はロックされます。", "Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "管理者が支払いゲートウェイ設定でコンプライアンス条件を確認するまで、サブスクリプションプランの作成と変更はロックされます。",
"Subscription Plans": "サブスクリプションプラン", "Subscription Plans": "サブスクリプションプラン",
"Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "サブスクリプションプランは紐付け済み商品を使用しません。各プランには専用の Pancake 商品があり、サブスクリプション管理画面で設定します(または「+ 作成」ボタンで自動作成します)。", "Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "サブスクリプションプランは紐付け済み商品を使用しません。各プランには専用の Pancake 商品があり、サブスクリプション管理画面で設定します(または「+ 作成」ボタンで自動作成します)。",
+3
View File
@@ -2035,6 +2035,7 @@
"Inspect requests, errors, and billing details": "Проверяйте запросы, ошибки и детали оплаты", "Inspect requests, errors, and billing details": "Проверяйте запросы, ошибки и детали оплаты",
"Inspect user prompts": "Просмотр запросов пользователя", "Inspect user prompts": "Просмотр запросов пользователя",
"Instance": "Экземпляр", "Instance": "Экземпляр",
"Insufficient balance": "Недостаточно средств",
"Integrations": "Интеграции", "Integrations": "Интеграции",
"Inter-group overrides": "Переопределения между группами", "Inter-group overrides": "Переопределения между группами",
"Inter-group ratio overrides": "Переопределения соотношений между группами", "Inter-group ratio overrides": "Переопределения соотношений между группами",
@@ -2852,6 +2853,7 @@
"Path:": "Путь:", "Path:": "Путь:",
"Pay": "Pay", "Pay": "Pay",
"Pay-as-you-go with real-time usage monitoring": "Оплата по мере использования с мониторингом в реальном времени", "Pay-as-you-go with real-time usage monitoring": "Оплата по мере использования с мониторингом в реальном времени",
"Pay with Balance": "Оплатить балансом",
"Payment": "Платеж", "Payment": "Платеж",
"Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "Режим платежного агрегатора — подключение через вашу зарегистрированную компанию (офшорное юрлицо). Создано для Enterprise.", "Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "Режим платежного агрегатора — подключение через вашу зарегистрированную компанию (офшорное юрлицо). Создано для Enterprise.",
"Payment Channel": "Платёжный канал", "Payment Channel": "Платёжный канал",
@@ -3763,6 +3765,7 @@
"Subscription First": "Подписка в приоритете", "Subscription First": "Подписка в приоритете",
"Subscription Management": "Управление подписками", "Subscription Management": "Управление подписками",
"Subscription Only": "Только подписка", "Subscription Only": "Только подписка",
"Subscription purchased successfully": "Подписка успешно приобретена",
"Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "Создание и изменение планов подписки заблокированы, пока администратор не подтвердит условия соответствия в настройках платежного шлюза.", "Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "Создание и изменение планов подписки заблокированы, пока администратор не подтвердит условия соответствия в настройках платежного шлюза.",
"Subscription Plans": "Планы подписки", "Subscription Plans": "Планы подписки",
"Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "Планы подписки НЕ используют привязанный продукт — у каждого плана есть собственный продукт Pancake, задаваемый в администрировании подписок (или автоматически создаваемый кнопкой «+ Создать»).", "Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "Планы подписки НЕ используют привязанный продукт — у каждого плана есть собственный продукт Pancake, задаваемый в администрировании подписок (или автоматически создаваемый кнопкой «+ Создать»).",
+3
View File
@@ -2035,6 +2035,7 @@
"Inspect requests, errors, and billing details": "Kiểm tra yêu cầu, lỗi và chi tiết thanh toán", "Inspect requests, errors, and billing details": "Kiểm tra yêu cầu, lỗi và chi tiết thanh toán",
"Inspect user prompts": "Kiểm tra lời nhắc của người dùng", "Inspect user prompts": "Kiểm tra lời nhắc của người dùng",
"Instance": "Phiên bản", "Instance": "Phiên bản",
"Insufficient balance": "Số dư không đủ",
"Integrations": "Tích hợp", "Integrations": "Tích hợp",
"Inter-group overrides": "Ghi đè liên nhóm", "Inter-group overrides": "Ghi đè liên nhóm",
"Inter-group ratio overrides": "Tỷ lệ liên nhóm ghi đè", "Inter-group ratio overrides": "Tỷ lệ liên nhóm ghi đè",
@@ -2852,6 +2853,7 @@
"Path:": "Đường dẫn:", "Path:": "Đường dẫn:",
"Pay": "Pay", "Pay": "Pay",
"Pay-as-you-go with real-time usage monitoring": "Thanh toán theo mức sử dụng với theo dõi mức sử dụng theo thời gian thực", "Pay-as-you-go with real-time usage monitoring": "Thanh toán theo mức sử dụng với theo dõi mức sử dụng theo thời gian thực",
"Pay with Balance": "Thanh toán bằng số dư",
"Payment": "Thanh toán", "Payment": "Thanh toán",
"Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "Chế độ tổng hợp thanh toán — đăng ký bằng công ty đã đăng ký của bạn (pháp nhân offshore). Dành cho doanh nghiệp.", "Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "Chế độ tổng hợp thanh toán — đăng ký bằng công ty đã đăng ký của bạn (pháp nhân offshore). Dành cho doanh nghiệp.",
"Payment Channel": "Kênh thanh toán", "Payment Channel": "Kênh thanh toán",
@@ -3763,6 +3765,7 @@
"Subscription First": "Ưu tiên đăng ký", "Subscription First": "Ưu tiên đăng ký",
"Subscription Management": "Quản lý đăng ký", "Subscription Management": "Quản lý đăng ký",
"Subscription Only": "Chỉ đăng ký", "Subscription Only": "Chỉ đăng ký",
"Subscription purchased successfully": "Đã mua gói đăng ký thành công",
"Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "Việc tạo và thay đổi gói đăng ký bị khóa cho đến khi quản trị viên xác nhận điều khoản tuân thủ trong cài đặt Cổng thanh toán.", "Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "Việc tạo và thay đổi gói đăng ký bị khóa cho đến khi quản trị viên xác nhận điều khoản tuân thủ trong cài đặt Cổng thanh toán.",
"Subscription Plans": "Gói đăng ký", "Subscription Plans": "Gói đăng ký",
"Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "Gói đăng ký KHÔNG dùng Sản phẩm đã liên kết — mỗi gói có một sản phẩm Pancake riêng, được đặt trong quản trị Đăng ký (hoặc tự động tạo bằng nút \"+ Create\" tại đó).", "Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "Gói đăng ký KHÔNG dùng Sản phẩm đã liên kết — mỗi gói có một sản phẩm Pancake riêng, được đặt trong quản trị Đăng ký (hoặc tự động tạo bằng nút \"+ Create\" tại đó).",
+3
View File
@@ -2035,6 +2035,7 @@
"Inspect requests, errors, and billing details": "查看请求、错误和计费详情", "Inspect requests, errors, and billing details": "查看请求、错误和计费详情",
"Inspect user prompts": "检查用户提示", "Inspect user prompts": "检查用户提示",
"Instance": "实例", "Instance": "实例",
"Insufficient balance": "余额不足",
"Integrations": "集成", "Integrations": "集成",
"Inter-group overrides": "分组间覆盖", "Inter-group overrides": "分组间覆盖",
"Inter-group ratio overrides": "分组间比例覆盖", "Inter-group ratio overrides": "分组间比例覆盖",
@@ -2852,6 +2853,7 @@
"Path:": "路径:", "Path:": "路径:",
"Pay": "支付", "Pay": "支付",
"Pay-as-you-go with real-time usage monitoring": "按量付费,实时监控使用情况", "Pay-as-you-go with real-time usage monitoring": "按量付费,实时监控使用情况",
"Pay with Balance": "使用余额支付",
"Payment": "支付", "Payment": "支付",
"Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "支付聚合模式:使用你自己的注册公司(离岸实体)入驻。面向企业场景构建。", "Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "支付聚合模式:使用你自己的注册公司(离岸实体)入驻。面向企业场景构建。",
"Payment Channel": "支付渠道", "Payment Channel": "支付渠道",
@@ -3763,6 +3765,7 @@
"Subscription First": "优先订阅", "Subscription First": "优先订阅",
"Subscription Management": "订阅管理", "Subscription Management": "订阅管理",
"Subscription Only": "仅用订阅", "Subscription Only": "仅用订阅",
"Subscription purchased successfully": "订阅购买成功",
"Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "管理员在支付网关设置中确认合规条款之前,订阅套餐的创建和修改会被锁定。", "Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "管理员在支付网关设置中确认合规条款之前,订阅套餐的创建和修改会被锁定。",
"Subscription Plans": "订阅套餐", "Subscription Plans": "订阅套餐",
"Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "订阅套餐不会使用已绑定的产品。每个套餐都有独立的 Pancake 产品,可在订阅管理中设置,或通过其中的“+ 创建”按钮自动生成。", "Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "订阅套餐不会使用已绑定的产品。每个套餐都有独立的 Pancake 产品,可在订阅管理中设置,或通过其中的“+ 创建”按钮自动生成。",