À 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 :

  1. 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.
  2. 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-File en 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 CAD au lieu de 0 (l'encaissement n'avait pas de contrepartie). On a redessiné le plan de comptes — ajout d'un compte external:{user} pour le monde bancaire et d'un compte de compensation platform: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 + FakePayoutProvider ont 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 / ar avec support RTL, et validation d'un référent religieux sur le point de fiqh.

Built With

Share this project:

Updates

Submission history