feat(bokslut): IL 18 kap pooled tax depreciation with method election (#1393)
* feat(bokslut): IL 18 kap pooled tax depreciation with method election Rakenskapsenlig (huvudregel 30 / kompletteringsregel 20) and restvarde 25 as a company-level annual pool separate from per-asset book depreciation. Method election persisted with immutable snapshots and book-conformity confirmation for rakenskapsenlig. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(db): move tax depreciation migrations to coordinated versions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bokslut): keep tax depreciation view loadable when a saved election goes stale A predecessor's changed closing value can push a saved elected deduction above the new statutory maximum; the view now falls back to the statutory recomputation so the snapshot is flagged stale instead of crashing. Ratchet naive-ore-round baseline down by 3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bokslut): resolve tax-depreciation period selects statically The no-phantom-columns guard counts every select it cannot resolve toward a hard ceiling, and the PERIOD_COLUMNS join pushed the repo 4 over (364 > 360). Inline the literal column list at the four call sites so the guard verifies these columns instead of skipping them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bokslut): address review findings on tax depreciation election - DepreciationPanel: gate the saving flag on a dedicated save sequence so a successful save (which refreshes the view and bumps the request version) no longer leaves the card permanently busy - computeTaxDepreciation: refuse kompletteringsregel_20 with a positive basis and no acquisition cohorts instead of degenerating to a full write-off the cohort evidence does not support (IL 18 kap. 17 §) - migration 227000: judge the asset-method guards on NEW.disposed_at so reversing a disposal cannot reactivate a grandfathered non-linear row - migration 227200: require snapshot column completeness in the CHECK; SQL NULL semantics let partially populated snapshots pass the pure arithmetic comparisons - depreciation route: use the string issue code 'custom' like the rest of the codebase instead of the Zod 3 compat enum Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
00ae3540db
commit
5ca64bde30
@@ -29,15 +29,19 @@ vi.mock('@/lib/auth/require-write', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/bokslut/assets/asset-service', () => ({
|
||||
createAsset: vi.fn(),
|
||||
listAssets: vi.fn(),
|
||||
getAsset: vi.fn(),
|
||||
updateAsset: vi.fn(),
|
||||
}))
|
||||
|
||||
import { getAsset, updateAsset } from '@/lib/bokslut/assets/asset-service'
|
||||
import { createAsset, getAsset, updateAsset } from '@/lib/bokslut/assets/asset-service'
|
||||
import { GET, PATCH } from '../[id]/route'
|
||||
import { POST } from '../route'
|
||||
|
||||
const mockGetAsset = vi.mocked(getAsset)
|
||||
const mockUpdateAsset = vi.mocked(updateAsset)
|
||||
const mockCreateAsset = vi.mocked(createAsset)
|
||||
const routeParams = { params: Promise.resolve({ id: 'asset-1' }) }
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -71,7 +75,60 @@ describe('GET /api/assets/[id]', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/assets', () => {
|
||||
it('rejects legacy per-asset tax depreciation methods with 400', async () => {
|
||||
const response = await POST(createMockRequest('/api/assets', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
name: 'Maskin',
|
||||
category: 'machinery',
|
||||
acquisition_date: '2025-01-01',
|
||||
acquisition_cost: 100_000,
|
||||
useful_life_months: 60,
|
||||
depreciation_method: 'declining_balance_30',
|
||||
},
|
||||
}))
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
})
|
||||
|
||||
it('creates a valid asset with ordinary linear depreciation', async () => {
|
||||
mockCreateAsset.mockResolvedValue({ id: 'asset-new', depreciation_method: 'linear' } as never)
|
||||
const response = await POST(createMockRequest('/api/assets', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
name: 'Maskin',
|
||||
category: 'machinery',
|
||||
acquisition_date: '2025-01-01',
|
||||
acquisition_cost: 100_000,
|
||||
useful_life_months: 60,
|
||||
depreciation_method: 'linear',
|
||||
},
|
||||
}))
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(mockCreateAsset).toHaveBeenCalledWith(
|
||||
supabase,
|
||||
'company-1',
|
||||
'user-1',
|
||||
expect.objectContaining({ depreciation_method: 'linear' }),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PATCH /api/assets/[id]', () => {
|
||||
it('rejects legacy per-asset tax depreciation methods with 400', async () => {
|
||||
const req = createMockRequest('/api/assets/asset-1', {
|
||||
method: 'PATCH',
|
||||
body: { depreciation_method: 'declining_balance_30' },
|
||||
})
|
||||
|
||||
const { status } = await parseJsonResponse(await PATCH(req, routeParams))
|
||||
|
||||
expect(status).toBe(400)
|
||||
expect(mockUpdateAsset).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects an invalid body (non-positive acquisition_cost) with 400', async () => {
|
||||
const req = createMockRequest('/api/assets/asset-1', {
|
||||
method: 'PATCH',
|
||||
|
||||
Reference in New Issue
Block a user