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
totalSurfaceSurface totale de la mission.analysisOptionsListe des options d’analyse selectionnees.
Chaque option d’analyse contient :
idcodenamebasePricepricePerHa
2. MissionPricingConfig
La configuration globale de pricing contient :
idnamemissionBasePriceForfait applique une fois par mission.missionPricePerHaPrix de base applique par hectare.minimumMissionPriceMontant minimum facture pour la mission.
Algorithme actuel
La fonction calculateMissionPrice(input, pricingConfig) applique les regles suivantes dans cet ordre :
- Validation des donnees
- Ajout du forfait mission
- Ajout du prix mission par hectare
- Ajout des lignes de chaque option d’analyse
- Calcul du sous-total
- Ajout eventuel d’un ajustement de minimum
- Calcul du total final
1. Validation
Le calcul rejette :
- une
totalSurfacenegative 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 = 1unitPrice = missionBasePriceamount = missionBasePrice
3. Prix mission par hectare
Si missionPricePerHa > 0, une ligne est ajoutee :
quantity = totalSurfaceunitPrice = missionPricePerHaamount = 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 :
totalPricelineItems
Chaque lineItem contient :
typecodelabelquantityunitPriceamount
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_baseforfait global de mission ;mission_surfaceprix de base par hectare ;mission_minimum_adjustmentajustement applique pour atteindre le minimum ;analysis_optionligne 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-basemission-surfacemission-minimum-adjustment
Pour les options d’analyse, le code est derive du code de l’option :
${analysisOption.code}:base${analysisOption.code}:surface
Exemples :
hydrometrie:basehydrometrie:surfaceazote: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
MissionPricingConfigmissionBasePrice,missionPricePerHa,minimumMissionPrice; - dans
AnalysisOptionbasePrice,pricePerHa; - dans l’entree de calcul
totalSurfaceet la liste des options selectionnees.
En pratique :
- changer
missionBasePricechange la lignemission_base; - changer
missionPricePerHachange la lignemission_surface; - changer
minimumMissionPricechange l’eventuelle lignemission_minimum_adjustment; - changer
analysisOption.basePriceouanalysisOption.pricePerHachange les lignesanalysis_optioncorrespondantes ; - changer
analysisOption.codene change pas la formule, mais change lecoderetourne dans les lignes ; - changer
analysisOption.namene change pas la formule, mais change lelabelretourne.
Exemple complet
Avec :
totalSurface = 12.5missionBasePrice = 50missionPricePerHa = 2minimumMissionPrice = 0- option
HydrometriebasePrice = 120,pricePerHa = 4.5,code = hydrometrie - option
AzotebasePrice = 90,pricePerHa = 3.2,code = azote
Le detail produit est :
mission_basecodemission-basemontant50mission_surfacecodemission-surfacemontant25analysis_optioncodehydrometrie:basemontant120analysis_optioncodehydrometrie:surfacemontant56.25analysis_optioncodeazote:basemontant90analysis_optioncodeazote:surfacemontant40
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
DRAFTpeut etre modifiee ; - publier une configuration archive automatiquement toute configuration deja
PUBLISHED; - une configuration
ARCHIVEDne peut plus etre re-archivee utilement.
Les schemas d’entree packages/api/src/schemas/pricing/schemas.ts imposent :
- un
namenon 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.