* fix(skatteverket): request the ska scope for skattekonto v2 The skattekonto v2 API rejects skahmst-only tokens with 403 "The required scopes are not authorized" (observed in prod 2026-07-20; no company has synced since 2026-05-10). The requested `skattekonto` scope is silently dropped from every grant, while `ska` appears in one real May grant, so request it too: SKV grants the intersection, so this is harmless if wrong. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skatteverket): correct the skattekonto scope model around ska Root cause of the May 10 skattekonto outage, confirmed via git history and prod token data: the `ska` scope (the interactive skattekonto API's actual scope, requested since the extension's first commit in March) was removed by the "remove unused scopes" cleanup in the #431 series. Every token issued after that hour lacks it and the API answers 403 "The required scopes are not authorized"; no company has synced since. The May 15 repair re-added skahmst, which per its tjanstebeskrivning is a different bulk E-transport service and does not substitute; `skattekonto` is not a real SKV scope name and is silently dropped from grants. Follow-up to the ska re-request (cd8f7a30): - document the confirmed scope model in oauth.ts so ska is never "cleaned up" again - panel missing-scope warning and reconnect-button now gate on ska, not skahmst/skattekonto - scope badge labels: ska takes the saldo & transaktioner label, skahmst relabeled as the E-transport file service - consent-page note covers both terse scope names and says ska is required Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(year-end): warn on untaxed profit at verkstall, Swedish readiness messages, always-visible period selector An aktiebolag could execute year-end with a profit and zero bolagsskatt booked without any warning (support case: closing moved 592k to 2099 untaxed). The preview now computes bolagsskattMissing (AB + profit + no 89xx account among closed accounts, 8999 excluded) and both the preview and execute steps render an advisory, bypassable warning. validateYearEndReadiness messages are now Swedish (the bokslut wizard is a stays-Swedish surface); the MCP year_end_readiness classifier matches both the new Swedish strings and the legacy English ones. The wizard period selector now always renders, keeps a selected-but- ineligible period selectable, and resets a stale ?period= id from another company instead of leaving the user stuck on the wrong year. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(year-end): administrative undo of an executed year-end closing Storno-only reset used when a bokslut was executed prematurely (e.g. without bolagsskatt) and no arsredovisning exists yet: reverses the next period's result_appropriation and opening_balance entries, reopens the period, reverses the closing entry, and detaches closing_entry_id. Resumable if interrupted midway; attribution per BFL 5 kap 6. Migration 20260720140000 adds the trigger escape hatch: closing_entry_id may only change once set when the old closing entry is reversed with a posted storno chain (status flag alone is forgeable via PostgREST), and a non-NULL replacement must be a posted year_end entry in the same period. Covered by a pg-real test. planResultAppropriation idempotency is now posted-only: a reversed omforing no longer blocks the re-run from posting a fresh 2099 -> 2098 reclassification (it previously returned null silently, leaving the new year's equity polluted). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): address CodeRabbit, PR-Agent and compliance findings - undo script: company_id filters on verify queries, period-scope the arsredovisning precondition checks, validate service-key format, escalate audit_log insert failure to a hard error (BFNAR 2013:2) - detach migration: company-scope the storno chain EXISTS, replace the em dash in the new error message Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): address round-2 compliance swarm and Swedish review findings - undo script: require --confirm-url with --commit so an env swap fails loud; retry the audit_log insert 3x and direct the operator to insert the behandlingshistorik row manually on final failure (BFNAR 2013:2) - year-end preview: document why resultAccountSummary is a complete 89xx scan; warning text now also names periodiseringsfond and overavskrivningar as legitimate zero-tax reasons Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
74 lines
3.0 KiB
PL/PgSQL
74 lines
3.0 KiB
PL/PgSQL
-- Allow detaching or replacing a period's closing_entry_id ONLY when the
|
|
-- previously referenced closing entry has been genuinely reversed by storno.
|
|
--
|
|
-- Background: the administrative year-end undo flow (scripts/
|
|
-- undo-year-end-closing.ts) reverses the bokslutsverifikation with a storno
|
|
-- (BFL 5 kap 5 §: never edit, never delete) and must then clear
|
|
-- closing_entry_id so the year-end wizard can be re-run. The previous rule
|
|
-- blocked ANY change to closing_entry_id once set, which made an executed
|
|
-- bokslut unrecoverable even before an arsredovisning exists.
|
|
--
|
|
-- The invariant that matters is preserved and tightened:
|
|
-- * a period can never abandon a LIVE (posted) closing entry;
|
|
-- * the status='reversed' flag alone is not trusted (it is reachable via a
|
|
-- direct PostgREST update by a writer-role member): the actual storno
|
|
-- chain that only the engine's reverseEntry() produces must exist
|
|
-- (a posted source_type='storno' entry with reverses_id pointing at the
|
|
-- old closing entry);
|
|
-- * a non-NULL replacement must be a posted year_end entry in the same
|
|
-- company and period (what executeYearEndClosing sets on a re-run).
|
|
--
|
|
-- The opening-balance clause is unchanged.
|
|
|
|
CREATE OR REPLACE FUNCTION public.enforce_opening_balance_immutability()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
SET search_path TO 'public'
|
|
AS $function$
|
|
BEGIN
|
|
-- Only check if opening_balance_entry_id is being changed
|
|
IF OLD.opening_balance_entry_id IS NOT NULL
|
|
AND OLD.opening_balances_set = true
|
|
AND NEW.opening_balance_entry_id IS DISTINCT FROM OLD.opening_balance_entry_id THEN
|
|
RAISE EXCEPTION 'Cannot modify opening_balance_entry_id on period "%" — opening balances are immutable once set',
|
|
OLD.name;
|
|
END IF;
|
|
|
|
-- Block changing closing_entry_id once set, UNLESS the referenced closing
|
|
-- entry has been reversed by a real storno (administrative year-end undo).
|
|
IF OLD.closing_entry_id IS NOT NULL
|
|
AND NEW.closing_entry_id IS DISTINCT FROM OLD.closing_entry_id THEN
|
|
|
|
IF NOT EXISTS (
|
|
SELECT 1
|
|
FROM journal_entries je
|
|
JOIN journal_entries storno
|
|
ON storno.reverses_id = je.id
|
|
AND storno.source_type = 'storno'
|
|
AND storno.status = 'posted'
|
|
AND storno.company_id = OLD.company_id
|
|
WHERE je.id = OLD.closing_entry_id
|
|
AND je.company_id = OLD.company_id
|
|
AND je.status = 'reversed'
|
|
) THEN
|
|
RAISE EXCEPTION 'Cannot modify closing_entry_id on period "%": year-end closing is immutable',
|
|
OLD.name;
|
|
END IF;
|
|
|
|
IF NEW.closing_entry_id IS NOT NULL AND NOT EXISTS (
|
|
SELECT 1 FROM journal_entries ne
|
|
WHERE ne.id = NEW.closing_entry_id
|
|
AND ne.company_id = NEW.company_id
|
|
AND ne.fiscal_period_id = NEW.id
|
|
AND ne.source_type = 'year_end'
|
|
AND ne.status = 'posted'
|
|
) THEN
|
|
RAISE EXCEPTION 'closing_entry_id on period "%" must reference a posted year_end entry in the same period',
|
|
OLD.name;
|
|
END IF;
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$function$;
|