Un forfait avec limite d'usage semble être la fonctionnalité la plus simple d'un SaaS : vous additionnez la consommation et vous la comparez au plafond. Ce calcul tient jusqu'au jour où vous vendez votre première recharge, et il faut alors migrer à chaud. Vous verrez ici comment le modéliser dès le départ pour éviter cela, où se cachent les compétitions entre requêtes concurrentes et comment les résoudre. Les exemples sont en Laravel, l'une de nos spécialités, mais le modèle de données est le même quel que soit le stack.
Le solde ne se calcule pas, il se stocke
La première version que tout le monde écrit additionne la consommation de la période et la soustrait de la limite du plan. Cela fonctionne parfaitement jusqu'au jour où vous vendez une recharge.
Car une recharge n'est pas une consommation, elle ne peut donc pas s'exprimer comme une soustraction de ce qui a été dépensé. Dès qu'un client peut acheter des crédits à l'unité en plus de ceux de son plan, le solde cesse d'être une opération arithmétique sur l'historique et devient un état qu'il faut stocker.
Ce n'est pas une optimisation, c'est le modèle. Et plus tôt vous l'admettez, moins vous aurez de migrations douloureuses.
Quatre tables, et chacune répond à une question différente
La structure qui fonctionne chez nous sépare quatre choses que l'on a tendance à mélanger en une seule :
Le compte conserve ce qui n'expire pas : dans quel plan il se trouve et combien de crédits achetés il lui reste.
Les fenêtres conservent ce que le plan a déjà couvert sur chaque période. Une ligne par compte et par fenêtre.
Les dépôts sont les entrées : achats, cadeaux, remboursements, corrections.
La consommation, ce sont les sorties, et c'est une table qui ne fait que croître. Elle n'est jamais modifiée ni supprimée.
Cette séparation compte parce qu'une recharge et une consommation sont des événements de nature différente, et les ranger dans la même table avec le signe inversé vous oblige à interpréter chaque ligne pour savoir ce qu'elle était.
Un plan accorde un chiffre, et chaque fenêtre en prend sa part
Un forfait n'a presque jamais une seule limite : il en a une mensuelle, une quotidienne et parfois une par session. La façon dont elles s'articulent décide si faire monter un client de forfait revient à changer un nombre ou à revoir trois tables.
L'intuitif consiste à déclarer une limite par période : tant par mois, tant par semaine, tant par session. Le problème apparaît au moment de monter de plan, car il faut penser à monter les trois, et le jour où l'on en oublie une, le client a payé davantage et reste plafonné là où il était.
Ce qui marche le mieux, c'est un seul chiffre et un pourcentage par fenêtre. Cinquante mille par mois font douze mille cinq cents par semaine et deux mille par session, et monter le plan monte les trois d'un coup. Un nouveau plan se déclare avec un nombre.
Un détail qui surprend la première fois : les pourcentages n'ont pas à faire cent. Chaque fenêtre calcule son plafond de son côté sur le même chiffre de base. Le mensuel peut être à cent pour cent et l'hebdomadaire à vingt-cinq, et ils ne se contredisent pas : ce sont deux plafonds distincts appliqués simultanément.
Les fenêtres se réinitialisent toutes seules, sans cron
Presque toutes les implémentations maison finissent avec une tâche planifiée qui remet les compteurs à zéro à minuit. C'est une pièce de plus qui peut tomber en panne, et quand elle tombe, personne ne s'en aperçoit avant qu'un client ne réclame.
Ce n'est pas nécessaire. Il suffit de stocker quand la fenêtre a commencé et de le vérifier au moment de débiter : si elle est échue, on la réinitialise sur place et on continue. La réinitialisation est paresseuse. Il n'y a rien à synchroniser, aucun webhook, aucune tâche qui reste bloquée.
Et il y a deux comportements à distinguer. Une fenêtre ancrée avance sur une grille fixe : le mois commence quand c'est son tour, que vous soyez venu ou non. Une fenêtre glissante commence à compter à partir du retour de l'utilisateur, ce que vous voulez pour un plafond par session.
Consulter le solde ne doit pas ouvrir une fenêtre. Cela paraît évident et l'erreur est facile : si l'objet qui représente la fenêtre est créé à la lecture, un utilisateur qui vient simplement regarder ce qu'il lui reste démarre sa fenêtre de session sans avoir rien consommé. Les lectures doivent répondre sans écrire une seule ligne.
D'abord le plan, ensuite l'acheté, et cela change l'écran
L'ordre de débit ressemble à un détail comptable et il a une conséquence visible.
On débite d'abord sur l'allocation du plan, et ce n'est qu'une fois celle-ci épuisée que l'on puise dans les crédits achetés. Et surtout : ce que paient les crédits achetés ne compte pas contre les fenêtres. Ils survivent à toutes les réinitialisations et ne consomment pas le pourcentage de la période.
L'effet pratique est que le compteur mensuel ne reflète pas la consommation totale. Un client qui a épuisé son allocation et continue de travailler avec des recharges voit sa barre à cent pour cent alors qu'il continue de produire. C'est correct, et il faut l'expliquer dans l'interface, sinon vous générez du support.
Un second effet, correct lui aussi et surprenant lui aussi : la fenêtre la plus étroite commande. Si le plafond hebdomadaire laisse cent crédits et le mensuel cinq mille, un débit de trois cents ne peut prendre que cent sur le plan, et le reste part sur l'acheté. Le mensuel n'avance que de cent.
Le débit ne doit pas refuser, la vérification préalable si
Il y a deux moments où l'on peut dire non : avant de commencer et à la fin. Cela semble équivalent et ne l'est pas, car entre les deux le travail a été fait et quelqu'un l'a payé.
Quand arrive le moment de débiter, l'opération est déjà à moitié faite. Le document a été généré, l'appel au modèle est parti, l'e-mail est envoyé. Refuser le débit à cet instant ne défait rien, cela laisse simplement le système sans trace de quelque chose qui a bel et bien eu lieu.
Donc débiter ne refuse jamais : cela enregistre la consommation, et s'il n'y avait pas de solde, cela le note comme découvert. L'endroit où freiner, c'est avant, avec une vérification explicite qui peut en plus dire quelque chose d'utile à l'utilisateur.
Cela fait de "demander d'abord" la partie qui doit être facile à écrire. Si vérifier le solde est pénible, les gens l'esquivent, et le découvert cesse alors d'être un filet pour devenir le chemin normal.
Où sont les courses critiques, ce que personne n'anticipe
Si vous vendez cela, deux requêtes simultanées ne peuvent pas dépenser le même crédit. Et il n'y a qu'un seul endroit où poser le verrou : la ligne du compte.
Le débit ouvre une transaction, verrouille cette ligne en exclusif, relit les fenêtres à l'intérieur de la transaction et calcule seulement alors ce qui rentre. Le second processus attend le premier et voit le solde déjà bougé. Sans la relecture à l'intérieur du verrou, le second calculerait sur des données antérieures et vous auriez la mise à jour perdue du manuel.
Il y a une deuxième course que presque tout le monde néglige : la création du compte lui-même. Deux requêtes concurrentes au premier jour d'un client peuvent tenter de le créer en même temps. Cela se résout avec un index unique et en relisant quand l'insertion échoue, pas en vérifiant d'abord s'il existe.
Et une qu'il vaut mieux accepter que fermer : vérifier et débiter ne sont pas atomiques. Deux requêtes peuvent passer toutes les deux la vérification et débiter toutes les deux. Fermer cette fenêtre exigerait de tenir le verrou depuis avant le début de l'opération jusqu'après sa fin, ce qui sérialiserait toute votre application. Comme débiter ne refuse pas, le résultat est un petit découvert enregistré, pas une opération à moitié faite. C'est le bon compromis.
Un remboursement ne rend pas la même chose que ce qui a été dépensé
Rembourser des crédits paraît l'opération inverse de leur consommation, et avec un seul type de solde ce le serait. Dès que coexistent l'allocation du forfait et les recharges achetées, annuler un débit oblige à décider vers laquelle des deux l'argent revient.
Si une opération a été payée sur l'allocation mensuelle et qu'elle échoue, le remboursement naturel est un dépôt. Mais un dépôt entre dans le solde acheté, il ne décrémente pas le compteur de la fenêtre. Le client récupère ses crédits dans le seau qui n'expire pas, et sa barre de consommation mensuelle continue d'afficher la dépense.
Ce n'est pas une erreur, c'est la conséquence du fait que les deux choses vivent à des endroits différents. Mais si votre interface parle en pourcentages, il y a là une conversation avec l'utilisateur qu'il vaut mieux anticiper.
Ce qui existe sur le marché et en quoi cela diffère
Sous Laravel, la référence pour encaisser est Cashier, et il n'entre pas en concurrence avec ceci : ce sont des pièces complémentaires. Cashier répond à la question de ce que le client a payé. Un compteur répond à celle de ce qu'il a dépensé de ce qu'il a payé. Dans nos projets, les deux cohabitent sur le même modèle.
Il y a une différence d'approche qui compte, elle. La facturation à l'usage des passerelles remonte la consommation au fournisseur et facture ensuite. Un compteur local freine avant, sans sortir sur le réseau, ce dont vous avez besoin quand c'est vous qui avancez le coût de l'opération.
Et il y a une raison de découpler l'horloge de facturation de l'horloge de quota : un abonnement annuel accorde généralement une allocation mensuelle. Une fenêtre ancrée sur la période de facturation ne la réinitialiserait qu'une fois par an.
Larameter, celui que nous maintenons, est né de là. Sur Crowd Legal, il a remplacé un compteur fait main, et la répartition hebdomadaire de vingt-cinq pour cent a été choisie précisément pour reproduire ce que celui-ci faisait déjà. Cela n'a rien à voir avec l'IA : on y débite des crédits pour créer un formulaire, générer un document, lancer une enquête, vérifier une identité et envoyer un e-mail, et aucune de ces actions n'appelle un modèle.
Et si vous ne travaillez pas avec Laravel
Les décisions sont les mêmes et aucune ne dépend du langage :
Stockez le solde, ne le calculez pas, dès lors que vous vendez des recharges. Séparez les entrées des sorties dans des tables distinctes. Un chiffre par plan et un pourcentage par fenêtre, pas un nombre par période. Réinitialisez au débit, pas avec un cron. Verrouillez la ligne du compte et relisez à l'intérieur de la transaction. Et que débiter ne refuse pas, en plaçant la porte avant le début de l'opération.
Si tout cela est en place, le paquet que vous utilisez importe assez peu.
Où est le code
Le paquet est publié sur Packagist, le code sur GitHub et la documentation du paquet recense toutes les options. Il est en production sur Crowd Legal et sur Abodara, cette dernière vendant des recharges via passerelle de paiement.
Si vous montez un produit sur abonnement avec des limites d'usage, c'est l'une des parties qui coûte le plus cher à refaire. Nous travaillons cela dans SaaS sur mesure et applications web, et si vous nous exposez votre modèle, nous vous dirons où sont les pièges.