feat: add platform invoice dunning workflow

This commit is contained in:
Codex
2026-06-30 05:27:58 +08:00
parent f9f96bee63
commit c74432ce98
25 changed files with 858 additions and 23 deletions

View File

@@ -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'>