Bug/skv konto numbers (#498)

* feat(skattekonto): add overdue transactions handling and split logic

* feat: enhance transaction handling and loading states

- Update BalanceHero component to display last synced date and additional information about Skatteverket updates.
- Refactor BookDirectlyDialog to simplify transaction linking logic and improve UI for transaction selection.
- Revamp InvoiceInboxWorkspace layout for better responsiveness and user experience, including improved skeleton loading states.
- Introduce new loading states for ExtensionWorkspace to match the live layout and improve user feedback during data fetching.
- Implement exchange rate fetching in QuickReviewDialog, ensuring transactions are always processed in SEK with error handling for rate fetching.
- Add structured error handling for unavailable exchange rates in the transaction API.

* feat(skattekonto): add 'Skattekonto – saldo & transaktioner' scope and update authorization checks

* feat: implement reverse charge handling in supplier invoice calculations and UI
This commit is contained in:
Mattsson
2026-05-15 16:25:05 +02:00
committed by GitHub
parent 3912c74a7b
commit 7175daee87
16 changed files with 780 additions and 623 deletions
+5 -1
View File
@@ -132,7 +132,11 @@ export const POST = withRouteContext(
const subtotal = items.reduce((sum, i) => sum + i.line_total, 0)
const vatAmount = items.reduce((sum, i) => sum + i.vat_amount, 0)
const total = Math.round((subtotal + vatAmount) * 100) / 100
// Reverse charge: supplier never invoices VAT, so the payable total equals
// the net. VAT is still tracked separately (vat_amount) for declarations
// and books fiktiv 2614/2645 in the engine, but neither side moves cash.
const payableVat = body.reverse_charge ? 0 : vatAmount
const total = Math.round((subtotal + payableVat) * 100) / 100
// Representation (BAS 6070–6079): ingående moms is only deductible up to
// 300 SEK base/person per ML 8 kap. 1 §, and the income-tax deduction was
@@ -0,0 +1,64 @@
import { NextResponse } from 'next/server'
import { withRouteContext } from '@/lib/api/with-route-context'
import { errorResponse, errorResponseFromCode } from '@/lib/errors/get-structured-error'
import { fetchExchangeRate } from '@/lib/currency/riksbanken'
import type { Currency, Transaction } from '@/types'
export const POST = withRouteContext(
'transaction.refreshExchangeRate',
async (_request, ctx, { params }: { params: Promise<{ id: string }> }) => {
const { id } = await params
const { supabase, companyId, log, requestId } = ctx
const { data: transaction, error: fetchError } = await supabase
.from('transactions')
.select('*')
.eq('id', id)
.eq('company_id', companyId)
.single<Transaction>()
if (fetchError || !transaction) {
return errorResponseFromCode('TX_CATEGORIZE_TX_NOT_FOUND', log, { requestId })
}
// No-op for SEK transactions, or when the rate is already cached.
if (
transaction.currency === 'SEK' ||
(transaction.amount_sek != null && transaction.exchange_rate != null)
) {
return NextResponse.json({ data: transaction })
}
const rate = await fetchExchangeRate(transaction.currency as Currency, new Date(transaction.date))
if (!rate) {
return errorResponseFromCode('TX_EXCHANGE_RATE_UNAVAILABLE', log, {
requestId,
details: { currency: transaction.currency, date: transaction.date },
})
}
const amountSek = Math.round(transaction.amount * rate.rate * 100) / 100
const { data: updated, error: updateError } = await supabase
.from('transactions')
.update({
amount_sek: amountSek,
exchange_rate: rate.rate,
exchange_rate_date: rate.date,
})
.eq('id', id)
.eq('company_id', companyId)
.is('journal_entry_id', null)
.select('*')
.single<Transaction>()
if (updateError || !updated) {
return errorResponse(updateError ?? new Error('Failed to persist exchange rate'), log, {
requestId,
})
}
return NextResponse.json({ data: updated })
},
{ requireWrite: true },
)