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:
co-authored by
Claude Fable 5
parent
90e4f668c9
commit
6bd85f94b6
@@ -113,7 +113,50 @@ describe('POST /api/bookkeeping/journal-entries/[id]/correct', () => {
|
||||
expect(status).toBe(200)
|
||||
expect(body.data.reversal).toEqual(reversal)
|
||||
expect(body.data.corrected).toEqual(corrected)
|
||||
expect(mockCorrectEntry).toHaveBeenCalledWith(expect.anything(), 'company-1', 'user-1', 'entry-1', lines)
|
||||
expect(mockCorrectEntry).toHaveBeenCalledWith(expect.anything(), 'company-1', 'user-1', 'entry-1', lines, {
|
||||
description: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('threads an optional description through to correctEntry (issue #1031)', async () => {
|
||||
const reversal = makeJournalEntry({ id: 'reversal-1', reverses_id: 'entry-1', source_type: 'storno' })
|
||||
const corrected = makeJournalEntry({ id: 'corrected-1', correction_of_id: 'entry-1', source_type: 'correction' })
|
||||
mockCorrectEntry.mockResolvedValue({ reversal, corrected })
|
||||
|
||||
const lines = [
|
||||
{ account_number: '2893', debit_amount: 1000, credit_amount: 0 },
|
||||
{ account_number: '1930', debit_amount: 0, credit_amount: 1000 },
|
||||
]
|
||||
|
||||
const request = createMockRequest('/api/bookkeeping/journal-entries/entry-1/correct', {
|
||||
method: 'POST',
|
||||
body: { lines, description: 'Rättelse: Skulder till närstående personer, kortfristig del' },
|
||||
})
|
||||
const response = await POST(request, createMockRouteParams({ id: 'entry-1' }))
|
||||
const { status } = await parseJsonResponse(response)
|
||||
|
||||
expect(status).toBe(200)
|
||||
expect(mockCorrectEntry).toHaveBeenCalledWith(expect.anything(), 'company-1', 'user-1', 'entry-1', lines, {
|
||||
description: 'Rättelse: Skulder till närstående personer, kortfristig del',
|
||||
})
|
||||
})
|
||||
|
||||
it('returns 400 when description is blank', async () => {
|
||||
const lines = [
|
||||
{ account_number: '1930', debit_amount: 1000, credit_amount: 0 },
|
||||
{ account_number: '3001', debit_amount: 0, credit_amount: 1000 },
|
||||
]
|
||||
|
||||
const request = createMockRequest('/api/bookkeeping/journal-entries/entry-1/correct', {
|
||||
method: 'POST',
|
||||
body: { lines, description: ' ' },
|
||||
})
|
||||
const response = await POST(request, createMockRouteParams({ id: 'entry-1' }))
|
||||
const { status, body } = await parseJsonResponse<{ error: string }>(response)
|
||||
|
||||
expect(status).toBe(400)
|
||||
expect(body.error).toBe('Validation failed')
|
||||
expect(mockCorrectEntry).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('maps an unbalanced-correction engine error to the canonical envelope (400)', async () => {
|
||||
|
||||
@@ -13,7 +13,9 @@ export const POST = withRouteContext<{ params: Promise<{ id: string }> }>(
|
||||
const { id } = await params
|
||||
const validation = await validateBody(request, CorrectJournalEntrySchema)
|
||||
if (!validation.success) return validation.response
|
||||
const result = await correctEntry(supabase, companyId, user.id, id, validation.data.lines)
|
||||
const result = await correctEntry(supabase, companyId, user.id, id, validation.data.lines, {
|
||||
description: validation.data.description,
|
||||
})
|
||||
return NextResponse.json({ data: result })
|
||||
},
|
||||
{ requireWrite: true },
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
* posted with the new lines. All three remain in the verifikationsserie,
|
||||
* linked via reverses_id, reversed_by_id, and correction_of_id.
|
||||
*
|
||||
* Body: `{ lines: [...] }`: the new balanced lines. The corrected entry
|
||||
* inherits entry_date, fiscal_period_id, description, and voucher_series
|
||||
* from the original.
|
||||
* Body: `{ lines: [...], description? }`: the new balanced lines. The
|
||||
* corrected entry inherits entry_date, fiscal_period_id, and voucher_series
|
||||
* from the original. Its description defaults to "Rättelse: <original>";
|
||||
* pass `description` to override it (e.g. when the original label named the
|
||||
* wrong account).
|
||||
*
|
||||
* Idempotent (mandatory Idempotency-Key). Dry-runnable.
|
||||
*/
|
||||
@@ -108,7 +110,7 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string; id: string
|
||||
details: { issues: parsed.error.issues.map((i) => ({ field: i.path.join('.'), message: i.message })) },
|
||||
})
|
||||
}
|
||||
const { lines } = parsed.data
|
||||
const { lines, description } = parsed.data
|
||||
|
||||
const balance = validateBalance(lines)
|
||||
if (!balance.valid) {
|
||||
@@ -178,6 +180,7 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string; id: string
|
||||
ctx.userId,
|
||||
entryId,
|
||||
lines,
|
||||
{ description },
|
||||
)
|
||||
return ok(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user