Skip to content

Commit

Permalink
Merge pull request #756 from Klantinteractie-Servicesysteem/DIM@6313_…
Browse files Browse the repository at this point in the history
…KvK_Zoeken_API_upgraden

DIM@6313 - KvK Zoeken API upgraden
  • Loading branch information
mstokericatt authored Mar 19, 2024
2 parents 0a126f7 + c1397ca commit 7fbbecc
Show file tree
Hide file tree
Showing 11 changed files with 232 additions and 215 deletions.
2 changes: 1 addition & 1 deletion .env.local.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
OIDC_CLIENT_SECRET=
OIDC_CLIENT_ID=
OIDC_AUTHORITY=
KVK_BASE_URL=
KVK_BASE_URL=https://api.kvk.nl/test/api
KVK_API_KEY=
HAAL_CENTRAAL_BASE_URL=
HAAL_CENTRAAL_API_KEY=
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
1. Make a copy of .env.local.example, rename it .env.local and fill in the required secrets. This is a subset, a complete overview can be found at https://kiss-klantinteractie-servicesysteem.readthedocs.io/en/latest/INSTALLATION/:
- `OIDC_CLIENT_SECRET`, `OIDC_CLIENT_ID`, `OIDC_AUTHORITY`: use any OIDC provider. Users have access if they have a claim of either type `role` or type `roles` and a value that corresponds to the environment variable `OIDC_KLANTCONTACTMEDEWERKER_ROLE` (`Klantcontactmedewerker` by default). If you're using Azure AD, this can be done by [creating an application role and assigning it to either groups or individual users](https://learn.microsoft.com/en-us/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps). Do the same with `OIDC_KLANTCONTACTMEDEWERKER_ROLE` (`Redacteur` by default) to enable a user to manage content in KISS.
- `OIDC_MEDEWERKER_IDENTIFICATIE_CLAIM` and `OIDC_MEDEWERKER_IDENTIFICATIE_TRUNCATE`: the current ZGW standards limit the medewerker identificatie to a maximum of 24 characters. If you are using KISS with OpenKlant / OpenZaak, this means you either need to configure a claim in `OIDC_MEDEWERKER_IDENTIFICATIE_CLAIM` that will never exceed this limit, or configure for it to be truncated by setting `OIDC_MEDEWERKER_IDENTIFICATIE_TRUNCATE` to 24. If you are using KISS with the eSuite, make sure you configure a claim in `OIDC_MEDEWERKER_IDENTIFICATIE_CLAIM` that contains te username known in the eSuite.
- `KVK_BASE_URL`: for the KvK test environment, use `https://api.kvk.nl/test/api/v1`
- `KVK_BASE_URL`: for the KvK test environment, use `https://api.kvk.nl/test/api`
- `KVK_API_KEY`: for the KvK test environment, look for the API key on [the KvK website](https://developers.kvk.nl/documentation/testing)
2. Download the Root and intermediate certificates from [the KvK website](https://developers.kvk.nl/documentation/install-tls-certificate#download-certificates) and place them in a `certificates` folder in the root of the repo
Now, you can either run the application from Visual Studio or with docker-compose
Expand Down
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions src/features/klant/bedrijf/BedrijfZoeker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@
import { parseKvkNummer, parsePostcodeHuisnummer } from "@/helpers/validation";
import { ensureState } from "@/stores/create-store";
import { computed, ref, watch } from "vue";
import { bedrijfQuery, useSearchBedrijven } from "./service";
import { useSearchBedrijven } from "./service/UseSearchBedrijven";
import SimpleSpinner from "@/components/SimpleSpinner.vue";
import Pagination from "@/nl-design-system/components/Pagination.vue";
import ApplicationMessage from "@/components/ApplicationMessage.vue";
Expand All @@ -86,6 +87,7 @@ import {
import { KlantType } from "../types";
import { useRouter } from "vue-router";
import { FriendlyError } from "@/services";
import { bedrijfQuery } from "./service/BedrijfsQuery";
type SearchFields = KlantSearchField | SearchCategories;
const labels: { [key in SearchFields]: string } = {
handelsnaam: "Bedrijfsnaam",
Expand Down Expand Up @@ -159,7 +161,7 @@ watch(
if (!(input instanceof HTMLInputElement)) return;
input.setCustomValidity(query instanceof Error ? query.message : "");
},
{ immediate: true }
{ immediate: true },
);
const klanten = useSearchKlanten({
Expand Down
2 changes: 1 addition & 1 deletion src/features/klant/bedrijf/enricher/bedrijf-enricher.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Klant } from "../../types";
import { useKlantByVestigingsnummer } from "../../service";
import { combineEnrichers } from "@/services";
import { useBedrijfByVestigingsnummer } from "../service";
import { useBedrijfByVestigingsnummer } from "../service/UseGetOndernememing";
import type { Bedrijf } from "../types";

const isKlant = (klantOfBedrijf: Klant | Bedrijf): klantOfBedrijf is Klant => {
Expand Down
203 changes: 0 additions & 203 deletions src/features/klant/bedrijf/service.ts
Original file line number Diff line number Diff line change
@@ -1,203 +0,0 @@
import {
fetchLoggedIn,
parseJson,
ServiceResult,
throwIfNotOk,
type Paginated,
enforceOneOrZero,
defaultPagination,
FriendlyError,
} from "@/services";

import type {
SearchCategories,
BedrijfQuery,
BedrijfQueryDictionary,
Bedrijf,
} from "./types";

export const bedrijfQuery = <K extends SearchCategories>(
query: BedrijfQuery<K>
) => query;

type KvkPagination = {
pagina: number;
aantal: number;
totaal: number;
resultaten: any[];
};

const parseKvkPagination = async ({
pagina,
aantal,
totaal,
resultaten,
}: KvkPagination): Promise<Paginated<Bedrijf>> => ({
page: await Promise.all(resultaten.map(mapHandelsRegister)),
pageNumber: pagina,
totalRecords: totaal,
pageSize: aantal,
totalPages: totaal === 0 ? 0 : Math.ceil(totaal / aantal),
});

const handelsRegisterBaseUrl = "/api/kvk";

const bedrijfQueryDictionary: BedrijfQueryDictionary = {
postcodeHuisnummer: ({ postcode, huisnummer }) => [
["postcode", postcode.numbers + postcode.digits],
["huisnummer", huisnummer],
],
kvkNummer: (search) => [["kvkNummer", search]],
handelsnaam: (search) => [["handelsnaam", search]],
};

const getSearchBedrijvenUrl = <K extends SearchCategories>({
query,
page,
}: SearchBedrijfArguments<K>) => {
if (!query?.value) return "";

const params = new URLSearchParams();
// TODO: think about how to search in both klantregister and handelsregister for phone / email

params.set("pagina", page?.toString() ?? "1");
params.set("type", "hoofdvestiging,nevenvestiging");

const searchParams = bedrijfQueryDictionary[query.field](query.value);

searchParams.forEach((tuple) => {
params.set(...tuple);
});

return `${handelsRegisterBaseUrl}/zoeken?${params}`;
};

async function mapHandelsRegister(json: any): Promise<Bedrijf> {
const { vestigingsnummer, kvkNummer, handelsnaam, straatnaam, plaats } =
json ?? {};

let vestiging: KvkVestiging | undefined;

if (vestigingsnummer) {
try {
vestiging = await fetchVestiging(getVestingUrl(vestigingsnummer));
} catch (e) {
console.error(e);
}
}

return {
_typeOfKlant: "bedrijf",
kvkNummer,
vestigingsnummer,
bedrijfsnaam: handelsnaam,
straatnaam,
woonplaats: plaats,
...(vestiging ?? {}),
};
}

type KvkVestiging = {
vestigingsnummer: string;
kvkNummer: string;
handelsnaam: string;
postcode: string;
huisnummer: string;
huisletter: string;
huisnummertoevoeging: string;
};

const mapVestiging = ({
vestigingsnummer,
kvkNummer,
eersteHandelsnaam,
adressen,
}: any): KvkVestiging => {
const { huisnummerToevoeging, postcode, huisnummer, huisletter } =
adressen?.find((x: any) => x?.type === "bezoekadres") ??
adressen?.[0] ??
{};

return {
vestigingsnummer,
kvkNummer,
handelsnaam: eersteHandelsnaam,
huisnummertoevoeging: huisnummerToevoeging,
postcode,
huisnummer,
huisletter,
};
};

const hasFoutCode = (body: unknown, code: string) => {
if (
body &&
typeof body === "object" &&
"fout" in body &&
Array.isArray(body.fout)
) {
return body.fout.some((x) => x?.code === code);
}
return false;
};

function searchBedrijvenInHandelsRegister(url: string) {
return fetchLoggedIn(url).then(async (r) => {
if (r.status === 404) {
const body = await r.json();
if (hasFoutCode(body, "IPD5200")) return defaultPagination([]);
}
if (r.status === 400) {
throw new FriendlyError("Invalide zoekopdracht");
}
throwIfNotOk(r);
const body = await r.json();
return parseKvkPagination(body);
});
}

type SearchBedrijfArguments<K extends SearchCategories> = {
query: BedrijfQuery<K> | undefined;
page: number | undefined;
};

export function useSearchBedrijven<K extends SearchCategories>(
getArgs: () => SearchBedrijfArguments<K>
) {
return ServiceResult.fromFetcher(
() => getSearchBedrijvenUrl(getArgs()),
searchBedrijvenInHandelsRegister
);
}

const getVestingUrl = (vestigingsnummer?: string) => {
if (!vestigingsnummer) return "";
return handelsRegisterBaseUrl + "/vestigingsprofielen/" + vestigingsnummer;
};

const fetchVestiging = (url: string) =>
fetchLoggedIn(url).then(throwIfNotOk).then(parseJson).then(mapVestiging);

export const useBedrijfByVestigingsnummer = (
getVestigingsnummer: () => string | undefined
) => {
const getUrl = () => {
const vestigingsnummer = getVestigingsnummer();
if (!vestigingsnummer) return "";
const searchParams = new URLSearchParams();
searchParams.set("vestigingsnummer", vestigingsnummer);
return `${handelsRegisterBaseUrl}/zoeken?${searchParams}`;
};

const getUniqueId = () => {
const url = getUrl();
return url && url + "_single";
};

const fetcher = (url: string) =>
searchBedrijvenInHandelsRegister(url).then(enforceOneOrZero);

return ServiceResult.fromFetcher(getUrl, fetcher, {
getUniqueId,
});
};
5 changes: 5 additions & 0 deletions src/features/klant/bedrijf/service/BedrijfsQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { BedrijfQuery, SearchCategories } from "../types";

export const bedrijfQuery = <K extends SearchCategories>(
query: BedrijfQuery<K>,
) => query;
30 changes: 30 additions & 0 deletions src/features/klant/bedrijf/service/UseGetOndernememing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ServiceResult, enforceOneOrZero } from "@/services";
import { searchBedrijvenInHandelsRegister } from "./shared/shared";

const zoekenUrl = "/api/kvk/v2/zoeken";

export const useBedrijfByVestigingsnummer = (
getVestigingsnummer: () => string | undefined,
) => {
const getUrl = () => {
const vestigingsnummer = getVestigingsnummer();
if (!vestigingsnummer) return "";
const searchParams = new URLSearchParams();
searchParams.set("vestigingsnummer", vestigingsnummer);
return `${zoekenUrl}?${searchParams}`;
};

// useBedrijfByVestigingsnummer private //////////////////////////

const getUniqueId = () => {
const url = getUrl();
return url && url + "_single";
};

const fetcher = (url: string) =>
searchBedrijvenInHandelsRegister(url).then(enforceOneOrZero);

return ServiceResult.fromFetcher(getUrl, fetcher, {
getUniqueId,
});
};
53 changes: 53 additions & 0 deletions src/features/klant/bedrijf/service/UseSearchBedrijven.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ServiceResult } from "@/services";

import type {
SearchCategories,
BedrijfQuery,
BedrijfQueryDictionary,
} from "../types";
import { searchBedrijvenInHandelsRegister, zoekenUrl } from "./shared/shared";

export function useSearchBedrijven<K extends SearchCategories>(
getArgs: () => SearchBedrijfArguments<K>,
) {
return ServiceResult.fromFetcher(
() => getSearchBedrijvenUrl(getArgs()),
searchBedrijvenInHandelsRegister,
);
}

// useSearchBedrijven private ///////////////////////////////////////////

const getSearchBedrijvenUrl = <K extends SearchCategories>({
query,
page,
}: SearchBedrijfArguments<K>) => {
if (!query?.value) return "";

const params = new URLSearchParams();
params.set("pagina", page?.toString() ?? "1");
params.set("type", "hoofdvestiging");
params.append("type", "nevenvestiging");

const searchParams = bedrijfQueryDictionary[query.field](query.value);

searchParams.forEach((tuple) => {
params.set(...tuple);
});

return `${zoekenUrl}?${params}`;
};

type SearchBedrijfArguments<K extends SearchCategories> = {
query: BedrijfQuery<K> | undefined;
page: number | undefined;
};

const bedrijfQueryDictionary: BedrijfQueryDictionary = {
postcodeHuisnummer: ({ postcode, huisnummer }) => [
["postcode", postcode.numbers + postcode.digits],
["huisnummer", huisnummer],
],
kvkNummer: (search) => [["kvkNummer", search]],
handelsnaam: (search) => [["naam", search]],
};
Loading

0 comments on commit 7fbbecc

Please sign in to comment.