* feat: implement skattekonto drift detection and alerting - Add skattekonto drift computation logic to compare Skatteverket's saldo with GL 1630 sum. - Implement alerting mechanism for significant drift changes, with throttling to prevent alert spamming. - Introduce database functions to sum GL 1630 entries and list unbooked skattekonto rows. feat: create own account transfer detection - Develop logic to detect transfers between a company's own cash accounts based on counterparty IBAN. - Implement tests to validate detection logic under various scenarios, including matching and non-matching IBANs. feat: establish cash accounts as a first-class entity - Create cash_accounts table to manage routable cash accounts, replacing ad-hoc JSONB structures. - Implement functions for listing, upserting, and managing cash accounts, including primary account designation. feat: enhance GL line reconciliation functionality - Modify get_unlinked_1930_lines RPC to accept any account number for reconciliation, improving flexibility for different currencies. - Update related functions to ensure compatibility with the new cash_accounts structure. feat: capture counterparty IBAN in transactions - Add counterparty_iban column to transactions table to facilitate intra-account transfer detection. - Create index for efficient lookups based on counterparty IBAN. * feat: Enhance cash account handling and reconciliation processes - Updated reconciliation routes to enforce cash account validation for all account numbers, including '1930'. - Improved error handling for unknown cash accounts in reconciliation status and unmatched entries routes. - Changed CashAccountSelector to use sessionStorage instead of localStorage for better data privacy. - Fixed mapping for employer payroll taxes to route to the correct account (2730 instead of 2731). - Added safety checks for company IDs in the guessCounterAccount function to prevent injection vulnerabilities. - Introduced atomic RPC for setting primary cash accounts to avoid intermediate states during updates. - Seeded default cash accounts for new companies to ensure reconciliation routes are accessible from day one. - Updated email notifications for drift detection to avoid exposing sensitive financial data. - Enhanced bank reconciliation logic to handle multi-currency transactions correctly. - Renamed and updated tests to reflect changes in the underlying RPCs and ensure accurate coverage. - Migrated existing cash account rules to correct mappings in compliance with Swedish accounting standards.
79 lines
2.7 KiB
TypeScript
79 lines
2.7 KiB
TypeScript
import { createClient } from '@/lib/supabase/server'
|
|
import { NextResponse } from 'next/server'
|
|
import { ensureInitialized } from '@/lib/init'
|
|
import { runReconciliation } from '@/lib/reconciliation/bank-reconciliation'
|
|
import { validateBody } from '@/lib/api/validate'
|
|
import { RunReconciliationSchema } from '@/lib/api/schemas'
|
|
import { requireCompanyId } from '@/lib/company/context'
|
|
import { requireWritePermission } from '@/lib/auth/require-write'
|
|
|
|
ensureInitialized()
|
|
|
|
export async function POST(request: Request) {
|
|
const supabase = await createClient()
|
|
const { data: { user } } = await supabase.auth.getUser()
|
|
|
|
if (!user) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
|
|
const writeCheck = await requireWritePermission(supabase, user.id)
|
|
if (!writeCheck.ok) return writeCheck.response
|
|
|
|
const companyId = await requireCompanyId(supabase, user.id)
|
|
|
|
const validation = await validateBody(request, RunReconciliationSchema)
|
|
if (!validation.success) return validation.response
|
|
const { date_from, date_to, account_number, dry_run } = validation.data
|
|
|
|
const accountNumber = account_number ?? '1930'
|
|
|
|
// Defense-in-depth: only allow account numbers the company has registered as
|
|
// a cash account. Applies uniformly including '1930' — the cash_accounts
|
|
// backfill seeds 1930 for every company that had a SEK PSD2 account, and the
|
|
// AccountPickerDialog seeds it for new companies on first connection.
|
|
const { data: cashAccount } = await supabase
|
|
.from('cash_accounts')
|
|
.select('currency')
|
|
.eq('company_id', companyId)
|
|
.eq('ledger_account', accountNumber)
|
|
.maybeSingle()
|
|
|
|
if (!cashAccount) {
|
|
return NextResponse.json(
|
|
{ error: 'Okänt kassakonto för det här företaget' },
|
|
{ status: 400 },
|
|
)
|
|
}
|
|
const currency = (cashAccount.currency as string | undefined) ?? 'SEK'
|
|
|
|
const result = await runReconciliation(supabase, companyId, user.id, {
|
|
dateFrom: date_from,
|
|
dateTo: date_to,
|
|
accountNumber,
|
|
currency,
|
|
dryRun: dry_run ?? false,
|
|
})
|
|
|
|
return NextResponse.json({
|
|
data: {
|
|
matches: result.matches.map((m) => ({
|
|
transaction_id: m.transaction.id,
|
|
transaction_date: m.transaction.date,
|
|
transaction_description: m.transaction.description,
|
|
transaction_amount: m.transaction.amount,
|
|
journal_entry_id: m.glLine.journal_entry_id,
|
|
voucher_number: m.glLine.voucher_number,
|
|
voucher_series: m.glLine.voucher_series,
|
|
entry_date: m.glLine.entry_date,
|
|
entry_description: m.glLine.entry_description,
|
|
method: m.method,
|
|
confidence: m.confidence,
|
|
})),
|
|
applied: result.applied,
|
|
errors: result.errors,
|
|
dry_run: dry_run ?? false,
|
|
},
|
|
})
|
|
}
|