* 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>
130 lines
4.3 KiB
TypeScript
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 },
|
|
)
|