* fix(invoices): repair send dialog fiscal-period query + editable issuance lines The send/mark-sent dialog queried fiscal_periods with start_date/end_date instead of period_start/period_end; the query always 400ed, and since PR #1023 made that fatal the dialog closed instantly, blocking mark-as-sent and email send for everyone. Also lets accrual companies edit the proposed journal lines before booking (both send and mark-sent), mirroring the mark-paid editor: untouched proposals still book via the server generator; edited lines book verbatim with balance validated at three layers. Credit notes and periodiserade invoices keep the read-only preview. The dialog now also respects defer_invoice_booking (#967). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): harden custom issuance-line validation per review findings Extract the custom-line parse + balance check into a shared validator so the send and mark-sent routes cannot drift. Reject rows carrying both debit and credit, and 29xx interim accounts (custom lines skip accrual schedule creation, so a 29xx balance would never be dissolved). Validate the payload only after the invoice ownership fetch, and emit structured log events when user-edited lines are booked or deliberately ignored, so manual overrides are visible in audit review. Account existence needs no route-level check: the engine already resolves every account against the company chart and throws AccountsNotInChartError. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): address CodeRabbit findings on issuance line editing Reject malformed JSON bodies with 400 instead of silently booking generated lines; restrict line editing to SEK invoices (custom lines cannot carry FX metadata); round each line before the client balance check to match the server; stop claiming a voucher was created in the mark-sent toast for deferred-booking companies; add programmatic labels to the editor inputs and remove-row buttons. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
92 lines
3.2 KiB
TypeScript
92 lines
3.2 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { parseCustomIssuanceLines } from '../issuance-custom-lines'
|
|
|
|
describe('parseCustomIssuanceLines', () => {
|
|
const balanced = [
|
|
{ account_number: '1510', debit_amount: 12500, credit_amount: 0 },
|
|
{ account_number: '3001', debit_amount: 0, credit_amount: 10000 },
|
|
{ account_number: '2611', debit_amount: 0, credit_amount: 2500 },
|
|
]
|
|
|
|
it('passes through a missing body as no lines', () => {
|
|
expect(parseCustomIssuanceLines(null)).toEqual({ ok: true, lines: undefined })
|
|
expect(parseCustomIssuanceLines(undefined)).toEqual({ ok: true, lines: undefined })
|
|
})
|
|
|
|
it('accepts a body without lines', () => {
|
|
expect(parseCustomIssuanceLines({})).toEqual({ ok: true, lines: undefined })
|
|
})
|
|
|
|
it('accepts balanced lines', () => {
|
|
const result = parseCustomIssuanceLines({ lines: balanced })
|
|
expect(result.ok).toBe(true)
|
|
if (result.ok) expect(result.lines).toHaveLength(3)
|
|
})
|
|
|
|
it('rejects malformed bodies via the schema', () => {
|
|
const result = parseCustomIssuanceLines({ lines: [{ account_number: 'x', debit_amount: -1 }] })
|
|
expect(result).toMatchObject({ ok: false, error: 'invalid_body' })
|
|
})
|
|
|
|
it('rejects fewer than two lines', () => {
|
|
const result = parseCustomIssuanceLines({ lines: [balanced[0]] })
|
|
expect(result).toMatchObject({ ok: false, error: 'invalid_body' })
|
|
})
|
|
|
|
it('rejects unbalanced lines', () => {
|
|
const result = parseCustomIssuanceLines({
|
|
lines: [balanced[0], { account_number: '3001', debit_amount: 0, credit_amount: 9999 }],
|
|
})
|
|
expect(result).toMatchObject({ ok: false, error: 'unbalanced' })
|
|
})
|
|
|
|
it('rejects zero-debit entries', () => {
|
|
const result = parseCustomIssuanceLines({
|
|
lines: [
|
|
{ account_number: '1510', debit_amount: 0, credit_amount: 0 },
|
|
{ account_number: '3001', debit_amount: 0, credit_amount: 0 },
|
|
],
|
|
})
|
|
expect(result).toMatchObject({ ok: false, error: 'unbalanced' })
|
|
})
|
|
|
|
it('rejects sub-öre payloads whose raw sums balance but rounded sums do not', () => {
|
|
const result = parseCustomIssuanceLines({
|
|
lines: [
|
|
{ account_number: '1510', debit_amount: 0.004, credit_amount: 0 },
|
|
{ account_number: '1930', debit_amount: 0.004, credit_amount: 0 },
|
|
{ account_number: '3001', debit_amount: 0, credit_amount: 0.008 },
|
|
],
|
|
})
|
|
expect(result).toMatchObject({ ok: false, error: 'unbalanced' })
|
|
})
|
|
|
|
it('rejects a row carrying both debit and credit', () => {
|
|
const result = parseCustomIssuanceLines({
|
|
lines: [
|
|
{ account_number: '1510', debit_amount: 100, credit_amount: 50 },
|
|
{ account_number: '3001', debit_amount: 0, credit_amount: 50 },
|
|
],
|
|
})
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: 'invalid_lines',
|
|
details: { reason: 'both_sides', index: 0 },
|
|
})
|
|
})
|
|
|
|
it('rejects 29xx interim accounts', () => {
|
|
const result = parseCustomIssuanceLines({
|
|
lines: [
|
|
{ account_number: '1510', debit_amount: 12500, credit_amount: 0 },
|
|
{ account_number: '2990', debit_amount: 0, credit_amount: 12500 },
|
|
],
|
|
})
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: 'invalid_lines',
|
|
details: { reason: 'accrual_interim_account', account: '2990' },
|
|
})
|
|
})
|
|
})
|