From 3e82295cceb12f9adfeff889698e30fdf240d079 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg <149234542+jakobwennberg@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:20:29 +0100 Subject: [PATCH] feat: semi-manual invoice payment booking dialog (#37) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: include reversed entries in all reports (general ledger, trial balance, VAT, SIE, NE, INK2) Reversed entries (storno) must appear alongside their original posted entries in reports for a complete audit trail. Previously, filtering by status='posted' excluded them, causing discrepancies when corrections had been made. Co-Authored-By: Claude Opus 4.6 (1M context) * feat: semi-manual invoice payment booking with editable journal lines When marking an invoice as paid, users now see a dialog where they can: - Choose which bank/cash account the payment goes to (1910, 1920, 1930, etc.) - Review and edit the proposed journal entry lines before committing - The happy path remains fast — lines are pre-filled correctly Implementation: - Pure proposePaymentLines() function for line computation (accrual + cash) - PaymentBookingDialog with AccountCombobox, balance validation, date picker - API accepts optional custom lines, falls back to auto-generation without them - 18 tests (8 unit + 10 API) all passing Co-Authored-By: Claude Opus 4.6 (1M context) * fix: address Greptile review — validation fallback, balance check, error handling - P1: Return 400 on invalid body instead of silently falling back to auto-generated lines (split JSON parse from schema validation) - P1: Add server-side balance check for custom lines before committing (debit must equal credit, totalDebit > 0) - P2: Wrap PaymentBookingDialog init() in try/catch with toast on failure and auto-close instead of silent empty state - Add 2 new tests: unbalanced lines → 400, invalid schema → 400 Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- app/(dashboard)/invoices/[id]/page.tsx | 28 +- .../[id]/mark-paid/__tests__/route.test.ts | 140 ++++++++ app/api/invoices/[id]/mark-paid/route.ts | 51 ++- components/invoices/PaymentBookingDialog.tsx | 330 ++++++++++++++++++ lib/api/schemas.ts | 6 + .../__tests__/propose-payment-lines.test.ts | 212 +++++++++++ lib/bookkeeping/propose-payment-lines.ts | 239 +++++++++++++ lib/reports/general-ledger.ts | 8 +- lib/reports/ink2/ink2-engine.ts | 2 +- lib/reports/ne-bilaga/ne-engine.ts | 2 +- lib/reports/sie-export.ts | 2 +- lib/reports/trial-balance.ts | 2 +- lib/reports/vat-declaration.ts | 4 +- 13 files changed, 996 insertions(+), 30 deletions(-) create mode 100644 components/invoices/PaymentBookingDialog.tsx create mode 100644 lib/bookkeeping/__tests__/propose-payment-lines.test.ts create mode 100644 lib/bookkeeping/propose-payment-lines.ts diff --git a/app/(dashboard)/invoices/[id]/page.tsx b/app/(dashboard)/invoices/[id]/page.tsx index 89c4d2dc..700ed5b8 100644 --- a/app/(dashboard)/invoices/[id]/page.tsx +++ b/app/(dashboard)/invoices/[id]/page.tsx @@ -31,6 +31,7 @@ import { MessageSquare, Trash2, } from 'lucide-react' +import PaymentBookingDialog from '@/components/invoices/PaymentBookingDialog' import type { Invoice, InvoiceItem, Customer, InvoiceStatus, InvoiceReminder, InvoiceDocumentType } from '@/types' const statusConfig: Record = { @@ -65,6 +66,7 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st const [creditNote, setCreditNote] = useState(null) const [originalInvoice, setOriginalInvoice] = useState(null) const [convertedFromInvoice, setConvertedFromInvoice] = useState(null) + const [showPaymentDialog, setShowPaymentDialog] = useState(false) const [isConverting, setIsConverting] = useState(false) const [isLoading, setIsLoading] = useState(true) const [isUpdating, setIsUpdating] = useState(false) @@ -173,15 +175,6 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st const data = await response.json() throw new Error(data.error || 'Kunde inte markera som skickad') } - } else if (status === 'paid') { - // Use mark-paid API for proper bookkeeping - const response = await fetch(`/api/invoices/${invoice.id}/mark-paid`, { - method: 'POST', - }) - if (!response.ok) { - const data = await response.json() - throw new Error(data.error || 'Kunde inte markera som betald') - } } else if (status === 'cancelled') { // Only drafts and proformas can be cancelled directly — sent/overdue/paid // invoices have committed journal entries and require a credit note instead @@ -418,7 +411,7 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st )} {(invoice.status === 'sent' || invoice.status === 'overdue') && isRealInvoice && ( - @@ -904,7 +897,7 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st <> + + ))} + + {/* Add row */} + + + + {/* Balance indicator */} +
+
+ {isBalanced ? ( + + + Debet = Kredit + + ) : ( + + + Obalanserad ({formatCurrency(Math.abs(totalDebit - totalCredit))}) + + )} +
+
+ {formatCurrency(totalDebit)} / {formatCurrency(totalCredit)} +
+
+ + )} + + + + + + + + ) +} diff --git a/lib/api/schemas.ts b/lib/api/schemas.ts index 162366bd..d504d3d5 100644 --- a/lib/api/schemas.ts +++ b/lib/api/schemas.ts @@ -174,6 +174,12 @@ export const MarkInvoicePaidSchema = z.object({ payment_date: isoDate.optional(), exchange_rate_difference: z.number().optional(), notes: z.string().optional(), + lines: z.array(z.object({ + account_number: accountNumber, + debit_amount: nonNegativeAmount.default(0), + credit_amount: nonNegativeAmount.default(0), + line_description: z.string().optional(), + })).min(2).optional(), }) // ============================================================ diff --git a/lib/bookkeeping/__tests__/propose-payment-lines.test.ts b/lib/bookkeeping/__tests__/propose-payment-lines.test.ts new file mode 100644 index 00000000..b3a862f7 --- /dev/null +++ b/lib/bookkeeping/__tests__/propose-payment-lines.test.ts @@ -0,0 +1,212 @@ +import { describe, it, expect } from 'vitest' +import { proposePaymentLines } from '../propose-payment-lines' +import type { InvoiceItem, VatTreatment } from '@/types' + +function makeItem(overrides: Partial = {}): InvoiceItem { + return { + id: 'item-1', + invoice_id: 'inv-1', + description: 'Konsulttjänst', + quantity: 1, + unit: 'st', + unit_price: 10000, + line_total: 10000, + vat_rate: 25, + vat_amount: 2500, + sort_order: 0, + created_at: '2025-01-01', + ...overrides, + } +} + +function makeInvoiceInput(overrides: Partial<{ + invoice_number: string + total: number + total_sek: number | null + subtotal: number + subtotal_sek: number | null + vat_amount: number + vat_amount_sek: number | null + currency: string + exchange_rate: number | null + vat_treatment: VatTreatment + items: InvoiceItem[] +}> = {}) { + return { + invoice_number: '2025-001', + total: 12500, + total_sek: null, + subtotal: 10000, + subtotal_sek: null, + vat_amount: 2500, + vat_amount_sek: null, + currency: 'SEK', + exchange_rate: null, + vat_treatment: 'standard_25' as VatTreatment, + items: [makeItem()], + ...overrides, + } +} + +describe('proposePaymentLines', () => { + describe('accrual method', () => { + it('SEK invoice → 2 lines (debit payment account, credit 1510)', () => { + const lines = proposePaymentLines({ + invoice: makeInvoiceInput(), + accountingMethod: 'accrual', + entityType: 'enskild_firma', + }) + + expect(lines).toHaveLength(2) + expect(lines[0]).toEqual({ + account_number: '1930', + debit_amount: '12500', + credit_amount: '', + line_description: 'Betalning faktura 2025-001', + }) + expect(lines[1]).toEqual({ + account_number: '1510', + debit_amount: '', + credit_amount: '12500', + line_description: 'Betalning faktura 2025-001', + }) + }) + + it('custom bank account (1920) → debit goes to 1920', () => { + const lines = proposePaymentLines({ + invoice: makeInvoiceInput(), + accountingMethod: 'accrual', + entityType: 'enskild_firma', + paymentAccount: '1920', + }) + + expect(lines).toHaveLength(2) + expect(lines[0].account_number).toBe('1920') + expect(lines[1].account_number).toBe('1510') + }) + + it('foreign currency with exchange rate gain → 3 lines', () => { + const lines = proposePaymentLines({ + invoice: makeInvoiceInput({ + total: 1000, + total_sek: 10000, + currency: 'EUR', + exchange_rate: 10, + }), + accountingMethod: 'accrual', + entityType: 'enskild_firma', + exchangeRateDifference: 500, + }) + + expect(lines).toHaveLength(3) + // Bank: actual received = 10000 + 500 = 10500 + expect(lines[0].account_number).toBe('1930') + expect(lines[0].debit_amount).toBe('10500') + // Clear receivable at booked amount + expect(lines[1].account_number).toBe('1510') + expect(lines[1].credit_amount).toBe('10000') + // Exchange gain + expect(lines[2].account_number).toBe('3960') + expect(lines[2].credit_amount).toBe('500') + }) + + it('foreign currency with exchange rate loss → 3 lines with 7960 debit', () => { + const lines = proposePaymentLines({ + invoice: makeInvoiceInput({ + total: 1000, + total_sek: 10000, + currency: 'EUR', + exchange_rate: 10, + }), + accountingMethod: 'accrual', + entityType: 'enskild_firma', + exchangeRateDifference: -300, + }) + + expect(lines).toHaveLength(3) + expect(lines[0].debit_amount).toBe('9700') + expect(lines[2].account_number).toBe('7960') + expect(lines[2].debit_amount).toBe('300') + }) + }) + + describe('cash method', () => { + it('single VAT rate → debit 1930, credit 3001, credit 2611', () => { + const lines = proposePaymentLines({ + invoice: makeInvoiceInput(), + accountingMethod: 'cash', + entityType: 'enskild_firma', + }) + + expect(lines).toHaveLength(3) + expect(lines[0]).toEqual({ + account_number: '1930', + debit_amount: '12500', + credit_amount: '', + line_description: 'Betalning faktura 2025-001', + }) + expect(lines[1]).toEqual({ + account_number: '3001', + debit_amount: '', + credit_amount: '10000', + line_description: 'Försäljning faktura 2025-001', + }) + expect(lines[2]).toEqual({ + account_number: '2611', + debit_amount: '', + credit_amount: '2500', + line_description: 'Utgående moms 25%', + }) + }) + + it('mixed VAT rates → multiple credit lines', () => { + const items = [ + makeItem({ id: 'i1', vat_rate: 25, line_total: 8000, vat_amount: 2000, unit_price: 8000 }), + makeItem({ id: 'i2', vat_rate: 12, line_total: 2000, vat_amount: 240, unit_price: 2000 }), + ] + + const lines = proposePaymentLines({ + invoice: makeInvoiceInput({ + total: 12240, + subtotal: 10000, + vat_amount: 2240, + items, + }), + accountingMethod: 'cash', + entityType: 'enskild_firma', + }) + + // 1 debit + 2 revenue + 2 VAT = 5 lines + expect(lines).toHaveLength(5) + expect(lines[0].account_number).toBe('1930') + + // Find the revenue/VAT lines by account + const accounts = lines.slice(1).map((l) => l.account_number) + expect(accounts).toContain('3001') // 25% revenue + expect(accounts).toContain('2611') // 25% VAT + expect(accounts).toContain('3002') // 12% revenue + expect(accounts).toContain('2621') // 12% VAT + }) + + it('defaults payment account to 1930', () => { + const lines = proposePaymentLines({ + invoice: makeInvoiceInput(), + accountingMethod: 'cash', + entityType: 'enskild_firma', + }) + + expect(lines[0].account_number).toBe('1930') + }) + + it('uses custom payment account', () => { + const lines = proposePaymentLines({ + invoice: makeInvoiceInput(), + accountingMethod: 'cash', + entityType: 'enskild_firma', + paymentAccount: '1910', + }) + + expect(lines[0].account_number).toBe('1910') + }) + }) +}) diff --git a/lib/bookkeeping/propose-payment-lines.ts b/lib/bookkeeping/propose-payment-lines.ts new file mode 100644 index 00000000..2f57596f --- /dev/null +++ b/lib/bookkeeping/propose-payment-lines.ts @@ -0,0 +1,239 @@ +/** + * Pure function to compute proposed journal entry lines for an invoice payment. + * Used by the PaymentBookingDialog to pre-fill the editable line grid. + * + * No DB or Supabase dependency — all inputs are plain data. + */ +import { resolveSekAmount } from './currency-utils' +import { getRevenueAccount, getOutputVatAccount } from './invoice-entries' +import { getVatTreatmentForRate } from '@/lib/invoices/vat-rules' +import type { FormLine } from '@/components/bookkeeping/JournalEntryForm' +import type { EntityType, InvoiceItem, VatTreatment } from '@/types' + +export interface ProposePaymentLinesInput { + invoice: { + invoice_number: string + total: number + total_sek?: number | null + subtotal: number + subtotal_sek?: number | null + vat_amount: number + vat_amount_sek?: number | null + currency: string + exchange_rate?: number | null + vat_treatment: VatTreatment + items?: InvoiceItem[] + } + accountingMethod: 'accrual' | 'cash' + entityType: EntityType + paymentAccount?: string + exchangeRateDifference?: number +} + +function toFormAmount(n: number): string { + const rounded = Math.round(n * 100) / 100 + return rounded === 0 ? '' : rounded.toString() +} + +/** + * Propose journal entry lines for an invoice payment. + * + * Accrual: Debit paymentAccount, Credit 1510, optional exchange rate diff. + * Cash: Debit paymentAccount, Credit 30xx + 26xx per VAT rate group. + */ +export function proposePaymentLines(input: ProposePaymentLinesInput): FormLine[] { + const { invoice, accountingMethod, entityType, exchangeRateDifference } = input + const paymentAccount = input.paymentAccount || '1930' + const desc = `Betalning faktura ${invoice.invoice_number}` + + if (accountingMethod === 'accrual') { + return proposeAccrualLines(invoice, paymentAccount, desc, exchangeRateDifference) + } + return proposeCashLines(invoice, paymentAccount, desc, entityType) +} + +function proposeAccrualLines( + invoice: ProposePaymentLinesInput['invoice'], + paymentAccount: string, + desc: string, + exchangeRateDifference?: number +): FormLine[] { + const bookedSekAmount = resolveSekAmount( + invoice.total, + invoice.total_sek, + invoice.currency, + invoice.exchange_rate + ) + const lines: FormLine[] = [] + + if (exchangeRateDifference && exchangeRateDifference !== 0) { + const actualSekReceived = bookedSekAmount + exchangeRateDifference + + lines.push({ + account_number: paymentAccount, + debit_amount: toFormAmount(actualSekReceived), + credit_amount: '', + line_description: desc, + }) + + lines.push({ + account_number: '1510', + debit_amount: '', + credit_amount: toFormAmount(bookedSekAmount), + line_description: desc, + }) + + if (exchangeRateDifference > 0) { + lines.push({ + account_number: '3960', + debit_amount: '', + credit_amount: toFormAmount(exchangeRateDifference), + line_description: 'Valutakursvinst', + }) + } else { + lines.push({ + account_number: '7960', + debit_amount: toFormAmount(Math.abs(exchangeRateDifference)), + credit_amount: '', + line_description: 'Valutakursförlust', + }) + } + } else { + const amount = Math.round(bookedSekAmount * 100) / 100 + lines.push({ + account_number: paymentAccount, + debit_amount: toFormAmount(amount), + credit_amount: '', + line_description: desc, + }) + lines.push({ + account_number: '1510', + debit_amount: '', + credit_amount: toFormAmount(amount), + line_description: desc, + }) + } + + return lines +} + +function proposeCashLines( + invoice: ProposePaymentLinesInput['invoice'], + paymentAccount: string, + desc: string, + entityType: EntityType +): FormLine[] { + const lines: FormLine[] = [] + const isForeign = invoice.currency !== 'SEK' + + const toSek = (amount: number): number => { + if (!isForeign) return amount + if (invoice.exchange_rate != null && invoice.exchange_rate > 0) { + return Math.round(amount * invoice.exchange_rate * 100) / 100 + } + return amount + } + + // Build credit lines per VAT rate group + const creditLines: FormLine[] = [] + + if (invoice.items && invoice.items.length > 0) { + const hasPerLineVat = invoice.items.some((item) => item.vat_rate !== undefined && item.vat_rate !== null) + + if (!hasPerLineVat) { + // Legacy: single rate from invoice level + const revenueAccount = getRevenueAccount(invoice.vat_treatment, entityType) + const subtotal = invoice.items.reduce((sum, item) => sum + item.line_total, 0) + creditLines.push({ + account_number: revenueAccount, + debit_amount: '', + credit_amount: toFormAmount(toSek(subtotal)), + line_description: `Försäljning faktura ${invoice.invoice_number}`, + }) + + const totalVat = invoice.items.reduce((sum, item) => sum + (item.vat_amount || 0), 0) + if (totalVat > 0) { + const vatAccount = getOutputVatAccount(invoice.vat_treatment) + creditLines.push({ + account_number: vatAccount, + debit_amount: '', + credit_amount: toFormAmount(toSek(totalVat)), + line_description: 'Utgående moms', + }) + } + } else { + // Group items by vat_rate + const rateGroups = new Map() + for (const item of invoice.items) { + const rate = item.vat_rate ?? 0 + const group = rateGroups.get(rate) || { subtotal: 0, vatAmount: 0 } + group.subtotal += item.line_total + group.vatAmount += item.vat_amount || 0 + rateGroups.set(rate, group) + } + + for (const [rate, group] of rateGroups) { + const treatment = rate === 0 && (invoice.vat_treatment === 'reverse_charge' || invoice.vat_treatment === 'export') + ? invoice.vat_treatment + : getVatTreatmentForRate(rate) + const revenueAccount = getRevenueAccount(treatment, entityType) + + creditLines.push({ + account_number: revenueAccount, + debit_amount: '', + credit_amount: toFormAmount(Math.round(toSek(group.subtotal) * 100) / 100), + line_description: `Försäljning faktura ${invoice.invoice_number}`, + }) + + const roundedVat = Math.round(toSek(group.vatAmount) * 100) / 100 + if (roundedVat !== 0) { + const vatAccount = getOutputVatAccount(treatment) + creditLines.push({ + account_number: vatAccount, + debit_amount: '', + credit_amount: toFormAmount(roundedVat), + line_description: `Utgående moms ${rate}%`, + }) + } + } + } + } else { + // Fallback: invoice-level amounts + const revenueAccount = getRevenueAccount(invoice.vat_treatment, entityType) + const subtotalSek = resolveSekAmount(invoice.subtotal, invoice.subtotal_sek, invoice.currency, invoice.exchange_rate) + creditLines.push({ + account_number: revenueAccount, + debit_amount: '', + credit_amount: toFormAmount(subtotalSek), + line_description: `Försäljning faktura ${invoice.invoice_number}`, + }) + + if (invoice.vat_amount > 0) { + const vatSek = resolveSekAmount(invoice.vat_amount, invoice.vat_amount_sek, invoice.currency, invoice.exchange_rate) + const vatAccount = getOutputVatAccount(invoice.vat_treatment) + creditLines.push({ + account_number: vatAccount, + debit_amount: '', + credit_amount: toFormAmount(vatSek), + line_description: `Utgående moms faktura ${invoice.invoice_number}`, + }) + } + } + + // Debit: balance guarantee + const totalCredits = creditLines.reduce((sum, l) => sum + (parseFloat(l.credit_amount) || 0), 0) + const debitAmount = isForeign + ? Math.round(totalCredits * 100) / 100 + : resolveSekAmount(invoice.total, invoice.total_sek, invoice.currency, invoice.exchange_rate) + + lines.push({ + account_number: paymentAccount, + debit_amount: toFormAmount(debitAmount), + credit_amount: '', + line_description: desc, + }) + + lines.push(...creditLines) + + return lines +} diff --git a/lib/reports/general-ledger.ts b/lib/reports/general-ledger.ts index 21af1b41..8107d0aa 100644 --- a/lib/reports/general-ledger.ts +++ b/lib/reports/general-ledger.ts @@ -51,13 +51,13 @@ export async function generateGeneralLedger( return { accounts: [], period: { start: '', end: '' } } } - // Fetch posted entries for this period + // Fetch posted and reversed entries for this period (reversed entries must appear alongside their storno) const { data: entries } = await supabase .from('journal_entries') .select('id, entry_date, voucher_number, voucher_series, description, source_type') .eq('user_id', userId) .eq('fiscal_period_id', periodId) - .eq('status', 'posted') + .in('status', ['posted', 'reversed']) if (!entries || entries.length === 0) { return { accounts: [], period: { start: period.period_start, end: period.period_end } } @@ -90,12 +90,12 @@ export async function generateGeneralLedger( accountNameMap.set(acc.account_number, acc.account_name) } - // Compute opening balances: sum all posted lines from entries before this period + // Compute opening balances: sum all posted/reversed lines from entries before this period const { data: priorEntries } = await supabase .from('journal_entries') .select('id') .eq('user_id', userId) - .eq('status', 'posted') + .in('status', ['posted', 'reversed']) .lt('entry_date', period.period_start) const openingBalances = new Map() diff --git a/lib/reports/ink2/ink2-engine.ts b/lib/reports/ink2/ink2-engine.ts index 3edfd464..121dc3aa 100644 --- a/lib/reports/ink2/ink2-engine.ts +++ b/lib/reports/ink2/ink2-engine.ts @@ -233,7 +233,7 @@ export async function generateINK2Declaration( .select('*, lines:journal_entry_lines(*)') .eq('user_id', userId) .eq('fiscal_period_id', fiscalPeriodId) - .eq('status', 'posted') + .in('status', ['posted', 'reversed']) if (entriesError) { throw new Error(`Failed to fetch journal entries: ${entriesError.message}`) diff --git a/lib/reports/ne-bilaga/ne-engine.ts b/lib/reports/ne-bilaga/ne-engine.ts index 621dcfb4..0e097d8f 100644 --- a/lib/reports/ne-bilaga/ne-engine.ts +++ b/lib/reports/ne-bilaga/ne-engine.ts @@ -190,7 +190,7 @@ export async function generateNEDeclaration( .select('*, lines:journal_entry_lines(*)') .eq('user_id', userId) .eq('fiscal_period_id', fiscalPeriodId) - .eq('status', 'posted') + .in('status', ['posted', 'reversed']) if (entriesError) { throw new Error(`Failed to fetch journal entries: ${entriesError.message}`) diff --git a/lib/reports/sie-export.ts b/lib/reports/sie-export.ts index 1f39452c..92579cdd 100644 --- a/lib/reports/sie-export.ts +++ b/lib/reports/sie-export.ts @@ -46,7 +46,7 @@ export async function generateSIEExport( .select('*, lines:journal_entry_lines(*)') .eq('user_id', userId) .eq('fiscal_period_id', options.fiscal_period_id) - .eq('status', 'posted') + .in('status', ['posted', 'reversed']) .order('voucher_number') // Fetch cost centers and projects for dimension records diff --git a/lib/reports/trial-balance.ts b/lib/reports/trial-balance.ts index d634fafa..dcb5e5e3 100644 --- a/lib/reports/trial-balance.ts +++ b/lib/reports/trial-balance.ts @@ -66,7 +66,7 @@ async function generateTrialBalanceManual( .select('id') .eq('user_id', userId) .eq('fiscal_period_id', fiscalPeriodId) - .eq('status', 'posted') + .in('status', ['posted', 'reversed']) if (entriesError || !entries || entries.length === 0) { return { rows: [], totalDebit: 0, totalCredit: 0, isBalanced: true } diff --git a/lib/reports/vat-declaration.ts b/lib/reports/vat-declaration.ts index fc7e1cda..e91917dc 100644 --- a/lib/reports/vat-declaration.ts +++ b/lib/reports/vat-declaration.ts @@ -143,7 +143,7 @@ export async function calculateVatDeclaration( `) .in('account_number', VAT_ACCOUNTS) .eq('journal_entries.user_id', userId) - .eq('journal_entries.status', 'posted') + .in('journal_entries.status', ['posted', 'reversed']) .gte('journal_entries.entry_date', start) .lte('journal_entries.entry_date', end) .range(from, to) @@ -193,7 +193,7 @@ export async function calculateVatDeclaration( .from('journal_entries') .select('source_type') .eq('user_id', userId) - .eq('status', 'posted') + .in('status', ['posted', 'reversed']) .gte('entry_date', start) .lte('entry_date', end)