Aller au contenu

ScoroOdkBridge

Service d'arrière-plan (.NET 10 Worker Service, sans UI ni endpoint HTTP) qui fait le pont entre Scoro (gestion de projets) et ODK Central (collecte de données terrain) pour LCL Environnement.

À intervalle régulier, le service interroge l'API Scoro pour les projets récemment modifiés, filtre ceux qui concernent le contrôle des matériaux, et crée une entity correspondante dans un dataset ODK Central, pour que les équipes de terrain puissent remplir des formulaires ODK rattachés à ces projets. Le pont ne fait que créer des entities ; il ne les modifie ni ne les supprime jamais.

Ce dépôt fait partie de l'infrastructure LCL Environnement décrite dans l'Infrastructure (un seul droplet, Coolify, Caddy, lclchantiers.com). Ce service tourne en arrière-plan uniquement : il n'a pas de sous-domaine.

Comment ça marche

À chaque cycle (Worker.cs) :

  1. Charge le fichier de suivi local (ProjectTracker) des IDs Scoro déjà traités.
  2. Interroge Scoro (ScoroClient.GetProjectsModifiedAsync) pour les projets modifiés depuis LookbackMinutes minutes.
  3. Filtre les projets éligibles : nom de projet préfixé CM- ou CMC- (ProjectMapper.IsEligible). CML- est volontairement exclu pour l'instant.
  4. Pour chaque projet éligible pas encore dans le tracker, construit le payload d'entity (ProjectMapper.ToEntity) et le crée dans ODK Central (OdkClient.CreateEntityAsync).
  5. Marque le projet comme traité dans le tracker dès la création réussie.
  6. Une erreur ODK sur un projet est loguée et n'interrompt ni le cycle ni le projet suivant (OdkApiException catché par projet). Une exception inattendue interrompt le cycle en cours mais pas la boucle du worker : elle est loguée et le prochain cycle démarre normalement au tick suivant.

Le premier cycle tourne immédiatement au démarrage, puis à chaque tick d'un PeriodicTimer (intervalle = PollIntervalMinutes, 20 min par défaut).

Architecture

Trois dossiers indépendants, assemblés dans Program.cs :

Scoro/    client de l'API Scoro (lecture seule)
ODK/      client de l'API entities d'ODK Central (création seule)
Bridge/   la colle : filtre d'éligibilité, mapping, fichier de suivi

Scoro/

  • ScoroClient : POST vers projects/list (paginé, filter.modified_date) et projects/view/{id}. La clé d'API Scoro est envoyée comme champ du corps de la requête (apiKey), pas comme header (voir ScoroClient.BuildBody).
  • ScoroServiceCollectionExtensions : pipeline de résilience (Microsoft.Extensions.Http.Resilience / Polly) : retry sur 429/503 et erreurs de transport, jusqu'à 5 tentatives, backoff exponentiel avec jitter. Respecte le header x-ratelimit-reset de Scoro (ou Retry-After) quand présent au lieu du backoff par défaut. Timeout de 20 s par tentative.
  • ScoroProject : désérialise la forme projects/list/projects/view, y compris le tableau custom_fields, aplati dans une map insensible à la casse (CustomFieldsMap) exposée via des accesseurs typés : SiteAdress (c_adresse_site), City (c_ville), PrescribedMandate (c_mandat_prescrit), Sharepoint (c_sharepoint), Referencer (c_referencer). Un FlexibleStringConverter coerce les champs personnalisés numériques/booléens Scoro en string, et les valeurs texte sont HTML-décodées (ex. Ayer's CliffAyer's Cliff).
  • ScoroResponse<T> : enveloppe status/statusCode/messages/data commune aux réponses Scoro ; EnsureOk lève une ScoroApiException si status != OK.

ODK/

  • OdkClient : CreateEntityAsync (POST v1/projects/{ProjectId}/datasets/{DatasetName}/entities) et GetExistingScoroIdsAsync (lecture OData paginée $top/@odata.nextLink sur le dataset, utilisée pour reconstruire le tracker si besoin).
  • OdkSessionHandler : DelegatingHandler qui gère l'authentification par jeton de session : connexion email/mot de passe au premier appel, jeton mis en cache, rafraîchi 30 min avant expiration, un seul retry automatique sur 401 (jeton révoqué côté serveur).
  • OdkServiceCollectionExtensions : enregistre le HttpClient typé avec le handler de session. En développement uniquement (IHostEnvironment.IsDevelopment()), fait confiance au certificat auto-signé d'ODK local, jamais activé en production.

Bridge/

  • ProjectMapper.IsEligible : seule source de vérité pour savoir quels projets Scoro sont pontés.
  • ProjectMapper.ToEntity : construit le payload (label, data) de l'entity : scoro_id, project_no, company_name, address, city, prescribed_mandate, start_date, end_date. label = nom du projet Scoro (affiché dans le menu déroulant du formulaire ODK).
  • ProjectTracker : ensemble d'IDs Scoro déjà pontés, persisté en JSON (data/tracking.json par défaut, chemin configurable). Les écritures passent par un fichier temporaire puis un File.Move atomique pour survivre à un crash en cours d'écriture. SeedAsync (actuellement non appelé automatiquement) permet de reconstruire le tracker à partir des entities existantes dans ODK via OdkClient.GetExistingScoroIdsAsync.

Le fichier de suivi est critique

data/tracking.json doit vivre sur un volume persistant en production. S'il est perdu ou vidé, le pont va tenter de recréer en entity ODK tous les projets éligibles historiques au prochain cycle, créant des doublons. Ne jamais le supprimer ou le réinitialiser sans d'abord le reconstruire via GetExistingScoroIdsAsync / ProjectTracker.SeedAsync.

Prérequis

  • .NET SDK 10
  • Accès réseau à l'API Scoro (https://lcl.scoro.com/api/v2) et à l'instance ODK Central cible

Configuration

Liée via le pattern Options (ValidateDataAnnotations().ValidateOnStart()) sur trois sections : Scoro, Odk, Bridge.

Section Clé Requis Défaut Description
Scoro BaseUrl non https://lcl.scoro.com/api/v2 URL de base de l'API Scoro
Scoro ApiKey oui - Clé d'API Scoro
Scoro CompanyAccountId oui - Identifiant de compte Scoro
Scoro Lang non fre Langue des réponses Scoro
Scoro PageSize non 25 Taille de page pour projects/list
Odk BaseUrl oui - URL de base d'ODK Central
Odk Email oui - Email du compte de service ODK
Odk Password oui - Mot de passe du compte de service ODK
Odk ProjectId non 1 ID du projet ODK Central cible
Odk DatasetName non scoro_projects Nom du dataset ODK cible
Bridge TrackingFilePath oui data/tracking.json Chemin du fichier de suivi (volume persistant en prod)
Bridge LookbackMinutes non 60 Fenêtre de recherche des projets modifiés (chevauche l'intervalle de poll par sécurité)
Bridge PollIntervalMinutes non 20 Intervalle entre deux cycles

appsettings.json contient la structure avec des secrets vides. appsettings.Development.json contient des valeurs de dev locales (URL ODK https://localhost). En production, les secrets viennent des variables d'environnement Coolify, jamais codés en dur (règle du CLAUDE.md racine). Le mapping standard ASP.NET Core s'applique, ex. Scoro__ApiKey, Odk__Password.

Un UserSecretsId est déjà configuré dans le .csproj : pour du dev local sans toucher appsettings.Development.json, dotnet user-secrets set "Odk:Password" "..." fonctionne aussi.

Lancer le projet

dotnet build
dotnet run                        # démarre la boucle du worker (poll toutes les PollIntervalMinutes)
dotnet run -- --selftest          # test de parsing JSON hors-ligne, aucun appel réseau, puis quitte
dotnet run -- --scoro-test        # appelle la vraie API Scoro avec les creds configurées, affiche les projets correspondants, puis quitte

Il n'y a pas de projet de test dédié. Scoro/Tests/ScoroParsingSelfTest.cs est un self-test artisanal (assertions Check(label, bool) affichées en PASS/FAIL sur stdout) qui valide la désérialisation JSON de ScoroProject/ScoroResponse contre un exemple de payload projects/list. Lancer --selftest après toute modification sous Scoro/ qui touche au parsing (mapping des champs personnalisés, HTML-decoding, coercion numérique→string, etc.).

Docker

Build multi-étapes, correspond à la production :

docker build .

mcr.microsoft.com/dotnet/sdk:10.0 (build) → mcr.microsoft.com/dotnet/runtime:10.0 (exécution), point d'entrée dotnet ScoroOdkBridge.dll. En production (Coolify), monter un volume persistant sur le chemin de Bridge:TrackingFilePath et fournir toutes les variables d'environnement de secrets (voir la section Configuration).

Problème connu : secrets commités

appsettings.Development.json est suivi par git (seul .dockerignore l'exclut ; .gitignore ne le fait pas) et contient une vraie clé d'API Scoro et un vrai mot de passe du compte de service ODK Central, en clair. Ceci viole la règle « jamais de secret dans le code » du CLAUDE.md racine.

Ne pas aggraver en commitant d'autres vrais secrets dans des fichiers de config. Ceci doit être traité comme une remédiation à faire, pas comme un état normal :

  1. Retirer appsettings.Development.json du suivi git et l'ajouter à .gitignore.
  2. Purger le secret de l'historique git.
  3. Faire tourner (rotate) la clé Scoro et le mot de passe du compte de service ODK compromis.

Notes de collaboration

  • Ne jamais dupliquer la logique déjà couverte par le fichier de suivi : il empêche les doublons et vit sur un volume persistant à ne jamais effacer sans comprendre pourquoi.
  • Le pont ne traite que les projets CM-/CMC- ; CML- est délibérément exclu (voir ProjectMapper.IsEligible).
  • Le pont ne fait que créer des entities ODK, jamais modifier ni supprimer.
  • Se réveille toutes les PollIntervalMinutes (20 min par défaut).