caprel / laravel-billing
Quota, overrun and billing plumbing shared by the CAP-REL SaaS webservices: three allowances per metric, overrun billed in packs, acts pushed to smartmakersaasbilling
Requires
- php: ^8.2
- illuminate/contracts: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.24
- orchestra/testbench: ^9.0|^10.0
- phpunit/phpunit: ^11.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-07 06:05:41 UTC
README
Ce qu'un webservice SaaS de la maison doit savoir faire de son quota, de ce
qui le dépasse, et de ce qu'il en facture. Pendant de
caprel/laravel-ops
pour le déploiement.
Il porte la mécanique, jamais la règle de prix. Les prix vivent dans les fiches produit du Dolibarr central, les quotas et les marges sur le contrat : le paquet ne fait que les lire et en tirer une décision.
Le modèle qu'il met en oeuvre est décrit ci-dessous : le paquet se lit seul, sans la documentation interne qui a servi à le trancher.
Le modèle, en trois nombres
Par métrique et par compte, trois nombres lus sur le contrat Dolibarr. Les deux marges s'expriment en dépassement autorisé, jamais en plafond absolu, et valent zéro par défaut.
quota:
documents:
included: 1000 # inclus dans le forfait
soft: 500 # dépassement servi et facturé
hard: 550 # au-delà, refusé
overage_pack: P-DOLIPDF-PACK-200
pack_size: 200
api_keys:
included: 5 # soft et hard à zéro : plafonné net
Pour un quota de 1000 avec ces marges :
| Consommé | Issue | Ce que le service fait |
|---|---|---|
| jusqu'à 1000 | INCLUDED | sert |
| 1001 à 1500 | SERVE_AND_BILL | sert, facture, signale une fois |
| 1501 à 1550 | SERVE_AND_WARN | sert, facture, alerte fortement |
| au-delà | REFUSED | refuse |
Trois propriétés de ce choix, et chacune a une raison :
- le défaut
0 / 0est sûr. Un compte créé sans que personne n'y pense s'arrête à son forfait : il ne peut ni accumuler une dette, ni être vidé si ses identifiants fuient ; - la marge dure n'est pas commerciale. La souple dit jusqu'où on fait crédit, la dure borne ce qu'un compte volé consomme en une période. Un client de dix ans mérite une souple large, pas une dure absente ;
- les deux ne se touchent pas. L'écart entre elles est un tampon : on ne coupe pas net au franchissement du crédit accordé, ce qui interromprait un traitement par lot en cours. On alerte d'abord.
Le dépassement se facture en packs
Jamais à l'unité, jamais par tranches. Une seule référence de pack par métrique et par contrat, et le dépassement est arrondi au pack supérieur.
Ce que cette règle achète : le prix reste dans la fiche produit, la quantité facturée est un entier, c'est linéaire en quantité donc tirable, le client reconnaît une ligne qu'il a déjà achetée, et il n'y a aucun optimiseur à écrire pour choisir entre trois packs de 200 et un de 500.
Utilisation
use Caprel\Billing\Quota\Allowance;
use Caprel\Billing\Quota\QuotaPolicy;
$allowance = Allowance::fromArray($contract['quota']['documents']);
$decision = (new QuotaPolicy)->decide($allowance, $consumedThisPeriod);
if (! $decision->isAllowed()) {
abort(402, 'Quota dépassé.');
}
if ($decision->outcome->isBillable()) {
// $decision->packsToBill() x $decision->overagePack()
}
QuotaPolicy est pur : des nombres entrent, une décision sort. Pas
d'Eloquent, pas de HTTP, pas d'horloge, donc la règle se teste sans base de
données et se lit sans contexte.
Le canal vers le Dolibarr central
Caprel\Billing\Channel\BillingClient appelle les huit verbes du module
smartmakersaasbilling. Il porte les règles que quatre services ont
apprises une à une, et chacune est tenue par un test qui la nomme :
- le jeton est mis en cache, jamais journalisé, et la politique de rejeu
s'applique aussi à son obtention - le portail limite
/oauth/tokenet répond 429, ce qui arrive pour de vrai quand un déploiement finit parcache:clear; - un 429 n'est pas un refus d'identifiants : compté comme tel, il ouvre le coupe-circuit et ferme la boutique un quart d'heure sur un incident qui se résout en secondes ;
- le coupe-circuit s'ouvre après trois refus consécutifs ; un appel qui passe remet le compteur à zéro ;
- on ne rejoue que ce qui est rejouable : connexion, 429, 5xx. Jamais un 422, qui signale une charge utile fausse ;
- la référence d'une commande n'est jamais encodée dans le chemin : un
brouillon s'appelle
(PROV12), et%28PROV12%29donne un 404 sur la présentation au paiement de toutes les commandes. Elle est vérifiée, pas échappée ; - aucun montant ne part d'ici. Une ligne porte une référence produit et une quantité entière.
La table d'actes, et pourquoi deux colonnes de référence
billing_acts est le vrai travail d'un chantier de facturation, bien avant
l'appel HTTP : la facturation existe même quand le canal est absent,
donc un acte s'enregistre et attend plutôt que de disparaître.
Deux colonnes de référence, parce qu'une seule en a porté deux sens et a produit trois pannes d'un coup, dont un accès qui ne se rouvrait jamais après un règlement pourtant encaissé :
| Colonne | Qui la fabrique | À quoi elle sert |
|---|---|---|
reference | nous, avant l'appel | l'idempotence, et le nom que la poussée renvoie |
draft_ref | Dolibarr, à la création | adresser la validation, et rien d'autre |
central_order_ref | Dolibarr, à la validation | tous les verbes suivants |
Le registre des mouvements
Dû par tout service appliquant le modèle du quota, et pas seulement par ceux qui se pensent vendeurs de crédits : un forfait avec quota a un solde, alimenté par le forfait du mois, les packs achetés et les gestes commerciaux, diminué par chaque acte. Un compteur seul ne dit pas d'où il vient.
Le solde se lit depuis les mouvements, et il n'y a pas de colonne de
solde à côté : ce qu'un grep sur les incréments devait garantir, l'absence
d'un second nombre le garantit par construction.
Recevoir un règlement
Caprel\Billing\Webhook\SettlementController vérifie la signature
HMAC-SHA256 du corps brut sous le secret de webhook de la fiche
SaasClient - pas le secret OAuth, et sans repli sur lui : les deux n'ont
ni le même cycle de vie ni la même façon d'être récupérés, et un repli
masque la mauvaise configuration au lieu de la nommer.
Le corps est un déclencheur, jamais une source : l'état est relu auprès du module avant toute écriture. Les quatre événements sont traités, dont les trois que Dolibarr décide seul - sans eux un service continue de servir un abonnement dont l'argent est reparti.
Le service écoute ActStateChanged et décide ce qu'un règlement veut dire
chez lui.
Les trois commandes d'un raccordement
php artisan billing:catalogue-template --output=catalogue.json
php artisan billing:sync-catalogue
php artisan billing:doctor
Le service déclare ce qu'il vend en liant CatalogueDefinition. Les
références produit se dérivent du préfixe configuré et ne sont jamais
écrites en dur : c'est le point qui tient toute la chaîne, puisque la
métrique OBAPI lit la même source. Écrites à deux endroits, elles divergent
au premier renommage et rien ne le signale - la mesure ne correspond alors
à aucune ligne de contrat, la ligne est facturée à sa quantité figée, sans
une erreur nulle part.
billing:doctor s'arrête à la première étape qui échoue et nomme la cause
plutôt que le code : un 401 sur un jeton pourtant délivré est un client
absent de SMARTAUTH_API_AUDIENCE, un 403 est une fiche SaasClient qui ne
correspond à aucun client OAuth, un 500 au corps vide est une erreur fatale
dans le module et la seule chose utile est de dire où regarder. Il
n'affiche jamais le jeton.
Développement
make install
make ci # pint, phpstan niveau 8 sans baseline, phpunit
Licence
AGPL-3.0-or-later, voir LICENSE.md.
Les services qui installent ce paquet sont publiés sous la même licence, et l'un d'eux ne doit pas cesser d'être redéployable par un tiers parce qu'une pièce manque. Un clone qui installe ce paquet sans configurer de canal de facturation applique quand même ses quotas : il ne facture simplement personne, ce qui est le comportement attendu d'une installation autonome.
Publier ne révèle rien de commercial, parce que rien de commercial n'est ici. Les prix, les barèmes, les références produit et les marges accordées à chaque client vivent dans le Dolibarr central, pas dans ce code.