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:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user