feat(dimensions): show the value's name after picking, and let an unused custom dimension be deleted (#2219) (#2255)

* feat(dimensions): show the value's name after picking, and let an unused custom dimension be deleted (#2219)

Two things from the same Discord report, both in bookkeeping from the
transaction view:

1. After picking a kostnadsställe the field showed only the code ("1").
   DimensionCombobox now writes the value's full name under the field once
   a code is committed, exactly as AccountCombobox does for the account
   name (looked up in the full registry so an archived code stays
   readable). The input text itself stays the code: the blur/revert logic
   keys on it.

2. A self-created dimension could not be removed at all: the DB already
   allowed it (enforce_dimension_registry_guards lets a non-system
   dimension go when no posted/reversed line carries its number, and the
   value retention trigger fires on the cascade), but no route or UI
   asked. New DELETE /api/dimensions/[id]: 400 DIMENSION_SYSTEM_DELETE for
   kostnadsställe/projekt, the guard's own Swedish P0001 verbatim as 409
   DIMENSION_REFERENCED, 404, and a happy path; the register gets a quiet
   "Ta bort dimension" link for the active custom dimension behind a
   DestructiveConfirmDialog. Keys added to sv and en.

Closes #2219

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VnConrmMCxJRQ5kfiPPWyy

* test: satisfy the TypeScript ratchet for two test files main inherited from #2247 and #2242

accounts-route.test.ts built SyncResult literals without the
requestedFromDate / historyNarrowed fields #2247 added (vitest does not
typecheck, so it passed locally); fiscal-periods route.test.ts got two
more one-argument POST(req) calls from #2242 in a file already at its
ratchet baseline. Both files now typecheck; the ratchet runs clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VnConrmMCxJRQ5kfiPPWyy

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-09-03 19:20:09 +02:00
committed by GitHub
co-authored by Claude Fable 5.1 Jakob Wennberg
parent 2c3ffaf51c
commit a48508e5b0
10 changed files with 269 additions and 9 deletions
+5
View File
@@ -1531,6 +1531,11 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-09-03] KPI monthly breakdown counts reversed originals (#2201): the monthly section of get_kpi_report_aggregates (new migration 20260903160000) and lib/reports/monthly-breakdown.ts now use tb_ex_year_end's entry set verbatim (posted + reversed, minus the undone year-end chain) instead of posted-only. A same-year storno then cancels inside the months as it does in the year total, so sum(months) = Nettoresultat; the reversal shows as negative revenue in its own month, which is the honest month view. The pg-real pin "in tb, not in monthly" was flipped, not worked around. Unblocks the per-month sum on Nyckeltal (#2196).
[2026-09-03] customers.country and suppliers.country are ISO 3166-1 alpha-2 at every writer (form select, internal + v1 REST, MCP, imports, provider migration), normalised through one helper (lib/vat/country-codes.ts) that also accepts the Swedish/English names the form used to write; unknown text is a 400 on write and left as-is by the backfill (migration 20260903170000 keeps the original in country_raw for a one-UPDATE rollback, and derives the country from the VAT prefix for eu_business rows whose country was null or only the old writer default SE: on prod that is one validated row plus sixteen without a country, and without it they would flip from reverse charge to 25% on their next invoice). No CHECK constraint on the column: unmapped legacy rows would violate it, and the periodisk report already warns on those. The country-vs-type rule (swedish_business = SE, eu_business = not SE and either in the EU VAT area with a matching prefix or holding an EU-trade VAT registration such as a Swiss company with a DE number or Northern Ireland XI, non_eu_business = outside the EU) is enforced on customers only, and on update only when type, country or VAT number is part of the change so a contradictory legacy row can still change its email; individuals are free (a foreign private person is still a Swedish-VAT customer) and suppliers get normalisation without the rule, since #2025/#2028 are about sales VAT. An omitted country on create is SE for Swedish types, derived from the VAT prefix for eu_business, and a 400 for non_eu_business: guessing a non-EU country is not possible, and Sweden-by-default was the bug. vat-rules.ts takes the country as a third optional argument and refuses reverse charge only for SE (a VIES-validated number outweighs a non-EU address), and not for an unknown/unmapped country: charging Swedish VAT to a genuine German customer whose row says Deutschland (Bayern) would be the worse error.
[2026-09-03] AGI kvittens cron: dropped the apigw_config bucket (#963) and its warn-once suppression for ACCESS_DENIED (#2226). The bucket existed because the APIGW client was known to lack the AGI hantera subscription in Utvecklarportalen; with that subscription expected in place, a gateway refusal is a regression and belongs in the ordinary error path (error level, generic 'error' status) rather than a status that hides it as a known gap. Same pass: the connector-mode gateway-refusal message names the connector operator by host instead of "kontakta supporten", because hosted is itself a Connect installation for the canary companies and "support" no longer says whose. Merging before the portal subscription is active means the 15-minute cron logs at error level per pending declaration until it is.
[2026-09-03] Payment terms 0 days (#2070): customer and supplier forms plus the three API schemas accept whole days 0-365 (0 = betalning direkt), and every `|| 30` fallback became `?? 30` so a stored 0 does not reopen as 30. Upper bound 365 is a sanity cap, not law; the invoice schema's own cap (90) is unchanged.
[2026-09-03] getErrorMessage passthrough (#2086): kept the keyword list as the first gate and added looksLikeUserFacingSwedish as a second (å/ä/ö, a strong Swedish word, or two weak function words, and no technical-leak pattern) instead of flipping to "show unless technical": several routes still return English free text ("Failed to disconnect") that must keep falling through to the status/context fallback, which existing tests pin. A registry-wide test asserts every message_sv passes.
[2026-09-03] SIE IB-imbalance warning (#2082): names the cause and links the manual IB wizard, but does NOT let the SIE import skip IB (the import refuses a period whose IB is already set), so the copy says the file is not imported on the manual path. Plugging the diff to 2010/2019 for enskild firma and running findUntransferredResults at parse time are separate founder calls.
[2026-09-03] Proforma at creation (#2217): a "Ny proformafaktura" split-button entry driven by ?proforma=1, mirroring ?quote=1, rather than surfacing the Förval document-type select earlier in the editor: the split button is where a Fortnox user looks, and the editor's own title already tracks the type.
[2026-09-03] Delete unused custom dimension (#2219): new DELETE /api/dimensions/[id] leans on the existing DB registry guard (enforce_dimension_registry_guards: system dims undeletable, any posted/reversed line tagged with the number refuses) and surfaces its P0001 verbatim as 409 DIMENSION_REFERENCED; the route only pre-empts the system-dimension case with 400 DIMENSION_SYSTEM_DELETE. No separate "is it used" query: the trigger is the single source of truth. The picker shows the value's name under the code after a pick, as AccountCombobox does, instead of changing the input text (the blur/revert logic keys on the code). Decision lines for #2070, #2086, #2082 and #2217 ride here for the same append-only-log reason as the first batch.
[2026-09-03] Fiscal period "first" = no existing period starts earlier (#2237): POST /api/bookkeeping/fiscal-periods decided first-period from `allPeriods.length === 0`, which refused a mid-month first räkenskapsår backfilled after a Fortnox import (2022-07-22) while the DB trigger enforce_first_of_month_for_subsequent_periods would have accepted the row. The route now mirrors the trigger instead of adding a second definition; the 1st-of-month rule (BFL 3 kap. 1 §) keeps binding subsequent years and its message now says why and what is allowed.
[2026-09-03] Tenant brand logos render with next/image `unoptimized` (#2203): the optimizer's remotePatterns allowlist is fixed at build time and the generic Docker image bakes a sentinel for NEXT_PUBLIC_SUPABASE_URL, so /_next/image rejected the runtime Supabase host with 400 and the sidebar mark broke on self-hosted while the favicon (same URL) worked. Chosen over a same-origin proxy route (more code, a second fetch hop for a 26px image) and over a runtime-configurable allowlist (Next.js has none). The remotePatterns block stays for builds that know the URL.
[2026-09-03] Danger zone (#2214): both company actions (Radera företag, Starta om migrering) and the account deletion already open a dialog that requires the company name / e-mail typed in, so nothing destructive can fire from a tap or swipe. The report came from a user who did not dare press the link to find that out, so the fix is copy on the row ("Inget händer direkt: du bekräftar i nästa steg ...") rather than a second dialog or a different control.