bouton Paypal

TUTORIEL BOUTON PAYPAL

Public: Développeurs web - HTML et PHP
Date publication: 15 avril 2024

Vous ouvrez une boutique en ligne?
À un moment donné le client devra payer.

Voici comment créer un bouton Paypal pour payer un produit ou une commande, avec l'API NVP/SOAP IPN.
Le client peut payer avec son compte Paypal, ou sa carte bleue s'il n'a pas de compte Paypal.

J'ai créé cette page parce que la doc Paypal est un vrai bordel.
Attention il faut être rigoureux et précis, à la moindre erreur ça plante.
Cependant ce n'est pas compliqué, il n'y a que deux fichiers HTML, un fichier PHP et une image.


CRÉER UN COMPTE PAYPAL BUSINESS

Paypal gère deux types de comptes utilisateur: Personnel et Business.
Il faut créer un compte Business pour recevoir les paiements (le nom doit correspondre à la carte d'identité pour éviter la fraude), et pour créer des comptes de test.


PHASE DE DÉVELOPPEMENT

Dans le Dashboard du compte Business, créer deux comptes Sandbox: un vendeur et un acheteur et leur donner $5000 à chacun

emailmot de passeNom
client@machin.frazertyuiJean Bon
vendeur@truc.frazertyuiJacques Attali (vendeur de pantalons)

Ensuite activer les messages IPN

FORMULAIRE HTML AVEC LE BOUTON

form.html
<meta charset="utf-8">
Blouson en daim<br>
74.35€ TTC<br>

<form name="_xclick" action="https://ipnpb.sandbox.paypal.com/cgi-bin/webscr" method="post">
<!--<form name="_xclick" action="https://www.paypal.com/cgi-bin/webscr" method="post">-->
    <input type="hidden" name="cmd" value="_xclick">

    <input type="hidden" name="business" value="vendeur@truc.fr">
    <!--<input type="hidden" name="business" value="votre-vraie-adresse@paypal-business.fr">-->

    <!--Référence commande générée par la boutique, ce qui permet à un tiers de payer la commande avec un autre email-->
    <input type="hidden" name="custom" value="AF05D5BA48E8877DAAF">
    <input type="hidden" name="item_name" value="Blouson en daim">
    <input type="hidden" name="amount" value="74.35">
    <input type="hidden" name="currency_code" value="EUR">
    <input type="hidden" name="lc" value="fr-FR">

    <!--The URL of the 150x50-pixel image displayed as your logo in the upper left corner of the PayPal checkout pages-->
    <input type="hidden" name="image_url" value="https://www.monsite.fr/boutique/paypal/harry.png">

    <!-- Instant Payment Notification messages script -->
    <input type="hidden" name="notify_url" value="https://www.monsite.fr/boutique/notify.php">
    <input type="hidden" name="rm" value="2"><!-- send POST to IPN-->

    <!--Page "Annuler le paiement et revenir au site"-->
    <input type="hidden" name="cancel_return" value="https://www.monsite.fr/boutique/">

    <!-- Après avoir effectué le paiement, quand le client clique sur "Retourner sur le site du marchand" dirige vers une page "Merci pour votre achat", ou "Mes commandes". -->
    <input type="hidden" name="return" value="https://www.monsite.fr/boutique/paypal/payment-ok.html">

    <input type="image" src="http://www.paypalobjects.com/fr_FR/i/btn/btn_buynow_LG.gif" border="0" name="submit" alt="Payer avec Paypal!">
</form>

Le formulaire contient l'email vendeur du sandbox dans le champ "business",
et les données de la commande "item_name" "custom" "amount" "currency_code".

Le champ "custom" est un identifiant unique de la commande généré par la boutique.
On pourrait faire sans ce champ optionnel, mais il permet à quelqu'un de payer avec une adresse paypal différente de celle de la boutique (ou permettre à un tiers de payer sa commande).

Ce bouton est du type _xclick <input type="hidden" name="cmd" value="_xclick">, il existe d'autres types de boutons Paypal

form.html ressemble à ceci:
Le bouton Paypal est un formulaire HTML

Plus tard dans le développement, form.html deviendra commande.php et PHP au moment d'envoyer le formulaire au client renseignera les champs Javascript peut faire la même chose.

SCRIPT PHP DE NOTIFICATION

Lorsque le client valide le paiement sur le site Paypal, Paypal invoque le script de notification sur votre serveur précisé dans "notify_url".

Ce script de notification,
Dans un 1er temps reçoit les données de la transaction en POST du serveur Paypal.
Dans un 2e temps, renvoie les données au serveur paypal pour lui dire "ok je suis bien un script IPN qui tourne, tu peux faire le paiement", puis
Dans un 3e temps reçoit de Paypal la réponse 'VERIFIED' ou 'INVALID'
et finalement envoie au serveur Paypal un en-tête HTTP 200 OK.

Si Paypal a effectué avec succès la transaction (VERIFIED) alors ce script de notification (à vous de voir ce dont vous aurez besoin): Si tout est bon, grâce au nom de la commande item_name et au champ custom le script peut récupérer la commande dans la base de données, la marquer comme payée et en attente d'expédition, mettre à jour le stock, envoyer un email au client et au vendeur...

Exemple officiel avec une classe IPN
Doc officelle: comment recevoir les données IPN

notify.php
<?php
/* Ce script écrit plusieurs fichiers log en mode append:
    log-error-php.log        erreurs PHP
    log-paypal                données IPN reçues et urldécodées
    log-erreur-transaction    en cas d'erreur de transaction

ATTENTION AUX LIGNES 118/119/120 qui doivent correspondre au formulaire HTML; et adapter si base de données*/

// SANDBOX
const VERIFY_URI = 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr'; 
const vendeur    = "vendeur@truc.fr";

/* PROD
const VERIFY_URI = 'https://ipnpb.paypal.com/cgi-bin/webscr';
const vendeur    = "maVraieAdresseBusiness@monsite.fr";
*/

error_reporting(E_ALL);
ini_set("log_errors", 1);
ini_set("display_errors", 0);
ini_set("error_log", realpath(dirname(__FILE__))."/log-error-php.log");

if (!count($_POST)){throw new Exception("Missing POST Data");}

$raw_post_data = file_get_contents('php://input');
$raw_post_array = explode('&', $raw_post_data);
$myPost = array();
$strlog='';
foreach ($raw_post_array as $keyval) {
    $keyval = explode('=', $keyval);
    if (count($keyval) == 2) {
        // Since we do not want the plus in the datetime string to be encoded to a space, we manually encode it.
        if ($keyval[0] === 'payment_date') {
            if (substr_count($keyval[1], '+') === 1) {
                $keyval[1] = str_replace('+', '%2B', $keyval[1]);
            }
        }
        $myPost[$keyval[0]] = urldecode($keyval[1]);
        $strlog .= $keyval[0].':'.$myPost[$keyval[0]]."\n"; // pour log-paypal qui stocke les transactions
    }
}

// le script ne peut pas faire de echo, donc logue les données POST reçues pour voir à quoi ça ressemble
file_put_contents("log-paypal", $strlog."\n", FILE_APPEND);

// Build the body of the verification post request, adding the _notify-validate command
$req = 'cmd=_notify-validate';
$get_magic_quotes_exists = false;
if (function_exists('get_magic_quotes_gpc')) {
    $get_magic_quotes_exists = true;
}
foreach ($myPost as $key => $value) {
    if ($get_magic_quotes_exists == true && get_magic_quotes_gpc() == 1) {
        $value = urlencode(stripslashes($value));
    } else {
        $value = urlencode($value);
    }
    $req .= "&$key=$value";
}

// Post the data back to PayPal, using curl. Throw exceptions if errors occur
$ch = curl_init(VERIFY_URI);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $req);
curl_setopt($ch, CURLOPT_SSLVERSION, 6);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);

// This is often required if the server is missing a global cert bundle, or is using an outdated one
//if ($this->use_local_certs) {
//    curl_setopt($ch, CURLOPT_CAINFO, __DIR__ . "/cert/cacert.pem");
//}
curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'User-Agent: PHP-IPN-Verification-Script',
    'Connection: Close',
));
$res = curl_exec($ch);
if (!($res)) {
    $errno = curl_errno($ch);
    $errstr = curl_error($ch);
    curl_close($ch);
    throw new Exception("cURL error: [$errno] $errstr");
}

$info = curl_getinfo($ch);
$http_code = $info['http_code'];
if ($http_code != 200) {throw new Exception("PayPal responded with http code $http_code");}

curl_close($ch);

// PayPal a validé la transaction pécunière
// récupère les données reçues pour ressortir les données de la commande
if($res === 'VERIFIED'){
    // récupère les données reçues de Paypal...
    $cmde        = $_POST["item_name"];
    $montant     = $_POST["mc_gross"];
    $txn_id      = $_POST["txn_id"];
    //$payer_email = $_POST["payer_email"]; // Attention un tiers ne peut payer pour un autre, sauf si on passe une référence commande en custom...
    $custom      = $_POST["custom"]; //id référence unique de la commande généré par la boutique

    $probleme = '';

    // Regarde le statut du paiement pour s'assurer que la transaction pécunière a été effectuée
    $statut = $_POST["payment_status"];
    if($statut != 'Completed') $probleme.='$_POST["payment_status"]='.$statut." != Completed\n";
    // Vérifie que l'email du vendeur est bien le mien
    if($_POST["receiver_email"] != vendeur) $probleme.='$_POST["receiver_email"]='.$_POST["receiver_email"].' != '.vendeur."\n";
    // Vérifie que la devise est conforme à ce qui était prévu avant la transaction
    if($_POST["mc_currency"]!='EUR') $probleme.='$_POST["mc_currency"]='.$_POST["mc_currency"]." != EUR\n";

    // Données de simulation sans SQL ------------------------
    $nomCmde = 'Blouson en daim'; 
    $TTC     = 74.35;
    $myref   = 'AF05D5BA48E8877DAAF';
    // Vérifie que la transaction correspond à une commande du site
    if($custom != $myref) $probleme.='$_POST["custom"]='.$custom." != $myref\n";
    // Vérifie que le prix de la transaction correspond au montant de la commande en base de données
    if($TTC != $montant) $probleme.='$_POST["mc_gross"]='.$montant." != $TTC\n";
    // Regarde si le nom de la commande correspond bien à ce paiement
    if($cmde != $nomCmde) $probleme.='$_POST["item_name"]='.$cmde." != $nomCmde\n";

    // Données de simulation avec SQL -----------------------
    // ... pour ressortir de la BDD les données de la commande, pour les comparer à ce qui a été reçu de Paypal.
    // Le formulaire de départ contient : Référence de la commande/Nom de la commande/Montant, on connaît ces valeurs.
    // "SELECT commande.id, commande.txid
    //     FROM commandes INNER JOIN client ON client.id=commandes.idclient
    //     WHERE commande.reference='$custom'
    //    AND commande.TTC=$montant
    //    AND commande.nom='$cmde';";
    //$cmdeID   = 1256; // Simulation de données sans SQL
    //$cmdeTXID = '';   // TransactionID est théoriquement vide pour l'instant
    //$myref = 'AF05D5BA48E8877DAAF'; //référence unique id de la commande généré par la boutique

    // Si la requête SQL ne retourne aucun enregistrement, il y a un problème
    //if(mysqli_num_rows($req)==0) $probleme.='SQL ne retourne aucun enregistrement pour cette transaction';// toutes les données reçues seront loguées pour enquête
    // Regarde l'id de transaction pour s'assurer que la transaction n'a pas déjà été faite
    //if($_POST["txn_id"]==$cmdeTXID) $probleme.='Paiement déjà effectué pour cette commande';

    // Si problème → logue et prévient l'administrateur de la boutique par email
    if($probleme){
        file_put_contents("log-erreur-transaction", $probleme."\n$strlog\n\n",FILE_APPEND);
        //mail(admin, );
    }
    //Sinon tout est nickel,
    else{
        // Marque sur le serveur la commande comme payée et en attente d'expédition, stocke l'id de transaction,
        // $sql->execute("UPDATE commandes SET txid='$txn_id', statut=1 WHERE reference='$myref';");
        // (donc dans la table si une commande a un id de transaction, ça signifie qu'elle a été payée). Statut 0:en attente de paiement. 1:payée en attente d'expédition 2:envoyée 3:reçue<

        // Pour chaque produit de la commande, déduit le produit du stock
        // SELECT idproduit, qte FROM details_commande WHERE refCommande='$myref';
        // while(){
        //     $stock = $sql->execute("SELECT stock FROM produits WHERE idproduit=");
        //     $nouveauStock = $stock - $qtecommandee;
        //     $sql->execute("UPDATE stock SET qte='$nouveauStock' WHERE idproduit=");
        // }
        // Prévient le client par email "Le paiement de votre commande n°55 a été validé, la commande est en attente d'expédition",
        //mail();

        // prévient l'admin par email qu'une commande a été payée (et qu'il faut l'envoyer vite).
        //mail(admin, );
    }
}
else if($res === 'INVALID'){ // IPN invalid, log for manual investigation
    file_put_contents("log-erreur-transaction", "IPN invalide\n$strlog\n\n",FILE_APPEND);
    //mail(admin, );
}

// Reply with an empty 200 response to indicate to paypal the IPN was received correctly
header("HTTP/1.1 200 OK");
?>

TEST SANDBOX

À ce stade on doit tester un paiement fictif pour voir si ça fonctionne.
Uploader sur un serveur (localhost ne marchera pas):

TEST DE PAIEMENT FICTIF

Afficher le formulaire form.html en local ou par une URL de votre site formulaire avec le bouton Paypal
et cliquer sur ce fameux bouton "Acheter", Paypal propose de se connecter ou payer par carte bancaire:
Page de connexion Paypal
Si le client clique sur "Annuler et retourner sur Test Store" il revient sur la page "cancel_return".
Se connecter avec le compte client Sandbox: client@machin.fr mot de passe azertyui

Le client arrive sur l'image perso et le joli bouton bleu du paiement:
page de paiement Paypal
Cliquer sur le montant bleu 74,35 EUR montre les détails.
Cliquer sur Terminer l'achat. Paypal appelle alors en arrière-plan le script de notification (communication serveur/serveur).
Cliquer sur détails indique le nom de la commande:

paiement effectué sur Paypal détails du paiement effectué sur Paypal
 
Après le paiement si le client clique sur "Retourner sur le site du marchand" il est dirigé vers la page "return" en GET avec son PayerID en paramètre GET.
Cependant il est probable que le client ne clique pas dessus et ferme la page directement donc peu importe. La transaction se passe en arrière-plan.

Quelques secondes plus tard vous devriez avoir sur le serveur un fichier log-paypal signifiant que le serveur Paypal a invoqué votre script IPN.
Si vous ne recevez pas de messages de notification regarder ici, regarder si le log d'erreur existe et ce qu'il dit, regardez le log d'erreurs du serveur web.
notify.php ne peut être appelé qu'en POST.

Si ça marche, voici un exemple de données IPN reçues du serveur Paypal
pour une commande "Blouson en cuir" de 14.35€,
pour Jean Bon client@machin.fr,
paiement confirmé au vendeur vendeur@truc.fr
Paypal indique 0.74€ de frais de transaction, et l'id de transaction.

log-paypal
mc_gross:14.35
protection_eligibility:Eligible
address_status:confirmed
payer_id:L5VQFDLQ2ZUL8
address_street:Av. de la Pelouse, 87648672 Mayet
payment_date:02:08:24 Apr 15, 2024 PDT
payment_status:Completed
charset:UTF-8
address_zip:75002
first_name:Jean
mc_fee:0.74
address_country_code:FR
address_name:Jean Bon
notify_version:3.9
custom:AF05D5BA48E8877DAAF
payer_status:verified
business:vendeur@truc.fr
address_country:France
address_city:Paris
quantity:1
verify_sign:AFbtoae17G1snrl8ZQGTOr-6vaYpAaLuXOmd.ihM9CpdMXpJ-FaYSzbh
payer_email:client@machin.fr
txn_id:8427561662008594T
payment_type:instant
last_name:Bon
address_state:Alsace
receiver_email:vendeur@truc.fr
payment_fee:0.74
shipping_discount:0.00
insurance_amount:0.00
receiver_id:PAP8U9ASPTCEW
txn_type:web_accept
item_name:Blouson en cuir
discount:0.00
mc_currency:EUR
item_number:
residence_country:FR
test_ipn:1
shipping_method:Default
transaction_subject:
payment_gross:
ipn_track_id:f825995a7ef92
Liste des variables IPN

Par contre ce n'est pas franchement instantané, il faut parfois attendre quelques longues secondes.
Donc ne pas dire trop tôt au client sur la page "return" que le paiement est validé.
Plutôt lui signifier que le paiement est en cours de validation par Paypal, qu'il repasse plus tard dans "Mes commandes" pour voir le statut de la commande, et qu'il sera informé par email, par Paypal et par nous-mêmes, lorsque Paypal aura effectivement procédé la transaction.


PASSAGE EN PRODUCTION

1. Modifier quelques lignes du formulaire en passant les valeurs de prod:
<form action="https://www.paypal.com/cgi-bin/webscr">
<input type="hidden" name="business" value="votre-vraie-adresse@paypal-business.fr">

2. Modifier notify.php en commentant les données SANDBOX et en décommentant les données PROD.

3. Faire un test sur une commande à 0.99€, pour être sûr que tout fonctionne en prod. Normalement oui mais la confiance n'exclut pas le contrôle.


POUR ALLER PLUS LOIN



"En espérant que vous avez apprécié le tutoriel (sans pub, ni cookies, ni appel aux dons), en vous souhaitant plein de ventes, un très gros chiffre d'affaire avec une marge mirobolante, un bénéfice gargantuesque et quelques miettes après impôt." - B.M.


VOS COMMENTAIRES