fix(bookkeeping): editable verifikationstext on andringsverifikation (#1035)

The correction header was always built server-side as
"Rattelse: <original description>". When the original entry was labelled
after the wrong account, the correction kept echoing that stale label even
after the user switched to the correct account (follow-up to the
line-description fix in #1029).

- CorrectJournalEntrySchema gains an optional trimmed description
- correctEntry() accepts options.description; blank or absent falls back
  to the canonical "Rattelse: <original>" auto text
- both correct routes (dashboard + v1, which share the schema) thread the
  description through
- CorrectionEntryDialog surfaces an editable verifikationstext field,
  pre-filled with the auto text; an untouched or cleared prefill is NOT
  sent, so the server-side fallback stays the source of truth (same
  only-overwrite-auto-filled principle as #1029)

Forward-only: already-posted corrections are immutable per BFL.

Fixes #1031

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-16 18:14:02 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 90e4f668c9
commit 6bd85f94b6
10 changed files with 236 additions and 8 deletions
+25
View File
@@ -2212,6 +2212,31 @@ describe('CorrectJournalEntrySchema', () => {
})
expect(result.success).toBe(false)
})
it('accepts an optional description and trims it', () => {
const result = CorrectJournalEntrySchema.safeParse({
description: ' Rättelse: Skulder till närstående personer, kortfristig del ',
lines: [
validJournalEntryLine({ account_number: '6200', debit_amount: 500, credit_amount: 0 }),
validJournalEntryLine({ account_number: '1930', debit_amount: 0, credit_amount: 500 }),
],
})
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.description).toBe('Rättelse: Skulder till närstående personer, kortfristig del')
}
})
it('rejects a blank description (omit it to use the server fallback)', () => {
const result = CorrectJournalEntrySchema.safeParse({
description: ' ',
lines: [
validJournalEntryLine({ account_number: '6200', debit_amount: 500, credit_amount: 0 }),
validJournalEntryLine({ account_number: '1930', debit_amount: 0, credit_amount: 500 }),
],
})
expect(result.success).toBe(false)
})
})
// ============================================================
+4
View File
@@ -910,6 +910,10 @@ export const CreateJournalEntrySchema = z.object({
})
export const CorrectJournalEntrySchema = z.object({
// Optional verifikationstext for the corrected entry. When omitted the
// server falls back to "Rättelse: <original description>"; supplying it lets
// the user replace a header that echoed the wrong account's label (#1031).
description: z.string().trim().min(1, 'Description cannot be empty').optional(),
lines: z.array(CreateJournalEntryLineSchema).min(2, 'At least two lines are required for double-entry'),
})
@@ -287,6 +287,43 @@ describe('correctEntry', () => {
expect(result.corrected).toBeDefined()
})
it('uses a caller-supplied description for the corrected entry (issue #1031)', async () => {
setupResults()
const supabase = makeClient()
await correctEntry(supabase as never, 'company-1', 'user-1', 'orig-1', correctedLines, {
description: 'Rättelse: Skulder till närstående personer, kortfristig del',
})
const entryInserts = inserts.filter((i) => i.table === 'journal_entries')
expect(entryInserts).toHaveLength(2)
const corrected = entryInserts[1].payload as { source_type: string; description: string }
expect(corrected.source_type).toBe('correction')
expect(corrected.description).toBe('Rättelse: Skulder till närstående personer, kortfristig del')
})
it('falls back to "Rättelse: <original>" when no description is supplied', async () => {
setupResults()
const supabase = makeClient()
await correctEntry(supabase as never, 'company-1', 'user-1', 'orig-1', correctedLines)
const entryInserts = inserts.filter((i) => i.table === 'journal_entries')
const corrected = entryInserts[1].payload as { source_type: string; description: string }
expect(corrected.source_type).toBe('correction')
expect(corrected.description).toBe('Rättelse: Test purchase')
})
it('falls back to the auto text when the supplied description is blank', async () => {
setupResults()
const supabase = makeClient()
await correctEntry(supabase as never, 'company-1', 'user-1', 'orig-1', correctedLines, {
description: ' ',
})
const entryInserts = inserts.filter((i) => i.table === 'journal_entries')
const corrected = entryInserts[1].payload as { description: string }
expect(corrected.description).toBe('Rättelse: Test purchase')
})
it('accepts a source_type=correction entry as the original (chained correction, BFL 5 kap. 5 §)', async () => {
// The user just corrected entry A → got correction C. They now want to
// correct C. Service must not care about source_type of the original:
+10 -1
View File
@@ -130,6 +130,13 @@ export async function correctEntry(
* the small TOCTOU window a second independent read would open.
*/
preloadedOriginal?: OriginalWithLines
/**
* Verifikationstext for the corrected entry. Defaults to
* "Rättelse: <original description>". Supplying it lets a user who
* corrects the account BECAUSE the original label was wrong avoid the
* stale label echoing in the correction header (issue #1031).
*/
description?: string
}
): Promise<{ reversal: JournalEntry; corrected: JournalEntry; documentRelinkError?: string }> {
// Validate the corrected lines are balanced
@@ -340,7 +347,9 @@ export async function correctEntry(
voucher_number: correctedVoucherNumber,
voucher_series: original.voucher_series || 'A',
entry_date: correctedDate,
description: `Rättelse: ${original.description}`,
// A caller-supplied verifikationstext wins; blank falls back to the
// canonical auto text so the header never ends up empty.
description: options?.description?.trim() || `Rättelse: ${original.description}`,
source_type: 'correction',
correction_of_id: originalEntryId,
status: 'draft',