fix(skattekonto): remove the drift email, its event and the unused drift route (#2149)

The nightly skattekonto sync emailed "Skattekontot stämmer inte med
bokföringen" whenever Skatteverket's saldo differed from BAS 1630 by more
than 1 kr, every 24 hours while it lasted. On 2026-09-02 it fired on a
35 842 kr gap that the reconciliation explained to the last krona with 14
unbooked rows, while the Hem notice and the reconciliation page (both
gated on unexplained_difference) said nothing was wrong.

The check shipped in May 2026 (#525) before any in-app skattekonto view
existed; the dashboard tile its comments promise was never built and the
drift API route had no consumer. Since 2026-08-25 the reconciliation page
and the Hem notice are the surface, with one definition of "stämmer inte".

Removed: skattekonto-drift.ts, skattekonto-drift-email.ts, their tests,
the skattekonto.drift_detected event type, the handler registration, the
cron's drift hook, GET /api/extensions/skatteverket/skattekonto/drift, and
the ROPA activity for the mail. The route is dropped from the ungated
extension route allowlist to lock the ratchet. skattekonto_drift_tolerance
stays: the Hem notice reads it. Stale skattekonto_drift_last_alert_at rows
in extension_data are inert.

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-02 11:28:55 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5.1
parent 5b64df9c80
commit 6e8d76a9cb
12 changed files with 9 additions and 1048 deletions
@@ -1,34 +0,0 @@
import { NextResponse } from 'next/server'
import { ensureInitialized } from '@/lib/init'
import { withRouteContext } from '@/lib/api/with-route-context'
import { createExtensionContext } from '@/lib/extensions/context-factory'
import { computeSkattekontoDrift } from '@/extensions/general/skatteverket/lib/skattekonto-drift'
ensureInitialized()
/**
* GET /api/extensions/skatteverket/skattekonto/drift
*
* Returns the current SKV saldo vs GL 1630 drift snapshot for the active
* company. Backs the dashboard SkattekontoDriftTile. Returns null when no
* snapshot exists yet (fresh company, never synced).
*
* Access is recorded through the structured logger (Vercel logs; the
* observability sink only sees errors and no-ops until a provider is
* configured) because the response carries sensitive GL drift figures. Persisting every
* dashboard tile poll into event_log would be too noisy: the structured
* log line gives an auditable record without overrunning the 30-day event
* log retention (SOC 2 CC8.1, ISO 27001 A.8.15).
*/
export const GET = withRouteContext(
'skatteverket.skattekonto.drift',
async (_request, { supabase, user, companyId, log, requestId }) => {
const ctx = createExtensionContext(supabase, user.id, companyId, 'skatteverket', requestId)
const drift = await computeSkattekontoDrift(ctx)
log.info('skattekonto drift snapshot accessed', {
hasDrift: drift !== null,
})
return NextResponse.json({ data: drift })
},
)
@@ -7,8 +7,6 @@ const mocks = vi.hoisted(() => ({
getCompanyIdsWithCapability: vi.fn(),
createExtensionContext: vi.fn(),
syncSkattekonto: vi.fn(),
computeSkattekontoDrift: vi.fn(),
maybeAlertDrift: vi.fn(),
}))
vi.mock('@supabase/supabase-js', () => ({
@@ -34,11 +32,6 @@ vi.mock('@/extensions/general/skatteverket/lib/skattekonto-sync', () => ({
syncSkattekonto: (...args: unknown[]) => mocks.syncSkattekonto(...args),
}))
vi.mock('@/extensions/general/skatteverket/lib/skattekonto-drift', () => ({
computeSkattekontoDrift: (...args: unknown[]) => mocks.computeSkattekontoDrift(...args),
maybeAlertDrift: (...args: unknown[]) => mocks.maybeAlertDrift(...args),
}))
vi.mock('@/extensions/general/skatteverket/lib/api-client', () => {
class SkatteverketAuthError extends Error {
constructor(
@@ -120,7 +113,6 @@ describe('GET /api/extensions/skatteverket/skattekonto/sync/cron', () => {
(supabase: unknown, userId: string, companyId: string) => ({ supabase, userId, companyId }),
)
mocks.syncSkattekonto.mockResolvedValue({ booked: 0, upcoming: 0 })
mocks.computeSkattekontoDrift.mockResolvedValue(null)
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {})
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
@@ -7,7 +7,6 @@ import { orderByStalestSync } from '@/lib/skatteverket/sync-order'
import { CAPABILITY } from '@/lib/entitlements/keys'
import { createExtensionContext } from '@/lib/extensions/context-factory'
import { syncSkattekonto, SKATTEKONTO_LAST_SYNCED_AT_KEY } from '@/extensions/general/skatteverket/lib/skattekonto-sync'
import { computeSkattekontoDrift, maybeAlertDrift } from '@/extensions/general/skatteverket/lib/skattekonto-drift'
import { SkatteverketAuthError, type SkvAuth } from '@/extensions/general/skatteverket/lib/api-client'
import { SkatteverketSkattekontoError } from '@/extensions/general/skatteverket/lib/skattekonto-client'
import { markNeedsReconsent, RECONSENT_ERROR_CODES } from '@/extensions/general/skatteverket/lib/token-store'
@@ -239,19 +238,6 @@ export async function GET(request: Request) {
source === 'system' ? { mode: 'system' } : { mode: 'user', supabase, userId, companyId }
const syncResult = await syncSkattekonto(ctx, auth)
// Drift check: compare the fresh SKV saldo against GL 1630 sum. Emits
// `skattekonto.drift_detected` when |drift| > tolerance and not throttled.
try {
const drift = await computeSkattekontoDrift(ctx)
if (drift) await maybeAlertDrift(ctx, drift)
} catch (driftErr) {
console.error('[skattekonto-sync-cron] Drift check failed', {
userId,
companyId,
message: driftErr instanceof Error ? driftErr.message : String(driftErr),
})
}
results.push({
userId,
companyId,