Files
accounted/app/api/company/[id]/migration-reset/route.ts
T
f266c386f3 chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers (#2150)
* chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers

Remove 33 dead files, ~270 unreferenced exports/types, 13 dead i18n
namespaces and 4 unused dependencies; fold byte-identical helper copies
into one canonical home each (lib/utils chunk/sleep/utcDateStamp,
lib/dates/iso, lib/invariants/uuid, lib/xml/escape, lib/reports/sru/format,
lib/pdf/number-text, lib/browser/panel-request, lib/api/v1/body +
v1ValidationError rolled out to ~55 v1 routes, booking-template schemas).

No behaviour change: v1 bodies and status codes, MCP tool schemas, DB
writes and money math are untouched. Naive ore rounding was deliberately
not swapped for roundOre; see DECISIONS.md 2026-09-02 for the full list
of things left alone on purpose.

tsc, lint, 19588 unit tests and check:guards green; antipattern baseline
ratcheted (naive-ore-round 622 -> 620, hand-rolled-invariant 115 -> 113).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(transactions): import RawTransaction from @/types after the ingest re-export removal

CI's type ratchet (check:types, full tsconfig) caught the one test file
that still imported the type through lib/transactions/ingest.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 11:51:16 +02:00

130 lines
4.3 KiB
TypeScript

import { NextResponse } from 'next/server'
import { withRouteContext } from '@/lib/api/with-route-context'
import { privateNoStore } from '@/lib/api/private-no-store'
import { validateBody } from '@/lib/api/validate'
import { CompanyMigrationResetSchema } from '@/lib/api/schemas'
import { errorResponseFromCode } from '@/lib/errors/get-structured-error'
import type { CompanyMigrationResetRpcResult } from '@/types'
type Params = { params: Promise<{ id: string }> }
const EXPECTED_CODES = new Set([
'COMPANY_RESET_NOT_FOUND',
'COMPANY_RESET_FORBIDDEN',
'COMPANY_RESET_INELIGIBLE',
'COMPANY_RESET_CONFIRMATION_MISMATCH',
'COMPANY_RESET_REASON_INVALID',
'COMPANY_RESET_CONFIRMATION_REQUIRED',
])
function rpcFailure(
result: CompanyMigrationResetRpcResult,
log: Parameters<typeof errorResponseFromCode>[1],
requestId: string,
) {
const code = result.code && EXPECTED_CODES.has(result.code)
? result.code
: 'COMPANY_RESET_FAILED'
return errorResponseFromCode(code, log, {
requestId,
details: result.details,
})
}
/**
* GET /api/company/[id]/migration-reset
*
* Returns the owner-only, fail-closed eligibility preview. The execution RPC
* rechecks every condition, so this response is informational only.
*/
export const GET = withRouteContext<Params>(
'company.migration-reset.preview',
async (_request, { supabase, companyId, log, requestId }, { params }) => {
const { id } = await params
if (id !== companyId) {
return privateNoStore(errorResponseFromCode('COMPANY_RESET_NOT_FOUND', log, { requestId }))
}
const { data, error } = await supabase.rpc(
'get_company_migration_reset_eligibility',
{ p_company_id: companyId },
)
if (error) {
log.error('migration reset eligibility RPC failed', error)
return privateNoStore(errorResponseFromCode('COMPANY_RESET_FAILED', log, { requestId }))
}
const result = data as CompanyMigrationResetRpcResult | null
if (!result?.ok) {
return privateNoStore(rpcFailure(result ?? { ok: false }, log, requestId))
}
return privateNoStore(NextResponse.json({ data: result.eligibility }))
},
)
/**
* POST /api/company/[id]/migration-reset
*
* Atomically archives the source company and creates a clean active company.
* No source accounting record is deleted, detached, renumbered, or copied.
*/
export const POST = withRouteContext<Params>(
'company.migration-reset.execute',
async (request, { supabase, companyId, log, requestId }, { params }) => {
const { id } = await params
if (id !== companyId) {
return privateNoStore(errorResponseFromCode('COMPANY_RESET_NOT_FOUND', log, { requestId }))
}
const validation = await validateBody(request, CompanyMigrationResetSchema, {
log,
operation: 'company.migration-reset.execute',
})
if (!validation.success) return privateNoStore(validation.response)
const body = validation.data
const { data, error } = await supabase.rpc('reset_company_for_migration', {
p_company_id: companyId,
p_confirmed_name: body.confirm_name,
p_reason: body.reason,
p_confirm_no_filed_declarations: body.confirm_no_filed_declarations,
p_confirm_retained_archive: body.confirm_retained_archive,
})
if (error) {
log.error('migration reset RPC failed', error)
return privateNoStore(errorResponseFromCode('COMPANY_RESET_FAILED', log, { requestId }))
}
const result = data as CompanyMigrationResetRpcResult | null
if (!result?.ok) {
return privateNoStore(rpcFailure(result ?? { ok: false }, log, requestId))
}
if (!result.replacement_company_id) {
log.error('migration reset RPC returned no replacement company id')
return privateNoStore(errorResponseFromCode('COMPANY_RESET_FAILED', log, { requestId }))
}
const response = NextResponse.json({
data: {
resetId: result.reset_id,
sourceCompanyId: result.source_company_id,
replacementCompanyId: result.replacement_company_id,
archivedAt: result.archived_at,
retainedCounts: result.counts,
},
})
response.cookies.set('gnubok-company-id', result.replacement_company_id, {
path: '/',
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 365,
})
return privateNoStore(response)
},
{ requireWrite: true },
)