Files
accounted/lib/pending-operations/schemas/__tests__/article.test.ts
T
79240cb2ed fix(articles): article ROT/RUT prefill was dead for every dashboard-created article (#1651)
* fix(articles): article ROT/RUT prefill was dead for every dashboard-created article

Follow-up to #1634. The user re-tested and picking a RUT article still left
the line on "Ingen": the article form has always stored the bare kind
('ROT'/'RUT'), while the prefill only recognised Skatteverket work-type codes
(BYGG, STAD, ...). On prod every dashboard-created ROT/RUT article holds the
bare kind, so the fix in #1634 never fired for a real user, and worse, since
the helper returned null for those values, picking such an article CLEARED a
deduction the user had set manually on the row.

- rot-rut-rules: parseArticleHouseworkType() understands both vocabularies
  (code -> kind + arbetstyp; bare ROT/RUT -> kind only), plus
  normalizeHouseworkType()/HOUSEWORK_TYPE_VALUES/workTypeLabel().
- InvoiceEditor.applyArticle: kind-only articles pre-fill the deduction and
  keep a same-kind arbetstyp already chosen on the row; "Spara som artikel"
  round-trips the code or, lacking one, the kind.
- ArticleForm: the ROT/RUT select now offers the real Skatteverket arbetstyper
  in ROT/RUT groups (its own hint always promised "förifyller arbetstyp");
  legacy kind-only values stay selectable as "RUT (arbetstyp ej vald)" so an
  edit never silently drops the flag. Article detail renders "RUT · Städning"
  instead of the raw code.
- API + MCP commit schemas normalize housework_type (case-insensitive code or
  ROT/RUT, '' clears) and reject anything else; the CSV article import
  normalizes the column the same way. Prod holds 178 articles with '0'/'1'
  from a boolean "Rot" column that the keyword detector mapped straight
  through; those now read as no flag everywhere and can no longer be created.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(articles): review follow-ups on #1651

- InvoiceEditor: switching a row's skattereduktion ROT<->RUT clears an
  arbetstyp from the other list, and Spara som artikel only round-trips a
  work type that belongs to the row's kind (CodeRabbit).
- MCP update_article: null / '' / whitespace now clear housework_type
  (commit drops only undefined keys, so the old undefined mapping made the
  flag un-clearable); create keeps treating them as unset. Tests.
- Article CSV import warns when a non-empty ROT/RUT value is dropped as
  not-an-arbetstyp instead of dropping it silently. Test.
- Hint wording: arbetstyp is pre-filled only when the article carries one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 19:47:29 +02:00

77 lines
3.6 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { CreateArticleParamsSchema, UpdateArticleParamsSchema } from '../article'
const base = { name: 'EU-konsulting', price_excl_vat: 950 }
describe('CreateArticleParamsSchema currency', () => {
it('accepts and normalizes an ISO code to upper case', () => {
const parsed = CreateArticleParamsSchema.parse({ ...base, currency: 'eur' })
expect(parsed.currency).toBe('EUR')
})
it('treats empty string and null as unset (commit defaults to SEK)', () => {
expect(CreateArticleParamsSchema.parse({ ...base, currency: '' }).currency).toBeUndefined()
expect(CreateArticleParamsSchema.parse({ ...base, currency: null }).currency).toBeUndefined()
expect(CreateArticleParamsSchema.parse(base).currency).toBeUndefined()
})
it('rejects non-ISO shapes', () => {
expect(() => CreateArticleParamsSchema.parse({ ...base, currency: 'EURO' })).toThrow()
expect(() => CreateArticleParamsSchema.parse({ ...base, currency: 'E1' })).toThrow()
})
})
describe('UpdateArticleParamsSchema currency', () => {
const id = { article_id: '3a9ac4d2-163a-4d43-8fa3-1b32827505fa' }
it('accepts a currency-only update', () => {
const parsed = UpdateArticleParamsSchema.parse({ ...id, currency: 'usd' })
expect(parsed.currency).toBe('USD')
})
it('leaves currency undefined when omitted (sparse update must not touch it)', () => {
const parsed = UpdateArticleParamsSchema.parse({ ...id, name: 'Nytt namn' })
expect(parsed.currency).toBeUndefined()
})
})
describe('CreateArticleParamsSchema housework_type', () => {
it('normalizes a work-type code or bare kind to upper case', () => {
expect(CreateArticleParamsSchema.parse({ ...base, housework_type: 'stad' }).housework_type).toBe('STAD')
expect(CreateArticleParamsSchema.parse({ ...base, housework_type: 'rut' }).housework_type).toBe('RUT')
})
it('treats empty, whitespace and null as unset', () => {
expect(CreateArticleParamsSchema.parse({ ...base, housework_type: '' }).housework_type).toBeUndefined()
expect(CreateArticleParamsSchema.parse({ ...base, housework_type: ' ' }).housework_type).toBeUndefined()
expect(CreateArticleParamsSchema.parse({ ...base, housework_type: null }).housework_type).toBeUndefined()
})
it('rejects free text: the flag would otherwise be silently dead on invoice lines', () => {
expect(() => CreateArticleParamsSchema.parse({ ...base, housework_type: 'Snickeri' })).toThrow()
expect(() => CreateArticleParamsSchema.parse({ ...base, housework_type: '1' })).toThrow()
})
})
describe('UpdateArticleParamsSchema housework_type', () => {
const id = { article_id: '3a9ac4d2-163a-4d43-8fa3-1b32827505fa' }
it('accepts a housework_type-only update and normalizes it', () => {
expect(UpdateArticleParamsSchema.parse({ ...id, housework_type: 'malning' }).housework_type).toBe('MALNING')
})
it('leaves housework_type undefined when omitted (sparse update)', () => {
expect(UpdateArticleParamsSchema.parse({ ...id, name: 'Nytt namn' }).housework_type).toBeUndefined()
})
it('null, empty and whitespace-only clear the flag (commit drops only undefined keys)', () => {
expect(UpdateArticleParamsSchema.parse({ ...id, housework_type: null }).housework_type).toBeNull()
expect(UpdateArticleParamsSchema.parse({ ...id, housework_type: '' }).housework_type).toBeNull()
expect(UpdateArticleParamsSchema.parse({ ...id, housework_type: ' ' }).housework_type).toBeNull()
})
it('rejects free text on update too', () => {
expect(() => UpdateArticleParamsSchema.parse({ ...id, housework_type: 'Snickeri' })).toThrow()
})
})