diff --git a/.gitignore b/.gitignore index 2388c04..8564387 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,5 @@ test.ts scripts/data/quechoisir/*.html dist/ publicodes-build/ -src/rules/alternatives.publicodes +src/rules/alternatives/achat-voiture.publicodes src/personas/personas.json diff --git a/scripts/generate-alternatives.js b/scripts/generate-alternatives.js index 764906e..75f5914 100644 --- a/scripts/generate-alternatives.js +++ b/scripts/generate-alternatives.js @@ -3,7 +3,7 @@ * motorisations */ -const ALTERNATIVES_VOITURE_NAMESPACE = "alternatives . voiture" +const ALTERNATIVES_VOITURE_NAMESPACE = "alternatives . acheter voiture" export default function generateAlternatives(rules) { const carburants = Object.keys(rules).flatMap((key) => { @@ -35,11 +35,8 @@ export default function generateAlternatives(rules) { }) const alternatives = { - alternatives: { - titre: "Alternatives", - }, - "alternatives . voiture": { - titre: "Voiture Individuelle", + "alternatives . acheter voiture": { + titre: "Acheter une nouvelle voiture", }, } diff --git a/scripts/precompile.js b/scripts/precompile.js index 8055713..8dd8740 100644 --- a/scripts/precompile.js +++ b/scripts/precompile.js @@ -11,7 +11,7 @@ const ROOT_PATH = new URL(".", import.meta.url).pathname const SRC_FILES = join(ROOT_PATH, "../src/rules/") const ALTERNATIVES_DEST_PATH = join( ROOT_PATH, - "../src/rules/alternatives.publicodes", + "../src/rules/alternatives/achat-voiture.publicodes", ) const PERSONAS_DEST_PATH = join(ROOT_PATH, "../src/personas/personas.json") @@ -26,13 +26,13 @@ const resolvedRules = Object.fromEntries( ) const alternatives = generateAlternatives(resolvedRules) -console.log(`✅ './src/rules/alternatives.publicodes' generated`) writeFileSync( ALTERNATIVES_DEST_PATH, `# GENERATED FILE - DO NOT EDIT\n\n${stringify(alternatives, { aliasDuplicateObjects: false, })}`, ) +console.log(`✅ './src/rules/alternatives/achat-voiture.publicodes' generated`) const personas = getPersonas(resolvedRules) writeFileSync(PERSONAS_DEST_PATH, JSON.stringify(personas)) diff --git a/src/CarSimulator.ts b/src/CarSimulator.ts index 9d834a2..1fc8ca2 100644 --- a/src/CarSimulator.ts +++ b/src/CarSimulator.ts @@ -38,15 +38,18 @@ export type TargetInfos = { } /** - * Models an alternative to the current car (i.e. defined by the inputs). - * This is used to compare the current car with other alternatives. - * - * @note For now, the only alternative is a car, but in the future, we might - * have other alternatives like public transport, bike, etc. + * Models an alternative to the current car (i.e. defined by the inputs). This + * is used to compare the current car with other alternatives. */ -export type Alternative = { - kind: "car" -} & EvaluatedCarInfos +export type Alternative = + | ({ kind: "buy-new-car" } & EvaluatedCarInfos) + | ({ + kind: "keep-current-car" + /** The additional period (in years) to keep the car */ + additionalPeriod: EvaluatedRuleInfos< + RuleValue["alternatives . garder sa voiture . durée supplémentaire"] + > + } & EvaluatedCarInfos) /** * Full information about an evaluated value. @@ -83,10 +86,12 @@ const engineLogger = { } export const RULE_NAMES = Object.keys(rules) as RuleName[] -export const ALTERNATIVES_VOITURE_NAMESPACE: RuleName = "alternatives . voiture" +export const ALTERNATIVES_NAMESPACE: RuleName = "alternatives" export const ALTERNATIVES_RULES = RULE_NAMES.filter( (rule) => - rule.startsWith(ALTERNATIVES_VOITURE_NAMESPACE) && + rule.startsWith(ALTERNATIVES_NAMESPACE) && + // NOTE: this is a bit hacking, we should probably add a field in the + // publicodes rules to mark the rules that are alternatives. `${rule} . coûts` in rules && `${rule} . empreinte` in rules, ) @@ -193,55 +198,81 @@ export class CarSimulator { * @note This method is an expensive operation. */ public evaluateAlternatives(): Alternative[] { - const infos = ALTERNATIVES_RULES.map((rule: RuleName) => { - const splittedRule = rule.split(" . ").slice(2) - const sizeOption = splittedRule[0] as Questions["voiture . gabarit"] - const motorisationOption = - splittedRule[1] as Questions["voiture . motorisation"] - const fuelOption = ( - motorisationOption !== "électrique" ? splittedRule[2] : undefined - ) as Questions["voiture . thermique . carburant"] - - if (!sizeOption || !motorisationOption) { - throw new Error( - `Invalid alternative rule ${rule}. It should have a size and a motorisation option.`, - ) + return ALTERNATIVES_RULES.map((rule: RuleName) => { + if (rule.includes("acheter voiture")) { + return this.evaluateBuyNewCarAlternative(rule) + } else if (rule.includes("garder sa voiture")) { + return this.evaluateKeepCurrentCarAlternative(rule) } + }).filter((alternative) => alternative !== undefined) + } - return { - kind: "car", - title: this.engine.getRule(rule).title, - cost: this.evaluateRule(ruleName(rule, "coûts")), - emissions: this.evaluateRule(ruleName(rule, "empreinte")), - size: { - value: sizeOption, - title: this.engine.getRule(ruleName("voiture . gabarit", sizeOption)) - .title, - isEnumValue: true, - isApplicable: true, - }, - motorisation: { - value: motorisationOption, - title: this.engine.getRule( - ruleName("voiture . motorisation", motorisationOption), - ).title, - isEnumValue: true, - isApplicable: true, - }, - fuel: fuelOption - ? { - value: fuelOption, - title: this.engine.getRule( - ruleName("voiture . thermique . carburant", fuelOption), - ).title, - isEnumValue: true, - isApplicable: true, - } - : undefined, - } as Alternative - }) + private evaluateBuyNewCarAlternative(rule: RuleName): Alternative { + const splittedRule = rule.split(" . ").slice(2) + const sizeOption = splittedRule[0] as Questions["voiture . gabarit"] + const motorisationOption = + splittedRule[1] as Questions["voiture . motorisation"] + const fuelOption = ( + motorisationOption !== "électrique" ? splittedRule[2] : undefined + ) as Questions["voiture . thermique . carburant"] + + if (!sizeOption || !motorisationOption) { + throw new Error( + `Invalid alternative rule ${rule}. It should have a size and a motorisation option.`, + ) + } + + return { + kind: "buy-new-car", + title: this.engine.getRule(rule).title, + cost: this.evaluateRule(ruleName(rule, "coûts")), + emissions: this.evaluateRule(ruleName(rule, "empreinte")), + size: { + value: sizeOption, + title: this.engine.getRule(ruleName("voiture . gabarit", sizeOption)) + .title, + isEnumValue: true, + isApplicable: true, + }, + motorisation: { + value: motorisationOption, + title: this.engine.getRule( + ruleName("voiture . motorisation", motorisationOption), + ).title, + isEnumValue: true, + isApplicable: true, + }, + fuel: fuelOption + ? { + value: fuelOption, + title: this.engine.getRule( + ruleName("voiture . thermique . carburant", fuelOption), + ).title, + isEnumValue: true, + isApplicable: true, + } + : undefined, + } as Alternative + } + + private evaluateKeepCurrentCarAlternative(rule: RuleName): Alternative { + const additionalPeriod = this.evaluateRule( + ruleName(rule, "durée supplémentaire"), + ) - return infos + return { + kind: "keep-current-car", + title: this.engine.getRule(rule).title, + cost: this.evaluateRule(ruleName(rule, "coûts")), + emissions: this.evaluateRule(ruleName(rule, "empreinte")), + additionalPeriod, + size: this.evaluateRule("voiture . gabarit"), + motorisation: this.evaluateRule("voiture . motorisation"), + fuel: + this.evaluateRule("voiture . motorisation").value !== "électrique" + ? this.evaluateRule("voiture . thermique . carburant") + : undefined, + } as Alternative } /** diff --git a/src/rules/alternatives/alternatives.publicodes b/src/rules/alternatives/alternatives.publicodes new file mode 100644 index 0000000..adb1e85 --- /dev/null +++ b/src/rules/alternatives/alternatives.publicodes @@ -0,0 +1,60 @@ +alternatives: + titre: Alternatives + +alternatives . garder sa voiture: + titre: Conserver sa voiture actuelle 5 ans de plus + description: > + Conserver sa voiture actuelle peut permettre de réduire les coûts annuels + liés à votre voiture. En effet, il n'y aura pas de coûts supplémentaires + liés à l'achat d'une nouvelle voiture et vous étendrez la durée + d'amortissement de l'achat de votre voiture actuelle. + + + Cependant, il est important de vérifier que votre voiture actuelle respecte + les [normes environnementales en + vigueur](https://www.ecologie.gouv.fr/politiques-publiques/zones-faibles-emissions-zfe). + avec: + empreinte: + valeur: empreinte + note: > + La durée de détention de la voiture n'impacte pas l'empreinte carbone. + En effet, nous utilisons le modèle de [Nos Gestes + Climat](https://nosgestesclimat.fr) qui a fait le choix d'amortir + l'empreinte carbone sur la durée de vie du véhicule en km parcourus. + Cela signifie que seule la distance parcourue en une année détermine + l'empreinte carbone annuelle de la voiture et permet de répartir + l'amortissement de l'empreinte de la construction équitablement entre + toustes les propriétaires successif:ves. + + + Voir la documentation sur + [l'amortissement](https://nosgestesclimat.fr/documentation/transport/voiture/amortissement) + pour plus d'informations. + + coûts: + # TODO: prendre en compte l'augmentation des coûts d'entretien de la voiture + valeur: coûts + contexte: + voiture . durée de détention totale: voiture . durée de détention totale + durée supplémentaire + + durée supplémentaire: + titre: Durée de détention supplémentaire + question: Combien de temps souhaitez-vous garder votre voiture actuelle ? + # TODO: déterminer la valeur à ajouter pour la détention de la voiture + par défaut: 5 + unité: an + +# alternatives . utiliser le vélo: +# applicable si: +# toutes ces conditions: +# - usage . km annuels . calculés +# - usage . km annuels . calculés . quotidien <= 15 +# titre: Utiliser le vélo pour les trajets quotidiens +# description: > +# Utiliser le vélo est une option qui peut être intéressante si vous avez des +# trajets courts à effectuer. Cela vous permet de vous déplacer de manière +# écologique et de faire du sport en même temps. +# +# avec: +# empreinte: +# valeur: empreinte diff --git a/test/CarSimulator.test.ts b/test/CarSimulator.test.ts index a1905ed..da2fb13 100644 --- a/test/CarSimulator.test.ts +++ b/test/CarSimulator.test.ts @@ -284,9 +284,10 @@ describe("CarSimulator", () => { // Electrique nbSizes - expect(alternatives).toHaveLength(nbAlternatives) + expect(alternatives).toHaveLength( + nbAlternatives + 1 /* Garder sa voiture */, + ) alternatives.forEach((alternative) => { - expect(alternative.kind).toEqual("car") expect(alternative.cost.value).toBeGreaterThan(0) expect(alternative.emissions.value).toBeGreaterThan(0) expect(alternative.size.value).toBeDefined() @@ -299,6 +300,16 @@ describe("CarSimulator", () => { } else { expect(alternative.fuel).toBeUndefined() } + + if (alternative.kind === "keep-current-car") { + expect(alternative.additionalPeriod).toEqual({ + isApplicable: true, + isEnumValue: false, + title: "Durée de détention supplémentaire", + value: 5, + unit: "an", + }) + } }) }) }) diff --git a/test/alternatives.test.ts b/test/alternatives.test.ts new file mode 100644 index 0000000..d8f0188 --- /dev/null +++ b/test/alternatives.test.ts @@ -0,0 +1,68 @@ +import Engine from "publicodes" +import rules, { RuleName } from "../publicodes-build" +import { expect, test, describe } from "vitest" + +describe("Alternatives", () => { + const engine = new Engine(rules, { + logger: { + log: () => {}, + warn: () => {}, + error: (message: string) => console.error(message), + }, + }) + + describe("Garder sa voiture actuelle", () => { + test("diminue les coûts annuels", () => { + const coutsDeBase = engine.evaluate("coûts").nodeValue?.valueOf() + const acoutsAlternative = engine + .evaluate("alternatives . garder sa voiture . coûts") + .nodeValue?.valueOf() + + if ( + typeof coutsDeBase === "number" && + typeof acoutsAlternative === "number" + ) { + expect(acoutsAlternative).toBeLessThan(coutsDeBase) + } else { + expect(false, "les coûts doivent être des nombres").toBeTruthy() + } + }) + + test("garde la même empreinte", () => { + const empreinteDeBase = engine.evaluate("empreinte").nodeValue?.valueOf() + const empreinteAlternative = engine + .evaluate("alternatives . garder sa voiture . empreinte") + .nodeValue?.valueOf() + + if ( + typeof empreinteDeBase === "number" && + typeof empreinteAlternative === "number" + ) { + expect(empreinteAlternative).toEqual(empreinteDeBase) + } else { + expect(false, "les empreintes doivent être des nombres").toBeTruthy() + } + }) + + test("plus la durée de détention est longue, plus les coûts annuels diminuent", () => { + const coutsAlternative5ans = engine + .evaluate("alternatives . garder sa voiture . coûts") + .nodeValue?.valueOf() + const coutsAlternative7ans = engine + .setSituation({ + "alternatives . garder sa voiture . durée supplémentaire": 7, + }) + .evaluate("alternatives . garder sa voiture . coûts") + .nodeValue?.valueOf() + + if ( + typeof coutsAlternative5ans === "number" && + typeof coutsAlternative7ans === "number" + ) { + expect(coutsAlternative7ans).toBeLessThan(coutsAlternative5ans) + } else { + expect(false, "les coûts doivent être des nombres").toBeTruthy() + } + }) + }) +})