À propos de ZakatUZ
Inspiration
La zakat al-fitr est une aumône obligatoire versée avant la prière de l'Aïd. Chaque année, deux problèmes reviennent :
- Le montant. Il correspond à la valeur d'un repas de base, dépend du pays (≈ 15 CAD au Canada, 7 € en France…) et change tous les ans — on paie souvent « au pif » et en retard.
- La confiance. Une fois l'argent envoyé à une association, plus aucune visibilité : combien part en frais, un pauvre a-t-il vraiment reçu quelque chose ?
On a voulu un seul outil qui calcule le bon montant automatiquement et rend chaque don vérifiable jusqu'au bénéficiaire, sans avoir à faire confiance à une boîte noire.
What it does
- Calcul instantané de la zakat selon le pays et le nombre de personnes à charge, au barème figé au moment du paiement.
- Paiement par carte, PayPal ou mode démo. Le donateur obtient des jetons de zakat (DZ), achetés à un bénéficiaire en attente ou émis par la plateforme.
- Le bénéficiaire encaisse en vrai argent (PayPal, carte). Il vend son droit à la zakat plutôt que de mendier.
- Vérification par paliers : e-mail → pièce d'identité validée par un admin → parrainage d'une association reconnue, avec des plafonds mensuels anti-abus.
- Ventes directes nominatives : un donateur peut acheter les DZ d'une personne précise (accord requis, refus possible).
- Transferts de DZ avec un message, visibles côté bénéficiaire.
- Répartition équitable du fonds commun (DZ émis sans vendeur) via un job quotidien pondéré.
- Registre comptable public : un bouton « Vérifier la chaîne » recalcule toute la comptabilité et confirme que rien n'a été altéré.
How we built it
Monorepo pnpm + Turborepo :
| Partie | Stack |
|---|---|
| API | NestJS 12 — 15 modules, Prisma 5.22, SQLite (un fichier, pas de Docker) |
| Front | React 19 + Vite 8 — SPA, 4 espaces par rôle + landing |
packages/money |
type Money immuable, arithmétique bigint en unités mineures |
packages/ledger |
moteur comptable pur, testé isolément (property-based) |
Le cœur — un registre en partie double, append-only. Une transaction contient $N$ écritures ; pour chaque actif $a$ (CAD, EUR, DZ:124…) :
$$\forall a,\quad \sum_{e\,\in\,\text{transaction}} \text{montant}(e,a) = 0$$
Le registre partant d'un état vide, l'invariant global est préservé en permanence :
$$\forall a,\quad \sum_{e\,\in\,\text{registre}} \text{montant}(e,a) = 0$$
Chaque transaction est chaînée par hachage :
$$h_n = \text{SHA256}\big(h_{n-1}\;\Vert\;\text{canonicalJSON}(T_n)\big)$$
Modifier une écriture passée casse tous les hachages suivants, et verifyChain() le détecte. Une clé d'idempotence sur chaque écriture monétaire empêche tout double comptage.
Répartition du pool primaire — score par bénéficiaire $i$ :
$$\text{score}_i = \frac{\text{attente}_i}{\max_j \text{attente}_j} \;+\; \frac{\text{palier}_i}{2} \;+\; \frac{\text{capacité restante}_i}{\max_j \text{capacité}_j}$$
Part proportionnelle au score, plafonnée par la capacité mensuelle (surplus redistribué), avec un montant minimum par personne.
Abstractions clés : LedgerStore injecté (indépendant de la base) ; PaymentGateway / PayoutGateway avec des implémentations Fake, Stripe, PayPal activées par variables d'environnement — sans clé API, tout retombe sur le mode démo, la chaîne complète reste démontrable.
Challenges we ran into
- Le BOM Windows.
Set-Content/Out-Fileen PowerShell ajoutent un caractère invisible qui faisait planter la validation du schéma Prisma (P1012). Il a fallu générer chaque fichier texte sans BOM. - Prisma 8 est sorti en RC pendant le hackathon, avec une CLI entièrement différente et incompatible avec tous les tutoriels — on s'est verrouillé sur la 5.22.0.
- Les exemples comptables du doc de référence ne s'équilibraient pas : ils sommaient à
+4680 CADau lieu de0(l'encaissement n'avait pas de contrepartie). On a redessiné le plan de comptes — ajout d'un compteexternal:{user}pour le monde bancaire et d'un compte de compensationplatform:float— pour que chaque transaction boucle vraiment à zéro. - SQLite n'a pas
SELECT … FOR UPDATE SKIP LOCKED. Deux donateurs achetant la même offre ne doivent pas réussir tous les deux : on a implémenté un décrément conditionnel (UPDATE … WHERE dzRemaining >= n), et si une offre disparaît entre le devis et le paiement, la tranche manquante bascule automatiquement sur le marché primaire. - Node exécute le TypeScript nativement, mais pas tout. Le type-stripping refuse les parameter properties et les
enum— les packages partagés ont dû être écrits en TS « effaçable ». - L'achat multi-offres : servir « 2 DZ chez A + 2 chez B + 1 généré » sur un seul paiement, en découpant zakat et taxe au prorata sans perdre une unité mineure (méthode du plus grand reliquat).
Accomplishments that we're proud of
- Un registre comptable réellement inviolable et vérifiable en un clic, avec un test de propriété qui tient : après 200 opérations aléatoires, chaque actif somme toujours à 0.
- La boucle complète donateur → pauvre → argent réel est démontrable de bout en bout sans aucune clé API (Stripe/PayPal en fallback simulé).
- 15 modules NestJS, ~53 routes, 4 espaces UI par rôle et une landing — en quelques jours.
- Un modèle de comptes corrigé qui, contrairement à la spec de départ, s'équilibre vraiment.
- Une répartition du pool pondérée et éthique (ancienneté + vérification + besoin), pas « au prorata aveugle ».
- Le moteur critique isolé dans une bibliothèque pure, sans framework ni base — trivial à tester et à auditer.
What we learned
- La comptabilité en partie double est un super-pouvoir. Un invariant unique — « tout somme à zéro » — vérifié en continu transforme une classe entière de bugs d'argent en erreurs impossibles.
- Écrire le composant critique comme une lib pure (sans NestJS, sans Prisma) a été le meilleur investissement : tests property-based faciles, raisonnement simple.
- Modéliser les questions non techniques d'abord : « qu'est-ce qu'un DZ juridiquement ? », « que devient un DZ dormant ? », « la taxe se stocke-t-elle en % ou en montant figé ? » — chaque réponse changeait l'architecture.
- Des abstractions minces battent les intégrations complètes pour un MVP :
FakePaymentProvider+FakePayoutProvideront suffi à prouver toute la chaîne.
What's next for ZakatUZ
- Brancher Stripe.js (collecte carte réelle) + vérification de signature webhook sur le body brut.
- Migration PostgreSQL + store ledger transactionnel pour une atomicité complète offre + registre.
- Anti-fraude bénéficiaires : empreinte d'appareil, unicité IBAN/téléphone, détection des cycles donateur ↔ bénéficiaire.
- Page publique de transparence avec ancrage Merkle quotidien de la racine du registre.
- i18n
fr/en/aravec support RTL, et validation d'un référent religieux sur le point de fiqh.
Log in or sign up for Devpost to join the conversation.