diff --git a/api/src/pdc/services/policy/engine/helpers/dateRange.ts b/api/src/pdc/services/policy/engine/helpers/dateRange.ts new file mode 100644 index 000000000..6e4923de7 --- /dev/null +++ b/api/src/pdc/services/policy/engine/helpers/dateRange.ts @@ -0,0 +1,56 @@ +/** + * Generate a list of dates between two dates + * + * @example + * const boosterDates = [ + * ...dateRange("2022-01-01", "2022-01-03"), + * ]; + * + * @param dates + * @returns + */ +export function dateRange(...dates: Array): string[] { + if (dates.length === 0) { + throw new Error("At least one date is required"); + } + + const [first, ...rest] = castAndSort(dates); + const last = rest.pop(); + + return last ? fill(first, last) : [format(first)]; +} + +function castAndSort(range: Array): Set { + return range + .map((d) => { + const date = typeof d === "string" ? new Date(d) : d; + if (Number.isNaN(date.getTime())) { + throw new Error(`Invalid Date`); + } + date.setHours(0, 0, 0, 0); + return date; + }) + .sort((a, b) => a.getTime() - b.getTime()) + .reduce((acc, d) => { + acc.add(d); + return acc; + }, new Set()); +} + +function format(d: Date): string { + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, "0"); + const day = String(d.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; +} + +function fill(first: Date, last: Date): Array { + const dates = new Set(); + const current = new Date(first); + while (current <= last) { + dates.add(format(current)); + current.setDate(current.getDate() + 1); + } + + return [...dates]; +} diff --git a/api/src/pdc/services/policy/engine/helpers/dateRange.unit.spec.ts b/api/src/pdc/services/policy/engine/helpers/dateRange.unit.spec.ts new file mode 100644 index 000000000..2c16a8e93 --- /dev/null +++ b/api/src/pdc/services/policy/engine/helpers/dateRange.unit.spec.ts @@ -0,0 +1,98 @@ +import { assertEquals, describe, it } from "@/dev_deps.ts"; +import { dateRange } from "./dateRange.ts"; + +describe("dateRange", () => { + it("should return a range of dates", () => { + const start = new Date("2022-01-01"); + const end = new Date("2022-01-03"); + const result = dateRange(start, end); + assertEquals(result, ["2022-01-01", "2022-01-02", "2022-01-03"]); + }); + + it("should return a range of dates when given strings", () => { + const start = "2022-01-01"; + const end = "2022-01-03"; + const result = dateRange(start, end); + assertEquals(result, ["2022-01-01", "2022-01-02", "2022-01-03"]); + }); + + it("should return a range over several months", () => { + const start = "2022-01-30"; + const end = "2022-02-02"; + const result = dateRange(start, end); + assertEquals(result, ["2022-01-30", "2022-01-31", "2022-02-01", "2022-02-02"]); + }); + + it("should sort input dates", () => { + const start = "2022-01-03"; + const end = "2022-01-01"; + const result = dateRange(start, end); + assertEquals(result, ["2022-01-01", "2022-01-02", "2022-01-03"]); + }); + + it("should sort more than two dates", () => { + const start = "2022-01-03"; + const middle = "2022-01-02"; + const end = "2022-01-01"; + const result = dateRange(start, middle, end); + assertEquals(result, ["2022-01-01", "2022-01-02", "2022-01-03"]); + }); + + it("should handle no input", () => { + try { + dateRange(); + } catch (e) { + assertEquals(e.message, "At least one date is required"); + } + }); + + it("should handle invalid date format", () => { + const start = "2022-13-45"; + const end = "2022-01-03"; + try { + dateRange(start, end); + } catch (e) { + assertEquals(e.message, "Invalid Date"); + } + }); + + it("should handle malformed date strings", () => { + const start = "not-a-date"; + const end = "2022-01-03"; + try { + dateRange(start, end); + } catch (e) { + assertEquals(e.message, "Invalid Date"); + } + }); + + it("should handle empty string input", () => { + const start = ""; + const end = "2022-01-03"; + try { + dateRange(start, end); + } catch (e) { + assertEquals(e.message, "Invalid Date"); + } + }); + + it("should handle single date input", () => { + const date = "2022-01-01"; + const result = dateRange(date); + assertEquals(result, ["2022-01-01"]); + }); + + it("should handle same date for start and end", () => { + const start = "2022-01-01"; + const end = "2022-01-01"; + const result = dateRange(start, end); + assertEquals(result, ["2022-01-01"]); + }); + + it("should handle large date ranges", () => { + const start = "2022-01-01"; + const end = "2022-12-31"; + const result = dateRange(start, end); + assertEquals(result.length, 365); + }); +}); diff --git a/api/src/pdc/services/policy/engine/policies/20240101_NantesMetropole.html.ts b/api/src/pdc/services/policy/engine/policies/20240101_NantesMetropole.html.ts index 731b2962e..372a722cf 100644 --- a/api/src/pdc/services/policy/engine/policies/20240101_NantesMetropole.html.ts +++ b/api/src/pdc/services/policy/engine/policies/20240101_NantesMetropole.html.ts @@ -1,5 +1,4 @@ -export const description = - `
+export const description = `

Campagne d'incitation au covoiturage du 01 janvier 2024 au 31 Décembre 2025

@@ -26,7 +25,25 @@ export const description =
  • De 17 à 29,5 km : 0.10 € par trajet par km par passager avec un maximum de 2,90 €
  • De 29,5 à 60 km : 2,90 € par passager transporté
  • - + +

    + Les trajets au départ OU à l'arrivée dans Nantes Métropôle effectués au sein + des Pays de la Loire sont incités selon les règles suivantes dans les conditions + "booster" uniquement : +

    + +
      +
    • De 5 à 60 km : 0,90 € par passager transporté
    • +
    + +

    + Les mois déclarés comme "booster" sont les suivants : +

    + +
      +
    • Décembre 2024
    • +
    +

    Les restrictions suivantes seront appliquées :

      diff --git a/api/src/pdc/services/policy/engine/policies/20240101_NantesMetropole.ts b/api/src/pdc/services/policy/engine/policies/20240101_NantesMetropole.ts index 494571e8f..7ac493a86 100644 --- a/api/src/pdc/services/policy/engine/policies/20240101_NantesMetropole.ts +++ b/api/src/pdc/services/policy/engine/policies/20240101_NantesMetropole.ts @@ -1,9 +1,7 @@ import { Timezone } from "@/pdc/providers/validator/types.ts"; import { NotEligibleTargetException } from "@/pdc/services/policy/engine/exceptions/NotEligibleTargetException.ts"; -import { - getOperatorsAt, - TimestampedOperators, -} from "@/pdc/services/policy/engine/helpers/getOperatorsAt.ts"; +import { dateRange } from "@/pdc/services/policy/engine/helpers/dateRange.ts"; +import { getOperatorsAt, TimestampedOperators } from "@/pdc/services/policy/engine/helpers/getOperatorsAt.ts"; import { isAdultOrThrow } from "@/pdc/services/policy/engine/helpers/isAdultOrThrow.ts"; import { isOperatorClassOrThrow } from "@/pdc/services/policy/engine/helpers/isOperatorClassOrThrow.ts"; import { isOperatorOrThrow } from "@/pdc/services/policy/engine/helpers/isOperatorOrThrow.ts"; @@ -14,15 +12,9 @@ import { watchForPersonMaxAmountByYear, watchForPersonMaxTripByDay, } from "@/pdc/services/policy/engine/helpers/limits.ts"; -import { - onDistanceRange, - onDistanceRangeOrThrow, -} from "@/pdc/services/policy/engine/helpers/onDistanceRange.ts"; +import { onDistanceRange, onDistanceRangeOrThrow } from "@/pdc/services/policy/engine/helpers/onDistanceRange.ts"; import { perKm, perSeat } from "@/pdc/services/policy/engine/helpers/per.ts"; -import { - endsAt, - startsAt, -} from "@/pdc/services/policy/engine/helpers/position.ts"; +import { endsAt, startsAt } from "@/pdc/services/policy/engine/helpers/position.ts"; import { AbstractPolicyHandler } from "@/pdc/services/policy/engine/policies/AbstractPolicyHandler.ts"; import { toTzString } from "@/pdc/services/policy/helpers/index.ts"; import { RunnableSlices } from "@/pdc/services/policy/interfaces/engine/PolicyInterface.ts"; @@ -36,23 +28,33 @@ import { import { description } from "./20240101_NantesMetropole.html.ts"; // Politique de Pays de la Loire 2024 -/* eslint-disable-next-line */ -export const NantesMetropole2024: PolicyHandlerStaticInterface = class - extends AbstractPolicyHandler +export const NantesMetropole2024: PolicyHandlerStaticInterface = class extends AbstractPolicyHandler implements PolicyHandlerInterface { static readonly id = "nantes_metropole_2024"; static readonly tz: Timezone = "Europe/Paris"; - public static mode(date: Date, regular: T, booster: T): T { - if (!NantesMetropole2024.boosterDates) return regular; + public static mode( + date: Date, + isInside: boolean, + regularInside: T, + regularOutside: T, + boosterInside: T, + boosterOutside: T, + ): T { + if (!NantesMetropole2024.boosterDates) return isInside ? regularInside : regularOutside; + const ymd = toTzString(date).slice(0, 10); - return NantesMetropole2024.boosterDates.includes(ymd) ? booster : regular; + if (NantesMetropole2024.boosterDates.includes(ymd)) { + return isInside ? boosterInside : boosterOutside; + } else { + return isInside ? regularInside : regularOutside; + } } // Liste de dates au format YYYY-MM-DD dans la zone Europe/Paris // pour lesquelles les règles de booster s'appliquent - protected boosterDates: string[] = [ - // Configure the booster dates here! + protected static boosterDates: string[] = [ + ...dateRange("2024-12-01", "2024-12-31"), ]; protected operators: TimestampedOperators = [ @@ -75,7 +77,7 @@ export const NantesMetropole2024: PolicyHandlerStaticInterface = class }, ]; - protected regularSlices: RunnableSlices = [ + protected regularInsideSlices: RunnableSlices = [ { start: 5_000, end: 17_000, @@ -86,13 +88,7 @@ export const NantesMetropole2024: PolicyHandlerStaticInterface = class end: 29_500, fn: (ctx: StatelessContextInterface) => { // 0,10 euro par trajet par km par passager avec un maximum de 2,00 euros - return perSeat( - ctx, - Math.min( - perKm(ctx, { amount: 10, offset: 17_000, limit: 29_500 }), - 200 - 75, - ), - ); + return perSeat(ctx, Math.min(perKm(ctx, { amount: 10, offset: 17_000, limit: 29_500 }), 200 - 75)); }, }, { @@ -102,7 +98,25 @@ export const NantesMetropole2024: PolicyHandlerStaticInterface = class }, ]; - protected boosterSlices: RunnableSlices = [ + protected regularOutsideSlices: RunnableSlices = [ + { + start: 5_000, + end: 17_000, + fn: () => 0, + }, + { + start: 17_000, + end: 29_500, + fn: () => 0, + }, + { + start: 29_500, + end: 60_000, + fn: () => 0, + }, + ]; + + protected boosterInsideSlices: RunnableSlices = [ { start: 5_000, end: 17_000, @@ -113,13 +127,7 @@ export const NantesMetropole2024: PolicyHandlerStaticInterface = class end: 29_500, fn: (ctx: StatelessContextInterface) => { // 0,10 euro par trajet par km par passager avec un maximum de 2,90 euros - return perSeat( - ctx, - Math.min( - perKm(ctx, { amount: 10, offset: 17_000, limit: 29_500 }), - 290 - 165, - ), - ); + return perSeat(ctx, Math.min(perKm(ctx, { amount: 10, offset: 17_000, limit: 29_500 }), 290 - 165)); }, }, { @@ -129,6 +137,24 @@ export const NantesMetropole2024: PolicyHandlerStaticInterface = class }, ]; + protected boosterOutsideSlices: RunnableSlices = [ + { + start: 5_000, + end: 17_000, + fn: (ctx: StatelessContextInterface) => perSeat(ctx, 90), + }, + { + start: 17_000, + end: 29_500, + fn: () => 0, + }, + { + start: 29_500, + end: 60_000, + fn: () => 0, + }, + ]; + constructor(public max_amount: number) { super(); this.limits = [ @@ -158,31 +184,8 @@ export const NantesMetropole2024: PolicyHandlerStaticInterface = class ]; } - protected processExclusion(ctx: StatelessContextInterface) { - isOperatorOrThrow( - ctx, - getOperatorsAt(this.operators, ctx.carpool.datetime), - ); - onDistanceRangeOrThrow(ctx, { min: 5_000, max: 60_001 }); - isOperatorClassOrThrow(ctx, ["C"]); - isAdultOrThrow(ctx); - - // Exclusion des OD des autres régions - if (!startsAt(ctx, { reg: ["52"] }) || !endsAt(ctx, { reg: ["52"] })) { - throw new NotEligibleTargetException(); - } - - // Exclusion des OD des autres AOM - if ( - !startsAt(ctx, { aom: ["244400404"] }) || - !endsAt(ctx, { aom: ["244400404"] }) - ) { - throw new NotEligibleTargetException(); - } - } - - processStateless(ctx: StatelessContextInterface): void { - this.processExclusion(ctx); + public processStateless(ctx: StatelessContextInterface): void { + this.processExclusions(ctx); super.processStateless(ctx); if (!NantesMetropole2024.mode) { @@ -190,11 +193,15 @@ export const NantesMetropole2024: PolicyHandlerStaticInterface = class } let amount = 0; - const slices = NantesMetropole2024.mode( + const slices = NantesMetropole2024.mode( ctx.carpool.datetime, - this.regularSlices, - this.boosterSlices, + this.isInsideNantesMetropole(ctx), + this.regularInsideSlices, + this.regularOutsideSlices, + this.boosterInsideSlices, + this.boosterOutsideSlices, ); + for (const { start, fn } of slices) { if (onDistanceRange(ctx, { min: start })) { amount += fn(ctx); @@ -204,28 +211,61 @@ export const NantesMetropole2024: PolicyHandlerStaticInterface = class ctx.incentive.set(amount); } - params(date?: Date): PolicyHandlerParamsInterface { + public params(date?: Date): PolicyHandlerParamsInterface { if (!NantesMetropole2024.mode) { throw new Error("NantesMetropole2024.mode is not defined"); } + const slices = date + ? NantesMetropole2024.mode( + date, + true, + this.regularInsideSlices, + this.regularOutsideSlices, + this.boosterInsideSlices, + this.boosterOutsideSlices, + ) + : this.regularInsideSlices; + return { tz: NantesMetropole2024.tz, - slices: date - ? NantesMetropole2024.mode(date, this.regularSlices, this.boosterSlices) - : this.regularSlices, - booster_dates: this.boosterDates, + slices, + booster_dates: NantesMetropole2024.boosterDates, operators: getOperatorsAt(this.operators), - allTimeOperators: Array.from( - new Set(this.operators.flatMap((entry) => entry.operators)), - ), + allTimeOperators: Array.from(new Set(this.operators.flatMap((entry) => entry.operators))), limits: { glob: this.max_amount, }, }; } - describe(): string { + public describe(): string { return description; } + + protected processExclusions(ctx: StatelessContextInterface) { + isOperatorOrThrow(ctx, getOperatorsAt(this.operators, ctx.carpool.datetime)); + onDistanceRangeOrThrow(ctx, { min: 5_000, max: 60_001 }); + isOperatorClassOrThrow(ctx, ["C"]); + isAdultOrThrow(ctx); + + this.isInsideTheRegion(ctx); + this.startsOrEndsInNantesMetropole(ctx); + } + + protected startsOrEndsInNantesMetropole(ctx: StatelessContextInterface) { + if (!startsAt(ctx, { aom: ["244400404"] }) && !endsAt(ctx, { aom: ["244400404"] })) { + throw new NotEligibleTargetException(); + } + } + + protected isInsideTheRegion(ctx: StatelessContextInterface) { + if (!startsAt(ctx, { reg: ["52"] }) || !endsAt(ctx, { reg: ["52"] })) { + throw new NotEligibleTargetException(); + } + } + + protected isInsideNantesMetropole(ctx: StatelessContextInterface): boolean { + return startsAt(ctx, { aom: ["244400404"] }) && endsAt(ctx, { aom: ["244400404"] }); + } }; diff --git a/api/src/pdc/services/policy/engine/policies/20240101_NantesMetropole.unit.spec.ts b/api/src/pdc/services/policy/engine/policies/20240101_NantesMetropole.unit.spec.ts index e3d3362f1..eb7c63354 100644 --- a/api/src/pdc/services/policy/engine/policies/20240101_NantesMetropole.unit.spec.ts +++ b/api/src/pdc/services/policy/engine/policies/20240101_NantesMetropole.unit.spec.ts @@ -1,4 +1,4 @@ -import { it, sinon } from "@/dev_deps.ts"; +import { it } from "@/dev_deps.ts"; import { v4 as uuidV4 } from "@/lib/uuid/index.ts"; import { OperatorsEnum } from "../../interfaces/index.ts"; import { makeProcessHelper } from "../tests/macro.ts"; @@ -43,324 +43,348 @@ const defaultCarpool = { const process = makeProcessHelper(defaultCarpool); -// Stub the mode method to control the booster date in tests -const boosterDates: string[] = ["2024-04-16"]; -sinon.stub(Handler, "mode").callsFake( - (date: Date, regular: any, booster: any) => { - const ymd = date.toISOString().slice(0, 10); - return boosterDates.includes(ymd) ? booster : regular; - }, -); +it("should work with exclusions", async () => + await process( + { + policy: { handler: Handler.id }, + carpool: [ + { distance: 4_999 }, + { distance: 60_001 }, + { operator_class: "A" }, -it( - "should work with exclusions", - async () => - await process( - { - policy: { handler: Handler.id }, - carpool: [ - { distance: 4_999 }, - { distance: 60_001 }, - { operator_class: "A" }, + // // OD hors AOM + { + start: { ...defaultPosition, aom: "244900015" }, + end: { ...defaultPosition, aom: "244900015" }, + }, - // // OD hors AOM - { - start: { ...defaultPosition, aom: "244900015" }, - end: { ...defaultPosition, aom: "244900015" }, - }, + // O dans l'AOM et D hors AOM + { + start: { ...defaultPosition, aom: "244400404" }, + end: { ...defaultPosition, aom: "247200132" }, + }, - // O dans l'AOM et D hors AOM - { - start: { ...defaultPosition, aom: "244400404" }, - end: { ...defaultPosition, aom: "247200132" }, - }, + // O hors AOM et D dans l'AOM + { + start: { ...defaultPosition, aom: "200071678" }, + end: { ...defaultPosition, aom: "244400404" }, + }, - // O hors AOM et D dans l'AOM - { - start: { ...defaultPosition, aom: "200071678" }, - end: { ...defaultPosition, aom: "244400404" }, - }, + // // Région Île-de-France + { start: { ...defaultPosition, reg: "11" } }, + { end: { ...defaultPosition, reg: "11" } }, + { passenger_is_over_18: false }, + ], + }, + { incentive: [0, 0, 0, 0, 0, 0, 0, 0, 0] }, + )); - // // Région Île-de-France - { start: { ...defaultPosition, reg: "11" } }, - { end: { ...defaultPosition, reg: "11" } }, - { passenger_is_over_18: false }, - ], - meta: [], - }, - { incentive: [0, 0, 0, 0, 0, 0, 0, 0, 0], meta: [] }, - ), -); +it("should work basic", async () => + await process( + { + policy: { handler: Handler.id }, + carpool: [ + { distance: 1_000, driver_identity_key: "one" }, + { distance: 5_000, driver_identity_key: "one" }, + { distance: 5_000, seats: 2, driver_identity_key: "one" }, + { distance: 20_000, driver_identity_key: "two" }, + { distance: 25_000, driver_identity_key: "two" }, + { distance: 29_500, driver_identity_key: "two" }, + { distance: 30_000, driver_identity_key: "two" }, + { distance: 55_000, driver_identity_key: "two" }, + { distance: 61_000, driver_identity_key: "two" }, + ], + meta: [], + }, + { + incentive: [0, 75, 150, 105, 155, 200, 200, 200, 0], + meta: [ + { + key: "max_amount_restriction.global.campaign.global", + value: 1085, + }, + { + key: "max_amount_restriction.0-one.month.3-2024", + value: 225, + }, + { + key: "max_amount_restriction.0-one.year.2024", + value: 225, + }, + { + key: "max_amount_restriction.0-two.month.3-2024", + value: 860, + }, + { + key: "max_amount_restriction.0-two.year.2024", + value: 860, + }, + ], + }, + )); -it( - "should work basic", - async () => - await process( - { - policy: { handler: Handler.id }, - carpool: [ - { distance: 1_000, driver_identity_key: "one" }, - { distance: 5_000, driver_identity_key: "one" }, - { distance: 5_000, seats: 2, driver_identity_key: "one" }, - { distance: 20_000, driver_identity_key: "two" }, - { distance: 25_000, driver_identity_key: "two" }, - { distance: 29_500, driver_identity_key: "two" }, - { distance: 30_000, driver_identity_key: "two" }, - { distance: 55_000, driver_identity_key: "two" }, - { distance: 61_000, driver_identity_key: "two" }, - ], - meta: [], - }, - { - incentive: [0, 75, 150, 105, 155, 200, 200, 200, 0], - meta: [ - { - key: "max_amount_restriction.global.campaign.global", - value: 1085, - }, - { - key: "max_amount_restriction.0-one.month.3-2024", - value: 225, - }, - { - key: "max_amount_restriction.0-one.year.2024", - value: 225, - }, - { - key: "max_amount_restriction.0-two.month.3-2024", - value: 860, - }, - { - key: "max_amount_restriction.0-two.year.2024", - value: 860, - }, - ], - }, - ), -); +it("should work with global limits", async () => + await process( + { + policy: { handler: Handler.id, max_amount: 2_200_000_00 }, + carpool: [{ distance: 5_000, driver_identity_key: "one" }], + meta: [ + { + key: "max_amount_restriction.global.campaign.global", + value: 2_199_999_25, + }, + ], + }, + { + incentive: [75], + meta: [ + { + key: "max_amount_restriction.global.campaign.global", + value: 2_200_000_00, + }, + { + key: "max_amount_restriction.0-one.month.3-2024", + value: 75, + }, + { + key: "max_amount_restriction.0-one.year.2024", + value: 75, + }, + ], + }, + )); -it( - "should work with global limits", - async () => - await process( - { - policy: { handler: Handler.id, max_amount: 2_200_000_00 }, - carpool: [{ distance: 5_000, driver_identity_key: "one" }], - meta: [ - { - key: "max_amount_restriction.global.campaign.global", - value: 2_199_999_25, - }, - ], - }, - { - incentive: [75], - meta: [ - { - key: "max_amount_restriction.global.campaign.global", - value: 2_200_000_00, - }, - { - key: "max_amount_restriction.0-one.month.3-2024", - value: 75, - }, - { - key: "max_amount_restriction.0-one.year.2024", - value: 75, - }, - ], - }, - ), -); +it("should work with day limits", async () => + await process( + { + policy: { handler: Handler.id }, + carpool: [ + { distance: 5_000, driver_identity_key: "one" }, + { distance: 5_000, driver_identity_key: "one" }, + { distance: 5_000, driver_identity_key: "one" }, + { distance: 5_000, driver_identity_key: "one" }, + { distance: 5_000, driver_identity_key: "one" }, + { distance: 5_000, driver_identity_key: "one" }, + { distance: 5_000, driver_identity_key: "one" }, + ], + meta: [], + }, + { + incentive: [75, 75, 75, 75, 75, 75, 0], + meta: [ + { + key: "max_amount_restriction.global.campaign.global", + value: 450, + }, + { + key: "max_amount_restriction.0-one.month.3-2024", + value: 450, + }, + { + key: "max_amount_restriction.0-one.year.2024", + value: 450, + }, + ], + }, + )); -it( - "should work with day limits", - async () => - await process( - { - policy: { handler: Handler.id }, - carpool: [ - { distance: 5_000, driver_identity_key: "one" }, - { distance: 5_000, driver_identity_key: "one" }, - { distance: 5_000, driver_identity_key: "one" }, - { distance: 5_000, driver_identity_key: "one" }, - { distance: 5_000, driver_identity_key: "one" }, - { distance: 5_000, driver_identity_key: "one" }, - { distance: 5_000, driver_identity_key: "one" }, - ], - meta: [], - }, - { - incentive: [75, 75, 75, 75, 75, 75, 0], - meta: [ - { - key: "max_amount_restriction.global.campaign.global", - value: 450, - }, - { - key: "max_amount_restriction.0-one.month.3-2024", - value: 450, - }, - { - key: "max_amount_restriction.0-one.year.2024", - value: 450, - }, - ], - }, - ), -); +it("should work with driver month limits of 84 €", async () => + await process( + { + policy: { handler: Handler.id }, + carpool: [ + { distance: 6_000, driver_identity_key: "one" }, + { distance: 6_000, driver_identity_key: "one" }, + ], + meta: [ + { + key: "max_amount_restriction.global.campaign.global", + value: 100_00, + }, + { + key: "max_amount_restriction.0-one.month.3-2024", + value: 83_25, + }, + { + key: "max_amount_restriction.0-one.year.2024", + value: 83_25, + }, + ], + }, + { + incentive: [75, 0], + meta: [ + { + key: "max_amount_restriction.global.campaign.global", + value: 100_75, + }, + { + key: "max_amount_restriction.0-one.month.3-2024", + value: 84_00, + }, + { + key: "max_amount_restriction.0-one.year.2024", + value: 84_75, + }, + ], + }, + )); -it( - "should work with driver month limits of 84 €", - async () => - await process( - { - policy: { handler: Handler.id }, - carpool: [ - { distance: 6_000, driver_identity_key: "one" }, - { distance: 6_000, driver_identity_key: "one" }, - ], - meta: [ - { - key: "max_amount_restriction.global.campaign.global", - value: 100_00, - }, - { - key: "max_amount_restriction.0-one.month.3-2024", - value: 83_25, - }, - { - key: "max_amount_restriction.0-one.year.2024", - value: 83_25, - }, - ], - }, - { - incentive: [75, 0], - meta: [ - { - key: "max_amount_restriction.global.campaign.global", - value: 100_75, - }, - { - key: "max_amount_restriction.0-one.month.3-2024", - value: 84_00, - }, - { - key: "max_amount_restriction.0-one.year.2024", - value: 84_75, - }, - ], - }, - ), -); +it("should work with driver year limits of 1008.00 €", async () => + await process( + { + policy: { handler: Handler.id }, + carpool: [ + { distance: 6_000, driver_identity_key: "one" }, + { distance: 6_000, driver_identity_key: "one" }, + ], + meta: [ + { + key: "max_amount_restriction.global.campaign.global", + value: 100_00, + }, + { + key: "max_amount_restriction.0-one.month.3-2024", + value: 0, + }, + { + key: "max_amount_restriction.0-one.year.2024", + value: 1007_25, + }, + ], + }, + { + incentive: [75, 0], + meta: [ + { + key: "max_amount_restriction.global.campaign.global", + value: 100_75, + }, + { + key: "max_amount_restriction.0-one.month.3-2024", + value: 75, + }, + { + key: "max_amount_restriction.0-one.year.2024", + value: 1008_00, + }, + ], + }, + )); -it( - "should work with driver year limits of 1008.00 €", - async () => - await process( - { - policy: { handler: Handler.id }, - carpool: [ - { distance: 6_000, driver_identity_key: "one" }, - { distance: 6_000, driver_identity_key: "one" }, - ], - meta: [ - { - key: "max_amount_restriction.global.campaign.global", - value: 100_00, - }, - { - key: "max_amount_restriction.0-one.month.3-2024", - value: 0, - }, - { - key: "max_amount_restriction.0-one.year.2024", - value: 1007_25, - }, - ], - }, - { - incentive: [75, 0], - meta: [ - { - key: "max_amount_restriction.global.campaign.global", - value: 100_75, - }, - { - key: "max_amount_restriction.0-one.month.3-2024", - value: 75, - }, - { - key: "max_amount_restriction.0-one.year.2024", - value: 1008_00, - }, - ], - }, - ), -); +it("should use boosterSlices on booster dates", async () => + await process( + { + policy: { handler: Handler.id }, + carpool: [ + { + distance: 6_000, + driver_identity_key: "reg", + datetime: new Date("2024-04-15"), + }, + { + distance: 6_000, + driver_identity_key: "boo", + datetime: new Date("2024-12-01"), + }, + { + distance: 11_000, + driver_identity_key: "reg", + datetime: new Date("2024-04-15"), + }, + { + distance: 11_000, + driver_identity_key: "boo", + datetime: new Date("2024-12-01"), + }, + { + distance: 17_000, + driver_identity_key: "reg", + datetime: new Date("2024-04-15"), + }, + { + distance: 17_000, + driver_identity_key: "boo", + datetime: new Date("2024-12-01"), + }, + { + distance: 30_000, + driver_identity_key: "reg", + datetime: new Date("2024-04-15"), + }, + { + distance: 30_000, + driver_identity_key: "boo", + datetime: new Date("2024-12-01"), + }, + { + distance: 75_000, + driver_identity_key: "reg", + datetime: new Date("2024-04-15"), + }, + { + distance: 75_000, + driver_identity_key: "boo", + datetime: new Date("2024-12-01"), + }, + ], + }, + { + incentive: [75, 165, 75, 165, 75, 165, 200, 290, 0, 0], + }, + )); -it( - "should use boosterSlices on booster dates", - async () => - await process( - { - policy: { handler: Handler.id }, - carpool: [ - { - distance: 6_000, - driver_identity_key: "reg", - datetime: new Date("2024-04-15"), - }, - { - distance: 6_000, - driver_identity_key: "boo", - datetime: new Date("2024-04-16"), - }, - { - distance: 11_000, - driver_identity_key: "reg", - datetime: new Date("2024-04-15"), - }, - { - distance: 11_000, - driver_identity_key: "boo", - datetime: new Date("2024-04-16"), - }, - { - distance: 17_000, - driver_identity_key: "reg", - datetime: new Date("2024-04-15"), - }, - { - distance: 17_000, - driver_identity_key: "boo", - datetime: new Date("2024-04-16"), - }, - { - distance: 30_000, - driver_identity_key: "reg", - datetime: new Date("2024-04-15"), - }, - { - distance: 30_000, - driver_identity_key: "boo", - datetime: new Date("2024-04-16"), - }, - { - distance: 75_000, - driver_identity_key: "reg", - datetime: new Date("2024-04-15"), - }, - { - distance: 75_000, - driver_identity_key: "boo", - datetime: new Date("2024-04-16"), - }, - ], - }, - { - incentive: [75, 165, 75, 165, 75, 165, 200, 290, 0, 0], - }, - ), -); +it("should detect trips inside the AOM", async () => + await process( + { + policy: { handler: Handler.id }, + carpool: [ + // regular - inside + { distance: 6_000, driver_identity_key: "one", datetime: new Date("2024-04-15") }, + + // // booster - inside + { distance: 6_000, driver_identity_key: "one", datetime: new Date("2024-12-01") }, + + // regular - outside + { + distance: 6_000, + datetime: new Date("2024-04-15"), + start: { ...defaultPosition, aom: "244900015" }, // Angers + }, + + // booster - outside + { + distance: 6_000, + datetime: new Date("2024-12-04"), + start: { ...defaultPosition, aom: "244900015" }, // Angers + seats: 1, + }, + + // booster - outside - many seats + { + distance: 6_000, + driver_identity_key: "one", + datetime: new Date("2024-12-12"), + start: { ...defaultPosition, aom: "244900015" }, // Angers + seats: 3, + }, + + // booster - outside - slice 2 + { + distance: 20_000, + datetime: new Date("2024-12-25"), + start: { ...defaultPosition, aom: "244900015" }, // Angers + }, + + // booster - outside - slice 3 + { + distance: 40_000, + datetime: new Date("2024-12-10"), + start: { ...defaultPosition, aom: "244900015" }, // Angers + }, + ], + }, + { + incentive: [75, 165, 0, 90, 90 * 3, 90, 90], + }, + )); diff --git a/api/src/pdc/services/policy/engine/policies/20240101_PaysDeLaLoire.html.ts b/api/src/pdc/services/policy/engine/policies/20240101_PaysDeLaLoire.html.ts index 28eb6d706..578a68500 100644 --- a/api/src/pdc/services/policy/engine/policies/20240101_PaysDeLaLoire.html.ts +++ b/api/src/pdc/services/policy/engine/policies/20240101_PaysDeLaLoire.html.ts @@ -1,5 +1,4 @@ -export const description = - `
      +export const description = `

      Campagne d'incitation au covoiturage du 01 janvier 2024 au 31 Décembre 2025

      @@ -7,22 +6,45 @@ export const description =

      Les conducteurs effectuant un trajet entre 5 et 60 km (inclus) - dans la région Pays de la Loire sont incités selon les règles suivantes : + dans la région Pays de la Loire sont incités selon les règles suivantes + dans les conditions normales :

      • De 5 à 17 km : 0,75 € par trajet par passager.
      • De 17 à 30 km : 0,10 € par trajet par km par passager avec un maximum de 2,00 €
      • +
      • De 30 à 60 km : 2,00 € par passager transporté
      -

      Les restrictions suivantes seront appliquées :

      +

      + Dans les conditions "booster", les règles suivantes sont appliquées : +

      + +
        +
      • De 5 à 17 km : 1,65 € par trajet par passager.
      • +
      • De 17 à 30 km : 0,10 € par trajet par km par passager avec un maximum de 2,90 €
      • +
      • De 30 à 60 km : 2,90 € par passager transporté
      • +
      + +

      Les dates des périodes booster sont les suivantes :

      + +
        +
      • aucune date pour le moment
      • +
      + +

      Les restrictions suivantes sont appliquées :

      • 6 trajets maximum pour le conducteur par jour.
      • 84,00 € maximum pour le conducteur par mois.
      • +
      • 1 008,00 € maximum pour le conducteur par an.
      -

      La campagne est éligible à tous les opérateurs du RPC proposant des preuves de classe C.

      +

      + La campagne est éligible aux opérateurs + BlablaCar Daily, Karos et Mobicoop + proposant des preuves de classe C. +

      Les trajets au départ et à l'arrivée des AOMs suivantes ne sont pas incités :

      diff --git a/api/src/pdc/services/policy/engine/policies/20240101_PaysDeLaLoire.ts b/api/src/pdc/services/policy/engine/policies/20240101_PaysDeLaLoire.ts index 1bcd4cab3..9d50db099 100644 --- a/api/src/pdc/services/policy/engine/policies/20240101_PaysDeLaLoire.ts +++ b/api/src/pdc/services/policy/engine/policies/20240101_PaysDeLaLoire.ts @@ -1,8 +1,6 @@ +import { Timezone } from "@/pdc/providers/validator/types.ts"; import { NotEligibleTargetException } from "@/pdc/services/policy/engine/exceptions/NotEligibleTargetException.ts"; -import { - getOperatorsAt, - TimestampedOperators, -} from "@/pdc/services/policy/engine/helpers/getOperatorsAt.ts"; +import { getOperatorsAt, TimestampedOperators } from "@/pdc/services/policy/engine/helpers/getOperatorsAt.ts"; import { isAdultOrThrow } from "@/pdc/services/policy/engine/helpers/isAdultOrThrow.ts"; import { isOperatorClassOrThrow } from "@/pdc/services/policy/engine/helpers/isOperatorClassOrThrow.ts"; import { isOperatorOrThrow } from "@/pdc/services/policy/engine/helpers/isOperatorOrThrow.ts"; @@ -10,19 +8,14 @@ import { LimitTargetEnum, watchForGlobalMaxAmount, watchForPersonMaxAmountByMonth, + watchForPersonMaxAmountByYear, watchForPersonMaxTripByDay, } from "@/pdc/services/policy/engine/helpers/limits.ts"; -import { - onDistanceRange, - onDistanceRangeOrThrow, -} from "@/pdc/services/policy/engine/helpers/onDistanceRange.ts"; +import { onDistanceRange, onDistanceRangeOrThrow } from "@/pdc/services/policy/engine/helpers/onDistanceRange.ts"; import { perKm, perSeat } from "@/pdc/services/policy/engine/helpers/per.ts"; -import { - endsAt, - startsAndEndsAt, - startsAt, -} from "@/pdc/services/policy/engine/helpers/position.ts"; +import { endsAt, startsAndEndsAt, startsAt } from "@/pdc/services/policy/engine/helpers/position.ts"; import { AbstractPolicyHandler } from "@/pdc/services/policy/engine/policies/AbstractPolicyHandler.ts"; +import { toTzString } from "@/pdc/services/policy/helpers/index.ts"; import { RunnableSlices } from "@/pdc/services/policy/interfaces/engine/PolicyInterface.ts"; import { OperatorsEnum, @@ -35,10 +28,21 @@ import { description } from "./20240101_PaysDeLaLoire.html.ts"; // Politique de Pays de la Loire 2024 /* eslint-disable-next-line */ -export const PaysDeLaLoire2024: PolicyHandlerStaticInterface = class - extends AbstractPolicyHandler +export const PaysDeLaLoire2024: PolicyHandlerStaticInterface = class extends AbstractPolicyHandler implements PolicyHandlerInterface { static readonly id = "pdll_2024"; + static readonly tz: Timezone = "Europe/Paris"; + + public static mode(date: Date, regular: T, booster: T): T { + if (!PaysDeLaLoire2024.boosterDates) return regular; + + const ymd = toTzString(date).slice(0, 10); + return PaysDeLaLoire2024.boosterDates.includes(ymd) ? booster : regular; + } + + // List dates in the YYYY-MM-DD format in the Europe/Paris timezone + // or use the dateRange() helper function for consecutive dates. + protected static boosterDates: string[] = []; protected operators: TimestampedOperators = [ { @@ -60,7 +64,7 @@ export const PaysDeLaLoire2024: PolicyHandlerStaticInterface = class }, ]; - protected slices: RunnableSlices = [ + protected regularSlices: RunnableSlices = [ { start: 5_000, end: 17_000, @@ -69,76 +73,83 @@ export const PaysDeLaLoire2024: PolicyHandlerStaticInterface = class { start: 17_000, end: 30_000, - fn: (ctx: StatelessContextInterface) => - perSeat(ctx, perKm(ctx, { amount: 10, offset: 17_000, limit: 29_500 })), + fn: (ctx: StatelessContextInterface) => { + // 0,10 euro par trajet par km par passager avec un maximum de 2,00 euros + return perSeat( + ctx, + Math.min(perKm(ctx, { amount: 10, offset: 17_000, limit: 30_000 }), 200 - 75), + ); + }, + }, + { + start: 30_000, + end: 60_000, + fn: () => 0, + }, + ]; + + protected boosterSlices: RunnableSlices = [ + { + start: 5_000, + end: 17_000, + fn: (ctx: StatelessContextInterface) => perSeat(ctx, 165), + }, + { + start: 17_000, + end: 30_000, + fn: (ctx: StatelessContextInterface) => { + // 0,10 euro par trajet par km par passager avec un maximum de 2,90 euros + return perSeat(ctx, Math.min(perKm(ctx, { amount: 10, offset: 17_000, limit: 30_000 }), 290 - 165)); + }, }, { start: 30_000, - fn: (_ctx: StatelessContextInterface) => 0, + end: 60_000, + fn: () => 0, }, ]; constructor(public max_amount: number) { super(); this.limits = [ + [ + "5499304F-2C64-AB1A-7392-52FF88F5E78D", + this.max_amount, + watchForGlobalMaxAmount, + ], [ "8C5251E8-AB82-EB29-C87A-2BF59D4F6328", 6, watchForPersonMaxTripByDay, LimitTargetEnum.Driver, ], - [ - "5499304F-2C64-AB1A-7392-52FF88F5E78D", - this.max_amount, - watchForGlobalMaxAmount, - ], [ "ECDE3CD4-96FF-C9D2-BA88-45754205A798", 84_00, watchForPersonMaxAmountByMonth, LimitTargetEnum.Driver, ], + [ + "c5ba8ecd-f1ee-4005-b2b0-fe94901d1286", + 1008_00, + watchForPersonMaxAmountByYear, + LimitTargetEnum.Driver, + ], ]; } - protected processExclusion(ctx: StatelessContextInterface) { - isOperatorOrThrow( - ctx, - getOperatorsAt(this.operators, ctx.carpool.datetime), - ); - onDistanceRangeOrThrow(ctx, { min: 5_000, max: 60_001 }); - isOperatorClassOrThrow(ctx, ["C"]); - isAdultOrThrow(ctx); - - /* - Exclure les trajets : - - 244400404: Nantes Métropole -> Nantes Métropole, - - 244900015: CU Angers Loire Métropole -> CU Angers Loire Métropole, - - 247200132: CU Le Mans Métropole -> CU Le Mans Métropole, - - 200071678: CA Agglomération du Choletais -> CA Agglomération du Choletais - */ - if ( - startsAndEndsAt(ctx, { aom: ["244400404"] }) || - startsAndEndsAt(ctx, { aom: ["244900015"] }) || - startsAndEndsAt(ctx, { aom: ["247200132"] }) || - startsAndEndsAt(ctx, { aom: ["200071678"] }) - ) { - throw new NotEligibleTargetException(); - } - - // Exclure les trajets qui ne sont pas dans l'AOM - if (!startsAt(ctx, { reg: ["52"] }) || !endsAt(ctx, { reg: ["52"] })) { - throw new NotEligibleTargetException(); - } - } - - processStateless(ctx: StatelessContextInterface): void { + public processStateless(ctx: StatelessContextInterface): void { this.processExclusion(ctx); super.processStateless(ctx); + if (!PaysDeLaLoire2024.mode) { + throw new Error("PaysDeLaLoire2024.mode is not defined"); + } + // Par kilomètre let amount = 0; - for (const { start, fn } of this.slices) { + const slices = PaysDeLaLoire2024.mode(ctx.carpool.datetime, this.regularSlices, this.boosterSlices); + for (const { start, fn } of slices) { if (onDistanceRange(ctx, { min: start })) { amount += fn(ctx); } @@ -147,21 +158,57 @@ export const PaysDeLaLoire2024: PolicyHandlerStaticInterface = class ctx.incentive.set(amount); } - params(): PolicyHandlerParamsInterface { + public params(date?: Date): PolicyHandlerParamsInterface { + if (!PaysDeLaLoire2024.mode) { + throw new Error("PaysDeLaLoire2024.mode is not defined"); + } + return { - tz: "Europe/Paris", - slices: this.slices, + tz: PaysDeLaLoire2024.tz, + slices: date ? PaysDeLaLoire2024.mode(date, this.regularSlices, this.boosterSlices) : this.regularSlices, operators: getOperatorsAt(this.operators), - allTimeOperators: Array.from( - new Set(this.operators.flatMap((entry) => entry.operators)), - ), + allTimeOperators: Array.from(new Set(this.operators.flatMap((entry) => entry.operators))), limits: { glob: this.max_amount, }, }; } - describe(): string { + public describe(): string { return description; } + + protected processExclusion(ctx: StatelessContextInterface) { + isOperatorOrThrow(ctx, getOperatorsAt(this.operators, ctx.carpool.datetime)); + onDistanceRangeOrThrow(ctx, { min: 5_000, max: 60_001 }); + isOperatorClassOrThrow(ctx, ["C"]); + isAdultOrThrow(ctx); + + this.excludeLocalAOM(ctx); + this.excludeOutsideOfAOM(ctx); + } + + /** + * Exclure les trajets : + * - 244400404: Nantes Métropole -> Nantes Métropole, + * - 244900015: CU Angers Loire Métropole -> CU Angers Loire Métropole, + * - 247200132: CU Le Mans Métropole -> CU Le Mans Métropole, + * - 200071678: CA Agglomération du Choletais -> CA Agglomération du Choletais + */ + protected excludeLocalAOM(ctx: StatelessContextInterface) { + if ( + startsAndEndsAt(ctx, { aom: ["244400404"] }) || + startsAndEndsAt(ctx, { aom: ["244900015"] }) || + startsAndEndsAt(ctx, { aom: ["247200132"] }) || + startsAndEndsAt(ctx, { aom: ["200071678"] }) + ) { + throw new NotEligibleTargetException(); + } + } + + protected excludeOutsideOfAOM(ctx: StatelessContextInterface) { + if (!startsAt(ctx, { reg: ["52"] }) || !endsAt(ctx, { reg: ["52"] })) { + throw new NotEligibleTargetException(); + } + } }; diff --git a/api/src/pdc/services/policy/engine/policies/20240101_PaysDeLaLoire.unit.spec.ts b/api/src/pdc/services/policy/engine/policies/20240101_PaysDeLaLoire.unit.spec.ts index 6270e31fd..97bea7597 100644 --- a/api/src/pdc/services/policy/engine/policies/20240101_PaysDeLaLoire.unit.spec.ts +++ b/api/src/pdc/services/policy/engine/policies/20240101_PaysDeLaLoire.unit.spec.ts @@ -1,302 +1,351 @@ -import { it } from "@/dev_deps.ts"; +import { describe, it, sinon } from "@/dev_deps.ts"; import { v4 as uuidV4 } from "@/lib/uuid/index.ts"; -import { OperatorsEnum } from "../../interfaces/index.ts"; +import { RunnableSlices } from "@/pdc/services/policy/interfaces/engine/PolicyInterface.ts"; +import { OperatorsEnum, TerritoryCodeInterface } from "../../interfaces/index.ts"; import { makeProcessHelper } from "../tests/macro.ts"; import { PaysDeLaLoire2024 as Handler } from "./20240101_PaysDeLaLoire.ts"; -const defaultPosition = { - arr: "85047", - com: "85047", - aom: "200071629", - epci: "200071629", - dep: "85", - reg: "52", - country: "XXXXX", - reseau: "430", -}; -const defaultLat = 48.72565703413325; -const defaultLon = 2.261827843187402; +describe("PaysDeLaLoire2024", () => { + const defaultPosition: TerritoryCodeInterface = { + arr: "85047", + com: "85047", + aom: "200071629", + epci: "200071629", + dep: "85", + reg: "52", + country: "XXXXX", + reseau: 1, + }; + const defaultLat = 48.72565703413325; + const defaultLon = 2.261827843187402; -const defaultCarpool = { - _id: 1, - operator_trip_id: uuidV4(), - passenger_identity_key: uuidV4(), - driver_identity_key: uuidV4(), - operator_uuid: OperatorsEnum.KAROS, - operator_class: "C", - passenger_is_over_18: true, - passenger_has_travel_pass: true, - driver_has_travel_pass: true, - datetime: new Date("2024-04-15"), - seats: 1, - distance: 5_000, - operator_journey_id: uuidV4(), - operator_id: 1, - driver_revenue: 20, - passenger_contribution: 20, - start: { ...defaultPosition }, - end: { ...defaultPosition }, - start_lat: defaultLat, - start_lon: defaultLon, - end_lat: defaultLat, - end_lon: defaultLon, -}; + const defaultCarpool = { + _id: 1, + operator_trip_id: uuidV4(), + passenger_identity_key: uuidV4(), + driver_identity_key: uuidV4(), + operator_uuid: OperatorsEnum.KAROS, + operator_class: "C", + passenger_is_over_18: true, + passenger_has_travel_pass: true, + driver_has_travel_pass: true, + datetime: new Date("2024-04-15"), + seats: 1, + distance: 5_000, + operator_journey_id: uuidV4(), + operator_id: 1, + driver_revenue: 20, + passenger_contribution: 20, + start: { ...defaultPosition }, + end: { ...defaultPosition }, + start_lat: defaultLat, + start_lon: defaultLon, + end_lat: defaultLat, + end_lon: defaultLon, + }; -const process = makeProcessHelper(defaultCarpool); + /** + * Stub the mode method while the PaysDeLaLoire2024 class has no booster dates. + */ + const boosterDates: string[] = ["2024-04-16"]; + sinon.stub(Handler, "mode").callsFake( + (date: Date, regular: RunnableSlices, booster: RunnableSlices) => { + const ymd = date.toISOString().slice(0, 10); + return boosterDates.includes(ymd) ? booster : regular; + }, + ); -it( - "should work with regular exclusions", - async () => - await process( - { - policy: { handler: Handler.id }, - carpool: [ - { distance: 4999 }, - { operator_class: "A" }, - { start: { ...defaultPosition, reg: "11" } }, - { end: { ...defaultPosition, reg: "11" } }, - { distance: 60_001 }, - { passenger_is_over_18: false }, - ], - }, - { incentive: [0, 0, 0, 0, 0, 0] }, - ), -); + const process = makeProcessHelper(defaultCarpool); -it( - "Klaxit removed on 2024-03-18", - async () => - await process( - { - policy: { handler: Handler.id }, - carpool: [ - { - operator_uuid: OperatorsEnum.KLAXIT, - datetime: new Date("2024-03-17T23:00:00+0100"), - }, - { - operator_uuid: OperatorsEnum.KLAXIT, - datetime: new Date("2024-03-18T10:00:00+0100"), - }, - ], - }, - { incentive: [75, 0] }, - ), -); + it("should work with regular exclusions", async () => + await process({ + policy: { handler: Handler.id }, + carpool: [ + { distance: 4999 }, + { operator_class: "A" }, + { start: { ...defaultPosition, reg: "11" } }, + { end: { ...defaultPosition, reg: "11" } }, + { distance: 60_001 }, + { passenger_is_over_18: false }, + ], + }, { incentive: [0, 0, 0, 0, 0, 0] })); -it( - "should work with AOM exclusions", - async () => - await process( - { - policy: { handler: Handler.id }, - carpool: [ - // Nantes Métropole (244400404) - { - driver_identity_key: "nantes", - start: { ...defaultPosition, aom: "244400404" }, - end: { ...defaultPosition, aom: "244400404" }, - }, - { - driver_identity_key: "nantes", - start: { ...defaultPosition, aom: "244400404" }, - end: { ...defaultPosition }, - }, - { - driver_identity_key: "nantes", - start: { ...defaultPosition }, - end: { ...defaultPosition, aom: "244400404" }, - }, + it("Klaxit removed on 2024-03-18", async () => + await process({ + policy: { handler: Handler.id }, + carpool: [ + { + operator_uuid: OperatorsEnum.KLAXIT, + datetime: new Date("2024-03-17T23:00:00+0100"), + }, + { + operator_uuid: OperatorsEnum.KLAXIT, + datetime: new Date("2024-03-18T10:00:00+0100"), + }, + ], + }, { incentive: [75, 0] })); - // Angers (244900015) - { - driver_identity_key: "angers", - start: { ...defaultPosition, aom: "244900015" }, - end: { ...defaultPosition, aom: "244900015" }, - }, - { - driver_identity_key: "angers", - start: { ...defaultPosition, aom: "244900015" }, - end: { ...defaultPosition }, - }, - { - driver_identity_key: "angers", - start: { ...defaultPosition }, - end: { ...defaultPosition, aom: "244900015" }, - }, + it("should work with AOM exclusions", async () => + await process({ + policy: { handler: Handler.id }, + carpool: [ + // Nantes Métropole (244400404) + { + driver_identity_key: "nantes", + start: { ...defaultPosition, aom: "244400404" }, + end: { ...defaultPosition, aom: "244400404" }, + }, + { + driver_identity_key: "nantes", + start: { ...defaultPosition, aom: "244400404" }, + end: { ...defaultPosition }, + }, + { + driver_identity_key: "nantes", + start: { ...defaultPosition }, + end: { ...defaultPosition, aom: "244400404" }, + }, - // Le Mans (247200132) - { - driver_identity_key: "le_mans", - start: { ...defaultPosition, aom: "247200132" }, - end: { ...defaultPosition, aom: "247200132" }, - }, - { - driver_identity_key: "le_mans", - start: { ...defaultPosition, aom: "247200132" }, - end: { ...defaultPosition }, - }, - { - driver_identity_key: "le_mans", - start: { ...defaultPosition }, - end: { ...defaultPosition, aom: "247200132" }, - }, + // Angers (244900015) + { + driver_identity_key: "angers", + start: { ...defaultPosition, aom: "244900015" }, + end: { ...defaultPosition, aom: "244900015" }, + }, + { + driver_identity_key: "angers", + start: { ...defaultPosition, aom: "244900015" }, + end: { ...defaultPosition }, + }, + { + driver_identity_key: "angers", + start: { ...defaultPosition }, + end: { ...defaultPosition, aom: "244900015" }, + }, - // CA Agglomération du Choletais (200071678) - { - driver_identity_key: "cholet", - start: { ...defaultPosition, aom: "200071678" }, - end: { ...defaultPosition, aom: "200071678" }, - }, - { - driver_identity_key: "cholet", - start: { ...defaultPosition, aom: "200071678" }, - end: { ...defaultPosition }, - }, - { - driver_identity_key: "cholet", - start: { ...defaultPosition }, - end: { ...defaultPosition, aom: "200071678" }, - }, - ], - }, - { incentive: [0, 75, 75, 0, 75, 75, 0, 75, 75, 0, 75, 75] }, - ), -); + // Le Mans (247200132) + { + driver_identity_key: "le_mans", + start: { ...defaultPosition, aom: "247200132" }, + end: { ...defaultPosition, aom: "247200132" }, + }, + { + driver_identity_key: "le_mans", + start: { ...defaultPosition, aom: "247200132" }, + end: { ...defaultPosition }, + }, + { + driver_identity_key: "le_mans", + start: { ...defaultPosition }, + end: { ...defaultPosition, aom: "247200132" }, + }, -it( - "should work basic", - async () => - await process( - { - policy: { handler: Handler.id }, - carpool: [ - { distance: 1_000, driver_identity_key: "one" }, - { distance: 5_000, driver_identity_key: "one" }, - { distance: 5_000, seats: 2, driver_identity_key: "one" }, - { distance: 20_000, driver_identity_key: "two" }, - { distance: 25_000, driver_identity_key: "two" }, - { distance: 55_000, driver_identity_key: "two" }, - { distance: 61_000, driver_identity_key: "two" }, - ], - meta: [], - }, - { - incentive: [0, 75, 150, 105, 155, 200, 0], - meta: [ - { - key: "max_amount_restriction.global.campaign.global", - value: 685, - }, - { - key: "max_amount_restriction.0-one.month.3-2024", - value: 225, - }, - { - key: "max_amount_restriction.0-two.month.3-2024", - value: 460, - }, - ], - }, - ), -); + // CA Agglomération du Choletais (200071678) + { + driver_identity_key: "cholet", + start: { ...defaultPosition, aom: "200071678" }, + end: { ...defaultPosition, aom: "200071678" }, + }, + { + driver_identity_key: "cholet", + start: { ...defaultPosition, aom: "200071678" }, + end: { ...defaultPosition }, + }, + { + driver_identity_key: "cholet", + start: { ...defaultPosition }, + end: { ...defaultPosition, aom: "200071678" }, + }, + ], + }, { incentive: [0, 75, 75, 0, 75, 75, 0, 75, 75, 0, 75, 75] })); -it( - "should work with global limits", - async () => - await process( - { - policy: { handler: Handler.id, max_amount: 2_200_000_00 }, - carpool: [{ distance: 5_000, driver_identity_key: "one" }], - meta: [ - { - key: "max_amount_restriction.global.campaign.global", - value: 2_199_999_25, - }, - ], - }, - { - incentive: [75], - meta: [ - { - key: "max_amount_restriction.global.campaign.global", - value: 2_200_000_00, - }, - { - key: "max_amount_restriction.0-one.month.3-2024", - value: 75, - }, - ], - }, - ), -); + it("should work basic", async () => + await process({ + policy: { handler: Handler.id }, + carpool: [ + { distance: 1_000, driver_identity_key: "one" }, + { distance: 5_000, driver_identity_key: "one" }, + { distance: 5_000, seats: 2, driver_identity_key: "one" }, + { distance: 20_000, driver_identity_key: "two" }, + { distance: 25_000, driver_identity_key: "two" }, + { distance: 55_000, driver_identity_key: "two" }, + { distance: 61_000, driver_identity_key: "two" }, + ], + meta: [], + }, { + incentive: [0, 75, 150, 105, 155, 200, 0], + meta: [ + { + key: "max_amount_restriction.global.campaign.global", + value: 685, + }, + { + key: "max_amount_restriction.0-one.month.3-2024", + value: 225, + }, + { + key: "max_amount_restriction.0-one.year.2024", + value: 225, + }, + { + key: "max_amount_restriction.0-two.month.3-2024", + value: 460, + }, + { + key: "max_amount_restriction.0-two.year.2024", + value: 460, + }, + ], + })); -it( - "should work with day limits", - async () => - await process( - { - policy: { handler: Handler.id }, - carpool: [ - { distance: 5_000, driver_identity_key: "one" }, - { distance: 5_000, driver_identity_key: "one" }, - { distance: 5_000, driver_identity_key: "one" }, - { distance: 5_000, driver_identity_key: "one" }, - { distance: 5_000, driver_identity_key: "one" }, - { distance: 5_000, driver_identity_key: "one" }, - { distance: 5_000, driver_identity_key: "one" }, - ], - meta: [], - }, - { - incentive: [75, 75, 75, 75, 75, 75, 0], - meta: [ - { - key: "max_amount_restriction.global.campaign.global", - value: 450, - }, - { - key: "max_amount_restriction.0-one.month.3-2024", - value: 450, - }, - ], - }, - ), -); + it("should apply booster rules on booster dates", async () => + await process({ + policy: { handler: Handler.id }, + carpool: [ + { distance: 6_000, datetime: new Date("2024-04-01") }, + { distance: 6_000, datetime: new Date("2024-04-16") }, + { distance: 20_000, datetime: new Date("2024-04-01") }, + { distance: 20_000, datetime: new Date("2024-04-16") }, + { distance: 55_000, datetime: new Date("2024-04-01") }, + { distance: 55_000, datetime: new Date("2024-04-16") }, + { distance: 80_000, datetime: new Date("2024-04-01") }, + { distance: 80_000, datetime: new Date("2024-04-16") }, + ], + }, { + incentive: [75, 165, 105, 195, 200, 290, 0, 0], + })); -it( - "should work with driver month limits of 84 €", - async () => - await process( - { - policy: { handler: Handler.id }, - carpool: [ - { distance: 6_000, driver_identity_key: "one" }, - { distance: 6_000, driver_identity_key: "one" }, - ], - meta: [ - { - key: "max_amount_restriction.0-one.month.3-2024", - value: 83_25, - }, - { - key: "max_amount_restriction.global.campaign.global", - value: 83_25, - }, - ], - }, - { - incentive: [75, 0], - meta: [ - { - key: "max_amount_restriction.global.campaign.global", - value: 84_00, - }, - { - key: "max_amount_restriction.0-one.month.3-2024", - value: 84_00, - }, - ], - }, - ), -); + it("should work with global limits", async () => + await process({ + policy: { handler: Handler.id, max_amount: 2_200_000_00 }, + carpool: [{ distance: 5_000, driver_identity_key: "one" }], + meta: [ + { + key: "max_amount_restriction.global.campaign.global", + value: 2_199_999_25, + }, + ], + }, { + incentive: [75], + meta: [ + { + key: "max_amount_restriction.global.campaign.global", + value: 2_200_000_00, + }, + { + key: "max_amount_restriction.0-one.month.3-2024", + value: 75, + }, + { + key: "max_amount_restriction.0-one.year.2024", + value: 75, + }, + ], + })); + + it("should work with day limits", async () => + await process({ + policy: { handler: Handler.id }, + carpool: [ + { distance: 5_000, driver_identity_key: "one" }, + { distance: 5_000, driver_identity_key: "one" }, + { distance: 5_000, driver_identity_key: "one" }, + { distance: 5_000, driver_identity_key: "one" }, + { distance: 5_000, driver_identity_key: "one" }, + { distance: 5_000, driver_identity_key: "one" }, + { distance: 5_000, driver_identity_key: "one" }, + ], + meta: [], + }, { + incentive: [75, 75, 75, 75, 75, 75, 0], + meta: [ + { + key: "max_amount_restriction.global.campaign.global", + value: 450, + }, + { + key: "max_amount_restriction.0-one.month.3-2024", + value: 450, + }, + { + key: "max_amount_restriction.0-one.year.2024", + value: 450, + }, + ], + })); + + it("should work with driver month limits of 84 €", async () => + await process({ + policy: { handler: Handler.id }, + carpool: [ + { distance: 6_000, driver_identity_key: "one" }, + { distance: 6_000, driver_identity_key: "one" }, + ], + meta: [ + { + key: "max_amount_restriction.0-one.month.3-2024", + value: 83_25, + }, + { + key: "max_amount_restriction.global.campaign.global", + value: 83_25, + }, + ], + }, { + incentive: [75, 0], + meta: [ + { + key: "max_amount_restriction.global.campaign.global", + value: 84_00, + }, + { + key: "max_amount_restriction.0-one.month.3-2024", + value: 84_00, + }, + { + key: "max_amount_restriction.0-one.year.2024", + value: 150, + }, + ], + })); + + it("should work with driver year limits of 1008 €", async () => + await process({ + policy: { handler: Handler.id }, + carpool: [ + { distance: 50_000, driver_identity_key: "one" }, + { distance: 50_000, driver_identity_key: "one" }, + { distance: 50_000, driver_identity_key: "one" }, + ], + meta: [ + { + key: "max_amount_restriction.0-one.month.3-2024", + value: 83_25, + }, + { + key: "max_amount_restriction.global.campaign.global", + value: 83_25, + }, + { + key: "max_amount_restriction.0-one.year.2024", + value: 1007_99, + }, + ], + }, { + incentive: [1, 0, 0], + meta: [ + { + key: "max_amount_restriction.global.campaign.global", + value: 83_26, + }, + { + key: "max_amount_restriction.0-one.month.3-2024", + value: 83_26, + }, + { + key: "max_amount_restriction.0-one.year.2024", + value: 1008_00, + }, + ], + })); +}); diff --git a/api/src/pdc/services/policy/interfaces/engine/PolicyInterface.ts b/api/src/pdc/services/policy/interfaces/engine/PolicyInterface.ts index ff8d812d0..4f16c80db 100644 --- a/api/src/pdc/services/policy/interfaces/engine/PolicyInterface.ts +++ b/api/src/pdc/services/policy/interfaces/engine/PolicyInterface.ts @@ -57,7 +57,7 @@ export interface PolicyHandlerStaticInterface { readonly id: string; readonly tz?: Timezone; readonly boosterDates?: string[]; - mode?(date: Date, regular: T, booster: T): T; + mode?(date: Date, ...args: T[] | unknown[]): T; /** * Optional max amount to spend for the policy */