feat: add platform subscription invoice batching

This commit is contained in:
Codex
2026-06-30 04:56:28 +08:00
parent d5d32e84b3
commit 5320558d13
16 changed files with 670 additions and 126 deletions
+104
View File
@@ -1232,6 +1232,110 @@ async function testPlatformTenantOperationsAndAudit() {
assert.ok(actions.has('platform.tenant.billing_profile_upserted'), 'platform audit should include billing profile update');
assert.ok(actions.has('platform.tenant.status_updated'), 'platform audit should include status update');
const candidates = await request('/api/platform-admin/invoices/subscription-candidates', {
tenantId: false,
userId: false,
headers: adminHeaders,
query: { tenantIds: tenantId, daysAhead: 400, limit: 20 },
});
const candidate = candidates.items?.find(item => item.tenantId === tenantId);
assert.ok(candidate?.subscriptionId, 'platform admin should preview subscription invoice candidates');
assert.equal(candidate.hasExistingInvoice, false, 'new tenant subscription should not have an existing invoice');
const invalidCandidateQuery = await request('/api/platform-admin/invoices/subscription-candidates', {
tenantId: false,
userId: false,
headers: adminHeaders,
query: { tenantIds: 'not-a-uuid' },
expectStatus: 400,
});
assert.equal(invalidCandidateQuery.code, 'INVALID_UUID', 'subscription candidate query should reject malformed UUID filters');
const candidateBySubscription = await request('/api/platform-admin/invoices/subscription-candidates', {
tenantId: false,
userId: false,
headers: adminHeaders,
query: { subscriptionIds: candidate.subscriptionId, daysAhead: 400, limit: 20 },
});
assert.equal(candidateBySubscription.items?.length, 1, 'subscription candidate query should support precise subscription filters');
assert.equal(candidateBySubscription.items?.[0]?.subscriptionId, candidate.subscriptionId, 'subscription candidate filter should return the requested subscription');
const dryRunBatch = await request('/api/platform-admin/invoices/from-subscriptions-batch', {
tenantId: false,
userId: false,
headers: adminHeaders,
method: 'POST',
body: {
tenantIds: [tenantId],
daysAhead: 400,
dryRun: true,
dueDate: '2026-07-31',
note: 'integration dry-run',
},
});
assert.equal(dryRunBatch.item?.dryRun, true, 'batch subscription invoice dry-run should not create invoices');
assert.ok(dryRunBatch.item?.items?.some(item => item.subscriptionId === candidate.subscriptionId), 'dry-run should include the candidate subscription');
const invalidBatchStatus = await request('/api/platform-admin/invoices/from-subscriptions-batch', {
tenantId: false,
userId: false,
headers: adminHeaders,
method: 'POST',
body: {
tenantIds: [tenantId],
daysAhead: 400,
status: 'unexpected',
},
expectStatus: 400,
});
assert.equal(invalidBatchStatus.code, 'INVALID_INVOICE_STATUS', 'batch subscription invoice should reject invalid invoice statuses');
const batchCreated = await request('/api/platform-admin/invoices/from-subscriptions-batch', {
tenantId: false,
userId: false,
headers: adminHeaders,
method: 'POST',
body: {
tenantIds: [tenantId],
daysAhead: 400,
dueDate: '2026-07-31',
note: 'integration batch invoice',
},
});
assert.equal(batchCreated.item?.createdCount, 1, 'batch subscription invoice should create one invoice');
assert.equal(batchCreated.item?.skippedCount, 0, 'first batch subscription invoice should not skip');
const duplicateBatch = await request('/api/platform-admin/invoices/from-subscriptions-batch', {
tenantId: false,
userId: false,
headers: adminHeaders,
method: 'POST',
body: {
tenantIds: [tenantId],
daysAhead: 400,
dueDate: '2026-07-31',
},
});
assert.equal(duplicateBatch.item?.createdCount, 0, 'duplicate batch invoice run should not create another invoice');
const duplicateSingle = await request('/api/platform-admin/invoices/from-subscription', {
tenantId: false,
userId: false,
headers: adminHeaders,
method: 'POST',
body: { tenantId },
expectStatus: 409,
});
assert.equal(duplicateSingle.code, 'SUBSCRIPTION_INVOICE_EXISTS', 'single subscription invoice should reject duplicates');
const auditAfterBatch = await request('/api/platform-admin/audit-logs', {
tenantId: false,
userId: false,
headers: adminHeaders,
query: { q: 'platform.invoice.subscription_batch_created', limit: 20 },
});
assert.ok(auditAfterBatch.items?.some(item => item.action === 'platform.invoice.subscription_batch_created'), 'batch invoice creation should write platform audit');
const studentAuditDenied = await request('/api/platform-admin/audit-logs', {
tenantId: false,
userId: USER_ID,