diff --git a/back/src/domains/establishment/adapters/PgSearchMadeRepository.integration.test.ts b/back/src/domains/establishment/adapters/PgSearchMadeRepository.integration.test.ts index a8d51b5851..3879149fef 100644 --- a/back/src/domains/establishment/adapters/PgSearchMadeRepository.integration.test.ts +++ b/back/src/domains/establishment/adapters/PgSearchMadeRepository.integration.test.ts @@ -1,28 +1,20 @@ import { Pool } from "pg"; -import { - AppellationCode, - WithAcquisition, - expectObjectsToMatch, - expectToEqual, -} from "shared"; +import { AppellationCode, expectToEqual } from "shared"; import { KyselyDb, makeKyselyDb } from "../../../config/pg/kysely/kyselyUtils"; import { getTestPgPool, optional } from "../../../config/pg/pgUtils"; -import { SearchMadeEntity } from "../entities/SearchMadeEntity"; +import { GeoParams, SearchMadeEntity } from "../entities/SearchMadeEntity"; import { PgSearchMadeRepository } from "./PgSearchMadeRepository"; -const defaultSearchMade: SearchMadeEntity = { - id: "9f6dad2c-6f02-11ec-90d6-0242ac120003", - distanceKm: 30, - lat: 48.119146, - lon: 4.17602, - needsToBeSearched: true, - sortedBy: "distance", - place: "Nantes", - voluntaryToImmersion: true, - numberOfResults: 1, -}; - describe("PgSearchesMadeRepository", () => { + const searchMadeWithoutLocation: SearchMadeEntity = { + id: "9f6dad2c-6f02-11ec-90d6-0242ac120003", + needsToBeSearched: true, + sortedBy: "distance", + place: "Nantes", + voluntaryToImmersion: true, + numberOfResults: 1, + }; + let pool: Pool; let db: KyselyDb; let pgSearchesMadeRepository: PgSearchMadeRepository; @@ -42,94 +34,153 @@ describe("PgSearchesMadeRepository", () => { await pool.end(); }); - it("insert a search made", async () => { - await pgSearchesMadeRepository.insertSearchMade(defaultSearchMade); - const retrievedSearchMade = await getSearchMadeById(defaultSearchMade.id); - expectToEqual(retrievedSearchMade, defaultSearchMade); + describe("with geo params", () => { + it.each([ + { + distanceKm: 30, + lat: 48.85909, + lon: 2.350196, + expectedDepartmentCode: "75", + }, + { + distanceKm: 2, + lat: 47.22221, + lon: -1.54212, + expectedDepartmentCode: "44", + }, + { + distanceKm: 3.2, + lat: 46.159457943708254, + lon: 0.15399270230344264, + expectedDepartmentCode: "86", + }, + { + distanceKm: 9, + lat: 46.15944708515133, + lon: 0.15392947992255213, + expectedDepartmentCode: "79", + }, + { + distanceKm: 9, + lat: -21.154576445369, + lon: 55.40104947929245, + expectedDepartmentCode: "974", + }, + ])( + "insert a search made with geo params (lat '$lat' , lon '$lon' , '$distanceKm' km) and save departement code with value '$expectedDepartmentCode'", + async ({ distanceKm, lat, lon, expectedDepartmentCode }) => { + const searchMadeWithLocation: SearchMadeEntity = { + ...searchMadeWithoutLocation, + distanceKm, + lat, + lon, + }; + + await pgSearchesMadeRepository.insertSearchMade(searchMadeWithLocation); + + expectToEqual( + await getSearchMadeById(db, searchMadeWithLocation.id), + searchMadeWithLocation, + ); + expectToEqual( + await db + .selectFrom("searches_made") + .select("department_code") + .where("id", "=", searchMadeWithLocation.id) + .executeTakeFirst(), + { department_code: expectedDepartmentCode }, + ); + }, + ); + }); + + it("insert a search made without location and have departement code null", async () => { + await pgSearchesMadeRepository.insertSearchMade(searchMadeWithoutLocation); + + expectToEqual( + await getSearchMadeById(db, searchMadeWithoutLocation.id), + searchMadeWithoutLocation, + ); + expectToEqual( + await db + .selectFrom("searches_made") + .select("department_code") + .where("id", "=", searchMadeWithoutLocation.id) + .executeTakeFirst(), + { department_code: null }, + ); }); it("insert a search made and keeps acquisition params", async () => { - const withAcquisition = { + const searchMade: SearchMadeEntity = { + ...searchMadeWithoutLocation, acquisitionKeyword: "acquisition-keyword", acquisitionCampaign: "acquisition-campaign", - } satisfies WithAcquisition; - await pgSearchesMadeRepository.insertSearchMade({ - ...defaultSearchMade, - ...withAcquisition, - }); + }; - const results = await db.selectFrom("searches_made").selectAll().execute(); + await pgSearchesMadeRepository.insertSearchMade(searchMade); - expect(results).toHaveLength(1); - expectObjectsToMatch(results[0], { - id: defaultSearchMade.id, - acquisition_keyword: withAcquisition.acquisitionKeyword, - acquisition_campaign: withAcquisition.acquisitionCampaign, - }); + expectToEqual(await getSearchMadeById(db, searchMade.id), searchMade); }); it("Remove duplicated appellationCodes then insert search", async () => { - const appellationCodes: AppellationCode[] = ["19365", "19365"]; + const appellationCode: AppellationCode = "19365"; + const searchMade: SearchMadeEntity = { - id: "9f6dad2c-6f02-11ec-90d6-0242ac120003", - appellationCodes, - distanceKm: 30, - lat: 48.119146, - lon: 4.17602, - needsToBeSearched: true, - sortedBy: "distance", - place: "Nantes", - voluntaryToImmersion: true, - numberOfResults: 1, + ...searchMadeWithoutLocation, + appellationCodes: [appellationCode, appellationCode], }; + await pgSearchesMadeRepository.insertSearchMade(searchMade); - const retrievedSearchMade = await getSearchMadeById(searchMade.id); - expect(retrievedSearchMade).toEqual({ + expectToEqual(await getSearchMadeById(db, searchMade.id), { ...searchMade, - appellationCodes: [appellationCodes[0]], + appellationCodes: [appellationCode], }); }); +}); + +const getSearchMadeById = async ( + db: KyselyDb, + id: string, +): Promise => { + const searchMadeResult = await db + .selectFrom("searches_made") + .selectAll() + .where("id", "=", id) + .executeTakeFirst(); - const getSearchMadeById = async ( - id: string, - ): Promise => { - const searchMadeResult = await db - .selectFrom("searches_made") - .selectAll() - .where("id", "=", id) - .executeTakeFirst(); - - const appellationCodes: AppellationCode[] = await db - .selectFrom("searches_made__appellation_code") - .selectAll() - .where("search_made_id", "=", id) - .execute() - .then((rows) => - rows - .map(({ appellation_code }) => appellation_code) - .filter( - (appellationCode): appellationCode is AppellationCode => - appellationCode !== null, - ), - ); - - return ( - searchMadeResult && { - id: searchMadeResult.id, - distanceKm: optional(searchMadeResult.distance), - lat: optional(searchMadeResult.lat), - lon: optional(searchMadeResult.lon), - sortedBy: optional(searchMadeResult.sorted_by), - voluntaryToImmersion: optional(searchMadeResult.voluntary_to_immersion), - place: optional(searchMadeResult.address), - needsToBeSearched: searchMadeResult.needstobesearched ?? false, - appellationCodes: appellationCodes.length - ? appellationCodes - : undefined, - apiConsumerName: optional(searchMadeResult.api_consumer_name), - numberOfResults: optional(searchMadeResult.number_of_results) ?? 0, - } + const appellationCodes: AppellationCode[] = await db + .selectFrom("searches_made__appellation_code") + .selectAll() + .where("search_made_id", "=", id) + .execute() + .then((rows) => + rows + .map(({ appellation_code }) => appellation_code) + .filter( + (appellationCode): appellationCode is AppellationCode => + appellationCode !== null, + ), ); - }; -}); + + return ( + searchMadeResult && { + id: searchMadeResult.id, + distanceKm: optional(searchMadeResult.distance), + lat: optional(searchMadeResult.lat), + lon: optional(searchMadeResult.lon), + sortedBy: optional(searchMadeResult.sorted_by), + voluntaryToImmersion: optional(searchMadeResult.voluntary_to_immersion), + place: optional(searchMadeResult.address), + needsToBeSearched: searchMadeResult.needstobesearched ?? false, + appellationCodes: appellationCodes.length ? appellationCodes : undefined, + apiConsumerName: optional(searchMadeResult.api_consumer_name), + numberOfResults: optional(searchMadeResult.number_of_results) ?? 0, + acquisitionCampaign: optional(searchMadeResult.acquisition_campaign), + acquisitionKeyword: optional(searchMadeResult.acquisition_keyword), + establishmentSearchableBy: optional(searchMadeResult.searchable_by), + romeCode: optional(searchMadeResult.rome), + } + ); +}; diff --git a/back/src/domains/establishment/adapters/PgSearchMadeRepository.ts b/back/src/domains/establishment/adapters/PgSearchMadeRepository.ts index df36a94d1a..7a4f003f96 100644 --- a/back/src/domains/establishment/adapters/PgSearchMadeRepository.ts +++ b/back/src/domains/establishment/adapters/PgSearchMadeRepository.ts @@ -1,6 +1,7 @@ +import { sql } from "kysely"; import { uniq } from "ramda"; import { AppellationCode } from "shared"; -import { KyselyDb } from "../../../config/pg/kysely/kyselyUtils"; +import { KyselyDb, values } from "../../../config/pg/kysely/kyselyUtils"; import { SearchMadeEntity, SearchMadeId, @@ -25,32 +26,132 @@ export class PgSearchMadeRepository implements SearchMadeRepository { async #insertSearchMade(searchMade: SearchMadeEntity) { await this.transaction .insertInto("searches_made") - .values({ - id: searchMade.id, - rome: searchMade.romeCode, - needstobesearched: searchMade.needsToBeSearched, - voluntary_to_immersion: searchMade.voluntaryToImmersion, - api_consumer_name: searchMade.apiConsumerName, - sorted_by: searchMade.sortedBy, - address: searchMade.place, - number_of_results: searchMade.numberOfResults, - searchable_by: searchMade.establishmentSearchableBy, - acquisition_keyword: searchMade.acquisitionKeyword, - acquisition_campaign: searchMade.acquisitionCampaign, - ...(hasSearchMadeGeoParams(searchMade) - ? { - lat: searchMade.lat, - lon: searchMade.lon, - distance: searchMade.distanceKm, - gps: `POINT(${searchMade.lon} ${searchMade.lat})`, - } - : { - lat: 0, - lon: 0, - distance: 0, - gps: "POINT(0 0)", - }), - }) + .columns([ + "acquisition_campaign", + "acquisition_keyword", + "address", + "api_consumer_name", + "department_code", + "distance", + "gps", + "id", + "lat", + "lon", + "needstobesearched", + "number_of_results", + "rome", + "searchable_by", + "sorted_by", + "voluntary_to_immersion", + ]) + .expression((eb) => + eb + .selectFrom( + values( + [ + { + id: searchMade.id, + rome: searchMade.romeCode, + needstobesearched: searchMade.needsToBeSearched, + voluntary_to_immersion: searchMade.voluntaryToImmersion, + api_consumer_name: searchMade.apiConsumerName, + sorted_by: searchMade.sortedBy, + address: searchMade.place, + number_of_results: searchMade.numberOfResults, + searchable_by: searchMade.establishmentSearchableBy, + acquisition_keyword: searchMade.acquisitionKeyword, + acquisition_campaign: searchMade.acquisitionCampaign, + ...(hasSearchMadeGeoParams(searchMade) + ? { + lat: searchMade.lat, + lon: searchMade.lon, + distance: searchMade.distanceKm, + gps: `POINT(${searchMade.lon} ${searchMade.lat})`, + } + : { + lat: null, + lon: null, + distance: null, + gps: null, + }), + }, + ], + "values", + ), + ) + .leftJoin("public_department_region", (join) => + join.on((eb) => + eb.and([ + eb("values.gps", "is not", null), + eb.fn("ST_DWithin", [ + "public_department_region.shape", + sql`${eb.ref("values.gps")}::geography`, + sql`0`, + ]), + ]), + ), + ) + .select((eb) => [ + "values.acquisition_campaign", + "values.acquisition_keyword", + "values.address", + "values.api_consumer_name", + "public_department_region.department_code", + eb.fn + .coalesce( + sql`${eb.ref("values.distance")}::double precision`, + sql`NULL`, + ) + .as("distance"), + eb.fn + .coalesce(sql`${eb.ref("values.gps")}::geography`, sql`NULL`) + .as("gps"), + sql`${eb.ref("values.id")}::uuid`.as("id"), + eb.fn + .coalesce( + sql`${eb.ref("values.lat")}::double precision`, + sql`NULL`, + ) + .as("lat"), + eb.fn + .coalesce( + sql`${eb.ref("values.lon")}::double precision`, + sql`NULL`, + ) + .as("lon"), + eb.fn + .coalesce( + sql`${eb.ref("values.needstobesearched")}::boolean`, + sql`NULL`, + ) + .as("needstobesearched"), + eb.fn + .coalesce( + sql`${eb.ref("values.number_of_results")}::integer`, + sql`NULL`, + ) + .as("number_of_results"), + "values.rome", + eb.fn + .coalesce( + sql`${eb.ref("values.searchable_by")}::searchable_by`, + sql`NULL`, + ) + .as("searchable_by"), + eb.fn + .coalesce( + sql`${eb.ref("values.sorted_by")}::sorted_by`, + sql`NULL`, + ) + .as("sorted_by"), + eb.fn + .coalesce( + sql`${eb.ref("values.voluntary_to_immersion")}::boolean`, + sql`NULL`, + ) + .as("voluntary_to_immersion"), + ]), + ) .execute(); }