@@ -102,6 +102,25 @@ function run(
|
||||
// --- Tests ---
|
||||
|
||||
describe('syncMappedAccounts: create pass', () => {
|
||||
it('persists a reviewed VAT treatment on a created identity account', async () => {
|
||||
const { supabase, inserts } = buildCapturingSupabase()
|
||||
const result = await run(supabase, [
|
||||
mapping({
|
||||
sourceAccount: '4056',
|
||||
targetAccount: '4056',
|
||||
sourceName: 'Inköp varor 25% EU',
|
||||
defaultVatTreatment: 'reverse_charge_eu_goods',
|
||||
defaultVatRate: 0.25,
|
||||
vatTreatmentReviewed: true,
|
||||
}),
|
||||
])
|
||||
expect(result.error).toBeNull()
|
||||
expect(inserts[0]).toMatchObject({
|
||||
default_vat_treatment: 'reverse_charge_eu_goods',
|
||||
default_vat_rate: 0.25,
|
||||
})
|
||||
})
|
||||
|
||||
it('creates a missing BAS account with the BAS default name when the file has no custom name', async () => {
|
||||
const { supabase, inserts } = buildCapturingSupabase()
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
applyVatTreatmentReview,
|
||||
enrichAccountMappingsWithVat,
|
||||
} from '../account-vat-treatment'
|
||||
import type { AccountMapping } from '../types'
|
||||
|
||||
function mapping(account: string, name: string): AccountMapping {
|
||||
return {
|
||||
sourceAccount: account,
|
||||
sourceName: name,
|
||||
targetAccount: account,
|
||||
targetName: name,
|
||||
confidence: 1,
|
||||
matchType: 'exact',
|
||||
isOverride: false,
|
||||
}
|
||||
}
|
||||
|
||||
describe('enrichAccountMappingsWithVat', () => {
|
||||
it('marks label suggestions for user review', () => {
|
||||
const [result] = enrichAccountMappingsWithVat(
|
||||
[mapping('4056', 'Inköp varor 25% EU')],
|
||||
[],
|
||||
)
|
||||
expect(result).toMatchObject({
|
||||
defaultVatTreatment: 'reverse_charge_eu_goods',
|
||||
defaultVatRate: 0.25,
|
||||
vatTreatmentSuggested: true,
|
||||
vatTreatmentReviewed: false,
|
||||
requiresVatTreatmentReview: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('requires review for suggested class 5 and 6 purchase treatments', () => {
|
||||
const results = enrichAccountMappingsWithVat(
|
||||
[
|
||||
mapping('5010', 'Inköp tjänst EU'),
|
||||
mapping('6010', 'Inköp tjänst utanför EU'),
|
||||
],
|
||||
[],
|
||||
)
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
defaultVatTreatment: 'reverse_charge_eu_services',
|
||||
requiresVatTreatmentReview: true,
|
||||
vatTreatmentReviewed: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
defaultVatTreatment: 'export_services',
|
||||
requiresVatTreatmentReview: true,
|
||||
vatTreatmentReviewed: false,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps an existing account treatment without asking again', () => {
|
||||
const [result] = enrichAccountMappingsWithVat(
|
||||
[mapping('3041', 'Försäljning tjänst 25% sv')],
|
||||
[{
|
||||
account_number: '3041',
|
||||
default_vat_treatment: 'standard_25',
|
||||
default_vat_rate: 0.25,
|
||||
} as never],
|
||||
)
|
||||
expect(result).toMatchObject({
|
||||
defaultVatTreatment: 'standard_25',
|
||||
vatTreatmentSuggested: false,
|
||||
vatTreatmentReviewed: true,
|
||||
requiresVatTreatmentReview: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('applyVatTreatmentReview', () => {
|
||||
it('marks only the selected row reviewed, including class 5 and 6 accounts', () => {
|
||||
const mappings = [
|
||||
mapping('5010', 'Inköp tjänst EU'),
|
||||
mapping('3041', 'Försäljning tjänst 25% sv'),
|
||||
]
|
||||
const result = applyVatTreatmentReview(
|
||||
mappings,
|
||||
'5010',
|
||||
'reverse_charge_eu_services',
|
||||
0.25,
|
||||
)
|
||||
expect(result[0]).toMatchObject({
|
||||
defaultVatTreatment: 'reverse_charge_eu_services',
|
||||
defaultVatRate: 0.25,
|
||||
vatTreatmentReviewed: true,
|
||||
})
|
||||
expect(result[1].vatTreatmentReviewed).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -14,6 +14,7 @@ import { classifyAccount } from '@/lib/bookkeeping/account-classifier'
|
||||
import { computeSRUCode } from '@/lib/bookkeeping/bas-data/sru-mapping'
|
||||
import { fetchAllRows } from '@/lib/supabase/fetch-all'
|
||||
import type { AccountMapping } from './types'
|
||||
import { isAccountVatTreatment, type AccountVatTreatment } from '@/lib/vat/account-vat-treatment'
|
||||
|
||||
export interface AccountSyncResult {
|
||||
/** Accounts inserted into chart_of_accounts */
|
||||
@@ -43,6 +44,7 @@ function buildInsertRow(
|
||||
basRef: BASReferenceAccount | undefined,
|
||||
companyId: string,
|
||||
userId: string,
|
||||
vatDefaults?: { treatment: AccountVatTreatment | null; rate: number | null },
|
||||
) {
|
||||
const sortOrder = /^\d+$/.test(accountNumber) ? parseInt(accountNumber, 10) : null
|
||||
|
||||
@@ -63,6 +65,8 @@ function buildInsertRow(
|
||||
is_system_account: false,
|
||||
description: basRef.description,
|
||||
sort_order: sortOrder,
|
||||
default_vat_treatment: vatDefaults?.treatment ?? null,
|
||||
default_vat_rate: vatDefaults?.rate ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +88,8 @@ function buildInsertRow(
|
||||
is_system_account: false,
|
||||
description: accountName,
|
||||
sort_order: sortOrder,
|
||||
default_vat_treatment: vatDefaults?.treatment ?? null,
|
||||
default_vat_rate: vatDefaults?.rate ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +149,27 @@ export async function syncMappedAccounts(
|
||||
if (m.targetAccount && fallback) fallbackNames.set(m.targetAccount, fallback)
|
||||
}
|
||||
|
||||
const vatDefaults = new Map<string, { treatment: AccountVatTreatment | null; rate: number | null }>()
|
||||
for (const mapping of mappings) {
|
||||
if (
|
||||
!mapping.vatTreatmentReviewed ||
|
||||
!mapping.targetAccount ||
|
||||
mapping.sourceAccount !== mapping.targetAccount
|
||||
) continue
|
||||
if (
|
||||
mapping.defaultVatTreatment !== null &&
|
||||
mapping.defaultVatTreatment !== undefined &&
|
||||
!isAccountVatTreatment(mapping.defaultVatTreatment)
|
||||
) {
|
||||
result.error = `Invalid VAT treatment for account ${mapping.sourceAccount}`
|
||||
return result
|
||||
}
|
||||
vatDefaults.set(mapping.targetAccount, {
|
||||
treatment: mapping.defaultVatTreatment ?? null,
|
||||
rate: mapping.defaultVatRate ?? null,
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch the company's chart once (paged) and filter in JS: avoids a huge
|
||||
// .in() URL for full-chart imports and the silent 1000-row PostgREST cap.
|
||||
let existingByNumber: Map<string, string>
|
||||
@@ -181,7 +208,7 @@ export async function syncMappedAccounts(
|
||||
basRef?.account_name ??
|
||||
fallbackNames.get(num) ??
|
||||
`Konto ${num}`
|
||||
return buildInsertRow(num, name, basRef, companyId, userId)
|
||||
return buildInsertRow(num, name, basRef, companyId, userId, vatDefaults.get(num))
|
||||
})
|
||||
|
||||
const { error: insertError } = await supabase.from('chart_of_accounts').insert(inserts)
|
||||
@@ -195,6 +222,23 @@ export async function syncMappedAccounts(
|
||||
result.created = missing.length
|
||||
}
|
||||
|
||||
const existingVatUpdates = [...vatDefaults]
|
||||
.filter(([account]) => existingByNumber.has(account))
|
||||
for (const [account, defaults] of existingVatUpdates) {
|
||||
const { error: vatUpdateError } = await supabase
|
||||
.from('chart_of_accounts')
|
||||
.update({
|
||||
default_vat_treatment: defaults.treatment,
|
||||
default_vat_rate: defaults.rate,
|
||||
})
|
||||
.eq('company_id', companyId)
|
||||
.eq('account_number', account)
|
||||
if (vatUpdateError) {
|
||||
result.error = vatUpdateError.message
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Rename pass: carry the file's names into existing accounts. The diff set
|
||||
// is small (only names that actually changed), so the UPDATEs run
|
||||
// concurrently in bounded batches: a full-chart re-sync must not serialize
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { BASAccount } from '@/types'
|
||||
import {
|
||||
suggestVatTreatment,
|
||||
type AccountVatTreatment,
|
||||
} from '@/lib/vat/account-vat-treatment'
|
||||
import type { AccountMapping } from './types'
|
||||
|
||||
/**
|
||||
* Add reviewable VAT suggestions to identity mappings. SIE itself has no VAT
|
||||
* treatment record, so suggestions come only from the account label and are
|
||||
* never considered reviewed until the user continues from the mapping step.
|
||||
*/
|
||||
export function enrichAccountMappingsWithVat(
|
||||
mappings: AccountMapping[],
|
||||
existingAccounts: BASAccount[],
|
||||
): AccountMapping[] {
|
||||
const existingByNumber = new Map(
|
||||
existingAccounts.map((account) => [account.account_number, account]),
|
||||
)
|
||||
|
||||
return mappings.map((mapping) => {
|
||||
if (!mapping.targetAccount || mapping.sourceAccount !== mapping.targetAccount) return mapping
|
||||
const accountClass = Number(mapping.sourceAccount.charAt(0))
|
||||
if (accountClass < 3 || accountClass > 6) return mapping
|
||||
|
||||
const existing = existingByNumber.get(mapping.targetAccount)
|
||||
if (existing?.default_vat_treatment) {
|
||||
return {
|
||||
...mapping,
|
||||
defaultVatTreatment: existing.default_vat_treatment,
|
||||
defaultVatRate: existing.default_vat_rate,
|
||||
vatTreatmentReviewed: true,
|
||||
vatTreatmentSuggested: false,
|
||||
requiresVatTreatmentReview: false,
|
||||
}
|
||||
}
|
||||
|
||||
const suggestion = suggestVatTreatment(mapping.sourceAccount, mapping.sourceName)
|
||||
return {
|
||||
...mapping,
|
||||
defaultVatTreatment: suggestion?.treatment ?? null,
|
||||
defaultVatRate: suggestion?.rate ?? existing?.default_vat_rate ?? null,
|
||||
vatTreatmentSuggested: Boolean(suggestion),
|
||||
vatTreatmentReviewed: false,
|
||||
requiresVatTreatmentReview: accountClass >= 3 && accountClass <= 6,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function applyVatTreatmentReview(
|
||||
mappings: AccountMapping[],
|
||||
sourceAccount: string,
|
||||
treatment: AccountVatTreatment | null,
|
||||
rate: number | null,
|
||||
): AccountMapping[] {
|
||||
return mappings.map((mapping) =>
|
||||
mapping.sourceAccount === sourceAccount
|
||||
? {
|
||||
...mapping,
|
||||
defaultVatTreatment: treatment,
|
||||
defaultVatRate: rate,
|
||||
vatTreatmentSuggested: false,
|
||||
vatTreatmentReviewed: true,
|
||||
}
|
||||
: mapping
|
||||
)
|
||||
}
|
||||
@@ -185,6 +185,11 @@ export interface AccountMapping {
|
||||
confidence: number // 0-1
|
||||
matchType: AccountMatchType
|
||||
isOverride: boolean // User manually set this
|
||||
defaultVatTreatment?: import('@/lib/vat/account-vat-treatment').AccountVatTreatment | null
|
||||
defaultVatRate?: number | null
|
||||
vatTreatmentSuggested?: boolean
|
||||
vatTreatmentReviewed?: boolean
|
||||
requiresVatTreatmentReview?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user