Search by

caprel / laravel-billing

Eric-CAP-REL

Quota, overrun and billing plumbing shared by the CAP-REL SaaS webservices: three allowances per metric, overrun billed in packs, acts pushed to smartmakersaasbilling

Package info

inligit.fr/cap-rel/laravel/laravel-billing

pkg:composer/caprel/laravel-billing

Statistics

Installs: 4

Dependents: 0

Suggesters: 0

v0.2.0 2026-10-07 05:39 UTC

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éIssueCe que le service fait
jusqu'à 1000INCLUDEDsert
1001 à 1500SERVE_AND_BILLsert, facture, signale une fois
1501 à 1550SERVE_AND_WARNsert, facture, alerte fortement
au-delàREFUSEDrefuse

Trois propriétés de ce choix, et chacune a une raison :

  • le défaut 0 / 0 est 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/token et répond 429, ce qui arrive pour de vrai quand un déploiement finit par cache: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%29 donne 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é :

ColonneQui la fabriqueÀ quoi elle sert
referencenous, avant l'appell'idempotence, et le nom que la poussée renvoie
draft_refDolibarr, à la créationadresser la validation, et rien d'autre
central_order_refDolibarr, à la validationtous 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.