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 (19)
......@@ -229,3 +229,19 @@ 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-france/tests/leximpact/
```
- Le paramètre `year` générera les variables d'output pour l'année `year`
- L'argument par défaut `outdir` est le chemin où l'on veut exporter les YAML générés. Ceux-ci ont vocation à être pushés dans openfisca_france/tests/leximpact
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.
{
"name": "leximpact-socio-fiscal-ui",
"version": "0.0.1012",
"version": "0.0.1024",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "leximpact-socio-fiscal-ui",
"version": "0.0.1012",
"version": "0.0.1024",
"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.324",
"@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.324",
"resolved": "https://registry.npmjs.org/@leximpact/socio-fiscal-openfisca-json/-/socio-fiscal-openfisca-json-0.0.324.tgz",
"integrity": "sha512-Y20FwQoO2PMOzOhX5FFWSe7C9SPUVQPuaHqfQZliRuK7DCVTPXQWSTWhFTaWJ8Os48az2J0CySShUARuzF7rNA==",
"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.1012",
"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.324",
"@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,23 +46,10 @@
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)}
......@@ -92,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>
......
......@@ -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)
}
}
}
}
......
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"
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,
},
{
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(
entityData: JsonObject,
outputVariablesList: string[],
) {
const input: JsonObject = {}
const output: JsonObject = {}
for (const [variable, value] of Object.entries(entityData)) {
if (outputVariablesList.includes(variable)) {
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(
sectionData: JsonObject,
outputVariablesList: string[],
) {
const inputSection: JsonObject = {}
const outputSection: JsonObject = {}
for (const [entityKey, entityData] of Object.entries(sectionData)) {
const { input, output } = splitVariablesByCategory(
entityData,
outputVariablesList,
)
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,
outputVariablesList: string[],
) {
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(
simulatedTestCase[section],
outputVariablesList,
)
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
}
async function main() {
console.info("Initializing...")
const personsEntityKey = Object.entries(entityByKey)
.filter(([, entity]) => entity.is_person)
.map(([key]) => key)[0]
const year = options.year
const outdir = options.outdir
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[]
for (const testCase of testCasesCore) {
console.info("Processing ", testCase.id)
let testCaseContainsMissingVariable = false
let jsonForApiCall: JsonObject = {}
const outputVariablesList: Array<string> = []
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.warn(
"variable",
variable,
"does not exists in openFisca-France.",
)
testCaseContainsMissingVariable = true
}
}
}
}
}
if (testCaseContainsMissingVariable) {
console.warn("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
) {
outputVariablesList.push(variable)
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)
console.info("Launching simulation on OpenFisca France API...")
const openFiscaApiUrl = "https://api.fr.openfisca.org/latest/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,
outputVariablesList,
)
const yamlOutput = YAML.dump(jsonForYamlOutput, {
noCompatMode: true,
noRefs: true,
})
// Remove quotes around year keys
const cleanedYaml = yamlOutput.replace(/'(\d{4})':/g, "$1:")
await fs.writeFile(path.join(outdir, testCase.id + ".yml"), cleanedYaml)
console.info("YAML file written :", outdir, testCase.id + ".yml")
}
}
main()
.then(() => {
process.exit(0)
})
.catch((error: unknown) => {
console.error(error)
process.exit(1)
})