pablo.mourato
PROJETS / QUITTEÉTUDE DE CAS · 03 / 05

Quitte.

Les dépenses d'un voyage, d'une colocation ou d'une soirée, partagées au centime près : chacun note ce qu'il avance, Quitte dit qui doit combien à qui, avec le moins de virements possible. Conçu, développé, testé et déployé seul.

RÔLE
Conception, back-end, front-end, déploiement
CONTEXTE
Troisième projet full-stack du portfolio, en Python et Angular
ANNÉE · DURÉE
2026 · 2 semaines
STACK
FastAPI, SQLAlchemy 2, Alembic, PostgreSQL 16, Angular 22, TypeScript
Quitte, onglet Soldes d'un week-end entre amis : virements proposés pour être quittes et solde de chaque personne
01/ — CONTEXTE

Le besoin.

Après un week-end entre amis, les comptes se font souvent sur un coin de note : qui a avancé le chalet, qui a payé l'essence, combien chacun doit-il ? Entre les arrondis approximatifs et les « je te dois combien, déjà ? », on finit par perdre plus de temps que ne valent les sommes en jeu.

Quitte tient ces comptes pour tout le groupe : chacun note ce qu'il avance, l'application calcule les soldes au centime près et propose la liste de remboursements la plus courte. Pour mon portfolio, c'était l'occasion de montrer une troisième stack, Python et Angular, sur un problème où la justesse des calculs compte autant que l'interface.

OBJECTIF 1Des comptes justes au centime, quelle que soit la façon de partager
OBJECTIF 2Ajouter ses amis tout de suite, même s’ils n’ont pas encore de compte
OBJECTIF 3Se rembourser avec le moins de virements possible
02/ — ARCHITECTURE

Le calcul
au centre.

L'API FastAPI est découpée en trois couches. Les routes vérifient les droits et valident les entrées avec Pydantic ; les services chargent les données avec SQLAlchemy ; le domaine, c'est-à-dire répartir une dépense et équilibrer les comptes, tient en fonctions pures, sans base de données ni HTTP. C'est ce cœur que visent les tests de propriétés. Le front Angular, avec signals et sans zone.js, consomme l'API en REST.

Front AngularAngular 22 · signals · formulaires réactifs · SVG · Vercel
↓ REST + JWT↑ JSON, erreurs en français
API FastAPIPydantic · SQLAlchemy 2 · Alembic · Argon2 · Render
↓ SQL paramétré↑ agrégats GROUP BY
PostgreSQL 16Groupes, membres, dépenses, parts, remboursements · Neon
35 TESTS, DONT DES TESTS DE PROPRIÉTÉS (HYPOTHESIS), CONTRE UN VRAI POSTGRESQL
03/ — FONCTIONNALITÉS

Du ticket
au virement.

Noter une dépense prend quelques secondes ; Quitte tient les comptes de tout le groupe à jour.

Invitation par code ou par lien, et membres avec leur place à réclamer
01

Inviter, même sans compte

On ajoute ses amis dès la création du groupe. Chacun rejoint ensuite avec le lien et réclame sa place : « je suis Tom ».

Répartition par parts : Hugo compte pour deux
02

Trois façons de partager

À parts égales, par parts (2 pour un couple) ou au montant exact. L'aperçu des parts s'affiche pendant la saisie.

Virements proposés et confirmation d’un remboursement
03

Soldes et remboursements

Qui a payé quoi, la part de chacun, et les virements pour être quittes, à marquer comme faits en un clic.

Alerte : la dépense vient d’être modifiée par quelqu’un d’autre
04

Modifications simultanées

Si deux personnes modifient la même dépense, la seconde est prévenue et recharge la dernière version au lieu d'écraser la première.

Statistiques : total, part personnelle, répartition par catégorie et par mois
05

Statistiques et export

Répartition par catégorie, dépenses par mois, votre part, et un export CSV qui s'ouvre directement dans Excel.

Trois écrans de Quitte sur téléphone
06

Pensé pour le téléphone

Une dépense se note sur le moment, souvent debout à la caisse : l'interface est pensée d'abord pour le mobile, en thème clair ou sombre.

← FAITES DÉFILER →
04/ — DÉFIS TECHNIQUES

Ce qui a
résisté.

DÉFI 01

Des centimes qui tombent juste

LE PROBLÈME

10 € entre trois personnes, c'est 3,333… € chacun. Arrondir chaque part fait disparaître ou apparaître un centime, et les nombres à virgule flottante ajoutent leurs propres erreurs : 0,1 + 0,2 n'y vaut pas 0,3.

MA SOLUTION

Tous les montants sont des entiers en centimes. La répartition suit la méthode du plus fort reste : chacun reçoit la partie entière de sa part, puis les centimes restants vont aux plus grandes parties fractionnaires. Des tests de propriétés vérifient, sur des centaines de cas tirés au hasard, que la somme des parts est toujours exacte.

domain/splitting.py — extrait
# Méthode du plus fort reste : la somme des parts est toujours exacte
total = sum(weights.values())
for index, (key, weight) in enumerate(weights.items()):
    quotient, remainder = divmod(amount * weight, total)
    shares[key] = quotient
    remainders.append((-remainder, index, key))

# Les centimes restants vont aux plus grands restes
missing = amount - sum(shares.values())
for _, _, key in sorted(remainders)[:missing]:
    shares[key] += 1
DÉFI 02

Le moins de virements possible

LE PROBLÈME

Rembourser chaque dépense séparément multiplie les virements : à quatre, après neuf dépenses, chacun finirait par envoyer de petites sommes à tout le monde.

MA SOLUTION

Quitte calcule d'abord le solde de chacun, ce qu'il a payé moins sa part, puis fait rembourser le plus gros débiteur au plus gros créancier, et recommence. Chaque virement solde au moins une personne : jamais plus de n − 1 virements. Trouver le minimum absolu est un problème NP-difficile ; cet algorithme glouton donne le bon résultat dans l'immense majorité des cas réels.

domain/settlement.py — extrait simplifié
# Le plus gros débiteur rembourse le plus gros créancier, puis on recommence
while i < len(debtors) and j < len(creditors):
    debtor, owes = debtors[i]
    creditor, due = creditors[j]
    amount = min(owes, due)
    transfers.append(Transfer(debtor, creditor, amount))
    # … chaque virement solde au moins l’une des deux personnes
    if owes == amount: i += 1
    if due == amount: j += 1
DÉFI 03

Deux personnes, la même dépense

LE PROBLÈME

Léa corrige le montant d'une dépense pendant que Tom change qui y participe. Sans précaution, le dernier à enregistrer écrase le travail de l'autre, sans que personne ne s'en rende compte.

MA SOLUTION

Verrouillage optimiste : chaque dépense porte un numéro de version, que le client renvoie. Si quelqu'un a enregistré entre-temps, la mise à jour ne touche aucune ligne et l'API répond 409 ; le front propose de recharger la dernière version. Un test lance 6 modifications en même temps : une seule passe.

models.py et routers/expenses.py — extraits
# models.py : SQLAlchemy ajoute « WHERE version = n » à chaque mise à jour
version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
__mapper_args__ = {"version_id_col": version}

# routers/expenses.py : la version envoyée par le client doit être la bonne
if data.version != expense.version:
    raise conflict(STALE)
try:
    session.flush()
except StaleDataError:  # enregistré par quelqu’un d’autre entre-temps
    raise conflict(STALE) from None
05/ — RÉSULTATS

En ligne.

35tests, dont des tests de propriétés sur des centaines de cas générés
1 / 6modification acceptée quand 6 arrivent en même temps sur la même dépense
88 Kochargés au premier affichage, compressés ; les autres écrans à la demande

L'application est en ligne : on crée un groupe, on invite ses amis avec un lien et chacun note ses dépenses depuis son téléphone. Le code est public, avec un README qui détaille les choix techniques, la sécurité et les tests.

CE QUE J'EN RETIENS

Garantir une précision absolue dans les opérations financières en anticipant les erreurs d’arrondi, en isolant les calculs dans des fonctions pures et en s’appuyant sur des tests basés sur des propriétés pour valider leur fiabilité dans une grande variété de situations.