Aller au contenu principal

Boutique en ligne : retour sur votre site

Votre serveur crée un lien de paiement par commande, y redirige le client, et le client revient sur votre site une fois le paiement terminé. C'est le parcours recommandé pour une boutique en ligne.

Le parcours​

  1. Créez un lien à montant fixe pour la commande, avec redirect_url (la page de retour) et l'identifiant de la commande dans metadata. Enregistrez l'identifiant du lien (id) avec la commande. Une expiration courte (expires_at, par exemple une heure) évite qu'un ancien lien soit payé après une modification du panier.
  2. Redirigez le client vers payment_url.
  3. Le client revient sur redirect_url, avec des paramètres ajoutés à l'adresse : payment_id, payment_token, status, amount, currency, phone.
  4. Votre serveur vérifie le paiement avec GET /api/v1/payments/{payment_id}/ : la réponse contient payment_link_id, l'identifiant du lien d'origine, à comparer avec celui de la commande.
  5. Le webhook confirme le paiement de façon définitive, même si le client ne revient jamais sur votre site (navigateur fermé, coupure réseau).
Ne validez jamais une commande sur les paramètres de l'adresse

Le client peut les modifier dans sa barre d'adresse, par exemple pour présenter le paiement d'une autre commande, moins chère. Interrogez toujours l'API, et contrôlez que le paiement provient du lien de cette commande (payment_link_id), qu'il est réussi, réel, et du bon montant.

Implémentation​

views.py · Django
from datetime import timedelta
from decimal import Decimal

from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone


def checkout(request, order_id):
order = get_object_or_404(Order, id=order_id, customer=request.user)
resp = session.post(f"{API_BASE}/pay-links/", json={
"title": f"Commande #{order.id}",
"is_amount_fixed": True,
"fixed_amount": str(order.total),
"expires_at": (timezone.now() + timedelta(hours=1)).isoformat(),
"redirect_url": request.build_absolute_uri(f"/commandes/{order.id}/retour/"),
"metadata": {"order_id": str(order.id)},
}, timeout=30)
resp.raise_for_status()
link = resp.json()
order.payment_link_id = link["id"] # pour vérifier le paiement au retour
order.save(update_fields=["payment_link_id"])
return redirect(link["payment_url"])


def payment_return(request, order_id):
order = get_object_or_404(Order, id=order_id, customer=request.user)
payment_id = request.GET.get("payment_id", "")

resp = session.get(f"{API_BASE}/payments/{payment_id}/", timeout=30)
if resp.status_code != 200:
return render(request, "paiement_en_attente.html", {"order": order})
payment = resp.json()

paid = (
payment["payment_link_id"] == str(order.payment_link_id) # le lien de CETTE commande
and payment["status"] == "SUCCEEDED"
and not payment["test"] # en production
and Decimal(payment["amount"]) == order.total
)
if paid:
order.mark_paid(payment_id=payment["id"]) # idempotent : sans effet si déjà payée
return render(request, "paiement_reussi.html", {"order": order})
if payment["status"] == "PENDING":
return render(request, "paiement_en_attente.html", {"order": order})
return render(request, "paiement_echoue.html", {"order": order})

Si le client ne revient pas​

Le client peut fermer son navigateur juste après avoir confirmé sur son téléphone. Dans ce cas, seul le webhook vous prévient : il contient link_metadata.order_id, l'identifiant de commande que vous avez donné au lien. Traitez le webhook et la page de retour avec la même fonction, qui ne fait rien si la commande est déjà payée.

Un paiement « en attente » (PENDING), fréquent avec Moov Money, se confirme en général en quelques secondes : affichez une page d'attente qui se recharge, ou un message invitant le client à vérifier ses e-mails.