fix(bookkeeping): reverseEntry defaults to original entry_date, not today (#762)
* fix(bookkeeping): reverseEntry defaults to original entry_date, not today When reverseEntry() is called without an explicit reversalDate, it was defaulting to getSwedishLocalDate() (today). This caused the storno to land in the current period rather than the original entry's period — any reversal of a past verifikation through uncategorize, re-categorize, the dashboard reverse button, salary correction, or fix-cash-mismatch would produce a makulering dated today instead of the original booking date. The entry is already fetched before the date is resolved, so defaulting to original.entry_date is safe. Callers that intentionally want a different date (credit notes, mark-paid with payment date, user-provided reverse date) still pass an explicit reversalDate and are unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * feat(api): v1 endpoints to stamp invoice inbox items as consumed Adds inbox_item_id support to POST /api/v1/companies/{companyId}/documents/{id}/link (best-effort stamp on the originating invoice_inbox_items row) and a new dedicated POST /api/v1/companies/{companyId}/inbox-items/{id}/stamp endpoint for stamping independently of the document link — both use documents:write scope and require Idempotency-Key. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * Revert "feat(api): v1 endpoints to stamp invoice inbox items as consumed" This reverts commit f1bf3a86385ec556a49830c9e0966236a10d3164. Signed-off-by: Jonas Flodén <jonas@floden.nu> --------- Signed-off-by: Jonas Flodén <jonas@floden.nu> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -484,6 +484,134 @@ describe('createDraftEntry — on-demand BAS account backfill', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('reverseEntry — entry_date defaults to original entry date', () => {
|
||||
it('uses original entry_date when no reversalDate is provided', async () => {
|
||||
const original = {
|
||||
id: 'entry-1',
|
||||
company_id: 'company-1',
|
||||
status: 'posted',
|
||||
fiscal_period_id: 'period-1',
|
||||
voucher_series: 'A',
|
||||
voucher_number: 3,
|
||||
entry_date: '2024-11-15',
|
||||
description: 'Hyra november',
|
||||
source_type: 'manual',
|
||||
source_id: null,
|
||||
lines: [
|
||||
{ account_number: '5010', debit_amount: 10000, credit_amount: 0 },
|
||||
{ account_number: '1930', debit_amount: 0, credit_amount: 10000 },
|
||||
],
|
||||
}
|
||||
const reversal = { id: 'reversal-1', reverses_id: 'entry-1' }
|
||||
|
||||
let jeCall = 0
|
||||
const jeResults = [
|
||||
{ data: original, error: null },
|
||||
{ data: reversal, error: null },
|
||||
{ data: null, error: null },
|
||||
{ data: [{ id: 'entry-1' }], error: null },
|
||||
{ data: { ...reversal, lines: [] }, error: null },
|
||||
]
|
||||
|
||||
let insertedEntryDate: string | undefined
|
||||
function jeBuilder() {
|
||||
const b: Record<string, unknown> = {}
|
||||
for (const m of ['select', 'eq', 'in', 'update']) b[m] = vi.fn().mockReturnValue(b)
|
||||
b.insert = vi.fn().mockImplementation((payload: unknown) => {
|
||||
const p = payload as Record<string, unknown>
|
||||
if (p.entry_date !== undefined) insertedEntryDate = p.entry_date as string
|
||||
return b
|
||||
})
|
||||
b.single = vi.fn().mockImplementation(async () => jeResults[jeCall++])
|
||||
b.then = (resolve: (v: unknown) => void) => resolve(jeResults[jeCall++])
|
||||
return b
|
||||
}
|
||||
|
||||
const supabase = {
|
||||
rpc: vi.fn().mockResolvedValue({ data: 4, error: null }),
|
||||
from: vi.fn().mockImplementation((table: string) => {
|
||||
if (table === 'journal_entries') return jeBuilder()
|
||||
if (table === 'chart_of_accounts') {
|
||||
const b: Record<string, unknown> = {}
|
||||
for (const m of ['select', 'eq', 'in']) b[m] = vi.fn().mockReturnValue(b)
|
||||
b.then = (resolve: (v: unknown) => void) =>
|
||||
resolve({ data: [{ id: 'acc-5010', account_number: '5010' }, { id: 'acc-1930', account_number: '1930' }], error: null })
|
||||
return b
|
||||
}
|
||||
if (table === 'journal_entry_lines') return { insert: vi.fn().mockResolvedValue({ error: null }) }
|
||||
return createMockChain()
|
||||
}),
|
||||
}
|
||||
|
||||
await reverseEntry(supabase as never, 'company-1', 'user-1', 'entry-1')
|
||||
|
||||
expect(insertedEntryDate).toBe('2024-11-15')
|
||||
})
|
||||
|
||||
it('uses explicit reversalDate when provided', async () => {
|
||||
const original = {
|
||||
id: 'entry-1',
|
||||
company_id: 'company-1',
|
||||
status: 'posted',
|
||||
fiscal_period_id: 'period-1',
|
||||
voucher_series: 'A',
|
||||
voucher_number: 3,
|
||||
entry_date: '2024-11-15',
|
||||
description: 'Hyra november',
|
||||
source_type: 'manual',
|
||||
source_id: null,
|
||||
lines: [
|
||||
{ account_number: '5010', debit_amount: 10000, credit_amount: 0 },
|
||||
{ account_number: '1930', debit_amount: 0, credit_amount: 10000 },
|
||||
],
|
||||
}
|
||||
const reversal = { id: 'reversal-1', reverses_id: 'entry-1' }
|
||||
|
||||
let jeCall = 0
|
||||
const jeResults = [
|
||||
{ data: original, error: null },
|
||||
{ data: reversal, error: null },
|
||||
{ data: null, error: null },
|
||||
{ data: [{ id: 'entry-1' }], error: null },
|
||||
{ data: { ...reversal, lines: [] }, error: null },
|
||||
]
|
||||
|
||||
let insertedEntryDate: string | undefined
|
||||
function jeBuilder() {
|
||||
const b: Record<string, unknown> = {}
|
||||
for (const m of ['select', 'eq', 'in', 'update']) b[m] = vi.fn().mockReturnValue(b)
|
||||
b.insert = vi.fn().mockImplementation((payload: unknown) => {
|
||||
const p = payload as Record<string, unknown>
|
||||
if (p.entry_date !== undefined) insertedEntryDate = p.entry_date as string
|
||||
return b
|
||||
})
|
||||
b.single = vi.fn().mockImplementation(async () => jeResults[jeCall++])
|
||||
b.then = (resolve: (v: unknown) => void) => resolve(jeResults[jeCall++])
|
||||
return b
|
||||
}
|
||||
|
||||
const supabase = {
|
||||
rpc: vi.fn().mockResolvedValue({ data: 4, error: null }),
|
||||
from: vi.fn().mockImplementation((table: string) => {
|
||||
if (table === 'journal_entries') return jeBuilder()
|
||||
if (table === 'chart_of_accounts') {
|
||||
const b: Record<string, unknown> = {}
|
||||
for (const m of ['select', 'eq', 'in']) b[m] = vi.fn().mockReturnValue(b)
|
||||
b.then = (resolve: (v: unknown) => void) =>
|
||||
resolve({ data: [{ id: 'acc-5010', account_number: '5010' }, { id: 'acc-1930', account_number: '1930' }], error: null })
|
||||
return b
|
||||
}
|
||||
if (table === 'journal_entry_lines') return { insert: vi.fn().mockResolvedValue({ error: null }) }
|
||||
return createMockChain()
|
||||
}),
|
||||
}
|
||||
|
||||
await reverseEntry(supabase as never, 'company-1', 'user-1', 'entry-1', '2025-01-01')
|
||||
|
||||
expect(insertedEntryDate).toBe('2025-01-01')
|
||||
})
|
||||
})
|
||||
|
||||
describe('reverseEntry — bank transaction unlink', () => {
|
||||
// After a reversal the booked bank transaction must return to "Att bokföra"
|
||||
// (journal_entry_id cleared) so the user can book it again. The agent paths
|
||||
|
||||
@@ -657,7 +657,7 @@ export async function reverseEntry(
|
||||
project: line.project || undefined,
|
||||
}))
|
||||
|
||||
const entryDate = reversalDate || getSwedishLocalDate()
|
||||
const entryDate = reversalDate ?? original.entry_date
|
||||
|
||||
// Get voucher number for the reversal
|
||||
const voucherNumber = await getNextVoucherNumber(
|
||||
|
||||
Reference in New Issue
Block a user