Skip to content

Commit

Permalink
[TRA-15707] Ajout des informations d'adresses splittées à l'objet Com…
Browse files Browse the repository at this point in the history
…pany (#3944)
  • Loading branch information
GaelFerrand authored Mar 4, 2025
1 parent 43975cf commit fda221a
Show file tree
Hide file tree
Showing 19 changed files with 823 additions and 47 deletions.
29 changes: 29 additions & 0 deletions back/src/common/__tests__/addresses.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,4 +279,33 @@ describe("splitAddress", () => {
// Then
expect(splitted).toMatchObject(expected);
});

test("[bug - infinite loop] should not jam", () => {
// When
const splitted = splitAddress(
"DWF OFFICES, 5 GEORGE'S DOCK, IFSC, DUBLIN 12",
"IE4893017M"
);

// Then
expect(splitted).toMatchObject({
street: "DWF OFFICES, 5 GEORGE'S DOCK, IFSC, DUBLIN 12",
postalCode: "",
city: "",
country: "IE"
});
});

test("dealing with special chars", () => {
// When
const splitted = splitAddress("4 BOULEVARD PASTEUR\n44100 NANTES");

// Then
expect(splitted).toMatchObject({
street: "4 BOULEVARD PASTEUR",
postalCode: "44100",
city: "NANTES",
country: "FR"
});
});
});
17 changes: 11 additions & 6 deletions back/src/common/addresses.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { checkVAT, countries } from "jsvat";
import { isDefinedStrict } from "./helpers";

// Source: https://github.com/unicode-org/cldr/blob/release-26-0-1/common/supplemental/postalCodeData.xml
// SO: https://stackoverflow.com/questions/578406/what-is-the-ultimate-postal-code-and-zip-regex
Expand All @@ -15,7 +16,7 @@ const POSTAL_CODE_REGEX_PER_COUNTRY = {
AU: "[0-9]{4}",
IT: "[0-9]{5}",
CH: "[0-9]{4}",
AT: "[0-9]{4}",
AT: "(AT-)?[0-9]{4}",
ES: "[0-9]{5}",
NL: "[0-9]{4}[ ]?[A-Z]{2}",
BE: "[0-9]{4}",
Expand Down Expand Up @@ -171,15 +172,19 @@ export function extractPostalCode(
address: string | null | undefined,
country: Country = "FR"
) {
const postalCodeRegex = POSTAL_CODE_REGEX_PER_COUNTRY[country];
const postalCodeRegex =
POSTAL_CODE_REGEX_PER_COUNTRY[country] ??
POSTAL_CODE_REGEX_PER_COUNTRY["FR"];

const regex = new RegExp(
new RegExp(/(^| |,)/).source + // There can be a space, a comma or beginning of string BEFORE
new RegExp(postalCodeRegex).source + // The postalCode regex
new RegExp(/($| |,)/).source // There can be a space, a comma or end of string AFTER
);

if (address) {
let addressUp = address.toUpperCase();
let addressUp = address.replace(/\n/g, " ").toUpperCase();

// Kind of a complex machine here because matches might overlap and not be
// detected by RegExp.matches()
// ex: 134 AV DU GENERAL EISENHOWER CS 42326 31100 TOULOUSE
Expand Down Expand Up @@ -226,7 +231,7 @@ export const splitAddress = (
address: string | null | undefined,
vatNumber?: string | null
) => {
if (!address) {
if (!isDefinedStrict(address)) {
return {
street: "",
postalCode: "",
Expand All @@ -246,7 +251,7 @@ export const splitAddress = (
if (!postalCode) {
return {
// Fallback: return the full address in 'street' field
street: address
street: address!
.replace(/\r?\n|\r/g, " ") // remove line breaks
.replace(/\s+/g, " "), // double spaces to single spaces
postalCode: "",
Expand All @@ -255,7 +260,7 @@ export const splitAddress = (
};
}

const splitted = address
const splitted = address!
.replace(/\r?\n|\r/g, " ") // remove line breaks
.replace(/\s+/g, " ") // double spaces to single spaces
.split(postalCode)
Expand Down
166 changes: 166 additions & 0 deletions back/src/companies/__tests__/companyUtils.integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { CompanyType } from "@prisma/client";
import { resetDatabase } from "../../../integration-tests/helper";
import { userWithCompanyFactory } from "../../__tests__/factories";
import { getCompanySplittedAddress } from "../companyUtils";

describe("getCompanySplittedAddress", () => {
afterEach(async () => {
await resetDatabase();
});

it("should return company's splitted address (FR)", async () => {
// Given
const { company } = await userWithCompanyFactory("ADMIN", {
name: "Acme FR",
address: "4 boulevard Pasteur 44100 Nantes"
});

const companySearch = {
orgId: company.orgId,
siret: company.orgId,
etatAdministratif: "A",
addressVoie: "72 rue du Barbâtre",
addressPostalCode: "37100",
addressCity: "Reims",
codePaysEtrangerEtablissement: ""
};

// When
const splittedAddress = getCompanySplittedAddress(companySearch, company);

// Then
expect(splittedAddress?.street).toBe("72 rue du Barbâtre");
expect(splittedAddress?.postalCode).toBe("37100");
expect(splittedAddress?.city).toBe("Reims");
expect(splittedAddress?.country).toBe("FR");
});

it("should return company's splitted address (foreign)", async () => {
// Given
const { company } = await userWithCompanyFactory("ADMIN", {
vatNumber: "BE0894129667",
orgId: "BE0894129667",
name: "Acme BE",
address: "Rue Bois de Goesnes 4 4570 Marchin",
companyTypes: [CompanyType.TRANSPORTER]
});

const companySearch = {
orgId: "BE0894129667",
vatNumber: "BE0894129667",
etatAdministratif: "A",
addressVoie: "",
addressPostalCode: "",
addressCity: "",
codePaysEtrangerEtablissement: "BE"
};

// When
const splittedAddress = getCompanySplittedAddress(companySearch, company);

// Then
expect(splittedAddress?.street).toBe("Rue Bois de Goesnes 4");
expect(splittedAddress?.postalCode).toBe("4570");
expect(splittedAddress?.city).toBe("Marchin");
expect(splittedAddress?.country).toBe("BE");
});

it("companySearch is empty > should update with company's address manual split", async () => {
// Given
const { company } = await userWithCompanyFactory("ADMIN", {
name: "Acme FR",
vatNumber: null,
address: "4 boulevard Pasteur 44100 Nantes"
});

const companySearch = {
orgId: company.orgId,
siret: company.orgId,
etatAdministratif: "A",
codePaysEtrangerEtablissement: ""
};

// When
const splittedAddress = getCompanySplittedAddress(companySearch, company);

// Then
expect(splittedAddress?.street).toBe("4 boulevard Pasteur");
expect(splittedAddress?.postalCode).toBe("44100");
expect(splittedAddress?.city).toBe("Nantes");
expect(splittedAddress?.country).toBe("FR");
});

it("aberrant address > should return null", async () => {
// Given
const { company } = await userWithCompanyFactory("ADMIN", {
name: "Acme FR",
vatNumber: null,
address: "Adresse test"
});

const companySearch = {
orgId: company.orgId,
siret: company.orgId,
etatAdministratif: "A",
codePaysEtrangerEtablissement: ""
};

// When
const splittedAddress = getCompanySplittedAddress(companySearch, company);

// Then
expect(splittedAddress?.street).toBe(null);
expect(splittedAddress?.postalCode).toBe(null);
expect(splittedAddress?.city).toBe(null);
expect(splittedAddress?.country).toBe(null);
});

it.each([null, undefined, {}])(
"companySearch is %p > should do manual split",
async companySearch => {
// Given
const { company } = await userWithCompanyFactory("ADMIN", {
name: "Acme FR",
vatNumber: null,
address: "4 boulevard pasteur 44100 Nantes"
});

// When
const splittedAddress = getCompanySplittedAddress(companySearch, company);

// Then
expect(splittedAddress?.street).toBe("4 boulevard pasteur");
expect(splittedAddress?.postalCode).toBe("44100");
expect(splittedAddress?.city).toBe("Nantes");
expect(splittedAddress?.country).toBe("FR");
}
);

it("partial addresses with no street > should return postalCode and city (API split)", async () => {
// Given
const { company } = await userWithCompanyFactory("ADMIN", {
name: "Acme FR",
vatNumber: null,
address: "48170 CHAUDEYRAC"
});

const companySearch = {
orgId: company.orgId,
siret: company.orgId,
etatAdministratif: "A",
addressVoie: "",
addressPostalCode: "48170",
addressCity: "CHAUDEYRAC",
codePaysEtrangerEtablissement: ""
};

// When
const splittedAddress = getCompanySplittedAddress(companySearch, company);

// Then
expect(splittedAddress?.street).toBe("");
expect(splittedAddress?.postalCode).toBe("48170");
expect(splittedAddress?.city).toBe("CHAUDEYRAC");
expect(splittedAddress?.country).toBe("FR");
});
});
83 changes: 83 additions & 0 deletions back/src/companies/companyUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { splitAddress } from "../common/addresses";
import { Company } from "@prisma/client";
import { isDefinedStrict } from "../common/helpers";
import type { CompanySearchResult } from "@td/codegen-back";

export type AddressCompanySearchResult = Pick<
CompanySearchResult,
| "addressVoie"
| "addressPostalCode"
| "addressCity"
| "codePaysEtrangerEtablissement"
>;

interface SplittedAddress {
street: string | null;
postalCode: string | null;
city: string | null;
country: string | null;
}

export type CompanyToSplit = Pick<Company, "vatNumber" | "address">;

/**
* Retourne l'adresse splittée d'une entreprise ('street', 'postalCode', 'city', 'country').
*
* Pour éviter de multiplier les appels aux APIs externes, cette fonction fait pas d'appel
* elle-même et prend en entrée un companySearchResult (partiel).
*
* Si le companySearchResult est valide, retourne les champs splittés.
*
* Si le companySearchResult mais l'adresse complète de l'entreprise est exploitable, retourne
* un split manuel.
*
* Si le split n'a pas fonctionné, retourne tous les champs à null.
*
* Attention: certaines entreprises ont des addresses du genre "codePostal ville",
* auquel cas on retourne "" pour la rue.
*/
export const getCompanySplittedAddress = (
companySearchResult: AddressCompanySearchResult | null | undefined,
company?: CompanyToSplit | null | undefined
): SplittedAddress => {
let res;

// Split manuel...
if (
// ...si pas de retour des APIs
!companySearchResult ||
// ...si entreprise étrangère
isDefinedStrict(companySearchResult.codePaysEtrangerEtablissement) ||
// ...si le retour des APIs ne comprend pas d'adresse fiable
!isDefinedStrict(companySearchResult?.addressPostalCode?.trim())
) {
res = splitAddress(company?.address, company?.vatNumber);
}
// Sinon, split avec les données retournées par les APIs
else {
res = {
street: companySearchResult.addressVoie,
postalCode: companySearchResult.addressPostalCode,
city: companySearchResult.addressCity,
country: "FR"
};
}

// Un certain nombre d'entreprises ont des addresses du genre "codePostal ville"
// (donc on tolère l'absence de libellé de voie)
// Mais s'il manque le code postal ou la ville, on considère l'adresse comme invalide.
// On retourne null plutôt qu'une adresse semi-complète.
if (
!isDefinedStrict(res.postalCode?.trim()) ||
!isDefinedStrict(res.city?.trim())
) {
return {
street: null,
postalCode: null,
city: null,
country: null
};
}

return res;
};
14 changes: 13 additions & 1 deletion back/src/companies/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,9 +420,17 @@ export async function updateFavorites(orgIds: string[]) {
}
}

interface UpdatedCompanyNameAndAddress
extends Pick<Company, "name" | "address" | "codeNaf"> {
addressVoie: string | null | undefined;
addressPostalCode: string | null | undefined;
addressCity: string | null | undefined;
codePaysEtrangerEtablissement: string | null | undefined;
}

export async function getUpdatedCompanyNameAndAddress(
company: Pick<Company, "name" | "address" | "orgId">
): Promise<Pick<Company, "name" | "address" | "codeNaf"> | null> {
): Promise<UpdatedCompanyNameAndAddress | null> {
let searchResult: null | SireneSearchResult | PartialCompanyVatSearchResult =
null;
if (isSiret(company.orgId)) {
Expand All @@ -440,6 +448,10 @@ export async function getUpdatedCompanyNameAndAddress(
searchResult.address && searchResult.address !== company.address
? searchResult.address
: company.address,
addressVoie: (searchResult as SireneSearchResult).addressVoie,
addressPostalCode: (searchResult as SireneSearchResult).addressPostalCode,
addressCity: (searchResult as SireneSearchResult).addressCity,
codePaysEtrangerEtablissement: searchResult.codePaysEtrangerEtablissement,
codeNaf: (searchResult as SireneSearchResult).naf ?? null
};
}
Expand Down
Loading

0 comments on commit fda221a

Please sign in to comment.