Code sample PHP – IPN

O Instant Payment Notification, ou Notificação Instantânea de Pagamento ou, simplesmente, IPN, é um serviço de mensagens que notifica sua aplicação sobre eventos relacionados às transações PayPal. Você pode utilizar as mensagens IPN para automatizar seu back-office ou funções administrativas, como finalização de pedidos, acompanhamento de consumidores e transações ou qualquer outra tarefa que dependa do status de pagamento de uma transação.

<?php
/**
 * Verifica se uma notificação IPN é válida, fazendo a autenticação
 * da mensagem segundo o protocolo de segurança do serviço.
 * 
 * @param array $message Um array contendo a notificação recebida.
 * @return boolean TRUE se a notificação for autência, ou FALSE se
 *                 não for.
 */
function isIPNValid(array $message)
{
    $endpoint = 'https://www.paypal.com';
 
    if (isset($message&#91;'test_ipn'&#93;) && $message&#91;'test_ipn'&#93; == '1') {
        $endpoint = 'https://www.sandbox.paypal.com';
    }
 
    $endpoint .= '/cgi-bin/webscr?cmd=_notify-validate';
 
    $curl = curl_init();
 
    curl_setopt($curl, CURLOPT_URL, $endpoint);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($curl, CURLOPT_POST, true);
    curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($message));
  
    $response = curl_exec($curl);
    $error = curl_error($curl);
    $errno = curl_errno($curl);
 
    curl_close($curl);
  
    return empty($error) && $errno == 0 && $response == 'VERIFIED';
}
&#91;/sourcecode&#93;
<div class="wp-git-embed" style="margin:10px 0; background-color:#f5f5f5; border:1px solid #d2d2d2; width:99%; "><a style="display:inline-block; padding:4px 6px;" href="https://raw.github.com/br-paypaldev/code-sample-ipn/master/ipn.sql" target="_blank">ipn.sql</a><a style="display:inline-block; padding:4px 6px; float:right;" href="https://github.com/br-paypaldev/code-sample-ipn/blob/master/ipn.sql" target="_blank">Veja no <strong>GitHub PayPal Developer Brasil</strong></a></div>CREATE SCHEMA IF NOT EXISTS `code_sample`;

USE `code_sample`;

CREATE TABLE `ipn` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
    `txn_id` INT UNSIGNED DEFAULT NULL,
    `txn_type` VARCHAR(55) DEFAULT NULL,
    `receiver_email` VARCHAR(127) NOT NULL,
    `payment_status` VARCHAR(17) DEFAULT NULL,
    `pending_reason` VARCHAR(17) DEFAULT NULL,
    `reason_code` VARCHAR(31) DEFAULT NULL,
    `custom` VARCHAR(45) DEFAULT NULL,
    `invoice` VARCHAR(45) DEFAULT NULL,
    `notification` MEDIUMTEXT NOT NULL,
    `hash` CHAR(32) NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `hash_UNIQUE` (`hash`),
    KEY `custom` (`custom`,`payment_status`),
    KEY `invoice` (`invoice`,`payment_status`),
    KEY `type` (`txn_type`,`payment_status`),
    KEY `id` (`txn_id`,`payment_status`)
);
<?php
/**
 * Grava a mensagem da notificação em uma base de dados para
 * verificação de duplicidade e, se for o caso, análise das
 * notificações recebidas.
 * 
 * @param PDO $pdo Objeto de conexão com a base.
 * @param array $message Mensagem IPN
 *
 * @return boolean
 */
function logIPN(PDO $pdo, array $message)
{
    $stm = $pdo->prepare('
        INSERT INTO `ipn`(
            `txn_id`,
            `txn_type`,
            `receiver_email`,
            `payment_status`,
            `pending_reason`,
            `reason_code`,
            `custom`,
            `invoice`,
            `notification`,
            `hash`
        ) VALUES (
            :txn_id,
            :txn_type,
            :receiver_email,
            :payment_status,
            :pending_reason,
            :reason_code,
            :custom,
            :invoice,
            :notification,
            :hash
        );');
 
    $ipn = array_merge(array(
        'txn_id' => null,
        'txn_type' => null,
        'payment_status' => null,
        'pending_reason' => null,
        'reason_code' => null,
        'custom' => null,
        'invoice' => null
    ), $message);
 
    $notification = serialize($message);
    $hash = md5($notification);
 
    $stm->bindValue(':txn_id', $ipn['txn_id']);
    $stm->bindValue(':txn_type', $ipn['txn_type']);
    $stm->bindValue(':receiver_email', $ipn['receiver_email']);
    $stm->bindValue(':payment_status', $ipn['payment_status']);
    $stm->bindValue(':pending_reason', $ipn['pending_reason']);
    $stm->bindValue(':reason_code', $ipn['reason_code']);
    $stm->bindValue(':custom', $ipn['custom']);
    $stm->bindValue(':invoice', $ipn['invoice']);
    $stm->bindValue(':notification', $notification);
    $stm->bindValue(':hash', $hash);
 
    return $stm->execute();
}
CREATE SCHEMA IF NOT EXISTS `code_sample`;

USE `code_sample`;

CREATE TABLE `customer` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
    `address_country` VARCHAR(64) DEFAULT NULL,
    `address_city` VARCHAR(40) DEFAULT NULL,
    `address_country_code` CHAR(2) DEFAULT NULL,
    `address_name` VARCHAR(128) DEFAULT NULL,
    `address_state` VARCHAR(40) DEFAULT NULL,
    `address_status` VARCHAR(11) DEFAULT NULL,
    `address_street` VARCHAR(200) DEFAULT NULL,
    `address_zip` VARCHAR(20) DEFAULT NULL,
    `contact_phone` VARCHAR(20) DEFAULT NULL,
    `first_name` VARCHAR(64) DEFAULT NULL,
    `last_name` VARCHAR(64) DEFAULT NULL,
    `business_name` VARCHAR(127) DEFAULT NULL,
    `email` VARCHAR(127) NOT NULL,
    `paypal_id` VARCHAR(13) NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE INDEX `email_UNIQUE` (`email` ASC),
    UNIQUE INDEX `paypal_id_UNIQUE` (`paypal_id` ASC)
);
<?php
/**
 * Armazena os dados do cliente recebidos em uma notificação IPN.
 *
 * @param PDO $pdo Objeto de conexão com a base.
 * @param array $message Mensagem IPN
 *
 * @return boolean
 */
function storeCustomer(PDO $pdo, array $message)
{
    $stm = $pdo->prepare('
        INSERT INTO `customer` (
            `address_country`,
            `address_city`,
            `address_country_code`,
            `address_name`,
            `address_state`,
            `address_status`,
            `address_street`,
            `address_zip`,
            `contact_phone`,
            `first_name`,
            `last_name`,
            `business_name`,
            `email`,
            `paypal_id`
        ) VALUES (
            :address_country,
            :address_city,
            :address_country_code,
            :address_name,
            :address_state,
            :address_status,
            :address_street,
            :address_zip,
            :contact_phone,
            :first_name,
            :last_name,
            :business_name,
            :email,
            :paypal_id
        );');

    $customer = array_merge(array(
        'address_country' => null,
        'address_city' => null,
        'address_country_code' => null,
        'address_name' => null,
        'address_state' => null,
        'address_status' => null,
        'address_street' => null,
        'address_zip' => null,
        'contact_phone' => null,
        'first_name' => null,
        'last_name' => null,
        'business_name' => null,
        'payer_email' => null,
        'payer_id' => null,
    ), $message);

    $stm->bindValue(':address_country', $customer['address_country']);
    $stm->bindValue(':address_city', $customer['address_city']);
    $stm->bindValue(':address_country_code', $customer['address_country_code']);
    $stm->bindValue(':address_name', $customer['address_name']);
    $stm->bindValue(':address_state', $customer['address_state']);
    $stm->bindValue(':address_status', $customer['address_status']);
    $stm->bindValue(':address_street', $customer['address_street']);
    $stm->bindValue(':address_zip', $customer['address_zip']);
    $stm->bindValue(':contact_phone', $customer['contact_phone']);
    $stm->bindValue(':first_name', $customer['first_name']);
    $stm->bindValue(':last_name', $customer['last_name']);
    $stm->bindValue(':business_name', $customer['business_name']);
    $stm->bindValue(':email', $customer['payer_email']);
    $stm->bindValue(':paypal_id', $customer['payer_id']);

    return $stm->execute();
}
CREATE SCHEMA IF NOT EXISTS `code_sample`;

USE `code_sample`;

CREATE  TABLE `transaction` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
    `invoice` VARCHAR(127) NULL DEFAULT NULL,
    `custom` VARCHAR(255) NULL DEFAULT NULL,
    `txn_type` VARCHAR(55) NOT NULL,
    `txn_id` INT NOT NULL,
    `payer_id` VARCHAR(13) NOT NULL,
    `currency` CHAR(3) NOT NULL,
    `gross` DECIMAL(10,2) NOT NULL,
    `fee` DECIMAL(10,2) NOT NULL,
    `handling` DECIMAL(10,2) NULL DEFAULT NULL,
    `shipping` DECIMAL(10,2) NULL DEFAULT NULL,
    `tax` DECIMAL(10,2) NULL DEFAULT NULL,
    `payment_status` VARCHAR(17) NULL DEFAULT NULL,
    `pending_reason` VARCHAR(17) NULL DEFAULT NULL,
    `reason_code` VARCHAR(31) NULL DEFAULT NULL,
    PRIMARY KEY (`id`),
    INDEX `payer` (`payer_id` ASC, `payment_status` ASC),
    INDEX `txn` (`txn_id` ASC, `payment_status` ASC),
    INDEX `custom` (`custom` ASC, `payment_status` ASC),
    INDEX `invoice` (`invoice` ASC, `payment_status` ASC)
);
<?php
/**
 * Armazena os dados da transação recebidos em uma notificação IPN.
 *
 * @param PDO $pdo Objeto de conexão com a base.
 * @param array $message Mensagem IPN
 *
 * @return boolean
 */
function storeTransaction(PDO $pdo, array $message)
{
    $stm = $pdo->prepare('
        INSERT INTO `transaction` (
            `invoice`,
            `custom`,
            `txn_type`,
            `txn_id`,
            `payer_id`,
            `currency`,
            `gross`,
            `fee`,
            `handling`,
            `shipping`,
            `tax`,
            `payment_status`,
            `pending_reason`,
            `reason_code`
        ) VALUES (
            :invoice,
            :custom,
            :txn_type,
            :txn_id,
            :payer_id,
            :currency,
            :gross,
            :fee,
            :handling,
            :shipping,
            :tax,
            :payment_status,
            :pending_reason,
            :reason_code
        );');
 
    $transaction = array_merge(array(
        'invoice' => null,
        'custom' => null,
        'txn_type' => null,
        'txn_id' => null,
        'payer_id' => null,
        'mc_currency' => null,
        'mc_gross' => null,
        'mc_fee' => null,
        'mc_handling' => null,
        'mc_shipping' => null,
        'tax' => null,
        'payment_status' => null,
        'pending_reason' => null,
        'reason_code' => null,
    ), $message);
 
    $stm->bindValue(':invoice', $transaction['invoice']);
    $stm->bindValue(':custom', $transaction['custom']);
    $stm->bindValue(':txn_type', $transaction['txn_type']);
    $stm->bindValue(':txn_id', $transaction['txn_id']);
    $stm->bindValue(':payer_id', $transaction['payer_id']);
    $stm->bindValue(':currency', $transaction['mc_currency']);
    $stm->bindValue(':gross', $transaction['mc_gross']);
    $stm->bindValue(':fee', $transaction['mc_fee']);
    $stm->bindValue(':handling', $transaction['mc_handling']);
    $stm->bindValue(':shipping', $transaction['mc_shipping']);
    $stm->bindValue(':tax', $transaction['tax']);
    $stm->bindValue(':payment_status', $transaction['payment_status']);
    $stm->bindValue(':pending_reason', $transaction['pending_reason']);
    $stm->bindValue(':reason_code', $transaction['reason_code']);
 
    return $stm->execute();
}
<?php
//Incluindo o arquivo que contém a função isIPNValid
require 'isIPNValid.php';
 
//Incluindo o arquivo que contém a função logIPN
require 'logIPN.php';
 
//Incluindo o arquivo que contém a função storeCustomer
require 'storeCustomer.php';
 
//Incluindo o arquivo que contém a função storeTransaction
require 'storeTransaction.php';
 
//Email da conta do vendedor, que será utilizada para verificar o
//destinatário da notificação.
$receiver_email = 'empresa@teste.com.br';
 
//Informações para conexão com o banco de dados, que utilizaremos
//para gravar o log.
$mysql = array(
    'host' => 'localhost',
    'user' => 'usuário',
    'pswd' => 'senha',
    'dbname' => 'code_sample'
);
 
//As notificações sempre serão via HTTP POST, então verificamos o método
//utilizado na requisição, antes de fazer qualquer coisa.
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    //Antes de trabalhar com a notificação, precisamos verificar se ela
    //é válida e, se não for, descartar.
    if (!isIPNValid($_POST)) {
        return;
    }
 
    //Se chegamos até aqui, significa que estamos lidando com uma
    //notificação IPN válida. Agora precisamos verificar se somos o
    //destinatário dessa notificação, verificando o campo receiver_email.
    if ($_POST['receiver_email'] == $receiver_email) {
        //Está tudo correto, somos o destinatário da notificação, vamos
        //gravar um log dessa notificação.
        $pdoattrs = array(
            PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8');
 
        $pdo = new PDO(sprintf('mysql:host=%s;dbname=%s',
                               $mysql['host'], $mysql['dbname']),
                       $mysql['user'],
                       $mysql['pswd'],
                       $pdoattrs);
 
        if (logIPN($pdo, $_POST)) {
            //Log gravado, podemos seguir com as regras de negócio para
            //essa notificação.
 
            //gravamos dados do cliente
            storeCustomer($pdo, $_POST);
 
            //gravamos dados da transação
            storeTransaction($pdo, $_POST);
        }
    }
}

Tutorial relacionado para facilitar o entendimento do código:

Guia de integração com IPN