feat: add platform tenant detail audit console

This commit is contained in:
Codex
2026-06-30 04:38:36 +08:00
parent e62653025c
commit d5d32e84b3
16 changed files with 688 additions and 111 deletions

View File

@@ -3,10 +3,15 @@ import Taro from '@tarojs/taro';
import { Button, Input, Text, View } from '@tarojs/components';
import {
createPlatformTenant,
loadPlatformAuditLogs,
loadPlatformPlans,
loadPlatformTenantDetail,
loadPlatformTenants,
updatePlatformTenantStatus,
upsertPlatformTenantBillingProfile,
type PlatformAuditLogItem,
type PlatformSaasPlan,
type PlatformTenantDetail,
type PlatformTenantItem,
} from '@/services/platformAdmin';
import '../platform.css';
@@ -21,11 +26,41 @@ function centsFromYuan(value: string) {
return Math.round(amount * 100);
}
function dateText(value?: string | null) {
return value ? String(value).slice(0, 10) : '-';
}
function auditDetailsText(item: PlatformAuditLogItem) {
try {
const text = JSON.stringify(item.details || {});
return text.length > 160 ? `${text.slice(0, 160)}...` : text;
} catch {
return '{}';
}
}
const emptyBillingForm = {
tenantId: '',
billingName: '',
taxId: '',
contactName: '',
contactPhone: '',
contactEmail: '',
billingAddress: '',
invoiceTitle: '',
invoiceType: 'none',
bankName: '',
bankAccountMasked: '',
};
export default function PlatformTenantsPage() {
const [keyword, setKeyword] = useState('');
const [status, setStatus] = useState('');
const [plans, setPlans] = useState<PlatformSaasPlan[]>([]);
const [tenants, setTenants] = useState<PlatformTenantItem[]>([]);
const [selectedTenantId, setSelectedTenantId] = useState('');
const [detail, setDetail] = useState<PlatformTenantDetail | null>(null);
const [auditLogs, setAuditLogs] = useState<PlatformAuditLogItem[]>([]);
const [tenantForm, setTenantForm] = useState({
slug: '',
name: '',
@@ -42,6 +77,7 @@ export default function PlatformTenantsPage() {
billingStatus: '',
reason: '',
});
const [billingForm, setBillingForm] = useState(emptyBillingForm);
const [busy, setBusy] = useState('');
const [error, setError] = useState('');
@@ -51,6 +87,43 @@ export default function PlatformTenantsPage() {
.catch(nextError => setError(nextError instanceof Error ? nextError.message : '租户列表加载失败'));
}
function fillBillingForm(nextDetail: PlatformTenantDetail | null, fallbackTenantId = '') {
const tenant = nextDetail?.tenant;
setBillingForm({
tenantId: tenant?.id || fallbackTenantId,
billingName: tenant?.billingName || '',
taxId: tenant?.taxId || '',
contactName: tenant?.contactName || '',
contactPhone: tenant?.contactPhone || '',
contactEmail: tenant?.contactEmail || '',
billingAddress: tenant?.billingAddress || '',
invoiceTitle: tenant?.invoiceTitle || '',
invoiceType: tenant?.invoiceType || 'none',
bankName: tenant?.bankName || '',
bankAccountMasked: tenant?.bankAccountMasked || '',
});
}
async function loadDetail(tenantId: string) {
if (!tenantId) return;
setError('');
setBusy('detail');
try {
const [detailPayload, auditPayload] = await Promise.all([
loadPlatformTenantDetail(tenantId),
loadPlatformAuditLogs({ tenantId, limit: 30 }).catch(() => ({ items: [] })),
]);
const nextDetail = detailPayload.item || null;
setDetail(nextDetail);
fillBillingForm(nextDetail, tenantId);
setAuditLogs(auditPayload.items || []);
} catch (nextError) {
setError(nextError instanceof Error ? nextError.message : '租户详情加载失败');
} finally {
setBusy('');
}
}
useEffect(() => {
reload('', '');
loadPlatformPlans().then(payload => {
@@ -73,6 +146,22 @@ export default function PlatformTenantsPage() {
setStatusForm(current => ({ ...current, [key]: value }));
}
function updateBillingForm(key: keyof typeof billingForm, value: string) {
setBillingForm(current => ({ ...current, [key]: value }));
}
function chooseTenant(item: PlatformTenantItem) {
setSelectedTenantId(item.id);
setStatusForm(current => ({
...current,
tenantId: item.id,
status: item.status || 'active',
billingStatus: item.billingStatus || '',
}));
fillBillingForm(null, item.id);
loadDetail(item.id);
}
async function confirm(title: string, content: string) {
const result = await Taro.showModal({ title, content, confirmText: '确认', cancelText: '取消' });
return result.confirm;
@@ -90,7 +179,7 @@ export default function PlatformTenantsPage() {
if (!ok) return;
setBusy('create');
try {
await createPlatformTenant({
const payload = await createPlatformTenant({
slug,
name,
legalName: tenantForm.legalName.trim() || undefined,
@@ -112,6 +201,10 @@ export default function PlatformTenantsPage() {
amountYuan: '',
}));
reload(status, keyword);
if (payload.item?.id) {
setSelectedTenantId(payload.item.id);
await loadDetail(payload.item.id);
}
} catch (nextError) {
setError(nextError instanceof Error ? nextError.message : '创建租户失败');
} finally {
@@ -141,6 +234,7 @@ export default function PlatformTenantsPage() {
});
Taro.showToast({ title: '已更新', icon: 'success' });
reload(status, keyword);
await loadDetail(statusForm.tenantId);
} catch (nextError) {
setError(nextError instanceof Error ? nextError.message : '状态更新失败');
} finally {
@@ -148,13 +242,47 @@ export default function PlatformTenantsPage() {
}
}
async function submitBillingProfile() {
setError('');
if (!billingForm.tenantId) {
setError('请先选择租户,再维护账务资料。');
return;
}
const ok = await confirm('保存账务资料', '确认更新该租户的开票和收款联系资料?');
if (!ok) return;
setBusy('billing');
try {
await upsertPlatformTenantBillingProfile({
tenantId: billingForm.tenantId,
billingName: billingForm.billingName.trim() || undefined,
taxId: billingForm.taxId.trim() || undefined,
contactName: billingForm.contactName.trim() || undefined,
contactPhone: billingForm.contactPhone.trim() || undefined,
contactEmail: billingForm.contactEmail.trim() || undefined,
billingAddress: billingForm.billingAddress.trim() || undefined,
invoiceTitle: billingForm.invoiceTitle.trim() || undefined,
invoiceType: billingForm.invoiceType.trim() || 'none',
bankName: billingForm.bankName.trim() || undefined,
bankAccountMasked: billingForm.bankAccountMasked.trim() || undefined,
});
Taro.showToast({ title: '已保存', icon: 'success' });
await loadDetail(billingForm.tenantId);
} catch (nextError) {
setError(nextError instanceof Error ? nextError.message : '账务资料保存失败');
} finally {
setBusy('');
}
}
const selectedTenant = detail?.tenant;
return (
<View className='platform-page'>
<View className='platform-shell'>
<View className='platform-header'>
<Text className='platform-kicker'>Tenants</Text>
<Text className='platform-title'></Text>
<Text className='platform-subtitle'> SaaS API</Text>
<Text className='platform-subtitle'> SaaS </Text>
</View>
<View className='platform-actions'>
@@ -176,7 +304,7 @@ export default function PlatformTenantsPage() {
<View className='platform-metric'><Text className='platform-metric-label'></Text><Text className='platform-metric-value'>{String(tenants.length)}</Text></View>
<View className='platform-metric'><Text className='platform-metric-label'></Text><Text className='platform-metric-value'>{String(tenants.filter(item => Number(item.openBalanceCents || 0) > 0).length)}</Text></View>
<View className='platform-metric'><Text className='platform-metric-label'></Text><Text className='platform-metric-value'>{String(tenants.filter(item => item.billingStatus === 'trial').length)}</Text></View>
<View className='platform-metric'><Text className='platform-metric-label'></Text><Text className='platform-metric-value'>{String(tenants.filter(item => item.status === 'suspended').length)}</Text></View>
<View className='platform-metric'><Text className='platform-metric-label'></Text><Text className='platform-metric-value'>{selectedTenant ? selectedTenant.slug : '-'}</Text></View>
</View>
<View className='platform-section'>
@@ -196,19 +324,6 @@ export default function PlatformTenantsPage() {
</View>
</View>
<View className='platform-section'>
<Text className='platform-section-title'></Text>
<View className='platform-form compact'>
<View className='platform-field wide'><Text className='platform-field-label'> ID</Text><Input className='platform-input' placeholder='从列表选择或粘贴 tenantId' value={statusForm.tenantId} onInput={event => updateStatusForm('tenantId', String(event.detail.value || ''))} /></View>
<View className='platform-field'><Text className='platform-field-label'></Text><Input className='platform-input' placeholder='active / suspended' value={statusForm.status} onInput={event => updateStatusForm('status', String(event.detail.value || ''))} /></View>
<View className='platform-field'><Text className='platform-field-label'></Text><Input className='platform-input' placeholder='trial / active / past_due' value={statusForm.billingStatus} onInput={event => updateStatusForm('billingStatus', String(event.detail.value || ''))} /></View>
<View className='platform-field wide'><Text className='platform-field-label'></Text><Input className='platform-input' placeholder='内部审计备注' value={statusForm.reason} onInput={event => updateStatusForm('reason', String(event.detail.value || ''))} /></View>
</View>
<View className='platform-actions'>
<Button className='platform-button primary' loading={busy === 'status'} onClick={submitStatusChange}></Button>
</View>
</View>
<View className='platform-section'>
<Text className='platform-section-title'></Text>
<View className='platform-list'>
@@ -216,17 +331,114 @@ export default function PlatformTenantsPage() {
<View className='platform-row' key={item.id}>
<Text className='platform-row-main'>{item.brandName || item.name}</Text>
<Text className='platform-row-meta'>{item.slug} · {item.legalName || '未填公司'} · {item.status || '-'} · {item.billingStatus || '-'}</Text>
<Text className='platform-row-meta'> {item.planCode || '未订阅'} · {item.subscriptionStatus || '-'} · {item.subscriptionExpiresAt ? String(item.subscriptionExpiresAt).slice(0, 10) : '-'}</Text>
<Text className='platform-row-meta'> {item.planCode || '未订阅'} · {item.subscriptionStatus || '-'} · {dateText(item.subscriptionExpiresAt)}</Text>
<Text className='platform-row-meta'> {money(item.openBalanceCents)}</Text>
<View className='platform-row-actions'>
<Button className='platform-mini-button' onClick={() => setStatusForm(current => ({ ...current, tenantId: item.id, status: item.status || 'active', billingStatus: item.billingStatus || '' }))}></Button>
<Button className='platform-mini-button danger' onClick={() => setStatusForm({ tenantId: item.id, status: 'suspended', billingStatus: item.billingStatus || '', reason: 'platform manual suspend' })}></Button>
<Button className='platform-mini-button' loading={busy === 'detail' && selectedTenantId === item.id} onClick={() => chooseTenant(item)}></Button>
<Button className='platform-mini-button danger' onClick={() => {
setSelectedTenantId(item.id);
setStatusForm({ tenantId: item.id, status: 'suspended', billingStatus: item.billingStatus || '', reason: 'platform manual suspend' });
fillBillingForm(null, item.id);
}}></Button>
</View>
</View>
))}
</View>
{!tenants.length ? <View className='platform-empty'></View> : null}
</View>
{selectedTenant ? (
<View className='platform-section'>
<Text className='platform-section-title'></Text>
<View className='platform-grid'>
<View className='platform-metric'><Text className='platform-metric-label'></Text><Text className='platform-metric-value'>{selectedTenant.brandName || selectedTenant.name}</Text></View>
<View className='platform-metric'><Text className='platform-metric-label'></Text><Text className='platform-metric-value'>{selectedTenant.status || '-'}</Text></View>
<View className='platform-metric'><Text className='platform-metric-label'></Text><Text className='platform-metric-value'>{selectedTenant.billingStatus || '-'}</Text></View>
<View className='platform-metric'><Text className='platform-metric-label'></Text><Text className='platform-metric-value'>{selectedTenant.legalName || '-'}</Text></View>
</View>
<View className='platform-section'>
<Text className='platform-section-title'></Text>
<View className='platform-form compact'>
<View className='platform-field wide'><Text className='platform-field-label'> ID</Text><Input className='platform-input' placeholder='tenantId' value={statusForm.tenantId} onInput={event => updateStatusForm('tenantId', String(event.detail.value || ''))} /></View>
<View className='platform-field'><Text className='platform-field-label'></Text><Input className='platform-input' placeholder='active / suspended' value={statusForm.status} onInput={event => updateStatusForm('status', String(event.detail.value || ''))} /></View>
<View className='platform-field'><Text className='platform-field-label'></Text><Input className='platform-input' placeholder='trial / active / past_due' value={statusForm.billingStatus} onInput={event => updateStatusForm('billingStatus', String(event.detail.value || ''))} /></View>
<View className='platform-field wide'><Text className='platform-field-label'></Text><Input className='platform-input' placeholder='内部审计备注' value={statusForm.reason} onInput={event => updateStatusForm('reason', String(event.detail.value || ''))} /></View>
</View>
<View className='platform-actions'>
<Button className='platform-button primary' loading={busy === 'status'} onClick={submitStatusChange}></Button>
</View>
</View>
<View className='platform-section'>
<Text className='platform-section-title'></Text>
<View className='platform-form'>
<View className='platform-field wide'><Text className='platform-field-label'> ID</Text><Input className='platform-input' value={billingForm.tenantId} onInput={event => updateBillingForm('tenantId', String(event.detail.value || ''))} /></View>
<View className='platform-field'><Text className='platform-field-label'></Text><Input className='platform-input' placeholder='公司或个人抬头' value={billingForm.billingName} onInput={event => updateBillingForm('billingName', String(event.detail.value || ''))} /></View>
<View className='platform-field'><Text className='platform-field-label'></Text><Input className='platform-input' placeholder='可选' value={billingForm.taxId} onInput={event => updateBillingForm('taxId', String(event.detail.value || ''))} /></View>
<View className='platform-field'><Text className='platform-field-label'></Text><Input className='platform-input' value={billingForm.contactName} onInput={event => updateBillingForm('contactName', String(event.detail.value || ''))} /></View>
<View className='platform-field'><Text className='platform-field-label'></Text><Input className='platform-input' value={billingForm.contactPhone} onInput={event => updateBillingForm('contactPhone', String(event.detail.value || ''))} /></View>
<View className='platform-field'><Text className='platform-field-label'></Text><Input className='platform-input' value={billingForm.contactEmail} onInput={event => updateBillingForm('contactEmail', String(event.detail.value || ''))} /></View>
<View className='platform-field'><Text className='platform-field-label'></Text><Input className='platform-input' placeholder='none / normal_vat / special_vat' value={billingForm.invoiceType} onInput={event => updateBillingForm('invoiceType', String(event.detail.value || ''))} /></View>
<View className='platform-field wide'><Text className='platform-field-label'></Text><Input className='platform-input' value={billingForm.invoiceTitle} onInput={event => updateBillingForm('invoiceTitle', String(event.detail.value || ''))} /></View>
<View className='platform-field wide'><Text className='platform-field-label'></Text><Input className='platform-input' value={billingForm.billingAddress} onInput={event => updateBillingForm('billingAddress', String(event.detail.value || ''))} /></View>
<View className='platform-field'><Text className='platform-field-label'></Text><Input className='platform-input' value={billingForm.bankName} onInput={event => updateBillingForm('bankName', String(event.detail.value || ''))} /></View>
<View className='platform-field'><Text className='platform-field-label'></Text><Input className='platform-input' placeholder='仅保存掩码' value={billingForm.bankAccountMasked} onInput={event => updateBillingForm('bankAccountMasked', String(event.detail.value || ''))} /></View>
</View>
<View className='platform-actions'>
<Button className='platform-button primary' loading={busy === 'billing'} onClick={submitBillingProfile}></Button>
<Button className='platform-button' onClick={() => Taro.navigateTo({ url: '/pages/platform-admin/billing/index' })}></Button>
</View>
</View>
<View className='platform-section'>
<Text className='platform-section-title'> / / / </Text>
<View className='platform-list'>
{(detail?.domains || []).map(item => (
<View className='platform-row' key={item.id}>
<Text className='platform-row-main'>{item.host || item.id}</Text>
<Text className='platform-row-meta'>{item.domainType || 'domain'} · {item.status || '-'} · {item.isPrimary ? 'primary' : 'secondary'} · {dateText(item.verifiedAt)}</Text>
</View>
))}
{(detail?.subscriptions || []).slice(0, 3).map(item => (
<View className='platform-row' key={item.id}>
<Text className='platform-row-main'>{item.planCode || item.id}</Text>
<Text className='platform-row-meta'>{item.status || '-'} · {item.billingCycle || '-'} · {money(item.amountCents)} · {dateText(item.startsAt)} {dateText(item.expiresAt)}</Text>
</View>
))}
{(detail?.invoices || []).slice(0, 4).map(item => (
<View className='platform-row' key={item.id}>
<Text className='platform-row-main'>{item.invoiceNo || item.id}</Text>
<Text className='platform-row-meta'>{item.status || '-'} · {money(item.totalCents)} · {money(item.paidCents)} · {money(item.balanceCents)} · {dateText(item.dueDate)}</Text>
</View>
))}
{(detail?.usage || []).slice(0, 6).map(item => (
<View className='platform-row' key={item.id}>
<Text className='platform-row-main'>{item.metricKey || 'metric'}</Text>
<Text className='platform-row-meta'>{String(item.metricValue || 0)} · {dateText(item.periodStart)} {dateText(item.periodEnd)}</Text>
</View>
))}
</View>
</View>
<View className='platform-section'>
<Text className='platform-section-title'></Text>
<View className='platform-list'>
{auditLogs.map(item => (
<View className='platform-row' key={item.id}>
<Text className='platform-row-main'>{item.action || '-'}</Text>
<Text className='platform-row-meta'>{dateText(item.createdAt)} · {item.actorName || item.actorUsername || item.actorPhone || 'system'} · {item.targetType || '-'} · {item.targetId || '-'}</Text>
<Text className='platform-row-meta'>{auditDetailsText(item)}</Text>
</View>
))}
</View>
{!auditLogs.length ? <View className='platform-empty'></View> : null}
</View>
</View>
) : (
<View className='platform-empty'></View>
)}
{error ? <Text className='platform-error'>{error}</Text> : null}
</View>
</View>

View File

@@ -2,11 +2,13 @@ import { useEffect, useState } from 'react';
import Taro from '@tarojs/taro';
import { Button, Text, View } from '@tarojs/components';
import {
loadPlatformAuditLogs,
loadPlatformInvoices,
loadPlatformOverview,
loadPlatformQuestionBankGrants,
loadPlatformQuestionBanks,
loadPlatformTenants,
type PlatformAuditLogItem,
type PlatformInvoiceItem,
type PlatformOverview,
type PlatformQuestionBankGrant,
@@ -23,6 +25,7 @@ export default function PlatformWorkbenchPage() {
const [overview, setOverview] = useState<PlatformOverview | null>(null);
const [tenants, setTenants] = useState<PlatformTenantItem[]>([]);
const [invoices, setInvoices] = useState<PlatformInvoiceItem[]>([]);
const [auditLogs, setAuditLogs] = useState<PlatformAuditLogItem[]>([]);
const [banks, setBanks] = useState<PlatformQuestionBankItem[]>([]);
const [grants, setGrants] = useState<PlatformQuestionBankGrant[]>([]);
const [error, setError] = useState('');
@@ -34,12 +37,14 @@ export default function PlatformWorkbenchPage() {
loadPlatformInvoices({ limit: 6 }).catch(() => ({ items: [] })),
loadPlatformQuestionBanks({ limit: 6 }).catch(() => ({ items: [] })),
loadPlatformQuestionBankGrants({ limit: 6 }).catch(() => ({ items: [] })),
]).then(([overviewPayload, tenantPayload, invoicePayload, bankPayload, grantPayload]) => {
loadPlatformAuditLogs({ limit: 6 }).catch(() => ({ items: [] })),
]).then(([overviewPayload, tenantPayload, invoicePayload, bankPayload, grantPayload, auditPayload]) => {
setOverview(overviewPayload.item || null);
setTenants(tenantPayload.items || []);
setInvoices(invoicePayload.items || []);
setBanks(bankPayload.items || []);
setGrants(grantPayload.items || []);
setAuditLogs(auditPayload.items || []);
}).catch(nextError => setError(nextError instanceof Error ? nextError.message : '平台后台加载失败'));
}, []);
@@ -105,6 +110,18 @@ export default function PlatformWorkbenchPage() {
<View className='platform-metric'><Text className='platform-metric-label'></Text><Text className='platform-metric-value'>{String(overview?.billing?.overdueInvoices || 0)}</Text></View>
</View>
</View>
<View className='platform-section'>
<Text className='platform-section-title'></Text>
<View className='platform-list'>
{auditLogs.map(item => (
<View className='platform-row' key={item.id}>
<Text className='platform-row-main'>{item.action || '-'}</Text>
<Text className='platform-row-meta'>{item.tenantName || item.tenantSlug || '平台'} · {item.actorName || item.actorUsername || 'system'} · {item.targetType || '-'} · {String(item.createdAt || '').slice(0, 19).replace('T', ' ')}</Text>
</View>
))}
</View>
{!auditLogs.length ? <View className='platform-empty'></View> : null}
</View>
{error ? <Text className='platform-error'>{error}</Text> : null}
</View>
</View>

View File

@@ -54,6 +54,60 @@ export interface PlatformTenantItem {
openBalanceCents?: number | string | null;
}
export interface PlatformTenantBillingProfile {
tenantId?: string | null;
billingName?: string | null;
taxId?: string | null;
contactName?: string | null;
contactPhone?: string | null;
contactEmail?: string | null;
billingAddress?: string | null;
invoiceTitle?: string | null;
invoiceType?: string | null;
bankName?: string | null;
bankAccountMasked?: string | null;
metadata?: Record<string, unknown> | null;
}
export interface PlatformTenantDetailTenant extends PlatformTenantItem, PlatformTenantBillingProfile {
shortName?: string | null;
serviceWechat?: string | null;
ownerUserId?: string | null;
metadata?: Record<string, unknown> | null;
createdAt?: string | null;
updatedAt?: string | null;
}
export interface PlatformTenantDomain {
id: string;
host?: string | null;
domainType?: string | null;
status?: string | null;
isPrimary?: boolean | null;
verifiedAt?: string | null;
createdAt?: string | null;
}
export interface PlatformTenantSubscription {
id: string;
planCode?: string | null;
status?: string | null;
startsAt?: string | null;
expiresAt?: string | null;
billingCycle?: string | null;
amountCents?: number | string | null;
metadata?: Record<string, unknown> | null;
createdAt?: string | null;
}
export interface PlatformTenantDetail {
tenant?: PlatformTenantDetailTenant | null;
domains?: PlatformTenantDomain[];
subscriptions?: PlatformTenantSubscription[];
invoices?: PlatformInvoiceItem[];
usage?: PlatformUsageItem[];
}
export interface PlatformInvoiceItem {
id: string;
tenantId: string;
@@ -113,6 +167,24 @@ export interface PlatformQuestionBankGrant {
expiresAt?: string | null;
}
export interface PlatformAuditLogItem {
id: string;
tenantId?: string | null;
tenantSlug?: string | null;
tenantName?: string | null;
actorUserId?: string | null;
actorUsername?: string | null;
actorName?: string | null;
actorPhone?: string | null;
action?: string | null;
targetType?: string | null;
targetId?: string | null;
details?: Record<string, unknown> | null;
ipAddress?: string | null;
userAgent?: string | null;
createdAt?: string | null;
}
export interface CreatePlatformTenantInput {
slug: string;
name: string;
@@ -133,6 +205,10 @@ export interface UpdatePlatformTenantStatusInput {
reason?: string;
}
export interface UpsertPlatformTenantBillingProfileInput extends PlatformTenantBillingProfile {
tenantId: string;
}
export interface CreatePlatformSubscriptionInput {
tenantId: string;
planCode: string;
@@ -198,6 +274,20 @@ export async function loadPlatformTenants(query: { q?: string; status?: string;
});
}
export async function loadPlatformTenantDetail(tenantId: string) {
return apiRequest<{ item?: PlatformTenantDetail }>('/api/platform-admin/tenants/detail', {
query: { tenantId },
tenantId: null,
});
}
export async function loadPlatformAuditLogs(query: { tenantId?: string; action?: string; targetType?: string; actorUserId?: string; q?: string; limit?: number } = {}) {
return apiRequest<{ items?: PlatformAuditLogItem[] }>('/api/platform-admin/audit-logs', {
query: { ...query, limit: query.limit || 100 },
tenantId: null,
});
}
export async function loadPlatformInvoices(query: { tenantId?: string; status?: string; limit?: number } = {}) {
return apiRequest<{ items?: PlatformInvoiceItem[] }>('/api/platform-admin/invoices', {
query: { ...query, limit: query.limit || 80 },
@@ -242,6 +332,14 @@ export async function updatePlatformTenantStatus(input: UpdatePlatformTenantStat
});
}
export async function upsertPlatformTenantBillingProfile(input: UpsertPlatformTenantBillingProfileInput) {
return apiRequest<{ item?: PlatformTenantBillingProfile }>('/api/platform-admin/tenants/billing-profile', {
method: 'PUT',
body: input,
tenantId: null,
});
}
export async function createPlatformSubscription(input: CreatePlatformSubscriptionInput) {
return apiRequest<{ item?: Record<string, unknown> }>('/api/platform-admin/subscriptions', {
method: 'POST',