Modèle de données

Les douze tables lrvo_billing_, leurs colonnes, leurs contraintes et ce que chaque index protège.

Douze tables, créées dans l'ordre ci-dessous par lrvo-db init et supprimées dans l'ordre inverse. La création est idempotente : une table existante est ignorée, jamais modifiée.

Unités

GrandeurTypeUnité
MontantINTEGERCent
QuantitéINTEGERMillième
Taux de taxeINTEGERPartie par million

Aucune colonne DECIMAL, NUMERIC ni flottante.

lrvo_billing_counter

Alimente la numérotation. Clé primaire (scope, period), où scope vaut invoice, transaction ou client, et period l'année, ou 0 pour une séquence continue.

La valeur suivante s'obtient par une écriture atomique qui prend le même verrou de ligne qu'un SELECT ... FOR UPDATE, tenu jusqu'à la validation de la transaction appelante. Deux émissions simultanées ne peuvent donc pas obtenir le même numéro.

lrvo_billing_job_lock

Un enregistrement par tâche planifiée : job_name en clé primaire, locked_until, locked_by, plus le résultat du dernier passage. Voir Récurrences et rappels.

lrvo_billing_client

ColonneDétail
codeUnique, de la forme CLI-0001. Permanent.
user_idTEXT, facultatif, vers lrvo_auth_users(id), mis à NULL à la suppression du compte.
company_name, first_name, last_name, emailUne contrainte impose un nom d'entreprise, ou un prénom et un nom.
default_tax_profile, default_terms_daysValeurs reprises par les nouvelles factures.
is_activeUn client inactif sort des sélecteurs sans quitter l'historique.

user_id est TEXT parce que lrvo_auth_users.id l'est. Il est facultatif : un client peut être facturé sans jamais avoir de compte sur le site.

lrvo_billing_interac

Un enregistrement par client, clé primaire client_id. Question et réponse de sécurité, plus un drapeau indiquant si la réponse est chiffrée. Supprimée avec le client.

lrvo_billing_category

Arbre auto-référencé pour ranger le catalogue. slug unique, parent_id mis à NULL si le parent disparaît. Une contrainte bloque l'auto-parentage ; les boucles plus longues sont refusées dans le code, par un parcours des ancêtres avant l'écriture.

lrvo_billing_item

Le catalogue : sku unique et facultatif, unit_price_cents, unit_label, tax_profile, category_id, is_active.

Un article ne fige rien : une ligne de facture le copie par valeur. Modifier un prix n'affecte aucune facture existante.

lrvo_billing_recurrence

Créée avant les factures pour qu'une facture puisse pointer vers sa récurrence sans cycle de clés étrangères.

ColonneDétail
frequency_unit, frequency_valueday, week, month ou year, et le multiple.
anchor_dayJour du mois visé, entre 1 et 28.
lead_daysDélai de création avant l'échéance.
auto_issueÉmettre et envoyer, ou laisser en brouillon.
linesJSONB, le gabarit des lignes.
next_due_dateTout l'état de la récurrence.
end_date, occurrences_max, occurrences_doneConditions d'arrêt.

La dernière facture produite se lit par une requête sur recurrence_id, plutôt que d'être recopiée dans une colonne qui pourrait diverger.

lrvo_billing_invoice

ColonneDétail
invoice_number, tokenUniques, nuls tant que la facture est un brouillon.
statusdraft, sent, partial, paid ou cancelled.
issue_date, due_date, issued_at, paid_at, cancelled_atLe calendrier de la facture.
gst_rate_ppm, qst_rate_ppm, gst_number, qst_numberRecopiés à l'émission.
subtotal_cents, gst_cents, qst_cents, total_centsRecalculés à chaque écriture de lignes.
paid_centsRecalculé à chaque écriture de transaction.
interest_cents, interest_accrued_onTenus par la tâche d'intérêts.
reminder_lead_daysPoint de départ de l'échelle de rappels, propre à la facture.
notes, internal_notesLes premières vont sur le PDF, les secondes jamais.

Une contrainte impose que toute facture non brouillon porte un numéro, un jeton et une date d'émission. C'est le cycle de vie inscrit dans le schéma : aucun chemin de code ne peut produire une facture à moitié émise.

Un index partiel sur (due_date, id) restreint aux statuts sent et partial sert les tâches d'intérêts et de rappels, qui ne regardent jamais autre chose.

Le solde n'est pas stocké. Il vaut max(0, total_cents + interest_cents − paid_cents) et se calcule à la lecture.

lrvo_billing_invoice_line

Supprimée avec sa facture. item_id facultatif, mis à NULL si l'article disparaît du catalogue : la ligne garde son nom et son prix.

quantity_milli porte trois décimales, ce qui couvre les heures et les poids sans quitter l'arithmétique entière. Une quantité négative est une ligne de rabais, raison pour laquelle ni line_total_cents ni les totaux de la facture ne portent de contrainte de positivité.

lrvo_billing_transaction

ColonneDétail
referenceUnique, TRX-2026-0001.
kindpayment ou refund.
amount_centsPositif pour un paiement, négatif pour un remboursement, imposé par contrainte.
methodstripe, interac, cash, cheque, transfer ou other.
stripe_session_id, stripe_payment_intent_id, stripe_refund_idFacultatifs.
created_byL'administrateur qui a saisi, s'il y en a un.

Deux index uniques partiels, sur stripe_session_id et sur stripe_refund_id, forment la deuxième ligne de défense contre un double crédit, indépendante de la table d'événements.

La suppression d'une facture est bloquée tant qu'une transaction la référence, et le code n'autorise de toute façon la suppression que d'un brouillon.

lrvo_billing_reminder

Le registre des envois. kind vaut issue, before, overdue, admin_overdue ou receipt, et bucket identifie l'échelon.

L'index unique partiel (invoice_id, kind, bucket) WHERE success est le mécanisme d'idempotence des courriels : le créneau est réservé avant l'envoi, et un envoi raté marque la ligne en échec, ce qui libère le créneau pour le passage suivant.

lrvo_billing_stripe_event

Un enregistrement par événement reçu, event_id en clé primaire. Statut received, processed, ignored ou failed.

Stripe livre au moins une fois. Sans cette grille, un checkout.session.completed rejoué créditerait la facture deux fois.

Ordre de suppression

Exact inverse de la création : stripe_event, reminder, transaction, invoice_line, invoice, recurrence, item, category, interac, client, job_lock, counter.

Notes

  • La création étant idempotente, elle n'atteint jamais une base déjà déployée. Une table existante évolue par une migration versionnée du module, appliquée par npx lrvo-db migrate. Voir Écrire une migration.
  • Aucune table ne référence celles d'un autre module. La seule dépendance externe est lrvo_auth_users, facultative.

Sur cette page