Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • leximpact/simulateur-socio-fiscal/leximpact-socio-fiscal-ui
  • dsmadja/leximpact-socio-fiscal-ui
2 results
Show changes
Commits on Source (31)
......@@ -229,3 +229,22 @@ Pour s'aider, on peut afficher la valeur dans la console :
```js
$: console.log(parameterSmicMensuel, smicValue)
```
### Générer les YAML de test OpenFisca à partir des cas-type LexImpact
```bash
npx tsx src/scripts/generate_openfisca_tests_yaml.ts -y 2025 --openfisca_venv /path/to/openfisca-france/.venv ../../openfisca-france/tests/leximpact/
```
- Le paramètre `year` (-y) générera les variables d'output pour l'année `year`
- Le paramètre `openfisca_venv` (-e) doit correspondre au chemin d'un venv dans lequel openfisca-france est installé. Il doit être possible d'y exécuter `openfisca test`.
- L'argument par défaut `outdir` est le chemin où l'on veut exporter les YAML générés.
Les YAML générés contiennent, pour chaque cas-type, toutes les variables d'entrées du cas-type dans le fichier `test_cases.json`.
Les valeurs sont mensualisées pour toutes les variables mensuelles.
La section `output:` du YAML contient toutes les variables calculées, présentes à la fois dans les décompositions LexImpact **et** dans les variables du country-package Openfisca-France.
Les valeurs des variables de type Enum, sont remplacées par leur index dans les valeurs possibles de l'Enum.
Les valeurs générées en sortie sont celles que retourne l'API d'openFisca-France **corrigées par les valeurs que retournent `openfisca test`** dans le cas où elles diffèrent.
{
"name": "leximpact-socio-fiscal-ui",
"version": "0.0.1005",
"version": "0.0.1026",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "leximpact-socio-fiscal-ui",
"version": "0.0.1005",
"version": "0.0.1026",
"devDependencies": {
"@auditors/core": "^0.7.0",
"@eslint/compat": "^1.2.4",
"@fontsource/lato": "^5.0.5",
"@fontsource/lora": "^5.0.5",
"@leximpact/socio-fiscal-openfisca-json": "^0.0.322",
"@leximpact/socio-fiscal-openfisca-json": "^0.0.333",
"@openfisca/json-model": "^3.1.0",
"@playwright/test": "^1.28.1",
"@popperjs/core": "^2.11.6",
......@@ -23,6 +23,7 @@
"@tailwindcss/typography": "^0.5.4",
"@tricoteuses/explorer-tools": "^0.6.2",
"@tricoteuses/legal-explorer": "^0.9.0",
"@types/command-line-args": "^5.2.3",
"@types/cookie": "^0.6.0",
"@types/d3-quadtree": "^3.0.4",
"@types/d3-scale": "^4.0.2",
......@@ -36,6 +37,7 @@
"autoprefixer": "^10.2.5",
"bits-ui": "^1.0.0-next.69",
"classnames": "^2.3.2",
"command-line-args": "^6.0.1",
"d3-quadtree": "^3.0.1",
"d3-scale": "^4.0.0",
"dedent-js": "^1.0.1",
......@@ -1072,9 +1074,9 @@
}
},
"node_modules/@leximpact/socio-fiscal-openfisca-json": {
"version": "0.0.322",
"resolved": "https://registry.npmjs.org/@leximpact/socio-fiscal-openfisca-json/-/socio-fiscal-openfisca-json-0.0.322.tgz",
"integrity": "sha512-mIZ0Ld8lncTfWbNv4X2XH6B3yITMalU66VOtRrP4rSH9YjkFKr82GLioyvuYEYpx9gZHfB7wEh62YvBjixSp8Q==",
"version": "0.0.333",
"resolved": "https://registry.npmjs.org/@leximpact/socio-fiscal-openfisca-json/-/socio-fiscal-openfisca-json-0.0.333.tgz",
"integrity": "sha512-4Dv3wsGba3PVovrQi1EtGXAdVMakofXZk5u0WIHc4rNrToi29auza6D1wYvKoMOJBl7X/kP/3otOrcODxuck3A==",
"dev": true,
"license": "AGPL-3.0-or-later",
"peerDependencies": {
......@@ -1717,6 +1719,13 @@
"svelte": "^5.2.8"
}
},
"node_modules/@types/command-line-args": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz",
"integrity": "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
......@@ -2182,6 +2191,16 @@
"node": ">= 0.4"
}
},
"node_modules/array-back": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz",
"integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.17"
}
},
"node_modules/array-buffer-byte-length": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
......@@ -2669,6 +2688,30 @@
"node": ">= 0.8"
}
},
"node_modules/command-line-args": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-6.0.1.tgz",
"integrity": "sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==",
"dev": true,
"license": "MIT",
"dependencies": {
"array-back": "^6.2.2",
"find-replace": "^5.0.2",
"lodash.camelcase": "^4.3.0",
"typical": "^7.2.0"
},
"engines": {
"node": ">=12.20"
},
"peerDependencies": {
"@75lb/nature": "latest"
},
"peerDependenciesMeta": {
"@75lb/nature": {
"optional": true
}
}
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
......@@ -3680,6 +3723,24 @@
"node": ">=8"
}
},
"node_modules/find-replace": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/find-replace/-/find-replace-5.0.2.tgz",
"integrity": "sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@75lb/nature": "latest"
},
"peerDependenciesMeta": {
"@75lb/nature": {
"optional": true
}
}
},
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
......@@ -4849,6 +4910,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.castarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
......@@ -7128,6 +7196,16 @@
"typescript": ">=4.8.4 <5.8.0"
}
},
"node_modules/typical": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz",
"integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.17"
}
},
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
......
{
"name": "leximpact-socio-fiscal-ui",
"version": "0.0.1005",
"version": "0.0.1026",
"type": "module",
"scripts": {
"build": "NODE_OPTIONS=--max_old_space_size=4096 vite build",
......@@ -18,7 +18,7 @@
"@eslint/compat": "^1.2.4",
"@fontsource/lato": "^5.0.5",
"@fontsource/lora": "^5.0.5",
"@leximpact/socio-fiscal-openfisca-json": "^0.0.322",
"@leximpact/socio-fiscal-openfisca-json": "^0.0.333",
"@openfisca/json-model": "^3.1.0",
"@playwright/test": "^1.28.1",
"@popperjs/core": "^2.11.6",
......@@ -29,6 +29,7 @@
"@tailwindcss/typography": "^0.5.4",
"@tricoteuses/explorer-tools": "^0.6.2",
"@tricoteuses/legal-explorer": "^0.9.0",
"@types/command-line-args": "^5.2.3",
"@types/cookie": "^0.6.0",
"@types/d3-quadtree": "^3.0.4",
"@types/d3-scale": "^4.0.2",
......@@ -42,6 +43,7 @@
"autoprefixer": "^10.2.5",
"bits-ui": "^1.0.0-next.69",
"classnames": "^2.3.2",
"command-line-args": "^6.0.1",
"d3-quadtree": "^3.0.1",
"d3-scale": "^4.0.0",
"dedent-js": "^1.0.1",
......
......@@ -46,28 +46,15 @@
displayMode.parametersVariableName!
]
const linkedVariablesList =
variableSummary !== undefined
? [
variableSummary.linked_output_variables?.filter(
(variableName) =>
!variableSummary.linked_added_variables?.includes(variableName),
),
variableSummary.linked_other_variables?.filter(
(variableName) =>
!variableSummary.linked_added_variables?.includes(variableName) &&
!variableSummary.linked_output_variables?.includes(variableName),
),
].filter((value) => value !== undefined && value.length > 0)
: undefined
const linkedVariables = variableSummary.linked_other_variables
</script>
{#if linkedVariablesList !== undefined && linkedVariablesList.length > 0}
{#if linkedVariables !== undefined}
<div class="fond flex px-4">
<!--Indentation pour chaque niveau de l'arbre, illustré par une bordure-->
{#each iterToDepth(depth)}
<div
class={`min-h-full border-l-2 bg-white pl-1 pr-2 ${
class={`min-h-full border-l-2 bg-white pr-3 ${
decomposition.name !== displayMode.parametersVariableName &&
!(
displayMode.parametersVariableName &&
......@@ -80,9 +67,7 @@
? "border-gray-400"
: "border-black"
}`}
>
&nbsp;
</div>
></div>
{/each}
<div
class="w-full border-dashed border-black"
......@@ -94,61 +79,59 @@
class="mb-2 flex w-full grow flex-col justify-end lg:flex-row"
id="situation_{situationIndex}_totalimpact"
>
{#each linkedVariablesList as linkedVariables}
<div class="flex w-full grow py-3 pl-5">
{#if linkedVariables !== undefined && linkedVariables.length > 0}
{@const linkedVariablesValueByCalculationName =
linkedVariables.map((name) =>
variableValueByCalculationNameFromEvaluation(
evaluationByName[name],
revaluationName,
billName,
shared.parametricReform,
),
)}
{#if shared.showNulls || !linkedVariablesValueByCalculationName.every(isNullVariableValueByCalculationName)}
<ul
class="flex h-fit flex-col rounded-md border bg-white p-2 text-gray-800"
>
{#each linkedVariables as linkedVariableName, index}
{@const linkedVariableValueByCalculationName =
linkedVariablesValueByCalculationName[index]}
{#if shared.showNulls || !isNullVariableValueByCalculationName(linkedVariableValueByCalculationName)}
{@const linkedVariableSummary =
billName === undefined
? variableSummaryByName[linkedVariableName]
: variableSummaryByNameByReformName[billName][
linkedVariableName
]}
<li class="flex justify-between gap-2 text-sm">
<a
class="2xl:text-md max-w-32 cursor-pointer overflow-x-hidden text-ellipsis text-nowrap hover:underline sm:max-w-none lg:max-w-44 xl:max-w-none"
href={newSimulationUrl({
...displayMode,
parametersVariableName: linkedVariableName,
})}
data-sveltekit-noscroll
><span class="text-gray-600"
>{linkedVariableSummary.short_label ??
linkedVariableSummary.label ??
linkedVariableName}&nbsp;:&nbsp;</span
></a
>
<VariableValueChange
{evaluationByName}
name={linkedVariableName}
valueByCalculationName={linkedVariableValueByCalculationName}
inline
bold
/>
</li>
{/if}
{/each}
</ul>
{/if}
<div class="flex w-full grow py-3 pl-5">
{#if linkedVariables !== undefined}
{@const linkedVariablesValueByCalculationName = linkedVariables.map(
(name) =>
variableValueByCalculationNameFromEvaluation(
evaluationByName[name],
revaluationName,
billName,
shared.parametricReform,
),
)}
{#if shared.showNulls || !linkedVariablesValueByCalculationName.every(isNullVariableValueByCalculationName)}
<ul
class="flex h-fit flex-col rounded-md border bg-white p-2 text-gray-800"
>
{#each linkedVariables as linkedVariableName, index}
{@const linkedVariableValueByCalculationName =
linkedVariablesValueByCalculationName[index]}
{#if shared.showNulls || !isNullVariableValueByCalculationName(linkedVariableValueByCalculationName)}
{@const linkedVariableSummary =
billName === undefined
? variableSummaryByName[linkedVariableName]
: variableSummaryByNameByReformName[billName][
linkedVariableName
]}
<li class="flex justify-between gap-2 text-sm">
<a
class="2xl:text-md max-w-32 cursor-pointer overflow-x-hidden text-ellipsis text-nowrap hover:underline sm:max-w-none lg:max-w-44 xl:max-w-none"
href={newSimulationUrl({
...displayMode,
parametersVariableName: linkedVariableName,
})}
data-sveltekit-noscroll
><span class="text-gray-600"
>{linkedVariableSummary.short_label ??
linkedVariableSummary.label ??
linkedVariableName}&nbsp;:&nbsp;</span
></a
>
<VariableValueChange
{evaluationByName}
name={linkedVariableName}
valueByCalculationName={linkedVariableValueByCalculationName}
inline
bold
/>
</li>
{/if}
{/each}
</ul>
{/if}
</div>
{/each}
{/if}
</div>
</div>
</div>
</div>
......
......@@ -361,7 +361,7 @@
<!--Indentation pour chaque niveau de l'arbre, illustré par une bordure-->
{#each iterToDepth(depth)}
<div
class={`min-h-full border-l-2 bg-white pl-1 pr-2 ${
class={`min-h-full border-l-2 bg-white pr-3 ${
decomposition.name !==
displayMode.parametersVariableName &&
!(
......@@ -375,9 +375,7 @@
? "border-gray-400"
: "border-black"
}`}
>
&nbsp;
</div>
></div>
{/each}
<!--Si la variable est obsolète, ou que sa date de relecture est inconnue ou trop ancienne, un picto attention est affiché -->
{#if decomposition.obsolete || decomposition.last_value_still_valid_on === undefined || decomposition.last_value_still_valid_on < (new Date().getFullYear() - 2).toString()}
......@@ -448,7 +446,7 @@
<!--Indentation pour chaque niveau de l'arbre, illustré par une bordure-->
{#each iterToDepth(depth)}
<div
class={`min-h-full border-l-2 bg-white pl-1 pr-2 ${
class={`min-h-full border-l-2 bg-white pr-3 ${
decomposition.name !==
displayMode.parametersVariableName &&
!(
......@@ -462,9 +460,7 @@
? "border-gray-400"
: "border-black"
}`}
>
&nbsp;
</div>
></div>
{/each}
<button
class="cursor-pointer overflow-x-hidden text-ellipsis text-left font-serif hover:z-20 hover:overflow-x-visible hover:bg-white hover:bg-opacity-90 hover:text-le-gris-dispositif-dark hover:underline"
......
......@@ -4,7 +4,7 @@
const bubble = createBubbler()
import type { ValueParameter } from "@openfisca/json-model"
import { billName, yearPLF } from "$lib/shared.svelte"
import { billName, revaluationName } from "$lib/shared.svelte"
import { formatValue } from "$lib/values"
interface Props {
......@@ -51,6 +51,21 @@
? formatValue(billInflator.values["latest"].value, billInflator.unit)
: undefined,
)
let revaluationInflator = $derived(
revaluationName === undefined
? undefined
: inflatorWithLatestByReformName?.[revaluationName],
)
let revaluationInflatorValueFormatted = $derived(
revaluationInflator?.values !== undefined
? formatValue(
revaluationInflator.values["latest"].value,
revaluationInflator.unit,
)
: undefined,
)
</script>
<button
......@@ -73,7 +88,7 @@
Revalorisation de {billInflatorValueFormatted}, suite à l'indexation
d'usage du barème de l'IR par le PLF.
{:else}
Indexation sur l'inflation à {billInflatorValueFormatted}.
Indexation sur l'inflation à {revaluationInflatorValueFormatted}.
{/if}
</p>
</button>
......
......@@ -140,7 +140,9 @@
)
let revaluationInflator = $derived(
inflatorWithLatestByReformName?.[revaluationName],
revaluationName === undefined
? undefined
: inflatorWithLatestByReformName?.[revaluationName],
)
let revaluationInflatorValueFormatted = $derived(
revaluationInflator !== undefined
......@@ -151,7 +153,12 @@
: undefined,
)
let billInflator = $derived(inflatorWithLatestByReformName?.[billName])
let billInflator = $derived(
billName === undefined
? undefined
: inflatorWithLatestByReformName?.[billName],
)
let billInflatorValueFormatted = $derived(
billInflator !== undefined
? formatValue(billInflator.values["latest"].value, billInflator.unit)
......
......@@ -1532,17 +1532,6 @@ function extractLinkedVariablesName(
if (variableSummary === undefined) {
console.warn("Unknown variable in extractLinkedVariablesName():", name)
}
for (const linkedVariableName of variableSummary?.linked_added_variables ??
[]) {
if (!linkedVariablesName.has(linkedVariableName)) {
linkedVariablesName.add(linkedVariableName)
extractLinkedVariablesName(
linkedVariablesName,
linkedVariableName,
variableSummaryByName,
)
}
}
for (const linkedVariableName of variableSummary?.linked_other_variables ??
[]) {
if (!linkedVariablesName.has(linkedVariableName)) {
......@@ -1554,17 +1543,6 @@ function extractLinkedVariablesName(
)
}
}
for (const linkedVariableName of variableSummary?.linked_output_variables ??
[]) {
if (!linkedVariablesName.has(linkedVariableName)) {
linkedVariablesName.add(linkedVariableName)
extractLinkedVariablesName(
linkedVariablesName,
linkedVariableName,
variableSummaryByName,
)
}
}
}
function extractWithLinkedVariableNames(
......@@ -1586,18 +1564,10 @@ function extractWithLinkedVariableNames(
if (!variableNames.includes(name)) {
variableNames.push(name)
const variableSummary = variableSummaryByName[name]
for (const linkedVariableName of variableSummary?.linked_added_variables ??
[]) {
linkedVariableNames.add(linkedVariableName)
}
for (const linkedVariableName of variableSummary?.linked_other_variables ??
[]) {
linkedVariableNames.add(linkedVariableName)
}
for (const linkedVariableName of variableSummary?.linked_output_variables ??
[]) {
linkedVariableNames.add(linkedVariableName)
}
}
}
}
......@@ -1638,18 +1608,10 @@ function extractNonVirtualVariablesName(
if (!decomposition.virtual && !nonVirtualVariablesName.includes(name)) {
nonVirtualVariablesName.push(name)
const variableSummary = variableSummaryByName[name]
for (const linkedVariableName of variableSummary.linked_added_variables ??
[]) {
linkedVariablesName.add(linkedVariableName)
}
for (const linkedVariableName of variableSummary.linked_other_variables ??
[]) {
linkedVariablesName.add(linkedVariableName)
}
for (const linkedVariableName of variableSummary.linked_output_variables ??
[]) {
linkedVariablesName.add(linkedVariableName)
}
}
}
}
......
......@@ -303,9 +303,13 @@ export function setSituationVariableValue(
) {
return false
}
valueByPeriod[year - 2] = value
valueByPeriod[year - 1] = value
valueByPeriod[year] = value
if (variable.name === "date_naissance") {
valueByPeriod[year] = value
} else {
valueByPeriod[year - 2] = value
valueByPeriod[year - 1] = value
valueByPeriod[year] = value
}
return true
}
......
import type { Situation } from "@openfisca/json-model"
import commandLineArgs from "command-line-args"
import fs from "fs-extra"
import path from "path"
import YAML from "js-yaml"
import testCasesCoreUnknown from "@leximpact/socio-fiscal-openfisca-json/test_cases.json"
import {
summaryCalculatedVariablesName,
otherCalculatedVariablesName,
variableSummaryByName,
} from "$lib/variables"
import { nonVirtualVariablesName } from "$lib/decompositions"
import type { EntityByKey, Entity, GroupEntity } from "@openfisca/json-model"
import { entityByKey } from "$lib/entities"
import { spawn } from "child_process"
type JsonObject = { [key: string]: any }
const optionsDefinitions = [
{
alias: "s",
help: "don't log anything",
name: "silent",
type: Boolean,
},
{
alias: "v",
help: "verbose logs",
name: "verbose",
type: Boolean,
},
{
alias: "y",
help: "Year to ask as simulation output",
name: "year",
type: Number,
},
{
alias: "e",
help: "Path to a venv with openfisca-france installed. You must be able to launch `openfisca test` within it.",
name: "openfisca_venv",
type: String,
},
{
alias: "t",
help: "Single Test-Case to run",
name: "testcase",
type: String,
},
{
defaultOption: true,
help: "Directory to write OpenFisca test YAML files",
name: "outdir",
type: String,
},
]
const options = commandLineArgs(optionsDefinitions)
async function fetchWithRetries(
url: string,
options: RequestInit,
maxRetries = 5,
delay = 10000,
) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options)
if (response.ok) {
return response
}
console.warn(
`Calling Openfisca API. Attempt ${attempt} : Failed with status ${response.status}`,
)
} catch (error) {
console.error(`Error on attempt ${attempt} :`, error)
}
if (attempt < maxRetries) {
console.log(`New attempt in ${delay / 1000}s...`)
await new Promise((res) => setTimeout(res, delay))
}
}
throw new Error(`Failed calling OpenFisca API after ${maxRetries} attempts`)
}
function removeSpacesFromKeys(
obj: JsonObject,
replacedKeys: { [key: string]: string },
): JsonObject {
const newObj: JsonObject = {}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
let newKey: string | number = key
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.replace(/ |’|°/g, "_")
if (newKey !== key) {
replacedKeys[key] = newKey
}
const value = obj[key]
if (
typeof value === "object" &&
value !== null &&
!Array.isArray(value)
) {
newObj[newKey] = removeSpacesFromKeys(value, replacedKeys)
} else {
newObj[newKey] = value
}
}
}
return newObj
}
function replaceValuesFromReplacedKeys(
obj: JsonObject,
replacedKeys: { [key: string]: string },
): JsonObject {
const newObj: JsonObject = {}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key]
if (
typeof value === "object" &&
value !== null &&
!Array.isArray(value)
) {
newObj[key] = replaceValuesFromReplacedKeys(value, replacedKeys)
} else if (
typeof value === "string" &&
replacedKeys.hasOwnProperty(value)
) {
newObj[key] = replacedKeys[value]
} else if (Array.isArray(value)) {
const newArray = []
for (const arrayValue of value) {
if (
typeof arrayValue === "string" &&
replacedKeys.hasOwnProperty(arrayValue)
) {
newArray.push(replacedKeys[arrayValue])
} else {
newArray.push(arrayValue)
}
}
newObj[key] = newArray
} else {
newObj[key] = value
}
}
}
return newObj
}
function removeZeroValues(obj: any): any {
if (typeof obj !== "object" || obj === null) {
return obj
}
if (Array.isArray(obj)) {
return obj.map(removeZeroValues)
}
const newObj: any = {}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key]
if (typeof value === "object" && value !== null) {
if (Object.values(value).every((v) => v === 0 || v === null)) {
continue
}
const cleanedValue = removeZeroValues(value)
if (Object.keys(cleanedValue).length > 0) {
newObj[key] = cleanedValue
}
} else {
newObj[key] = value
}
}
}
return newObj
}
function cleanSimulatedJson(jsonData: any): any {
const sections = ["familles", "foyers_fiscaux", "menages", "individus"]
for (const section of sections) {
if (jsonData[section]) {
jsonData[section] = Object.fromEntries(
Object.entries(jsonData[section]).map(([key, value]) => [
key,
removeZeroValues(value),
]),
)
}
}
return jsonData
}
function splitVariablesByCategory(
section: string,
entityKey: string,
entityData: JsonObject,
initialTestCase: Situation,
) {
const input: JsonObject = {}
const output: JsonObject = {}
for (const [variable, value] of Object.entries(entityData)) {
const replacedKeys: { [key: string]: string } = {}
initialTestCase = removeSpacesFromKeys(initialTestCase, replacedKeys)
if (
initialTestCase[section][entityKey] !== undefined &&
initialTestCase[section][entityKey][variable] === undefined
) {
if (
variableSummaryByName[variable].value_type === "Enum" &&
variableSummaryByName[variable].possible_values !== undefined
) {
// Replace Enum values by their index in possible_values
const newValue: Record<string, number> = {}
for (const [period, periodValue] of Object.entries(value)) {
newValue[period] = Object.keys(
variableSummaryByName[variable].possible_values,
).indexOf(periodValue as string)
}
output[variable] = newValue
} else {
output[variable] = value
}
} else {
input[variable] = value
}
}
return { input, output }
}
function processSection(
section: string,
sectionData: JsonObject,
initialTestCase: Situation,
) {
const inputSection: JsonObject = {}
const outputSection: JsonObject = {}
for (const [entityKey, entityData] of Object.entries(sectionData)) {
const { input, output } = splitVariablesByCategory(
section,
entityKey,
entityData,
initialTestCase,
)
if (Object.keys(input).length > 0) {
inputSection[entityKey] = input
}
if (Object.keys(output).length > 0) {
outputSection[entityKey] = output
}
}
return { input: inputSection, output: outputSection }
}
function buildYamlOutput(
name: string,
year: string,
simulatedTestCase: JsonObject,
testCase: Situation,
) {
const sections = ["familles", "foyers_fiscaux", "menages", "individus"]
const jsonForYamlOutput: {
name: string
period: string
max_spiral_loops: number
input: JsonObject
output: JsonObject
} = {
name: name,
period: year,
max_spiral_loops: 4,
input: {},
output: {},
}
for (const section of sections) {
if (simulatedTestCase[section]) {
const { input, output } = processSection(
section,
simulatedTestCase[section],
testCase,
)
if (Object.keys(input).length > 0) {
jsonForYamlOutput.input[section] = input
}
if (Object.keys(output).length > 0) {
jsonForYamlOutput.output[section] = output
}
}
}
return jsonForYamlOutput
}
function mensualiseVariable(obj: any, variable: string) {
if (!obj || typeof obj !== "object") return obj
const annuelValues = obj
const monthlyValues: any = {}
for (const year in annuelValues) {
if (typeof annuelValues[year] === "number") {
const monthlyAmount =
variableSummaryByName[variable]["set_input"] ===
"set_input_divide_by_period"
? parseFloat((annuelValues[year] / 12).toFixed(2))
: annuelValues[year]
for (let month = 1; month <= 12; month++) {
const monthStr = month < 10 ? `0${month}` : `${month}`
monthlyValues[`${year}-${monthStr}`] = monthlyAmount
}
} else {
monthlyValues[year] = annuelValues[year]
}
}
obj = monthlyValues
return obj
}
function getEntityKeyFromKeyPlural(
keyPlural: string,
entityByKey: EntityByKey,
): string | null {
for (const entity of Object.values(entityByKey)) {
if (entity.key_plural === keyPlural) {
return entity.key
}
}
return null
}
function isGroupEntity(entity: Entity): entity is GroupEntity {
return (entity as GroupEntity).roles !== undefined
}
function isKeyOrPlural(str: string): boolean {
const keys = Object.keys(entityByKey)
for (const key of keys) {
const entity = entityByKey[key]
if (entity.key === str || entity.key_plural === str) {
return true
}
if (isGroupEntity(entity) && entity.roles) {
for (const role of entity.roles) {
if (role.key === str || role.key_plural === str) {
return true
}
if (role.subroles) {
for (const subrole of role.subroles) {
if (subrole.key === str || subrole.key_plural === str) {
return true
}
}
}
}
}
}
return false
}
function replaceVariableValue(
jsonData: any,
variableName: string,
currentValue: any,
newValue: any,
period: string,
): any {
const entities = ["familles", "foyers_fiscaux", "menages", "individus"]
for (const entity of entities) {
if (!jsonData.output[entity]) continue
for (const entityKey in jsonData.output[entity]) {
const entityData = jsonData.output[entity][entityKey]
if (entityData.hasOwnProperty(variableName)) {
const variableData = entityData[variableName]
if (typeof variableData === "object") {
if (
variableData.hasOwnProperty(period) &&
Math.trunc(Number(variableData[period])) ===
Math.trunc(Number(currentValue))
) {
console.info(
`Replacing ${variableName} (${period}): ${variableData[period]}${newValue}`,
)
variableData[period] = Number(newValue)
} else {
console.log(
"Warning : rounded value",
Math.trunc(Number(currentValue)),
"not found for",
variableName,
"\nThe cause can be multiple instance of the same variable for different entities... or bug",
)
}
}
}
}
}
return
}
async function runOpenFiscaTest(
openFiscaVenvPath: string,
yamlFilePath: string,
): Promise<any> {
return new Promise<void>((resolve, reject) => {
const openfiscaExecutable = path.join(openFiscaVenvPath, "bin", "openfisca")
const args = ["test", "--country-package", "openfisca_france", yamlFilePath]
const env = {
...process.env,
VIRTUAL_ENV: openFiscaVenvPath,
PATH: `${path.join(openFiscaVenvPath, "bin")}:${process.env.PATH}`,
PYTHONPATH: path.join(
openFiscaVenvPath,
"lib",
"python3.12",
"site-packages",
),
}
let testOutput = ""
const pythonProcess = spawn(openfiscaExecutable, args, {
stdio: ["inherit", "pipe", "pipe"],
env,
})
pythonProcess.stdout.on("data", (data) => {
testOutput += data.toString()
})
pythonProcess.stderr.on("data", (data) => {
console.error(`stderr: ${data}`)
reject(data)
})
pythonProcess.on("close", (code) => {
let returned = undefined as any
if (code !== 0) {
returned = { passed: false }
const testOutputLines = testOutput.split("\n")
const regex =
/(?<variable>[a-zA-Z0-9_]+)@(?<period>\d{4}(?:-\d{2})?):\s+(?<calculated>-?\d+(?:\.\d+)?)\s+differs\s+from\s+(?<expected>-?\d+(?:\.\d+)?)/
testOutputLines.forEach((line) => {
const match = line.match(regex)
if (match && match.groups) {
returned = { ...returned, ...match.groups }
}
})
if (returned.variable !== undefined) {
resolve(returned)
} else
reject(
"Erreur de récupération des valeurs lors d'un appel à openfisca test",
)
} else {
returned = { passed: true }
resolve(returned)
}
})
})
}
async function main() {
console.info("Initializing...")
const year = options.year
const outdir = options.outdir
const singleTestCase = options.testcase
const openFiscaVenvPath = options.openfisca_venv
const variablesFromDecompositions = [
...summaryCalculatedVariablesName,
...otherCalculatedVariablesName,
...nonVirtualVariablesName,
]
const openFiscaVariablesListApiCall = await fetch(
"https://api.fr.openfisca.org/latest/variables",
{
method: "GET",
},
)
if (!openFiscaVariablesListApiCall.ok) {
console.error(
`Error ${
openFiscaVariablesListApiCall.status
} while calling Openfisca France API`,
)
return
}
const openFiscaVariablesList = await openFiscaVariablesListApiCall.json()
const variablesToCalculate = variablesFromDecompositions.filter(
(variable: string) => openFiscaVariablesList.hasOwnProperty(variable),
)
const testCasesCore = testCasesCoreUnknown as unknown as Situation[]
const testCaseCount = testCasesCore.length
let processedCounter = 0
for (const testCase of testCasesCore) {
if (singleTestCase !== undefined && testCase.id !== singleTestCase) {
continue
}
console.info("Processing ", testCase.id)
processedCounter++
let testCaseContainsMissingVariable = false
let jsonForApiCall: JsonObject = {}
jsonForApiCall.input = {}
const replacedKeys: { [key: string]: string } = {}
const sections = ["familles", "foyers_fiscaux", "menages", "individus"]
// Mensualise monthly input variables
for (const section of sections) {
if (testCase[section] !== undefined) {
for (const entityKey in testCase[section]) {
for (const variable in testCase[section][entityKey]) {
if (
variableSummaryByName.hasOwnProperty(variable) &&
variableSummaryByName[variable].definition_period === "month"
) {
testCase[section][entityKey][variable] = mensualiseVariable(
testCase[section][entityKey][variable],
variable,
)
} else if (
!openFiscaVariablesList.hasOwnProperty(variable) &&
!isKeyOrPlural(variable)
) {
console.info(
"variable",
variable,
"does not exists in openFisca-France.",
)
testCaseContainsMissingVariable = true
}
}
}
}
}
if (testCaseContainsMissingVariable) {
console.info("Ignoring test case", testCase.id)
continue
}
for (const section of sections) {
if (testCase[section] !== undefined) {
jsonForApiCall.input[section] = removeSpacesFromKeys(
testCase[section],
replacedKeys,
)
for (const entityKey in jsonForApiCall.input[section]) {
for (const variable of variablesToCalculate) {
if (variableSummaryByName[variable] !== undefined) {
if (
variableSummaryByName[variable].entity ===
getEntityKeyFromKeyPlural(section, entityByKey) &&
jsonForApiCall.input[section][entityKey][variable] === undefined
) {
switch (variableSummaryByName[variable].definition_period) {
case "year": {
jsonForApiCall.input[section][entityKey][variable] = {
[year]: null,
}
break
}
case "month": {
const monthVariableSection = {}
for (let month = 1; month <= 12; month++) {
const monthString = month < 10 ? "0" + month : month
const monthPeriod = year + "-" + monthString
Object.assign(monthVariableSection, {
[monthPeriod]: null,
})
}
jsonForApiCall.input[section][entityKey][variable] =
monthVariableSection
}
}
}
} else {
console.log(
"cette variable n'existe pas dans summary : ",
variable,
)
}
}
}
}
}
jsonForApiCall = replaceValuesFromReplacedKeys(jsonForApiCall, replacedKeys)
// await fs.writeFile(
// path.join(".", testCase.id + ".json"),
// JSON.stringify(jsonForApiCall.input, null, 2),
// )
console.info("Launching simulation on OpenFisca France API...")
const openFiscaApiUrl = "https://api.fr.openfisca.org/latest/calculate"
// const openFiscaApiUrl = "http://localhost:5000/calculate"
const openFiscaApiOptions: RequestInit = {
body: JSON.stringify(jsonForApiCall.input),
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
method: "POST",
}
let simulatedTestCaseFromOpenFiscaApi: Response | null = null
try {
simulatedTestCaseFromOpenFiscaApi = await fetchWithRetries(
openFiscaApiUrl,
openFiscaApiOptions,
)
if (!simulatedTestCaseFromOpenFiscaApi.ok) {
console.error(
`Giving up calling openFisca API because of status ${simulatedTestCaseFromOpenFiscaApi.status}`,
)
return
}
} catch (error) {
console.error("Error : ", error)
}
console.info("Post-processing...")
const simulatedTestCase = cleanSimulatedJson(
await simulatedTestCaseFromOpenFiscaApi!.json(),
)
const jsonForYamlOutput = buildYamlOutput(
testCase.id!,
year,
simulatedTestCase,
testCase,
)
let ok = false
while (!ok) {
const yamlOutput = YAML.dump(jsonForYamlOutput, {
noCompatMode: true,
noRefs: true,
})
const cleanedYaml = yamlOutput.replace(/'(\d{4})':/g, "$1:")
await fs.writeFile(path.join(outdir, testCase.id + ".yml"), cleanedYaml)
try {
const openFiscaTestResult = await runOpenFiscaTest(
openFiscaVenvPath,
path.join(outdir, testCase.id + ".yml"),
)
if (!openFiscaTestResult.passed) {
if (
openFiscaTestResult.variable === undefined ||
openFiscaTestResult.expected === undefined ||
openFiscaTestResult.calculated === undefined ||
openFiscaTestResult.period === undefined
) {
throw new Error("Error retrieving values to update")
}
replaceVariableValue(
jsonForYamlOutput,
openFiscaTestResult.variable,
openFiscaTestResult.expected,
openFiscaTestResult.calculated,
openFiscaTestResult.period,
)
} else {
ok = true
console.info("YAML file written :", outdir, testCase.id + ".yml")
}
} catch (error) {
console.error(error)
}
}
console.info("Processed test-case", processedCounter, "over", testCaseCount)
}
}
main()
.then(() => {
process.exit(0)
})
.catch((error: unknown) => {
console.error(error)
process.exit(1)
})