Files
accounted/app/api/salary/runs/[id]/agi/submit/route.ts
T
Mattsson e11f70b347 Bug/gh issues fiz (#1103)
* refactor: optimize page loading and data fetching

* fix: resolve recurring production runtime errors

* feat: add MCP company and customer updates

* fix: handle year-end tax adjustments

* feat: harden annual report compliance

* fix: expand invoice logo and font support

* fix: sanitize API route error responses

* fix: sanitize user-facing error messages

* feat: persist onboarding and tax assessment notices

* fix: reduce cloud backup audit churn

* feat: refine invoice editor layout

* fix: show saved tax adjustments in INK2

* fix: complete annual report API mappings

* docs: record operational safeguards and decisions

* fix: harden annual report review findings

* fix: adjust column span for description based on VAT registration

* New css class name
2026-07-21 23:00:15 +02:00

148 lines
5.4 KiB
TypeScript

import { NextResponse } from 'next/server'
import { ensureInitialized } from '@/lib/init'
import { withRouteContext } from '@/lib/api/with-route-context'
import { eventBus } from '@/lib/events'
import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message'
ensureInitialized()
/**
* Submit AGI to Skatteverket via the extension API.
*
* This route orchestrates the AGI submission flow:
* 1. Validates the salary run is in a submittable state
* 2. Ensures AGI has been generated (in agi_declarations table)
* 3. Calls the Skatteverket extension to save draft + lock for signing
* 4. Returns the signeringslänk for BankID signing
*
* The user then signs on Skatteverket's site (Mina Sidor). The frontend
* polls /api/extensions/ext/skatteverket/agi/kvittenser to detect completion.
*/
export const POST = withRouteContext<{ params: Promise<{ id: string }> }>(
'salary.run.agi.submit',
async (request, ctx, { params }) => {
const { id } = await params
const { user, supabase, companyId, log } = ctx
// Load salary run
const { data: run, error: runError } = await supabase
.from('salary_runs')
.select('*')
.eq('id', id)
.eq('company_id', companyId)
.single()
if (runError || !run) {
return NextResponse.json({ error: 'Lönekörning hittades inte' }, { status: 404 })
}
if (!['review', 'approved', 'paid', 'booked'].includes(run.status)) {
return NextResponse.json(
{ error: 'AGI kan bara skickas till Skatteverket efter granskning' },
{ status: 400 }
)
}
// Ensure AGI has been generated
const { data: agiDeclaration } = await supabase
.from('agi_declarations')
.select('id, status')
.eq('company_id', companyId)
.eq('salary_run_id', id)
.single()
if (!agiDeclaration) {
return NextResponse.json(
{ error: 'AGI har inte genererats ännu. Generera AGI XML först.' },
{ status: 400 }
)
}
if (agiDeclaration.status === 'submitted' || agiDeclaration.status === 'accepted') {
return NextResponse.json(
{ error: 'AGI har redan skickats till Skatteverket för denna period' },
{ status: 409 }
)
}
// The actual SKV interaction lives in the Skatteverket extension. This
// route is a thin orchestrator: it forwards the salary_run_id to the
// extension's /agi/submit endpoint (which posts the stored XML underlag),
// then records that the AGI submission process has started.
//
// The frontend (AGIPanel) handles the rest of the flow:
// 1. POST /api/extensions/ext/skatteverket/agi/submit { salaryRunId }
// → returns { inlamningId }
// 2. GET /api/extensions/ext/skatteverket/agi/kontrollresultat?inlamningId=...
// → poll until status != PROCESSING
// 3. POST /api/extensions/ext/skatteverket/agi/spara { inlamningId }
// 4. POST /api/extensions/ext/skatteverket/agi/granskningsunderlag?arbetsgivare&period
// → returns { link } (Mina Sidor BankID signing)
// 5. GET /api/extensions/ext/skatteverket/agi/kvittenser?arbetsgivare&period
const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
try {
const submitResponse = await fetch(
`${appUrl}/api/extensions/ext/skatteverket/agi/submit`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Cookie': request.headers.get('Cookie') || '',
},
body: JSON.stringify({ salaryRunId: id }),
}
)
if (!submitResponse.ok) {
const errorData = await submitResponse.json().catch(() => ({ error: 'Okänt fel' }))
return NextResponse.json(
{ error: errorData.error || `Kunde inte skicka AGI-underlag (${submitResponse.status})` },
{ status: submitResponse.status }
)
}
const submitData = await submitResponse.json()
// Don't stamp salary_runs.agi_submitted_at here. The underlag has only
// been ingested; the user still has to pass kontrollresultat, save,
// produce a granskningsunderlag, and sign with BankID before the AGI is
// actually filed. Recording the submission time at ingest would make the
// audit trail lie about when filing completed.
//
// The real timestamp is set by the kvittenser handler in the extension
// (extensions/general/skatteverket/index.ts /agi/kvittenser route) when
// it observes a uuidKvittens for the period, mirroring SKV's signeradTid.
await eventBus.emit({
type: 'agi.submitted',
payload: {
salaryRunId: id,
periodYear: run.period_year,
periodMonth: run.period_month,
userId: user.id,
companyId,
},
})
return NextResponse.json({
data: {
...submitData.data,
salaryRunId: id,
periodYear: run.period_year,
periodMonth: run.period_month,
message: 'AGI-underlag inläst hos Skatteverket. Skapa granskningsunderlag och signera med BankID i Mina Sidor.',
},
})
} catch (err) {
log.error('[salary/agi/submit] Error', err as Error)
return NextResponse.json(
{ error: err instanceof Error ? getUserErrorMessage(err) : 'Kunde inte skicka AGI till Skatteverket' },
{ status: 500 }
)
}
},
{ requireWrite: true },
)