Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma strängvärde byggs med konkatenering i stället för att matcha mönstret.
634 lines
23 KiB
TypeScript
634 lines
23 KiB
TypeScript
/**
|
|
* Tests for GET/PATCH /api/assets/[id].
|
|
*
|
|
* Exercises the routes through the real withRouteContext wrapper, mocking the
|
|
* asset service and auth/company dependencies. The K3 component cross-sum
|
|
* validation runs the REAL validateComponents so the regression case (body
|
|
* changes acquisition_cost and k3_components together — sum must match the
|
|
* NEW cost) is covered end to end.
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { NextResponse } from 'next/server'
|
|
import { createQueuedMockSupabase, createMockRequest, parseJsonResponse } from '@/tests/helpers'
|
|
|
|
const { supabase, enqueue, reset } = createQueuedMockSupabase()
|
|
|
|
const requireAuthMock = vi.fn()
|
|
vi.mock('@/lib/auth/require-auth', () => ({
|
|
requireAuth: (...args: unknown[]) => requireAuthMock(...args),
|
|
}))
|
|
|
|
vi.mock('@/lib/company/context', () => ({
|
|
getActiveCompanyId: vi.fn().mockResolvedValue('company-1'),
|
|
requireCompanyId: vi.fn().mockResolvedValue('company-1'),
|
|
}))
|
|
|
|
const requireWriteMock = vi.fn()
|
|
vi.mock('@/lib/auth/require-write', () => ({
|
|
requireWritePermission: (...args: unknown[]) => requireWriteMock(...args),
|
|
}))
|
|
|
|
// Keep DEFAULT_ACCOUNTS_BY_CATEGORY (and other pure exports) real: the routes
|
|
// resolve category-default accounts through it for the K2_EXCLUDED_ACCOUNT
|
|
// framework gate. Only the service functions that hit Supabase are mocked.
|
|
vi.mock('@/lib/bokslut/assets/asset-service', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('@/lib/bokslut/assets/asset-service')>()
|
|
return {
|
|
...actual,
|
|
createAsset: vi.fn(),
|
|
listAssets: vi.fn(),
|
|
getAsset: vi.fn(),
|
|
updateAsset: vi.fn(),
|
|
}
|
|
})
|
|
|
|
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(() => {
|
|
vi.clearAllMocks()
|
|
reset()
|
|
requireAuthMock.mockResolvedValue({ user: { id: 'user-1' }, supabase, error: null })
|
|
requireWriteMock.mockResolvedValue({ ok: true })
|
|
})
|
|
|
|
describe('GET /api/assets/[id]', () => {
|
|
it('returns 401 when not authenticated', async () => {
|
|
requireAuthMock.mockResolvedValue({
|
|
user: null,
|
|
supabase,
|
|
error: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }),
|
|
})
|
|
|
|
const res = await GET(createMockRequest('/api/assets/asset-1'), routeParams)
|
|
expect(res.status).toBe(401)
|
|
})
|
|
|
|
it('returns 404 when the asset does not exist', async () => {
|
|
mockGetAsset.mockResolvedValue(null)
|
|
|
|
const { status, body } = await parseJsonResponse<{ error: { code: string } }>(
|
|
await GET(createMockRequest('/api/assets/asset-1'), routeParams)
|
|
)
|
|
|
|
expect(status).toBe(404)
|
|
expect(body.error.code).toBe('ASSET_NOT_FOUND')
|
|
})
|
|
})
|
|
|
|
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' }),
|
|
)
|
|
})
|
|
|
|
// K2 forbids only EGENUPPARBETADE immateriella tillgångar; an ACQUIRED one
|
|
// (a bought licence, a trademark) is lawful, so the category defaults now
|
|
// resolve to the acquired pair 1090/1099 for a non-K3 company and the gate
|
|
// must let the create through. See
|
|
// .claude/skills/swedish-year-end-closing/references/k2-vs-k3.md:24.
|
|
it('lets a K2 company create an immaterial asset on the category defaults', async () => {
|
|
enqueue({ data: { accounting_framework: 'k2' } })
|
|
mockCreateAsset.mockResolvedValue({ id: 'asset-licence' } as never)
|
|
|
|
const response = await POST(createMockRequest('/api/assets', {
|
|
method: 'POST',
|
|
body: {
|
|
name: 'Programvarulicens',
|
|
category: 'immaterial',
|
|
acquisition_date: '2025-01-01',
|
|
acquisition_cost: 100_000,
|
|
useful_life_months: 60,
|
|
},
|
|
}))
|
|
|
|
expect(response.status).toBe(200)
|
|
// No accounts in the body: the service resolves 1090/1099 itself, so the
|
|
// client never has to know the rule (asset-service.test.ts pins the pair).
|
|
expect(mockCreateAsset).toHaveBeenCalledWith(
|
|
supabase,
|
|
'company-1',
|
|
'user-1',
|
|
expect.objectContaining({ category: 'immaterial' }),
|
|
)
|
|
const [, , , input] = mockCreateAsset.mock.calls[0]
|
|
expect(input.bas_asset_account).toBeUndefined()
|
|
expect(input.bas_accumulated_account).toBeUndefined()
|
|
})
|
|
|
|
// Only a deliberate override onto an Ej K2 account is still unlawful.
|
|
it.each([
|
|
['1010', '1039'],
|
|
['1011', '1039'],
|
|
['1012', '1039'],
|
|
['1018', '1039'],
|
|
['1019', '1039'],
|
|
['1081', '1039'],
|
|
])(
|
|
'rejects a K2 company explicitly overriding onto %s with 422',
|
|
async (assetAccount, accumulatedAccount) => {
|
|
enqueue({ data: { accounting_framework: 'k2', entity_type: 'aktiebolag' } })
|
|
|
|
const { status, body } = await parseJsonResponse<{
|
|
error: { code: string; message: string }
|
|
}>(
|
|
await POST(createMockRequest('/api/assets', {
|
|
method: 'POST',
|
|
body: {
|
|
name: 'Utvecklingsprojekt',
|
|
category: 'immaterial',
|
|
acquisition_date: '2025-01-01',
|
|
acquisition_cost: 50_000,
|
|
useful_life_months: 60,
|
|
bas_asset_account: assetAccount,
|
|
bas_accumulated_account: accumulatedAccount,
|
|
},
|
|
}))
|
|
)
|
|
|
|
expect(status).toBe(422)
|
|
expect(body.error.code).toBe('K2_EXCLUDED_ACCOUNT')
|
|
expect(body.error.message).toContain(assetAccount)
|
|
// All six are kontogrupp 10 and the company is an AB preparing an
|
|
// årsredovisning, so the punkt 10.4 citation applies.
|
|
expect(body.error.message).toContain('BFNAR 2016:10 punkt 10.4')
|
|
expect(mockCreateAsset).not.toHaveBeenCalled()
|
|
},
|
|
)
|
|
|
|
// companies.accounting_framework is NOT NULL DEFAULT 'k2', so an enskild
|
|
// firma runs into the same gate. It prepares a förenklat årsbokslut, not an
|
|
// årsredovisning under BFNAR 2016:10, so the K2 citation would be a false
|
|
// legal claim and "use K3 instead" an impossible remedy. The block stands;
|
|
// the wording drops both and keeps the actionable 1090 remedy.
|
|
it('rejects an enskild firma on 1010 without citing BFNAR 2016:10 or K3', async () => {
|
|
enqueue({ data: { accounting_framework: 'k2', entity_type: 'enskild_firma' } })
|
|
|
|
const { status, body } = await parseJsonResponse<{
|
|
error: { code: string; message: string; message_en: string }
|
|
}>(
|
|
await POST(createMockRequest('/api/assets', {
|
|
method: 'POST',
|
|
body: {
|
|
name: 'Utvecklingsprojekt',
|
|
category: 'immaterial',
|
|
acquisition_date: '2025-01-01',
|
|
acquisition_cost: 50_000,
|
|
useful_life_months: 60,
|
|
bas_asset_account: '1010',
|
|
bas_accumulated_account: '1039',
|
|
},
|
|
}))
|
|
)
|
|
|
|
expect(status).toBe(422)
|
|
expect(body.error.code).toBe('K2_EXCLUDED_ACCOUNT')
|
|
expect(body.error.message).toContain('1010')
|
|
expect(body.error.message).not.toContain('BFNAR 2016:10')
|
|
expect(body.error.message).not.toContain('10.4')
|
|
expect(body.error.message).not.toContain('K3')
|
|
expect(body.error.message_en).not.toContain('BFNAR 2016:10')
|
|
expect(body.error.message_en).not.toContain('K3')
|
|
// Still actionable: the lawful account, and what the system did.
|
|
expect(body.error.message).toContain('1090')
|
|
expect(body.error.message).toContain('anläggningsregistret')
|
|
expect(mockCreateAsset).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects a K2 company overriding the ACCUMULATED account onto 1019 with 422', async () => {
|
|
enqueue({ data: { accounting_framework: 'k2' } })
|
|
|
|
const { status, body } = await parseJsonResponse<{ error: { code: string; message: string } }>(
|
|
await POST(createMockRequest('/api/assets', {
|
|
method: 'POST',
|
|
body: {
|
|
name: 'Patent',
|
|
category: 'immaterial',
|
|
acquisition_date: '2025-01-01',
|
|
acquisition_cost: 50_000,
|
|
useful_life_months: 60,
|
|
bas_asset_account: '1030',
|
|
bas_accumulated_account: '1019',
|
|
},
|
|
}))
|
|
)
|
|
|
|
expect(status).toBe(422)
|
|
expect(body.error.code).toBe('K2_EXCLUDED_ACCOUNT')
|
|
expect(body.error.message).toContain('1019')
|
|
expect(mockCreateAsset).not.toHaveBeenCalled()
|
|
})
|
|
|
|
// Switching regelverk drags in komponentavskrivning and uppskjuten skatt and
|
|
// rewrites the whole årsredovisning: it is never the remedy for one account.
|
|
// The rejection must also not assert which framework the company applies,
|
|
// since the companies read behind it discards its error.
|
|
it('does not tell the user to switch accounting framework in the rejection', async () => {
|
|
enqueue({ data: { accounting_framework: 'k2' } })
|
|
|
|
const { body } = await parseJsonResponse<{
|
|
error: { message: string; message_en: string }
|
|
}>(
|
|
await POST(createMockRequest('/api/assets', {
|
|
method: 'POST',
|
|
body: {
|
|
name: 'Utvecklingsprojekt',
|
|
category: 'immaterial',
|
|
acquisition_date: '2025-01-01',
|
|
acquisition_cost: 50_000,
|
|
useful_life_months: 60,
|
|
bas_asset_account: '1010',
|
|
bas_accumulated_account: '1039',
|
|
},
|
|
}))
|
|
)
|
|
|
|
expect(body.error.message).not.toContain('Byt regelverk')
|
|
expect(body.error.message).not.toContain('Inställningar')
|
|
expect(body.error.message).not.toContain('företaget tillämpar')
|
|
expect(body.error.message_en).not.toContain('Switch the accounting framework')
|
|
expect(body.error.message_en).not.toContain('Settings')
|
|
// It points at the lawful account instead.
|
|
expect(body.error.message).toContain('1090')
|
|
expect(body.error.message_en).toContain('1090')
|
|
})
|
|
|
|
it('accepts a K2 company creating an immaterial asset on a purchased pair (1030/1039)', async () => {
|
|
enqueue({ data: { accounting_framework: 'k2' } })
|
|
mockCreateAsset.mockResolvedValue({ id: 'asset-patent' } as never)
|
|
|
|
const response = await POST(createMockRequest('/api/assets', {
|
|
method: 'POST',
|
|
body: {
|
|
name: 'Patent',
|
|
category: 'immaterial',
|
|
acquisition_date: '2025-01-01',
|
|
acquisition_cost: 80_000,
|
|
useful_life_months: 60,
|
|
bas_asset_account: '1030',
|
|
bas_accumulated_account: '1039',
|
|
},
|
|
}))
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(mockCreateAsset).toHaveBeenCalledWith(
|
|
supabase,
|
|
'company-1',
|
|
'user-1',
|
|
expect.objectContaining({ bas_asset_account: '1030' }),
|
|
)
|
|
})
|
|
|
|
it('accepts a K3 company creating an immaterial asset on the 1010/1019 defaults', async () => {
|
|
enqueue({ data: { accounting_framework: 'k3' } })
|
|
mockCreateAsset.mockResolvedValue({ id: 'asset-dev' } as never)
|
|
|
|
const response = await POST(createMockRequest('/api/assets', {
|
|
method: 'POST',
|
|
body: {
|
|
name: 'Utvecklingsutgifter plattform',
|
|
category: 'immaterial',
|
|
acquisition_date: '2025-01-01',
|
|
acquisition_cost: 100_000,
|
|
useful_life_months: 60,
|
|
},
|
|
}))
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(mockCreateAsset).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
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',
|
|
body: { acquisition_cost: -5 },
|
|
})
|
|
|
|
const { status } = await parseJsonResponse(await PATCH(req, routeParams))
|
|
|
|
expect(status).toBe(400)
|
|
expect(mockUpdateAsset).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects k3_components for a K2 company with 422', async () => {
|
|
enqueue({ data: { accounting_framework: 'k2' } })
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
mockGetAsset.mockResolvedValue({ id: 'asset-1', acquisition_cost: 100000 } as any)
|
|
|
|
const req = createMockRequest('/api/assets/asset-1', {
|
|
method: 'PATCH',
|
|
body: {
|
|
k3_components: [{ name: 'Stomme', cost: 100000, useful_life_months: 600 }],
|
|
},
|
|
})
|
|
|
|
const { status, body } = await parseJsonResponse<{ error: { code: string } }>(
|
|
await PATCH(req, routeParams)
|
|
)
|
|
|
|
expect(status).toBe(422)
|
|
expect(body.error.code).toBe('K3_REQUIRED_FOR_COMPONENTS')
|
|
expect(mockUpdateAsset).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('validates the component sum against the NEW acquisition_cost when both change', async () => {
|
|
// Regression: stored cost is 100 000 but the PATCH raises it to 120 000.
|
|
// Components summing to 120 000 must pass — previously they were checked
|
|
// against the stale stored cost and wrongly rejected.
|
|
enqueue({ data: { accounting_framework: 'k3' } })
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
mockGetAsset.mockResolvedValue({ id: 'asset-1', acquisition_cost: 100000 } as any)
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
mockUpdateAsset.mockResolvedValue({ id: 'asset-1', acquisition_cost: 120000 } as any)
|
|
|
|
const req = createMockRequest('/api/assets/asset-1', {
|
|
method: 'PATCH',
|
|
body: {
|
|
acquisition_cost: 120000,
|
|
k3_components: [
|
|
{ name: 'Stomme', cost: 90000, useful_life_months: 600 },
|
|
{ name: 'Tak', cost: 30000, useful_life_months: 240 },
|
|
],
|
|
},
|
|
})
|
|
|
|
const { status } = await parseJsonResponse(await PATCH(req, routeParams))
|
|
|
|
expect(status).toBe(200)
|
|
expect(mockUpdateAsset).toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects components that sum to the OLD cost when the PATCH changes the cost', async () => {
|
|
enqueue({ data: { accounting_framework: 'k3' } })
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
mockGetAsset.mockResolvedValue({ id: 'asset-1', acquisition_cost: 100000 } as any)
|
|
|
|
const req = createMockRequest('/api/assets/asset-1', {
|
|
method: 'PATCH',
|
|
body: {
|
|
acquisition_cost: 120000,
|
|
k3_components: [{ name: 'Stomme', cost: 100000, useful_life_months: 600 }],
|
|
},
|
|
})
|
|
|
|
const { status, body } = await parseJsonResponse<{ error: { code: string } }>(
|
|
await PATCH(req, routeParams)
|
|
)
|
|
|
|
expect(status).toBe(400)
|
|
expect(body.error.code).toBe('INVALID_K3_COMPONENTS')
|
|
expect(mockUpdateAsset).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects a K2 company patching the asset account onto 1010 with 422', async () => {
|
|
enqueue({ data: { accounting_framework: 'k2', entity_type: 'aktiebolag' } })
|
|
mockGetAsset.mockResolvedValue({
|
|
id: 'asset-1',
|
|
category: 'immaterial',
|
|
bas_asset_account: '1030',
|
|
bas_accumulated_account: '1039',
|
|
bas_expense_account: '7813',
|
|
} as never)
|
|
|
|
const req = createMockRequest('/api/assets/asset-1', {
|
|
method: 'PATCH',
|
|
body: { bas_asset_account: '1010' },
|
|
})
|
|
|
|
const { status, body } = await parseJsonResponse<{ error: { code: string; message: string } }>(
|
|
await PATCH(req, routeParams)
|
|
)
|
|
|
|
expect(status).toBe(422)
|
|
expect(body.error.code).toBe('K2_EXCLUDED_ACCOUNT')
|
|
expect(body.error.message).toContain('1010')
|
|
// AB on the companies row, so the K2 citation is the right one here.
|
|
expect(body.error.message).toContain('BFNAR 2016:10 punkt 10.4')
|
|
expect(mockUpdateAsset).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects a K2 company recategorizing AND overriding onto 1010 with 422', async () => {
|
|
// Explicit accounts suppress the realign in updateAsset(), so the gate has
|
|
// to evaluate the override rather than the (now lawful) category defaults.
|
|
enqueue({ data: { accounting_framework: 'k2' } })
|
|
mockGetAsset.mockResolvedValue({
|
|
id: 'asset-1',
|
|
category: 'equipment',
|
|
bas_asset_account: '1220',
|
|
bas_accumulated_account: '1229',
|
|
bas_expense_account: '7832',
|
|
} as never)
|
|
|
|
const req = createMockRequest('/api/assets/asset-1', {
|
|
method: 'PATCH',
|
|
body: {
|
|
category: 'immaterial',
|
|
bas_asset_account: '1010',
|
|
bas_accumulated_account: '1019',
|
|
},
|
|
})
|
|
|
|
const { status, body } = await parseJsonResponse<{ error: { code: string; message: string } }>(
|
|
await PATCH(req, routeParams)
|
|
)
|
|
|
|
expect(status).toBe(422)
|
|
expect(body.error.code).toBe('K2_EXCLUDED_ACCOUNT')
|
|
expect(body.error.message).toContain('1010')
|
|
expect(mockUpdateAsset).not.toHaveBeenCalled()
|
|
})
|
|
|
|
// The Ej K2 flag also covers accounts that have nothing to do with
|
|
// intangibles (uppskjuten skatt, verkligt värde, säkringsredovisning, ...).
|
|
// PATCH can reach them: UpdateAssetSchema has no BAS range refinement, so an
|
|
// override outside the category range hits this gate before updateAsset()
|
|
// raises its range error. Those rejections must NOT claim punkt 10.4.
|
|
it('rejects a K2 company patching onto 1370 without citing the intangible rule', async () => {
|
|
enqueue({ data: { accounting_framework: 'k2', entity_type: 'aktiebolag' } })
|
|
mockGetAsset.mockResolvedValue({
|
|
id: 'asset-1',
|
|
category: 'immaterial',
|
|
bas_asset_account: '1030',
|
|
bas_accumulated_account: '1039',
|
|
bas_expense_account: '7813',
|
|
} as never)
|
|
|
|
const req = createMockRequest('/api/assets/asset-1', {
|
|
method: 'PATCH',
|
|
body: { bas_asset_account: '1370' },
|
|
})
|
|
|
|
const { status, body } = await parseJsonResponse<{
|
|
error: { code: string; message: string; message_en: string }
|
|
}>(await PATCH(req, routeParams))
|
|
|
|
expect(status).toBe(422)
|
|
expect(body.error.code).toBe('K2_EXCLUDED_ACCOUNT')
|
|
expect(body.error.message).toContain('1370')
|
|
expect(body.error.message).toContain('Ej K2')
|
|
expect(body.error.message).toContain('K3')
|
|
expect(body.error.message).not.toContain('10.4')
|
|
expect(body.error.message).not.toContain('egenupparbetade')
|
|
expect(body.error.message_en).toContain('Ej K2')
|
|
expect(body.error.message_en).not.toContain('10.4')
|
|
expect(body.error.message_en).not.toContain('intangible')
|
|
expect(mockUpdateAsset).not.toHaveBeenCalled()
|
|
})
|
|
|
|
// The concrete case this route used to break: a K2 aktiebolag bought a
|
|
// software licence, first filed it as "Inventarier", and now recategorizes
|
|
// it to "Immateriell tillgång" from a dialog that sends only the changed
|
|
// field and has no account inputs. K2 forbids EGENUPPARBETADE intangibles
|
|
// only, so this is lawful and the defaults must land on the acquired pair.
|
|
it('lets a K2 company recategorize to immaterial (defaults land on 1090/1099)', async () => {
|
|
enqueue({ data: { accounting_framework: 'k2' } })
|
|
mockGetAsset.mockResolvedValue({
|
|
id: 'asset-1',
|
|
category: 'equipment',
|
|
bas_asset_account: '1220',
|
|
bas_accumulated_account: '1229',
|
|
bas_expense_account: '7832',
|
|
} as never)
|
|
mockUpdateAsset.mockResolvedValue({
|
|
id: 'asset-1',
|
|
category: 'immaterial',
|
|
bas_asset_account: '1090',
|
|
bas_accumulated_account: '1099',
|
|
} as never)
|
|
|
|
const req = createMockRequest('/api/assets/asset-1', {
|
|
method: 'PATCH',
|
|
body: { category: 'immaterial' },
|
|
})
|
|
|
|
const { status, body } = await parseJsonResponse<{
|
|
data: { bas_asset_account: string; bas_accumulated_account: string }
|
|
}>(await PATCH(req, routeParams))
|
|
|
|
expect(status).toBe(200)
|
|
expect(body.data.bas_asset_account).toBe('1090')
|
|
expect(body.data.bas_accumulated_account).toBe('1099')
|
|
// The route forwards the bare category patch: updateAsset realigns the
|
|
// triple itself (asset-service.test.ts pins the realigned pair).
|
|
expect(mockUpdateAsset).toHaveBeenCalledWith(supabase, 'company-1', 'asset-1', {
|
|
category: 'immaterial',
|
|
})
|
|
})
|
|
|
|
it('allows a K2 company moving a legacy 1010 asset onto a purchased pair', async () => {
|
|
enqueue({ data: { accounting_framework: 'k2' } })
|
|
mockGetAsset.mockResolvedValue({
|
|
id: 'asset-1',
|
|
category: 'immaterial',
|
|
bas_asset_account: '1010',
|
|
bas_accumulated_account: '1019',
|
|
bas_expense_account: '7811',
|
|
} as never)
|
|
mockUpdateAsset.mockResolvedValue({ id: 'asset-1', bas_asset_account: '1030' } as never)
|
|
|
|
const req = createMockRequest('/api/assets/asset-1', {
|
|
method: 'PATCH',
|
|
body: { bas_asset_account: '1030', bas_accumulated_account: '1039' },
|
|
})
|
|
|
|
const { status } = await parseJsonResponse(await PATCH(req, routeParams))
|
|
|
|
expect(status).toBe(200)
|
|
expect(mockUpdateAsset).toHaveBeenCalled()
|
|
})
|
|
|
|
it('allows a K3 company patching the asset account onto 1010', async () => {
|
|
enqueue({ data: { accounting_framework: 'k3' } })
|
|
mockGetAsset.mockResolvedValue({
|
|
id: 'asset-1',
|
|
category: 'immaterial',
|
|
bas_asset_account: '1030',
|
|
bas_accumulated_account: '1039',
|
|
bas_expense_account: '7813',
|
|
} as never)
|
|
mockUpdateAsset.mockResolvedValue({ id: 'asset-1', bas_asset_account: '1010' } as never)
|
|
|
|
const req = createMockRequest('/api/assets/asset-1', {
|
|
method: 'PATCH',
|
|
body: { bas_asset_account: '1010', bas_accumulated_account: '1019' },
|
|
})
|
|
|
|
const { status } = await parseJsonResponse(await PATCH(req, routeParams))
|
|
|
|
expect(status).toBe(200)
|
|
expect(mockUpdateAsset).toHaveBeenCalled()
|
|
})
|
|
|
|
it('skips the framework gate for patches that touch neither category nor accounts', async () => {
|
|
// No company row enqueued and getAsset unmocked: if the gate ran anyway
|
|
// it would resolve a null company (treated as K2) and 404 on the missing
|
|
// asset. A 200 therefore proves the name-only patch never hit the gate,
|
|
// which keeps legacy K2 assets already sitting on 1010 editable.
|
|
mockUpdateAsset.mockResolvedValue({ id: 'asset-1', name: 'Nytt namn' } as never)
|
|
|
|
const req = createMockRequest('/api/assets/asset-1', {
|
|
method: 'PATCH',
|
|
body: { name: 'Nytt namn' },
|
|
})
|
|
|
|
const { status } = await parseJsonResponse(await PATCH(req, routeParams))
|
|
|
|
expect(status).toBe(200)
|
|
expect(mockUpdateAsset).toHaveBeenCalled()
|
|
})
|
|
})
|