Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Pricing

Contexte

Le pricing actuel des missions est calcule cote serveur dans packages/api/src/routers/missions/pricing.ts. Le calcul est volontairement simple et deterministe :

  • une configuration globale de mission fournit les montants communs a toutes les missions ;
  • chaque option d’analyse peut ajouter un forfait fixe et/ou un prix variable par hectare ;
  • un minimum de mission peut relever le total final si le sous-total est trop bas.

L’objectif est de conserver le serveur comme source de verite du prix et de produire un detail exploitable pour l’UI, l’audit et les tests.

Donnees d’entree

Le calcul repose sur deux objets :

1. MissionPricingInput

  • totalSurface Surface totale de la mission.
  • analysisOptions Liste des options d’analyse selectionnees.

Chaque option d’analyse contient :

  • id
  • code
  • name
  • basePrice
  • pricePerHa

2. MissionPricingConfig

La configuration globale de pricing contient :

  • id
  • name
  • missionBasePrice Forfait applique une fois par mission.
  • missionPricePerHa Prix de base applique par hectare.
  • minimumMissionPrice Montant minimum facture pour la mission.

Algorithme actuel

La fonction calculateMissionPrice(input, pricingConfig) applique les regles suivantes dans cet ordre :

  1. Validation des donnees
  2. Ajout du forfait mission
  3. Ajout du prix mission par hectare
  4. Ajout des lignes de chaque option d’analyse
  5. Calcul du sous-total
  6. Ajout eventuel d’un ajustement de minimum
  7. Calcul du total final

1. Validation

Le calcul rejette :

  • une totalSurface negative ou non finie ;
  • un montant negatif ou non fini dans la configuration globale ;
  • un montant negatif ou non fini sur une option d’analyse.

null et undefined sont acceptes pour basePrice et pricePerHa sur les options. Ils sont traites comme 0.

2. Forfait mission

Si missionBasePrice > 0, une ligne est ajoutee :

  • quantity = 1
  • unitPrice = missionBasePrice
  • amount = missionBasePrice

3. Prix mission par hectare

Si missionPricePerHa > 0, une ligne est ajoutee :

  • quantity = totalSurface
  • unitPrice = missionPricePerHa
  • amount = missionPricePerHa * totalSurface

4. Options d’analyse

Pour chaque option :

  • si basePrice > 0, une ligne forfaitaire est ajoutee ;
  • si pricePerHa > 0, une ligne surfacique est ajoutee.

Autrement dit, la formule d’une option est :

optionTotal = (basePrice ?? 0) + (pricePerHa ?? 0) * totalSurface

5. Sous-total

Le sous-total correspond a la somme des amount de toutes les lignes generees avant application du minimum.

6. Minimum mission

Si minimumMissionPrice > subtotal, une ligne supplementaire est ajoutee pour combler l’ecart :

minimumAdjustment = minimumMissionPrice - subtotal

Cette ligne ne remplace pas les autres. Elle s’ajoute au detail pour rendre l’ajustement visible.

7. Arrondi

Chaque montant est arrondi a 2 decimales via roundMoney. Le total final est lui aussi recalcule a partir des lignes et arrondi a 2 decimales.

Structure de sortie

La fonction retourne :

  • totalPrice
  • lineItems

Chaque lineItem contient :

  • type
  • code
  • label
  • quantity
  • unitPrice
  • amount

Role de type et code

Aujourd’hui, type et code ne configurent pas le prix. Ils decrivent les lignes produites par le calcul.

type

type categorise la nature metier de la ligne :

  • mission_base forfait global de mission ;
  • mission_surface prix de base par hectare ;
  • mission_minimum_adjustment ajustement applique pour atteindre le minimum ;
  • analysis_option ligne issue d’une option d’analyse.

type est utile pour :

  • grouper ou afficher les lignes dans le front ;
  • reconnaitre les categories de cout dans les tests ;
  • garder une semantique stable cote API.

Mais type n’est pas lu comme une regle dynamique de pricing. Il est derive par le code, pas saisi en base pour piloter l’algorithme.

code

code identifie plus precisement la ligne.

Pour les lignes globales de mission, les codes sont statiques :

  • mission-base
  • mission-surface
  • mission-minimum-adjustment

Pour les options d’analyse, le code est derive du code de l’option :

  • ${analysisOption.code}:base
  • ${analysisOption.code}:surface

Exemples :

  • hydrometrie:base
  • hydrometrie:surface
  • azote:base

code sert donc a :

  • rattacher une ligne a une option precise ;
  • differencier la part forfaitaire et la part surfacique d’une meme option ;
  • produire un identifiant stable pour le recapitulatif ou d’eventuels traitements aval.

Comme type, code est aujourd’hui une sortie descriptive, pas un parametre de configuration du calcul.

Ce qui configure reellement le prix aujourd’hui

Les vraies entrees de configuration sont :

  • dans MissionPricingConfig missionBasePrice, missionPricePerHa, minimumMissionPrice ;
  • dans AnalysisOption basePrice, pricePerHa ;
  • dans l’entree de calcul totalSurface et la liste des options selectionnees.

En pratique :

  • changer missionBasePrice change la ligne mission_base ;
  • changer missionPricePerHa change la ligne mission_surface ;
  • changer minimumMissionPrice change l’eventuelle ligne mission_minimum_adjustment ;
  • changer analysisOption.basePrice ou analysisOption.pricePerHa change les lignes analysis_option correspondantes ;
  • changer analysisOption.code ne change pas la formule, mais change le code retourne dans les lignes ;
  • changer analysisOption.name ne change pas la formule, mais change le label retourne.

Exemple complet

Avec :

  • totalSurface = 12.5
  • missionBasePrice = 50
  • missionPricePerHa = 2
  • minimumMissionPrice = 0
  • option Hydrometrie basePrice = 120, pricePerHa = 4.5, code = hydrometrie
  • option Azote basePrice = 90, pricePerHa = 3.2, code = azote

Le detail produit est :

  • mission_base code mission-base montant 50
  • mission_surface code mission-surface montant 25
  • analysis_option code hydrometrie:base montant 120
  • analysis_option code hydrometrie:surface montant 56.25
  • analysis_option code azote:base montant 90
  • analysis_option code azote:surface montant 40

Total :

50 + 25 + 120 + 56.25 + 90 + 40 = 381.25

Cycle de vie de la configuration globale

L’administration du pricing global passe par packages/api/src/routers/pricing/router.ts.

Une configuration :

  • est creee en DRAFT ;
  • peut etre modifiee tant qu’elle reste en DRAFT ;
  • peut etre publiee ;
  • peut etre archivee.

Contraintes actuelles :

  • seule une configuration DRAFT peut etre modifiee ;
  • publier une configuration archive automatiquement toute configuration deja PUBLISHED ;
  • une configuration ARCHIVED ne peut plus etre re-archivee utilement.

Les schemas d’entree packages/api/src/schemas/pricing/schemas.ts imposent :

  • un name non vide, max 120 caracteres ;
  • des montants numeriques positifs ou nuls ;
  • des montants exprimes en EUR avec jusqu’a 2 decimales selon la convention documentee.

Limites du modele actuel

Le modele actuel fonctionne bien pour un pricing compose de :

  • forfait mission ;
  • prix mission par hectare ;
  • forfait par option ;
  • prix par hectare par option ;
  • minimum de mission.

En revanche, type et code ne permettent pas a eux seuls de rendre le pricing dynamique. Ils restent de simples marqueurs de sortie.

Si demain le projet veut rendre le modele tarifaire reellement configurable, il faudra introduire un moteur de regles versionne en base, avec des kind metier interpretes par le serveur, plutot que de transformer type ou code en champs libres pilotant le calcul.