Build composite subscription bucket two

This commit is contained in:
Heatherm Huang
2026-07-23 09:20:52 +08:00
parent 3a683fff55
commit ce3272c41b
6 changed files with 357 additions and 13 deletions
@@ -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)')
})
})