Aller au contenu

Protocole de synchronisation

Version 1, 29 septembre 2026. Livrable n° 1 des travaux de conception du Lot 0. S’appuie sur le découpage (décisions « Synchronisation », « Session hors-ligne », « Commandes hors-ligne », « Durée maximale hors-ligne », « Révocation ») et sur EC-009, EC-012, EC-013, EC-022, EC-029, EC-031, EC-032, EC-033 et GPS-01 à GPS-03.

  • Le téléphone n’écrit jamais dans les tables métier. Il enregistre des commandes (« enregistrer une collecte ») dans une file locale.
  • Le serveur traite les commandes dans l’ordre de chaque appareil, une par une. Il revalide tout avec les mêmes règles (packages/domain), attribue les numéros officiels et écrit les données métier.
  • Chaque commande reçoit une issue : acceptée, acceptée en contrôle ou rejetée. Un problème métier ne produit jamais de rejet : il produit une entrée dans la file de contrôle. Le rejet est réservé aux anomalies de sécurité.
  • Les données descendent vers le téléphone par PowerSync, filtrées par organisation et par périmètre de l’agent.
  • Tant qu’une commande n’est pas traitée, le téléphone affiche ce qu’il a saisi comme provisoire.
Principe Conséquence
Le serveur est la seule autorité Numéros officiels, capacités, statuts de qualité et validations ne sont décidés que par le serveur.
Des commandes, pas des modifications de lignes Le téléphone exprime une intention métier complète et datée. Le serveur la traduit en écritures.
Idempotence Une commande rejouée n’est enregistrée qu’une fois (UUID de commande, EC-009).
Ordre par appareil Les commandes d’un appareil sont traitées dans l’ordre de leur numéro de séquence local, sans trou.
Un fait physique n’est jamais rejeté sans trace Tout problème métier mène à « acceptée en contrôle ». Seul un humain peut écarter une commande, avec un motif.
Même calcul des deux côtés Le téléphone et le serveur exécutent les mêmes fonctions de packages/domain. En cas d’écart, le serveur fait foi et l’écart est tracé.
Droits à la date de l’opération Le serveur vérifie les droits de l’agent et l’état de l’appareil à la date où l’opération a eu lieu, pas à la date de réception.
TÉLÉPHONE (apps/terrain, Capacitor) SERVEUR (Kubernetes, région UE)
Écran ──▶ packages/domain (contrôles locaux)
│
▼
commande_locale + commande (file d'envoi)
│ │
│ │ POST /v1/sync/commandes
│ └───────────────────────────▶ API NestJS
│ ├─ validation (Ajv, packages/domain)
│ ├─ écritures métier, numéros, mouvements
│ ├─ journal d'audit, file de contrôle
│ └─ tâches pg-boss (SMS, documents)
│ │
│ ▼
│ PostgreSQL + PostGIS
│ │ réplication logique
▼ ▼
Tables synchronisées ◀──────── flux de lecture ──────── Service PowerSync
(lecture seule) (règles de synchronisation)

Deux canaux distincts :

  • Écriture : commandes envoyées à l’API.
  • Lecture : sous-ensemble de la base, poussé par PowerSync.

Les photos, pièces d’identité, signatures et traces GPS brutes passent par un troisième canal, le stockage objet (section 10). Elles ne transitent pas par PowerSync (EC-029).

Chaque commande est un document JSON à enveloppe fixe.

Champ Type Rôle
id UUID v7 Généré sur le téléphone. Clé d’idempotence.
type texte Type de commande, ex. collecte.enregistrer (section 4).
version_schema entier Version du JSON Schema du payload.
organisation_id UUID Organisation de l’agent.
appareil_id UUID Appareil enrôlé.
utilisateur_id UUID Agent auteur.
sequence entier Séquence locale de l’appareil. Commence à 1, ne revient jamais à zéro.
cree_le_appareil horodatage Heure du téléphone au moment de la saisie.
horloge objet ou nul ref_serveur (dernière heure serveur connue) et ms_depuis_ref (temps écoulé mesuré par l’horloge monotone). Nul si le téléphone a redémarré depuis.
reference_provisoire texte ou nul BR-{code appareil}-{AAAAMMJJ}-{NNNN} pour les commandes qui produisent un document de terrain (EC-009).
versions objet Versions utilisées par le téléphone : pack, paramètres, application.
pieces_jointes liste fichier_id, sha256, role (signature, photo, piece_identite, trace_gps…).
payload objet Contenu propre au type, validé par son JSON Schema.
empreinte texte SHA-256 du JSON canonique (RFC 8785) de tous les champs sauf empreinte. Reprise dans le QR code du document de terrain.

Exemple :

{
"id": "0192f3a4-7c1e-7b2a-9d41-6f0c2e8a1b37",
"type": "collecte.enregistrer",
"version_schema": 1,
"organisation_id": "0192a0d1-…",
"appareil_id": "0192b8f0-…",
"utilisateur_id": "0192b8e2-…",
"sequence": 42,
"cree_le_appareil": "2026-10-14T09:12:31.412Z",
"horloge": { "ref_serveur": "2026-10-13T17:40:02.000Z", "ms_depuis_ref": 55949412 },
"reference_provisoire": "BR-K7Q2-20261014-0042",
"versions": { "pack": "[email protected]", "parametres": 3, "application": "0.4.1" },
"pieces_jointes": [
{ "fichier_id": "0192f3a4-8a02-…", "sha256": "9f2c…", "role": "signature" }
],
"payload": {
"collecte_id": "0192f3a4-7c1e-7000-…",
"campagne_id": "…",
"producteur_id": "…",
"parcelle_id": "…",
"produit_id": "…",
"contrat_id": "…",
"pesees": [{ "brut_g": 105000, "tare_g": 5000 }],
"humidite_cp": 1400,
"impuretes_cp": 150,
"calcul_telephone": { "net_g": 100000, "refaction_g": 2000, "net_marchand_g": 98000 },
"qualite_declaree": { "UE": "bio" }
},
"empreinte": "3b8e…"
}

Deux identifiants distincts. L’id identifie la commande. Les entités créées (ici collecte_id) ont leur propre UUID v7, lui aussi généré sur le téléphone. Le serveur l’utilise comme clé primaire. C’est ce qui permet à une commande suivante de référencer une entité créée hors-ligne, et au téléphone de faire le lien entre sa saisie provisoire et la donnée synchronisée.

Type Lot Contenu principal Contrôles serveur propres Motifs de contrôle possibles
producteur.enregistrer 3 Identité, groupement, pièce d’identité, photo Doublon de pièce d’identité doublon_identite
producteur.modifier 3 Champs modifiés uniquement, version_base Modification concurrente modification_concurrente
parcelle.enregistrer 3 Parcelle, surface déclarée, premier relevé Validité, chevauchement polygone_invalide, chevauchement_parcelle
parcelle.relever 3 Nouvelle version de polygone, métadonnées GPS-03, trace brute Validité, chevauchement, recalcul depuis la trace polygone_invalide, chevauchement_parcelle, ecart_calcul
collecte.enregistrer 3 Pesées, humidité, impuretés, qualité, signature, calcul du téléphone Capacité, contrat, certificat, recalcul capacite_depassee, reference_inactive, ecart_calcul
collecte.signaler_erreur 3 Collecte visée, motif, correction proposée – erreur_signalee_agent (toujours)
recu.reimprimer 3 Document visé, nombre d’exemplaires – – (trace du duplicata)
livraison.etablir_bon 3 Collectes regroupées, véhicule, destination Collectes existantes et non expédiées reference_inconnue
intrant.remettre 4 Producteur, intrant, quantité, valeur, avance Intrant autorisé en bio intrant_non_autorise
inspection.enregistrer 4 Fiche, version du formulaire, réponses, photos Conflit d’intérêts (AV1-07) conflit_interets
recolte.estimer 4 Parcelle, date prévue, quantité estimée Parcelle active reference_inactive
non_conformite.constater 4 Membre ou parcelle, constat, gravité, photos – –

Chaque type a son JSON Schema dans packages/schemas/commandes/{type}.v{n}.json. Le serveur accepte la version courante et la précédente (N et N-1).

Toute commande peut aussi recevoir les motifs communs de la section 6.3.

Elles n’existent que dans le back-office, en ligne : valider un lot, transformer, vendre ou expédier, valider une capacité, payer, dévalider, annuler, lever un contrôle, modifier un paramètre ou un pack, enrôler ou révoquer un appareil.

L’application terrain n’expose aucun écran pour ces opérations.

Table Nature PowerSync Contenu
Tables synchronisées (producteur, parcelle_version, solde_capacite…) Synchronisées Données descendues du serveur. Lecture seule : aucune écriture applicative.
commande Insertion seule File d’envoi. Chaque ligne insérée part dans la file de téléversement PowerSync.
commande_locale Locale uniquement Copie complète de chaque commande, avec son état local (en_attente, traitee). Sert à l’affichage provisoire, à la réimpression et au diagnostic.
commande_resultat Synchronisée Issue de chaque commande de cet appareil, descendue par PowerSync.

Les fichiers joints sont stockés dans le système de fichiers de l’application (Capacitor Filesystem), avec leur empreinte, jusqu’à leur envoi.

Dans une seule transaction locale :

  1. incrémenter le compteur de séquence de l’appareil ;
  2. construire l’enveloppe et calculer son empreinte (packages/domain) ;
  3. insérer dans commande_locale et dans commande.

Le document de terrain (reçu, bon, fiche) est généré après cette transaction, à partir de l’enveloppe enregistrée. Un reçu ne peut donc jamais exister sans sa commande.

  • Le connecteur PowerSync (uploadData) lit la file d’envoi dans l’ordre et envoie les commandes par lots de 50 au plus à POST /v1/sync/commandes.
  • Réponse 200 : la transaction PowerSync est marquée terminée, même si certaines commandes sont en contrôle.
  • Erreur réseau, 5xx ou 429 : nouvel essai avec délai croissant. La file est conservée telle quelle.
  • 401 : renouvellement du jeton avec le jeton hors-ligne, puis nouvel essai.
  • Arrêt pour trou de séquence (section 6.5) : alerte technique remontée au support. Ce cas signale un défaut de l’application.
  • Les fichiers joints sont envoyés en parallèle, indépendamment des commandes (section 10).

Pour chaque entité saisie sur le terrain, une vue locale combine :

  • les lignes synchronisées ;
  • les commandes de commande_locale sans résultat, projetées sous la même forme.

La fusion se fait sur l’identifiant de l’entité (collecte_id, producteur_id…). Quand la donnée synchronisée arrive, elle remplace la ligne provisoire.

Rendu : une ligne provisoire porte sa référence grise précédée de « prov. » (design system, composant « Référence »). Une ligne dont la commande est en contrôle affiche « En vérification », sans le détail du motif.

Le téléphone exécute les mêmes règles que le serveur, avec les données dont il dispose. Leur résultat est indicatif.

Situation Comportement du téléphone
Capacité restante insuffisante Message « Capacité dépassée de 120 kg. La collecte sera vérifiée par le RSCI. » L’agent peut poursuivre.
Polygone invalide Enregistrement refusé, avec proposition de reprendre le relevé. L’agent peut forcer l’enregistrement « pour contrôle ».
Producteur possiblement en doublon Avertissement avec la fiche existante. L’agent choisit : utiliser la fiche existante ou créer quand même.
Durée hors-ligne supérieure à 7 jours Plus aucune nouvelle saisie. La file existante est conservée.
Données non synchronisées depuis plus de 24 h Pastille d’état de synchronisation en terre.

Une ligne de commande_locale est purgée quand les trois conditions sont réunies :

  • son résultat est descendu ;
  • l’entité correspondante est synchronisée, ou la commande est en contrôle ;
  • 7 jours se sont écoulés depuis la réception du résultat (paramètre).

Les fichiers joints sont purgés dès que le serveur confirme leur réception avec la bonne empreinte.

Point d’entrée Usage
POST /v1/sync/commandes Envoi d’un lot de commandes
GET /v1/sync/etat-appareil Dernière séquence reçue, statut de l’appareil, versions minimales, heure serveur
POST /v1/sync/jeton Émission du jeton PowerSync (section 12)
POST /v1/fichiers/url-envoi Adresse d’envoi signée pour un fichier joint
POST /v1/appareils/enroler Enrôlement, en ligne uniquement

Le lot est traité séquentiellement, une transaction par commande. Une commande validée reste acquise même si la suivante échoue.

  1. Authentification de l’appel : jeton Keycloak valide, audience biotrace-api.
  2. Verrou de l’appareil : SELECT … FOR UPDATE sur la ligne appareil. Deux envois simultanés du même appareil sont ainsi sérialisés.
  3. Identité : organisation, utilisateur et appareil de l’enveloppe doivent correspondre au jeton et à l’enrôlement.
  4. Idempotence : si l’id est connu avec la même empreinte, le résultat existant est renvoyé, sans nouveau traitement.
  5. Ordre : sequence doit valoir derniere_sequence + 1.
  6. Empreinte recalculée et comparée.
  7. Forme : validation Ajv du payload selon type et version_schema.
  8. Date d’opération (section 6.4).
  9. Droits à la date d’opération : utilisateur actif, permission correspondant au type, appareil non révoqué, délai hors-ligne respecté.
  10. Références : chaque entité référencée existe, est active et n’est pas elle-même en contrôle.
  11. Rejeu des calculs avec packages/domain et la version de pack figée par la campagne. Comparaison avec calcul_telephone.
  12. Règles métier propres au type (section 4.1).
  13. Écriture, dans la transaction ouverte :
    • SET LOCAL app.organisation_id, app.utilisateur_id, app.appareil_id, app.commande_id, pour le journal d’audit ;
    • enregistrement de la commande brute ;
    • écritures métier, numéro officiel, mouvements de matière ;
    • entrées de la file de contrôle ;
    • résultat (commande_resultat) ;
    • tâches pg-boss (SMS, génération de documents) ;
    • appareil.derniere_sequence et appareil.derniere_synchro.

Les contrôles de capacité verrouillent la ligne de solde concernée (FOR UPDATE). Deux agents qui collectent le même producteur au même moment sont donc traités l’un après l’autre.

Issues

Issue Écritures métier Cas
acceptee Oui Tous les contrôles passent
acceptee_controle Oui, entité marquée « en anomalie » Au moins un motif de contrôle
acceptee_controle sans écriture Non : seuls la commande et le contrôle sont enregistrés Données illisibles, version obsolète, empreinte incohérente
rejetee Non : seule la commande est enregistrée, avec une alerte de sécurité Anomalie de sécurité (tableau ci-dessous)

Motifs communs de contrôle

Motif Déclencheur
donnees_illisibles payload non conforme à son schéma
version_client_obsolete version_schema plus ancienne que N-1
empreinte_incoherente Empreinte recalculée différente de celle reçue
apres_revocation Date d’opération postérieure à la révocation de l’appareil
hors_ligne_depasse Date d’opération au-delà de 7 jours après la dernière synchronisation
droit_manquant Permission absente à la date d’opération
reference_inconnue Entité référencée introuvable
reference_inactive Entité référencée retirée, expirée ou close (contrat, campagne, certificat)
dependance_en_controle Entité référencée elle-même en contrôle
ecart_calcul Calcul du serveur différent de celui du téléphone
parametre_introuvable Aucune règle ne donne de valeur pour le cas traité (PC-07). L’opération est enregistrée, sans blocage
horloge_invraisemblable Date d’opération impossible à établir (section 6.4)
piece_manquante Fichier joint non reçu dans le délai
module_inactif Module de la commande inactif à la date d’opération (EC-096). Commande conservée, sans écriture métier

Rejets (sécurité uniquement)

Code Déclencheur
identite_incoherente Organisation, utilisateur ou appareil différents du jeton ou de l’enrôlement
identifiant_reutilise id connu avec une autre empreinte
sequence_reutilisee Séquence déjà utilisée par une autre commande

Alertes. Le résultat peut aussi porter des alertes non bloquantes, sans contrôle : horloge_corrigee, rendement_non_calibre, version_pack_differente.

  1. Si horloge est renseignée : estimation = ref_serveur + ms_depuis_ref.
  2. Si l’écart entre cree_le_appareil et estimation est inférieur ou égal à 10 minutes (paramètre), la date d’opération est cree_le_appareil.
  3. Sinon, la date d’opération est estimation, avec l’alerte horloge_corrigee.
  4. Si horloge est nulle (redémarrage), cree_le_appareil est retenue si elle est comprise entre la dernière synchronisation de l’appareil et l’heure de réception. Sinon : motif horloge_invraisemblable, et la date d’opération provisoire est l’heure de réception.
  5. La date d’opération n’est jamais postérieure à l’heure de réception.

Le serveur conserve toujours les trois valeurs : heure du téléphone, date d’opération retenue, heure de réception.

{
"resultats": [
{
"commande_id": "0192f3a4-7c1e-7b2a-9d41-6f0c2e8a1b37",
"issue": "acceptee_controle",
"motifs": ["capacite_depassee"],
"alertes": [],
"numero_officiel": "26A/COL/ANA/0137",
"entites": { "collecte": "0192f3a4-7c1e-7000-…" },
"date_operation": "2026-10-14T09:12:31.412Z"
}
],
"derniere_sequence": 42,
"heure_serveur": "2026-10-16T08:03:11.020Z",
"arret": null
}

Si une commande a une séquence trop grande, le traitement s’arrête avant elle : "arret": { "raison": "sequence_attendue", "attendue": 40 }. Les commandes précédentes du lot restent traitées.

Le résultat est aussi écrit dans commande_resultat, synchronisé vers l’appareil. Si la réponse HTTP est perdue, le téléphone apprend quand même l’issue.

Mise en œuvre (Lot 1, A9, 01/10/2026). Les règles définitives sont dans infra/powersync/sync-config.yaml (format bucket_definitions, EC-076), avec cinq compartiments : globales, organisation, contrats_actifs (un par contrat d’achat actif), perimetre_agent et appareil. L’exemple ci-dessous reste indicatif. Voir Lot 1, séquence A9 pour les colonnes descendues, les colonnes exclues, les colonnes générées que PostgreSQL 16 ne réplique pas (EC-077) et le volume mesuré.

Le jeton PowerSync est émis par l’API (section 12). Il porte organisation_id, utilisateur_id et appareil_id. Toutes les règles filtrent sur ces valeurs.

Compartiment Paramètre Contenu
packs – Versions de pack publiées utilisées par au moins une campagne ouverte
organisation organisation_id Paramètres, campagnes ouvertes, produits, unités, contrats d’achat actifs, formulaires en vigueur, taux de change
perimetre_agent groupements affectés à l’agent Producteurs, parcelles et version de polygone en vigueur, statuts de conformité, certificats (résumé), capacités validées et soldes
collectes_agent utilisateur_id Collectes et bons de livraison de l’agent, tant qu’ils sont marqués synchronisables
inspection (Lot 4) utilisateur_id Plan d’inspection de l’inspecteur, historique des non-conformités ouvertes des membres concernés
appareil appareil_id commande_resultat de l’appareil, statut de l’appareil

Exemple, en syntaxe indicative (à caler sur la version de PowerSync retenue) :

bucket_definitions:
organisation:
parameters: SELECT request.jwt() ->> 'organisation_id' AS organisation_id
data:
- SELECT * FROM parametre WHERE organisation_id = bucket.organisation_id
- SELECT * FROM campagne WHERE organisation_id = bucket.organisation_id AND ouverte = true
perimetre_agent:
parameters: >
SELECT groupement_id FROM affectation_agent
WHERE utilisateur_id = request.jwt() ->> 'utilisateur_id' AND active = true
data:
- SELECT * FROM producteur WHERE groupement_id = bucket.groupement_id
- SELECT * FROM parcelle_version_en_vigueur WHERE groupement_id = bucket.groupement_id
- SELECT * FROM solde_capacite WHERE groupement_id = bucket.groupement_id
appareil:
parameters: SELECT request.jwt() ->> 'appareil_id' AS appareil_id
data:
- SELECT * FROM commande_resultat WHERE appareil_id = bucket.appareil_id
  • Pas de jointure dans les règles de données. Chaque table descendue par périmètre porte directement groupement_id, en plus d’organisation_id (EC-032). Cette colonne est tenue à jour par la base.
  • Pas de filtre sur une date glissante. Une colonne booléenne synchronisable sur les collectes est remise à false par une tâche nocturne pg-boss après 30 jours (paramètre).
  • Pas de PostGIS sur le téléphone. Les polygones descendent dans une colonne texte GeoJSON, tenue par un déclencheur à partir de la colonne geometry.
  • Soldes précalculés. solde_capacite (capacité validée, quantité consommée, reste) est mis à jour dans la même transaction que chaque collecte, pour que le téléphone ait un solde à jour sans calcul.
  • Budget. Cible : moins de 20 Mo de données synchronisées par téléphone. À mesurer au prototype sur un périmètre réaliste.
Conflit Détection Traitement
Deux agents, même producteur, capacité dépassée Au traitement de la collecte qui franchit le plafond (ordre de traitement serveur) Les deux collectes sont acceptées. Celle qui franchit le plafond passe en contrôle capacite_depassee (EC-013). Le RSCI voit les deux.
Doublon de pièce d’identité Au traitement de producteur.enregistrer Fiche créée, contrôle doublon_identite. Ni capacité ni paiement tant que non résolu (EC-031).
Collecte sur un producteur en contrôle Étape 10 Collecte acceptée, contrôle dependance_en_controle.
Modification concurrente d’un producteur version_base antérieure à une modification d’un autre auteur sur les mêmes champs Les champs non concernés sont appliqués. Les champs en conflit gardent la valeur du serveur, la proposition de l’agent va en contrôle modification_concurrente.
Deux relevés du même polygone Deux parcelle.relever sur la même parcelle Chacun crée une version. La version en vigueur est celle dont la date d’opération est la plus récente. Si une capacité validée existe sur l’ancienne surface, elle repasse en validation (EC-012).
Chevauchement de parcelles ST_Intersects sur les versions en vigueur, au-delà d’une surface minimale (paramètre) Contrôle chevauchement_parcelle. Capacité non validable tant que non résolu.
Référentiel périmé sur le téléphone Version de pack ou de paramètres différente de celle de la campagne Recalcul serveur. Alerte version_pack_differente. Contrôle ecart_calcul si le résultat diffère.
Contrat ou campagne clos entre-temps Étape 10 Contrôle reference_inactive.
Appareil révoqué Étape 9 Commandes antérieures à la révocation traitées normalement. Commandes postérieures en contrôle apres_revocation.

Un mécanisme unique pour tous les motifs.

Motif Traité par Décisions possibles Effet tant que le contrôle est ouvert
capacite_depassee RSCI Régularisation, déclassement, non-conformité Lot en anomalie : ni transformation, ni vente sous le référentiel bio
doublon_identite RSCI ou administrateur Fusion, confirmation (personnes distinctes) Ni capacité ni paiement pour la nouvelle fiche
polygone_invalide, chevauchement_parcelle RSCI Correction (nouveau relevé), confirmation Capacité non validable (AV1-01)
modification_concurrente Administrateur Valeur retenue, champ par champ Valeur du serveur conservée
ecart_calcul RSCI Confirmation de la valeur serveur, correction Paiement de la collecte bloqué
apres_revocation, hors_ligne_depasse, droit_manquant, horloge_invraisemblable RSCI et administrateur Confirmation, écart manuel motivé Entité en anomalie
reference_inconnue, reference_inactive, dependance_en_controle RSCI Correction, confirmation Entité en anomalie
donnees_illisibles, version_client_obsolete, empreinte_incoherente, module_inactif Support et RSCI Reprise manuelle dans le back-office, liée à la commande Aucune entité créée
piece_manquante RSCI Pièce fournie, confirmation sans pièce Entité en anomalie
erreur_signalee_agent RSCI Correction (dévalidation, Lot 2), confirmation Paiement de la collecte bloqué

Règles communes :

  • l’auteur de la commande ne peut pas lever son propre contrôle (vérifié par la base) ;
  • toute décision exige un motif écrit ;
  • un contrôle levé ne se modifie plus ;
  • l’écart manuel d’une commande (équivalent d’un rejet) est une décision tracée comme les autres, jamais une suppression ;
  • le bandeau « À traiter » du back-office affiche les contrôles ouverts, triés par ancienneté et par effet bloquant.
  1. Sur le téléphone, le fichier est enregistré localement, son SHA-256 calculé, et un fichier_id (UUID v7) attribué.
  2. La commande référence le fichier par fichier_id et sha256.
  3. Quand le réseau le permet, l’application demande une adresse d’envoi signée (POST /v1/fichiers/url-envoi) et envoie le fichier au stockage objet.
  4. L’API vérifie l’empreinte à réception. Si elle diffère, le fichier est marqué empreinte_invalide et renvoyé.
  5. La commande et le fichier peuvent arriver dans n’importe quel ordre. Une commande arrivée avant son fichier est traitée normalement. Le fichier est « attendu ».
  6. Un fichier toujours attendu après 14 jours (paramètre) ouvre un contrôle piece_manquante.

La trace GPS brute (GPS-03) suit le même chemin, au format GeoJSON.

  • Numéro officiel. Attribué dans la transaction de traitement, y compris pour une commande acceptée en contrôle : la marchandise existe. Il est lié à la référence provisoire, qui reste consultable.
  • QR code du document de terrain. Il porte la référence provisoire, l’id de commande et les 16 premiers caractères de l’empreinte. La page de vérification (Lot 5) les compare aux données du serveur.
  • SMS au producteur. Une tâche pg-boss est inscrite dans la transaction d’une collecte acceptée ou acceptée en contrôle. Le SMS confirme la référence, la date et le poids net marchand. Il ne mentionne jamais un contrôle.

Le détail de la configuration est dans la page « Configuration Keycloak ». Ce qui concerne le protocole :

Enrôlement (en ligne, au bureau)

  1. Un administrateur génère dans le back-office un code d’enrôlement pour un agent, valable 24 heures.
  2. L’agent se connecte à l’application terrain (Keycloak, avec le jeton hors-ligne) et saisit le code.
  3. POST /v1/appareils/enroler vérifie le code, crée l’appareil, lui attribue un code court unique (EC-009) et renvoie la séquence de départ (1) et l’heure serveur.
  4. L’agent définit son code PIN local.

Jeton PowerSync

L’API émet le jeton PowerSync (durée 5 minutes) après avoir vérifié le jeton Keycloak et le statut de l’appareil. Le service PowerSync valide ce jeton avec la clé publique de l’API. Un appareil révoqué ne reçoit donc plus aucune donnée, dès la première tentative.

Révocation

  1. L’administrateur révoque l’appareil dans le back-office. L’API révoque la session hors-ligne dans Keycloak et note la date de révocation.
  2. À la reconnexion, l’application apprend la révocation par GET /v1/sync/etat-appareil.
  3. Elle envoie d’abord sa file de commandes, puis efface ses données locales. Les commandes postérieures à la révocation passent en contrôle apres_revocation, sans rejet (découpage).
Paramètre Valeur par défaut
Taille maximale d’un lot d’envoi 50 commandes
Écart d’horloge toléré 10 minutes
Durée maximale de saisie hors-ligne 7 jours
Alerte de données non synchronisées 24 heures
Rétention des commandes locales après résultat 7 jours
Délai avant piece_manquante 14 jours
Durée de synchronisation des collectes de l’agent 30 jours
Versions de schéma acceptées N et N-1
Validité d’un code d’enrôlement 24 heures
Surface minimale de chevauchement signalée 10 m²

Tous sont versionnés par organisation, sauf la taille de lot et les versions de schéma, qui sont techniques.

id Scénario Résultat attendu
SP-01 La même commande est envoyée deux fois Un seul enregistrement ; même résultat renvoyé
SP-02 La réponse HTTP est perdue après traitement Le téléphone reçoit le résultat par commande_resultat ; rien n’est dupliqué
SP-03 Dernière séquence reçue : 40. La 41 manque, la 42 arrive Arrêt avant 42, attendue: 41 ; aucune commande perdue
SP-04 Même id, contenu modifié Rejet identifiant_reutilise, alerte de sécurité
SP-05 Horloge du téléphone décalée de 3 jours, horloge renseignée Date d’opération corrigée, alerte horloge_corrigee
SP-06 Téléphone redémarré, horloge incohérente Contrôle horloge_invraisemblable
SP-07 Deux agents, même producteur, capacité dépassée Deux collectes acceptées, une en contrôle capacite_depassee
SP-08 Producteur créé hors-ligne, en doublon, puis collecté Fiche et collecte acceptées, deux contrôles, aucun paiement possible
SP-09 Modification concurrente d’un même champ Valeur serveur conservée, proposition en contrôle
SP-10 Fichier joint arrivé après la commande, puis jamais arrivé Traitement normal, puis piece_manquante au délai
SP-11 Appareil révoqué avec 12 commandes en file, dont 5 postérieures 7 acceptées, 5 en contrôle apres_revocation, données locales effacées après envoi
SP-12 Commande en version de schéma N-2 Contrôle version_client_obsolete, commande conservée
SP-13 Payload altéré en transit Contrôle empreinte_incoherente
SP-14 Trois jours hors-ligne : 300 collectes, 40 relevés, 60 photos Synchronisation complète sans perte ni doublon ; mesure de la durée et du volume
SP-15 L’auteur tente de lever son propre contrôle Refus par la base

Tests dans apps/api/test/ (pnpm --filter @biotrace/api test:integration), contre PostgreSQL et l’application Nest.

id État
SP-01 à SP-06 Exécutés
SP-07 à SP-10 Écrits, sautés : capacité (lots 1 et 2), types de commande du lot 3, stockage objet
SP-11 Exécuté côté API (7 acceptées, 5 en contrôle) ; l’effacement des données locales relève du téléphone
SP-12, SP-13 Exécutés
SP-14 Version réduite côté API : 300 commandes en 6 lots ; volume complet au lot 3
SP-15 Couvert en base : db/tests/sb-12.sql

Précisions issues de la mise en œuvre

  • Les rejets de sécurité s’écrivent dans sync.rejet (EC-043), pas dans sync.commande.
  • Un type de commande inconnu, ou une version de schéma plus récente que la courante : acceptee_controle sans écriture, motif donnees_illisibles (EC-044).
  • appareil.derniere_synchro s’inscrit à la fin du lot, pas à chaque commande : pendant un lot, « hors_ligne_depasse » et la date d’opération se jugent par rapport à la synchronisation précédente.
  • Une enveloppe illisible (forme de l’enveloppe, §3) fait refuser le lot entier en 400 : rien n’est traité.
  • Le jeton Keycloak 26.0.5 porte le nom de l’organisation ; l’API la retrouve par son code (EC-045).
  • La permission requise se lit dans la table type → permission de packages/domain (permissionRequise, EC-038 : identifiants anglais, collecte.enregistrer → collection.record). L’utilisateur doit être actif ; la permission se juge à la date d’opération.
  • Les paramètres du §13 sont des valeurs par défaut dans le code ; leur lecture par organisation viendra avec le module parametres (PC-01 à PC-08).
  • Non faits : POST /v1/fichiers/url-envoi et POST /v1/appareils/enroler.
  • Limite : une reference_provisoire déjà utilisée par une autre commande provoque une erreur 500 (contrainte d’unicité), non traitée.

SP-07 et SP-14 couvrent les critères de fin du Lot 3. SP-01 à SP-04 couvrent le critère du Lot 0 (« rejouée, validée ou rejetée sans perte ni doublon »).

Préparé, vérifié dans un navigateur, non exécuté sur téléphone. Test sur téléphone reporté à un lot ultérieur, hors critères de fin du Lot 0 (EC-041). Voir Prototype PowerSync sous Capacitor : mode d’emploi, résultats déjà obtenus et grille à remplir. Le point 2 exige un téléphone.

Ces points ne changent pas le protocole, mais peuvent changer sa mise en œuvre.

  1. Tables en insertion seule de PowerSync : confirmer leur comportement (persistance locale, ordre de la file) et la combinaison avec la table locale commande_locale.
  2. PowerSync sous Capacitor : SDK dédié avec SQLite natif, ou SDK web dans la WebView. Mesurer la tenue sur trois jours de données et après un redémarrage.
  3. Réplication des colonnes GeoJSON tenues par déclencheur.
  4. Jeton émis par l’API : validation par PowerSync via la clé publique (JWKS) de l’API.
  5. Budget de volume sur un téléphone d’entrée de gamme, avec un périmètre de 2 000 producteurs.
  • Découpage. La décision « Stockage PWA » devient caduque avec EC-033. Elle est remplacée par la base locale de l’application Capacitor et le stockage sécurisé Android.
  • ADR à rédiger dans apps/docs/src/content/docs/technique/adr/ :
    • modèle d’écriture par commandes et file de contrôle unique ;
    • jeton PowerSync émis par l’API ;
    • application terrain sous Capacitor (EC-033).
  • Hors périmètre v1, à réexaminer au Lot 7 : signature de chaque commande par une clé propre à l’appareil (stockée dans le Keystore Android), pour renforcer la preuve d’origine.