From 73846fd86f8897377463872c6107994b1f95f505 Mon Sep 17 00:00:00 2001
From: Toufic Batache <taffou2a@gmail.com>
Date: Fri, 1 Sep 2023 17:42:30 +0200
Subject: [PATCH] =?UTF-8?q?Automatiquement=20d=C3=A9tecter=20si=20une=20si?=
 =?UTF-8?q?mulation=20publique=20est=20disponible?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../components/BudgetConnexionModal.svelte    | 131 ++++++++++++------
 .../BudgetSimulationSharingModal.svelte       |  11 +-
 src/lib/hash.ts                               |  17 +++
 src/lib/simulations.ts                        |   1 +
 src/routes/simulations/+server.ts             |  10 +-
 src/routes/simulations_budget/+server.ts      |  16 +--
 6 files changed, 124 insertions(+), 62 deletions(-)
 create mode 100644 src/lib/hash.ts

diff --git a/src/lib/components/BudgetConnexionModal.svelte b/src/lib/components/BudgetConnexionModal.svelte
index 5bc8170fd..afb27f2f1 100644
--- a/src/lib/components/BudgetConnexionModal.svelte
+++ b/src/lib/components/BudgetConnexionModal.svelte
@@ -6,19 +6,43 @@
     Transition,
     TransitionChild,
   } from "@rgossiaux/svelte-headlessui"
+  import { getContext } from "svelte"
+  import type { Writable } from "svelte/types/runtime/store"
 
   import { browser } from "$app/environment"
   import { page } from "$app/stores"
+  import { hashObject } from "$lib/hash"
+  import type { ParametricReform } from "$lib/reforms"
   import type { CachedSimulation } from "$lib/simulations"
+  import { budgetEditableParametersName } from "$lib/variables"
 
   export let isOpen = false
 
   let cachedSimulations: CachedSimulation[] = []
+  let currentSimulationCache: CachedSimulation | undefined
+  const parametricReform = getContext(
+    "parametricReform",
+  ) as Writable<ParametricReform>
 
-  $: if (browser) {
+  $: if (browser && isOpen) {
     fetchCachedSimulations()
   }
 
+  $: currentSimulationCache = cachedSimulations.find(
+    (simulation) => simulation.parametersHash === parametricReformDigest,
+  )
+
+  $: parametricReformDigest = hashObject(
+    Object.entries($parametricReform)
+      .sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
+      .reduce((filtered: ParametricReform, [parameterName, value]) => {
+        if (budgetEditableParametersName.has(parameterName)) {
+          filtered[parameterName] = value
+        }
+        return filtered
+      }, {}),
+  )
+
   async function fetchCachedSimulations() {
     const res = await fetch("/simulations_budget/index", {
       method: "POST",
@@ -125,51 +149,78 @@
                 >.</span
               >
             </div>
-            <div class="bg-gray-100 px-5 pb-10 pt-5 border-t mt-10">
-              <h2 class="w-full text-left text-2xl font-bold">
-                Demandez le calcul de votre réforme au service LexImpact&nbsp;:
-              </h2>
-              <p class="w-full text-base font-normal leading-6 mt-1.5 mb-5">
-                Après vérification par nos services, si elle est calculable avec
-                les données dont nous disposons et répond au secret statistique,
-                la simulation sera rendue publique. Vous serez alors informé par
-                e-mail :
-              </p>
-              <span class="font-bold text-sm py-2 pl-10"
-                >Votre adresse e-mail :</span
-              >
+
+            {#if currentSimulationCache !== undefined}
               <div
-                class="flex md:flex-row flex-col w-full px-0 md:px-10 items-center gap-5"
+                class="flex justify-between items-center bg-le-jaune-light bg-opacity-50 px-5 py-3 mt-10"
               >
-                <div
-                  class="flex rounded-t-md border-b-2 border-b-black bg-white px-2 grow max-w-lg"
+                <h2 class="text-2xl font-bold">
+                  🎉&nbsp;&nbsp;&nbsp;Votre réforme est déjà calculée !
+                </h2>
+                <a
+                  class="flex items-center gap-2 p-2 hover:bg-gray-500 hover:bg-opacity-10 active:bg-gray-500 active:bg-opacity-20 rounded-md underline text-gray-600 text-sm font-bold tracking-[0.085em] uppercase"
+                  data-sveltekit-reload
+                  href="/simulations_budget/{currentSimulationCache.hash}"
+                  title="Voir la simulation"
                 >
-                  <input
-                    autocomplete="off"
-                    class="w-full px-3 py-2 border-none bg-transparent text-sm text-gray-900 placeholder-gray-400 !ring-transparent focus:outline-none 2xl:text-base"
-                    id="search"
-                    placeholder="e-mail@email.fr"
-                    type="search"
+                  Voir la simulation<iconify-icon
+                    class="align-[-0.25rem] text-xl"
+                    icon="ri-arrow-right-line"
                   />
-                </div>
+                </a>
+              </div>
+            {/if}
+
+            <div class="flex flex-col gap-10 bg-gray-100 p-5 border-t mt-10">
+              {#if currentSimulationCache === undefined}
                 <div>
-                  <a
-                    class="flex items-center gap-2 py-2 px-5 shadow-lg bg-white hover:bg-gray-100 active:bg-gray-200 rounded-md border-2 border-le-bleu text-le-bleu text-sm font-bold tracking-[0.085em] uppercase"
-                    data-sveltekit-reload
-                    href={`/auth/login?redirect=${encodeURIComponent(
-                      $page.url.toString(),
-                    )}`}
-                    title="Envoyer votre réforme budgétaire avec cet e-mail"
+                  <h2 class="w-full text-left text-2xl font-bold">
+                    Demandez le calcul de votre réforme au service
+                    LexImpact&nbsp;:
+                  </h2>
+                  <p class="w-full text-base font-normal leading-6 mt-1.5 mb-5">
+                    Après vérification par nos services, si elle est calculable
+                    avec les données dont nous disposons et répond au secret
+                    statistique, la simulation sera rendue publique. Vous serez
+                    alors informé par e-mail :
+                  </p>
+                  <span class="font-bold text-sm py-2 pl-10"
+                    >Votre adresse e-mail :</span
                   >
-                    Demander&nbsp;le&nbsp;calcul <iconify-icon
-                      class="ml-2 align-[-0.25rem] text-xl"
-                      icon="ri-send-plane-2-line"
-                    />
-                  </a>
+                  <div
+                    class="flex md:flex-row flex-col w-full px-0 md:px-10 items-center gap-5"
+                  >
+                    <div
+                      class="flex rounded-t-md border-b-2 border-b-black bg-white px-2 grow max-w-lg"
+                    >
+                      <input
+                        autocomplete="off"
+                        class="w-full px-3 py-2 border-none bg-transparent text-sm text-gray-900 placeholder-gray-400 !ring-transparent focus:outline-none 2xl:text-base"
+                        id="search"
+                        placeholder="e-mail@email.fr"
+                        type="search"
+                      />
+                    </div>
+                    <div>
+                      <a
+                        class="flex items-center gap-2 py-2 px-5 shadow-lg bg-white hover:bg-gray-100 active:bg-gray-200 rounded-md border-2 border-le-bleu text-le-bleu text-sm font-bold tracking-[0.085em] uppercase"
+                        data-sveltekit-reload
+                        href={`/auth/login?redirect=${encodeURIComponent(
+                          $page.url.toString(),
+                        )}`}
+                        title="Envoyer votre réforme budgétaire avec cet e-mail"
+                      >
+                        Demander&nbsp;le&nbsp;calcul <iconify-icon
+                          class="ml-2 align-[-0.25rem] text-xl"
+                          icon="ri-send-plane-2-line"
+                        />
+                      </a>
+                    </div>
+                  </div>
                 </div>
-              </div>
+              {/if}
               {#if cachedSimulations !== undefined && cachedSimulations.length > 0}
-                <div class="mt-10 flex flex-col items-center">
+                <div class="flex flex-col items-center">
                   <h2 class="w-full text-left text-xl font-bold">
                     Consultez la liste des simulations budgétaires déjà
                     disponibles&nbsp;:
@@ -196,13 +247,13 @@
                     {/each}
                   </table>
                   <a
-                    class="mt-2 flex items-center gap-2 py-2 px-5 hover:bg-gray-200 active:bg-gray-300 rounded-md underline text-gray-600 text-sm font-bold tracking-[0.085em] uppercase"
+                    class="mt-2 flex items-center gap-3 py-2 px-5 hover:bg-gray-200 active:bg-gray-300 rounded-md underline text-gray-600 text-sm font-bold tracking-[0.085em] uppercase"
                     data-sveltekit-reload
                     href="/"
                     title="Voir toutes les simulations"
                   >
                     Voir toutes les simulations<iconify-icon
-                      class="ml-2 align-[-0.25rem] text-xl"
+                      class="align-[-0.25rem] text-xl"
                       icon="ri-arrow-right-line"
                     />
                   </a>
diff --git a/src/lib/components/BudgetSimulationSharingModal.svelte b/src/lib/components/BudgetSimulationSharingModal.svelte
index c247fa95c..e05e4b099 100644
--- a/src/lib/components/BudgetSimulationSharingModal.svelte
+++ b/src/lib/components/BudgetSimulationSharingModal.svelte
@@ -11,8 +11,8 @@
 
   import { page } from "$app/stores"
   import type { BudgetSimulation } from "$lib/budgets"
-  import type { DisplayMode } from "$lib/displays"
   import CopyClipboard from "$lib/components/CopyClipboard.svelte"
+  import type { DisplayMode } from "$lib/displays"
   import type { ParametricReform } from "$lib/reforms"
   import { budgetEditableParametersName } from "$lib/variables"
 
@@ -68,15 +68,14 @@
         body: JSON.stringify({
           budgetSimulation: $budgetSimulation?.result,
           displayMode,
-          parametricReform: Object.entries($parametricReform).reduce(
-            (filtered, [parameterName, value]) => {
+          parametricReform: Object.entries($parametricReform)
+            .sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
+            .reduce((filtered: ParametricReform, [parameterName, value]) => {
               if (budgetEditableParametersName.has(parameterName)) {
                 filtered[parameterName] = value
               }
               return filtered
-            },
-            {},
-          ),
+            }, {}),
         }),
         headers: {
           Accept: "application/json",
diff --git a/src/lib/hash.ts b/src/lib/hash.ts
new file mode 100644
index 000000000..a0f845b8e
--- /dev/null
+++ b/src/lib/hash.ts
@@ -0,0 +1,17 @@
+import { XXH64 } from "xxh3-ts"
+
+// Sometimes, the magnitude of the BigInt value generated by XXH64()
+// is not large enough to require 16 characters when represented in base 16.
+// As a result, the base 16 representation has leading zeros that are not necessary,
+// and JavaScript omits them when converting the BigInt to a string. To fix this,
+// we add zeros in the beginning until we reach a length of 16.
+
+export function hashString(str: string): string {
+  return XXH64(Buffer.from(str)).toString(16).padStart(16, "0")
+}
+
+export function hashObject(obj: object): string {
+  return XXH64(Buffer.from(JSON.stringify(obj)))
+    .toString(16)
+    .padStart(16, "0")
+}
diff --git a/src/lib/simulations.ts b/src/lib/simulations.ts
index 1722aca0b..e5951ad36 100644
--- a/src/lib/simulations.ts
+++ b/src/lib/simulations.ts
@@ -2,6 +2,7 @@ export interface CachedSimulation {
   date: string
   hash: string
   parameters: string[]
+  parametersHash: string
   title: string
 }
 
diff --git a/src/routes/simulations/+server.ts b/src/routes/simulations/+server.ts
index 7faefcd57..3788808c2 100644
--- a/src/routes/simulations/+server.ts
+++ b/src/routes/simulations/+server.ts
@@ -8,8 +8,7 @@ import { error, json } from "@sveltejs/kit"
 import fs from "fs-extra"
 import path from "path"
 
-import { XXH64 } from "xxh3-ts"
-
+import { hashString } from "$lib/hash"
 import config from "$lib/server/config"
 
 import type { RequestHandler } from "./$types"
@@ -83,12 +82,7 @@ export const POST: RequestHandler = async ({ request, url }) => {
   }
   const bodyJson = JSON.stringify(body, null, 2)
 
-  // Sometimes, the magnitude of the BigInt value generated by XXH64()
-  // is not large enough to require 16 characters when represented in base 16.
-  // As a result, the base 16 representation has leading zeros that are not necessary,
-  // and JavaScript omits them when converting the BigInt to a string. To fix this,
-  // we add zeros in the beginning until we reach a length of 16.
-  const digest = XXH64(Buffer.from(bodyJson)).toString(16).padStart(16, "0")
+  const digest = hashString(bodyJson)
 
   const simulationDir = path.join(simulationsDir, digest.substring(0, 2))
   const simulationFilePath = path.join(simulationDir, `${digest}.json`)
diff --git a/src/routes/simulations_budget/+server.ts b/src/routes/simulations_budget/+server.ts
index 78e675b01..003a1d657 100644
--- a/src/routes/simulations_budget/+server.ts
+++ b/src/routes/simulations_budget/+server.ts
@@ -2,7 +2,6 @@ import { auditRequire, cleanAudit, type Audit } from "@auditors/core"
 import { error, json } from "@sveltejs/kit"
 import fs from "fs-extra"
 import path from "path"
-import { XXH64 } from "xxh3-ts"
 
 import { getParameter, rootParameter } from "$lib/parameters"
 import type { ParametricReform } from "$lib/reforms"
@@ -10,6 +9,7 @@ import config from "$lib/server/config"
 import type { CachedSimulation } from "$lib/simulations"
 
 import type { RequestHandler } from "./$types"
+import { hashObject, hashString } from "$lib/hash"
 
 const { simulationsBudgetDir } = config
 
@@ -81,12 +81,11 @@ export const POST: RequestHandler = async ({ request, url }) => {
 
   const bodyJson = JSON.stringify(body, null, 2)
 
-  // Sometimes, the magnitude of the BigInt value generated by XXH64()
-  // is not large enough to require 16 characters when represented in base 16.
-  // As a result, the base 16 representation has leading zeros that are not necessary,
-  // and JavaScript omits them when converting the BigInt to a string. To fix this,
-  // we add zeros in the beginning until we reach a length of 16.
-  const digest = XXH64(Buffer.from(bodyJson)).toString(16).padStart(16, "0")
+  const digest = hashString(bodyJson)
+
+  const parametricReformDigest = hashObject(
+    (body as { parametricReform: ParametricReform }).parametricReform,
+  )
 
   const simulationDir = path.join(simulationsBudgetDir, digest.substring(0, 2))
   const simulationFilePath = path.join(simulationDir, `${digest}.json`)
@@ -105,11 +104,12 @@ export const POST: RequestHandler = async ({ request, url }) => {
       date: new Intl.DateTimeFormat("fr-FR").format(new Date()),
       hash: digest,
       parameters: modifiedParametersNames,
+      parametersHash: parametricReformDigest,
       title: modifiedParametersTitles.join(" | "),
     } as CachedSimulation,
   ].filter(
     (value, index, self) =>
-      index === self.findIndex((el) => el.hash === value.hash),
+      index === self.findLastIndex((el) => el.hash === value.hash),
   )
   await fs.writeFile(indexDir, JSON.stringify(contents))
 
-- 
GitLab