fix(booking): explicit Ingen moms in quick review books exempt, not the derived default (#1450)
QuickReviewDialog collapsed 'none' to undefined on the wire, so the server re-derived the category default and booked 25% moms against an explicit no-VAT choice while the preview showed no VAT line. 'none' now maps to 'exempt' only when it deviates from the seeded default: untouched bookings on exempt-default categories (bank fees) stay byte-identical, an explicit Ingen moms books no VAT line with the classification the momsdeklaration should see, and the class-2 account auto-'none' stops booking a stray moms line. Same gap the swedish-compliance bot flagged on #1443. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Jakob Wennberg
Claude Fable 5
parent
7c93d53fd5
commit
858ad49852
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { resolveQuickReviewDefaults, type ReviewTemplate } from '../quick-review-defaults'
|
||||
import { resolveQuickReviewDefaults, resolveExplicitVat, type ReviewTemplate } from '../quick-review-defaults'
|
||||
import { getDefaultAccountForCategory } from '@/lib/bookkeeping/category-mapping'
|
||||
|
||||
/**
|
||||
@@ -70,3 +70,24 @@ describe('resolveQuickReviewDefaults', () => {
|
||||
expect(vat === 'none' || typeof vat === 'string').toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveExplicitVat', () => {
|
||||
it('passes explicit rate-bearing treatments through unchanged', () => {
|
||||
expect(resolveExplicitVat('standard_25', 'standard_25')).toBe('standard_25')
|
||||
expect(resolveExplicitVat('reduced_12', 'standard_25')).toBe('reduced_12')
|
||||
expect(resolveExplicitVat('reverse_charge', 'none')).toBe('reverse_charge')
|
||||
})
|
||||
|
||||
it("keeps a seeded 'none' default off the wire (server derives, no VAT line)", () => {
|
||||
// Bank fees etc.: category default is null -> displayed as 'none'.
|
||||
// Untouched submit must stay byte-identical to the pre-fix behavior.
|
||||
expect(resolveExplicitVat('none', 'none')).toBeUndefined()
|
||||
})
|
||||
|
||||
it("sends explicit 'exempt' when the user deviates to Ingen moms", () => {
|
||||
// The old collapse to undefined made the server re-derive the category
|
||||
// default and book 25% moms against an explicit no-VAT choice.
|
||||
expect(resolveExplicitVat('none', 'standard_25')).toBe('exempt')
|
||||
expect(resolveExplicitVat('none', 'reduced_12')).toBe('exempt')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -70,3 +70,31 @@ export function resolveQuickReviewDefaults(
|
||||
|
||||
return { account, vat }
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the VAT treatment the review dialog should put on the wire.
|
||||
*
|
||||
* 'none' is a UI-only sentinel with two meanings that must not be conflated:
|
||||
*
|
||||
* - As the SEEDED default (exempt categories like bank fees, or a template
|
||||
* without a treatment): send nothing and let the server derive, exactly as
|
||||
* before. The derivation yields no VAT line for those categories, and
|
||||
* keeping the wire empty preserves byte-identical bookings for the common
|
||||
* untouched case.
|
||||
* - As a DEVIATION (the user explicitly picked "Ingen moms" on a category
|
||||
* whose default carries VAT, or the dialog auto-set 'none' for a class-2
|
||||
* account override): send 'exempt'. The old collapse to undefined made the
|
||||
* server re-derive the default, silently booking 25% moms against an
|
||||
* explicit no-VAT choice, while the preview showed no VAT line. An explicit
|
||||
* 'exempt' books no VAT line and records the classification the
|
||||
* momsdeklaration should see (see buildMappingResultFromCategory's note);
|
||||
* for income it also lands revenue on 3004 instead of a rate-bearing
|
||||
* account.
|
||||
*/
|
||||
export function resolveExplicitVat(
|
||||
selected: VatTreatment | 'none',
|
||||
seededDefault: VatTreatment | 'none',
|
||||
): VatTreatment | undefined {
|
||||
if (selected !== 'none') return selected
|
||||
return seededDefault === 'none' ? undefined : 'exempt'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user