Geler les montants à la commandeFreezing the amounts at order time
Un montant recalculé à l'affichage change tout seul dès que la grille bouge. Chaque envoi fige donc son palier et tous ses montants, une fois pour toutes.An amount recalculated at display time changes on its own as soon as the price grid moves. Each submission therefore freezes its tier and all its amounts, once and for all.

Il y a une classe de bogues qui n’existe pas encore quand on écrit le code, et qui apparaît le jour où une donnée change. Celui-ci est le plus désagréable de la famille : le montant juste dépend du moment où on le calcule. Tant que rien ne bouge, tout est correct. Au premier changement, le passé se réécrit tout seul.There is a class of bug that does not exist yet when you write the code, and that appears the day a piece of data changes. This one is the most unpleasant of the family: the correct amount depends on when you calculate it. As long as nothing moves, everything is right. At the first change, the past rewrites itself.
Sur djplays, trois choses peuvent bouger indépendamment : le prix que chaque DJ fixe, le palier dans lequel l’équipe le place, et la grille tarifaire elle-même. Trois leviers, donc trois façons pour un montant recalculé plus tard de ne plus valoir ce qu’il valait. Cette note explique comment le projet s’en protège, et ce que la protection coûte.On djplays three things can move independently: the price each DJ sets, the tier the team places them in, and the price grid itself. Three levers, so three ways for an amount recalculated later to no longer be worth what it was worth. This note explains how the project protects itself from that, and what the protection costs.
Le piège du calcul à la voléeThe trap of calculating on the fly
La façon naturelle d’écrire ça est une fonction. On lui donne un prix, elle renvoie la part du DJ et la commission, et on l’appelle partout où il faut afficher un montant : la page du DJ, le récapitulatif du producteur, le tableau de bord, le rapport quotidien. Une seule implémentation, aucune duplication, rien à synchroniser. C’est du bon code, et ça marche parfaitement jusqu’au premier changement de prix.The natural way to write this is a function. You give it a price, it returns the DJ share and the commission, and you call it everywhere an amount has to be displayed: the DJ page, the producer summary, the dashboard, the daily report. One implementation, no duplication, nothing to keep in sync. It is good code, and it works perfectly until the first price change.
Le symptôme, quand il arrive, est déroutant : une page affiche aujourd’hui un montant différent d’hier, pour une commande terminée depuis des semaines. Personne n’a touché à cette commande. Personne n’a touché au code. Un DJ a simplement relevé son prix pour les commandes à venir, et toutes ses commandes passées ont suivi, parce qu’elles ne portaient pas de montant : elles portaient une référence vers un prix.The symptom, when it arrives, is baffling: a page shows a different amount today than yesterday, for an order completed weeks ago. Nobody touched that order. Nobody touched the code. A DJ simply raised their price for future orders, and all of their past orders followed, because they did not carry an amount: they carried a reference to a price.
Ce bogue a deux propriétés qui le rendent coûteux. Il est silencieux : rien ne tombe en panne, aucune erreur n’est journalisée, l’application continue de répondre avec aplomb. Et il est rétroactif : il ne casse pas le présent, il réécrit le passé. Les tests ne le voient pas davantage, parce qu’un test qui calcule le montant attendu avec la même fonction que le code testé sera toujours d’accord avec lui-même.This bug has two properties that make it expensive. It is silent: nothing fails, no error is logged, the application keeps answering with confidence. And it is retroactive: it does not break the present, it rewrites the past. Tests do not see it either, because a test that computes the expected amount with the same function as the code under test will always agree with itself.
Ce qui bouge dans la grille, et à quelle vitesseWhat moves in the grid, and how fast
Pour comprendre la suite, il faut la grille. Les prix ne sont pas libres : chaque DJ fixe le sien, et la plateforme le plafonne selon son palier, déterminé par son audience vérifiée. Trois paliers, deux plafonds par palier.To follow what comes next, you need the grid. Prices are not free: each DJ sets their own, and the platform caps it according to their tier, determined by their verified audience. Three tiers, two caps per tier.
- Découverte, moins de 200 d’audience vérifiée : plafond de 3 euros pour une écoute, 9 euros pour un passage.Rising, under 200 verified audience: a cap of 3 euros for a listen, 9 euros for a play.
- Confirmé, de 200 à 2 000 : plafond de 6 euros pour une écoute, 18 euros pour un passage.Established, from 200 to 2,000: a cap of 6 euros for a listen, 18 euros for a play.
- Tête d’affiche, plus de 2 000 : plafond de 12 euros pour une écoute, 36 euros pour un passage.Headliner, above 2,000: a cap of 12 euros for a listen, 36 euros for a play.
Le plafond de passage vaut trois fois le plafond d’écoute du palier, et le prix d’écoute a un plancher d’un euro, identique pour tous. Dans ces bornes, le DJ règle lui-même son prix d’écoute, puis son prix de passage, qui ne peut pas descendre sous son prix d’écoute. Les réglages se font par pas de cinquante centimes, à raison d’un changement par mois, et uniquement pour les nouvelles commandes. Un nouveau DJ démarre au plafond de son palier.The play cap is three times the listen cap of the tier, and the listen price has a floor of one euro, the same for everyone. Within those bounds the DJ sets their own listen price, then their play price, which cannot go below their listen price. Adjustments are made in steps of fifty cents, one change per month, and only for new orders. A new DJ starts at the cap of their tier.
Le palier, lui, est fixé par l’équipe d’après l’audience vérifiée, pas par le DJ. S’il baisse, les prix du DJ sont ramenés sous le nouveau plafond : un prix peut donc changer sans que personne ne l’ait demandé. Et la grille elle-même peut être révisée, ce qui déplacerait les plafonds de tout le monde d’un coup. Trois sources de dérive, dont deux échappent complètement au DJ.The tier, for its part, is set by the team from the verified audience, not by the DJ. If it goes down, the DJ prices are brought back under the new cap: a price can therefore change without anyone having asked for it. And the grid itself can be revised, which would move every cap at once. Three sources of drift, two of which the DJ does not control at all.
Les colonnes figées dans la table des envoisThe frozen columns in the submissions table
La décision tient en une phrase : chaque envoi fige à la commande son palier et tous ses montants. L’unité de gel est l’envoi, c’est-à-dire un titre vers un DJ, et non la commande entière. C’est le bon niveau, parce qu’une commande peut viser plusieurs DJs qui n’ont ni le même palier ni les mêmes prix : il n’existe pas de montant unique à figer pour une commande.The decision fits in one sentence: each submission freezes its tier and all of its amounts at order time. The unit of freezing is the submission, that is, one track towards one DJ, and not the whole order. It is the right level, because an order can target several DJs who share neither tier nor prices: there is no single amount to freeze for an order.
Concrètement, la table des envois porte sept colonnes de montants, écrites une fois au moment de la commande et jamais recalculées ensuite. Pour l’écoute comme pour le passage, on y trouve le prix, la part du DJ et la commission ; s’y ajoute ce qui permet de rejouer le calcul, dont le palier retenu. Un envoi ne contient pas une référence vers un prix : il contient le prix.In practice the submissions table carries seven amount columns, written once at order time and never recalculated afterwards. For the listen as for the play you find the price, the DJ share and the commission; to that is added what allows the calculation to be replayed, including the tier applied. A submission does not contain a reference to a price: it contains the price.
Ce qui disparaît alors, c’est la fonction appelée à l’affichage. L’affichage lit, il ne calcule plus. La facture au nom du DJ, la facture de commission, le versement, le rapport quotidien et le récapitulatif annuel lisent tous les mêmes colonnes, donc ils affichent tous la même chose, aujourd’hui et dans trois ans. C’est moins élégant qu’une fonction partagée, et c’est beaucoup plus sûr.What disappears then is the function called at display time. Display reads, it no longer computes. The invoice in the name of the DJ, the commission invoice, the payout, the daily report and the annual summary all read the same columns, so they all show the same thing, today and in three years. It is less elegant than a shared function, and considerably safer.
Le palier figé mérite un mot à part, parce qu’il ne sert à aucun calcul. Il sert à l’explication. Des mois plus tard, quand un DJ demande pourquoi telle écoute lui a rapporté ce montant-là, on peut répondre sans conditionnel : parce qu’il était dans ce palier, avec ce prix, ce jour-là. Garder la raison d’un chiffre à côté du chiffre évite la plupart des échanges pénibles, et coûte une colonne.The frozen tier deserves a word of its own, because it serves no calculation. It serves the explanation. Months later, when a DJ asks why a given listen earned them that particular amount, you can answer without hedging: because they were in that tier, at that price, on that day. Keeping the reason for a figure next to the figure avoids most awkward exchanges, and costs one column.
Pourquoi une facture déjà émise ne peut pas changerWhy an invoice already issued cannot change
Le gel ne protège pas seulement un affichage. Un montant recalculé ne reste pas dans l’application : il a déjà servi. Il a été imprimé sur une facture émise au nom du DJ et adressée au producteur, et sur la facture de commission qui vient en face. Recalculer, c’est donc contredire deux documents qui sont partis, et dont la plateforme n’a plus le contrôle.Freezing does not only protect a display. A recalculated amount does not stay inside the application: it has already been used. It was printed on an invoice issued in the name of the DJ and addressed to the producer, and on the commission invoice facing it. Recalculating therefore means contradicting two documents that have left, and over which the platform no longer has any control.
Il a aussi servi à payer. Ce que le DJ a touché n’est pas une estimation susceptible d’être affinée, c’est un virement qui a eu lieu, pour un montant qui figure quelque part dans ses comptes. Un chiffre qui a servi à payer quelqu’un n’est plus une valeur à calculer : c’est un fait.It was also used to pay. What the DJ received is not an estimate open to refinement, it is a transfer that took place, for an amount recorded somewhere in their accounts. A figure that has been used to pay somebody is no longer a value to be computed: it is a fact.
Un montant n’est pas une valeur, c’est un événement. À partir du moment où il a servi à facturer ou à payer, il appartient au passé de la commande, et plus à la version courante du code.An amount is not a value, it is an event. From the moment it has been used to invoice or to pay, it belongs to the past of the order, and no longer to the current version of the code.
Les arrondis et l'invariant qu'ils doivent respecterRounding, and the invariant it has to respect
Tous les montants du projet sont des entiers, en centimes. C’est la décision la plus banale de cette note et l’une des plus utiles : les nombres à virgule flottante ne représentent pas exactement les fractions décimales, et une somme d’argent qui vaut parfois un millième de centime de moins que prévu finit toujours par produire un total qui ne tombe pas juste. En centimes entiers, le problème n’existe pas, et il n’y a rien à surveiller.Every amount in the project is an integer, in cents. It is the most mundane decision in this note and one of the most useful: floating point numbers do not represent decimal fractions exactly, and a sum of money that is sometimes worth a thousandth of a cent less than expected always ends up producing a total that does not add up. In whole cents the problem does not exist, and there is nothing to watch.
La règle de partage est fixée par la grille : le DJ touche la moitié du prix d’écoute et les deux tiers du prix de passage, arrondis au centime le plus proche, le demi-centime vers le haut. La commission n’est pas calculée séparément : elle est le reste.The sharing rule is set by the grid: the DJ receives half of the listen price and two thirds of the play price, rounded to the nearest cent, with the half cent rounded up. The commission is not calculated separately: it is the remainder.
Un exemple suffit à montrer pourquoi. Prenons un prix de passage de 2,50 euros, soit 250 centimes. Les deux tiers valent 166,66 et quelques, donc la part du DJ est de 167 centimes. La commission est alors 250 moins 167, soit 83 centimes. On vérifie : 167 plus 83 font 250, exactement. Et 83 n’est pas le tiers de 250, qui vaudrait 83,33 : la commission porte l’arrondi, parce qu’elle est définie comme ce qui reste.One example is enough to show why. Take a play price of 2.50 euros, that is 250 cents. Two thirds come to 166.66 and change, so the DJ share is 167 cents. The commission is then 250 minus 167, that is 83 cents. Check it: 167 plus 83 make 250, exactly. And 83 is not a third of 250, which would be 83.33: the commission carries the rounding, because it is defined as what is left.
C’est tout l’intérêt de la définition par le reste. L’égalité entre le prix d’un côté, et la part plus la commission de l’autre, tient par construction et non par coïncidence. Deux arrondis calculés indépendamment peuvent tomber juste pendant des mois, puis se tromper d’un centime sur un cas particulier ou sur une nouvelle règle de partage. Le jour où cela arrive, c’est un centime qui manque dans une facture, et un centime qui manque dans une facture se remarque autant qu’un euro.That is the whole point of defining it as the remainder. The equality between the price on one side, and the share plus the commission on the other, holds by construction and not by coincidence. Two independently rounded figures can land correctly for months, then be a cent out on an edge case or on a new sharing rule. The day that happens, a cent is missing from an invoice, and a cent missing from an invoice gets noticed as much as a euro.
Reste à dire pourquoi la règle du demi-centime est écrite noir sur blanc. Un arrondi non spécifié est un arrondi qui dépend de l’outil : les langages et les bases de données ne traitent pas tous le demi-centime de la même façon, certains arrondissant vers le haut, d’autres vers le pair le plus proche. Écrire la convention, même pour un cas rare, c’est garantir que deux morceaux du système qui calculent la même chose obtiennent le même résultat.That leaves the question of why the half cent rule is written down explicitly. An unspecified rounding is a rounding that depends on the tool: languages and databases do not all treat the half cent the same way, some rounding up, others to the nearest even number. Writing the convention down, even for a rare case, guarantees that two parts of the system computing the same thing get the same result.
Une seule source pour la grilleA single source for the grid
Les bornes de prix, les parts et les commissions vivent dans un seul fichier, chargé par le fichier commun du projet, donc disponible partout de la même façon : l’application, le site vitrine qui affiche les tarifs, et les scripts en ligne de commande. Un plafond qui change, c’est une ligne à modifier à un endroit, et non trois valeurs à retrouver dans trois contextes qui finiraient par ne plus dire la même chose.The price bounds, the shares and the commissions live in a single file, loaded by the common file of the project, so available everywhere in the same way: the application, the public site that displays the prices, and the command line scripts. A cap that changes is one line to edit in one place, not three values to track down in three contexts that would eventually stop saying the same thing.
Cela a l’air de contredire le gel, et c’est la nuance qui m’a demandé le plus de soin à poser. Il n’y a pas de contradiction, parce que les deux ne parlent pas du même moment. La source unique sert à calculer les montants d’une commande nouvelle, et à vérifier qu’un prix proposé respecte bien les bornes de son palier. Une fois la commande passée, elle n’a plus rien à dire sur elle. La grille décrit le présent ; les envois gardent le passé.This looks as if it contradicts the freezing, and it is the nuance that took me the most care to get right. There is no contradiction, because the two do not talk about the same moment. The single source serves to compute the amounts of a new order, and to check that a proposed price respects the bounds of its tier. Once the order has been placed, it has nothing more to say about it. The grid describes the present; the submissions keep the past.
Ce que le gel coûteWhat freezing costs
Deux choses, et il faut les regarder en face. D’abord la redondance : sept colonnes par envoi là où une fonction suffisait, et une base qui grossit plus vite. Ensuite, et c’est le vrai coût, une erreur de calcul devient difficile à corriger. Un montant figé faux reste faux, et aucun déploiement ne le répare. Il faut le traiter comme on traite une facture erronée : par un geste explicite, tracé et décidé, jamais par un recalcul silencieux qui remettrait tout le monde d’accord sans témoin.Two things, and they deserve to be looked at squarely. First the redundancy: seven columns per submission where a function was enough, and a database that grows faster. Then, and this is the real cost, a calculation error becomes hard to fix. A frozen amount that is wrong stays wrong, and no deployment repairs it. It has to be handled the way a wrong invoice is handled: by an explicit, traced, deliberate act, never by a silent recalculation that would quietly bring everyone into agreement with no witness.
Ce qu’on achète en échange vaut largement ces deux inconvénients. Une commande se relit des mois plus tard et raconte elle-même ce qui s’est passé. La grille peut évoluer sans que personne ne craigne un effet de bord sur l’historique. Et la question qui revient le plus souvent dans ce genre de système, pourquoi ce montant-là, a une réponse écrite dans la ligne, au lieu d’une reconstitution à partir de l’état actuel des prix.What you buy in exchange is well worth those two drawbacks. An order can be read back months later and tells you itself what happened. The grid can evolve without anyone fearing a side effect on the history. And the question that comes up most often in this kind of system, why that amount, has an answer written in the row, instead of a reconstruction from the current state of the prices.
Le projet derrière cette noteThe project behind this note
Tout est parti de djplays, la place de marché. La fiche détaille le contexte, ce qui a été construit et ce que le client en a tiré.It all started with djplays, la place de marché. The project page covers the context, what was built and what the client got out of it.