diff --git a/admin/src/scenes/plan-transport/schema-repartition/components/BoxCentres.tsx b/admin/src/scenes/plan-transport/schema-repartition/components/BoxCentres.tsx index 3256cc617e..696f159ecf 100644 --- a/admin/src/scenes/plan-transport/schema-repartition/components/BoxCentres.tsx +++ b/admin/src/scenes/plan-transport/schema-repartition/components/BoxCentres.tsx @@ -10,7 +10,13 @@ import { User } from "@/types"; import FrenchMap from "@/assets/icons/FrenchMap"; import ChevronRight from "@/assets/icons/ChevronRight"; -export default function BoxCentres({ summary, className, loading, isNational, isDepartmental, user }: BoxProps & { isNational?: boolean; isDepartmental?: boolean; user: User }) { +interface BoxCentresProps extends BoxProps { + isNational?: boolean; + isDepartmental?: boolean; + user: User; +} + +export default function BoxCentres({ summary, className, loading, isNational, isDepartmental, user }: BoxCentresProps) { return ( @@ -34,7 +40,7 @@ export default function BoxCentres({ summary, className, loading, isNational, is
  • {region.name}
  • {isDepartmental && (
  • - {region.departments.map((department) => `${department} (${getDepartmentNumber(department)})`).join(", ")} + {region.departments.map((department) => `${department} (${getDepartmentNumber(department.name)})`).join(", ")}
  • )} diff --git a/api/src/__tests__/cohort-session.test.ts b/api/src/__tests__/cohort-session.test.ts index bd18dbc464..dff81fede0 100644 --- a/api/src/__tests__/cohort-session.test.ts +++ b/api/src/__tests__/cohort-session.test.ts @@ -1,9 +1,10 @@ import request from "supertest"; import { fakerFR as faker } from "@faker-js/faker"; +import { addDays, addYears } from "date-fns"; import { Types } from "mongoose"; const { ObjectId } = Types; -import { COHORT_TYPE, ERRORS, ROLES } from "snu-lib"; +import { COHORT_TYPE, ERRORS, GRADES, ROLES, YOUNG_STATUS } from "snu-lib"; import { CohortModel } from "../models"; import { dbConnect, dbClose } from "./helpers/db"; @@ -54,9 +55,40 @@ describe("Cohort Session Controller", () => { zip: "", }); expect(response.status).toBe(200); + expect(response.body.ok).toBe(true); + }); + + it("should return 200 if young data is valid", async () => { + const young = { + department: "Loire-Atlantique", + region: "Pays de la Loire", + schoolRegion: "", + birthdateAt: faker.date.past({ years: 3, refDate: addYears(new Date(), -15) }), + grade: GRADES["2ndeGT"], + status: YOUNG_STATUS.REFUSED, + zip: faker.location.zipCode(), + }; + await createCohortHelper( + getNewCohortFixture({ + inscriptionEndDate: faker.date.future(), + eligibility: { + zones: [young.department!], + schoolLevels: [young.grade || ""], + bornAfter: addYears(new Date(), -18), + bornBefore: addYears(new Date(), -15), + }, + }), + ); + const response = await request(getAppHelper({ role: ROLES.ADMIN })) + .post("/cohort-session/eligibility/2023/") + .send(young); + expect(response.status).toBe(200); + expect(response.body.ok).toBe(true); + expect(Array.isArray(response.body.data)).toBe(true); + expect(response.body.data.length).toBe(1); }); - it("should return filtered sessions if young is valid and admin cle", async () => { + it("admin cle, should return filtered sessions if young is valid", async () => { const young = await createYoungHelper( getNewYoungFixture({ schooled: "false", // not HTZ @@ -72,8 +104,8 @@ describe("Cohort Session Controller", () => { eligibility: { zones: [young.department!], schoolLevels: [young.grade || ""], - bornAfter: new Date("2000-01-01"), - bornBefore: new Date("2010-01-01"), + bornAfter: addYears(new Date(), -18), + bornBefore: addYears(new Date(), -15), }, }), ); @@ -85,8 +117,8 @@ describe("Cohort Session Controller", () => { eligibility: { zones: [young.department!], schoolLevels: [young.grade || ""], - bornAfter: new Date("2000-01-01"), - bornBefore: new Date("2010-01-01"), + bornAfter: addYears(new Date(), -18), + bornBefore: addYears(new Date(), -15), }, }), ); @@ -98,7 +130,7 @@ describe("Cohort Session Controller", () => { expect(response.body.data.length).toBe(1); }); - it("should bypass sessions filter if young is valid and ref dep with getAllSessions is true", async () => { + it("referent dep, should bypass sessions filter with getAllSessions is true if young is valid", async () => { const young = await createYoungHelper( getNewYoungFixture({ schooled: "false", // not HTZ @@ -114,8 +146,8 @@ describe("Cohort Session Controller", () => { eligibility: { zones: [young.department!], schoolLevels: [young.grade || ""], - bornAfter: new Date("2000-01-01"), - bornBefore: new Date("2010-01-01"), + bornAfter: addYears(new Date(), -18), + bornBefore: addYears(new Date(), -15), }, }), ); @@ -127,8 +159,8 @@ describe("Cohort Session Controller", () => { eligibility: { zones: [young.department!], schoolLevels: [young.grade || ""], - bornAfter: new Date("2000-01-01"), - bornBefore: new Date("2010-01-01"), + bornAfter: addYears(new Date(), -18), + bornBefore: addYears(new Date(), -15), }, }), ); @@ -140,7 +172,8 @@ describe("Cohort Session Controller", () => { expect(response.body.data.length).toBe(2); }); - it("should return sessions if young is valid and cohort available", async () => { + it("admin, should return sessions if young is valid and cohort available", async () => { + // résidant+scolarisé dans dép X : si le dép X a un séjour, vérifier que le jeune peut candidater const young = await createYoungHelper( getNewYoungFixture({ schooled: "false", // not HTZ @@ -156,8 +189,8 @@ describe("Cohort Session Controller", () => { eligibility: { zones: [young.department!], schoolLevels: [young.grade || ""], - bornAfter: new Date("2000-01-01"), - bornBefore: new Date("2010-01-01"), + bornAfter: addYears(new Date(), -18), + bornBefore: addYears(new Date(), -15), }, }), ); @@ -168,7 +201,37 @@ describe("Cohort Session Controller", () => { expect(response.body.data.length).toBe(1); }); - it("should return sessions if young is valid and cohort available (HTZ)", async () => { + it("should return no sessions if young is valid and cohort not available for his department", async () => { + // résidant+scolarisé dans dép X : si le dép X n’a PAS de séjour, vérifier que le jeune peut PAS candidater + const young = await createYoungHelper( + getNewYoungFixture({ + schooled: "false", // not HTZ + region: "Pays de la Loire", + department: "Loire-Atlantique", + schoolDepartment: "Loire-Atlantique", + }), + ); + await createCohortHelper( + getNewCohortFixture({ + name: young.cohort, + inscriptionEndDate: faker.date.future(), + eligibility: { + zones: ["Morbihan"], + schoolLevels: [young.grade || ""], + bornAfter: addYears(new Date(), -18), + bornBefore: addYears(new Date(), -15), + }, + }), + ); + const response = await request(getAppHelper({ role: ROLES.ADMIN })).post(`/cohort-session/eligibility/2023/${young._id}`); + expect(response.status).toBe(200); + expect(response.body.ok).toBe(true); + expect(Array.isArray(response.body.data)).toBe(true); + expect(response.body.data.length).toBe(0); + }); + + it("admin, should return sessions if young is valid and cohort available (HTZ)", async () => { + // résidant dans dép Y + scolarisé dans dép X : si le dép X a un séjour, vérifier que le jeune peut candidater const young = await createYoungHelper( getNewYoungFixture({ schooled: "true", // HTZ @@ -184,8 +247,8 @@ describe("Cohort Session Controller", () => { eligibility: { zones: [young.schoolDepartment!], schoolLevels: [young.grade || ""], - bornAfter: new Date("2000-01-01"), - bornBefore: new Date("2010-01-01"), + bornAfter: addYears(new Date(), -18), + bornBefore: addYears(new Date(), -15), }, }), ); @@ -196,17 +259,25 @@ describe("Cohort Session Controller", () => { expect(response.body.data.length).toBe(1); }); - it("should return no sessions if young is valid and cohort not available (wrong dep)", async () => { - const young = await createYoungHelper(getNewYoungFixture({ region: "Pays de la Loire", department: "Loire-Atlantique", schoolDepartment: "Loire-Atlantique" })); + it("admin, should not return sessions if young is valid and cohort not available in his department (HTZ)", async () => { + // résidant dans dép Y + scolarisé dans dép X : si le dép X n’a PAS de séjour, alors vérifier que le jeune ne peut PAS candidaté + const young = await createYoungHelper( + getNewYoungFixture({ + schooled: "true", // HTZ + region: "Pays de la Loire", + department: "Morbihan", + schoolDepartment: "Loire-Atlantique", + }), + ); await createCohortHelper( getNewCohortFixture({ name: young.cohort, inscriptionEndDate: faker.date.future(), eligibility: { - zones: ["A"], + zones: [young.department!], schoolLevels: [young.grade || ""], - bornAfter: new Date("2000-01-01"), - bornBefore: new Date("2010-01-01"), + bornAfter: addYears(new Date(), -18), + bornBefore: addYears(new Date(), -15), }, }), ); @@ -217,7 +288,35 @@ describe("Cohort Session Controller", () => { expect(response.body.data.length).toBe(0); }); - it("should return no sessions if young is valid and cohort not available (inscriptionEnded)", async () => { + it("admin, should return no sessions if young is valid and cohort not available (wrong dep)", async () => { + // Cas d’une région qui ne participe pas en totalité à un séjour (seul un département participe) + const young = await createYoungHelper( + getNewYoungFixture({ + region: "Pays de la Loire", + department: "Loire-Atlantique", + schoolDepartment: "Loire-Atlantique", + }), + ); + await createCohortHelper( + getNewCohortFixture({ + name: young.cohort, + inscriptionEndDate: faker.date.future(), + eligibility: { + zones: ["Morbihan"], + schoolLevels: [young.grade || ""], + bornAfter: addYears(new Date(), -18), + bornBefore: addYears(new Date(), -15), + }, + }), + ); + const response = await request(getAppHelper({ role: ROLES.ADMIN })).post(`/cohort-session/eligibility/2023/${young._id}`); + expect(response.status).toBe(200); + expect(response.body.ok).toBe(true); + expect(Array.isArray(response.body.data)).toBe(true); + expect(response.body.data.length).toBe(0); + }); + + it("admin, should return no sessions if young is valid and cohort not available (inscriptionEnded)", async () => { const young = await createYoungHelper(getNewYoungFixture({ region: "Pays de la Loire", department: "Loire-Atlantique", schoolDepartment: "Loire-Atlantique" })); await createCohortHelper( getNewCohortFixture({ @@ -225,8 +324,8 @@ describe("Cohort Session Controller", () => { eligibility: { zones: [young.department!], schoolLevels: [young.grade || ""], - bornAfter: new Date("2000-01-01"), - bornBefore: new Date("2010-01-01"), + bornAfter: addYears(new Date(), -18), + bornBefore: addYears(new Date(), -15), }, }), ); @@ -236,10 +335,67 @@ describe("Cohort Session Controller", () => { expect(Array.isArray(response.body.data)).toBe(true); expect(response.body.data.length).toBe(0); }); + + it("admin, should return no sessions if young has invalid birthdate and cohort is available", async () => { + const cohort = await createCohortHelper( + getNewCohortFixture({ + inscriptionEndDate: faker.date.future(), + dateStart: addDays(addYears(new Date(), -18), -7), + dateEnd: addDays(addYears(new Date(), -18), 0), + eligibility: { + zones: ["Loire-Atlantique"], + schoolLevels: [GRADES["2ndeGT"]], + bornAfter: addYears(new Date(), -18), + bornBefore: addYears(new Date(), -15), + }, + }), + ); + // 15 ans à J-1 + let young = await createYoungHelper( + getNewYoungFixture({ + cohort: cohort.name, + region: "Pays de la Loire", + department: cohort.eligibility.zones[0], + schoolDepartment: cohort.eligibility.zones[0], + birthdateAt: addDays(addYears(new Date(), -15), 1), + }), + ); + let response = await request(getAppHelper({ role: ROLES.ADMIN })).post(`/cohort-session/eligibility/2023/${young._id}`); + expect(response.status).toBe(200); + expect(response.body.data.length).toBe(0); + + // 18 ans au cours du jour du séjour + young = await createYoungHelper( + getNewYoungFixture({ + cohort: cohort.name, + region: "Pays de la Loire", + department: cohort.eligibility.zones[0], + schoolDepartment: cohort.eligibility.zones[0], + birthdateAt: addDays(addYears(new Date(), -18), 3), + }), + ); + response = await request(getAppHelper({ role: ROLES.ADMIN })).post(`/cohort-session/eligibility/2023/${young._id}`); + expect(response.status).toBe(200); + expect(response.body.data.length).toBe(0); + + // 18 au dernier jour du séjour + young = await createYoungHelper( + getNewYoungFixture({ + cohort: cohort.name, + region: "Pays de la Loire", + department: cohort.eligibility.zones[0], + schoolDepartment: cohort.eligibility.zones[0], + birthdateAt: addDays(addYears(new Date(), -18), 0), + }), + ); + response = await request(getAppHelper({ role: ROLES.ADMIN })).post(`/cohort-session/eligibility/2023/${young._id}`); + expect(response.status).toBe(200); + expect(response.body.data.length).toBe(0); + }); }); describe("GET /api/cohort-session/isReInscriptionOpen", () => { - it("should return 200 OK with data", async () => { + it("admin, should return 200 OK with data", async () => { await createCohortHelper( getNewCohortFixture({ type: COHORT_TYPE.VOLONTAIRE, diff --git a/api/src/__tests__/fixtures/young.ts b/api/src/__tests__/fixtures/young.ts index 96f935daa6..7d8df7261a 100644 --- a/api/src/__tests__/fixtures/young.ts +++ b/api/src/__tests__/fixtures/young.ts @@ -1,4 +1,5 @@ import { fakerFR as faker } from "@faker-js/faker"; +import { addYears } from "date-fns"; import { departmentList, regionList, YoungType } from "snu-lib"; function randomDepartment() { @@ -22,7 +23,7 @@ export default function getNewYoungFixture(fields: Partial = {}): Par phone: faker.phone.number(), phoneZone: "FRANCE", gender: faker.person.gender(), - birthdateAt: faker.date.past({ years: 1, refDate: "01/01/2007" }), + birthdateAt: faker.date.past({ years: 3, refDate: addYears(new Date(), -15) }), cohort: "Juillet 2023", acceptCGU: "true", phase: "CONTINUE", diff --git a/api/src/utils/cohort.ts b/api/src/utils/cohort.ts index 309694bb61..da358b0e33 100644 --- a/api/src/utils/cohort.ts +++ b/api/src/utils/cohort.ts @@ -10,17 +10,21 @@ export type CohortDocumentWithPlaces = CohortDocument<{ isEligible?: boolean; }>; +type YoungInfo = Pick & { + isReInscription?: boolean; +}; + // TODO: déplacer isReInscription dans un nouveau params plutot que dans le young -export async function getFilteredSessions(young: YoungType & { isReInscription?: boolean }, timeZoneOffset?: string | number | null) { +export async function getFilteredSessions(young: YoungInfo, timeZoneOffset?: string | number | null) { const cohorts = await CohortModel.find({}); const region = getRegionForEligibility(young); const department = getDepartmentForEligibility(young); const currentCohortYear = young.cohort ? new Date(cohorts.find((c) => c.name === young.cohort)?.dateStart || "")?.getFullYear() : undefined; - const sessions: CohortDocumentWithPlaces[] = cohorts.filter( - (session) => + const sessions: CohortDocumentWithPlaces[] = cohorts.filter((session) => { // if the young has already a cohort, he can only apply for the cohorts of the same year + return ( (!young.cohort || currentCohortYear === session.dateStart.getFullYear()) && session.eligibility?.zones.includes(department) && session.eligibility?.schoolLevels.includes(young.grade || "") && @@ -30,15 +34,16 @@ export async function getFilteredSessions(young: YoungType & { isReInscription?: session.eligibility?.bornBefore.setTime(session.eligibility?.bornBefore.getTime() + 11 * 60 * 60 * 1000) >= young.birthdateAt && (session.getIsInscriptionOpen(Number(timeZoneOffset)) || (session.getIsReInscriptionOpen(Number(timeZoneOffset)) && young.isReInscription) || - (session.getIsInstructionOpen(Number(timeZoneOffset)) && ([YOUNG_STATUS.WAITING_CORRECTION, YOUNG_STATUS.WAITING_VALIDATION] as string[]).includes(young.status))), + (session.getIsInstructionOpen(Number(timeZoneOffset)) && ([YOUNG_STATUS.WAITING_CORRECTION, YOUNG_STATUS.WAITING_VALIDATION] as string[]).includes(young.status))) ); + }); for (let session of sessions) { session.isEligible = true; } return getPlaces(sessions, region); } -export async function getAllSessions(young: YoungType) { +export async function getAllSessions(young: YoungInfo) { const cohorts = await CohortModel.find({}); const region = getRegionForEligibility(young); const sessionsWithPlaces = await getPlaces(cohorts, region); diff --git a/packages/lib/src/region-and-departments.ts b/packages/lib/src/region-and-departments.ts index f5d815f52c..a9a4b7118b 100644 --- a/packages/lib/src/region-and-departments.ts +++ b/packages/lib/src/region-and-departments.ts @@ -1,3 +1,5 @@ +import { YoungType } from "./mongoSchema"; + const departmentLookUp = { "01": "Ain", "02": "Aisne", @@ -112,9 +114,9 @@ const departmentLookUp = { const departmentList = Object.values(departmentLookUp); -const getDepartmentNumber = (d) => Object.keys(departmentLookUp).find((key) => departmentLookUp[key] === d); +const getDepartmentNumber = (depNum: string | number) => Object.keys(departmentLookUp).find((key) => departmentLookUp[key] === depNum); -const getDepartmentByZip = (zip) => { +const getDepartmentByZip = (zip?: string) => { if (!zip) return; if (zip.length !== 5) return; @@ -127,7 +129,7 @@ const getDepartmentByZip = (zip) => { return departmentLookUp[departmentCode]; }; -const getRegionByZip = (zip) => { +const getRegionByZip = (zip?: string) => { if (!zip) return; if (zip.length !== 5) return; @@ -340,7 +342,7 @@ const region2zone = { Etranger: "Etranger", }; -const getRegionForEligibility = (young) => { +const getRegionForEligibility = (young: Pick) => { let region = young.schooled === "true" ? young.schoolRegion : young.region; if (!region) { let dep = young?.schoolDepartment || young?.department || getDepartmentByZip(young?.zip); @@ -354,7 +356,7 @@ const getRegionForEligibility = (young) => { return region; }; -const getDepartmentForEligibility = (young) => { +const getDepartmentForEligibility = (young: Pick) => { let dep; if (young._id && young.schooled === "true") dep = young.schoolDepartment; if (young._id && young.schooled === "false") dep = young.department; @@ -368,23 +370,23 @@ const getDepartmentForEligibility = (young) => { return dep; }; -const isFromMetropole = (young) => { +const isFromMetropole = (young: YoungType) => { const region = getRegionForEligibility(young); return region2zone[region] === "A" || region2zone[region] === "B" || region2zone[region] === "C"; }; -const isFromDOMTOM = (young) => { +const isFromDOMTOM = (young: YoungType) => { const region = getRegionForEligibility(young); return region2zone[region] === "DOM"; }; -const isFromFrenchPolynesia = (young) => { +const isFromFrenchPolynesia = (young: YoungType) => { const region = getRegionForEligibility(young); return region2zone[region] === "PF"; }; // attention avant l'utilisation : depuis juillet 2023 WF est aussi attaché à la zone + region NC sur la plateforme (avant c'était region WF et zone DOM) -const isFromNouvelleCaledonie = (young) => { +const isFromNouvelleCaledonie = (young: YoungType) => { const region = getRegionForEligibility(young); return region2zone[region] === "NC"; }; diff --git a/packages/lib/src/routes/cohort/getEligibility.ts b/packages/lib/src/routes/cohort/getEligibility.ts index d67715d470..f0d16bf1d8 100644 --- a/packages/lib/src/routes/cohort/getEligibility.ts +++ b/packages/lib/src/routes/cohort/getEligibility.ts @@ -20,7 +20,7 @@ export interface GetEligibilityRoute extends BasicRoute { }; response: RouteResponseBody< Array< - CohortType & { + Pick & { numberOfCandidates?: number; numberOfValidated?: number; goal?: number;