fix(agi): update AGI submission logic to reflect SKV's auto-persistence and adjust error handling (#432)

fix(pdf): align company logo and document title in invoice PDF layout
This commit is contained in:
Mattsson
2026-05-10 15:37:19 +02:00
committed by GitHub
parent c40e1114ca
commit 1876b3b402
3 changed files with 37 additions and 25 deletions
+10 -17
View File
@@ -338,9 +338,12 @@ export function AGIPanel(props: AGIPanelProps) {
* status flips out of PROCESSING. Skatteverket's spec says polling is
* usually instantaneous, but we cap at 8 attempts × 1s to be safe.
*
* On DONE_SUCCESS we automatically call /agi/spara to commit into Eget
* utrymme, mirroring the user's intent ("send AGI") and matching what the
* old draft-then-lock UX promised.
* On DONE_SUCCESS the underlag is auto-persisted by SKV — no /spara call.
* Calling /spara when there are no errors returns 400 felkod 20
* ("Inlämningen är redan sparad/borttagen eller innehöll inga felaktiga
* underlag") because /spara is specifically for re-persisting rejected
* underlag so the user can fix them later in Mina Sidor. Successful
* underlag move straight to the granskningsunderlag step.
*
* On DONE_REJECTED we surface the validation findings; the user can still
* choose to save (so they can fix it in Mina Sidor) or abort.
@@ -391,20 +394,10 @@ export function AGIPanel(props: AGIPanelProps) {
setKontroller(findings)
if (kr.status === 'DONE_SUCCESS') {
const sparaRes = await fetch('/api/extensions/ext/skatteverket/agi/spara', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
// Include salaryRunId so the handler can promote the matching
// agi_declarations row to status='pending_signature' without
// doing a fallback lookup against locally-cached submission state.
body: JSON.stringify({ inlamningId, salaryRunId }),
})
const sparaJson = await sparaRes.json()
if (!sparaRes.ok || sparaJson.error) {
setError(sparaJson.error || `Kunde inte spara underlag (${sparaRes.status})`)
return
}
setSuccess('Underlag accepterat och sparat hos Skatteverket. Skapa granskningsunderlag för att fortsätta till BankID-signering.')
setSuccess(
'Underlag accepterat hos Skatteverket. Klicka "Skapa signeringslänk" ' +
'för att gå vidare till BankID-signering i Mina Sidor.',
)
} else if (kr.status === 'DONE_REJECTED') {
setError(`Underlaget innehåller ${findings.filter(f => f.status === 'STOPP').length} stoppande fel. Åtgärda och skicka igen.`)
} else {
+19
View File
@@ -1099,6 +1099,25 @@ export const skatteverketExtension: Extension = {
}),
)
// Flip the matching agi_declarations row to pending_signature when
// SKV returns a usable granskningsunderlag. Previously this happened
// in /agi/spara, but /spara is only valid for re-saving rejected
// underlag — successful underlag are auto-persisted by SKV, so
// /spara returns felkod 20 on the happy path. Doing the flip here
// ensures the audit trail still moves through pending_signature
// → submitted (BFNAR 2013:2 kap 8 / BFL 5 kap 6§) without /spara.
if (canSign) {
const periodYear = parseInt(period.slice(0, 4))
const periodMonth = parseInt(period.slice(4, 6))
await ctx.supabase
.from('agi_declarations')
.update({ status: 'pending_signature' })
.eq('company_id', ctx.companyId)
.eq('period_year', periodYear)
.eq('period_month', periodMonth)
.in('status', ['generated', 'rejected'])
}
return NextResponse.json({ data: result.data })
} catch (err) {
return handleSkvError(err)
+8 -8
View File
@@ -28,7 +28,7 @@ const styles = StyleSheet.create({
color: '#1a1a1a',
},
companyInfo: {
textAlign: 'right',
textAlign: 'left',
},
companyName: {
fontSize: 14,
@@ -373,15 +373,9 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
{/* Header */}
<View style={styles.header}>
<View>
<Text style={[styles.title, isCreditNote ? styles.creditNoteTitle : {}]}>
{getDocumentTitle(invoice)}
</Text>
<Text style={{ marginTop: 5, color: '#666' }}>{invoice.invoice_number ?? 'FÖRHANDSGRANSKNING'}</Text>
</View>
<View style={styles.companyInfo}>
{company.logo_url && (
<Image src={company.logo_url} style={{ maxHeight: 40, maxWidth: 150, marginBottom: 6, alignSelf: 'flex-end' }} />
<Image src={company.logo_url} style={{ maxHeight: 40, maxWidth: 150, marginBottom: 6, alignSelf: 'flex-start' }} />
)}
<Text style={styles.companyName}>{company.trade_name || company.company_name}</Text>
{company.trade_name && company.company_name && (
@@ -396,6 +390,12 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
)}
{company.vat_number && <Text>VAT: {company.vat_number}</Text>}
</View>
<View style={{ textAlign: 'right' }}>
<Text style={[styles.title, isCreditNote ? styles.creditNoteTitle : {}]}>
{getDocumentTitle(invoice)}
</Text>
<Text style={{ marginTop: 5, color: '#666' }}>{invoice.invoice_number ?? 'FÖRHANDSGRANSKNING'}</Text>
</View>
</View>
{/* Credit note reference */}