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>
This commit is contained in:
co-authored by
Claude Fable 5
Jakob Wennberg
parent
1cd33d9d97
commit
79240cb2ed
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { HouseworkTypeSchema } from '../schemas'
|
||||
import {
|
||||
// Enums
|
||||
EntityTypeSchema,
|
||||
@@ -2871,3 +2872,28 @@ describe('CreateSalaryLineItemSchema: derived-only item types', () => {
|
||||
expect(UpdateSalaryLineItemSchema.safeParse({ item_type: 'bonus' }).success).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('HouseworkTypeSchema (articles.housework_type)', () => {
|
||||
it('stores a Skatteverket work-type code upper-cased', () => {
|
||||
expect(HouseworkTypeSchema.parse('stad')).toBe('STAD')
|
||||
expect(HouseworkTypeSchema.parse('BYGG')).toBe('BYGG')
|
||||
})
|
||||
|
||||
it('accepts the bare kind ROT / RUT (deduction only, no arbetstyp pre-fill)', () => {
|
||||
expect(HouseworkTypeSchema.parse('rut')).toBe('RUT')
|
||||
expect(HouseworkTypeSchema.parse('ROT')).toBe('ROT')
|
||||
})
|
||||
|
||||
it('treats empty string as clear and passes null/undefined through', () => {
|
||||
expect(HouseworkTypeSchema.parse('')).toBeNull()
|
||||
expect(HouseworkTypeSchema.parse(' ')).toBeNull()
|
||||
expect(HouseworkTypeSchema.parse(null)).toBeNull()
|
||||
expect(HouseworkTypeSchema.parse(undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('rejects values the invoice editor could never interpret', () => {
|
||||
for (const bad of ['1', '0', 'Ja', 'SNICKERI', 'RUT-städning']) {
|
||||
expect(HouseworkTypeSchema.safeParse(bad).success).toBe(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
+28
-1
@@ -13,6 +13,7 @@ import { DimensionsBagSchema } from '@/lib/bookkeeping/dimension-resolver'
|
||||
import { validateEmployeeBankAccount } from '@/lib/salary/payment/bank-account'
|
||||
import { MAX_INVOICE_EMAIL_COPY_RECIPIENTS } from '@/lib/invoices/email-recipients'
|
||||
import { INVOICE_POSTING_ACCOUNT_REGEX } from '@/lib/invoices/posting-account'
|
||||
import { HOUSEWORK_TYPE_VALUES, normalizeHouseworkType } from '@/lib/invoices/rot-rut-rules'
|
||||
import { PERSONAL_NUMBER_INPUT_RE } from '@/lib/customers/mask-personal-number'
|
||||
import type { AuditAction } from '@/types'
|
||||
import type { BankFileFormatId } from '@/lib/import/bank-file/types'
|
||||
@@ -637,6 +638,32 @@ export const RotRutBeslutFileSchema = z.object({
|
||||
|
||||
export const ArticleTypeSchema = z.enum(['vara', 'tjanst'])
|
||||
|
||||
/**
|
||||
* articles.housework_type: a Skatteverket arbetstypskod (BYGG, EL, ..., STAD,
|
||||
* TRADGARD, ...) or the bare kind ROT / RUT (deduction only, no arbetstyp
|
||||
* pre-fill). Case-insensitive, stored upper-case; '' clears to null. The
|
||||
* invoice editor derives a line's skattereduktion from this value, so any
|
||||
* other string is a silently dead flag and is rejected here.
|
||||
*/
|
||||
export const HouseworkTypeSchema = z
|
||||
.string()
|
||||
.max(64)
|
||||
.nullable()
|
||||
.optional()
|
||||
.transform((v, ctx) => {
|
||||
if (v == null) return v
|
||||
if (v.trim() === '') return null
|
||||
const normalized = normalizeHouseworkType(v)
|
||||
if (!normalized) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Ogiltig ROT/RUT-arbetstyp. Tillåtna värden: ${HOUSEWORK_TYPE_VALUES.join(', ')}`,
|
||||
})
|
||||
return z.NEVER
|
||||
}
|
||||
return normalized
|
||||
})
|
||||
|
||||
export const CreateArticleSchema = z.object({
|
||||
name: z.string().min(1, 'Article name is required').max(200),
|
||||
type: ArticleTypeSchema.optional(),
|
||||
@@ -655,7 +682,7 @@ export const CreateArticleSchema = z.object({
|
||||
cost_price: nonNegativeAmount.nullable().optional(),
|
||||
ean: z.string().max(32).nullable().optional(),
|
||||
// ROT/RUT arbetstyp; only meaningful for type === 'tjanst'.
|
||||
housework_type: z.string().max(64).nullable().optional(),
|
||||
housework_type: HouseworkTypeSchema,
|
||||
name_en: z.string().max(200).nullable().optional(),
|
||||
notes: z.string().max(2000).nullable().optional(),
|
||||
// Manual article number; omit to auto-generate via generate_article_number.
|
||||
|
||||
Reference in New Issue
Block a user