Cobrando por servicios a través de PayPal con PHP

Hace un tiempo necesitábamos crear un sistema de compras a través de PayPal con la particularidad de que teníamos recibir confirmación instantánea del pago para reflejar esas compras al usuario. Esta era la diferencia principal respecto de un carro de compra de una web donde vendes algo físicamente, en este caso no necesitas saber el estado de una compra, cuando el equipo de almacén prepare el pedido ya comprobará si se ha realizado el pago antes de servirlo. Nuestro caso es más complicado. Vendemos servicios que pueden ser suscripciones, créditos de uso en la web, servicios premium, acceso a zonas privadas… todo este tipo de opciones donde el usuario, después de pagar, vuelve a tu web para disfrutar de los servicios que ha comprado.

Al querer cobrar a través de PayPal tienes dos opciones, o creas botones estáticos (Comprar Ahora) desde la web de PayPal o generas los tuyos propios. El problema de crearlos estáticos es que debes crear uno por cada usuario y para cada servicio que necesites vender, algo absurdo para el caso que tratamos ya que estarías ante miles de botones. Al generar los tuyos propios puedes hacerlos normales o seguros, para que no se puedan modificar por el camino. Este es el método que deberías utilizar normalmente ya que, en los normales, un usuario experimentado puede modificar el importe a cobrar, algo que no deseas que ocurra.

Para que el sistema funcionase bien deberíamos recibir en una URL las compras realizadas de alguna manera que nos permitiese identificar al usuario que habia hecho la compra para añadirle los servicios en cuestión. La solución es crear botones firmados online utilizando como identificador de producto de compra el idUsuario de tu base de datos, de esta forma las confirmaciones de compra te indican el usuario al que pertenecen y ya puedas darle los servicios por los que ha pagado. Puedes utilizar combinaciones más elaboradas para reutilizar el sistema para distintos productos, por ejemplo idUsuario-idServicio (12345-3). Al recibirlo, con dividir la cadena por el guión ya tienes todo lo necesario. En nuestro caso había varios tipos de servicio pero se indentificaban por el coste del mismo, con lo cual con el identificador de usuario teníamos suficiente.

Aparentemente es sencillo el proceso, pero se complica ya que la documentación, a pesar de ser extensa, no esta del todo claro. Por un lado no está nada claro cómo configurar tu cuenta de PayPal para tener el sistema completo y por otro es bastante lioso el modo de crear los botones y recibir las confirmaciones. Vamos a explicarlo detalladamente ya que es un interesante ejercicio de programación en PHP.

Configurando la cuenta de PayPal

Lo primero que tienes que hacer es crear un certificado público X.509, único sistema que acepta PayPal.

Las instrucciones detallas las tienes aquí. básicamente necesitas tener openssl funcionando, cualquier sistema Linux lo tendrá instalado y sino tienes versiones para Windows.

  1. openssl genrsa -out my-prvkey.pem 1024
  2. openssl req -new -key my-prvkey.pem -x509 -days 3650 -out my-pubcert.pem

Con esto generamos primero la clave privada y a continuación el certificado público X.509 de esa clave. Es importante el parámetro -days, lo hemos puesto a 10 años para no tener problemas. En mi caso, la primera vez puse 365 días tal como viene en el ejemplo y al año dejó de funcionar sin que te dieses cuenta, fueron los usuarios los que nos avisaron. Guarda los dos archivos, my-prvkey.pem y my-pubcert.pem pues los necesitaremos a continuación.

Ya tenemos nuestro certificado preparado. Ahora debemos subirlo a PayPal para que sepa desencriptar nuestros botones. En tu cuenta de PayPal debes ir a:

Perfil->Configuración de pago codificado

Desde ahí, por un lado descargas el certificado público de PayPal, lo necesitaremos para codificar nuestros botones, y por otro subes tu certificado público que hemos llamado my-pubcert.pem. Obtendrás un ID de certificado, apúntatelo.

Si todo ha ido bien, ahora debemos configurar la cuenta para que PayPal sólamente acepte botones firmados, de manera que nadie pueda suplantar nuestra identidad con botones a otros precios, un detalle muy importante. Vamos entonces a:

Perfil->Preferencias de pago en el sitio Web

Primero activas Transferencia de datos de pago y te copias el Código Personal de Identidad que te indica, lo necesitaremos más adelante. A continuación, en la sección Pagos en el sitio Web codificado activamos la opción Bloquear pago en el sitio Web no codificado. Como véis no estaba tan claro el proceso.

Este paso no es obligatorio, pero haciéndolo conseguimos que el usuario vuelva a tu web una vez haya realizado el pago y, si ha sido correcto, tenga ya sus servicios disponibles. Activa la opción Retroceso automático e indica la URL de devolución, es decir, la URL donde será devuelto tu cliente, por ejemplo: http://www.tudominio.com/index.php?accion=creditos_paypalok

Finalmente activaremos la opción que nos permitirá recibir notificaciones de pagos online. Para ello vamos a:

Perfil->Preferencias de Notificación de pago instantánea

Y activaremos la notificación indicando la URL donde las vamos a recibir, por ejemplo, http://www.tudominio.com/secure/paypal/ipn.php.

Si has llegado a este punto, ya tienes todo lo necesario para comenzar con el código. Como recordatorio, necesitas:

  • Tu clave privada, my-prvkey.pem.
  • Tu certificado público, my-pubcert.pem.
  • Tu ID de certificado en PayPal
  • Tu Código Personal de Identidad
  • Certificado público de Paypal, paypal_cert_pem.txt.

Nos quedan, entonces, tres tareas pendientes:

  • Crear botones de compra
  • Crear el script de recepción de cobros realizados
  • Crear el script de vuelta después de una compra

Creando los botones

Comenzamos con el código. El único requerimiento es que tu instalación de PHP debe tener configurada la extensión openssl imprescindible para trabajar con los certificados. Con esta clase que encontré en su momento (me costó bastante localizar algo sencillo) tienes todo el proceso automatizado, sólo debes preocuparte por indicarle los datos que hemos ido guardando, los certificados y el ID del tu certificado en PayPal, simple ¿no?.

  1. include("Class.PayPalEWP.php");
  2. $paypal = &new PayPalEWP();
  3. $paypal->setTempFileDirectory("/tmp");
  4. $paypal->setCertificate("my-pubcert.pem", "my-prvkey.pem");
  5. $paypal->setCertificateID("XXXXXXXXXX");
  6. $paypal->setPayPalCertificate("paypal_cert_pem.txt");
  7.  
  8. $paypalParam = array(
  9.     ‘cmd’ => ‘_xclick’,
  10.     ‘business’ => ‘info@tudominio.com’,
  11.     ‘item_name’ => ‘Comprar Servicio X,
  12.    ‘item_number‘ => $_SESSION['idUsuario'],
  13.    ‘amount‘ => ‘5‘,
  14.    ‘no_shipping‘ => ‘1‘,
  15.    ‘currency_code‘ => ‘EUR‘,
  16.    ‘lc‘ => ‘ES‘,
  17. );
  18. $form5="<form action=\”https://www.paypal.com/cgi-bin/webscr\” method=\”post\”>
  19.            <input type=\”hidden\” name=\”cmd\” value=\”_s-xclick\”/>
  20.            <input type=\”hidden\” name=\”encrypted\” value=\”—–BEGIN PKCS7—–\n".$paypal->encryptButton($paypalParam)."\n—–END PKCS7—–\”/>
  21.            <input type=\”image\” src=\”imagenes/comprar_paypal.gif\” border=\”0\” name=\”submit\” alt=\”Realice pagos con PayPal: es rápido, gratis y seguro.\” style=\”border:0;\”>
  22.        </form>";

El código es bastante claro.
Ya lo tenemos, $form5 contiene el código de tu botón.
Los parámetros importantes son:

  • item_name: informativo, para que tu cliente sepa lo que compra. Por ejemplo: compra de suscripción a noticias.
  • item_number: Aquí configuramos el idUsuario de tu cliente en tu base de datos, así sabes quién compra.
  • amount: Precio que le cobras, en este caso 5 euros.

Modifica estos y los demás parámetros para reflejar tus opciones y adecuarlos a tu aplicación. Al sacar el código de $form5 en tu página tienes listo el sistema de compra.

Recibiendo las notificaciones

La recepción de notificaciones es la piedra angular del sistema para estar seguros de que un cliente ha pagado por un servicio. PayPal ha pensado que puedes llegar a tener problamas temporales de conectividad que te impidan reconocer una compra, con lo cual obliga a que le confirmes que has recibido la confirmación reenviándole los mismos parámetros que te ha enviado. Un sistema curioso pero efectivo, no puedes suplantarlo puesto que no sabes los identificadores de operación que te va a enviar, así que sólo el receptor de la confirmación podrá confirmar la recepción. Lo que hacemos es crear una solicitud HTTP POST con todos los parámetros que nos ha enviado y se la devolvemos a PayPal. Si todo ha ido bien recibiremos un VERIFIED y podremos aceptar esa transaccion como válida y hacer el procesado que estimemos oportuno, comenzando por comprobar la duplicidad de la transacción ya que PayPal podría estar reenviándola y terminando por otorgar al usuario el servicio por el que ha pagado.

  1. // read the post from PayPal system and add ‘cmd’
  2. $req = ‘cmd=_notify-validate’;
  3. foreach ($_POST as $key => $value) {
  4.     $value = urlencode(stripslashes($value));
  5.     $req .= "&$key=$value";
  6. }
  7.  
  8. // post back to PayPal system to validate
  9. $header .= "POST /cgi-bin/webscr HTTP/1.0\r\n";
  10. $header .= "Content-Type: application/x-www-form-urlencoded\r\n";
  11. $header .= "Content-Length: " . strlen($req) . "\r\n\r\n";
  12. $fp = fsockopen (‘www.paypal.com’, 80, $errno, $errstr, 30);
  13.  
  14. // assign posted variables to local variables
  15. $item_name = $_POST[‘item_name’];
  16. $item_number = $_POST[‘item_number’];
  17. $payment_status = $_POST[‘payment_status’];
  18. $payment_amount = $_POST[‘mc_gross’];
  19. $payment_currency = $_POST[‘mc_currency’];
  20. $txn_id = $_POST[‘txn_id’];
  21. $receiver_email = $_POST[‘receiver_email’];
  22. $payer_email = $_POST[‘payer_email’];
  23. $transid=$_POST[‘txn_id’];
  24. $idUsuario=$_POST[‘item_number’];
  25. $cantidad=$_POST[‘mc_gross’];
  26. $creditos=100;
  27.  
  28. if (!$fp) {
  29.     //CONTROL DE ERRORES; NO SE PUEDE CONECTAR CON PAYPAL
  30.     //NO ES GRAVE, COMO NO LE CONFIRMAMOS LA TRANSACCION
  31.     //ELLOS MISMOS LA REINTENTARÁN MÁS ADELANTE
  32. }else{
  33.     fputs ($fp, $header . $req);
  34.     while (!feof($fp)) {
  35.         $res = fgets ($fp, 1024);
  36.         if (strcmp ($res, "VERIFIED") == 0) {
  37.             //compruebo que no se haya procesado ya la transaccion
  38.             $query="select * from paypal where transid=’$transid’ and estado=1";
  39.             $rs=$conn->Execute($query);
  40.             $sumar=$rs->recordcount();
  41.             if($sumar==0){
  42.                 //LOGEAMOS TODA LA TRANSACCION
  43.                 $vars="GET: ".serialize($_GET)."\r\nPOST: ".serialize($_POST)."";
  44.                 $query="insert into paypal (transid, fecha, estado, variables)
  45.                    VALUES (‘$transid’, now(), 1, ‘$vars’)";
  46.                 $rs=$conn->Execute($query);
  47.  
  48.                 //aquí debes hacer ahora tus operaciones
  49.                 //para conceder el servicio al usuario: $idUsuario
  50.                 //incluso comprobar que idUsuario es válido
  51.             }else{
  52.               //TRANSACCION DUPLICADA, NO HACEMOS NADA
  53.             }
  54.         }else if (strcmp ($res, "INVALID") == 0) {
  55.             //CONTROL DE ERRORES
  56.         }
  57.     }
  58.     fclose ($fp);
  59. }

Ya hemos recibido la confirmación de pago de PayPal, la hemos guardado en nuestra base de datos y le hemos dado a nuestro cliente su servicio, este proceso será distinto para cada aplicación así que no lo explicaremos, haz el tuyo como creas oportuno. Lo que sí te recomiendo es guardar una tabla con todas las transacciones recibidas a modo de log, te servirá para buscar errores o reclamaciones de usuarios.

Cabe señalar que PayPal sólamente lanza este proceso con las transacciones correctas, aquellas se que se han cobrado correctamente, nunca con las erróneas (falta de saldo, tarjeta incorrecta, etc.).

Devolviendo al usuario a nuestra web

Una vez que el usuario ha terminado su transacción en PayPal deberíamos enviarlo de nuevo a nuestra web para que comience a utilizar el servicio por el que ha pagado. Para ello PayPal nos provee del método Retroceso automático que mandará al usuario a la URL que le hayamos especificado indicando los parámetros de la transacción, entre ellos si ha sido válida o no. PayPal, además, ha pensado en todo: no puedo devolver a un usuario a la web de origen sin antes haberle comunicado el éxito de la transacción (el paso anterior). Así el mecanismo de PayPal retrasa el reenvío del usuario unos segundos para intentar que tu servidor ya esté informado de esa transacción. Simplemente genial. Ahora utilizaremos tu código personal de identidad que hemos obtenido al configurar la cuenta en PayPal. Es el parámetro que en el código llamamos $auth_token.

El proceso es parecido a la confirmación de transacciones. Debemos comunicar a PayPal que hemos recibido al petición de vuelta del usuario y que somos nosotros quién lo hacemos, sólo así nos dará los datos de la transacción. De nuevo un método curioso. Para hacerlo, nos indica por GET el identificador de la transacción y debemos devolvérselo junto a nuestro código personal de identidad mediante otra llamada HTTP POST, de este modo nos aseguramos de que somos nosotros quienes solicitamos la información. Si todo ha sido correcto PayPal nos devuelve los datos de la transacción como respuesta a esta llamada. Otra vez genial el sistema.

¿Por qué no hacerlo como en el paso anterior? Sencillo, el método IPN es transparence al usuario, es una llamada interna que hacen los sistemas de PayPal a los tuyos, nadie va a saber que datos se envían, con lo cual puedes utilizar esos datos para confirmar la transacción. En el método de retroceso, esta URL llega al usuario, con lo que podría hacer cosas que no deseamos con los datos, con lo que no nos envían nada en la solicitud, simplemente el identificador de la transacción para que internamente nosotros pidamos los datos validándonos con el código personal.

  1. //read the post from PayPal system and add ‘cmd’
  2. $tx_token = $_GET[‘tx’];
  3. $auth_token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
  4. $req = ‘cmd=_notify-synch’;
  5. $req .= "&tx=$tx_token&at=$auth_token";
  6. $header .= "POST /cgi-bin/webscr HTTP/1.0\r\n";
  7. $header .= "Content-Type: application/x-www-form-urlencoded\r\n";
  8. $header .= "Content-Length: " . strlen($req) . "\r\n\r\n";
  9.  
  10. $fp = fsockopen (‘www.paypal.com’, 80, $errno, $errstr, 30);
  11. $isError=0;
  12. if(!$fp) {
  13.     $isError=2;//error HTTP
  14. }else{
  15.     fputs ($fp, $header . $req);
  16.     // read the body data
  17.     $res = ;
  18.     $headerdone = false;
  19.     while (!feof($fp)) {
  20.         $line = fgets ($fp, 1024);
  21.         if (strcmp($line, "\r\n") == 0) {
  22.             // read the header
  23.             $headerdone = true;
  24.         }else if ($headerdone){
  25.             // header has been read. now read the contents
  26.             $res .= $line;
  27.         }
  28.     }    // parse the data
  29.     $lines = explode("\n", $res);
  30.     $keyarray = array();
  31.     if (strcmp ($lines[0], "SUCCESS") == 0) {
  32.         for ($i=1; $i<count($lines);$i++){
  33.             list($key,$val) = explode("=", $lines[$i]);
  34.             $keyarray[urldecode($key)] = urldecode($val);
  35.         }
  36.  
  37.         $isError=0;//no error
  38.         $nombre = $keyarray[‘first_name’]." ".$keyarray[‘last_name’];
  39.         $producto = $keyarray[‘item_name’];
  40.         $amount = $keyarray[‘payment_gross’];
  41.         $idUsuario=$keyarray[‘item_number’];
  42.         $cantidad=0+$keyarray[‘mc_gross’];
  43.         $estado=$keyarray[‘payment_status’];
  44.         $transid=$keyarray[‘txn_id’];
  45.  
  46.         //ahora ya puedes evaluar lo que necesites de tu transacción
  47.         //y termina informando al usuario de que todo ha ido bien y ya tiene su servicio
  48.     }else if (strcmp ($lines[0], "FAIL") == 0) {
  49.         $isError=1; //error de transaccion
  50.     }
  51. }
  52. fclose ($fp);

La respuesta que recibimos es una respuesta HTTP estandar, con lo cual debemos ser conscientes de que vamos a recibir primero todas las cabeceras HTTP de la respuesta, después dos saltos de línea y a continuación la respuesta propiamente dicha. En una petición normal esta respuesta sería el código HTML de tu página, pero en este caso recibimos la lista de parámetros de la transacción, uno por línea y del tipo:

parámetro1=valor1
parámetro2=valor2
...

El código tiene esto en cuenta y, al recoger la respuesta, regenera la lista de parámetros/valores recibidos con lo que tenemos los datos necesarios.
La primera línea va a ser SUCESS ó FAIL, está claro el dato, una indica que la transacción ha sido válida y la otra que no. Obviamente debes informar al usuario de que la operación ha sido correcta y que ya tiene su servicio disponible.

Si has entendido bien todo lo explicado hasta ahora, verás que los sistemas de recepción de notificaciones y retroceso automático son muy similares, de hecho puedes utilizar este último para validar las transacciones de igual modo que con el primero y no necesitarías éste. Pero ¿qué ocurriría si sólo implementas el segundo y el usuario cierra la ventana del pago mientras está en esos segundos de espera antes de reenviarlo a tu web? Simplemente que el usuario no tendría su servicio disponible ya que nunca ha llegado a realizar el proceso del Retroceso automático. Para solucionarlo implementamos los dos. Con el primero aseguramos que la mayoría de transacciones válidas son procesadas y los servicios otorgados al cliente, con la segunda, además de informar al cliente del éxito o fracaso se su operación, tenemos un sistema de redundancia por si el primer procedimiento hubiese fallado. Como en ámbos tenemos el identificador de transacción, simplemente debemos comprobar que esa transacción ya existe y no hacer nada o procesarla si no existe.

Y eso es todo amigos. Con esto hemos aprendido a tener un sistema de compras de servicios seguro y dinámico en PayPal. Cómo habéis podido observar, el procedimiento es bastante complejo en cuanto a configuración y desarrollo, pero es muy interesante el estudio del resultado para tener una idea más amplia de como implementar sistemas seguros.

104 comentarios en “Cobrando por servicios a través de PayPal con PHP

  1. Hola, el resultado de los files cambia con cada refresh ahunque tenga la misma informacion. esto es normal? aqui tengo un MD5 del resultado dentro del file. lo saque usando:

    var_dump(md5(file_get_contents(‘file uno path’)));echo”;
    var_dump(md5(file_get_contents(‘file dos path’)));echo”;
    var_dump(md5(file_get_contents(‘file tres path’)));echo”;

    nota como cambian los files 2 y 3

    string(32) “649c5aa3aceb5e7005f98d93820322e8″
    string(32) “a853644bfc16ddeca152c94e6dc67e1f”
    string(32) “713ebb09f44f37d1b39f4ffbb0baf908″

    despues del refresh me da

    string(32) “649c5aa3aceb5e7005f98d93820322e8″
    string(32) “b8e7edd53e9c4de2f1cac761286d423d”
    string(32) “307e6885382c2d9231f1e82850d357e0″

    Nota: el nombre del file no cambia -para probar estoy usando el mismo nombre. Dime porfavor. Esto es Normal?
    Gracias

    (PS. tambien el string del boton cambia)

    Deberia de ser lo mismo, pues esta hutilizando lo mismo cada vez, o no?

  2. hola como estan solo me falta la definicion de la variable $_SESSION['idUsuario'] para poder implementar como yo deseo el sistema pero no he logrado hacer esta parte la verdad es que soy novato en esto de php si alguien me ayuda se lo agradeceria muchisimo

  3. El Tutorial es muy bueno.

    he podido crear mi boton comprar ahora sin problema…

    el problema está con los otros dos script, con el cambio que ha habido en paypal en este ultimo año, varias cosas han cambiado.
    la verdad ninguno de los 2 script me funcionaron, he buscado y uniendo varios script logré hacerlo funcionar.
    pero este tutorial me sirvió mucho, pues me ayudo a empezar…

  4. Olver Edgar, podrías compartir como solucionaste los dos ultimos scripts ?, gracias de antemano.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

Puedes usar las siguientes etiquetas y atributos HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>