diff --git a/lib/packs/__tests__/port-is-lossless.test.ts b/lib/packs/__tests__/port-is-lossless.test.ts index 0265e103..4c319dc8 100644 --- a/lib/packs/__tests__/port-is-lossless.test.ts +++ b/lib/packs/__tests__/port-is-lossless.test.ts @@ -23,18 +23,42 @@ import seeded from './fixtures/seeded-system-templates.json' * Every entry is a defect found by the pack validator and fixed with a domain * source cited in the commit. */ -const INTENTIONAL_DIVERGENCES: Record = { - loneutbetalning: - 'Seeded version debited 2710 @0.3 + 2920 @0.12 + 7010 @1.0 against a single 1.0 credit, ' + +interface Divergence { + /** + * The name this template carried in the seed. Needed because a fix may rename + * it, and the seeded fixture predates pack_slug so name is the only join key + * available there. Exactly the fragility that made pack_slug the sync's key. + */ + seededName: string + reason: string +} + +const INTENTIONAL_DIVERGENCES: Record = { + loneutbetalning: { + seededName: 'Löneutbetalning', + reason: + 'Seeded version debited 2710 @0.3 + 2920 @0.12 + 7010 @1.0 against a single 1.0 credit, ' + 'so it totalled 1.42x the amount and could never post. Rebuilt per the swedish-payroll ' + 'skill: Debit 7010 gross, Credit 2710 tax, Credit 1930 net. The 2920 semesterlöneskuld ' + 'line moved out because vacation accrual is its own verifikat (7290/2920).', - 'periodiseringsfond-avsattning-ab': - 'Seeded version used account 2113, i.e. the fund for tax year 2013 under the pre-2020 ' + + }, + 'periodiseringsfond-avsattning-ab': { + seededName: 'Periodiseringsfond avsättning (AB)', + reason: + 'Seeded version used account 2113, i.e. the fund for tax year 2013 under the pre-2020 ' + 'year-tagged block. Those funds had to be reversed years ago and the account is not in ' + 'BAS 2026, so the template could not resolve. Now uses 2110 Periodiseringsfonder, which ' + 'does not rot annually; the legal_note points at the year-tagged 2120-2129 alternative.', - 'periodiseringsfond-aterforing-ab': 'Same 2113 fix as periodiseringsfond-avsattning-ab.', + }, + 'periodiseringsfond-aterforing-ab': { + seededName: 'Periodiseringsfond återföring (AB)', + reason: 'Same 2113 fix as periodiseringsfond-avsattning-ab.', + }, + 'representation-avdragsgill-25-moms': { + seededName: 'Representation (avdragsgill, 25% moms)', + reason: + 'Seeded version booked to 6072 (Representation, EJ avdragsgill) while naming and labelling itself "avdragsgill", which is the exact confusion the swedish-vat skill lists under Representation errors. The account was right and the words were wrong: meal representation stopped being income-tax deductible in 2017. Renamed to match, VAT moved from 25% to the 12% restaurang rate our own static representation_external template already used (with the net ratio 1/1.12 that pairs with it), and a legal_note added for the 300 kr per person VAT cap, which this format cannot compute because it has no participant count. Slug deliberately unchanged: it is an identifier, not a label.', + }, } interface SeededTemplate { @@ -76,11 +100,7 @@ describe('pack catalogue is a lossless port of the seeded system templates', () it('reproduces the seeded templates exactly, except where we deliberately fixed one', () => { const unchanged = packs.filter((p) => !(p.pack.meta.slug in INTENTIONAL_DIVERGENCES)) - const changedNames = new Set( - packs - .filter((p) => p.pack.meta.slug in INTENTIONAL_DIVERGENCES) - .map((p) => p.pack.meta.name), - ) + const changedNames = new Set(Object.values(INTENTIONAL_DIVERGENCES).map((d) => d.seededName)) const fromPacks = unchanged.map((p) => canonical(packToLibraryRow(p.pack))).sort() const fromDb = (seeded as SeededTemplate[]) @@ -95,11 +115,11 @@ describe('pack catalogue is a lossless port of the seeded system templates', () it('every declared divergence actually diverges, so the list cannot go stale', () => { const byName = new Map((seeded as SeededTemplate[]).map((t) => [t.name, canonical(t)])) - for (const slug of Object.keys(INTENTIONAL_DIVERGENCES)) { + for (const [slug, divergence] of Object.entries(INTENTIONAL_DIVERGENCES)) { const pack = packs.find((p) => p.pack.meta.slug === slug) expect(pack, `${slug} is in INTENTIONAL_DIVERGENCES but no such pack exists`).toBeDefined() - const seededForm = byName.get(pack!.pack.meta.name) - expect(seededForm, `no seeded template named "${pack!.pack.meta.name}"`).toBeDefined() + const seededForm = byName.get(divergence.seededName) + expect(seededForm, `no seeded template named "${divergence.seededName}"`).toBeDefined() expect( canonical(packToLibraryRow(pack!.pack)), `${slug} is listed as diverging but matches the seed: remove its entry`, diff --git a/packs/representation-avdragsgill-25-moms.yaml b/packs/representation-avdragsgill-25-moms.yaml index cdcd6e97..913253fe 100644 --- a/packs/representation-avdragsgill-25-moms.yaml +++ b/packs/representation-avdragsgill-25-moms.yaml @@ -1,24 +1,44 @@ # Konteringspaket. Schema: lib/packs/schema.ts # Validated by `npm run validate:packs` (CI gate). +# +# The slug still says "avdragsgill". That is deliberate: a slug is an +# identifier, not a label (packs/README.md). Renaming it would retire this +# template and insert a new one, breaking every company's booking_template_usage +# history for it. The NAME is what users read, and that is now correct. meta: slug: representation-avdragsgill-25-moms order: 13 - name: 'Representation (avdragsgill, 25% moms)' + name: 'Representation, måltid (ej avdragsgill)' category: representation entity_type: all description: >- - Extern representation med avdragsgill moms. Max 300 kr/person exkl. moms. + Extern representation, måltid. Kostnaden är inte avdragsgill vid + inkomstbeskattningen; momsen är avdragsgill upp till ett tak per person. + legal_note: >- + Avdraget för måltidsrepresentation vid inkomstbeskattningen slopades 2017, + därför bokas hela kostnaden på 6072 Representation, ej avdragsgill. + Momsen är avdragsgill på ett underlag om högst 300 kr exkl. moms per person + och tillfälle. Mallen känner inte antalet deltagare och kan därför inte + räkna ut taket: kontrollera momsraden manuellt mot antalet personer, och + använd schablonbeloppet när måltiden innehåller både mat och alkohol. + Enklare förtäring (max 60 kr per person) är en annan situation och bokas + på 6071 Representation, avdragsgill. Momssatsen 12 % avser restaurang- + och cateringtjänster; kontrollera satsen mot underlaget, den beror på vad + som köpts och inte på mallen. +# ratio 0.892857 = 1/1.12: the cost line is the amount NET of the VAT that the +# vat line extracts from the same VAT-inclusive total. It is arithmetic tied to +# vat_rate, not a deductibility percentage. Change one and you must change both. lines: - account: '6072' - label: 'Representation avdragsgill' + label: 'Representation, ej avdragsgill' side: debit type: business - ratio: 0.8 + ratio: 0.892857 - account: '2641' - label: 'Ingående moms' + label: 'Ingående moms (justera mot taket)' side: debit type: vat - vat_rate: 0.25 + vat_rate: 0.12 - account: '1930' label: 'Företagskonto' side: credit