forked from wangziqi/gongxue-base
feat: add platform dunning notifications
This commit is contained in:
@@ -1602,6 +1602,107 @@ async function testPlatformTenantOperationsAndAudit() {
|
||||
query: { tenantId, invoiceId: ids.platformOverdueInvoice, limit: 10 },
|
||||
});
|
||||
assert.ok(reminders.items?.some(item => item.invoiceId === ids.platformOverdueInvoice && item.reminderType === 'overdue'), 'platform admin should list invoice reminders');
|
||||
const platformReminder = reminders.items?.find(item => item.invoiceId === ids.platformOverdueInvoice && item.reminderType === 'overdue');
|
||||
assert.ok(platformReminder?.id, 'platform overdue reminder should expose reminder id');
|
||||
|
||||
const dunningChannel = await request('/api/platform-admin/dunning-notification-channels', {
|
||||
tenantId: false,
|
||||
userId: false,
|
||||
headers: adminHeaders,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
channelCode: 'integration_platform_dunning',
|
||||
name: '集成测试平台催缴通知',
|
||||
provider: 'generic',
|
||||
webhookUrl: 'https://ops.example.test/platform-dunning',
|
||||
secret: 'integration-platform-dunning-notification-secret',
|
||||
reminderTypes: ['overdue'],
|
||||
reminderChannels: ['internal'],
|
||||
minReminderLevel: 1,
|
||||
tenantIds: [tenantId],
|
||||
timeoutSec: 5,
|
||||
metadata: { owner: 'finance' },
|
||||
},
|
||||
});
|
||||
assert.equal(dunningChannel.item?.channelCode, 'integration_platform_dunning', 'platform admin should upsert dunning notification channel');
|
||||
assert.equal(dunningChannel.item?.secretRef, 'app_private.platform_secrets:webhook:platform_dunning_integration_platform_dunning', 'dunning channel should expose only platform secretRef');
|
||||
assert.equal(dunningChannel.item?.webhook?.host, 'ops.example.test', 'dunning channel response should expose safe webhook host');
|
||||
assert.equal(dunningChannel.item?.webhookUrl, undefined, 'dunning channel response must not expose raw webhook URL');
|
||||
assert.ok(!JSON.stringify(dunningChannel).includes('integration-platform-dunning-notification-secret'), 'dunning channel response must not leak webhook secret');
|
||||
|
||||
const dunningChannels = await request('/api/platform-admin/dunning-notification-channels', {
|
||||
tenantId: false,
|
||||
userId: false,
|
||||
headers: adminHeaders,
|
||||
query: { enabled: true, provider: 'generic', limit: 20 },
|
||||
});
|
||||
assert.ok(
|
||||
dunningChannels.items?.some(item => item.channelCode === 'integration_platform_dunning'),
|
||||
'platform admin should list dunning notification channels',
|
||||
);
|
||||
assert.ok(!JSON.stringify(dunningChannels).includes('integration-platform-dunning-notification-secret'), 'dunning channel list must not leak webhook secret');
|
||||
|
||||
const invalidDunningChannel = await request('/api/platform-admin/dunning-notification-channels', {
|
||||
tenantId: false,
|
||||
userId: false,
|
||||
headers: adminHeaders,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
channelCode: 'bad_dunning_channel',
|
||||
name: 'bad dunning channel',
|
||||
provider: 'generic',
|
||||
webhookUrl: 'ftp://ops.example.test/hook',
|
||||
},
|
||||
expectStatus: 400,
|
||||
});
|
||||
assert.equal(invalidDunningChannel.code, 'INVALID_WEBHOOK_URL', 'dunning notification channel should reject unsafe webhook URL');
|
||||
|
||||
const dunningEventPool = new pg.Pool({ connectionString: process.env.DATABASE_URL || DEFAULT_DATABASE_URL });
|
||||
let platformDunningNotificationEventId = '';
|
||||
try {
|
||||
const channelRow = await dunningEventPool.query(
|
||||
"select id from public.platform_dunning_notification_channels where channel_code = 'integration_platform_dunning' limit 1",
|
||||
);
|
||||
const insertedEvent = await dunningEventPool.query(
|
||||
`
|
||||
insert into public.platform_dunning_notification_events (
|
||||
channel_id, reminder_id, invoice_id, tenant_id, provider, status, attempts,
|
||||
last_http_code, request_payload, last_response_summary
|
||||
)
|
||||
values (
|
||||
$1, $2, $3, $4, 'generic', 'sent', 1,
|
||||
200,
|
||||
'{"body":{"token":"must-not-leak","nested":{"password":"must-not-leak"}}}'::jsonb,
|
||||
'{"ok":true}'
|
||||
)
|
||||
on conflict (channel_id, reminder_id)
|
||||
do update set status = excluded.status,
|
||||
attempts = excluded.attempts,
|
||||
request_payload = excluded.request_payload,
|
||||
updated_at = now()
|
||||
returning id
|
||||
`,
|
||||
[channelRow.rows[0].id, platformReminder.id, ids.platformOverdueInvoice, tenantId],
|
||||
);
|
||||
platformDunningNotificationEventId = insertedEvent.rows[0].id;
|
||||
} finally {
|
||||
await dunningEventPool.end();
|
||||
}
|
||||
|
||||
const dunningEvents = await request('/api/platform-admin/dunning-notification-events', {
|
||||
tenantId: false,
|
||||
userId: false,
|
||||
headers: adminHeaders,
|
||||
query: { reminderId: platformReminder.id, status: 'sent', limit: 20 },
|
||||
});
|
||||
assert.ok(
|
||||
dunningEvents.items?.some(item => item.id === platformDunningNotificationEventId),
|
||||
'platform admin should list dunning notification events',
|
||||
);
|
||||
const listedDunningEvent = dunningEvents.items?.find(item => item.id === platformDunningNotificationEventId);
|
||||
assert.equal(listedDunningEvent?.requestPayload?.body?.token, '[REDACTED]', 'dunning event list should redact token-like payload details');
|
||||
assert.equal(listedDunningEvent?.requestPayload?.body?.nested?.password, '[REDACTED]', 'dunning event list should redact nested password-like payload details');
|
||||
assert.ok(!JSON.stringify(dunningEvents).includes('must-not-leak'), 'dunning event list must not leak sensitive details');
|
||||
|
||||
const overdueAudit = await request('/api/platform-admin/audit-logs', {
|
||||
tenantId: false,
|
||||
@@ -1668,6 +1769,20 @@ async function testPlatformTenantOperationsAndAudit() {
|
||||
});
|
||||
assert.equal(studentAuditNotificationEventDenied.code, 'PLATFORM_ADMIN_REQUIRED', 'student must not read platform audit notification events');
|
||||
|
||||
const studentDunningNotificationChannelDenied = await request('/api/platform-admin/dunning-notification-channels', {
|
||||
tenantId: false,
|
||||
userId: USER_ID,
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(studentDunningNotificationChannelDenied.code, 'PLATFORM_ADMIN_REQUIRED', 'student must not read platform dunning notification channels');
|
||||
|
||||
const studentDunningNotificationEventDenied = await request('/api/platform-admin/dunning-notification-events', {
|
||||
tenantId: false,
|
||||
userId: USER_ID,
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(studentDunningNotificationEventDenied.code, 'PLATFORM_ADMIN_REQUIRED', 'student must not read platform dunning notification events');
|
||||
|
||||
const studentReminderDenied = await request('/api/platform-admin/invoices/reminders', {
|
||||
tenantId: false,
|
||||
userId: USER_ID,
|
||||
|
||||
Reference in New Issue
Block a user