Le contexte marché
Au Sénégal, on paie par mobile money (Orange Money, Wave), pas par carte. D'où SenePay, qui agrège ces moyens. Le freemium : contenu de base gratuit (le moteur du trafic), fonctionnalités premium à 1 000 FCFA.
L'architecture du paywall
Le premium est un flag avec expiration, contrôlé par un voter Symfony :
public function hasPremiumAccess(): bool
{
return $this->isPremium
&& $this->premiumUntil !== null
&& $this->premiumUntil > new \DateTimeImmutable();
}
class PremiumVoter extends Voter
{
protected function voteOnAttribute(string $attr, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
return $user instanceof User && $user->hasPremiumAccess();
}
}
$this->denyAccessUnlessGranted('PREMIUM_ACCESS');
Le webhook : ne jamais faire confiance au navigateur
Le point critique : on active le premium uniquement sur webhook serveur-à-serveur, jamais sur le retour navigateur (falsifiable).
#[Route('/webhook/senepay', name: 'senepay_webhook', methods: ['POST'])]
public function webhook(Request $request): Response
{
$data = json_decode($request->getContent(), true);
if (!$this->senePay->verifySignature($request)) {
return new Response('Invalid signature', 403);
}
if ($this->paymentRepo->isAlreadyProcessed($data['reference'])) {
return new Response('OK', 200); // idempotence
}
if ($data['status'] === 'success') {
$user = $this->resolveUser($data['reference']);
$user->grantPremium(new \DateInterval('P30D'));
$this->em->flush();
}
return new Response('OK', 200);
}
Les trois règles d'or
- Vérifier la signature du webhook.
- Idempotence : traiter une transaction une seule fois.
- Source de vérité = le serveur. Le retour navigateur, c'est de l'UX.
Ce que je retiens
- Adapter le moyen de paiement au marché : mobile money au Sénégal.
- Un voter Symfony centralise proprement le contrôle d'accès premium.
- Le webhook est la seule source de vérité.