feat(year-end): administrative undo of executed year-end closing + skatteverket scope fixes (#1081)
* 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>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e2d6c92e3a
commit
4e47335308
@@ -72,12 +72,14 @@ describe('gnubok_year_end_readiness: execute', () => {
|
||||
vi.mocked(validateYearEndReadiness).mockResolvedValue({
|
||||
ready: false,
|
||||
errors: [
|
||||
'3 draft journal entries must be posted or deleted before closing',
|
||||
'Unexplained voucher gap in series A: 5-7',
|
||||
'Trial balance is not balanced: debit=100, credit=200',
|
||||
// Current Swedish wording from validateYearEndReadiness…
|
||||
'3 utkast måste bokföras eller raderas innan bokslut',
|
||||
'Oförklarat verifikationsnummerglapp i serie A: 5-7',
|
||||
'Råbalansen balanserar inte: debet=100, kredit=200',
|
||||
// …and one legacy English string to prove the fallback still maps.
|
||||
'Sequence counter integrity error in series A: counter=3 but max voucher=5',
|
||||
],
|
||||
warnings: ['No posted journal entries in this period'],
|
||||
warnings: ['Inga bokförda verifikationer i perioden'],
|
||||
draftCount: 3,
|
||||
voucherGaps: [{ series: 'A', gap_start: 5, gap_end: 7 }],
|
||||
unexplainedGaps: [{ series: 'A', gap_start: 5, gap_end: 7 }],
|
||||
|
||||
@@ -10969,17 +10969,20 @@ export const tools: McpTool[] = [
|
||||
// Reshape error strings into structured blockers so the agent (and any
|
||||
// dashboard) can render and act on each one independently. The lib
|
||||
// returns flat strings; we tag each with a `kind` heuristic for routing.
|
||||
// validateYearEndReadiness emits Swedish messages (the bokslut wizard
|
||||
// renders them verbatim); English alternates are kept as fallback so
|
||||
// classification never regresses if an older message slips through.
|
||||
const blockers = validation.errors.map((message) => {
|
||||
let kind: string = 'other'
|
||||
if (/draft journal entries/i.test(message)) kind = 'draft_entries'
|
||||
else if (/voucher gap/i.test(message)) kind = 'unexplained_voucher_gap'
|
||||
else if (/Sequence counter integrity/i.test(message)) kind = 'sequence_mismatch'
|
||||
else if (/Trial balance is not balanced/i.test(message)) kind = 'trial_balance_unbalanced'
|
||||
else if (/already closed/i.test(message)) kind = 'period_already_closed'
|
||||
else if (/has not yet ended/i.test(message)) kind = 'period_not_ended'
|
||||
else if (/closing entry already exists/i.test(message)) kind = 'closing_entry_exists'
|
||||
else if (/continuity check failed/i.test(message)) kind = 'opening_balance_continuity'
|
||||
else if (/Fiscal period not found/i.test(message)) kind = 'period_not_found'
|
||||
if (/draft journal entries|utkast måste bokföras/i.test(message)) kind = 'draft_entries'
|
||||
else if (/voucher gap|verifikationsnummerglapp/i.test(message)) kind = 'unexplained_voucher_gap'
|
||||
else if (/Sequence counter integrity|Nummerserien i serie/i.test(message)) kind = 'sequence_mismatch'
|
||||
else if (/Trial balance is not balanced|Råbalansen balanserar inte/i.test(message)) kind = 'trial_balance_unbalanced'
|
||||
else if (/already closed|redan stängd/i.test(message)) kind = 'period_already_closed'
|
||||
else if (/has not yet ended|slutdatumet har inte passerat/i.test(message)) kind = 'period_not_ended'
|
||||
else if (/closing entry already exists|Bokslutsverifikation finns redan/i.test(message)) kind = 'closing_entry_exists'
|
||||
else if (/continuity check failed|IB\/UB-kontinuiteten/i.test(message)) kind = 'opening_balance_continuity'
|
||||
else if (/Fiscal period not found|Räkenskapsperioden hittades inte/i.test(message)) kind = 'period_not_found'
|
||||
return { kind, severity: 'high' as const, message }
|
||||
})
|
||||
|
||||
|
||||
@@ -22,8 +22,23 @@ const DEFAULT_OAUTH_BASE_URL = 'https://peroauth2.test.skatteverket.se/oauth2/v1
|
||||
// description PDF, Tjänstebeskrivning Arbetsgivardeklaration inlämning v1.7,
|
||||
// section 4.1.2.2: the 403 "Felaktigt access scope" example shows
|
||||
// `"description": "The required scope agd has been requested for that access token."`
|
||||
// The other tokens match the path segments of their respective APIs.
|
||||
const DEFAULT_SCOPES = 'momsdeklaration inkforetag skahmst skattekonto agd'
|
||||
// The other tokens match the path segments of their respective APIs,
|
||||
// EXCEPT skattekonto. The scope names there, learned the hard way:
|
||||
// - `ska` = the interactive skattekonto REST API (saldo +
|
||||
// transaktioner). Requested since the extension's first
|
||||
// commit; removed 2026-05-10 by a "remove unused scopes"
|
||||
// cleanup (#431 series), which instantly broke skattekonto
|
||||
// sync for every token issued after that hour: the API
|
||||
// answers 403 "The required scopes are not authorized"
|
||||
// without it. Re-added 2026-07-20. Do not "clean up" again.
|
||||
// - `skahmst` = a DIFFERENT bulk service (Skattekonto Hämta huvudmäns
|
||||
// saldo och transaktioner, file via E-transport for
|
||||
// juridiska läsombud; see dev_docs/skatteverket/skahmst).
|
||||
// Not what the sync uses, but harmless to request.
|
||||
// - `skattekonto` is NOT a real SKV scope name: SKV silently drops it
|
||||
// from every grant. Kept only so a future SKV rename in
|
||||
// our favor costs nothing.
|
||||
const DEFAULT_SCOPES = 'momsdeklaration inkforetag skahmst skattekonto ska agd'
|
||||
|
||||
function getOAuthBaseUrl(): string {
|
||||
return process.env.SKATTEVERKET_OAUTH_BASE_URL || DEFAULT_OAUTH_BASE_URL
|
||||
|
||||
Reference in New Issue
Block a user