forked from wangziqi/gongxue-base
feat: add platform invoice dunning workflow
This commit is contained in:
@@ -7,11 +7,14 @@ import {
|
||||
createPlatformInvoiceFromSubscription,
|
||||
createPlatformInvoicesBatchFromSubscriptions,
|
||||
createPlatformSubscription,
|
||||
loadPlatformInvoiceReminders,
|
||||
loadPlatformInvoices,
|
||||
loadPlatformPlans,
|
||||
loadPlatformSubscriptionInvoiceCandidates,
|
||||
loadPlatformUsage,
|
||||
processPlatformOverdueInvoices,
|
||||
recordPlatformUsage,
|
||||
type PlatformInvoiceReminderItem,
|
||||
type PlatformInvoiceItem,
|
||||
type PlatformSaasPlan,
|
||||
type PlatformSubscriptionInvoiceCandidate,
|
||||
@@ -38,6 +41,7 @@ export default function PlatformBillingPage() {
|
||||
const [status, setStatus] = useState('');
|
||||
const [plans, setPlans] = useState<PlatformSaasPlan[]>([]);
|
||||
const [invoices, setInvoices] = useState<PlatformInvoiceItem[]>([]);
|
||||
const [reminders, setReminders] = useState<PlatformInvoiceReminderItem[]>([]);
|
||||
const [usage, setUsage] = useState<PlatformUsageItem[]>([]);
|
||||
const [candidates, setCandidates] = useState<PlatformSubscriptionInvoiceCandidate[]>([]);
|
||||
const [batchResult, setBatchResult] = useState('');
|
||||
@@ -77,13 +81,15 @@ export default function PlatformBillingPage() {
|
||||
Promise.all([
|
||||
loadPlatformPlans(true).catch(() => ({ items: [] })),
|
||||
loadPlatformInvoices({ status: nextStatus || undefined, limit: 100 }),
|
||||
loadPlatformInvoiceReminders({ limit: 80 }).catch(() => ({ items: [] })),
|
||||
loadPlatformUsage({ limit: 80 }).catch(() => ({ items: [] })),
|
||||
loadPlatformSubscriptionInvoiceCandidates({ daysAhead: Number(batchInvoiceForm.daysAhead || 45), limit: 100 }).catch(() => ({ items: [] })),
|
||||
]).then(([planPayload, invoicePayload, usagePayload, candidatePayload]) => {
|
||||
]).then(([planPayload, invoicePayload, reminderPayload, usagePayload, candidatePayload]) => {
|
||||
const nextPlans = planPayload.items || [];
|
||||
setPlans(nextPlans);
|
||||
setSubscriptionForm(current => ({ ...current, planCode: current.planCode || nextPlans[0]?.code || '' }));
|
||||
setInvoices(invoicePayload.items || []);
|
||||
setReminders(reminderPayload.items || []);
|
||||
setUsage(usagePayload.items || []);
|
||||
setCandidates(candidatePayload.items || []);
|
||||
}).catch(nextError => setError(nextError instanceof Error ? nextError.message : '账务数据加载失败'));
|
||||
@@ -283,6 +289,32 @@ export default function PlatformBillingPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function submitOverdueProcess(dryRun: boolean) {
|
||||
setError('');
|
||||
const ok = dryRun
|
||||
? true
|
||||
: await confirm('处理逾期账单', '确认扫描已过期未结清的平台服务费账单,并生成内部催缴记录?该动作不会自动停用租户。');
|
||||
if (!ok) return;
|
||||
setBusy(dryRun ? 'overdue-dry-run' : 'overdue-process');
|
||||
try {
|
||||
const payload = await processPlatformOverdueInvoices({
|
||||
dryRun,
|
||||
channel: 'internal',
|
||||
limit: 100,
|
||||
});
|
||||
const item = payload.item || {};
|
||||
setBatchResult(`${dryRun ? '逾期预览' : '逾期处理'}完成:处理 ${item.processed || 0},标记 ${item.markedOverdue || 0},催缴 ${item.reminderCreated || 0},跳过 ${item.skippedReminder || 0}`);
|
||||
if (!dryRun) {
|
||||
Taro.showToast({ title: '已处理', icon: 'success' });
|
||||
reload(status);
|
||||
}
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : '逾期处理失败');
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='platform-page'>
|
||||
<View className='platform-shell'>
|
||||
@@ -381,6 +413,25 @@ export default function PlatformBillingPage() {
|
||||
{!candidates.length ? <View className='platform-empty'>暂无即将到期且未开票的订阅。</View> : null}
|
||||
</View>
|
||||
|
||||
<View className='platform-section'>
|
||||
<Text className='platform-section-title'>逾期与催缴</Text>
|
||||
<View className='platform-actions'>
|
||||
<Button className='platform-button' loading={busy === 'overdue-dry-run'} onClick={() => submitOverdueProcess(true)}>预览逾期</Button>
|
||||
<Button className='platform-button primary' loading={busy === 'overdue-process'} onClick={() => submitOverdueProcess(false)}>生成催缴</Button>
|
||||
</View>
|
||||
<View className='platform-list'>
|
||||
{reminders.slice(0, 12).map(item => (
|
||||
<View className='platform-row' key={item.id}>
|
||||
<Text className='platform-row-main'>{item.invoiceNo || item.invoiceId}</Text>
|
||||
<Text className='platform-row-meta'>{item.tenantName || item.tenantSlug || item.tenantId} · {item.reminderType || 'reminder'} · {item.channel || 'internal'} · {item.status || '-'}</Text>
|
||||
<Text className='platform-row-meta'>第 {String(item.reminderLevel || 1)} 次 · 账单到期 {item.dueDate ? String(item.dueDate).slice(0, 10) : '-'} · 快照余额 {money(item.balanceCentsSnapshot)}</Text>
|
||||
<Text className='platform-row-meta'>{item.message || '暂无催缴备注'}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{!reminders.length ? <View className='platform-empty'>暂无催缴记录。</View> : null}
|
||||
</View>
|
||||
|
||||
<View className='platform-section'>
|
||||
<Text className='platform-section-title'>SaaS 套餐</Text>
|
||||
<View className='platform-list'>
|
||||
|
||||
@@ -126,6 +126,24 @@ export interface PlatformInvoiceItem {
|
||||
note?: string | null;
|
||||
}
|
||||
|
||||
export interface PlatformInvoiceReminderItem {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
tenantSlug?: string | null;
|
||||
tenantName?: string | null;
|
||||
invoiceId: string;
|
||||
invoiceNo?: string | null;
|
||||
reminderType?: string | null;
|
||||
channel?: string | null;
|
||||
status?: string | null;
|
||||
reminderDate?: string | null;
|
||||
reminderLevel?: number | string | null;
|
||||
dueDate?: string | null;
|
||||
balanceCentsSnapshot?: number | string | null;
|
||||
message?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
|
||||
export interface PlatformUsageItem {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
@@ -265,6 +283,12 @@ export interface ConfirmPlatformInvoicePaymentInput {
|
||||
providerTradeNo?: string;
|
||||
}
|
||||
|
||||
export interface ProcessPlatformOverdueInvoicesInput {
|
||||
dryRun?: boolean;
|
||||
channel?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface RecordPlatformUsageInput {
|
||||
tenantId: string;
|
||||
metricKey: string;
|
||||
@@ -417,6 +441,34 @@ export async function confirmPlatformInvoicePayment(input: ConfirmPlatformInvoic
|
||||
});
|
||||
}
|
||||
|
||||
export async function processPlatformOverdueInvoices(input: ProcessPlatformOverdueInvoicesInput = {}) {
|
||||
return apiRequest<{
|
||||
item?: {
|
||||
dryRun?: boolean;
|
||||
processed?: number;
|
||||
markedOverdue?: number;
|
||||
reminderCreated?: number;
|
||||
skippedReminder?: number;
|
||||
items?: Array<PlatformInvoiceItem & {
|
||||
wouldMarkOverdue?: boolean;
|
||||
wouldCreateReminder?: boolean;
|
||||
reminderCreated?: boolean;
|
||||
}>;
|
||||
};
|
||||
}>('/api/platform-admin/invoices/process-overdue', {
|
||||
method: 'POST',
|
||||
body: input,
|
||||
tenantId: null,
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadPlatformInvoiceReminders(query: { tenantId?: string; invoiceId?: string; status?: string; reminderType?: string; limit?: number } = {}) {
|
||||
return apiRequest<{ items?: PlatformInvoiceReminderItem[] }>('/api/platform-admin/invoices/reminders', {
|
||||
query: { ...query, limit: query.limit || 100 },
|
||||
tenantId: null,
|
||||
});
|
||||
}
|
||||
|
||||
export async function recordPlatformUsage(input: RecordPlatformUsageInput) {
|
||||
return apiRequest<{ item?: Record<string, unknown> }>('/api/platform-admin/usage', {
|
||||
method: 'POST',
|
||||
|
||||
Reference in New Issue
Block a user