fix(bookkeeping): correct seeded BAS account names that contradicted engine bookings (#1321)

- seed_chart_of_accounts named 7210 'Semesterlöner' while payroll books
  gross salaries there (BAS: 'Löner till tjänstemän'; vacation pay is
  7285), so every seeded AB showed salary costs under a vacation-pay
  label in Nyckeltal and every other report
- 3002 was named 'Försäljning varor 25%' although 3002 is the 12% revenue
  account everywhere else (invoice booking, category mapping,
  default_vat_rate seeding)
- 7010 and 3001 get their BAS 2026 names; 2631 loses a double space
- backfill renames existing rows only on exact seeded-literal match plus
  is_system_account, so accounts users renamed survive untouched
- account-descriptions.ts had the 7010/7210 names swapped;
  client-account-names.ts labeled 2510 (Skatteskulder) as 'Personalskatt'

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-31 08:58:27 +02:00
committed by GitHub
parent f49dc3438d
commit 4933fae7a9
5 changed files with 216 additions and 14 deletions
+2
View File
@@ -719,3 +719,5 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-07-30] A missing org number on either side of the Bokio connect probe does NOT block the connection; only a confident mismatch does. Accounted allows companies without an org number and a provider response can omit it, so blocking on absence would refuse legitimate connections to prevent a mismatch we have no evidence of. Absence instead falls through to labelling the consent with the company the credentials actually opened, which is what lets the user catch it. Same reasoning applied to keeping 429/5xx from the probe out of the invalid-credentials mapping: a provider outage must not read as "your token is wrong".
[2026-07-30] The sandbox lock on a paid extension workspace is checked BEFORE the capability gate, and gets its own state (ExtensionSandboxLockState), not the billing upsell. Every new company, sandbox included, gets a 30-day trial grant from the seed_trial trigger, so the paywall waves a demo company straight through onto a workspace whose external services lib/sandbox/guard.ts blocks. And an anonymous demo user has no billing to upgrade: the exit is "Skapa konto", which must sign the anonymous session out first (registering on top of it registers into the sandbox).
[2026-07-31] Seeded chart names corrected to BAS 2026 where the label contradicted what the engine books on the account (7210 'Semesterlöner' -> 'Löner till tjänstemän', 7010, 3001/3002), with an exact-literal + is_system_account backfill so user renames survive. The mislabel class fixed here is "name says X, bookings are Y": payroll books salaries to 7210, 12% revenue books to 3002. The 3100 'Momsfri försäljning' deviation from BAS ('Försäljning av varor utanför Sverige') stays: there the label and the ruta 42 mapping agree by design, so renaming to BAS would create the mismatch, not fix one.
+8 -7
View File
@@ -274,19 +274,20 @@ const ACCOUNT_DESCRIPTIONS: Record<string, AccountDescription> = {
explanation: 'Diverse externa kostnader som inte passar in under andra konton.',
},
// Class 7: Personnel & depreciation
// Class 7: Personnel & depreciation. Per BAS 2026: 7010 kollektivanställda,
// 7210 tjänstemän (the account the payroll engine books salaries to).
'7010': {
name: 'Löner kollektivanställda',
classLabel: 'Personal och avskrivningar',
type: 'expense',
explanation: 'Bruttolöner (före skatt) till arbetare och kollektivanställda.',
},
'7210': {
name: 'Löner tjänstemän',
classLabel: 'Personal och avskrivningar',
type: 'expense',
explanation: 'Bruttolöner (före skatt) till anställda tjänstemän.',
},
'7210': {
name: 'Löner kollektiv',
classLabel: 'Personal och avskrivningar',
type: 'expense',
explanation: 'Bruttolöner till arbetare och kollektivanställda.',
},
'7510': {
name: 'Arbetsgivaravgifter',
classLabel: 'Personal och avskrivningar',
+2 -2
View File
@@ -18,7 +18,7 @@ const ACCOUNT_NAMES: Record<string, string> = {
'2350': 'Långfristiga skulder',
'2393': 'Kortfristig skuld närstående',
'2440': 'Leverantörsskulder',
'2510': 'Personalskatt',
'2510': 'Skatteskulder',
'2611': 'Utg. moms 25%',
'2621': 'Utg. moms 12%',
'2631': 'Utg. moms 6%',
@@ -83,7 +83,7 @@ const ACCOUNT_NAMES: Record<string, string> = {
'6991': 'Övriga kostnader',
// Personnel & financial (7xxx / 8xxx)
'7210': 'Löner',
'7210': 'Löner tjänstemän',
'7410': 'Pensionsförsäkring',
'7610': 'Utbildning',
'7622': 'Intern representation',
@@ -0,0 +1,192 @@
-- Correct BAS account names in seed_chart_of_accounts where the seeded label
-- contradicted what the engine actually books on the account (#nyckeltal
-- mislabel report, 2026-07-31):
--
-- 7210 was seeded as 'Semesterlöner', but BAS 7210 is 'Löner till
-- tjänstemän' and the payroll engine books gross salaries there
-- (lib/salary/account-mapping.ts: monthly_salary/hourly_salary ->
-- '7210'; vacation pay goes to 7285). Every seeded aktiebolag that
-- ran payroll showed its salary costs under a "Semesterlöner" row,
-- e.g. in the Nyckeltal "Största kostnaderna" list.
-- 7010 was seeded as the bare 'Löner'; BAS 7010 is 'Löner till
-- kollektivanställda'.
-- 3002 was seeded as 'Försäljning varor 25%', but 3002 is the 12% revenue
-- account everywhere else: invoice booking routes 12% lines there
-- (lib/bookkeeping/invoice-entries.ts), category mapping does the
-- same, and 20260728120000 seeds default_vat_rate = 0.12 on it.
-- 3001 was seeded as 'Försäljning tjänster 25%' although it receives ALL
-- 25% revenue (goods and services alike); it gets the BAS name too.
-- 2631 carried a double space ('Sverige, 6%'); cosmetic, fixed while the
-- literals are re-emitted.
--
-- Two parts: (1) redefine the seed function with the corrected literals so
-- new companies are right from the start; (2) backfill existing rows, but
-- ONLY where the name is still byte-identical to a seeded literal (or its
-- pre-20260516 ASCII fold) and the row is a system account, so an account a
-- user renamed on purpose is never touched. Bookings are not affected:
-- account_name is a label, the account_number is the identity.
--
-- The function body is otherwise identical to 20260516130000 (the latest
-- definition). Signature unchanged, so CREATE OR REPLACE keeps the EXECUTE
-- grant restored by that migration.
CREATE OR REPLACE FUNCTION public.seed_chart_of_accounts(p_company_id uuid, p_entity_type text)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_account_count integer;
v_user_id uuid;
BEGIN
SELECT created_by INTO v_user_id FROM public.companies WHERE id = p_company_id;
SELECT count(*) INTO v_account_count
FROM public.chart_of_accounts
WHERE company_id = p_company_id;
IF v_account_count > 0 THEN
RETURN;
END IF;
-- Assets (1xxx)
INSERT INTO public.chart_of_accounts (user_id, company_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account, sru_code)
VALUES
(v_user_id, p_company_id, '1510', 'Kundfordringar', 1, '15', 'asset', 'debit', 'k1', true, '7211'),
(v_user_id, p_company_id, '1910', 'Kassa', 1, '19', 'asset', 'debit', 'k1', true, '7212'),
(v_user_id, p_company_id, '1930', 'Företagskonto / checkkonto', 1, '19', 'asset', 'debit', 'k1', true, '7212'),
(v_user_id, p_company_id, '1940', 'Övriga bankkonton', 1, '19', 'asset', 'debit', 'k1', true, '7212');
-- Equity (2xxx)
IF p_entity_type = 'enskild_firma' THEN
-- Enskild firma equity accounts: sru_code intentionally NULL.
-- BAS reference maps these to INK2 SRU 7221 ("Övrigt eget kapital"),
-- which is the aktiebolag tax form. EF entities file NE-bilaga, not
-- INK2, and owner drawings/contributions on 2013/2018 must not be
-- reported as balance-sheet equity by SIE/INK2 consumers.
INSERT INTO public.chart_of_accounts (user_id, company_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account, sru_code)
VALUES
(v_user_id, p_company_id, '2010', 'Eget kapital', 2, '20', 'equity', 'credit', 'k1', true, NULL),
(v_user_id, p_company_id, '2013', 'Övriga egna uttag', 2, '20', 'equity', 'credit', 'k1', true, NULL),
(v_user_id, p_company_id, '2018', 'Övriga egna insättningar', 2, '20', 'equity', 'credit', 'k1', true, NULL);
END IF;
IF p_entity_type = 'aktiebolag' THEN
INSERT INTO public.chart_of_accounts (user_id, company_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account, sru_code)
VALUES
(v_user_id, p_company_id, '2081', 'Aktiekapital', 2, '20', 'equity', 'credit', 'k1', true, '7220'),
(v_user_id, p_company_id, '2091', 'Balanserat resultat', 2, '20', 'equity', 'credit', 'k1', true, '7221'),
(v_user_id, p_company_id, '2099', 'Årets resultat', 2, '20', 'equity', 'credit', 'k1', true, '7222');
END IF;
-- Liabilities (2xxx) - BAS 2026 VAT account labels
INSERT INTO public.chart_of_accounts (user_id, company_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account, sru_code)
VALUES
(v_user_id, p_company_id, '2440', 'Leverantörsskulder', 2, '24', 'liability', 'credit', 'k1', true, '7230'),
(v_user_id, p_company_id, '2611', 'Utgående moms försäljning inom Sverige, 25%', 2, '26', 'liability', 'credit', 'k1', true, '7231'),
(v_user_id, p_company_id, '2621', 'Utgående moms försäljning inom Sverige, 12%', 2, '26', 'liability', 'credit', 'k1', true, '7231'),
(v_user_id, p_company_id, '2631', 'Utgående moms försäljning inom Sverige, 6%', 2, '26', 'liability', 'credit', 'k1', true, '7231'),
(v_user_id, p_company_id, '2641', 'Debiterad ingående moms', 2, '26', 'liability', 'credit', 'k1', true, '7231'),
(v_user_id, p_company_id, '2650', 'Redovisningskonto för moms', 2, '26', 'liability', 'credit', 'k1', true, '7231'),
(v_user_id, p_company_id, '2710', 'Personalskatt', 2, '27', 'liability', 'credit', 'k1', true, '7231'),
(v_user_id, p_company_id, '2731', 'Avräkning socialavgifter', 2, '27', 'liability', 'credit', 'k1', true, '7231');
IF p_entity_type = 'aktiebolag' THEN
INSERT INTO public.chart_of_accounts (user_id, company_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account, sru_code)
VALUES
(v_user_id, p_company_id, '2893', 'Skuld till aktieägare', 2, '28', 'liability', 'credit', 'k1', true, '7231');
END IF;
-- Revenue (3xxx). 3001/3002 carry the official BAS 2026 names: 3001 takes
-- ALL 25% revenue and 3002 is the 12% account (invoice booking, category
-- mapping and default_vat_rate all treat it as 12%), so the name must say
-- so.
INSERT INTO public.chart_of_accounts (user_id, company_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account, sru_code)
VALUES
(v_user_id, p_company_id, '3001', 'Försäljning inom Sverige, 25 % moms', 3, '30', 'revenue', 'credit', 'k1', true, '7310'),
(v_user_id, p_company_id, '3002', 'Försäljning inom Sverige, 12 % moms', 3, '30', 'revenue', 'credit', 'k1', true, '7310'),
(v_user_id, p_company_id, '3100', 'Momsfri försäljning', 3, '31', 'revenue', 'credit', 'k1', true, '7311'),
(v_user_id, p_company_id, '3900', 'Övriga rörelseintäkter', 3, '39', 'revenue', 'credit', 'k1', true, '7311'),
(v_user_id, p_company_id, '3960', 'Valutakursvinster', 3, '39', 'revenue', 'credit', 'k1', true, '7310');
-- COGS (4xxx)
INSERT INTO public.chart_of_accounts (user_id, company_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account, sru_code)
VALUES
(v_user_id, p_company_id, '4000', 'Varuinköp', 4, '40', 'expense', 'debit', 'k1', true, '7320');
-- External expenses (5xxx-6xxx)
INSERT INTO public.chart_of_accounts (user_id, company_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account, sru_code)
VALUES
(v_user_id, p_company_id, '5010', 'Lokalhyra', 5, '50', 'expense', 'debit', 'k1', true, '7321'),
(v_user_id, p_company_id, '5410', 'Förbrukningsinventarier', 5, '54', 'expense', 'debit', 'k1', true, '7321'),
(v_user_id, p_company_id, '5420', 'Programvaror', 5, '54', 'expense', 'debit', 'k1', true, '7321'),
(v_user_id, p_company_id, '5460', 'Förbrukningsmaterial', 5, '54', 'expense', 'debit', 'k1', true, '7321'),
(v_user_id, p_company_id, '5800', 'Resekostnader', 5, '58', 'expense', 'debit', 'k1', true, '7321'),
(v_user_id, p_company_id, '5910', 'Annonsering', 5, '59', 'expense', 'debit', 'k1', true, '7321'),
(v_user_id, p_company_id, '6071', 'Representation avdragsgill', 6, '60', 'expense', 'debit', 'k1', true, '7321'),
(v_user_id, p_company_id, '6110', 'Kontorsmateriel', 6, '61', 'expense', 'debit', 'k1', true, '7321'),
(v_user_id, p_company_id, '6212', 'Mobiltelefon', 6, '62', 'expense', 'debit', 'k1', true, '7321'),
(v_user_id, p_company_id, '6230', 'Datakommunikation', 6, '62', 'expense', 'debit', 'k1', true, '7321'),
(v_user_id, p_company_id, '6530', 'Redovisningstjänster', 6, '65', 'expense', 'debit', 'k1', true, '7321'),
(v_user_id, p_company_id, '6570', 'Bankavgifter', 6, '65', 'expense', 'debit', 'k1', true, '7321'),
(v_user_id, p_company_id, '6991', 'Övriga avdragsgilla kostnader', 6, '69', 'expense', 'debit', 'k1', true, '7330');
-- Personnel (7xxx). BAS names: 7010 kollektivanställda, 7210 tjänstemän.
-- The payroll engine books gross salaries to 7210 and vacation pay to
-- 7285 (auto-created with its BAS name when first needed).
IF p_entity_type = 'aktiebolag' THEN
INSERT INTO public.chart_of_accounts (user_id, company_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account, sru_code)
VALUES
(v_user_id, p_company_id, '7010', 'Löner till kollektivanställda', 7, '70', 'expense', 'debit', 'k1', true, '7322'),
(v_user_id, p_company_id, '7210', 'Löner till tjänstemän', 7, '72', 'expense', 'debit', 'k1', true, '7322'),
(v_user_id, p_company_id, '7510', 'Arbetsgivaravgifter', 7, '75', 'expense', 'debit', 'k1', true, '7322');
END IF;
INSERT INTO public.chart_of_accounts (user_id, company_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account, sru_code)
VALUES
(v_user_id, p_company_id, '7960', 'Valutakursförluster', 7, '79', 'expense', 'debit', 'k1', true, '7360');
-- Financial (8xxx)
INSERT INTO public.chart_of_accounts (user_id, company_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account, sru_code)
VALUES
(v_user_id, p_company_id, '8310', 'Ränteintäkter', 8, '83', 'revenue', 'credit', 'k1', true, '7313'),
(v_user_id, p_company_id, '8410', 'Räntekostnader', 8, '84', 'expense', 'debit', 'k1', true, '7323');
END;
$$;
-- Backfill existing companies. Exact-literal matches only (current form plus
-- the pre-20260516 ASCII fold), restricted to seeded system accounts, so any
-- name a user set themselves survives untouched.
UPDATE public.chart_of_accounts
SET account_name = 'Löner till tjänstemän'
WHERE account_number = '7210'
AND is_system_account = true
AND account_name IN ('Semesterlöner', 'Semesterloner');
UPDATE public.chart_of_accounts
SET account_name = 'Löner till kollektivanställda'
WHERE account_number = '7010'
AND is_system_account = true
AND account_name IN ('Löner', 'Loner');
UPDATE public.chart_of_accounts
SET account_name = 'Försäljning inom Sverige, 25 % moms'
WHERE account_number = '3001'
AND is_system_account = true
AND account_name IN ('Försäljning tjänster 25%', 'Forsaljning tjanster 25%');
UPDATE public.chart_of_accounts
SET account_name = 'Försäljning inom Sverige, 12 % moms'
WHERE account_number = '3002'
AND is_system_account = true
AND account_name IN ('Försäljning varor 25%', 'Forsaljning varor 25%');
UPDATE public.chart_of_accounts
SET account_name = 'Utgående moms försäljning inom Sverige, 6%'
WHERE account_number = '2631'
AND is_system_account = true
AND account_name IN ('Utgående moms försäljning inom Sverige, 6%', 'Utgaende moms forsaljning inom Sverige, 6%');
NOTIFY pgrst, 'reload schema';
@@ -113,13 +113,16 @@ describe('seed_chart_of_accounts: Swedish characters', () => {
'2440': 'Leverantörsskulder',
'2611': 'Utgående moms försäljning inom Sverige, 25%',
'2621': 'Utgående moms försäljning inom Sverige, 12%',
'2631': 'Utgående moms försäljning inom Sverige, 6%',
'2631': 'Utgående moms försäljning inom Sverige, 6%',
'2641': 'Debiterad ingående moms',
'2650': 'Redovisningskonto för moms',
'2731': 'Avräkning socialavgifter',
'2893': 'Skuld till aktieägare',
'3001': 'Försäljning tjänster 25%',
'3002': 'Försäljning varor 25%',
// 3001/3002 carry the BAS 2026 names: 3002 is the 12% revenue account
// (invoice booking, category mapping, default_vat_rate) and was once
// seeded as 'Försäljning varor 25%', see 20260731090000.
'3001': 'Försäljning inom Sverige, 25 % moms',
'3002': 'Försäljning inom Sverige, 12 % moms',
'3100': 'Momsfri försäljning',
'3900': 'Övriga rörelseintäkter',
'4000': 'Varuinköp',
@@ -127,8 +130,12 @@ describe('seed_chart_of_accounts: Swedish characters', () => {
'5460': 'Förbrukningsmaterial',
'6530': 'Redovisningstjänster',
'6991': 'Övriga avdragsgilla kostnader',
'7010': 'Löner',
'7210': 'Semesterlöner',
// BAS 2026: 7010 kollektivanställda, 7210 tjänstemän. 7210 was once
// seeded as 'Semesterlöner' (vacation pay lives on 7285), which put the
// payroll engine's salary bookings under a vacation-pay label in every
// report; see 20260731090000.
'7010': 'Löner till kollektivanställda',
'7210': 'Löner till tjänstemän',
'7960': 'Valutakursförluster',
'8310': 'Ränteintäkter',
'8410': 'Räntekostnader',