Passer au contenu principal
Apprenez à construire une application de coffre-fort d’épargne numérique avec Yabetoo, inspirée de plateformes comme SafeLock. Ce guide couvre la création de coffres, les dépôts, les déblocages programmés et le traitement des retraits.

Aperçu

Les plateformes d’épargne numériques doivent gérer :
  • Des coffres d’épargne par objectifs avec périodes de blocage
  • Les dépôts via mobile money
  • Les déblocages automatiques et manuels
  • Le traitement des retraits avec frais
  • Les calculs d’intérêts ou récompenses
  • La gestion multi-coffres par utilisateur

Comment fonctionne SafeLock

SafeLock est une application mobile de gestion financière qui aide les utilisateurs à épargner grâce à des “coffres” - des conteneurs d’épargne verrouillés liés à des objectifs personnels avec des dates de déblocage prédéfinies. Fonctionnalités clés :
  • Suivi des objectifs : Plusieurs coffres pour différents objectifs
  • Intégration Mobile Money : Support MTN et Airtel Money
  • Déblocage flexible : Déblocage auto à maturité ou retrait anticipé avec frais
  • Support hors ligne : Accès au compte sans internet

Architecture

Implémentation

1. Modèles de données

Définissez les structures de coffres et transactions :
interface Vault {
  id: string;
  userId: string;
  name: string;
  goal: string;
  targetAmount: number;
  currentAmount: number;
  currency: string;
  unlockDate: Date;
  status: 'active' | 'matured' | 'withdrawn' | 'cancelled';
  autoWithdraw: boolean;
  createdAt: Date;
  unlockedAt?: Date;
  withdrawnAt?: Date;
}

interface VaultTransaction {
  id: string;
  vaultId: string;
  userId: string;
  type: 'deposit' | 'withdrawal' | 'early_withdrawal' | 'interest';
  amount: number;
  fee: number;
  netAmount: number;
  paymentIntentId?: string;
  disbursementId?: string;
  status: 'pending' | 'completed' | 'failed';
  createdAt: Date;
}

// Configuration des frais
const FEES = {
  EARLY_WITHDRAWAL_PERCENT: 3,  // 3% de frais pour retrait anticipé
  MATURITY_WITHDRAWAL_PERCENT: 0, // Pas de frais à maturité
  MINIMUM_DEPOSIT: 500,  // 500 XAF minimum
  MINIMUM_VAULT_PERIOD_DAYS: 7  // Minimum 7 jours de blocage
};

2. Créer un coffre d’épargne

Permettre aux utilisateurs de créer des coffres par objectif :
import Yabetoo from '@yabetoo/sdk-js';

const yabetoo = new Yabetoo(process.env.YABETOO_SECRET_KEY!);

async function createVault(
  userId: string,
  vaultData: {
    name: string;
    goal: string;
    targetAmount: number;
    unlockDate: Date;
    autoWithdraw: boolean;
    initialDeposit?: number;
  }
) {
  const user = await db.users.findById(userId);

  // Valider la date de déblocage
  const minUnlockDate = new Date();
  minUnlockDate.setDate(minUnlockDate.getDate() + FEES.MINIMUM_VAULT_PERIOD_DAYS);

  if (vaultData.unlockDate < minUnlockDate) {
    throw new Error(`Le coffre doit être bloqué au moins ${FEES.MINIMUM_VAULT_PERIOD_DAYS} jours`);
  }

  // Créer le coffre
  const vault: Vault = {
    id: generateVaultId(),
    userId,
    name: vaultData.name,
    goal: vaultData.goal,
    targetAmount: vaultData.targetAmount,
    currentAmount: 0,
    currency: 'XAF',
    unlockDate: vaultData.unlockDate,
    status: 'active',
    autoWithdraw: vaultData.autoWithdraw,
    createdAt: new Date()
  };

  await db.vaults.create(vault);

  // Gérer le dépôt initial si fourni
  let paymentIntent = null;
  if (vaultData.initialDeposit && vaultData.initialDeposit >= FEES.MINIMUM_DEPOSIT) {
    paymentIntent = await createDeposit(vault.id, vaultData.initialDeposit);
  }

  return { vault, paymentIntent };
}

3. Dépôt sur un coffre

Traiter les dépôts via mobile money :
async function createDeposit(vaultId: string, amount: number) {
  if (amount < FEES.MINIMUM_DEPOSIT) {
    throw new Error(`Dépôt minimum: ${FEES.MINIMUM_DEPOSIT} XAF`);
  }

  const vault = await db.vaults.findById(vaultId);
  if (vault.status !== 'active') throw new Error('Le coffre n\'est pas actif');

  const user = await db.users.findById(vault.userId);

  // Créer l'intention de paiement
  const intent = await yabetoo.payments.create({
    amount,
    currency: 'XAF',
    description: `Dépôt vers ${vault.name}`,
    metadata: {
      vaultId,
      userId: vault.userId,
      vaultName: vault.name,
      type: 'vault_deposit'
    }
  });

  // Créer la transaction en attente
  await db.vaultTransactions.create({
    id: generateTransactionId(),
    vaultId,
    userId: vault.userId,
    type: 'deposit',
    amount,
    fee: 0,
    netAmount: amount,
    paymentIntentId: intent.id,
    status: 'pending',
    createdAt: new Date()
  });

  return intent;
}

4. Gestionnaire Webhook pour les dépôts

Traiter les dépôts réussis :
app.post('/webhooks/yabetoo', async (req, res) => {
  const event = req.body;

  if (event.type === 'payment_intent.succeeded') {
    if (event.data.metadata.type === 'vault_deposit') {
      await handleDepositSuccess(event.data);
    }
  }

  res.json({ received: true });
});

async function handleDepositSuccess(data: any) {
  const { vaultId, userId } = data.metadata;
  const amount = data.amount;

  // Mettre à jour la transaction
  await db.vaultTransactions.updateByPaymentIntent(data.id, {
    status: 'completed'
  });

  // Mettre à jour le solde du coffre
  await db.vaults.incrementAmount(vaultId, amount);

  // Mettre à jour l'épargne totale de l'utilisateur
  await db.users.incrementTotalSavings(userId, amount);

  const vault = await db.vaults.findById(vaultId);

  // Envoyer notification
  await sendNotification(userId, {
    type: 'deposit_success',
    title: 'Dépôt réussi',
    message: `${amount} XAF ajoutés à votre coffre "${vault.name}". Nouveau solde: ${vault.currentAmount} XAF`,
    vaultId
  });

  // Vérifier si l'objectif est atteint
  if (vault.currentAmount >= vault.targetAmount) {
    await sendNotification(userId, {
      type: 'target_reached',
      title: 'Objectif atteint ! 🎉',
      message: `Félicitations ! Vous avez atteint votre objectif d'épargne pour "${vault.name}" !`,
      vaultId
    });
  }
}

5. Traitement de la maturité des coffres

Gérer les déblocages quand la date de maturité est atteinte :
// Exécuté quotidiennement via cron
async function processMaturedVaults() {
  const maturedVaults = await db.vaults.findMany({
    where: {
      status: 'active',
      unlockDate: { lte: new Date() }
    }
  });

  for (const vault of maturedVaults) {
    // Mettre à jour le statut
    await db.vaults.update(vault.id, {
      status: 'matured',
      unlockedAt: new Date()
    });

    // Notifier l'utilisateur
    await sendNotification(vault.userId, {
      type: 'vault_matured',
      title: 'Coffre débloqué ! 🔓',
      message: `Votre coffre "${vault.name}" est maintenant débloqué ! Vous pouvez retirer ${vault.currentAmount} XAF sans frais.`,
      vaultId: vault.id
    });

    // Traiter le retrait automatique si activé
    if (vault.autoWithdraw && vault.currentAmount > 0) {
      await processWithdrawal(vault.id, vault.currentAmount, false);
    }
  }
}

6. Traitement des retraits

Gérer les retraits à maturité et anticipés :
async function processWithdrawal(
  vaultId: string,
  amount: number,
  isEarly: boolean = false
) {
  const vault = await db.vaults.findById(vaultId);
  const user = await db.users.findById(vault.userId);

  if (amount > vault.currentAmount) {
    throw new Error('Solde insuffisant dans le coffre');
  }

  // Vérifier si retrait anticipé
  if (vault.status === 'active' && new Date() < vault.unlockDate) {
    isEarly = true;
  }

  // Calculer les frais
  const feePercent = isEarly
    ? FEES.EARLY_WITHDRAWAL_PERCENT
    : FEES.MATURITY_WITHDRAWAL_PERCENT;
  const fee = Math.round(amount * (feePercent / 100));
  const netAmount = amount - fee;

  // Créer la transaction de retrait
  const transaction = {
    id: generateTransactionId(),
    vaultId,
    userId: vault.userId,
    type: isEarly ? 'early_withdrawal' : 'withdrawal',
    amount,
    fee,
    netAmount,
    status: 'pending',
    createdAt: new Date()
  };

  await db.vaultTransactions.create(transaction);

  // Traiter le décaissement
  const disbursement = await yabetoo.disbursements.create({
    amount: netAmount,
    currency: 'XAF',
    firstName: user.firstName,
    lastName: user.lastName,
    paymentMethodData: {
      type: 'momo',
      momo: {
        msisdn: user.phone,
        country: 'cg',
        operatorName: user.operatorName
      }
    }
  });

  // Mettre à jour le coffre
  await db.vaults.update(vaultId, {
    currentAmount: vault.currentAmount - amount,
    status: vault.currentAmount - amount === 0 ? 'withdrawn' : vault.status
  });

  return { transaction, disbursement, fee, netAmount };
}

7. Statistiques du tableau de bord

Fournir aux utilisateurs des insights sur leur épargne :
async function getUserDashboard(userId: string) {
  const user = await db.users.findById(userId);
  const vaults = await db.vaults.findByUser(userId);
  const transactions = await db.vaultTransactions.findByUser(userId);

  const activeVaults = vaults.filter(v => v.status === 'active');

  // Progression vers les objectifs
  const vaultProgress = activeVaults.map(vault => ({
    vaultId: vault.id,
    name: vault.name,
    goal: vault.goal,
    currentAmount: vault.currentAmount,
    targetAmount: vault.targetAmount,
    progressPercent: Math.round((vault.currentAmount / vault.targetAmount) * 100),
    daysUntilUnlock: Math.ceil(
      (vault.unlockDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
    )
  }));

  return {
    summary: {
      totalSavings: user.totalSavings,
      activeVaults: activeVaults.length,
      totalDeposits: transactions.filter(t => t.type === 'deposit' && t.status === 'completed').reduce((s, t) => s + t.amount, 0)
    },
    vaultProgress,
    recentTransactions: transactions.slice(0, 10)
  };
}

Flux utilisateur

1

Créer un compte

L’utilisateur s’inscrit avec son numéro de téléphone (MTN ou Airtel).
2

Créer un coffre

L’utilisateur crée un coffre d’épargne avec un objectif, un montant cible et une date de déblocage.
3

Effectuer des dépôts

L’utilisateur dépose de l’argent via mobile money à tout moment.
4

Suivre la progression

L’utilisateur surveille sa progression vers ses objectifs dans l’app.
5

Maturité du coffre

Quand la date de déblocage arrive, le coffre devient disponible pour retrait.
6

Retirer

L’utilisateur retire les fonds vers son compte mobile money.

Structure des frais

ActionFrais
DépôtGratuit
Retrait à maturitéGratuit
Retrait anticipé3%
Maintenance compteGratuit

Bonnes pratiques

Créez des défis hebdomadaires ou mensuels pour encourager les dépôts réguliers.
Permettez aux utilisateurs de partager leur progression (anonymement) pour créer de la responsabilité.
Laissez les utilisateurs ajuster les montants cibles et les dates si les circonstances changent.
Envisagez d’offrir de petites récompenses pour les objectifs atteints à temps.

Modèles de notifications

const NOTIFICATION_TEMPLATES = {
  deposit_success: (amount: number, vaultName: string, balance: number) =>
    `✅ ${amount} XAF déposés dans "${vaultName}". Solde: ${balance} XAF`,

  vault_matured: (vaultName: string, amount: number) =>
    `🔓 Votre coffre "${vaultName}" est débloqué ! Retirez ${amount} XAF sans frais.`,

  target_reached: (vaultName: string) =>
    `🎉 Objectif atteint pour "${vaultName}" ! Continuez ou attendez le déblocage.`,

  savings_reminder: (vaultName: string, remaining: number, days: number) =>
    `💰 "${vaultName}" a besoin de ${remaining} XAF dans ${days} jours. Déposez maintenant !`
};

Ressources associées