* fix(invoices): apply configured voucher series to payments + preview next voucher The booking engine resolves the series from default_voucher_series_per_source_type, but the global "Standardserie" dropdown wrote a separate field the engine ignored, and cash-method invoice payments (invoice_cash_payment) weren't exposed in settings — so configured series were silently dropped to "A". - Expose cash/private payment source types in the per-source-type form - Write the global default through to the map on save, keeping overrides - Resolve voucher-sequences/next by source_type (+date) to match the engine - Show the upcoming voucher (V2) in the payment dialog title - Share resolveInvoicePaymentSourceType so preview and booking can't drift Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(salary): keep AGI panel in sync with Skatteverket signing state The AGI panel mixed run-scoped generation state (agi_generated_at, agi_declarations) with period-scoped submission state (extension_data agi_submission_{period}), so the two could drift and present contradictory UI. Reconcile them: - Auto-detect a Mina Sidor BankID signature: while awaiting_signing, poll /agi/kvittenser on mount and on tab refocus so the panel flips to "signed" (hiding the signing actions) without a manual "Hamta kvittens" click. - Warn instead of offering to sign when the locked granskningsunderlag predates the run's latest AGI generation (draftIsStale) — avoids filing superseded figures. - Self-heal a stale "AGI-XML saknas" error once the run's AGI is (re)generated out-of-band (MCP/API/other tab). - Refetch the salary run on tab focus so agi_generated_at reflects out-of-band generation without a hard reload. - /agi/lasUpp now clears the cached agi_submission_{period} record, so unlocking drops the panel back to the pre-submission state instead of stranding it on a released "redo att signeras" draft. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: Implement VAT registration handling and invoice item line types - Added VAT registration check in commitCreateInvoice to set VAT rate to 0% for non-VAT registered companies. - Updated invoice creation logic to reflect 'exempt' VAT treatment and adjusted related fields accordingly. - Introduced support for free-text and blank spacer rows in invoice items by adding a new line_type field. - Enhanced invoice and credit note handling to accommodate new line types. - Added new localized messages for text rows in English and Swedish. - Created tests for salary run approval logic, ensuring bank details are validated correctly. - Implemented effective net payout calculation for salary runs, considering tax overrides. - Added SQL migrations to support new invoice item line types and accounting method awareness for linking invoices to vouchers. * feat(articles): artikelregister with revenue account + VAT rate per article Article register (non-inventory) with per-article VAT rate and optional BAS class-3 revenue-account override. Includes API routes, UI pages, MCP tools, pending-operation staging, and the activate-or-create account flow (ACCOUNTS_NOT_IN_CHART -> ActivateAccountsDialog, unknown numbers -> AddAccountDialog) reusing the journal entry UX. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bookkeeping): no-doc-required batch + bulk-missing endpoints Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(payments): supplier payment lines + cash-method invoice matching Shared payment-line proposal for supplier invoices, improved match-invoice/match-supplier-invoice flows (kontantmetoden-aware), and voucher-link support without requiring a 151x clearing entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bookkeeping): new journal entry dialog, SIE import tweaks, misc New journal entry dialog component, journal list/page updates, invoice editor updates, SIE import adjustments, transaction ingest and api-key tweaks, pr-agent workflow update. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(invoices): implement tax reduction features and localization updates * feat(tests): add VAT registration gate to pending operations commit tests --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
102 lines
3.5 KiB
TypeScript
102 lines
3.5 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 { effectiveNetPayout } from '@/lib/salary/payment/effective-net'
|
|
|
|
ensureInitialized()
|
|
|
|
/** review → approved (authorization recorded, with pre-approve validation) */
|
|
export const POST = withRouteContext<{ params: Promise<{ id: string }> }>(
|
|
'salary.run.approve',
|
|
async (_request, { supabase, companyId, user }, { params }) => {
|
|
const { id } = await params
|
|
|
|
// Verify run exists and is in review status
|
|
const { data: run, error: runError } = await supabase
|
|
.from('salary_runs')
|
|
.select('*')
|
|
.eq('id', id)
|
|
.eq('company_id', companyId)
|
|
.eq('status', 'review')
|
|
.single()
|
|
|
|
if (runError || !run) {
|
|
return NextResponse.json({ error: 'Lönekörningen måste vara i granskningsstatus' }, { status: 400 })
|
|
}
|
|
|
|
// Load all employees in this run for validation
|
|
const { data: runEmployees } = await supabase
|
|
.from('salary_run_employees')
|
|
.select('*, employee:employees(first_name, last_name, clearing_number, bank_account_number, email)')
|
|
.eq('salary_run_id', id)
|
|
|
|
const validationErrors: string[] = []
|
|
const warnings: string[] = []
|
|
|
|
for (const sre of runEmployees || []) {
|
|
const emp = sre.employee as {
|
|
first_name: string
|
|
last_name: string
|
|
clearing_number: string | null
|
|
bank_account_number: string | null
|
|
email: string | null
|
|
} | null
|
|
if (!emp) continue
|
|
const name = `${emp.first_name} ${emp.last_name}`
|
|
|
|
// Bank details are only required when there's an actual payout. A zero
|
|
// net (nollkörning, or fully net-deducted) produces no payment-file line,
|
|
// so no destination account is needed — mirrors the pain.001 / BG-LB
|
|
// generators, which only include employees with effectiveNet > 0.
|
|
if (effectiveNetPayout(sre) > 0 && (!emp.clearing_number || !emp.bank_account_number)) {
|
|
validationErrors.push(`${name}: Bankuppgifter saknas (clearingnummer och/eller kontonummer)`)
|
|
}
|
|
|
|
// Must have been calculated (calculation_breakdown exists)
|
|
if (!sre.calculation_breakdown) {
|
|
validationErrors.push(`${name}: Beräkning saknas — kör beräkning först`)
|
|
}
|
|
|
|
// Warning: no email means pay slip cannot be sent
|
|
if (!emp.email) {
|
|
warnings.push(`${name}: E-post saknas — lönebesked kan inte skickas`)
|
|
}
|
|
}
|
|
|
|
if (validationErrors.length > 0) {
|
|
return NextResponse.json({
|
|
error: 'Valideringsfel — korrigera innan godkännande',
|
|
details: validationErrors,
|
|
warnings,
|
|
}, { status: 400 })
|
|
}
|
|
|
|
// All validation passed — approve
|
|
const { data: updatedRun, error } = await supabase
|
|
.from('salary_runs')
|
|
.update({
|
|
status: 'approved',
|
|
approved_by: user.id,
|
|
approved_at: new Date().toISOString(),
|
|
})
|
|
.eq('id', id)
|
|
.eq('company_id', companyId)
|
|
.eq('status', 'review')
|
|
.select()
|
|
.single()
|
|
|
|
if (error || !updatedRun) {
|
|
return NextResponse.json({ error: 'Kunde inte godkänna lönekörningen' }, { status: 500 })
|
|
}
|
|
|
|
await eventBus.emit({
|
|
type: 'salary_run.approved',
|
|
payload: { salaryRunId: id, approvedBy: user.id, userId: user.id, companyId },
|
|
})
|
|
|
|
return NextResponse.json({ data: updatedRun, warnings })
|
|
},
|
|
{ requireWrite: true },
|
|
)
|