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
@@ -86,18 +86,37 @@ export default function YearEndPage() {
|
||||
return
|
||||
}
|
||||
const { data } = (await res.json()) as { data: FiscalPeriod[] }
|
||||
const all = data ?? []
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const eligible = (data ?? []).filter(
|
||||
const eligible = all.filter(
|
||||
(p) => !p.is_closed && !p.closing_entry_id && p.period_end <= today,
|
||||
)
|
||||
// Oldest first: accountants close in order.
|
||||
eligible.sort((a, b) => a.period_start.localeCompare(b.period_start))
|
||||
if (cancelled) return
|
||||
setPeriods(eligible)
|
||||
setHasAnyPeriods((data ?? []).length > 0)
|
||||
if (!selectedPeriodId && eligible.length > 0) {
|
||||
setHasAnyPeriods(all.length > 0)
|
||||
|
||||
// The URL ?period= param may point anywhere: at an ineligible period
|
||||
// (e.g. a year that has not ended) or, after a company switch, at a
|
||||
// period that does not exist in this company at all. An unknown id is
|
||||
// reset to the first eligible period; a known-but-ineligible one is
|
||||
// kept selectable in the dropdown so the user can navigate away from
|
||||
// it instead of being stuck (the readiness step explains why it
|
||||
// cannot be closed).
|
||||
let options = eligible
|
||||
if (selectedPeriodId) {
|
||||
const known = all.find((p) => p.id === selectedPeriodId)
|
||||
if (!known) {
|
||||
setSelectedPeriodId(eligible.length > 0 ? eligible[0].id : null)
|
||||
} else if (!eligible.some((p) => p.id === selectedPeriodId)) {
|
||||
options = [...eligible, known].sort((a, b) =>
|
||||
a.period_start.localeCompare(b.period_start),
|
||||
)
|
||||
}
|
||||
} else if (eligible.length > 0) {
|
||||
setSelectedPeriodId(eligible[0].id)
|
||||
}
|
||||
setPeriods(options)
|
||||
} catch {
|
||||
if (!cancelled) setPeriodsError('Kunde inte hämta perioder')
|
||||
}
|
||||
@@ -256,7 +275,7 @@ export default function YearEndPage() {
|
||||
)
|
||||
)}
|
||||
|
||||
{showWizard && periods && periods.length > 1 && step !== 'result' && (
|
||||
{showWizard && periods && periods.length > 0 && step !== 'result' && (
|
||||
<Card>
|
||||
<CardContent className="p-4 flex items-center gap-4">
|
||||
<label className="text-sm font-medium shrink-0">Period</label>
|
||||
@@ -349,6 +368,7 @@ export default function YearEndPage() {
|
||||
periodName={report.period.name}
|
||||
isRunning={executing}
|
||||
error={executeError}
|
||||
bolagsskattMissing={preview?.bolagsskattMissing ?? false}
|
||||
onBack={() => setStep('preview')}
|
||||
onExecute={executeYearEnd}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user