De quoi on parle, et où vit la facturation.
IoT-Link vend des jauges connectées — principalement LevelStream, une sonde ultrason qui mesure le niveau d'une citerne — puis un abonnement récurrent qui couvre l'accès au portail, la connectivité et les notifications. Le matériel est une vente ponctuelle. Tout le sujet de ce document est l'abonnement, qui est la source de revenu récurrent.
Le trajet de la donnée, pour situer :
sonde ──MQTT-SN/eSIM──▶ Thingstream ──▶ connector AWS ──▶ DynamoDB
│
service .NET/WLangage │ (polling, décodage)
▼
base HFSQL IoT-Link_Portail
Trois consommateurs travaillent en parallèle sur cette base : le portail web (WebDev), l'API Stripe pour les paiements et les factures, et la tâche planifiée qui produit la facturation. Cette dernière est le cœur du sujet.
Tout part du type de client. Il détermine l'architecture de facturation appliquée.
| CustomerType | Acteur | Ce que c'est |
|---|---|---|
| 0 | IoT-Link | La racine de l'arbre commercial. Pas de facturation. |
| 1 | Client final direct | Un particulier ou une entreprise qui équipe sa propre citerne, sans intermédiaire. |
| 2 | Gestionnaire | Un distributeur de mazout, un installateur. Il gère un parc de jauges pour ses propres clients. C'est le cas le plus riche. |
| 3 | Client final lié à un gestionnaire | Le client d'un gestionnaire. Il peut payer lui-même, ou son gestionnaire paie pour lui. |
| 4 | Distributeur | Prévu, non défini à ce jour. |
La hiérarchie est portée par Customer.IDCustomerLead, une auto-référence : le client
d'un gestionnaire pointe vers son gestionnaire.
Customer.BillStockDevice — booléen, au niveau du client. Détermine si les jauges
en stock chez le gestionnaire sont facturées ou non. Un changement prend effet
au cycle suivant, jamais en cours de cycle.ObjectsIoT.WhoPays — au niveau de la jauge. Pointe vers l'IDCustomer
qui prend en charge la facturation de cette jauge précisément. La granularité est à la
jauge, pas au client : un gestionnaire peut décider sonde par sonde s'il paie ou si son client
paie. À ne pas confondre avec ObjectsIoT.IDCustomer, qui dit à qui la sonde
appartient physiquement.Le catalogue vit dans StripeProducts, typé par ProductsType.
Liste donnée par Franck le 20 mai 2026.
| ProductsType | Produit | Assiette |
|---|---|---|
| 0 | Abonnement de base gestionnaire | Fixe, un par gestionnaire. Ouvre l'accès au portail. |
| 1 | Abonnement full leasing | Formule tout compris — matériel et service dans une mensualité unique. |
| 2 | Abonnement device gestionnaire | Par jauge présente au portail, placée ou non, active ou non. |
| 3 | Abonnement client | L'abonnement d'un client final — modèle X. |
| 4 | Pack SMS | Achat ponctuel de crédits de notification. Facturé hors abonnement. |
| 5 | Dépassement de messages | Par jauge ayant dépassé son quota sur la période. |
Ce ne sont pas des produits, et ils n'ont donc pas de ProductsType. Ce sont
des prix. Le catalogue est une hiérarchie à trois niveaux :
StripeProducts une catégorie de produit, typée par ProductsType
│ 1..N
StripePrices une déclinaison tarifaire de cette catégorie
│ ← C'EST ICI que vivent Bronze, Silver, Gold
│ au même titre que l'abo de base, le forfait device
│ ou un pack SMS
│ N..1
ThingstreamTarifType le coût de connectivité et le quota de messages inclus
Dire qu'« une jauge est en Gold » signifie que sa ligne dans
StripeSubscriptionsObjectsIoT porte un IDStripePrices pointant vers le
prix nommé Gold. Ce prix pointe lui-même vers un ThingstreamTarifType, qui porte
Cost — ce que la connectivité coûte à IoT-Link — et Quantity — le
nombre de messages inclus.
StripePrices.SMSQuantity compte les SMS de notification,
pas les messages MQTT.
Huit fichiers portent la facturation. Un seul est vraiment central.
| Fichier | Rôle | État |
|---|---|---|
StripeProducts | Le catalogue : quels produits existent, typés par ProductsType. | En place |
StripePrices | Les prix. Immuables — un prix ne se modifie jamais, toute évolution tarifaire crée un nouveau prix et désactive l'ancien. | En place |
ThingstreamTarifType | Le tarif de connectivité rattaché à un prix : Cost et Quantity. | En place |
StripeSubscriptions | L'abonnement souscrit par un client. Porte les identifiants Stripe, les dates, le statut. | En place |
StripeSubscriptionsObjectsIoTsouvent abrégé SSO | Le fichier central. Une ligne = une jauge rattachée à un abonnement, sur une période, à un prix donné. C'est là que vit le prorata et l'état de traitement. | à compléter |
StripeInvoices | Les factures émises. Status reprend les valeurs natives Stripe : draft, open, paid, uncollectible, void. | En place |
StripeInvoicesDetails | Le détail de la facture consolidée d'un gestionnaire : quelles lignes, quelles quantités, quels montants. | à créer |
StripeSubscriptionsHistory | L'historique au niveau abonnement : quel client a eu quel abonnement, à quel prix, de quand à quand, et pourquoi il s'est terminé. | à créer |
Un neuvième fichier existe, StripePlans : six rubriques, il stocke le prix qu'utilise
un abonnement. Une fois que chaque ligne de StripeSubscriptionsObjectsIoT portera son
propre IDStripePrices, il fera doublon à un grain plus grossier. Ne rien construire
dessus.
StripePrices. Le pointeur suffit à garantir l'historique : le prix
pointé ne changera jamais.
Selon le type de client, deux mécaniques différentes coexistent. C'est la décision la plus structurante du domaine.
| Client final — modèle X | Gestionnaire — modèle Y1-B | |
|---|---|---|
| Qui | CustomerType 1 ou 3 | CustomerType 2 |
| Cycle | Annuel, payé d'avance | Mensuel |
| Abonnements | Un par sonde. Chaque sonde a sa propre date anniversaire. | Un seul abonnement master multi-lignes pour tout le parc. |
| Facture | Une à la souscription, puis une par échéance annuelle | Une facture consolidée par mois, reprenant tout le parc |
| Facture systématique ? | Non — hors anniversaire, une facture n'est créée que s'il y a dépassement | Oui — à chaque anniversaire mensuel, quoi qu'il arrive |
| Prorata | Calculé une fois à l'ajout, puis figé | Calculé par mouvement, consolidé mensuellement |
| Engagement | 1, 2 ou 3 ans au choix, payé intégralement d'avance | Non défini à ce jour |
| État | En place | En développement |
Côté base, la structure est identique dans les deux cas : un abonnement porte une à N lignes de jauge. Ce qui diffère, c'est la fréquence, le réglage du stock, et la condition de génération de facture.
Deux choses à ne pas confondre : ce que la base porte aujourd'hui, et le cycle de vie qui a été conçu mais jamais mis en place.
ObjectsIoT.ActivityState aujourd'huiEn placeUn état technique de fonctionnement, écrit par les services de Serge. Cinq valeurs :
| Valeur | Signification |
|---|---|
99 | Non évalué — la sonde n'a pas encore émis |
1 | En fonctionnement |
0 | Arrêtée — message STOP reçu |
-1 | Arrêt anormal |
-2 | Inconnu |
Source : documentation de l'API REST publique
api.iot-link.app/v1, version du 12 juin 2026, rédigée par Serge. En pratique, les
valeurs rencontrées dans la base sont surtout 99, 1 et
-1.
Six statuts nommés ont été définis pour que l'état d'une jauge commande à la fois le plan Thingstream — donc le coût de connectivité — et l'état de facturation. Ce modèle n'existe aujourd'hui ni dans la base ni dans le code.
| Statut visé | Situation | Plan Thingstream | Facturation |
|---|---|---|---|
NON_ASSEMBLEE | eSIM importée, sonde pas encore construite | Inactif | Aucune |
EN_TEST | Assemblage et tests usine | Actif temporairement | Démarre |
PRETE_ACTIVATION | En stock ou vendue, en attente d'abonnement | Inactif | Aucune |
ACTIVE | Abonnement souscrit et premier démarrage aimant effectué | Actif | En cours |
ARRETEE | Le client a demandé l'arrêt, un STOP a été envoyé | Actif si l'abonnement l'est | Continue |
ARRETEE_NON_PAIEMENT | Désactivée pour défaut de paiement | Inactif | Suspendue |
Ce que ce modèle apporterait : la distinction entre fabrication, stock et service, et surtout le
lien explicite entre l'état d'une sonde et sa facturation. Le point contre-intuitif à retenir est
que ARRETEE ne signifierait pas « plus facturée » — un client qui demande l'arrêt des
remontées continue de payer son abonnement jusqu'à sa résiliation effective.
ActivityState est écrit par les services de Serge, pas par le portail. Passer
du modèle actuel au modèle cible n'est donc pas une modification de l'analyse ni du code de
facturation : c'est un arbitrage à trois — Michael, Serge et Franck — qui touche la chaîne de
traitement des messages. Ouvert depuis le 2 juin, jamais tenu.
C'est le principe qui commande toute la tâche planifiée. Il surprend souvent.
Toutes les factures sont calées sur la date anniversaire de souscription, jamais sur le 1er du mois. Un gestionnaire qui a souscrit un 17 est facturé le 17 de chaque mois. Son « mois » va du 17 au 16.
Chaque situation avec ce qui se passe concrètement. Les montants sont illustratifs — la grille tarifaire n'est pas arrêtée. Les exemples de prorata utilisent un plan Gold à 4,99 € par mois sur un cycle de 30 jours.
CustomerType 1StripeSubscriptions est créé, annuel, avec sa propre date anniversaire.Processed = TRUE,
MontantProrata = 0. Pas de prorata, puisque son cycle démarre le jour même.1.CustomerType 2StripeSubscriptions master mensuel est créé.BillStockDevice décideSi TRUE — les jauges en stock sont facturées :
Abonnement de base 100,00 €
Forfait device 10 × 0,75 € 7,50 €
Si FALSE — le stock n'est pas facturé :
Abonnement de base 100,00 €
Cycle du 1er au 30. Une jauge Gold est activée le 15.
IDStripePrices = le prix Gold, StartDate
= 15, EndDate = NULL, IsResilie = FALSE.MontantProrata est calculé sur les jours restants du cycle — 16 jours sur 30 →
+2,66 € — et Processed reste à FALSE.Processed à TRUE.Le détail reste disponible, et c'est ce qui rend la
consolidation acceptable. Le fichier StripeInvoicesDetails reçoit une ligne par
jauge — prix, période, montant — et chaque ligne d'abonnement pointe vers la facture qui l'a
portée. À la question « ce prorata de 47,32 €, ça correspond à quoi ? », la réponse se lit sonde
par sonde, numéros de série compris, sans refaire le calcul. Montage classique en télécom :
facture synthétique, détail consultable.
EndDate = 15, IsResilie
= TRUE. Elle n'est pas modifiée au-delà.Sonde activée le 15, retournée en stock le 25. La question a été posée le 22 mai et n'a jamais été tranchée : facture-t-on les dix jours réellement utilisés, ou le cycle entamé en entier ?
Règle A — correction proratisée. Une ligne négative annule la part non consommée :
Ligne 1 Qte +1 MontantProrata +2,66 € (16 jours, à l'entrée)
Ligne 2 Qte -1 MontantProrata -1,00 € (6 jours non utilisés)
─────────
Net consolidé +1,66 € = 10 jours d'usage réel
Règle B — cycle entamé dû. Aucune correction. Le prorata reste appliqué à l'entrée, la sortie ne produit rien. Plus simple à coder, plus simple à expliquer, moins précis pour le client.
WhoPaysObjectsIoT.IDCustomer.ObjectsIoT.WhoPays pointe vers le client final : c'est lui qui est facturé
pour cette jauge.ProductsType = 5 est produite.StripeInvoices.IDStripeSubscriptions
puisse être vide : une facture de pack SMS n'a pas d'abonnement. Elle est rattachée par
IDStripePurchaseSMS, et IDCustomer permet de retrouver le client.STOP est envoyée à la sonde.ARRETEE_NON_PAIEMENT, qui n'existe pas encore (voir §6).WhoPays.
À concevoir — ne pas implémenter le comportement natif sans arbitrage.
ProductsType = 1Matériel et service fondus dans une mensualité unique, sans investissement de départ. Cet abonnement remplace l'abonnement de base et tous les abonnements de jauge : il n'y a ni forfait device ni plan par sonde à côté. Déjà en production.
Un client final arrête d'utiliser sa sonde avant la fin de son engagement — il déménage, il n'a plus de citerne, il la donne. Deux besoins ont été exprimés en mai et aucun n'a été traité : pouvoir délier la sonde du compte de son propriétaire, et pouvoir la réintégrer chez un gestionnaire pour la replacer ailleurs.
Ce qui est acquis : un abonnement payé n'est pas remboursé. Le client garde l'usage jusqu'au terme qu'il a payé. La date de fin est posée, et la tâche planifiée annule l'abonnement dans Stripe le moment venu.
Deux mécaniques, une par modèle. Ne pas les confondre.
MontantProrata signé sur les jours restants, Processed = FALSE.
Aucun appel Stripe.Processed à TRUE.Processed répond à « ai-je déjà facturé le prorata de cette ligne ? ».
EndDate répond à « cette jauge est-elle encore active pour les cycles à venir ? ».
Ce sont deux questions indépendantes, et les quatre combinaisons existent.
Pour compter les jauges actives : WHERE EndDate IS NULL.
Pour calculer les proratas dus : WHERE Processed = FALSE.
Chacun a déjà coûté du temps à quelqu'un.
0 en WLanguageLa lecture WLanguage d'une rubrique numérique nullable renvoie 0,
pas NULL. Pour distinguer une valeur réellement à zéro d'une valeur absente, il faut
EstNull(), ou une requête SQL avec IS NULL explicite. Corollaire :
tester une date par = "" ne détecte pas une date absente.
Le DDL exporté d'une base HFSQL ne contient ni les cardinalités, ni l'intégrité référentielle : elles vivent dans l'analyse et sont appliquées par le moteur côté application. Une clé étrangère peut apparaître nullable dans l'export et être parfaitement protégée dans les faits. Et l'inverse : une rubrique présente dans l'analyse peut manquer dans la base tant que la modification automatique des fichiers n'a pas été passée.
Créer une facture pour un client final enchaîne la création de la facture, l'ajout de ses lignes, la finalisation, la création de l'intention de paiement, puis sa confirmation. C'est ce qui rend la tâche planifiée lourde en appels réseau, et c'est pourquoi elle est le point sensible du dispositif.
Customer.IDCustomerLead désigne le parent commercial dans la
hiérarchie des clients. Les champs …Lead côté Stripe désignent tout autre chose —
un device associé. Aucun rapport entre les deux.
La technologie n'est plus certifiée sur le matériel récent. Elle ne doit être exposée nulle part : ni dans l'affichage du type de réseau au portail, ni dans la documentation. La cible est LTE-M et NB-IoT.
À savoir avant de lire le code et de s'étonner.
Une partie de ce qui est décrit ci-dessus n'est pas encore
implémentable : la cardinalité n'autorise qu'une seule ligne d'abonnement par jauge, les
rubriques IDStripePrices, IsResilie et IDStripeInvoices
n'existent pas, et les deux fichiers d'historique ne sont pas créés. Concrètement, le changement
de plan du cas F ne peut pas fonctionner comme écrit. Ces modifications font l'objet d'un
document dédié.
Le passage à la facture consolidée était en développement en juin, une partie en local. Ce qui tourne aujourd'hui en production sur la partie gestionnaire — facture unique ou factures séparées, récurrence Stripe retirée ou non — n'est pas connu de ce document. C'est la première chose à faire confirmer.
Les modifications 4 et 8 stockent la composition de chaque facture, jauge par jauge. L'écran qui l'affiche au gestionnaire quand il ouvre une facture reste à faire — c'est lui qui rend la ligne de prorata consolidée défendable sans intervention humaine.
ActivityState ne porte aujourd'hui qu'un état technique de
fonctionnement, sans lien avec la facturation. Les six statuts conçus en juin n'existent ni dans
la base ni dans le code, et le champ est écrit par les services de Serge : c'est un arbitrage à
trois, pas une modification de l'analyse. Détail en §6.