local M = {}
local lp = require("assay.lead_provider")
local url = require("assay.url")
local PROVIDER = "registry:companies_house"
local function trim(s) return (tostring(s or ""):gsub("^%s+", ""):gsub("%s+$", "")) end
local STATUS = {
active = "ACTIVE",
open = "ACTIVE",
registered = "ACTIVE",
dissolved = "CLOSED",
closed = "CLOSED",
removed = "CLOSED",
["converted-closed"] = "CLOSED",
liquidation = "LIQUIDATING",
receivership = "LIQUIDATING",
administration = "LIQUIDATING",
["voluntary-arrangement"] = "LIQUIDATING",
["insolvency-proceedings"] = "LIQUIDATING",
}
local function status_of(value)
local raw = trim(value):lower()
if raw == "" then return nil end
return STATUS[raw] or raw:upper():gsub("%-", "_")
end
local function normalize(e, from)
e = e or {}
local addr = e.registered_office_address or e.address or {}
local sic = e.sic_codes or {}
return lp.company(PROVIDER, from, {
registry_id = e.company_number,
name = e.company_name or e.title,
status = status_of(e.company_status),
legal_form = e.type or e.company_type,
jurisdiction = "GB",
city = addr.locality,
country = addr.country,
industry_code = sic[1],
founded_at = e.date_of_creation,
registered_at = e.date_of_creation,
})
end
local function born_at(dob)
if type(dob) ~= "table" or not dob.year or not dob.month then return nil end
return string.format("%04d-%02d", tonumber(dob.year), tonumber(dob.month))
end
local function normalize_officer(o, from)
o = o or {}
local person = lp.person(PROVIDER, from, {
full_name = o.name,
title = o.occupation,
location = o.country_of_residence,
})
person.officer_role = o.officer_role
person.appointed_on = o.appointed_on
person.resigned_on = o.resigned_on
person.nationality = o.nationality
person.born_at = born_at(o.date_of_birth)
person.active = o.resigned_on == nil
return person
end
function M.client(opts)
opts = opts or {}
local api_key = opts.api_key or env.get("COMPANIES_HOUSE_KEY")
if not api_key or trim(api_key) == "" then
error("companies_house: api_key required (opts.api_key or COMPANIES_HOUSE_KEY)")
end
local base_url = (opts.base_url or "https://api.company-information.service.gov.uk"):gsub("/+$", "")
local auth = "Basic " .. base64.encode(trim(api_key) .. ":")
local function api_get(path_str)
local target = base_url .. path_str
local resp = http.get(target, {
headers = { Authorization = auth, Accept = "application/json" },
})
if resp.status == 404 then return nil, target end
if resp.status == 401 or resp.status == 403 then
error("companies_house: GET " .. path_str .. " rejected the api_key (HTTP " .. resp.status .. ")")
end
if resp.status == 429 then
error("companies_house: rate limited (HTTP 429) — 600 requests per 5 minutes")
end
if resp.status ~= 200 then
error("companies_house: GET " .. path_str .. " HTTP " .. resp.status .. ": " .. (resp.body or ""))
end
local ok, parsed = pcall(json.parse, resp.body or "")
if not ok then error("companies_house: " .. path_str .. " returned unparseable JSON") end
return parsed, target
end
local function number_of(company_number)
local id = trim(company_number):gsub("%s", "")
if id == "" then error("companies_house: a company number is required") end
return url.encode(id)
end
local c = {}
function c:search(name, o)
o = o or {}
local body, from = api_get("/search/companies?q=" .. url.encode(trim(name))
.. "&items_per_page=" .. tostring(o.limit or 20))
local out = {}
for _, item in ipairs(body and body.items or {}) do
out[#out + 1] = normalize(item, from)
end
return out
end
function c:get(company_number)
local body, from = api_get("/company/" .. number_of(company_number))
if not body or not body.company_number then return nil end
return normalize(body, from)
end
function c:officers(company_number, o)
o = o or {}
local path_str = "/company/" .. number_of(company_number)
.. "/officers?items_per_page=" .. tostring(o.limit or 35)
if o.register_type then path_str = path_str .. "®ister_type=" .. url.encode(o.register_type) end
local body, from = api_get(path_str)
local out = {}
for _, item in ipairs(body and body.items or {}) do
local person = normalize_officer(item, from)
if not o.active_only or person.active then out[#out + 1] = person end
end
return out
end
return c
end
return M