billing invoice

This commit is contained in:
root
2026-05-25 05:35:55 -04:00
parent 8d0b7a5ceb
commit 33b6bb55f0
12 changed files with 5434 additions and 645 deletions
@@ -136,4 +136,194 @@ describe('Admin API', () => {
)
})
})
describe('Billing admin workflows', () => {
let billingAccountId: string
beforeAll(async () => {
const res = await request(app)
.get('/api/v1/admin/billing')
.set(authHeader(financeToken))
expect(res.status).toBe(200)
const account = res.body.data.data.find((item: any) => item.company.id === companyId)
expect(account).toBeTruthy()
billingAccountId = account.id
})
it('returns billing account detail', async () => {
const res = await request(app)
.get(`/api/v1/admin/billing/${companyId}`)
.set(authHeader(financeToken))
expect(res.status).toBe(200)
expect(res.body.data.id).toBe(billingAccountId)
expect(res.body.data.company.id).toBe(companyId)
expect(Array.isArray(res.body.data.invoices)).toBe(true)
})
it('supports create, finalize, pay, refund, and pdf download', async () => {
const draftRes = await request(app)
.post(`/api/v1/admin/billing/accounts/${billingAccountId}/invoices`)
.set(authHeader(financeToken))
.send({
invoiceType: 'MANUAL',
adminReason: 'Enterprise onboarding',
lineItems: [
{
type: 'SETUP_FEE',
description: 'Onboarding setup',
quantity: 1,
unitAmount: 50000,
},
{
type: 'PROFESSIONAL_SERVICES',
description: 'Migration assistance',
quantity: 2,
unitAmount: 15000,
},
],
})
expect(draftRes.status).toBe(201)
expect(draftRes.body.data.status).toBe('DRAFT')
const invoiceId = draftRes.body.data.id as string
const finalizeRes = await request(app)
.post(`/api/v1/admin/billing/invoices/${invoiceId}/finalize`)
.set(authHeader(financeToken))
.send({})
expect(finalizeRes.status).toBe(200)
expect(finalizeRes.body.data.status).toBe('OPEN')
expect(finalizeRes.body.data.invoiceNumber).toMatch(/^INV-\d{4}-\d{6}$/)
expect(finalizeRes.body.data.totalAmount).toBe(80000)
const pdfRes = await request(app)
.get(`/api/v1/admin/billing/invoices/${invoiceId}/pdf`)
.set(authHeader(financeToken))
expect(pdfRes.status).toBe(200)
expect(pdfRes.headers['content-type']).toContain('application/pdf')
const payRes = await request(app)
.post(`/api/v1/admin/billing/invoices/${invoiceId}/pay`)
.set(authHeader(financeToken))
.send({ amount: 80000, providerPaymentId: 'manual-payment-1' })
expect(payRes.status).toBe(200)
expect(payRes.body.data.status).toBe('PAID')
expect(payRes.body.data.amountDue).toBe(0)
const refundRes = await request(app)
.post(`/api/v1/admin/billing/invoices/${invoiceId}/refunds`)
.set(authHeader(financeToken))
.send({ amount: 20000, reason: 'Service credit after go-live issue' })
expect(refundRes.status).toBe(200)
expect(refundRes.body.data.status).toBe('PARTIALLY_REFUNDED')
})
it('supports credit notes, dunning controls, voids, and write-offs', async () => {
const pauseRes = await request(app)
.post(`/api/v1/admin/billing/accounts/${billingAccountId}/pause-dunning`)
.set(authHeader(financeToken))
.send({})
expect(pauseRes.status).toBe(200)
expect(pauseRes.body.data.dunningPaused).toBe(true)
const invoiceRes = await request(app)
.post(`/api/v1/admin/billing/accounts/${billingAccountId}/invoices`)
.set(authHeader(financeToken))
.send({
invoiceType: 'MANUAL',
lineItems: [
{
type: 'MANUAL_ADJUSTMENT',
description: 'Support package',
quantity: 1,
unitAmount: 30000,
},
],
})
expect(invoiceRes.status).toBe(201)
const invoiceId = invoiceRes.body.data.id as string
const finalizeRes = await request(app)
.post(`/api/v1/admin/billing/invoices/${invoiceId}/finalize`)
.set(authHeader(financeToken))
.send({})
expect(finalizeRes.status).toBe(200)
expect(finalizeRes.body.data.status).toBe('OPEN')
const creditRes = await request(app)
.post(`/api/v1/admin/billing/invoices/${invoiceId}/credit-notes`)
.set(authHeader(financeToken))
.send({ amount: 5000, reason: 'Manual goodwill credit' })
expect(creditRes.status).toBe(200)
expect(creditRes.body.data.amountDue).toBe(25000)
const retryRes = await request(app)
.post(`/api/v1/admin/billing/invoices/${invoiceId}/retry-payment`)
.set(authHeader(financeToken))
.send({})
expect(retryRes.status).toBe(200)
expect(retryRes.body.data.status).toBe('PAYMENT_PENDING')
const voidRes = await request(app)
.post(`/api/v1/admin/billing/invoices/${invoiceId}/void`)
.set(authHeader(financeToken))
.send({ reason: 'Customer rolled into replacement invoice' })
expect(voidRes.status).toBe(200)
expect(voidRes.body.data.status).toBe('VOID')
const badDebtRes = await request(app)
.post(`/api/v1/admin/billing/accounts/${billingAccountId}/invoices`)
.set(authHeader(financeToken))
.send({
invoiceType: 'SUBSCRIPTION_RENEWAL',
isSubscriptionBlocking: true,
lineItems: [
{
type: 'SUBSCRIPTION_FEE',
description: 'Renewal period',
quantity: 1,
unitAmount: 12000,
},
],
})
expect(badDebtRes.status).toBe(201)
const badDebtInvoiceId = badDebtRes.body.data.id as string
const finalizeBadDebtRes = await request(app)
.post(`/api/v1/admin/billing/invoices/${badDebtInvoiceId}/finalize`)
.set(authHeader(financeToken))
.send({})
expect(finalizeBadDebtRes.status).toBe(200)
const writeoffRes = await request(app)
.post(`/api/v1/admin/billing/invoices/${badDebtInvoiceId}/uncollectible`)
.set(authHeader(financeToken))
.send({ reason: 'Customer account closed after failed recovery' })
expect(writeoffRes.status).toBe(200)
expect(writeoffRes.body.data.status).toBe('UNCOLLECTIBLE')
const resumeRes = await request(app)
.post(`/api/v1/admin/billing/accounts/${billingAccountId}/resume-dunning`)
.set(authHeader(financeToken))
.send({})
expect(resumeRes.status).toBe(200)
expect(resumeRes.body.data.dunningPaused).toBe(false)
})
})
})
+11
View File
@@ -2,6 +2,17 @@ import { beforeAll, afterAll } from 'vitest'
import { prisma } from '../lib/prisma'
const delegates = [
'billingEvent',
'billingRefund',
'billingCreditNote',
'billingCreditLedgerEntry',
'billingCreditBalance',
'billingPaymentAttempt',
'billingPaymentIntent',
'billingInvoiceLineItem',
'billingInvoice',
'billingPaymentMethod',
'billingAccount',
'auditLog',
'adminPermission',
'adminUser',