feat(onboarding): the orgnr field also accepts a company name (#2421)
* feat(onboarding): the orgnr field also accepts a company name The journey's first question kept asking for an organisationsnummer, and people who do not know theirs by heart left to look it up. The same field now takes either: digits (with dashes or spaces) run the existing orgnr lookup unchanged; anything else with three or more characters runs a free-text name search against the same TIC index. One hit continues exactly as a typed orgnr would; several hits render as a chip row "Name / orgnr / city" inside the same question, and the pick applies the hit's already-fetched lookup result. No hits stays on the step with a note to refine or type the number. The screen, placeholder and hint are otherwise untouched; only the mobile keyboard changes from numeric to text. Why the problem occurred: the lookup was keyed on the one identifier the user is least likely to remember, while the provider index behind it is a full-text index that already answers names. What was removed or simplified: nothing new is stored. The TIC search document carries every field /lookup returns, so a name hit is mapped by the same mapper and a picked hit costs no second provider call. The reducer gained one shared "TIC answered" transition (applyLookupFound) that the typed-orgnr path, the single-hit path and the pick path all use, instead of three copies of the fact-to-settings mapping. Why this shape and not the proposed one: search-as-you-type autocomplete would burn the 3000/mo TIC budget in days, so the search fires on Enter only, like the orgnr lookup. Taking the top hit blind on several matches was rejected: name ranking is fuzzy and common names or sole-trader surnames would land on a stranger's company; a five-chip pick row is the smallest thing that keeps the user in control. The route answers 400 under three characters, 404 in the handler's own "Company not found" shape so the client's existing dispatcher-vs-handler mapping applies, and every TIC failure code maps through the same handler as /lookup. Fixes #2418 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKHTqmnvBJAgdW4V7wZsAW * fix(onboarding): reduce Lens registration numbers to the 10-digit form for name-search hits Skeptic pass on 1d70716a8 (issue #2418). A sole trader found by name got Lens's 16-digit registration number (century-prefixed personnummer plus a 4-digit serial) stored as org_number; createCompany refuses anything normalizeOrgNumber rejects, so the journey dead-ended at the last step and the returned orgnr step could only shake. The typed-orgnr path never stored Lens's number, so this was the first place it reached settings. - searchCompaniesForLookup derives orgNumber through the new lensRegistrationToOrgNumber (16-prefixed 12 digits and the 16-digit enskild-firma form reduce to the 10-digit key; hits that do not normalize are dropped, never dead-ended). - Sole-trader chips show "Enskild firma" and city instead of the number, which is the owner's personnummer. - The name path resets the duplicate note on submit, so an earlier orgnr's "you already have X" no longer sits above the chip row. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKHTqmnvBJAgdW4V7wZsAW * fix(onboarding): keep the typed name in the field after a search pick Compliance swarm on PR #2421: writing the picked hit's org number into the visible input printed a sole trader's personnummer in plain text on Back, the one thing the chip row masks. The field now keeps the name the user typed; Back re-searches it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKHTqmnvBJAgdW4V7wZsAW --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
5987523a25
commit
1157ff1b66
@@ -0,0 +1,194 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
vi.mock('../lib/tic-client', () => ({
|
||||
searchCompanyByOrgNumber: vi.fn(),
|
||||
searchCompaniesByName: vi.fn(),
|
||||
getBankAccounts: vi.fn(),
|
||||
getIndustryCodes: vi.fn(),
|
||||
getEmails: vi.fn(),
|
||||
getPhones: vi.fn(),
|
||||
getFiscalYears: vi.fn(),
|
||||
}))
|
||||
|
||||
import { ticExtension } from '../index'
|
||||
import { searchCompaniesByName } from '../lib/tic-client'
|
||||
import { lensRegistrationToOrgNumber, searchCompaniesForLookup } from '../lib/lookup'
|
||||
import { TICAPIError } from '../lib/tic-types'
|
||||
import type { TICCompanyDocument } from '../lib/tic-types'
|
||||
|
||||
const mockSearch = vi.mocked(searchCompaniesByName)
|
||||
|
||||
function makeRequest(q?: string): Request {
|
||||
const url = q
|
||||
? `http://localhost/api/extensions/ext/tic/search?q=${encodeURIComponent(q)}`
|
||||
: 'http://localhost/api/extensions/ext/tic/search'
|
||||
return new Request(url)
|
||||
}
|
||||
|
||||
const route = ticExtension.apiRoutes!.find((r) => r.path === '/search')!
|
||||
const searchHandler = route.handler
|
||||
|
||||
function doc(overrides: Partial<TICCompanyDocument>): TICCompanyDocument {
|
||||
return {
|
||||
companyId: 1,
|
||||
registrationNumber: '5560360793',
|
||||
names: [{ nameOrIdentifier: 'Testbrand AB', companyNamingType: 'name' }],
|
||||
legalEntityType: 'AB',
|
||||
registrationDate: Math.floor(Date.UTC(2020, 0, 1) / 1000),
|
||||
mostRecentRegisteredAddress: { streetAddress: 'Storgatan 1', postalCode: '111 22', city: 'Stockholm' },
|
||||
isRegisteredForFTax: true,
|
||||
isRegisteredForVAT: true,
|
||||
isCeased: false,
|
||||
activityStatus: 'isActive',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('searchCompaniesForLookup', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('maps each document to a hit with a cleaned org number', async () => {
|
||||
mockSearch.mockResolvedValue([doc({ registrationNumber: '556036-0793' })])
|
||||
const hits = await searchCompaniesForLookup('Testbrand')
|
||||
expect(mockSearch).toHaveBeenCalledWith('Testbrand', 5)
|
||||
expect(hits).toHaveLength(1)
|
||||
expect(hits[0].orgNumber).toBe('5560360793')
|
||||
expect(hits[0].result.companyName).toBe('Testbrand AB')
|
||||
expect(hits[0].result.legalEntityType).toBe('AB')
|
||||
expect(hits[0].result.address?.city).toBe('Stockholm')
|
||||
})
|
||||
|
||||
it('sinks ceased companies below active ones without reordering otherwise', async () => {
|
||||
mockSearch.mockResolvedValue([
|
||||
doc({ companyId: 1, registrationNumber: '5560000019', isCeased: true }),
|
||||
doc({ companyId: 2, registrationNumber: '5560000027' }),
|
||||
doc({ companyId: 3, registrationNumber: '5560000035', isCeased: true }),
|
||||
doc({ companyId: 4, registrationNumber: '5560000043' }),
|
||||
])
|
||||
const hits = await searchCompaniesForLookup('Testbrand')
|
||||
expect(hits.map((h) => h.orgNumber)).toEqual([
|
||||
'5560000027',
|
||||
'5560000043',
|
||||
'5560000019',
|
||||
'5560000035',
|
||||
])
|
||||
})
|
||||
|
||||
it("reduces a sole trader's 16-digit Lens number to the 10-digit personnummer form", async () => {
|
||||
mockSearch.mockResolvedValue([
|
||||
doc({
|
||||
registrationNumber: '2002011732750001',
|
||||
legalEntityType: 'EF',
|
||||
names: [{ nameOrIdentifier: 'Alices Konsult', companyNamingType: 'name' }],
|
||||
}),
|
||||
])
|
||||
const hits = await searchCompaniesForLookup('Alices Konsult')
|
||||
expect(hits).toHaveLength(1)
|
||||
expect(hits[0].orgNumber).toBe('0201173275')
|
||||
expect(hits[0].result.legalEntityType).toBe('EF')
|
||||
})
|
||||
|
||||
it('drops a hit whose registration number cannot become a valid org number', async () => {
|
||||
mockSearch.mockResolvedValue([
|
||||
doc({ companyId: 1, registrationNumber: '5560000000' }),
|
||||
doc({ companyId: 2, registrationNumber: '5560000027' }),
|
||||
])
|
||||
const hits = await searchCompaniesForLookup('Testbrand')
|
||||
expect(hits.map((h) => h.orgNumber)).toEqual(['5560000027'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('lensRegistrationToOrgNumber', () => {
|
||||
it('keeps a valid 10-digit organisationsnummer', () => {
|
||||
expect(lensRegistrationToOrgNumber('5560360793')).toBe('5560360793')
|
||||
expect(lensRegistrationToOrgNumber('556036-0793')).toBe('5560360793')
|
||||
})
|
||||
|
||||
it('strips the 16 century prefix from a 12-digit organisationsnummer', () => {
|
||||
expect(lensRegistrationToOrgNumber('165560360793')).toBe('5560360793')
|
||||
})
|
||||
|
||||
it('strips century and serial from a 16-digit enskild firma number', () => {
|
||||
expect(lensRegistrationToOrgNumber('2002011732750001')).toBe('0201173275')
|
||||
expect(lensRegistrationToOrgNumber('1980010112310001')).toBe('8001011231')
|
||||
})
|
||||
|
||||
it('returns null for a VAT-number shape, a bad check digit, or garbage', () => {
|
||||
expect(lensRegistrationToOrgNumber('556036079301')).toBeNull()
|
||||
expect(lensRegistrationToOrgNumber('5560360794')).toBeNull()
|
||||
expect(lensRegistrationToOrgNumber('')).toBeNull()
|
||||
expect(lensRegistrationToOrgNumber('abc')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('TIC search route', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('is registered with skipCompanyContext (user has no company yet)', () => {
|
||||
expect(route.method).toBe('GET')
|
||||
expect(route.skipCompanyContext).toBe(true)
|
||||
})
|
||||
|
||||
it('returns 400 when q is missing', async () => {
|
||||
const res = await searchHandler(makeRequest())
|
||||
expect(res.status).toBe(400)
|
||||
expect(mockSearch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns 400 when q is shorter than the minimum', async () => {
|
||||
const res = await searchHandler(makeRequest('ab'))
|
||||
expect(res.status).toBe(400)
|
||||
expect(mockSearch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('trims q before measuring it', async () => {
|
||||
const res = await searchHandler(makeRequest(' ab '))
|
||||
expect(res.status).toBe(400)
|
||||
expect(mockSearch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("returns the TIC handler's 404 body when nothing matched", async () => {
|
||||
mockSearch.mockResolvedValue([])
|
||||
const res = await searchHandler(makeRequest('Nothing Like This'))
|
||||
expect(res.status).toBe(404)
|
||||
expect(await res.json()).toEqual({ error: 'Company not found' })
|
||||
})
|
||||
|
||||
it('returns hits in /lookup shape on success', async () => {
|
||||
mockSearch.mockResolvedValue([
|
||||
doc({ registrationNumber: '5560360793' }),
|
||||
doc({ companyId: 2, registrationNumber: '5566778899', names: [{ nameOrIdentifier: 'Testbrand Bygg AB', companyNamingType: 'name' }] }),
|
||||
])
|
||||
const res = await searchHandler(makeRequest('Testbrand'))
|
||||
expect(res.status).toBe(200)
|
||||
const { data } = await res.json()
|
||||
expect(data).toHaveLength(2)
|
||||
expect(data[0]).toMatchObject({
|
||||
orgNumber: '5560360793',
|
||||
result: { companyName: 'Testbrand AB', registration: { fTax: true, vat: true } },
|
||||
})
|
||||
expect(data[1].result.companyName).toBe('Testbrand Bygg AB')
|
||||
})
|
||||
|
||||
it('maps a rate limit to 429', async () => {
|
||||
mockSearch.mockRejectedValue(new TICAPIError('Rate limit exceeded', 429, 'RATE_LIMIT_EXCEEDED'))
|
||||
const res = await searchHandler(makeRequest('Testbrand'))
|
||||
expect(res.status).toBe(429)
|
||||
})
|
||||
|
||||
it('maps NOT_CONFIGURED to 503', async () => {
|
||||
mockSearch.mockRejectedValue(new TICAPIError('missing', undefined, 'NOT_CONFIGURED'))
|
||||
const res = await searchHandler(makeRequest('Testbrand'))
|
||||
expect(res.status).toBe(503)
|
||||
})
|
||||
|
||||
it('maps an upstream 5xx to 502', async () => {
|
||||
mockSearch.mockRejectedValue(new TICAPIError('boom', 500))
|
||||
const res = await searchHandler(makeRequest('Testbrand'))
|
||||
expect(res.status).toBe(502)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,93 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { searchCompaniesByName, __resetTicCacheForTest } from '../lib/tic-client'
|
||||
|
||||
const PROXY_URL = 'https://proxy.example.com/api/tic/proxy'
|
||||
|
||||
function hit(registrationNumber: string) {
|
||||
return {
|
||||
document: {
|
||||
companyId: 1,
|
||||
registrationNumber,
|
||||
names: [{ nameOrIdentifier: 'Testbrand AB', companyNamingType: 'name' }],
|
||||
legalEntityType: 'AB',
|
||||
registrationDate: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('searchCompaniesByName', () => {
|
||||
beforeEach(() => {
|
||||
__resetTicCacheForTest()
|
||||
vi.stubGlobal('fetch', vi.fn())
|
||||
vi.stubEnv('TIC_API_PROXY_URL', PROXY_URL)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllEnvs()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('queries the nested name field with an encoded, trimmed query and a page cap', async () => {
|
||||
const mockFetch = vi.mocked(fetch)
|
||||
mockFetch.mockResolvedValue(
|
||||
new Response(JSON.stringify({ found: 1, hits: [hit('5560360793')] }), { status: 200 }),
|
||||
)
|
||||
|
||||
const docs = await searchCompaniesByName(' Testbrand Bygg & Co ', 5)
|
||||
|
||||
const endpoint = '/search-public/companies?q=Testbrand%20Bygg%20%26%20Co&query_by=names.nameOrIdentifier&per_page=5'
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`${PROXY_URL}?endpoint=${encodeURIComponent(endpoint)}`,
|
||||
expect.anything(),
|
||||
)
|
||||
expect(docs).toHaveLength(1)
|
||||
expect(docs[0].registrationNumber).toBe('5560360793')
|
||||
})
|
||||
|
||||
it('returns an empty array without calling upstream for a blank query', async () => {
|
||||
const docs = await searchCompaniesByName(' ')
|
||||
expect(docs).toEqual([])
|
||||
expect(fetch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns an empty array when the index has no hits', async () => {
|
||||
vi.mocked(fetch).mockResolvedValue(
|
||||
new Response(JSON.stringify({ found: 0, hits: [] }), { status: 200 }),
|
||||
)
|
||||
expect(await searchCompaniesByName('Nothing')).toEqual([])
|
||||
})
|
||||
|
||||
it('returns an empty array on a 404 from the proxy', async () => {
|
||||
vi.mocked(fetch).mockResolvedValue(new Response('', { status: 404 }))
|
||||
expect(await searchCompaniesByName('Nothing')).toEqual([])
|
||||
})
|
||||
|
||||
it('drops documents without a registration number and caps at the limit', async () => {
|
||||
vi.mocked(fetch).mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
found: 4,
|
||||
hits: [
|
||||
hit('1111111111'),
|
||||
{ document: { ...hit('').document, registrationNumber: '' } },
|
||||
hit('2222222222'),
|
||||
hit('3333333333'),
|
||||
],
|
||||
}),
|
||||
{ status: 200 },
|
||||
),
|
||||
)
|
||||
const docs = await searchCompaniesByName('Testbrand', 2)
|
||||
expect(docs.map((d) => d.registrationNumber)).toEqual(['1111111111', '2222222222'])
|
||||
})
|
||||
|
||||
it('serves a repeated query from the process cache (one upstream call)', async () => {
|
||||
vi.mocked(fetch).mockResolvedValue(
|
||||
new Response(JSON.stringify({ found: 1, hits: [hit('5560360793')] }), { status: 200 }),
|
||||
)
|
||||
await searchCompaniesByName('Testbrand')
|
||||
await searchCompaniesByName('Testbrand')
|
||||
expect(fetch).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -34,7 +34,12 @@ import {
|
||||
readBankIdFlow,
|
||||
setBankIdFlowCookies,
|
||||
} from './lib/bankid-flow-cookie'
|
||||
import { lookupCompanyByOrgNumber, registrationDateToMs } from './lib/lookup'
|
||||
import {
|
||||
lookupCompanyByOrgNumber,
|
||||
registrationDateToMs,
|
||||
searchCompaniesForLookup,
|
||||
} from './lib/lookup'
|
||||
import { COMPANY_SEARCH_MIN_CHARS } from '@/lib/company-lookup/types'
|
||||
import {
|
||||
hasForeignCredential,
|
||||
isUnadoptedPendingAccount,
|
||||
@@ -359,7 +364,7 @@ function toFinancialReportSummary(
|
||||
function handleTicError(
|
||||
error: unknown,
|
||||
log: { error: (msg: string, meta?: unknown) => void } | Console,
|
||||
route: 'lookup' | 'profile',
|
||||
route: 'lookup' | 'profile' | 'search',
|
||||
orgNumber: string,
|
||||
fallbackMessage: string
|
||||
): Response {
|
||||
@@ -1692,6 +1697,36 @@ export const ticExtension: Extension = {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/search',
|
||||
// Onboarding's orgnr field also accepts a company name: one Lens call
|
||||
// returns up to five hits in /lookup's shape so a pick needs no second
|
||||
// call. User is authenticated but may not yet have a company.
|
||||
skipCompanyContext: true,
|
||||
handler: async (request: Request, ctx?) => {
|
||||
const log = ctx?.log ?? console
|
||||
const url = new URL(request.url)
|
||||
const query = (url.searchParams.get('q') ?? '').trim()
|
||||
|
||||
if (query.length < COMPANY_SEARCH_MIN_CHARS) {
|
||||
return NextResponse.json(
|
||||
{ error: `q must be at least ${COMPANY_SEARCH_MIN_CHARS} characters` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
const hits = await searchCompaniesForLookup(query)
|
||||
if (hits.length === 0) {
|
||||
return NextResponse.json({ error: 'Company not found' }, { status: 404 })
|
||||
}
|
||||
return NextResponse.json({ data: hits })
|
||||
} catch (error) {
|
||||
return handleTicError(error, log, 'search', query, 'Failed to search companies')
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
eventHandlers: [],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { searchCompanyByOrgNumber } from './tic-client'
|
||||
import { searchCompaniesByName, searchCompanyByOrgNumber } from './tic-client'
|
||||
import type { TICCompanyDocument } from './tic-types'
|
||||
import type { CompanyLookupResult } from '@/lib/company-lookup/types'
|
||||
import type { CompanyLookupResult, CompanySearchHit } from '@/lib/company-lookup/types'
|
||||
import { normalizeOrgNumber, orgNumberKey } from '@/lib/invariants/org-number'
|
||||
|
||||
/**
|
||||
* Shared org-number → CompanyLookupResult lookup, used by both the /lookup
|
||||
@@ -110,3 +111,44 @@ export async function lookupCompanyByOrgNumber(
|
||||
if (!doc) return null
|
||||
return mapDocumentToLookupResult(doc)
|
||||
}
|
||||
|
||||
/** Maximum hits the onboarding chip row shows for a name search. */
|
||||
export const COMPANY_SEARCH_LIMIT = 5
|
||||
|
||||
/**
|
||||
* Free-text name search mapped to the same shape /lookup returns, one per
|
||||
* hit, active companies first. Empty array means nothing matched. One Lens
|
||||
* call per distinct query; a picked hit reuses its result, so the whole
|
||||
* search-and-pick flow costs the same as an org-number lookup.
|
||||
*/
|
||||
export async function searchCompaniesForLookup(query: string): Promise<CompanySearchHit[]> {
|
||||
const docs = await searchCompaniesByName(query, COMPANY_SEARCH_LIMIT)
|
||||
const hits: CompanySearchHit[] = []
|
||||
for (const doc of docs) {
|
||||
const orgNumber = lensRegistrationToOrgNumber(doc.registrationNumber)
|
||||
if (!orgNumber) continue
|
||||
hits.push({ orgNumber, result: mapDocumentToLookupResult(doc) })
|
||||
}
|
||||
// Stable: ceased companies sink below active ones but keep their rank.
|
||||
return [...hits.filter((h) => !h.result.isCeased), ...hits.filter((h) => h.result.isCeased)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Lens `registrationNumber` → Accounted's 10-digit org number, or null when
|
||||
* the document cannot become a valid one.
|
||||
*
|
||||
* The typed-orgnr path never stores Lens's number (it keeps what the user
|
||||
* typed), so this is the first place a Lens identifier enters settings. Lens
|
||||
* shapes: an AB is 10 digits or 16-prefixed 12; an enskild firma is 16
|
||||
* digits, the century-prefixed personnummer plus a 4-digit serial
|
||||
* (`2002011732750001` for personnummer `0201173275`, see
|
||||
* searchCompanyByOrgNumber). createCompany refuses anything normalizeOrgNumber
|
||||
* rejects, so a hit that does not reduce to a valid number is dropped here
|
||||
* rather than dead-ending the journey at submit.
|
||||
*/
|
||||
export function lensRegistrationToOrgNumber(registrationNumber: string): string | null {
|
||||
const digits = registrationNumber.replace(/\D/g, '')
|
||||
const candidate = /^(18|19|20)\d{14}$/.test(digits) ? digits.slice(0, 12) : digits
|
||||
const key = orgNumberKey(candidate)
|
||||
return key && normalizeOrgNumber(key) ? key : null
|
||||
}
|
||||
|
||||
@@ -197,6 +197,32 @@ export async function searchCompanyByOrgNumber(
|
||||
return match?.document ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Free-text search for companies by name. Returns the ranked documents
|
||||
* (at most `limit`), or an empty array when nothing matched.
|
||||
*
|
||||
* Same Typesense index as `searchCompanyByOrgNumber`, queried on the nested
|
||||
* `names.nameOrIdentifier` field (verified live 2026-09-08; the bare `name`
|
||||
* field is not indexed). The response documents carry the same shape as an
|
||||
* org-number hit, so the caller can map them with the /lookup mapper and a
|
||||
* picked hit never costs a second Lens call.
|
||||
*/
|
||||
export async function searchCompaniesByName(
|
||||
query: string,
|
||||
limit = 5
|
||||
): Promise<TICCompanyDocument[]> {
|
||||
const trimmed = query.trim()
|
||||
if (!trimmed) return []
|
||||
const data = await ticApiFetch<TICCompanyResponse>(
|
||||
`/search-public/companies?q=${encodeURIComponent(trimmed)}&query_by=names.nameOrIdentifier&per_page=${limit}`
|
||||
)
|
||||
if (!data || data.found === 0 || !data.hits?.length) return []
|
||||
return data.hits
|
||||
.map((hit) => hit.document)
|
||||
.filter((doc): doc is TICCompanyDocument => Boolean(doc?.registrationNumber))
|
||||
.slice(0, limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bank accounts for a company. v2 narrows this endpoint to Bankgirot
|
||||
* numbers only (returns `Bankgironumber_Dto[]`); v1's IBAN / plusgiro
|
||||
|
||||
Reference in New Issue
Block a user