Aller au contenu

Lot 1, séquence A7 — Contrats

1er octobre 2026. Mise en œuvre de la séquence A7 du plan du Lot 1. Décisions : EC-028, EC-030, EC-051, EC-052, EC-070 ; écart ouvert : EC-073.

  • Un contrat porte une contrepartie, une campagne, une période et des lignes : un produit, une quantité en grammes, un prix par unité de prix, une tolérance, une formule de réfaction. Des primes s’y ajoutent.
  • Aucun montant sans devise. La devise d’une prime est celle de sa ligne, garantie par une clé composée.
  • Un contrat est modifiable en brouillon, puis figé à l’activation : un changement de prix est un nouveau contrat.
  • La formule de réfaction vit sur la ligne et se lit à la réception : calculerPeseeSurContrat est le premier appelant de pesee.calculerPesee.
  • EC-070 corrigé : le taux de change de la base est publié en unités majeures, et le domaine l’adapte aux unités mineures.
  • Aucune route REST en A7 : le contrat est gelé en A9, les écrans B3 demanderont leurs routes.

app.taux_change stocke EUR → XOF à 655957/1000 : le taux publié, 1 EUR = 655,957 XOF, en unités majeures. Les montants du domaine sont en unités mineures (centimes d’euro, francs CFA). Il manquait le facteur 10^(décimales de la devise cible − décimales de la devise source) : lu tel quel, le taux aurait multiplié par 100 un montant EUR → XOF.

La base garde le taux publié (c’est la parité officielle, auditable). Le domaine l’adapte :

Élément Rôle
app.devise Devises connues et leurs décimales (XOF 0, EUR 2, USD 2). Globale, en lecture seule pour l’API. Cible des clés étrangères de montants
devises.tauxUnitesMineures EUR → XOF donne 655957/100000, XOF → EUR donne 100000/655957, comme VT-ENT-015
devises.choisirTaux Le taux publié le plus récent au plus tard à la date, dans le sens demandé, sinon l’inverse de l’autre sens ; un taux futur n’est pas appliqué
taux.service.ts Lit la base, adapte, rend un TauxChange prêt pour convertirMontant ; convertirALaDate convertit un montant

L’éligibilité au groupe (A4) résout maintenant son taux elle-même quand le chiffre d’affaires d’un producteur n’est pas dans la devise du seuil : un test passe par le taux réel de la base.

Table Contenu
app.contrat Code unique, type (achat, vente, prestation), une contrepartie exactement (producteur, groupement, fournisseur ou client) cohérente avec le type, campagne, période, statut, conditions de paiement, pièce jointe
app.contrat_ligne Produit (un par contrat), quantite_engagee_g, prix_unite_mineure par unite_prix, devise, tolerance_cp, formule_refaction en JSON
app.prime Type fermé (bio, equitable, qualite), mode montant (par unité de prix, devise de la ligne) ou taux (centièmes de point du prix de base) ; un type par ligne

Cohérence contrepartie et type : un achat va à un producteur, un groupement ou un fournisseur ; une vente à un client ; une prestation à un client ou à un fournisseur (EC-052). Le prix a une unité : 150 XOF par kilo ; le stock reste en grammes (EC-030).

Cycle de vie : brouillon → actif → solde ou resilie, sans retour ; un brouillon peut être abandonné (resilie). L’activation exige au moins une ligne et une campagne non clôturée. Hors brouillon, seul le statut change, y compris pour le rôle des migrations.

Quatre formes, validées par FormuleRefaction (Ajv) : aucune, excedent_lineaire, lineaire_simple, bilan_de_masse, avec leurs normes en centièmes de point (0 à 10 000, et sous 10 000 pour une humidité de norme). Le mode d’arrondi fait partie de la formule : à la saisie il est facultatif et le service injecte plus_proche (valeur de départ d’EC-051, à confirmer sur un premier contrat type) ; une formule stockée sans arrondi est signalée (formule_invalide), jamais devinée.

versFormuleDomaine rapproche le type des schémas de celui du domaine par un switch exhaustif : si l’un gagne une formule que l’autre ignore, la compilation échoue.

Exemple chiffré (1 000 kg nets, humidité 17 %, impuretés 3 %, normes d’exemple 15 % et 2 %) : excédent linéaire 30 000 g de réfaction, linéaire simple 49 400 g, bilan de masse 52 824 g.

enregistrerContrat (forme validée avant toute écriture, formule complétée, devise de prime reprise de la ligne), activerContrat (sur un refus, la transaction est à annuler), calculerPeseeSurContrat, valeurEngagee (valeur au prix de base par devise, un arrondi par ligne, hors primes).

contrats.json : 2 contrats d’achat chargés en brouillon avec leurs lignes et leurs primes, puis activés (150 et 145 XOF le kilo ; le premier sans réfaction, fruit frais, le second avec une formule d’exemple non calibrée). Les prix d’origine (15 000 et 14 500 sans unité) valaient plus de 20 EUR le kilo et ont été corrigés. Le chargement est rejouable : les lignes étant figées une fois le contrat actif, il n’insère que ce qui manque.

Test Ce qu’il vérifie
VT-DEV-001 à 013, VT-CONT-001 à 008 Taux d’unités mineures dans les deux sens, choix du taux daté, valeur d’une ligne à un seul arrondi
db/tests/l1-a7.sql Devises en lecture seule, contrepartie et type, devise et unité connues, formule en objet typé, primes sans montant sans devise, cycle de vie, contrat figé, isolation
apps/api/test/contrats.test.ts Formules invalides refusées avant écriture, arrondi par défaut, calculerPesee sur les quatre formules, devise de prime, contrat figé, valeur engagée, conversion par le taux réel de la base, éligibilité sur ce taux
packages/schemas/src/contrat.test.ts Forme des formules, des lignes et des primes

Solde engagé − livré et alerte de tolérance, vente sans contrat « spot », référentiels exigés d’une vente (Lot 2) ; formule de réfaction par défaut du produit (EC-073) ; suppression d’une ligne d’un brouillon (le rôle de l’API n’a aucun droit de suppression : on abandonne le brouillon et on en crée un autre) ; filtrage des contrats sur le téléphone, qui n’ont pas toujours de groupement (client, fournisseur) : à régler avec les règles PowerSync (A9) ; routes REST (écrans B3).