mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-30 16:23:31 +00:00
Build composite subscription bucket two
This commit is contained in:
@@ -162,6 +162,9 @@ const labelClass = computed(() => {
|
||||
if (props.platform === 'grok') {
|
||||
return `${base} bg-zinc-300/70 text-zinc-800 dark:bg-zinc-700/60 dark:text-zinc-200`
|
||||
}
|
||||
if (props.platform === 'composite') {
|
||||
return `${base} bg-cyan-200/70 text-cyan-900 dark:bg-cyan-900/50 dark:text-cyan-300`
|
||||
}
|
||||
return `${base} bg-violet-200/60 text-violet-800 dark:bg-violet-800/40 dark:text-violet-300`
|
||||
})
|
||||
|
||||
@@ -197,6 +200,11 @@ const badgeClass = computed(() => {
|
||||
? 'bg-zinc-200 text-zinc-800 dark:bg-zinc-700 dark:text-zinc-100'
|
||||
: 'bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-200'
|
||||
}
|
||||
if (props.platform === 'composite') {
|
||||
return isSubscription.value
|
||||
? 'bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-300'
|
||||
: 'bg-cyan-50 text-cyan-800 dark:bg-cyan-900/20 dark:text-cyan-300'
|
||||
}
|
||||
// Fallback: original colors
|
||||
return isSubscription.value
|
||||
? 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-400'
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { defineComponent } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
import PlanEditDialog from '../PlanEditDialog.vue'
|
||||
import type { AdminGroup } from '@/types'
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
@@ -26,21 +29,108 @@ vi.mock('@/api/admin/payment', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
function mountDialog(paymentConfig: Record<string, unknown> | null) {
|
||||
const BaseDialogStub = defineComponent({
|
||||
name: 'BaseDialog',
|
||||
props: {
|
||||
show: Boolean,
|
||||
title: String,
|
||||
width: String,
|
||||
},
|
||||
template: '<div v-if="show"><slot /><slot name="footer" /></div>',
|
||||
})
|
||||
|
||||
const SelectStub = defineComponent({
|
||||
name: 'SelectStub',
|
||||
props: {
|
||||
modelValue: [String, Number],
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
placeholder: String,
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(_props, { emit }) {
|
||||
const onChange = (event: Event) => {
|
||||
const value = (event.target as HTMLSelectElement).value
|
||||
emit('update:modelValue', value === '' ? null : Number(value))
|
||||
}
|
||||
return { onChange }
|
||||
},
|
||||
template: `
|
||||
<select
|
||||
:value="modelValue ?? ''"
|
||||
@change="onChange"
|
||||
>
|
||||
<option value="">{{ placeholder }}</option>
|
||||
<option
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
:data-platform="option.platform"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
`,
|
||||
})
|
||||
|
||||
const groupFixture = (overrides: Partial<AdminGroup>): AdminGroup => ({
|
||||
id: 1,
|
||||
name: 'OpenAI',
|
||||
description: null,
|
||||
platform: 'openai',
|
||||
rate_multiplier: 1,
|
||||
rpm_limit: 0,
|
||||
is_exclusive: false,
|
||||
status: 'active',
|
||||
subscription_type: 'subscription',
|
||||
daily_limit_usd: null,
|
||||
weekly_limit_usd: null,
|
||||
monthly_limit_usd: null,
|
||||
allow_image_generation: false,
|
||||
image_rate_independent: false,
|
||||
image_rate_multiplier: 1,
|
||||
image_price_1k: null,
|
||||
image_price_2k: null,
|
||||
image_price_4k: null,
|
||||
peak_rate_enabled: false,
|
||||
peak_start: '',
|
||||
peak_end: '',
|
||||
peak_rate_multiplier: 1,
|
||||
claude_code_only: false,
|
||||
fallback_group_id: null,
|
||||
fallback_group_id_on_invalid_request: null,
|
||||
allow_messages_dispatch: false,
|
||||
require_oauth_only: false,
|
||||
require_privacy_set: false,
|
||||
created_at: '2026-07-01T00:00:00Z',
|
||||
updated_at: '2026-07-01T00:00:00Z',
|
||||
model_routing: null,
|
||||
model_routing_enabled: false,
|
||||
mcp_xml_inject: false,
|
||||
sort_order: 0,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
function mountDialog({
|
||||
groups = [],
|
||||
paymentConfig = null,
|
||||
}: {
|
||||
groups?: AdminGroup[]
|
||||
paymentConfig?: Record<string, unknown> | null
|
||||
} = {}) {
|
||||
return mount(PlanEditDialog, {
|
||||
props: {
|
||||
show: true,
|
||||
plan: null,
|
||||
groups: [],
|
||||
groups,
|
||||
paymentConfig,
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
BaseDialog: {
|
||||
props: ['show'],
|
||||
template: '<div v-if="show"><slot /><slot name="footer" /></div>',
|
||||
},
|
||||
Select: true,
|
||||
BaseDialog: BaseDialogStub,
|
||||
Select: SelectStub,
|
||||
Icon: true,
|
||||
GroupBadge: true,
|
||||
},
|
||||
@@ -48,11 +138,13 @@ function mountDialog(paymentConfig: Record<string, unknown> | null) {
|
||||
})
|
||||
}
|
||||
|
||||
describe('PlanEditDialog subscription CNY payment preview', () => {
|
||||
describe('PlanEditDialog', () => {
|
||||
it('shows CNY channel charge using the configured subscription rate and fee', async () => {
|
||||
const wrapper = mountDialog({
|
||||
subscription_usd_to_cny_rate: 7.15,
|
||||
recharge_fee_rate: 2.5,
|
||||
paymentConfig: {
|
||||
subscription_usd_to_cny_rate: 7.15,
|
||||
recharge_fee_rate: 2.5,
|
||||
},
|
||||
})
|
||||
|
||||
await wrapper.find('input[type="number"]').setValue('9.99')
|
||||
@@ -65,8 +157,10 @@ describe('PlanEditDialog subscription CNY payment preview', () => {
|
||||
|
||||
it('hides the preview when the subscription rate is not configured', async () => {
|
||||
const wrapper = mountDialog({
|
||||
subscription_usd_to_cny_rate: 0,
|
||||
recharge_fee_rate: 2.5,
|
||||
paymentConfig: {
|
||||
subscription_usd_to_cny_rate: 0,
|
||||
recharge_fee_rate: 2.5,
|
||||
},
|
||||
})
|
||||
|
||||
await wrapper.find('input[type="number"]').setValue('9.99')
|
||||
@@ -74,4 +168,29 @@ describe('PlanEditDialog subscription CNY payment preview', () => {
|
||||
expect(wrapper.text()).not.toContain('preview')
|
||||
expect(wrapper.text()).not.toContain('¥71.43')
|
||||
})
|
||||
|
||||
it('allows composite subscription groups for payment plans', () => {
|
||||
const wrapper = mountDialog({
|
||||
groups: [
|
||||
groupFixture({
|
||||
id: 10,
|
||||
name: 'OpenAI + Claude + Gemini + Grok',
|
||||
platform: 'composite',
|
||||
rate_multiplier: 1.2,
|
||||
subscription_type: 'subscription',
|
||||
}),
|
||||
groupFixture({
|
||||
id: 11,
|
||||
name: 'Standard OpenAI',
|
||||
platform: 'openai',
|
||||
subscription_type: 'standard',
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const options = wrapper.findAll('option').map(option => option.text())
|
||||
|
||||
expect(options).toContain('OpenAI + Claude + Gemini + Grok — composite (1.2x)')
|
||||
expect(options).not.toContain('Standard OpenAI — openai (1x)')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user