Files
accounted/app/api/parties/registry/route.ts
T
fcda4a75dd feat(parties): fill customer and supplier forms from the register on a valid org number (#2355)
The registry lookup was built for rows that already exist (the detail
page's "Hämta uppgifter" records facts on the row's party), so on the
create form people typed what SCB already knew. Org number now comes
first in both forms; a complete, check-digit valid number of a Swedish
legal person is looked up once and fills name, address, postal code,
city (and the VAT number where the form shows one) wherever nothing has
been typed. A typed value is never replaced; a corrected number replaces
only its own earlier fill. A personnummer never reaches the register
(client key and server gate), and an environment without SCB credentials
answers 503 once and the form stays quiet.

- GET /api/parties/registry?org_number=: read-only route over the same
  SCB client, credential gate and registrySummary reader as the enrich
  route; writes nothing.
- lib/parties/registry-form-fill.ts: registryLookupKey (one rule for
  what may be looked up) and registryFormFill (contactFill's untouched
  rule plus name and VAT number), pure and tested.
- components/parties/use-registry-autofill.ts + RegistryAutofillNote:
  debounced, once per distinct number, skips the number an edit dialog
  opened with, one muted line under the field.
- Adressrad 2 is now an input on both forms: the register's c/o goes on
  line 1 with the street on line 2, as on the row, and both edit dialogs
  already passed the column in.


Claude-Session: https://claude.ai/code/session_01LvMaHcTnwAfxzgYD1fGYX1

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

56 lines
2.7 KiB
TypeScript

import { NextResponse } from 'next/server'
import { withRouteContext } from '@/lib/api/with-route-context'
import { validateQuery } from '@/lib/api/validate'
import { PartyRegistryLookupQuerySchema } from '@/lib/api/schemas'
import { errorResponseFromCode } from '@/lib/errors/get-structured-error'
import { createScbClient } from '@/lib/parties/scb/client'
import { isScbConfigured, scbConfigFromEnv } from '@/lib/parties/scb/config'
import { ScbApiError } from '@/lib/parties/scb/transport'
import { displayNameFromRegistry } from '@/lib/parties/registry-name'
import { registryLookupKey, type RegistryLookup } from '@/lib/parties/registry-form-fill'
import { registrySummary } from '@/lib/parties/registry-summary'
/**
* GET /api/parties/registry?org_number=: what SCB knows about a Swedish
* legal person, for the customer and supplier forms while the row does not
* exist yet. Reads only: no party, no facts, no row is written; provenance
* lands through POST /api/parties/[id]/enrich once the row has a party.
* Same client and same gates as that route: an environment without SCB
* credentials answers 503 before anything else so the form can go quiet,
* and a personnummer (a sole trader's org number) never reaches SCB.
* Writers only: the lookup exists to create a row, which viewers cannot.
*/
export const GET = withRouteContext(
'parties.registry.lookup',
async (request, { log, requestId }) => {
if (!isScbConfigured()) return errorResponseFromCode('SCB_NOT_CONFIGURED', log, { requestId })
const validated = validateQuery(request, PartyRegistryLookupQuerySchema, { log, operation: 'parties.registry.lookup' })
if (!validated.success) return validated.response
const orgNumber = registryLookupKey(validated.data.org_number)
if (!orgNumber) return errorResponseFromCode('SCB_NOT_A_LEGAL_PERSON', log, { requestId })
let lookup
try {
lookup = await createScbClient(scbConfigFromEnv()).lookupByOrgNumber(orgNumber)
} catch (err) {
log.warn('scb lookup failed', { orgNumber, status: err instanceof ScbApiError ? err.status : undefined, message: err instanceof Error ? err.message : String(err) })
return errorResponseFromCode('SCB_LOOKUP_FAILED', log, { requestId })
}
const registry = lookup.found ? registrySummary(lookup.facts.map((f) => ({ ...f, source: 'registry_scb' as const, fetchedAt: lookup.fetchedAt }))) : null
if (!registry) {
const missing: RegistryLookup = { found: false, orgNumber }
return NextResponse.json({ data: missing })
}
const data: RegistryLookup = {
found: true,
orgNumber,
name: registry.legal_name ? displayNameFromRegistry(registry.legal_name) : '',
registry,
}
return NextResponse.json({ data })
},
{ requireWrite: true },
)