diff --git a/backend/kesaseteli/applications/tests/test_security_with_non_staff_user.py b/backend/kesaseteli/applications/tests/test_security_with_non_staff_user.py
index 20963a00e0..08407ea77d 100644
--- a/backend/kesaseteli/applications/tests/test_security_with_non_staff_user.py
+++ b/backend/kesaseteli/applications/tests/test_security_with_non_staff_user.py
@@ -862,6 +862,7 @@ def test_company_openly_accessible_to_non_staff_user(user_client):
street_address="Test street 1",
postcode="00100",
city="Test city",
+ organization_type="company",
)
set_company_business_id_to_client(company, user_client)
@@ -877,6 +878,7 @@ def test_company_openly_accessible_to_non_staff_user(user_client):
"name": "Test company",
"postcode": "00100",
"street_address": "Test street 1",
+ "organization_type": "company",
}
diff --git a/backend/kesaseteli/companies/api/v1/serializers.py b/backend/kesaseteli/companies/api/v1/serializers.py
index 8312c18b29..f2f353b031 100644
--- a/backend/kesaseteli/companies/api/v1/serializers.py
+++ b/backend/kesaseteli/companies/api/v1/serializers.py
@@ -10,6 +10,7 @@ class Meta:
"id",
"name",
"business_id",
+ "organization_type",
"company_form",
"industry",
"street_address",
diff --git a/backend/kesaseteli/companies/services.py b/backend/kesaseteli/companies/services.py
index b718d8607a..c5fe709c75 100644
--- a/backend/kesaseteli/companies/services.py
+++ b/backend/kesaseteli/companies/services.py
@@ -9,6 +9,7 @@
from companies.models import Company
from companies.tests.data.company_data import DUMMY_ORG_ROLES
from shared.oidc.utils import get_organization_roles
+from shared.ytj.exceptions import YTJNotFoundError, YTJParseError
from shared.ytj.ytj_client import YTJClient
LOGGER = logging.getLogger(__name__)
@@ -21,9 +22,15 @@ def get_or_create_company_using_company_data(
Get or create a company instance using a dict of the company data and
attach the ytj_data json for the instance.
"""
- company, _ = Company.objects.get_or_create(
+ company, created = Company.objects.get_or_create(
**company_data, defaults={"ytj_json": ytj_data}
)
+ if created:
+ LOGGER.info(
+ f"Created company {company.name} ({company.business_id}) using YTJ data"
+ )
+ else:
+ LOGGER.info(f"Found company {company.name} ({company.business_id})")
return company
@@ -44,9 +51,13 @@ def get_or_create_company_with_name_and_business_id(
business_id: str,
) -> Company:
"""
- Get or create a company instance using a dict of the company data and
- attach the ytj_data json for the instance.
+ Get or create a company instance using the company name and business_id.
+ This is used as a fallback when YTJ data is not available.
"""
+ LOGGER.info(
+ f"Creating/getting company {name} ({business_id}) "
+ "using fallback method (no YTJ data)"
+ )
company, _ = Company.objects.get_or_create(
name=name,
business_id=business_id,
@@ -78,6 +89,10 @@ def handle_mock_company(request: HttpRequest):
if not company:
company = create_mock_company_and_store_org_roles_in_session(request)
+ LOGGER.info(
+ f"Using mock company data: {company.name} ({company.business_id}). "
+ f"Organization roles: {org_roles}"
+ )
return company
@@ -105,7 +120,10 @@ def get_or_create_company_using_organization_roles(request: HttpRequest) -> Comp
try:
organization_roles = get_organization_roles(request)
- except RequestException:
+ except RequestException as e:
+ LOGGER.error(
+ f"Unable to fetch organization roles from eauthorizations API: {e}"
+ )
raise NotFound(
detail="Unable to fetch organization roles from eauthorizations API"
)
@@ -117,13 +135,15 @@ def get_or_create_company_using_organization_roles(request: HttpRequest) -> Comp
if not company:
try:
company = get_or_create_company_from_ytj_api(business_id)
- except ValueError:
+ except YTJNotFoundError as e:
+ LOGGER.warning(f"YTJ API error for business_id {business_id}: {str(e)}")
raise NotFound(detail="Could not handle the response from YTJ API")
- except RequestException:
- LOGGER.warning(
- "YTJ API is under heavy load or no company found with the given"
- f" business id: {business_id}"
- )
+ except RequestException as e:
+ LOGGER.error(f"YTJ API connection error for business_id {business_id}: {e}")
+ name = organization_roles.get("name")
+ company = get_or_create_company_with_name_and_business_id(name, business_id)
+ except YTJParseError as e:
+ LOGGER.error(f"YTJ API parsing error for business_id {business_id}: {e}")
name = organization_roles.get("name")
company = get_or_create_company_with_name_and_business_id(name, business_id)
diff --git a/backend/kesaseteli/companies/tests/data/company_data.py b/backend/kesaseteli/companies/tests/data/company_data.py
index 31e63cc134..a2f3478bbc 100644
--- a/backend/kesaseteli/companies/tests/data/company_data.py
+++ b/backend/kesaseteli/companies/tests/data/company_data.py
@@ -2,7 +2,7 @@
"id": "8c0c7a56-cb98-4c31-87ca-6f1853253986",
"name": "I. Haanpää Oy",
"business_id": "0877830-0",
- "company_form": "OY",
+ "company_form": "Osakeyhtiö",
"industry": "Taloustavaroiden vähittäiskauppa",
"street_address": "Vasaratie 4 A 3",
"postcode": "65350",
@@ -19,714 +19,85 @@
DUMMY_YTJ_RESPONSE = {
- "type": "fi.prh.opendata.tr",
- "version": "1",
- "totalResults": -1,
- "resultsFrom": 0,
- "previousResultsUri": None,
- "nextResultsUri": None,
- "exceptionNoticeUri": None,
- "results": [
+ "companies": [
{
- "businessId": "0877830-0",
- "name": "I. Haanpää Oy",
- "registrationDate": "1992-01-29",
- "companyForm": "OY",
- "detailsUri": "http://avoindata.prh.fi/opendata/tr/v1/0877830-0",
- "bisDetailsUri": "http://avoindata.prh.fi/opendata/bis/v1/0877830-0",
- "language": "FI",
- "latestRegistrationDate": "2021-01-12",
- "checkDate": None,
- "names": [
+ "businessId": {
+ "value": "0877830-0",
+ "registrationDate": "1992-01-29",
+ "source": "3",
+ },
+ "companyForms": [
{
- "order": 0,
- "name": "I. Haanpää Oy",
+ "type": "OY",
+ "descriptions": [
+ {"languageCode": "1", "description": "Osakeyhtiö"},
+ {"languageCode": "2", "description": "Aktiebolag"},
+ {"languageCode": "3", "description": "Limited company"},
+ ],
"registrationDate": "1999-03-31",
- "endDate": None,
- "language": None,
- }
- ],
- "auxiliaryNames": [],
- "companyForms": [{"type": "OY", "registrationDate": "1999-03-31"}],
- "addresses": [
- {
- "street": "Vasaratie 4 A 3",
- "postCode": "65350",
- "type": 2,
- "city": "Vaasa",
- "country": "FI",
- "website": None,
- "phone": "0400 665254",
- "fax": None,
- "registrationDate": "2020-07-22",
- "endDate": None,
+ "version": 1,
+ "source": "1",
}
],
- "publicNotices": [
- {
- "recordNumber": "2020/55730X",
- "registrationDate": "2021-01-01",
- "typeOfRegistration": "TA",
- "entryCodes": ["TASE"],
- "detailsUri": (
- "http://avoindata.prh.fi/opendata/tr/v1/publicnotices/2020/55730X"
- ),
- },
- {
- "recordNumber": "2020/290401",
- "registrationDate": "2020-06-15",
- "typeOfRegistration": "M",
- "entryCodes": [""],
- "detailsUri": (
- "http://avoindata.prh.fi/opendata/tr/v1/publicnotices/2020/290401"
- ),
- },
- {
- "recordNumber": "2019/53986T",
- "registrationDate": "2019-12-05",
- "typeOfRegistration": "TA",
- "entryCodes": ["TASE"],
- "detailsUri": (
- "http://avoindata.prh.fi/opendata/tr/v1/publicnotices/2019/53986T"
- ),
- },
- {
- "recordNumber": "2019/250695",
- "registrationDate": "2019-07-09",
- "typeOfRegistration": "M",
- "entryCodes": ["HAL"],
- "detailsUri": (
- "http://avoindata.prh.fi/opendata/tr/v1/publicnotices/2019/250695"
- ),
- },
- {
- "recordNumber": "2018/50210V",
- "registrationDate": "2018-10-19",
- "typeOfRegistration": "TA",
- "entryCodes": ["TASE"],
- "detailsUri": (
- "http://avoindata.prh.fi/opendata/tr/v1/publicnotices/2018/50210V"
- ),
- },
- {
- "recordNumber": "2017/46683V",
- "registrationDate": "2017-10-21",
- "typeOfRegistration": "TA",
- "entryCodes": ["TASE"],
- "detailsUri": (
- "http://avoindata.prh.fi/opendata/tr/v1/publicnotices/2017/46683V"
- ),
- },
- {
- "recordNumber": "2016/86280V",
- "registrationDate": "2016-11-03",
- "typeOfRegistration": "TA",
- "entryCodes": ["TASE"],
- "detailsUri": (
- "http://avoindata.prh.fi/opendata/tr/v1/publicnotices/2016/86280V"
- ),
- },
- {
- "recordNumber": "2015/739492",
- "registrationDate": "2015-11-04",
- "typeOfRegistration": "M",
- "entryCodes": ["TILTAR"],
- "detailsUri": (
- "http://avoindata.prh.fi/opendata/tr/v1/publicnotices/2015/739492"
- ),
- },
- {
- "recordNumber": "2015/84509U",
- "registrationDate": "2015-10-08",
- "typeOfRegistration": "TA",
- "entryCodes": ["TASE"],
- "detailsUri": (
- "http://avoindata.prh.fi/opendata/tr/v1/publicnotices/2015/84509U"
- ),
- },
- ],
- "registeredOffices": [
- {
- "registeredOffice": "Vaasa",
- "language": "EN",
- "registrationDate": "2005-01-25",
- "endDate": None,
- },
- {
- "registeredOffice": "Vasa",
- "language": "SE",
- "registrationDate": "2005-01-25",
- "endDate": None,
- },
- {
- "registeredOffice": "Vaasa",
- "language": "FI",
- "registrationDate": "2005-01-25",
- "endDate": None,
- },
- ],
- }
- ],
-}
-
-
-DUMMY_YTJ_BUSINESS_DETAILS_RESPONSE = {
- "type": "fi.prh.opendata.bis",
- "version": "1",
- "totalResults": -1,
- "resultsFrom": 0,
- "previousResultsUri": None,
- "nextResultsUri": None,
- "exceptionNoticeUri": None,
- "results": [
- {
- "businessId": "0877830-0",
- "name": "I. Haanpää Oy",
- "registrationDate": "1992-03-03",
- "companyForm": "OY",
- "detailsUri": None,
- "liquidations": [],
"names": [
{
- "order": 0,
- "version": 1,
"name": "I. Haanpää Oy",
+ "type": "1",
"registrationDate": "1999-03-31",
- "endDate": None,
- "source": 1,
+ "version": 1,
+ "source": "1",
},
{
- "order": 0,
- "version": 2,
"name": "I. Haanpää Ky",
+ "type": "2",
"registrationDate": "1992-01-29",
"endDate": "1999-03-30",
- "source": 1,
+ "version": 2,
+ "source": "1",
},
],
- "auxiliaryNames": [],
"addresses": [
{
- "careOf": None,
+ "type": 1,
"street": "Vasaratie 4 A 3",
"postCode": "65350",
- "type": 1,
- "version": 2,
- "city": "VAASA",
- "country": None,
- "registrationDate": "2013-12-16",
- "endDate": "2013-12-18",
- "language": "FI",
- "source": 0,
+ "postOffices": [
+ {"city": "Vaasa", "languageCode": "1"},
+ {"city": "Vasa", "languageCode": "2"},
+ ],
+ "registrationDate": "2020-07-22",
+ "source": "1",
},
{
- "careOf": None,
+ "type": 2,
"street": "PL 327",
"postCode": "65101",
- "type": 2,
- "version": 2,
- "city": "VAASA",
- "country": None,
+ "postOffices": [
+ {"city": "Vaasa", "languageCode": "1"},
+ ],
"registrationDate": "2013-12-16",
"endDate": "2020-07-21",
- "language": "FI",
- "source": 0,
- },
- {
- "careOf": None,
- "street": "",
- "postCode": None,
- "type": 1,
- "version": 1,
- "city": None,
- "country": None,
- "registrationDate": "2013-12-18",
- "endDate": None,
- "language": "FI",
- "source": 0,
- },
- {
- "careOf": None,
- "street": "Vasaratie 4 A 3",
- "postCode": "65350",
- "type": 2,
- "version": 1,
- "city": "VAASA",
- "country": None,
- "registrationDate": "2020-07-22",
- "endDate": None,
- "language": "FI",
- "source": 0,
- },
- ],
- "companyForms": [
- {
- "version": 1,
- "name": "Osakeyhtiö",
- "type": "OY",
- "registrationDate": "1999-03-31",
- "endDate": None,
- "language": "FI",
- "source": 1,
- },
- {
- "version": 1,
- "name": "Aktiebolag",
- "type": "AB",
- "registrationDate": "1999-03-31",
- "endDate": None,
- "language": "SE",
- "source": 1,
- },
- {
- "version": 1,
- "name": "Limited company",
- "type": None,
- "registrationDate": "1999-03-31",
- "endDate": None,
- "language": "EN",
- "source": 1,
- },
- ],
- "businessLines": [
- {
- "order": 0,
- "version": 1,
- "code": "47594",
- "name": "Retail sale of household articles",
- "registrationDate": "2007-12-31",
- "endDate": None,
- "language": "EN",
- "source": 2,
- },
- {
- "order": 0,
- "version": 1,
- "code": "47594",
- "name": "Taloustavaroiden vähittäiskauppa",
- "registrationDate": "2007-12-31",
- "endDate": None,
- "language": "FI",
- "source": 2,
- },
- {
- "order": 0,
- "version": 1,
- "code": "47594",
- "name": "Specialiserad butikshandel med hushållsartiklar",
- "registrationDate": "2007-12-31",
- "endDate": None,
- "language": "SE",
- "source": 2,
- },
- ],
- "languages": [
- {
- "version": 1,
- "name": "Finnish",
- "registrationDate": "1992-03-03",
- "endDate": None,
- "language": "EN",
- "source": 0,
- },
- {
- "version": 1,
- "name": "Suomi",
- "registrationDate": "1992-03-03",
- "endDate": None,
- "language": "FI",
- "source": 0,
- },
- {
- "version": 1,
- "name": "Finska",
- "registrationDate": "1992-03-03",
- "endDate": None,
- "language": "SE",
- "source": 0,
- },
- ],
- "registedOffices": [
- {
- "order": 0,
- "version": 1,
- "name": "VAASA",
- "registrationDate": "2005-01-25",
- "endDate": None,
- "language": "EN",
- "source": 0,
- },
- {
- "order": 0,
- "version": 1,
- "name": "VAASA",
- "registrationDate": "2005-01-25",
- "endDate": None,
- "language": "FI",
- "source": 0,
- },
- {
- "order": 0,
- "version": 1,
- "name": "VASA",
- "registrationDate": "2005-01-25",
- "endDate": None,
- "language": "SE",
- "source": 0,
- },
- {
- "order": 0,
- "version": 2,
- "name": "VÄHÄKYRÖ",
- "registrationDate": "1999-03-31",
- "endDate": "2005-01-24",
- "language": "EN",
- "source": 0,
- },
- {
- "order": 0,
- "version": 2,
- "name": "VÄHÄKYRÖ",
- "registrationDate": "1999-03-31",
- "endDate": "2005-01-24",
- "language": "FI",
- "source": 0,
- },
- {
- "order": 0,
- "version": 2,
- "name": "LILLKYRO",
- "registrationDate": "1999-03-31",
- "endDate": "2005-01-24",
- "language": "SE",
- "source": 0,
- },
- ],
- "contactDetails": [
- {
- "version": 1,
- "value": "0400 665254",
- "type": "Matkapuhelin",
- "registrationDate": "2005-01-11",
- "endDate": None,
- "language": "FI",
- "source": 0,
- },
- {
- "version": 1,
- "value": "0400 665254",
- "type": "Mobiltelefon",
- "registrationDate": "2005-01-11",
- "endDate": None,
- "language": "SE",
- "source": 0,
- },
- {
- "version": 1,
- "value": "0400 665254",
- "type": "Mobile phone",
- "registrationDate": "2005-01-11",
- "endDate": None,
- "language": "EN",
- "source": 0,
- },
- {
- "version": 2,
- "value": "06 3129811",
- "type": "Puhelin",
- "registrationDate": "2005-01-11",
- "endDate": "2020-07-21",
- "language": "FI",
- "source": 0,
- },
- {
- "version": 2,
- "value": "06 3129811",
- "type": "Telefon",
- "registrationDate": "2005-01-11",
- "endDate": "2020-07-21",
- "language": "SE",
- "source": 0,
- },
- {
- "version": 2,
- "value": "06 3129811",
- "type": "Telephone",
- "registrationDate": "2005-01-11",
- "endDate": "2020-07-21",
- "language": "EN",
- "source": 0,
- },
- {
- "version": 2,
- "value": "06 9129812",
- "type": "Faksi",
- "registrationDate": "2005-01-11",
- "endDate": "2020-07-21",
- "language": "FI",
- "source": 0,
- },
- {
- "version": 2,
- "value": "06 9129812",
- "type": "Fax",
- "registrationDate": "2005-01-11",
- "endDate": "2020-07-21",
- "language": "SE",
- "source": 0,
- },
- {
- "version": 2,
- "value": "06 9129812",
- "type": "Fax",
- "registrationDate": "2005-01-11",
- "endDate": "2020-07-21",
- "language": "EN",
- "source": 0,
- },
- {
- "version": 1,
- "value": "",
- "type": "Puhelin",
- "registrationDate": "2020-07-22",
- "endDate": None,
- "language": "FI",
- "source": 0,
- },
- {
- "version": 1,
- "value": "",
- "type": "Telefon",
- "registrationDate": "2020-07-22",
- "endDate": None,
- "language": "SE",
- "source": 0,
- },
- {
- "version": 1,
- "value": "",
- "type": "Telephone",
- "registrationDate": "2020-07-22",
- "endDate": None,
- "language": "EN",
- "source": 0,
- },
- {
- "version": 1,
- "value": "",
- "type": "Faksi",
- "registrationDate": "2020-07-22",
- "endDate": None,
- "language": "FI",
- "source": 0,
- },
- {
- "version": 1,
- "value": "",
- "type": "Fax",
- "registrationDate": "2020-07-22",
- "endDate": None,
- "language": "SE",
- "source": 0,
- },
- {
- "version": 1,
- "value": "",
- "type": "Fax",
- "registrationDate": "2020-07-22",
- "endDate": None,
- "language": "EN",
- "source": 0,
- },
- ],
- "registeredEntries": [
- {
- "authority": 1,
- "register": 4,
- "status": 1,
- "registrationDate": "1992-03-03",
- "endDate": None,
- "statusDate": "2020-06-03",
- "language": "FI",
- "description": "Rekisterissä",
- },
- {
- "authority": 1,
- "register": 4,
- "status": 1,
- "registrationDate": "1992-03-03",
- "endDate": None,
- "statusDate": "2020-06-03",
- "language": "SE",
- "description": "Registrerad",
- },
- {
- "authority": 1,
- "register": 4,
- "status": 1,
- "registrationDate": "1992-03-03",
- "endDate": None,
- "statusDate": "2020-06-03",
- "language": "EN",
- "description": "Registered",
- },
- {
- "authority": 1,
- "register": 5,
- "status": 1,
- "registrationDate": "1995-03-01",
- "endDate": None,
- "statusDate": "2017-03-16",
- "language": "FI",
- "description": "Rekisterissä",
- },
- {
- "authority": 1,
- "register": 5,
- "status": 1,
- "registrationDate": "1995-03-01",
- "endDate": None,
- "statusDate": "2017-03-16",
- "language": "SE",
- "description": "Registrerad",
- },
- {
- "authority": 1,
- "register": 5,
- "status": 1,
- "registrationDate": "1995-03-01",
- "endDate": None,
- "statusDate": "2017-03-16",
- "language": "EN",
- "description": "Registered",
- },
- {
- "authority": 1,
- "register": 6,
- "status": 1,
- "registrationDate": "1994-06-01",
- "endDate": None,
- "statusDate": "2008-07-21",
- "language": "FI",
- "description": "Liiketoiminnasta arvonlisäverovelvollinen",
- },
- {
- "authority": 1,
- "register": 6,
- "status": 1,
- "registrationDate": "1994-06-01",
- "endDate": None,
- "statusDate": "2008-07-21",
- "language": "SE",
- "description": "Momsskyldig för rörelseverksamhet",
- },
- {
- "authority": 1,
- "register": 6,
- "status": 1,
- "registrationDate": "1994-06-01",
- "endDate": None,
- "statusDate": "2008-07-21",
- "language": "EN",
- "description": "VAT-liable for business activity",
- },
- {
- "authority": 1,
- "register": 6,
- "status": 1,
- "registrationDate": "2008-08-01",
- "endDate": None,
- "statusDate": "2008-07-21",
- "language": "FI",
- "description": "Kiinteistön käyttöoikeuden luovuttamisesta",
- },
- {
- "authority": 1,
- "register": 6,
- "status": 1,
- "registrationDate": "2008-08-01",
- "endDate": None,
- "statusDate": "2008-07-21",
- "language": "SE",
- "description": (
- "För överlåtelse av nyttjanderätten till en fastighet"
- ),
- },
- {
- "authority": 1,
- "register": 6,
- "status": 1,
- "registrationDate": "2008-08-01",
- "endDate": None,
- "statusDate": "2008-07-21",
- "language": "EN",
- "description": (
- "VAT-obliged for the transfer of rights to use immovable"
- " property"
- ),
- },
- {
- "authority": 1,
- "register": 7,
- "status": 1,
- "registrationDate": "1992-03-01",
- "endDate": None,
- "statusDate": "2001-03-30",
- "language": "FI",
- "description": "Rekisterissä",
- },
- {
- "authority": 1,
- "register": 7,
- "status": 1,
- "registrationDate": "1992-03-01",
- "endDate": None,
- "statusDate": "2001-03-30",
- "language": "SE",
- "description": "Registrerad",
- },
- {
- "authority": 1,
- "register": 7,
- "status": 1,
- "registrationDate": "1992-03-01",
- "endDate": None,
- "statusDate": "2001-03-30",
- "language": "EN",
- "description": "Registered",
- },
- {
- "authority": 2,
- "register": 1,
- "status": 1,
- "registrationDate": "1992-01-29",
- "endDate": None,
- "statusDate": "1992-01-29",
- "language": "FI",
- "description": "Rekisterissä",
- },
- {
- "authority": 2,
- "register": 1,
- "status": 1,
- "registrationDate": "1992-01-29",
- "endDate": None,
- "statusDate": "1992-01-29",
- "language": "SE",
- "description": "Registrerad",
- },
- {
- "authority": 2,
- "register": 1,
- "status": 1,
- "registrationDate": "1992-01-29",
- "endDate": None,
- "statusDate": "1992-01-29",
- "language": "EN",
- "description": "Registered",
+ "source": "1",
},
],
- "businessIdChanges": [],
+ "mainBusinessLine": {
+ "descriptions": [
+ {
+ "languageCode": "1",
+ "description": "Taloustavaroiden vähittäiskauppa",
+ },
+ {
+ "languageCode": "2",
+ "description": "Specialiserad butikshandel med hushållsartiklar",
+ },
+ {
+ "languageCode": "3",
+ "description": "Retail sale of household articles",
+ },
+ ],
+ "registrationDate": "2007-12-31",
+ "source": "2",
+ },
}
- ],
+ ]
}
diff --git a/backend/kesaseteli/companies/tests/test_company_api.py b/backend/kesaseteli/companies/tests/test_company_api.py
index 5d2f410876..c82c0114c0 100644
--- a/backend/kesaseteli/companies/tests/test_company_api.py
+++ b/backend/kesaseteli/companies/tests/test_company_api.py
@@ -10,7 +10,6 @@
from companies.models import Company
from companies.tests.data.company_data import (
DUMMY_COMPANY_DATA,
- DUMMY_YTJ_BUSINESS_DETAILS_RESPONSE,
DUMMY_YTJ_RESPONSE,
)
@@ -19,18 +18,11 @@ def get_company_api_url():
return "/v1/company/"
-def set_up_mock_requests(
- ytj_response: dict, business_details_response: dict, requests_mock
-):
+def set_up_mock_requests(ytj_response: dict, requests_mock):
"""
Set up the mock responses.
"""
- business_id = ytj_response["results"][0]["businessId"]
- ytj_url = f"{settings.YTJ_BASE_URL}/{business_id}"
- business_details_url = ytj_response["results"][0]["bisDetailsUri"]
-
- requests_mock.get(ytj_url, json=ytj_response)
- requests_mock.get(business_details_url, json=business_details_response)
+ requests_mock.get(f"{settings.YTJ_BASE_URL}/companies", json=ytj_response)
@pytest.mark.django_db
@@ -56,9 +48,10 @@ def test_get_mock_company_not_found_from_ytj(api_client):
for field in [
f
for f in Company._meta.fields
- if f.name not in ["id", "name", "business_id", "organization_type", "ytj_json"]
+ if f.name
+ not in ["id", "name", "business_id", "ytj_json", "created_at", "modified_at"]
]:
- assert response.data[field.name] == ""
+ assert response.data.get(field.name, "") == ""
@pytest.mark.django_db
@@ -68,7 +61,7 @@ def test_get_mock_company_not_found_from_ytj(api_client):
EAUTHORIZATIONS_CLIENT_ID="test",
EAUTHORIZATIONS_CLIENT_SECRET="test",
)
-def test_get_company_organization_roles_error(api_client, requests_mock, user):
+def test_get_company_organization_roles_error(api_client, requests_mock, user, caplog):
session = api_client.session
session.pop("organization_roles")
session.save()
@@ -83,12 +76,13 @@ def test_get_company_organization_roles_error(api_client, requests_mock, user):
response.data["detail"]
== "Unable to fetch organization roles from eauthorizations API"
)
+ assert "Unable to fetch organization roles from eauthorizations API" in caplog.text
@pytest.mark.django_db
@override_settings(
NEXT_PUBLIC_MOCK_FLAG=False,
- YTJ_BASE_URL="http://example.com",
+ YTJ_BASE_URL="http://example.com/v3",
)
def test_get_company_from_ytj(api_client, requests_mock):
session = api_client.session
@@ -97,9 +91,7 @@ def test_get_company_from_ytj(api_client, requests_mock):
Company.objects.all().delete()
- set_up_mock_requests(
- DUMMY_YTJ_RESPONSE, DUMMY_YTJ_BUSINESS_DETAILS_RESPONSE, requests_mock
- )
+ set_up_mock_requests(DUMMY_YTJ_RESPONSE, requests_mock)
org_roles_json = {
"name": "Activenakusteri Oy",
@@ -120,7 +112,8 @@ def test_get_company_from_ytj(api_client, requests_mock):
assert response.data == company_data
assert (
- response.data["business_id"] == DUMMY_YTJ_RESPONSE["results"][0]["businessId"]
+ response.data["business_id"]
+ == DUMMY_YTJ_RESPONSE["companies"][0]["businessId"]["value"]
)
for field in ["company_form", "industry", "street_address", "postcode", "city"]:
assert response.data[field]
@@ -129,7 +122,7 @@ def test_get_company_from_ytj(api_client, requests_mock):
@pytest.mark.django_db
@override_settings(
NEXT_PUBLIC_MOCK_FLAG=False,
- YTJ_BASE_URL="http://example.com",
+ YTJ_BASE_URL="http://example.com/v3",
)
def test_get_company_not_found_from_ytj(api_client, requests_mock, user):
matcher = re.compile(re.escape(settings.YTJ_BASE_URL))
@@ -154,23 +147,23 @@ def test_get_company_not_found_from_ytj(api_client, requests_mock, user):
for field in [
f
for f in Company._meta.fields
- if f.name not in ["id", "name", "business_id", "organization_type", "ytj_json"]
+ if f.name
+ not in ["id", "name", "business_id", "ytj_json", "created_at", "modified_at"]
]:
- assert response.data[field.name] == ""
+ assert response.data.get(field.name, "") == ""
@pytest.mark.django_db
@override_settings(
NEXT_PUBLIC_MOCK_FLAG=False,
- YTJ_BASE_URL="http://example.com",
+ YTJ_BASE_URL="http://example.com/v3",
)
-def test_get_company_from_ytj_invalid_response(api_client, requests_mock, user):
+def test_get_company_from_ytj_invalid_response(api_client, requests_mock, user, caplog):
ytj_reponse = copy.deepcopy(DUMMY_YTJ_RESPONSE)
- ytj_reponse["results"][0]["addresses"] = []
+ # Simulate invalid data: empty companies list or no addresses
+ ytj_reponse["companies"][0]["addresses"] = []
- set_up_mock_requests(
- ytj_reponse, DUMMY_YTJ_BUSINESS_DETAILS_RESPONSE, requests_mock
- )
+ set_up_mock_requests(ytj_reponse, requests_mock)
org_roles_json = {
"name": "Activenakusteri Oy",
@@ -184,5 +177,15 @@ def test_get_company_from_ytj_invalid_response(api_client, requests_mock, user):
):
response = api_client.get(get_company_api_url())
- assert response.status_code == 404
- assert response.data["detail"] == "Could not handle the response from YTJ API"
+ assert response.status_code == 200
+ assert response.data["name"] == org_roles_json["name"]
+ assert response.data["business_id"] == org_roles_json["identifier"]
+ assert "YTJ API parsing error for business_id" in caplog.text
+
+ for field in [
+ f
+ for f in Company._meta.fields
+ if f.name
+ not in ["id", "name", "business_id", "ytj_json", "created_at", "modified_at"]
+ ]:
+ assert response.data.get(field.name, "") == ""
diff --git a/backend/kesaseteli/kesaseteli/settings.py b/backend/kesaseteli/kesaseteli/settings.py
index 90cbe34a73..d059be17b1 100644
--- a/backend/kesaseteli/kesaseteli/settings.py
+++ b/backend/kesaseteli/kesaseteli/settings.py
@@ -58,7 +58,7 @@
CSRF_COOKIE_DOMAIN=(str, "localhost"),
CSRF_TRUSTED_ORIGINS=(list, []),
CSRF_COOKIE_NAME=(str, "yjdhcsrftoken"),
- YTJ_BASE_URL=(str, "http://avoindata.prh.fi/opendata/tr/v1"),
+ YTJ_BASE_URL=(str, "https://avoindata.prh.fi/opendata-ytj-api/v3"),
YTJ_TIMEOUT=(int, 30),
NEXT_PUBLIC_MOCK_FLAG=(bool, False),
SESSION_COOKIE_AGE=(int, 60 * 60 * 2),
diff --git a/backend/shared/shared/ytj/README.md b/backend/shared/shared/ytj/README.md
new file mode 100644
index 0000000000..ea82e0e1b4
--- /dev/null
+++ b/backend/shared/shared/ytj/README.md
@@ -0,0 +1,52 @@
+# YTJ Client (V3)
+
+This module provides a pythonic interface to the [Finnish Trade Register (YTJ) Open Data API V3](https://avoindata.prh.fi/opendata-ytj-api/v3/).
+
+## Overview
+
+The client allows fetching company details using a Business ID (Y-tunnus). It handles the complexity of the V3 API response structure, parsing it into strongly-typed dataclasses and providing helper properties to access preferred values (e.g., prioritizing Finnish names and addresses).
+
+## Key Components
+
+- **`YTJClient`** (`ytj_client.py`): The main entry point. Use `get_company_info_with_business_id(business_id)` to get raw data or `get_company_data_from_ytj_data(data)` to extract a simplified dictionary for the application.
+- **`YTJCompany`** (`ytj_dataclasses.py`): The root dataclass representing the API response. It contains logic for:
+ - Language prioritization (FI > SV > EN).
+ - Address prioritization (Visiting > Postal).
+ - Data parsing from JSON.
+- **`ytj_schema.yaml`**: A local copy of the OpenAPI V3 schema used for reference and validation during development.
+
+## Usage
+
+```python
+from shared.ytj.ytj_client import YTJClient
+
+client = YTJClient()
+
+# 1. Fetch data
+# Returns a dict matching the JSON response
+data = client.get_company_info_with_business_id("1234567-8")
+
+# 2. Extract simplified company info
+# Returns:
+# {
+# "name": "Company Oy",
+# "business_id": "1234567-8",
+# "company_form": "Osakeyhtiö",
+# "industry": "Ohjelmistokehitys",
+# "street_address": "Katu 1",
+# "postcode": "00100",
+# "city": "HELSINKI"
+# }
+company_info = YTJClient.get_company_data_from_ytj_data(data)
+```
+
+## Configuration
+
+Required `django.conf.settings`:
+- `YTJ_BASE_URL`: Base URL for the V3 API (e.g., `https://avoindata.prh.fi/opendata-ytj-api/v3`).
+- `YTJ_TIMEOUT`: Request timeout in seconds.
+
+## Development
+
+The dataclasses in `ytj_dataclasses.py` are based on the OpenAPI specification.
+See `ytj_schema.yaml` in this directory for the full schema definition.
diff --git a/backend/shared/shared/ytj/exceptions.py b/backend/shared/shared/ytj/exceptions.py
new file mode 100644
index 0000000000..4b8fd3816a
--- /dev/null
+++ b/backend/shared/shared/ytj/exceptions.py
@@ -0,0 +1,10 @@
+class YTJNotFoundError(ValueError):
+ """
+ Exception raised when no company is found in YTJ.
+ """
+
+
+class YTJParseError(ValueError):
+ """
+ Exception raised when YTJ data cannot be parsed (missing required fields).
+ """
diff --git a/backend/shared/shared/ytj/tests/test_ytj_client_v3.py b/backend/shared/shared/ytj/tests/test_ytj_client_v3.py
new file mode 100644
index 0000000000..96aee1d537
--- /dev/null
+++ b/backend/shared/shared/ytj/tests/test_ytj_client_v3.py
@@ -0,0 +1,317 @@
+from unittest.mock import Mock, patch
+
+import pytest
+from django.conf import settings
+
+from shared.ytj.exceptions import YTJNotFoundError
+from shared.ytj.ytj_client import YTJClient
+from shared.ytj.ytj_dataclasses import (
+ YTJAddressType,
+ YTJCompany,
+ YTJLanguageCode,
+ YTJNameType,
+)
+
+# Sample Nokia Data (partial, reconstructed/mocked based on investigation)
+MOCK_YTJ_RESPONSE = {
+ "totalResults": 1,
+ "companies": [
+ {
+ "businessId": {
+ "value": "0112038-9",
+ "registrationDate": "1978-03-15",
+ "source": "3",
+ },
+ "companyForms": [
+ {
+ "type": "OYJ",
+ "descriptions": [
+ {"languageCode": "1", "description": "Julkinen osakeyhtiö"},
+ {"languageCode": "3", "description": "Public limited company"},
+ ],
+ }
+ ],
+ "names": [
+ {
+ "name": "Nokia Oyj",
+ "type": "1",
+ "registrationDate": "1997-09-01",
+ "version": 1,
+ "source": "1",
+ },
+ {
+ "name": "Oy Nokia Ab",
+ "type": "1",
+ "registrationDate": "1966-06-10",
+ "endDate": "1997-08-31",
+ "version": 2,
+ "source": "1",
+ },
+ ],
+ "mainBusinessLine": {
+ "descriptions": [
+ {"languageCode": "3", "description": "Activities of head offices"},
+ {"languageCode": "1", "description": "Pääkonttorien toiminta"},
+ ]
+ },
+ "addresses": [
+ {
+ "type": 1,
+ "street": "Karakaari 7",
+ "postCode": "02610",
+ "postOffices": [
+ {"city": "ESBO", "languageCode": "2"},
+ {"city": "ESPOO", "languageCode": "1"},
+ ],
+ }
+ ],
+ }
+ ],
+}
+
+
+@pytest.fixture
+def ytj_client():
+ return YTJClient()
+
+
+@patch("requests.get")
+def test_get_company_info_with_business_id(mock_get, ytj_client):
+ mock_response = Mock()
+ mock_response.json.return_value = MOCK_YTJ_RESPONSE
+ mock_response.raise_for_status.return_value = None
+ mock_get.return_value = mock_response
+
+ business_id = "0112038-9"
+ result = ytj_client.get_company_info_with_business_id(business_id)
+
+ # Check that requests.get was called with correct URL and params
+ mock_get.assert_called_once()
+ args, kwargs = mock_get.call_args
+ assert args[0] == f"{settings.YTJ_BASE_URL}/companies"
+ assert kwargs["params"] == {"businessId": business_id}
+ assert kwargs["timeout"] == settings.YTJ_TIMEOUT
+
+ assert result == MOCK_YTJ_RESPONSE
+
+
+@patch("requests.get")
+def test_get_company_info_not_found(mock_get, ytj_client):
+ mock_response = Mock()
+ mock_response.json.return_value = {"companies": []}
+ mock_response.raise_for_status.return_value = None
+ mock_get.return_value = mock_response
+
+ business_id = "0000000-0"
+ with pytest.raises(
+ YTJNotFoundError,
+ match=f"No company found in YTJ for business ID: {business_id}",
+ ):
+ ytj_client.get_company_info_with_business_id(business_id)
+
+
+def test_get_company_data_from_ytj_data():
+ data = YTJClient.get_company_data_from_ytj_data(MOCK_YTJ_RESPONSE)
+
+ assert data["name"] == "Nokia Oyj"
+ assert data["business_id"] == "0112038-9"
+ assert data["company_form"] == "Julkinen osakeyhtiö" # FI description
+ assert data["industry"] == "Pääkonttorien toiminta" # FI description
+ assert data["street_address"] == "Karakaari 7"
+ assert data["postcode"] == "02610"
+ assert data["city"] == "ESPOO"
+
+
+def test_get_company_data_missing_fields():
+ # Test minimal data or missing fields handling
+ minimal_data = {
+ "companies": [
+ {
+ "businessId": {"value": "1234567-8"},
+ "names": [{"name": "Test Company", "type": YTJNameType.MAIN_NAME}],
+ "addresses": [
+ {
+ "type": YTJAddressType.VISITING_ADDRESS,
+ "street": "Test street",
+ "postCode": "00100",
+ "postOffices": [
+ {"city": "Helsinki", "languageCode": YTJLanguageCode.FI}
+ ],
+ }
+ ],
+ "mainBusinessLine": {
+ "descriptions": [
+ {
+ "languageCode": YTJLanguageCode.FI,
+ "description": "Test Industry",
+ }
+ ]
+ },
+ }
+ ]
+ }
+
+ data = YTJClient.get_company_data_from_ytj_data(minimal_data)
+ assert data["name"] == "Test Company"
+
+
+def test_get_company_data_no_companies():
+ with pytest.raises(YTJNotFoundError, match="No companies found"):
+ YTJClient.get_company_data_from_ytj_data({})
+
+
+class TestYTJCompany:
+ @pytest.fixture
+ def company_data(self):
+ return {
+ "businessId": {"value": "1234567-8"},
+ "names": [{"name": "Test Oy", "type": YTJNameType.MAIN_NAME}],
+ "companyForms": [
+ {
+ "type": "OY",
+ "descriptions": [
+ {
+ "languageCode": YTJLanguageCode.FI,
+ "description": "Osakeyhtiö",
+ },
+ {
+ "languageCode": YTJLanguageCode.SV,
+ "description": "Aktiebolag",
+ },
+ ],
+ }
+ ],
+ "mainBusinessLine": {
+ "descriptions": [
+ {
+ "languageCode": YTJLanguageCode.FI,
+ "description": "Ohjelmistokehitys",
+ },
+ {
+ "languageCode": YTJLanguageCode.SV,
+ "description": "Programvaruutveckling",
+ },
+ ]
+ },
+ "addresses": [
+ {
+ "type": YTJAddressType.VISITING_ADDRESS,
+ "street": "Katu 1",
+ "postCode": "00100",
+ "postOffices": [
+ {"city": "Helsinki", "languageCode": YTJLanguageCode.FI},
+ {"city": "Helsingfors", "languageCode": YTJLanguageCode.SV},
+ ],
+ },
+ {
+ "type": YTJAddressType.POSTAL_ADDRESS,
+ "street": "Box 123",
+ "postCode": "00101",
+ "postOffices": [
+ {"city": "Helsinki", "languageCode": YTJLanguageCode.FI}
+ ],
+ },
+ ],
+ }
+
+ def test_company_form_language_priority(self, company_data):
+ # Default (FI)
+ company = YTJCompany.from_json(company_data)
+ assert company.company_form == "Osakeyhtiö"
+
+ # SV fallback
+ company_data["companyForms"][0]["descriptions"] = [
+ {"languageCode": YTJLanguageCode.SV, "description": "Aktiebolag"}
+ ]
+ company = YTJCompany.from_json(company_data)
+ assert company.company_form == "Aktiebolag"
+
+ # Type fallback
+ company_data["companyForms"][0]["descriptions"] = []
+ company = YTJCompany.from_json(company_data)
+ assert company.company_form == "OY"
+
+ def test_industry_language_priority(self, company_data):
+ # Default (FI)
+ company = YTJCompany.from_json(company_data)
+ assert company.industry == "Ohjelmistokehitys"
+
+ # SV fallback
+ company_data["mainBusinessLine"]["descriptions"] = [
+ {"languageCode": YTJLanguageCode.SV, "description": "Programvaruutveckling"}
+ ]
+ company = YTJCompany.from_json(company_data)
+ assert company.industry == "Programvaruutveckling"
+
+ # Fail if no FI/SV
+ company_data["mainBusinessLine"]["descriptions"] = [
+ {"languageCode": YTJLanguageCode.EN, "description": "Software Development"}
+ ]
+ company = YTJCompany.from_json(company_data)
+ with pytest.raises(ValueError, match="Company industry missing"):
+ _ = company.industry
+
+ def test_address_priority(self, company_data):
+ # Default (Visiting Address - Type 1)
+ company = YTJCompany.from_json(company_data)
+ assert company.address["street_address"] == "Katu 1"
+
+ # Fallback to Postal Address (Type 2)
+ company_data["addresses"] = [
+ d
+ for d in company_data["addresses"]
+ if d["type"] == YTJAddressType.POSTAL_ADDRESS
+ ]
+ company = YTJCompany.from_json(company_data)
+ assert company.address["street_address"] == "Box 123"
+
+ # Fail if no valid address
+ company_data["addresses"] = []
+ company = YTJCompany.from_json(company_data)
+ with pytest.raises(ValueError, match="Company address missing"):
+ _ = company.address
+
+ def test_city_language_priority(self, company_data):
+ # Default (FI)
+ company = YTJCompany.from_json(company_data)
+ assert company.address["city"] == "Helsinki"
+
+ # SV Fallback (simulate only SV city available)
+ company_data["addresses"][0]["postOffices"] = [
+ {"city": "Helsingfors", "languageCode": YTJLanguageCode.SV}
+ ]
+ company = YTJCompany.from_json(company_data)
+ assert company.address["city"] == "Helsingfors"
+
+ def test_name_priority(self, company_data):
+ # Default (Main Name - Type 1)
+ company = YTJCompany.from_json(company_data)
+ assert company.name == "Test Oy"
+
+ # Fallback to any name if type 1 missing
+ company_data["names"] = [{"name": "Secondary Name", "type": "2"}]
+ company = YTJCompany.from_json(company_data)
+ assert company.name == "Secondary Name"
+
+ # Fail if no names
+ company_data["names"] = []
+ company = YTJCompany.from_json(company_data)
+ with pytest.raises(ValueError, match="Company name missing"):
+ _ = company.name
+
+ def test_parsing_with_undocumented_fields(self, company_data):
+ # Add a field that is NOT in our dataclass
+ company_data["addresses"][0]["extra_metadata_from_api"] = "some-value"
+ company_data["mainBusinessLine"]["new_api_feature"] = 123
+
+ # This should NOT raise a TypeError
+ company = YTJCompany.from_json(company_data)
+ assert company.business_id_value == "1234567-8"
+
+ def test_root_fields_mapping(self, company_data):
+ company_data["status"] = "Registered"
+ company_data["endDate"] = "2025-01-01"
+
+ company = YTJCompany.from_json(company_data)
+ assert company.status == "Registered"
+ assert company.endDate == "2025-01-01"
diff --git a/backend/shared/shared/ytj/ytj_client.py b/backend/shared/shared/ytj/ytj_client.py
index 0f62d8f8e4..d50866faea 100644
--- a/backend/shared/shared/ytj/ytj_client.py
+++ b/backend/shared/shared/ytj/ytj_client.py
@@ -1,10 +1,13 @@
import requests
from django.conf import settings
+from shared.ytj.exceptions import YTJNotFoundError
+from shared.ytj.ytj_dataclasses import YTJCompany
+
class YTJClient:
"""
- https://avoindata.prh.fi/
+ https://avoindata.prh.fi/opendata-ytj-api/v3/
"""
def __init__(self):
@@ -21,50 +24,31 @@ def get_company_data_from_ytj_data(ytj_data: dict) -> dict:
"""
Get the required company fields from YTJ data.
"""
- # Address of type 1 is the visiting address
- company_address = next(
- (x for x in ytj_data["addresses"] if x["type"] == 1), None
- )
-
- if not company_address:
- # A fallback if address of type 1 does not exist
- company_address = next(
- (x for x in ytj_data["addresses"] if x["type"] == 2), None
- )
-
- if not company_address:
- raise ValueError("Company address missing from YTJ data")
-
- # Get the Finnish name of the business line
- company_industry = next(
- (x for x in ytj_data["businessLines"] if x["language"] == "FI"), None
- )
-
- if not company_industry:
- raise ValueError("Company industry missing from YTJ data")
-
- company_data = {
- "name": ytj_data["name"],
- "business_id": ytj_data["businessId"],
- "company_form": ytj_data["companyForm"],
- "industry": company_industry["name"],
- "street_address": company_address["street"],
- "postcode": company_address["postCode"],
- "city": company_address["city"],
+ if not ytj_data or "companies" not in ytj_data or not ytj_data["companies"]:
+ raise YTJNotFoundError("No companies found in YTJ data")
+
+ # Parse first company
+ company = YTJCompany.from_json(ytj_data["companies"][0])
+
+ return {
+ "name": company.name,
+ "business_id": company.business_id_value,
+ "company_form": company.company_form,
+ "industry": company.industry,
+ **company.address,
}
- return company_data
-
def get_company_info_with_business_id(self, business_id: str, **kwargs) -> dict:
- company_info_url = f"{settings.YTJ_BASE_URL}/{business_id}"
+ # V3 uses query parameters
+ company_info_url = f"{settings.YTJ_BASE_URL}/companies"
+ params = {"businessId": business_id}
+ kwargs.update({"params": params})
ytj_data = self._get(company_info_url, **kwargs)
- company_result = ytj_data["results"][0]
- business_details = self._get(company_result["bisDetailsUri"])
-
- company_result["businessLines"] = business_details["results"][0][
- "businessLines"
- ]
+ if not ytj_data.get("companies"):
+ raise YTJNotFoundError(
+ f"No company found in YTJ for business ID: {business_id}"
+ )
- return company_result
+ return ytj_data
diff --git a/backend/shared/shared/ytj/ytj_dataclasses.py b/backend/shared/shared/ytj/ytj_dataclasses.py
new file mode 100644
index 0000000000..36cfd3e363
--- /dev/null
+++ b/backend/shared/shared/ytj/ytj_dataclasses.py
@@ -0,0 +1,342 @@
+"""
+Dataclasses representing the company data structure returned by the YTJ V3 API.
+Includes logic for parsing the API response and extracting preferred fields
+(e.g., Finnish names/addresses).
+
+Full API documentation: https://avoindata.prh.fi/fi/ytj/swagger-ui
+Local Schema Reference: ytj_schema.yaml (OpenAPI 3.0)
+"""
+
+# flake8: noqa: N815
+from dataclasses import dataclass, field
+from enum import IntEnum, StrEnum
+from typing import List, Optional
+
+from shared.ytj.exceptions import YTJParseError
+
+
+class YTJLanguageCode(StrEnum):
+ FI = "1"
+ SV = "2"
+ EN = "3"
+
+
+class YTJNameType(StrEnum):
+ MAIN_NAME = "1"
+
+
+class YTJAddressType(IntEnum):
+ VISITING_ADDRESS = 1
+ POSTAL_ADDRESS = 2
+
+
+@dataclass
+class YTJDescription:
+ """Description object with language code."""
+
+ languageCode: str
+ description: str
+
+
+@dataclass
+class YTJName:
+ """Company name details."""
+
+ name: str
+ type: str
+ registrationDate: Optional[str] = None
+ version: Optional[int] = None
+ source: Optional[str] = None
+ endDate: Optional[str] = None
+
+
+@dataclass
+class YTJBusinessId:
+ """Business ID details."""
+
+ value: str
+ registrationDate: Optional[str] = None
+ source: Optional[str] = None
+
+
+@dataclass
+class YTJCompanyForm:
+ """Company form (legal entity type) details."""
+
+ type: Optional[str] = None
+ version: Optional[int] = None
+ source: Optional[str] = None
+ registrationDate: Optional[str] = None
+ endDate: Optional[str] = None
+ descriptions: List[YTJDescription] = field(default_factory=list)
+
+
+@dataclass
+class YTJPostOffice:
+ """Post office details including city name."""
+
+ city: str
+ languageCode: Optional[str] = None
+ municipalityCode: Optional[str] = None
+
+
+@dataclass
+class YTJAddress:
+ """Address details."""
+
+ type: int
+ registrationDate: Optional[str] = None
+ source: Optional[str] = None
+ street: Optional[str] = None
+ postCode: Optional[str] = None
+ postOffices: List[YTJPostOffice] = field(default_factory=list)
+ buildingNumber: Optional[str] = None
+ entrance: Optional[str] = None
+ apartmentNumber: Optional[str] = None
+ apartmentIdSuffix: Optional[str] = None
+ co: Optional[str] = None
+ postOfficeBox: Optional[str] = None
+ country: Optional[str] = None
+ freeAddressLine: Optional[str] = None
+
+
+@dataclass
+class YTJBusinessLine:
+ """Main business line (industry) details."""
+
+ source: Optional[str] = None
+ registrationDate: Optional[str] = None
+ type: Optional[str] = None
+ descriptions: List[YTJDescription] = field(default_factory=list)
+ typeCodeSet: Optional[str] = None
+
+
+@dataclass
+class YTJRegisteredEntry:
+ """Registered entry details."""
+
+ type: str
+ register: str
+ authority: str
+ registrationDate: Optional[str] = None
+ endDate: Optional[str] = None
+ descriptions: List[YTJDescription] = field(default_factory=list)
+
+
+def _parse_descriptions(d_list: list) -> List[YTJDescription]:
+ return [YTJDescription(**d) for d in d_list] if d_list else []
+
+
+def _parse_company_forms(data: list) -> List[YTJCompanyForm]:
+ company_forms = []
+ for cf in data:
+ # Create a copy to avoid mutating original dict if reused
+ cf = cf.copy()
+ desc = _parse_descriptions(cf.pop("descriptions", []))
+ company_forms.append(YTJCompanyForm(descriptions=desc, **cf))
+ return company_forms
+
+
+def _parse_addresses(data: list) -> List[YTJAddress]:
+ addresses = []
+ for addr in data:
+ addr = addr.copy()
+ po_list = [YTJPostOffice(**po) for po in addr.pop("postOffices", [])]
+
+ # Safe unpacking helper
+ known_fields = {
+ "type",
+ "registrationDate",
+ "source",
+ "street",
+ "postCode",
+ "buildingNumber",
+ "entrance",
+ "apartmentNumber",
+ "apartmentIdSuffix",
+ "co",
+ "postOfficeBox",
+ "country",
+ "freeAddressLine",
+ }
+ addr_data = {k: v for k, v in addr.items() if k in known_fields}
+
+ addresses.append(YTJAddress(postOffices=po_list, **addr_data))
+ return addresses
+
+
+def _parse_registered_entries(data: list) -> List[YTJRegisteredEntry]:
+ registered_entries = []
+ for re in data:
+ re = re.copy()
+ desc = _parse_descriptions(re.pop("descriptions", []))
+ registered_entries.append(YTJRegisteredEntry(descriptions=desc, **re))
+ return registered_entries
+
+
+def _parse_main_business_line(data: dict) -> Optional[YTJBusinessLine]:
+ if not data:
+ return None
+ data = data.copy()
+ desc = _parse_descriptions(data.pop("descriptions", []))
+
+ # Whitelist fields for BusinessLine to prevent TypeError from undocumented API
+ # fields
+ known_fields = {"source", "registrationDate", "type", "typeCodeSet"}
+ line_data = {k: v for k, v in data.items() if k in known_fields}
+
+ return YTJBusinessLine(descriptions=desc, **line_data)
+
+
+@dataclass
+class YTJCompany:
+ """
+ Root object for a company in the YTJ API response.
+ Contains methods to parse JSON and properties to extract preferred display values.
+ """
+
+ businessId: YTJBusinessId
+ names: List[YTJName] = field(default_factory=list)
+ addresses: List[YTJAddress] = field(default_factory=list)
+ companyForms: List[YTJCompanyForm] = field(default_factory=list)
+ mainBusinessLine: Optional[YTJBusinessLine] = None
+ # Add other fields as optional if needed (euId, website, etc)
+ euId: Optional[dict] = None
+ website: Optional[dict] = None
+ companySituations: List[dict] = field(default_factory=list)
+ registeredEntries: List[YTJRegisteredEntry] = field(default_factory=list)
+ tradeRegisterStatus: Optional[str] = None
+ status: Optional[str] = None
+ registrationDate: Optional[str] = None
+ endDate: Optional[str] = None
+ lastModified: Optional[str] = None
+
+ @classmethod
+ def from_json(cls, data: dict) -> "YTJCompany":
+ """
+ Parse raw JSON data from YTJ V3 API into a YTJCompany object.
+ """
+ processed = {
+ "businessId": YTJBusinessId(**data.get("businessId", {})),
+ "names": [YTJName(**n) for n in data.get("names", [])],
+ "companyForms": _parse_company_forms(data.get("companyForms", [])),
+ "addresses": _parse_addresses(data.get("addresses", [])),
+ "mainBusinessLine": _parse_main_business_line(data.get("mainBusinessLine")),
+ "registeredEntries": _parse_registered_entries(
+ data.get("registeredEntries", [])
+ ),
+ }
+ return cls(**{**data, **processed})
+
+ def _get_preferred_translation(
+ self,
+ items: list,
+ attr_name: str,
+ preferred_langs: Optional[List[YTJLanguageCode]] = None,
+ ) -> Optional[str]:
+ """
+ Returns the first available translation for the given attribute name
+ based on the provided language priority.
+ """
+ if preferred_langs is None:
+ preferred_langs = [YTJLanguageCode.FI, YTJLanguageCode.SV]
+
+ for lang in preferred_langs:
+ match = next(
+ (getattr(i, attr_name) for i in items if i.languageCode == lang),
+ None,
+ )
+ if match:
+ return match
+ return None
+
+ @property
+ def business_id_value(self) -> str:
+ """Returns the business ID string."""
+ if not self.businessId or not self.businessId.value:
+ raise YTJParseError("Business ID missing from YTJ data")
+ return self.businessId.value
+
+ @property
+ def name(self) -> str:
+ """
+ Returns the preferred company name.
+ Prioritizes the main name (type '1'). Falls back to the first available name.
+ """
+ # Name: prefer type 1 (main name?), fallback to first available
+ name = next(
+ (x.name for x in self.names if x.type == YTJNameType.MAIN_NAME), None
+ )
+ if not name and self.names:
+ name = self.names[0].name
+
+ if not name:
+ raise YTJParseError("Company name missing from YTJ data")
+ return name
+
+ @property
+ def company_form(self) -> str:
+ """
+ Returns the company form as a string description (e.g., "Osakeyhtiö").
+ Prioritizes Finnish description. Falls back to type code.
+ """
+ company_form_obj = self.companyForms[0] if self.companyForms else None
+ company_form = company_form_obj.type if company_form_obj else None
+
+ # Try to find description in preferred languages
+ if company_form_obj:
+ translation = self._get_preferred_translation(
+ company_form_obj.descriptions, "description"
+ )
+ if translation:
+ company_form = translation
+ return company_form
+
+ @property
+ def industry(self) -> str:
+ """
+ Returns the main industry/business line description.
+ Prioritizes Finnish, then Swedish.
+ """
+ mbl = self.mainBusinessLine
+ if not mbl:
+ raise YTJParseError("Company industry missing from YTJ data")
+
+ descriptions = mbl.descriptions
+ industry = self._get_preferred_translation(descriptions, "description")
+
+ if not industry:
+ raise YTJParseError("Company industry missing from YTJ data")
+ return industry
+
+ @property
+ def address(self) -> dict:
+ """
+ Returns a dict with street_address, postcode, and city.
+ Prioritizes Visiting Address (type 1), then Postal Address (type 2).
+ City is selected from postOffices list (prioritizing Finnish).
+ """
+ addresses = self.addresses
+ # Type 1 is visiting address, Type 2 is postal
+ company_address = next(
+ (x for x in addresses if x.type == YTJAddressType.VISITING_ADDRESS),
+ next(
+ (x for x in addresses if x.type == YTJAddressType.POSTAL_ADDRESS), None
+ ),
+ )
+
+ if not company_address:
+ raise YTJParseError("Company address missing from YTJ data")
+
+ # Safely extract city: Prioritize preferred languages, then anything available.
+ city = None
+ if company_address.postOffices:
+ city = self._get_preferred_translation(company_address.postOffices, "city")
+ if not city:
+ city = company_address.postOffices[0].city
+
+ return {
+ "street_address": company_address.street,
+ "postcode": company_address.postCode,
+ "city": city,
+ }
diff --git a/backend/shared/shared/ytj/ytj_schema.yaml b/backend/shared/shared/ytj/ytj_schema.yaml
new file mode 100644
index 0000000000..2b0349544e
--- /dev/null
+++ b/backend/shared/shared/ytj/ytj_schema.yaml
@@ -0,0 +1,727 @@
+---
+openapi: 3.0.1
+info:
+ title: Opendata YTJ-Api
+ version: 2.0
+ license:
+ name: Creative Commons Nimeä 4.0
+ url: https://creativecommons.org/licenses/by/4.0/
+servers:
+ - url: https://avoindata.prh.fi/opendata-ytj-api/v3
+paths:
+ /companies:
+ get:
+ summary: "Hae yrityksiä toiminimen, postitoimipaikan Y-tunnuksen tai yritysmuodon perusteella"
+ operationId: GetCompanies
+ parameters:
+ - in: query
+ name: name
+ description: "Toiminimi (haku tehdään nykyiseen ja edelliseen toiminimeen, rinnakkaistoiminimeen ja aputoiminimeen)"
+ schema:
+ type: string
+ - in: query
+ name: location
+ description: "Postitoimipaikka"
+ schema:
+ type: string
+ - in: query
+ name: businessId
+ description: "Y-tunnus"
+ schema:
+ type: string
+ - in: query
+ name: companyForm
+ description: >
+ Yritysmuoto
+ schema:
+ type: string
+ enum: [ "AOY", "ASH", "ASY", "AY", "AYH", "ETS", "ETY", "SCE", "SCP", "HY", "KOY", "KVJ", "KVY", "KY", "OK",
+ "OP", "OY", "OYJ", "SE", "SL", "SP", "SÄÄ", "TYH", "VOJ", "VOY", "VY", "VALTLL"]
+ - in: query
+ name: mainBusinessLine
+ description: >
+ Päätoimiala (Hae joko Tilastokeskuksen toimialakoodilla tai tekstillä)
+ schema:
+ type: string
+ - in: query
+ name: registrationDateStart
+ description: "Yrityksen rekisteröintipäivä aikavälinä (kirjoita tähän alkamispäivä muodossa vvvv-kk-pp)"
+ schema:
+ type: string
+ format: date
+ minLength: 10
+ maxLength: 10
+ - in: query
+ name: registrationDateEnd
+ description: "Yrityksen rekisteröintipäivä aikavälinä (kirjoita tähän päättymispäivä muodossa vvvv-kk-pp)"
+ schema:
+ type: string
+ format: date
+ minLength: 10
+ maxLength: 10
+ - in: query
+ name: postCode
+ description: "Käynti- tai postiosoitteen postinumero"
+ schema:
+ type: string
+ - in: query
+ name: businessIdRegistrationStart
+ description: "Y-tunnuksen antamispäivä aikavälinä (Kirjoita tähän alkamispäivä muodossa vvvv-kk-pp)"
+ schema:
+ type: string
+ format: date
+ minLength: 10
+ maxLength: 10
+ - in: query
+ name: businessIdRegistrationEnd
+ description: "Y-tunnuksen antamispäivä aikavälinä (kirjoita tähän päättymispäivä muodossa vvvv-kk-pp)"
+ schema:
+ type: string
+ format: date
+ minLength: 10
+ maxLength: 10
+ - in: query
+ name: page
+ description: "Jos haku palauttaa yli 100 tulosta, tulokset palautetaan sivutettuina. Page -parametrilla voit kertoa haluamasi tulossivun. Jos page -parametria ei ole annettu tai se osoittaa sivulle, jota tuloksista ei löydy, palautetaan ensimmäisen sivun tulokset (esimerkiksi jos haet sivua 5 ja tuloksissa on vain 3 sivua)."
+ schema:
+ type: integer
+ format: int32
+ responses:
+ 200:
+ description: "OK"
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CompanyResult'
+ 400:
+ description: "Virheellinen pyyntö"
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ 429:
+ description: "Liian monta pyyntöä"
+ content:
+ text/plain:
+ schema:
+ type: string
+ 500:
+ description: "Sisäinen palvelinvirhe"
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ 503:
+ description: "Palvelin ei voi käsitellä pyyntöä"
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ /all_companies:
+ get:
+ summary: "Hae kaikki kaupparekisterissä olevat ja vireillä olevat yritykset JSON tiedostona (päivitetään kerran päivässä)"
+ operationId: GetAllCompanies
+ responses:
+ 200:
+ description: "OK"
+ content:
+ application/zip:
+ schema:
+ type: string
+ format: binary
+ 400:
+ description: "Virheellinen pyyntö"
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ 429:
+ description: "Liian monta pyyntöä"
+ content:
+ text/plain:
+ schema:
+ type: string
+ 500:
+ description: "Sisäinen palvelinvirhe"
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ 503:
+ description: "Palvelin ei voi käsitellä pyyntöä"
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ /description:
+ get:
+ summary: "Hae koodiston tiedot"
+ operationId: GetDescription
+ parameters:
+ - in: query
+ name: code
+ required: true
+ description: "Koodi"
+ schema:
+ type: string
+ enum: [ "YRMU", "REK_KDI", "TLAJI", "SELTILA", "REK", "VIRANOM", "TLAHDE", "KIELI", "TOIMI", "TOIMI2", "TOIMI3", "TOIMI4", "KONK", "SANE", "STATUS3", "SELTILA,SANE,KONK" ]
+ - in: query
+ name: lang
+ required: true
+ description: "Kielitunnus"
+ schema:
+ type: string
+ enum: [ "en", "fi", "sv" ]
+ responses:
+ 200:
+ description: "OK"
+ content:
+ text/plain:
+ schema:
+ type: string
+ 400:
+ description: "Virheellinen pyyntö"
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ 429:
+ description: "Liian monta pyyntöä"
+ content:
+ text/plain:
+ schema:
+ type: string
+ 500:
+ description: "Sisäinen palvelinvirhe"
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ 503:
+ description: "Palvelin ei voi käsitellä pyyntöä"
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ /post_codes:
+ get:
+ summary: "Hae postikoodiston tiedot"
+ operationId: GetPostCodes
+ parameters:
+ - in: query
+ name: lang
+ required: true
+ description: "Kielitunnus"
+ schema:
+ type: string
+ enum: [ "en", "fi", "sv" ]
+ responses:
+ 200:
+ description: "OK"
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/PostOfficeEntry'
+ 400:
+ description: "Virheellinen pyyntö"
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ 429:
+ description: "Liian monta pyyntöä"
+ content:
+ text/plain:
+ schema:
+ type: string
+ 500:
+ description: "Sisäinen palvelinvirhe"
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ 503:
+ description: "Palvelin ei voi käsitellä pyyntöä"
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+components:
+ schemas:
+ CompanyResult:
+ type: object
+ description: "Yrityksen tai yhteisön tulokset"
+ required: ["totalResults", "companies"]
+ properties:
+ totalResults:
+ type: integer
+ format: int64
+ description: "Kokonaistulosten määrä"
+ companies:
+ type: array
+ description: "Yrityksen tai yhteisön tiedot"
+ items:
+ $ref: '#/components/schemas/Company'
+ Company:
+ type: object
+ description: "Yrityksen tai yhteisön tiedot"
+ required: ["businessId", "registeredEntries", "tradeRegisterStatus", "lastModified"]
+ properties:
+ businessId:
+ type: object
+ required: ["value", "source"]
+ description: "Y-tunnus, esim 0116297-6"
+ properties:
+ value:
+ type: string
+ description: "Y-tunnus"
+ minLength: 9
+ maxLength: 9
+ registrationDate:
+ type: string
+ nullable: true
+ format: date
+ description: "Y-tunnuksen antamispäivä"
+ minLength: 10
+ maxLength: 10
+ source:
+ $ref: '#/components/schemas/Source'
+ euId:
+ type: object
+ required: ["value", "source"]
+ description: "EUID-tunnus, esim FIFPRO.0116297-6"
+ properties:
+ value:
+ type: string
+ description: "EUID tunnus"
+ minLength: 16
+ maxLength: 16
+ source:
+ $ref: '#/components/schemas/Source'
+ names:
+ type: array
+ description: "Yrityksen nimet; päätoiminimi, rinnakkaistoiminimet ja aputoiminimet"
+ items:
+ $ref: '#/components/schemas/RegisterName'
+ minItems: 0
+ mainBusinessLine:
+ type: object
+ required: ["type", "source"]
+ description: "Päätoimiala"
+ properties:
+ type:
+ type: string
+ description: "Toimialakoodi"
+ minLength: 2
+ maxLength: 5
+ descriptions:
+ type: array
+ description: "Toimialan kuvaukset"
+ items:
+ $ref: '#/components/schemas/DescriptionEntry'
+ minItems: 0
+ typeCodeSet:
+ type: string
+ description: "Toimialaluokituksen koodistot TOIMI, TOIMI2, TOIMI3, TOIMI4"
+ minLength: 5
+ maxLength: 6
+ registrationDate:
+ type: string
+ nullable: true
+ format: date
+ description: "Toimialakoodin alkupäivämäärä muodossa vvvv-kk-pp"
+ minLength: 10
+ maxLength: 10
+ source:
+ $ref: '#/components/schemas/Source'
+ website:
+ type: object
+ required: ["url", "source"]
+ description: "Yrityksen verkkosivut"
+ properties:
+ url:
+ type: string
+ description: "Verkkosivut"
+ minLength: 0
+ maxLength: 255
+ registrationDate:
+ type: string
+ nullable: true
+ format: date
+ minLength: 10
+ maxLength: 10
+ source:
+ $ref: '#/components/schemas/Source'
+ companyForms:
+ type: array
+ description: "Yritysmuoto ja edellinen tieto, jos se on olemassa"
+ items:
+ $ref: '#/components/schemas/CompanyForm'
+ minItems: 0
+ maxItems: 2
+ companySituations:
+ type: array
+ description: "Yrityksen tilanne (mahdollinen saneeraus, selvitystila tai konkurssi)"
+ items:
+ $ref: '#/components/schemas/CompanySituation'
+ minItems: 0
+ registeredEntries:
+ type: array
+ description: "Yrityksen rekisterimerkinnät"
+ items:
+ $ref: '#/components/schemas/RegisteredEntry'
+ minItems: 0
+ addresses:
+ type: array
+ description: "Yrityksen käynti- tai postiosoite"
+ items:
+ $ref: '#/components/schemas/Address'
+ minItems: 0
+ maxItems: 2
+ tradeRegisterStatus:
+ type: string
+ description: >
+ [Yrityksen kaupparekisterin tilatieto](/opendata-ytj-api/v3/description?code=REK_KDI&lang=fi)
+ status:
+ type: string
+ description: >
+ [Yrityksen y-tunnuksen tila](/opendata-ytj-api/v3/description?code=STATUS3&lang=fi)
+ registrationDate:
+ type: string
+ nullable: true
+ format: date
+ description: "Yrityksen rekisteröintipäivämäärä muodossa vvvv-kk-pp"
+ minLength: 10
+ maxLength: 10
+ endDate:
+ type: string
+ nullable: true
+ minLength: 10
+ maxLength: 10
+ format: date
+ description: "Lakkaamispäivä muodossa vvvv-kk-pp"
+ lastModified:
+ type: string
+ minLength: 19
+ maxLength: 19
+ format: date-time
+ x-field-extra-annotation: "@com.fasterxml.jackson.annotation.JsonFormat(shape = com.fasterxml.jackson.annotation.JsonFormat.Shape.STRING, pattern = \"yyyy-MM-ddTHH:mm:ss\")"
+ description: "Viimeksi muokattu sekuntitasolla ilman aikavyöhykettä muodossa 'vvvv-kk-ppT00:00:00'"
+ Address:
+ type: object
+ description: "Osoitetiedot"
+ required: ["type", "source"]
+ properties:
+ type:
+ type: integer
+ format: int32
+ description: "Osoitteen laji, käyntiosoite: 1, postiosoite: 2. "
+ street:
+ type: string
+ nullable: true
+ description: "Katuosoite"
+ minLength: 0
+ maxLength: 50
+ postCode:
+ type: string
+ nullable: true
+ description: "Postinumero"
+ minLength: 0
+ maxLength: 5
+ postOffices:
+ type: array
+ description: "Postitoimipaikka eri kielillä"
+ items:
+ $ref: '#/components/schemas/PostOffice'
+ minItems: 0
+ postOfficeBox:
+ type: string
+ nullable: true
+ description: "Postilokero"
+ minLength: 0
+ maxLength: 5
+ buildingNumber:
+ type: string
+ nullable: true
+ description: "Talo"
+ minLength: 0
+ maxLength: 13
+ entrance:
+ type: string
+ nullable: true
+ description: "Porras"
+ minLength: 0
+ maxLength: 13
+ apartmentNumber:
+ type: string
+ nullable: true
+ description: "Huoneisto"
+ minLength: 0
+ maxLength: 4
+ apartmentIdSuffix:
+ type: string
+ nullable: true
+ description: "Jakokirjain"
+ minLength: 0
+ maxLength: 1
+ co:
+ type: string
+ nullable: true
+ description: "Osoitteen c/o-tieto"
+ minLength: 0
+ maxLength: 34
+ country:
+ type: string
+ nullable: true
+ description: "Kaksikirjaiminen maakoodi"
+ minLength: 0
+ maxLength: 2
+ freeAddressLine:
+ type: string
+ nullable: true
+ description: "Vapaamuotoinen osoite esim. ulkomaalaiselle osoitteelle. Rivinvaihdot korvattu välilyönnillä, välilyönnit korvattu alaviivalla esim Norgårdsvägen_3 _ SE-451_75 Uddevalla"
+ minLength: 0
+ maxLength: 1000
+ registrationDate:
+ type: string
+ nullable: true
+ format : date
+ description: "Rekisteröintipäivämäärä muodossa vvvv-kk-pp"
+ minLength: 10
+ maxLength: 10
+ source:
+ $ref: '#/components/schemas/Source'
+ RegisterName:
+ type: object
+ description: "Nimitiedot"
+ required: ["name", "type", "version", "source"]
+ properties:
+ name:
+ type: string
+ description: "Toiminimi"
+ minLength: 0
+ maxLength: 1000
+ type:
+ type: string
+ description: >
+ [Nimen tyyppi](/opendata-ytj-api/v3/description?code=TLAJI&lang=fi)
+ minLength: 1
+ maxLength: 8
+ registrationDate:
+ type: string
+ nullable: true
+ format: date
+ description: "Rekisteröintipäivämäärä muodossa vvvv-kk-pp"
+ minLength: 10
+ maxLength: 10
+ endDate:
+ type: string
+ nullable: true
+ format: date
+ description: "Rekisteröinnin loppupäivämäärä muodossa vvvv-kk-pp"
+ minLength: 10
+ maxLength: 10
+ version:
+ type: integer
+ format: int32
+ description: "Versio, 1 nykyinen versio, muut numerot aiempia versioita"
+ source:
+ $ref: '#/components/schemas/Source'
+ CompanyForm:
+ type: object
+ required: ["type", "version", "source"]
+ description: "Yritysmuoto"
+ properties:
+ type:
+ type: string
+ description: >
+ [Yritysmuodon koodi](/opendata-ytj-api/v3/description?code=YRMU&lang=fi)
+ minLength: 1
+ maxLength: 8
+ descriptions:
+ type: array
+ description: "Yritysmuodon kuvaukset"
+ items:
+ $ref: '#/components/schemas/DescriptionEntry'
+ minItems: 0
+ registrationDate:
+ type: string
+ nullable: true
+ format: date
+ description: "Rekisteröintipäivämäärä muodossa vvvv-kk-pp"
+ minLength: 10
+ maxLength: 10
+ endDate:
+ type: string
+ nullable: true
+ format: date
+ description: "Rekisteröinnin loppupäivämäärä muodossa vvvv-kk-pp"
+ minLength: 10
+ maxLength: 10
+ version:
+ type: integer
+ format: int32
+ description: "Versio, 1 nykyinen versio, muut numerot aiempia versioita"
+ source:
+ $ref: '#/components/schemas/Source'
+ CompanySituation:
+ type: object
+ required: ["type", "source"]
+ description: "Yrityksen tilanne (mahdollinen saneeraus, selvitystila tai konkurssi)"
+ properties:
+ type:
+ type: string
+ description: >
+ [Yrityksen tilanteen koodi](/opendata-ytj-api/v3/description?code=SELTILA,SANE,KONK&lang=fi)
+ enum: ["SANE", "SELTILA", "KONK"]
+ registrationDate:
+ type: string
+ nullable: true
+ format: date
+ description: "Rekisteröintipäivämäärä muodossa vvvv-kk-pp"
+ minLength: 10
+ maxLength: 10
+ endDate:
+ type: string
+ nullable: true
+ format: date
+ description: "Rekisteröinnin loppupäivämäärä muodossa vvvv-kk-pp"
+ minLength: 10
+ maxLength: 10
+ source:
+ $ref: '#/components/schemas/Source'
+ RegisteredEntry:
+ type: object
+ required: ["type", "register", "authority"]
+ description: "Rekisterimerkinnät"
+ properties:
+ type:
+ type: string
+ description: >
+ [Yrityksen rekisteröintitilan koodi](/opendata-ytj-api/v3/description?code=REK_KDI&lang=fi). Tulkittava Koodistosta REK_KDI yhdistämällä rekisterimerkinnän koodi rekisterikoodiin erotettuna alaviivalla, esim '1_0' on rekisteröimätön kaupparekisterissä
+ minLength: 1
+ maxLength: 8
+ descriptions:
+ type: array
+ description: "Rekisteröintitilan kuvaukset"
+ items:
+ $ref: '#/components/schemas/DescriptionEntry'
+ minItems: 0
+ registrationDate:
+ type: string
+ nullable: true
+ format: date
+ description: "Rekisteröintipäivämäärä muodossa vvvv-kk-pp"
+ minLength: 10
+ maxLength: 10
+ endDate:
+ type: string
+ nullable: true
+ format: date
+ description: "Rekisteröinnin loppupäivämäärä muodossa vvvv-kk-pp"
+ minLength: 10
+ maxLength: 10
+ register:
+ type: string
+ description: >
+ [Rekisterin koodi](/opendata-ytj-api/v3/description?code=REK&lang=fi)
+ minLength: 1
+ maxLength: 8
+ authority:
+ type: string
+ description: >
+ [Viranomaisen koodi](/opendata-ytj-api/v3/description?code=VIRANOM&lang=fi)
+ minLength: 1
+ maxLength: 8
+ Source:
+ type: string
+ description: >
+ Tietolähde
+ DescriptionEntry:
+ type: object
+ required: ["languageCode"]
+ description: "Tiedon kuvaus"
+ properties:
+ languageCode:
+ type: string
+ description: "Kielikoodi, 1 - Suomi, 2 - Ruotsi, 3 - Englanti"
+ maxLength: 2
+ description:
+ type: string
+ description: "Koodiseloste"
+ nullable: true
+ maxLength: 255
+ PostOffice:
+ type: object
+ required: ["city", "languageCode"]
+ description: "Postiosoitteet"
+ properties:
+ city:
+ type: string
+ description: "Postitoimipaikka"
+ maxLength: 50
+ languageCode:
+ type: string
+ description: >
+ [Kielikoodi](/opendata-ytj-api/v3/description?code=KIELI&lang=fi)
+ maxLength: 8
+ municipalityCode:
+ type: string
+ description: "Kuntakoodi"
+ nullable: true
+ minLength: 3
+ maxLength: 3
+ PostOfficeEntry:
+ type: object
+ required: ["postCode", "city", "active", "languageCode"]
+ description: "Postiosoitteet"
+ properties:
+ postCode:
+ type: string
+ description: "Postinumero"
+ minLength: 5
+ maxLength: 5
+ city:
+ type: string
+ description: "Postitoimipaikka"
+ maxLength: 50
+ active:
+ type: boolean
+ description: "Onko tieto aktiivinen"
+ languageCode:
+ type: string
+ description: >
+ [Kielikoodi](/opendata-ytj-api/v3/description?code=KIELI&lang=fi)
+ maxLength: 8
+ municipalityCode:
+ type: string
+ description: "Kuntakoodi"
+ nullable: true
+ minLength: 3
+ maxLength: 3
+ ErrorResponse:
+ type: object
+ required: [ "timestamp", "code" ]
+ description: "Virheilmoitus"
+ properties:
+ timestamp:
+ type: string
+ minLength: 19
+ maxLength: 19
+ format: date-time
+ x-field-extra-annotation: "@com.fasterxml.jackson.annotation.JsonFormat(shape = com.fasterxml.jackson.annotation.JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd HH:mm:ss\")"
+ description: "Aika sekuntitasolla ilman aikavyöhykettä muodossa 'vvvv-kk-pp 00:00:00'"
+ message:
+ type: string
+ description: "Virheviesti"
+ maxLength: 1000
+ errorcode:
+ type: integer
+ format: int32
+ description: "Virheen koodi"
+
+externalDocs:
+ description: "Lisätiedot Swagger-ohjelmistosta"
+ url: 'http://swagger.io'
\ No newline at end of file