Files
accounted/app/api/invoices/[id]/mark-paid/__tests__/route.test.ts
T
Jakob WennbergandClaude Opus 4.6 4031ab680d fix: downgrade missing extension env vars to warning, add error boundaries and cron auth fixes
- Change missing extension env vars from throw to log.warn (graceful degradation)
- Move initialized flag after all init steps complete
- Add error.tsx and loading.tsx for dashboard error boundaries
- Fix cron route auth header checks
- Add ensureInitialized() to journal entry reverse and invoice mark-paid/sent routes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:09:23 +01:00

193 lines
6.7 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import {
createMockRequest,
parseJsonResponse,
createMockRouteParams,
createQueuedMockSupabase,
makeInvoice,
makeCustomer,
} from '@/tests/helpers'
const { supabase: mockSupabase, enqueue, reset } = createQueuedMockSupabase()
vi.mock('@/lib/supabase/server', () => ({
createClient: () => Promise.resolve(mockSupabase),
}))
vi.mock('@/lib/init', () => ({
ensureInitialized: vi.fn(),
}))
const mockCreateInvoicePaymentJournalEntry = vi.fn()
const mockCreateInvoiceCashEntry = vi.fn()
vi.mock('@/lib/bookkeeping/invoice-entries', () => ({
createInvoicePaymentJournalEntry: (...args: unknown[]) =>
mockCreateInvoicePaymentJournalEntry(...args),
createInvoiceCashEntry: (...args: unknown[]) =>
mockCreateInvoiceCashEntry(...args),
}))
import { POST } from '../route'
describe('POST /api/invoices/[id]/mark-paid', () => {
const mockUser = { id: 'user-1', email: 'test@test.se' }
beforeEach(() => {
vi.clearAllMocks()
reset()
mockSupabase.auth.getUser.mockResolvedValue({ data: { user: mockUser } })
})
it('returns 401 when not authenticated', async () => {
mockSupabase.auth.getUser.mockResolvedValue({ data: { user: null } })
const request = createMockRequest('/api/invoices/inv-1/mark-paid', { method: 'POST' })
const response = await POST(request, createMockRouteParams({ id: 'inv-1' }))
const { status, body } = await parseJsonResponse(response)
expect(status).toBe(401)
expect(body).toEqual({ error: 'Unauthorized' })
})
it('returns 404 when invoice not found', async () => {
enqueue({ data: null, error: { message: 'Not found' } })
const request = createMockRequest('/api/invoices/inv-1/mark-paid', { method: 'POST' })
const response = await POST(request, createMockRouteParams({ id: 'inv-1' }))
const { status, body } = await parseJsonResponse<{ error: string }>(response)
expect(status).toBe(404)
expect(body.error).toBe('Fakturan hittades inte')
})
it('returns 400 when invoice is in draft status', async () => {
const invoice = makeInvoice({ status: 'draft' })
enqueue({ data: invoice, error: null })
const request = createMockRequest('/api/invoices/inv-1/mark-paid', { method: 'POST' })
const response = await POST(request, createMockRouteParams({ id: 'inv-1' }))
const { status, body } = await parseJsonResponse<{ error: string }>(response)
expect(status).toBe(400)
expect(body.error).toBe('Fakturan kan inte markeras som betald i nuvarande status')
})
it('returns 400 when invoice is already paid', async () => {
const invoice = makeInvoice({ status: 'paid' })
enqueue({ data: invoice, error: null })
const request = createMockRequest('/api/invoices/inv-1/mark-paid', { method: 'POST' })
const response = await POST(request, createMockRouteParams({ id: 'inv-1' }))
const { status, body } = await parseJsonResponse<{ error: string }>(response)
expect(status).toBe(400)
expect(body.error).toBe('Fakturan kan inte markeras som betald i nuvarande status')
})
it('returns 400 when invoice is credited', async () => {
const invoice = makeInvoice({ status: 'credited' })
enqueue({ data: invoice, error: null })
const request = createMockRequest('/api/invoices/inv-1/mark-paid', { method: 'POST' })
const response = await POST(request, createMockRouteParams({ id: 'inv-1' }))
const { status, body } = await parseJsonResponse<{ error: string }>(response)
expect(status).toBe(400)
})
it('marks sent invoice as paid with accrual method', async () => {
const customer = makeCustomer()
const invoice = makeInvoice({
id: 'inv-1',
status: 'sent',
total: 12500,
customer,
})
// Fetch invoice
enqueue({ data: invoice, error: null })
// Update invoice status
enqueue({ data: null, error: null })
// Fetch company settings
enqueue({ data: { accounting_method: 'accrual', entity_type: 'enskild_firma' }, error: null })
mockCreateInvoicePaymentJournalEntry.mockResolvedValue({ id: 'je-1' })
const request = createMockRequest('/api/invoices/inv-1/mark-paid', { method: 'POST' })
const response = await POST(request, createMockRouteParams({ id: 'inv-1' }))
const { status, body } = await parseJsonResponse<{
success: boolean
status: string
paid_amount: number
journal_entry_id: string | null
}>(response)
expect(status).toBe(200)
expect(body.success).toBe(true)
expect(body.status).toBe('paid')
expect(body.paid_amount).toBe(12500)
expect(body.journal_entry_id).toBe('je-1')
expect(mockCreateInvoicePaymentJournalEntry).toHaveBeenCalledWith(
expect.anything(),
'user-1',
expect.objectContaining({ id: 'inv-1' }),
expect.any(String),
undefined
)
})
it('marks overdue invoice as paid with cash method', async () => {
const customer = makeCustomer()
const invoice = makeInvoice({
id: 'inv-1',
status: 'overdue',
total: 12500,
customer,
})
enqueue({ data: invoice, error: null })
enqueue({ data: null, error: null })
enqueue({ data: { accounting_method: 'cash', entity_type: 'enskild_firma' }, error: null })
mockCreateInvoiceCashEntry.mockResolvedValue({ id: 'je-2' })
const request = createMockRequest('/api/invoices/inv-1/mark-paid', { method: 'POST' })
const response = await POST(request, createMockRouteParams({ id: 'inv-1' }))
const { status, body } = await parseJsonResponse<{
success: boolean
journal_entry_id: string | null
}>(response)
expect(status).toBe(200)
expect(body.success).toBe(true)
expect(body.journal_entry_id).toBe('je-2')
expect(mockCreateInvoiceCashEntry).toHaveBeenCalledWith(
expect.anything(),
'user-1',
expect.objectContaining({ id: 'inv-1' }),
expect.any(String),
'enskild_firma'
)
})
it('returns success with null journal_entry_id when journal entry creation fails', async () => {
const invoice = makeInvoice({ id: 'inv-1', status: 'sent', total: 12500 })
enqueue({ data: invoice, error: null })
enqueue({ data: null, error: null })
enqueue({ data: { accounting_method: 'accrual', entity_type: 'enskild_firma' }, error: null })
mockCreateInvoicePaymentJournalEntry.mockRejectedValue(new Error('Period locked'))
const request = createMockRequest('/api/invoices/inv-1/mark-paid', { method: 'POST' })
const response = await POST(request, createMockRouteParams({ id: 'inv-1' }))
const { status, body } = await parseJsonResponse<{
success: boolean
journal_entry_id: string | null
}>(response)
expect(status).toBe(200)
expect(body.success).toBe(true)
expect(body.journal_entry_id).toBeNull()
})
})