Nel settore telco, le controversie sulle fatture sono una costante. Un operatore contesta un importo, l'ufficio amministrativo cerca il foglio Excel del mese scorso, e nel frattempo i dati sono stati già sovrascritti. Carrier HUB è stato progettato per eliminare questa situazione alla radice: ogni voce di ricavo è tracciabile fino alla regola che l'ha generata, indipendentemente da quando viene consultata.
Il problema: i billing engine sono scatole nere
I sistemi di fatturazione tradizionali calcolano un importo e lo salvano. Se il calcolo cambia — per una variazione di tariffe, un'eccezione cliente o un errore corretto — non c'è modo di sapere perché la fattura del mese precedente riportava un valore diverso.
Carrier HUB risolve questo con tre componenti distinte che lavorano insieme: uno stack di regole ispezionabile, un ledger immutabile per voce, e un sistema di versioning delle regole con fingerprinting delle fatture.
Lo stack di regole: BaseBillingRule
Il calcolo di ogni fattura parte da uno stack ordinato di regole, ognuna delle quali eredita da BaseBillingRule. Ogni regola riceve un BillingContext — un oggetto che porta la sessione DB, il cliente, la fattura, il report traffico, e il dict dei parametri policy — e lo modifica in modo controllato.
class BaseBillingRule:
name: str
def evaluate(self, context: BillingContext) -> None:
raise NotImplementedError
Lo stack è costruito in modo deterministico da BillingCalculator._build_rule_stack(). L'ordine è fisso: prima si calcolano i canoni base, poi le attivazioni, poi le voci extra. Ogni regola scrive solo i campi di sua competenza. Il risultato finale è la somma di contributi separati, ognuno con una firma.
Il ledger: LedgerEntry e context.log_trace
Ogni regola può registrare la propria decisione in due modi:
context.log_trace(rule_name, description, amount) scrive nel trace della fattura una riga leggibile che spiega perché è stato calcolato quel valore. Ad esempio:
NumeroVerdeRule → "Numero Verde da invoice.importo_numero_verde" → 0.00
AttivazioniRule → "Attivazioni VDSL: 1 connessione Fibra 1G" → 120.00
LedgerEntry è più strutturato: ogni voce ha tipo (RICAVO o COSTO), importo, codice articolo ERP, e un flag exportable che determina se la voce concorre al totale export. L'ExportPolicyEngine usa questo ledger per costruire il pacchetto dati verso il sistema contabile, separando i ricavi esportabili dai costi interni.
@dataclass
class LedgerEntry:
entry_type: Literal["RICAVO", "COSTO"]
label: str
amount: Decimal
article_code: str | None
exportable: bool
Il totale di export non è mai calcolato sommando tutti i ricavi: è la somma dei soli LedgerEntry con exportable=True e entry_type="RICAVO". Se una voce non deve uscire verso l'ERP — per policy, per eccezione o per azzeramento esplicito — basta marcarla exportable=False nel ledger, e il totale cambia di conseguenza senza toccare il calcolo base.
Versioning e validità temporale delle regole
Il modello ExportArticleRule gestisce la mappatura tra le voci di billing e gli articoli del sistema contabile. Con l'introduzione di Fase 2, ogni regola porta quattro campi aggiuntivi:
version(intero) — incrementato ad ogni modifica, mai decrementatostatus—draft,activeoarchivedvalid_from/valid_to— la finestra temporale in cui la regola è applicabile
Quando viene generato un export, il resolver applica solo le regole con status = active e la cui finestra di validità copre la data di competenza del cliente. Questo significa che una regola archiviata a febbraio non influisce sul ricalcolo di gennaio: il sistema determina il set corretto in base alla data, non in base allo stato attuale.
def _filter_rules_by_date(
rules: list[ExportArticleRule],
competenza_date: date,
) -> list[ExportArticleRule]:
result = []
for r in rules:
vf = getattr(r, "valid_from", None)
vt = getattr(r, "valid_to", None)
if vf is not None and vf > competenza_date:
continue
if vt is not None and vt < competenza_date:
continue
result.append(r)
return result
Il filtro avviene in memoria (il DB carica già solo le regole active), una volta per ogni cliente in base alla sua data di competenza effettiva — che può differire tra clienti con fatturazione anticipata e posticipata nello stesso ciclo mensile.
Il fingerprint: article_rules_hash
Per ogni fattura esportata, Carrier HUB calcola un fingerprint SHA-256 (troncato a 16 caratteri hex) del set di regole che è stato effettivamente applicato:
pairs = sorted((r.id, r.version) for r in active_rules)
hash = sha256(json.dumps(pairs).encode()).hexdigest()[:16]
Questo hash viene scritto su MonthlyInvoice.article_rules_hash al momento dell'export. Il risultato pratico: se in futuro un cliente contesta una fattura, basta leggere il fingerprint e confrontarlo con i set di regole versionate nel DB per ricostruire esattamente quale configurazione ha prodotto quell'importo.
Policy configurabile: separare le decisioni dal codice
Le scelte di calcolo non hardcoded nelle regole — come la modalità di gestione delle attivazioni, il comportamento del Numero Verde o la sorgente del costo fornitore VDSL — sono esposte come parametri in resolve_billing_policy(). Ogni profilo runtime può specificare valori diversi da quelli di default, e le regole li leggono da context.policy senza logica condizionale sparsa nel codice:
if context.policy.get("numero_verde_mode", "standard") == "disabled":
context.importo_numero_verde = Decimal("0.00")
context.log_trace(self.name, "Numero Verde disabilitato da policy", Decimal("0"))
return
Cambiare una policy non richiede un deploy. Richiede una modifica alla configurazione del profilo runtime e, se necessario, un ricalcolo della fattura — che produce un nuovo fingerprint, tracciabile.
Conclusioni
Un billing engine non è mai davvero "funzionante" se non è anche spiegabile. In Carrier HUB, la domanda "perché questo importo?" ha sempre una risposta: il trace, il ledger, il fingerprint e la cronologia versioni delle regole. Non è un'aggiunta a posteriori — è il modo in cui il sistema è stato costruito.
Vuoi vedere il motore applicato al tuo ciclo carrier? Richiedi una Demo.
