Fiscal period and multi bank (#228)
* feat: add fiscal period backward chaining and entry date validation Support creating fiscal periods before the earliest existing period (backward chaining) for backfill scenarios, alongside the existing forward chaining. The engine now validates that entry dates fall within the selected fiscal period, with a Swedish error message. The journal entry form auto-selects the matching period and shows a warning with a CreatePeriodDialog when no period covers the entry date. * feat: support multi-bank-account for imports and reconciliation Plumb a configurable settlement account through the entire bank import pipeline — mapping engine, transaction entries, ingest, and reconciliation — so secondary bank accounts (e.g. 1931, 1932) work correctly instead of hardcoding 1930. Adds a get_unlinked_bank_lines RPC that generalizes the existing get_unlinked_1930_lines with a fallback for backwards compatibility. The bank file import UI now shows a bank account selector when multiple 19xx accounts exist. Also adds default_vat_code/sru_code to account creation and fixes uploadDocument argument order in enable-banking sync.
This commit is contained in:
+3
-1
@@ -494,7 +494,9 @@ export const CreateAccountSchema = z.object({
|
||||
account_type: AccountTypeSchema,
|
||||
normal_balance: NormalBalanceSchema,
|
||||
plan_type: z.enum(['k1', 'full_bas']).optional(),
|
||||
description: z.string().optional(),
|
||||
description: z.string().nullable().optional(),
|
||||
default_vat_code: z.string().nullable().optional(),
|
||||
sru_code: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
export const UpdateAccountSchema = z.object({
|
||||
|
||||
@@ -105,6 +105,20 @@ describe('createDraftEntry — cancelled status on line-insert failure', () => {
|
||||
|
||||
const supabase = {
|
||||
from: vi.fn().mockImplementation((table: string) => {
|
||||
if (table === 'fiscal_periods') {
|
||||
return {
|
||||
select: vi.fn().mockReturnValue({
|
||||
eq: vi.fn().mockReturnValue({
|
||||
eq: vi.fn().mockReturnValue({
|
||||
single: vi.fn().mockResolvedValue({
|
||||
data: { name: 'FY 2024', period_start: '2024-01-01', period_end: '2024-12-31' },
|
||||
error: null,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
}
|
||||
if (table === 'journal_entries') {
|
||||
return {
|
||||
insert: vi.fn().mockReturnValue({
|
||||
@@ -158,6 +172,177 @@ describe('createDraftEntry — cancelled status on line-insert failure', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('createDraftEntry — date/period cross-validation', () => {
|
||||
function buildSupabase(periodData: { name: string; period_start: string; period_end: string } | null) {
|
||||
return {
|
||||
from: vi.fn().mockImplementation((table: string) => {
|
||||
if (table === 'fiscal_periods') {
|
||||
return {
|
||||
select: vi.fn().mockReturnValue({
|
||||
eq: vi.fn().mockReturnValue({
|
||||
eq: vi.fn().mockReturnValue({
|
||||
single: vi.fn().mockResolvedValue({
|
||||
data: periodData,
|
||||
error: periodData ? null : { message: 'Not found' },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
}
|
||||
if (table === 'chart_of_accounts') {
|
||||
return {
|
||||
select: vi.fn().mockReturnValue({
|
||||
eq: vi.fn().mockReturnValue({
|
||||
in: vi.fn().mockResolvedValue({
|
||||
data: [{ account_number: '1930', id: 'acc-1' }, { account_number: '3001', id: 'acc-2' }],
|
||||
error: null,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
}
|
||||
if (table === 'journal_entries') {
|
||||
return {
|
||||
insert: vi.fn().mockReturnValue({
|
||||
select: vi.fn().mockReturnValue({
|
||||
single: vi.fn().mockResolvedValue({
|
||||
data: { id: 'entry-1', status: 'draft' },
|
||||
error: null,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
select: vi.fn().mockReturnValue({
|
||||
eq: vi.fn().mockReturnValue({
|
||||
single: vi.fn().mockResolvedValue({
|
||||
data: { id: 'entry-1', status: 'draft', lines: [] },
|
||||
error: null,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
}
|
||||
if (table === 'journal_entry_lines') {
|
||||
return {
|
||||
insert: vi.fn().mockResolvedValue({ error: null }),
|
||||
}
|
||||
}
|
||||
return createMockChain()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
const validLines = [
|
||||
{ account_number: '1930', debit_amount: 1000, credit_amount: 0 },
|
||||
{ account_number: '3001', debit_amount: 0, credit_amount: 1000 },
|
||||
]
|
||||
|
||||
it('rejects entry date before period start', async () => {
|
||||
const supabase = buildSupabase({
|
||||
name: 'FY 2025',
|
||||
period_start: '2025-01-01',
|
||||
period_end: '2025-12-31',
|
||||
})
|
||||
|
||||
await expect(
|
||||
createDraftEntry(supabase as never, 'company-1', 'user-1', {
|
||||
fiscal_period_id: 'period-1',
|
||||
entry_date: '2024-12-15',
|
||||
description: 'Test',
|
||||
source_type: 'manual',
|
||||
lines: validLines,
|
||||
})
|
||||
).rejects.toThrow('Entry date 2024-12-15 is outside fiscal period "FY 2025"')
|
||||
})
|
||||
|
||||
it('rejects entry date after period end', async () => {
|
||||
const supabase = buildSupabase({
|
||||
name: 'FY 2025',
|
||||
period_start: '2025-01-01',
|
||||
period_end: '2025-12-31',
|
||||
})
|
||||
|
||||
await expect(
|
||||
createDraftEntry(supabase as never, 'company-1', 'user-1', {
|
||||
fiscal_period_id: 'period-1',
|
||||
entry_date: '2026-01-15',
|
||||
description: 'Test',
|
||||
source_type: 'manual',
|
||||
lines: validLines,
|
||||
})
|
||||
).rejects.toThrow('Entry date 2026-01-15 is outside fiscal period "FY 2025"')
|
||||
})
|
||||
|
||||
it('accepts entry date within period', async () => {
|
||||
const supabase = buildSupabase({
|
||||
name: 'FY 2025',
|
||||
period_start: '2025-01-01',
|
||||
period_end: '2025-12-31',
|
||||
})
|
||||
|
||||
const result = await createDraftEntry(supabase as never, 'company-1', 'user-1', {
|
||||
fiscal_period_id: 'period-1',
|
||||
entry_date: '2025-06-15',
|
||||
description: 'Test',
|
||||
source_type: 'manual',
|
||||
lines: validLines,
|
||||
})
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result.id).toBe('entry-1')
|
||||
})
|
||||
|
||||
it('accepts entry date on period start boundary', async () => {
|
||||
const supabase = buildSupabase({
|
||||
name: 'FY 2025',
|
||||
period_start: '2025-01-01',
|
||||
period_end: '2025-12-31',
|
||||
})
|
||||
|
||||
const result = await createDraftEntry(supabase as never, 'company-1', 'user-1', {
|
||||
fiscal_period_id: 'period-1',
|
||||
entry_date: '2025-01-01',
|
||||
description: 'Test',
|
||||
source_type: 'manual',
|
||||
lines: validLines,
|
||||
})
|
||||
|
||||
expect(result).toBeDefined()
|
||||
})
|
||||
|
||||
it('accepts entry date on period end boundary', async () => {
|
||||
const supabase = buildSupabase({
|
||||
name: 'FY 2025',
|
||||
period_start: '2025-01-01',
|
||||
period_end: '2025-12-31',
|
||||
})
|
||||
|
||||
const result = await createDraftEntry(supabase as never, 'company-1', 'user-1', {
|
||||
fiscal_period_id: 'period-1',
|
||||
entry_date: '2025-12-31',
|
||||
description: 'Test',
|
||||
source_type: 'manual',
|
||||
lines: validLines,
|
||||
})
|
||||
|
||||
expect(result).toBeDefined()
|
||||
})
|
||||
|
||||
it('throws when fiscal period not found', async () => {
|
||||
const supabase = buildSupabase(null)
|
||||
|
||||
await expect(
|
||||
createDraftEntry(supabase as never, 'company-1', 'user-1', {
|
||||
fiscal_period_id: 'nonexistent',
|
||||
entry_date: '2025-06-15',
|
||||
description: 'Test',
|
||||
source_type: 'manual',
|
||||
lines: validLines,
|
||||
})
|
||||
).rejects.toThrow('Fiscal period not found')
|
||||
})
|
||||
})
|
||||
|
||||
describe('JournalEntryStatus type includes cancelled', () => {
|
||||
it('cancelled is a valid JournalEntryStatus value', () => {
|
||||
const status: JournalEntryStatus = 'cancelled'
|
||||
|
||||
@@ -153,6 +153,24 @@ export async function createDraftEntry(
|
||||
)
|
||||
}
|
||||
|
||||
// Validate that entry_date falls within the selected fiscal period
|
||||
const { data: period, error: periodError } = await supabase
|
||||
.from('fiscal_periods')
|
||||
.select('name, period_start, period_end')
|
||||
.eq('id', input.fiscal_period_id)
|
||||
.eq('company_id', companyId)
|
||||
.single()
|
||||
|
||||
if (periodError || !period) {
|
||||
throw new Error('Fiscal period not found')
|
||||
}
|
||||
|
||||
if (input.entry_date < period.period_start || input.entry_date > period.period_end) {
|
||||
throw new Error(
|
||||
`Entry date ${input.entry_date} is outside fiscal period "${period.name}" (${period.period_start} - ${period.period_end})`
|
||||
)
|
||||
}
|
||||
|
||||
// Resolve account IDs
|
||||
const accountIdMap = await resolveAccountIds(supabase, companyId, input.lines)
|
||||
|
||||
|
||||
@@ -50,8 +50,11 @@ export async function evaluateMappingRules(
|
||||
supabase: SupabaseClient,
|
||||
companyId: string,
|
||||
transaction: Transaction,
|
||||
entityType?: EntityType
|
||||
entityType?: EntityType,
|
||||
settlementAccount?: string
|
||||
): Promise<MappingResult> {
|
||||
const bankAccount = settlementAccount || '1930'
|
||||
|
||||
// Fetch all active rules (user-specific + system defaults), ordered by priority
|
||||
const { data: rules, error } = await supabase
|
||||
.from('mapping_rules')
|
||||
@@ -63,29 +66,29 @@ export async function evaluateMappingRules(
|
||||
if (error || !rules || rules.length === 0) {
|
||||
// Try counterparty templates before static template fallback
|
||||
const counterpartyResult = await evaluateCounterpartyTemplates(supabase, companyId, transaction, entityType)
|
||||
if (counterpartyResult) return counterpartyResult
|
||||
if (counterpartyResult) return applySettlementAccount(counterpartyResult, bankAccount)
|
||||
|
||||
const templateResult = evaluateTemplateRules(transaction, entityType)
|
||||
if (templateResult) return templateResult
|
||||
return getDefaultResult(transaction)
|
||||
if (templateResult) return applySettlementAccount(templateResult, bankAccount)
|
||||
return getDefaultResult(transaction, bankAccount)
|
||||
}
|
||||
|
||||
// Evaluate each rule in priority order
|
||||
for (const rule of rules as MappingRule[]) {
|
||||
if (matchesRule(rule, transaction)) {
|
||||
return buildResult(rule, transaction, entityType)
|
||||
return applySettlementAccount(buildResult(rule, transaction, entityType), bankAccount)
|
||||
}
|
||||
}
|
||||
|
||||
// Try counterparty templates before static template fallback
|
||||
const counterpartyResult = await evaluateCounterpartyTemplates(supabase, companyId, transaction, entityType)
|
||||
if (counterpartyResult) return counterpartyResult
|
||||
if (counterpartyResult) return applySettlementAccount(counterpartyResult, bankAccount)
|
||||
|
||||
// Try template-based matching before default fallback
|
||||
const templateResult = evaluateTemplateRules(transaction, entityType)
|
||||
if (templateResult) return templateResult
|
||||
if (templateResult) return applySettlementAccount(templateResult, bankAccount)
|
||||
|
||||
return getDefaultResult(transaction)
|
||||
return getDefaultResult(transaction, bankAccount)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -261,13 +264,13 @@ function buildResult(rule: MappingRule, transaction: Transaction, entityType?: E
|
||||
/**
|
||||
* Default result when no rule matches (uncategorized)
|
||||
*/
|
||||
function getDefaultResult(transaction: Transaction): MappingResult {
|
||||
function getDefaultResult(transaction: Transaction, bankAccount = '1930'): MappingResult {
|
||||
const isExpense = transaction.amount < 0
|
||||
|
||||
return {
|
||||
rule: null,
|
||||
debit_account: isExpense ? '6991' : '1930',
|
||||
credit_account: isExpense ? '1930' : '3900',
|
||||
debit_account: isExpense ? '6991' : bankAccount,
|
||||
credit_account: isExpense ? bankAccount : '3900',
|
||||
risk_level: 'MEDIUM',
|
||||
confidence: 0.1,
|
||||
requires_review: true,
|
||||
@@ -277,6 +280,20 @@ function getDefaultResult(transaction: Transaction): MappingResult {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace any default 1930 references in a mapping result with the actual settlement account.
|
||||
* This allows mapping rules and templates that don't explicitly set a bank account
|
||||
* to work correctly with secondary bank accounts (e.g. 1931).
|
||||
*/
|
||||
function applySettlementAccount(result: MappingResult, bankAccount: string): MappingResult {
|
||||
if (bankAccount === '1930') return result
|
||||
return {
|
||||
...result,
|
||||
debit_account: result.debit_account === '1930' ? bankAccount : result.debit_account,
|
||||
credit_account: result.credit_account === '1930' ? bankAccount : result.credit_account,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a user-level mapping rule learned from categorization.
|
||||
*
|
||||
|
||||
@@ -110,7 +110,7 @@ export async function createTransactionJournalEntry(
|
||||
debit_amount: 0,
|
||||
credit_amount: absAmount,
|
||||
line_description: transaction.description,
|
||||
...(settlementAccount === '1930' ? currencyMeta : {}),
|
||||
...(isForeign ? currencyMeta : {}),
|
||||
})
|
||||
} else {
|
||||
// Debit bank for full amount
|
||||
@@ -119,7 +119,7 @@ export async function createTransactionJournalEntry(
|
||||
debit_amount: absAmount,
|
||||
credit_amount: 0,
|
||||
line_description: transaction.description,
|
||||
...(settlementAccount === '1930' ? currencyMeta : {}),
|
||||
...(isForeign ? currencyMeta : {}),
|
||||
})
|
||||
// All non-settlement lines
|
||||
for (const line of mappingResult.vat_lines) {
|
||||
@@ -176,7 +176,7 @@ export async function createTransactionJournalEntry(
|
||||
debit_amount: 0,
|
||||
credit_amount: absAmount,
|
||||
line_description: transaction.description,
|
||||
...(creditAccount === '1930' ? currencyMeta : {}),
|
||||
...(isForeign ? currencyMeta : {}),
|
||||
})
|
||||
} else {
|
||||
// Income (legacy single debit/credit path)
|
||||
@@ -196,7 +196,7 @@ export async function createTransactionJournalEntry(
|
||||
debit_amount: absAmount,
|
||||
credit_amount: 0,
|
||||
line_description: transaction.description,
|
||||
...(debitAccount === '1930' ? currencyMeta : {}),
|
||||
...(isForeign ? currencyMeta : {}),
|
||||
})
|
||||
// Credit revenue for net amount
|
||||
lines.push({
|
||||
@@ -222,7 +222,7 @@ export async function createTransactionJournalEntry(
|
||||
debit_amount: absAmount,
|
||||
credit_amount: 0,
|
||||
line_description: transaction.description,
|
||||
...(debitAccount === '1930' ? currencyMeta : {}),
|
||||
...(isForeign ? currencyMeta : {}),
|
||||
},
|
||||
{
|
||||
account_number: creditAccount,
|
||||
@@ -253,7 +253,8 @@ export function buildDomesticExpenseLines(
|
||||
amount: number,
|
||||
expenseAccount: string,
|
||||
description: string,
|
||||
vatRate: number = 0.25
|
||||
vatRate: number = 0.25,
|
||||
bankAccount: string = '1930'
|
||||
): CreateJournalEntryLineInput[] {
|
||||
const absAmount = Math.abs(amount)
|
||||
const lines: CreateJournalEntryLineInput[] = []
|
||||
@@ -276,7 +277,7 @@ export function buildDomesticExpenseLines(
|
||||
line_description: `Ingående moms ${vatRate * 100}%`,
|
||||
},
|
||||
{
|
||||
account_number: '1930', // Företagskonto
|
||||
account_number: bankAccount,
|
||||
debit_amount: 0,
|
||||
credit_amount: absAmount,
|
||||
line_description: description,
|
||||
@@ -291,7 +292,7 @@ export function buildDomesticExpenseLines(
|
||||
line_description: description,
|
||||
},
|
||||
{
|
||||
account_number: '1930',
|
||||
account_number: bankAccount,
|
||||
debit_amount: 0,
|
||||
credit_amount: absAmount,
|
||||
line_description: description,
|
||||
|
||||
@@ -206,6 +206,95 @@ export async function createNextPeriod(
|
||||
return newPeriod as FiscalPeriod
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a previous fiscal period before the given one.
|
||||
* Computes a 12-month period ending the day before the given period starts.
|
||||
* Updates previous_period_id chain so the given period points to the new one.
|
||||
*/
|
||||
export async function createPreviousPeriod(
|
||||
supabase: SupabaseClient,
|
||||
companyId: string,
|
||||
userId: string,
|
||||
currentPeriodId: string
|
||||
): Promise<FiscalPeriod> {
|
||||
|
||||
const { data: current, error: fetchError } = await supabase
|
||||
.from('fiscal_periods')
|
||||
.select('*')
|
||||
.eq('id', currentPeriodId)
|
||||
.eq('company_id', companyId)
|
||||
.single()
|
||||
|
||||
if (fetchError || !current) {
|
||||
throw new Error('Current fiscal period not found')
|
||||
}
|
||||
|
||||
// Compute previous period end (day before current start)
|
||||
const prevEnd = new Date(current.period_start + 'T12:00:00Z')
|
||||
prevEnd.setUTCDate(prevEnd.getUTCDate() - 1)
|
||||
|
||||
// Compute previous period start (1st of month, 12 months before prevEnd)
|
||||
const prevStart = new Date(prevEnd)
|
||||
prevStart.setUTCMonth(prevStart.getUTCMonth() - 11)
|
||||
prevStart.setUTCDate(1)
|
||||
|
||||
const prevStartStr = prevStart.toISOString().split('T')[0]
|
||||
const prevEndStr = prevEnd.toISOString().split('T')[0]
|
||||
|
||||
// Validate period duration
|
||||
const durationError = validatePeriodDuration(prevStartStr, prevEndStr, { isFirstPeriod: false })
|
||||
if (durationError) {
|
||||
throw new Error(durationError)
|
||||
}
|
||||
|
||||
// Check for overlapping periods
|
||||
const { data: overlapping } = await supabase
|
||||
.from('fiscal_periods')
|
||||
.select('id')
|
||||
.eq('company_id', companyId)
|
||||
.lte('period_start', prevEndStr)
|
||||
.gte('period_end', prevStartStr)
|
||||
.limit(1)
|
||||
|
||||
if (overlapping && overlapping.length > 0) {
|
||||
throw new Error('Previous fiscal period already exists or overlaps with an existing period')
|
||||
}
|
||||
|
||||
// Generate name
|
||||
const startYear = prevStart.getFullYear()
|
||||
const endYear = prevEnd.getFullYear()
|
||||
const name = startYear === endYear ? `FY ${startYear}` : `FY ${startYear}/${endYear}`
|
||||
|
||||
const { data: newPeriod, error: insertError } = await supabase
|
||||
.from('fiscal_periods')
|
||||
.insert({
|
||||
company_id: companyId,
|
||||
user_id: userId,
|
||||
name,
|
||||
period_start: prevStartStr,
|
||||
period_end: prevEndStr,
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (insertError || !newPeriod) {
|
||||
throw new Error(`Failed to create previous period: ${insertError?.message}`)
|
||||
}
|
||||
|
||||
// Update the current period to point to the new one
|
||||
const { error: updateError } = await supabase
|
||||
.from('fiscal_periods')
|
||||
.update({ previous_period_id: newPeriod.id })
|
||||
.eq('id', currentPeriodId)
|
||||
.eq('company_id', companyId)
|
||||
|
||||
if (updateError) {
|
||||
throw new Error(`Failed to update period chain: ${updateError.message}`)
|
||||
}
|
||||
|
||||
return newPeriod as FiscalPeriod
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status summary for a fiscal period.
|
||||
*/
|
||||
|
||||
@@ -80,6 +80,10 @@ const ERROR_PATTERN_MAP: [RegExp, string | null][] = [
|
||||
/Cannot attach documents to entries in a locked/i,
|
||||
'Kan inte bifoga dokument till verifikationer i en låst period.',
|
||||
],
|
||||
[
|
||||
/Entry date .+ is outside fiscal period/i,
|
||||
'Datumet ligger utanför det valda räkenskapsåret.',
|
||||
],
|
||||
]
|
||||
|
||||
/**
|
||||
|
||||
@@ -412,7 +412,7 @@ describe('manualLink', () => {
|
||||
const result = await manualLink(supabase as never, 'company-1', 'tx-1', 'je-1')
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toBe('Journal entry has no line on account 1930')
|
||||
expect(result.error).toBe('Verifikationen saknar rad på bankkonto (19xx)')
|
||||
})
|
||||
|
||||
it('succeeds when all validations pass', async () => {
|
||||
|
||||
@@ -206,13 +206,14 @@ export async function runReconciliation(
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Compare bank transaction totals vs GL 1930 balance.
|
||||
* Compare bank transaction totals vs GL bank account balance.
|
||||
*/
|
||||
export async function getReconciliationStatus(
|
||||
supabase: SupabaseClient,
|
||||
companyId: string,
|
||||
dateFrom?: string,
|
||||
dateTo?: string
|
||||
dateTo?: string,
|
||||
bankAccount = '1930'
|
||||
): Promise<ReconciliationStatus> {
|
||||
// Get all transactions in range
|
||||
let txQuery = supabase
|
||||
@@ -226,11 +227,11 @@ export async function getReconciliationStatus(
|
||||
|
||||
const { data: transactions } = await txQuery
|
||||
|
||||
// Get GL 1930 lines (all, not just unlinked)
|
||||
// Get GL bank account lines (all, not just unlinked)
|
||||
let glQuery = supabase
|
||||
.from('journal_entry_lines')
|
||||
.select('debit_amount, credit_amount, journal_entries!inner(company_id, entry_date, status)')
|
||||
.eq('account_number', '1930')
|
||||
.eq('account_number', bankAccount)
|
||||
.eq('journal_entries.company_id', companyId)
|
||||
.eq('journal_entries.status', 'posted')
|
||||
|
||||
@@ -280,7 +281,7 @@ export async function getReconciliationStatus(
|
||||
|
||||
/**
|
||||
* Manually link a transaction to an existing journal entry.
|
||||
* Validates that the journal entry has a 1930 line and amounts are directionally compatible.
|
||||
* Validates that the journal entry has a bank account line and amounts are directionally compatible.
|
||||
*/
|
||||
export async function manualLink(
|
||||
supabase: SupabaseClient,
|
||||
@@ -321,15 +322,16 @@ export async function manualLink(
|
||||
return { success: false, error: 'Journal entry is not posted' }
|
||||
}
|
||||
|
||||
// Check for 1930 line
|
||||
// Check for a bank account line (19xx class accounts)
|
||||
const { data: lines } = await supabase
|
||||
.from('journal_entry_lines')
|
||||
.select('debit_amount, credit_amount')
|
||||
.select('debit_amount, credit_amount, account_number')
|
||||
.eq('journal_entry_id', journalEntryId)
|
||||
.eq('account_number', '1930')
|
||||
.gte('account_number', '1900')
|
||||
.lte('account_number', '1999')
|
||||
|
||||
if (!lines || lines.length === 0) {
|
||||
return { success: false, error: 'Journal entry has no line on account 1930' }
|
||||
return { success: false, error: 'Verifikationen saknar rad på bankkonto (19xx)' }
|
||||
}
|
||||
|
||||
// Check that no other transaction is already linked to this entry
|
||||
@@ -434,19 +436,32 @@ export async function unlinkReconciliation(
|
||||
// Helpers
|
||||
// ============================================================
|
||||
|
||||
/** Fetch unlinked 1930 GL lines via the RPC function */
|
||||
/** Fetch unlinked bank GL lines via the RPC function */
|
||||
export async function fetchUnlinkedGLLines(
|
||||
supabase: SupabaseClient,
|
||||
companyId: string,
|
||||
dateFrom?: string,
|
||||
dateTo?: string
|
||||
dateTo?: string,
|
||||
bankAccount = '1930'
|
||||
): Promise<UnlinkedGLLine[]> {
|
||||
const { data, error } = await supabase.rpc('get_unlinked_1930_lines', {
|
||||
const { data, error } = await supabase.rpc('get_unlinked_bank_lines', {
|
||||
p_company_id: companyId,
|
||||
p_date_from: dateFrom || null,
|
||||
p_date_to: dateTo || null,
|
||||
p_account_number: bankAccount,
|
||||
})
|
||||
|
||||
// Fall back to legacy RPC if the new one doesn't exist yet
|
||||
if (error && bankAccount === '1930') {
|
||||
const { data: fallbackData, error: fallbackError } = await supabase.rpc('get_unlinked_1930_lines', {
|
||||
p_company_id: companyId,
|
||||
p_date_from: dateFrom || null,
|
||||
p_date_to: dateTo || null,
|
||||
})
|
||||
if (fallbackError || !fallbackData) return []
|
||||
return fallbackData as UnlinkedGLLine[]
|
||||
}
|
||||
|
||||
if (error || !data) return []
|
||||
return data as UnlinkedGLLine[]
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ export async function ingestTransactions(
|
||||
// Pre-fetch unlinked GL lines for reconciliation (non-critical)
|
||||
let glLinePool: UnlinkedGLLine[] = []
|
||||
try {
|
||||
glLinePool = await fetchUnlinkedGLLines(supabase, companyId)
|
||||
glLinePool = await fetchUnlinkedGLLines(supabase, companyId, undefined, undefined, options?.settlementAccount)
|
||||
} catch {
|
||||
// Non-critical — reconciliation will be skipped
|
||||
}
|
||||
@@ -365,7 +365,9 @@ export async function ingestTransactions(
|
||||
const mappingResult = await evaluateMappingRules(
|
||||
supabase,
|
||||
companyId,
|
||||
newTransaction as Transaction
|
||||
newTransaction as Transaction,
|
||||
undefined,
|
||||
options?.settlementAccount
|
||||
)
|
||||
|
||||
if (mappingResult.confidence >= 0.8 && !mappingResult.requires_review) {
|
||||
|
||||
Reference in New Issue
Block a user