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
@@ -11,8 +11,13 @@ import {
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import AccountCombobox from '@/components/bookkeeping/AccountCombobox'
import CorrectionPreview from '@/components/bookkeeping/CorrectionPreview'
import {
autoCorrectionDescription,
correctionDescriptionForSubmit,
} from '@/components/bookkeeping/correction-entry-description'
import { nextLineDescriptionForAccountChange } from '@/components/bookkeeping/correction-line-description'
import { useToast } from '@/components/ui/use-toast'
import { getErrorMessage } from '@/lib/errors/get-error-message'
@@ -40,6 +45,7 @@ export default function CorrectionEntryDialog({ entry, open, onOpenChange, onCor
const router = useRouter()
const [accounts, setAccounts] = useState<BASAccount[]>([])
const [lines, setLines] = useState<CorrectionLine[]>([])
const [description, setDescription] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const originalLines = ((entry.lines || []) as JournalEntryLine[])
@@ -57,6 +63,9 @@ export default function CorrectionEntryDialog({ entry, open, onOpenChange, onCor
line_description: l.line_description || '',
}))
)
// Pre-fill the verifikationstext with the same auto text the server
// would generate; only a user edit is sent along (see handleSubmit).
setDescription(autoCorrectionDescription(entry.description))
fetchAccounts()
}
}, [open, entry.id]) // eslint-disable-line react-hooks/exhaustive-deps
@@ -124,7 +133,12 @@ export default function CorrectionEntryDialog({ entry, open, onOpenChange, onCor
const res = await fetch(`/api/bookkeeping/journal-entries/${entry.id}/correct`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ lines: apiLines }),
body: JSON.stringify({
lines: apiLines,
// Only sent when the user changed the auto prefill: the server
// fallback ("Rättelse: <original>") stays the source of truth.
description: correctionDescriptionForSubmit(description, entry.description),
}),
})
const result = await res.json()
@@ -194,6 +208,23 @@ export default function CorrectionEntryDialog({ entry, open, onOpenChange, onCor
{/* Live diff: original | storno | correction | förändring */}
<CorrectionPreview originalLines={originalLines} correctedLines={lines} />
{/* Verifikationstext for the new (corrected) entry. Pre-filled with
the auto text; editable so a header named after the wrong account
is not echoed on the correction (issue #1031). */}
<div className="space-y-1">
<Label htmlFor="correction-description">Verifikationstext</Label>
<Input
id="correction-description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder={autoCorrectionDescription(entry.description)}
/>
<p className="text-xs text-muted-foreground">
Texten på den nya verifikationen. Ändra den om originalets beskrivning inte längre
stämmer, till exempel när rättelsen byter konto.
</p>
</div>
{/* Corrected lines (editable) */}
<div className="space-y-2">
<div className="space-y-1">
@@ -0,0 +1,40 @@
import { describe, it, expect } from 'vitest'
import {
autoCorrectionDescription,
correctionDescriptionForSubmit,
} from '@/components/bookkeeping/correction-entry-description'
const ORIGINAL = 'Lån från närstående personer, långfristig del'
describe('autoCorrectionDescription', () => {
it('matches the server-side fallback format', () => {
expect(autoCorrectionDescription(ORIGINAL)).toBe(`Rättelse: ${ORIGINAL}`)
})
})
describe('correctionDescriptionForSubmit', () => {
it('sends nothing when the field still equals the auto prefill', () => {
expect(correctionDescriptionForSubmit(`Rättelse: ${ORIGINAL}`, ORIGINAL)).toBeUndefined()
})
it('sends nothing when the prefill only gained surrounding whitespace', () => {
expect(correctionDescriptionForSubmit(` Rättelse: ${ORIGINAL} `, ORIGINAL)).toBeUndefined()
})
it('sends nothing when the field was cleared (server fallback applies)', () => {
expect(correctionDescriptionForSubmit('', ORIGINAL)).toBeUndefined()
expect(correctionDescriptionForSubmit(' ', ORIGINAL)).toBeUndefined()
})
it('sends a user-edited description (the reported bug: relabel after account change)', () => {
expect(
correctionDescriptionForSubmit('Rättelse: Skulder till närstående personer, kortfristig del', ORIGINAL),
).toBe('Rättelse: Skulder till närstående personer, kortfristig del')
})
it('trims a user-edited description before sending', () => {
expect(correctionDescriptionForSubmit(' Omföring till rätt konto ', ORIGINAL)).toBe(
'Omföring till rätt konto',
)
})
})
@@ -0,0 +1,34 @@
/**
* Entry-level verifikationstext handling for the correction dialog.
*
* The correction header defaults server-side to "Rättelse: <original>"
* (lib/core/bookkeeping/storno-service.ts). If the original entry was
* labelled after the wrong account (e.g. "Lån från närstående personer,
* långfristig del"), that header keeps echoing the wrong label even after
* the account itself is corrected (issue #1031). The dialog therefore
* pre-fills an editable field with the same auto text and only sends a
* description when the user actually changed it: an untouched auto prefill
* is omitted from the request so the server-side fallback stays the single
* source of truth for the default format. Same only-overwrite-auto-filled
* principle as the line-description guard from #1029.
*/
/** The auto text the server would generate for the correction header. */
export function autoCorrectionDescription(originalDescription: string): string {
return `Rättelse: ${originalDescription}`
}
/**
* Decide what to send as the correction's description.
* Returns undefined when the field is blank or still equals the auto prefill:
* in both cases the server-side fallback should apply.
*/
export function correctionDescriptionForSubmit(
currentDescription: string,
originalDescription: string,
): string | undefined {
const trimmed = currentDescription.trim()
if (!trimmed) return undefined
if (trimmed === autoCorrectionDescription(originalDescription).trim()) return undefined
return trimmed
}