feat: add safe owner-only migration reset (#1682)

* feat: add safe company migration reset

* fix: harden company reset eligibility

* fix: close company reset compliance gaps

* test: fix migration reset pg-real probes

* fix: preserve migration archive access

* docs: explain migration numbering continuity

* fix: block reset with VAT workflow state

* fix: block externally staged reset data

* fix: address migration reset review findings

* fix: clear stale migration archive estimate

* fix: retry migration archive estimates
This commit is contained in:
Mattsson
2026-08-19 12:04:24 +02:00
committed by GitHub
parent b07a4a4bca
commit 3a1b842e4a
24 changed files with 5017 additions and 57 deletions
@@ -0,0 +1,118 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mockSkvRequest = vi.fn()
const mockWriteSkatteverketAudit = vi.fn()
vi.mock('../lib/api-client', async (importOriginal) => {
const actual = await importOriginal<typeof import('../lib/api-client')>()
return {
...actual,
skvRequest: (...args: unknown[]) => mockSkvRequest(...args),
}
})
vi.mock('../lib/audit', () => ({
writeSkatteverketAudit: (...args: unknown[]) => mockWriteSkatteverketAudit(...args),
}))
vi.mock('@/lib/entitlements/has-capability', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/lib/entitlements/has-capability')>()
return {
...actual,
requireCapability: vi.fn().mockResolvedValue(null),
}
})
import type { ExtensionContext } from '@/lib/extensions/types'
import { skatteverketExtension } from '../index'
function makeContext(): ExtensionContext {
return {
userId: 'user-1',
companyId: 'company-1',
extensionId: 'skatteverket',
requestId: 'req-lock-audit',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
supabase: {} as any,
emit: vi.fn().mockResolvedValue(undefined),
log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
settings: {
get: vi.fn().mockResolvedValue(null),
set: vi.fn().mockResolvedValue(undefined),
clear: vi.fn().mockResolvedValue(undefined),
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any
}
function lockRoute() {
const route = skatteverketExtension.apiRoutes?.find(
(candidate) => candidate.method === 'PUT' && candidate.path === '/declaration/lock',
)
expect(route).toBeDefined()
return route!
}
describe('direct VAT declaration lock audit', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('records a successful lock before persisting the signing state', async () => {
mockSkvRequest.mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ signeringsLank: 'https://skv.test/sign/vat' }),
})
const ctx = makeContext()
const response = await lockRoute().handler(
new Request(
'https://test.local/api/extensions/ext/skatteverket/declaration/lock?redovisare=165560000000&redovisningsperiod=202606',
{ method: 'PUT' },
),
ctx,
)
expect(response.status).toBe(200)
expect(mockWriteSkatteverketAudit).toHaveBeenCalledWith(ctx, {
endpoint: 'declaration/lock',
agRegistreradId: '165560000000',
redovisningsperiod: '202606',
outcome: 'ok',
responseStatus: 200,
})
expect(ctx.settings.set).toHaveBeenCalledWith(
'submission_202606',
expect.stringContaining('"status":"draft_locked"'),
)
expect(mockWriteSkatteverketAudit.mock.invocationCallOrder[0]).toBeLessThan(
vi.mocked(ctx.settings.set).mock.invocationCallOrder[0]!,
)
})
it('records a rejected lock response', async () => {
mockSkvRequest.mockResolvedValue({
ok: false,
status: 409,
text: async () => 'already locked',
})
const ctx = makeContext()
const response = await lockRoute().handler(
new Request(
'https://test.local/api/extensions/ext/skatteverket/declaration/lock?redovisare=165560000000&redovisningsperiod=202606',
{ method: 'PUT' },
),
ctx,
)
expect(response.status).toBe(409)
expect(mockWriteSkatteverketAudit).toHaveBeenCalledWith(ctx, {
endpoint: 'declaration/lock',
agRegistreradId: '165560000000',
redovisningsperiod: '202606',
outcome: 'skv_error',
responseStatus: 409,
})
expect(ctx.settings.set).not.toHaveBeenCalled()
})
})
+8
View File
@@ -973,6 +973,14 @@ export const skatteverketExtension: Extension = {
`/las/${redovisare}/${redovisningsperiod}`
)
await writeSkatteverketAudit(ctx, {
endpoint: 'declaration/lock',
agRegistreradId: redovisare,
redovisningsperiod,
outcome: response.ok ? 'ok' : 'skv_error',
responseStatus: response.status,
})
if (!response.ok) {
const text = await response.text()
return NextResponse.json(