Come implementare un pagamento online: Le procedure generali (3/6)
Nell’articolo precedente di questa serie abbiamo visto, ragionando con una logica ad oggetti, quello che dovremo realizzare ora nella pratica. In questo articolo vedremo come implementare la classe parent (che chiameremo IPNListener). Questa classe la potremo utilizzare per qualsiasi nostro progetto che prevede l’implementazione di un pagamento.
Come prima cosa però, creiamo un file di configurazione denominato pp_config.php in questo modo:
<?php //system define("SIMULATION", 1); define("SIMULATION_URL", "www.sandbox.paypal.com"); define("PRODUCTION_URL", "www.paypal.com"); define("PRIMARY_PAYPAL_EMAIL", "email@sito.com"); define("PRIMARY_SANDBOX_EMAIL", "admin_1284367352_biz@bluewin.ch"); //db define("HOST", "localhost"); define("DB_USER", "root"); define("DB_NAME", "paypal"); define("DB_PASSWORD", "*******"); //messages define("ADMIN_MAIL", "maurizio.tarchini@bluewin.ch"); define("NO_REPLY", "no_reply@site.com"); define("AMMOUNT", 50); ?>
La prima costante definisce se stiamo utilizzando il nostro sistema in simulazione o in produzione. In questo modo dovremo solo modificare questo parametro quando passeremo da una circostanza all’altra.
In effetti si potrebbe anche fare automaticamente, infatti tra i vari parametri che riceviamo possiamo trovare test_ipn che sarà impostato a 1 se siamo in Sandbox o a 0 se stiamo lavorando con PayPal. Ma qualcosa dentro di me mi dice che è meglio impostare manualmente (non lo dico ironicamente, non penso che possa creare dei problemi, ma ugualmente non mi fido).
La seconda e la terza costante definiscono l’url dell’ambiente di simulazione e dell’ambiente reale, di produzione. Vedremo in seguito come utilizzarle.
La quarta costante definisce il mio account primario di commerciante (reale) mentre la quinta definisce l’account primario nell’ambiente di simulazione.
Possiamo disporre infatti di diversi indirizzi email nel nostro account PayPal (o Sandbox), ma solo uno sarà il primario. Per verificare quale sia:
- vai in Sandbox;
- seleziona l’account amministrativo;
- loggati all’account amministrativo;
- clicca su profilo;
- clicca su email
Se hai creato l’account come descritto nel primo articolo, sarà anche l’unico indirizzo email.
Seguono i dati di connessione al database, il mio email, l’email di risposta per i messaggi di sistema, ed infine il costo del servizio.
La classe IPNListener
Creiamo ora un nuovo file denominato IPNListener.php, nel quale inizieremo ad includere il file di configurazione e a dichiarare la classe (astratta) IPNListener.
<?php require_once 'pp_config.php'; abstract class IPNListener {
Il primo metodo che implementeremo sarà sendReport(). Questo metodo invia un messaggio all’amministratore con alcuni dati della transazione nel caso in cui la verifica incontri problemi.
private function sendReport() { if(SIMULATION) { $add = "- SIMULAZIONE -"; } else { $add = ""; } //messaggio all'amministratore $subject = "$add Problema IPN"; $message = "Si è verificato un problema nella seguente transazione:\r\n\r\n"; $message .= "Nome: " . $_POST['first_name'] . ' ' . $_POST['last_name'] . "\r\n"; $message .= "email: " . $_POST['payer_email'] . "\r\n"; $message .= "id transazione: " . $_POST['txn_id'] . "\r\n"; $message .= "oggetto: " . $_POST['transaction_subject']; mail(ADMIN_MAIL,$subject,$message,"From: " . NO_REPLY); return; }
Come vedi, se rilevo che si tratta di una simulazione, aggiungerò all’oggetto dell’email la parola simulazione, evitando così di fare confusione.
Ora andremo ad implementare il metodo isVerifiedIPN.
Per verificare la legittimità di una notifica devo procedere in questo modo:
- Ricevere la richiesta (POST) da PayPal.
- Creare una richiesta contenente gli stessi valori nello stesso ordine: (chiave=valore&chiave=valore…) preceduti da un valore aggiuntivo: cmd=_notify-validate.
- Aprire una connessione (socket) con PayPal (o con Sandbox se siamo in simulazione)
- Inviare questa richiesta
- PayPal (o Sandbox) risponderà VERIFIED se la richiesta è legittima o INVALID se non lo è.
Se la risposta sarà VERIFIED, la notifica è legittima.
In pratica, alla ricezione di una notifica, contattiamo PayPal e chiediamo: “Ciao, ho appena ricevuto questa notifica, me l’hai mandata tu?”.
Per implementare questo metodo utilizzerò un codice standard di PayPal, al quale apporteremo qualche modifica.
Iniziamo a scrivere il metodo:
private function isVerifiedIPN() { $req = 'cmd=_notify-validate'; foreach ($_POST as $key => $value) { $value = urlencode(stripslashes($value)); $req .= "&$key=$value"; }
Come vedi inseriremo i valori nella variabile $req. Iniziamo con l’inserire il parametro aggiuntivo (cmd=_notify-validate) ed in seguito con un ciclo foreach scorriamo l’array POST ed inseriamo le coppie chiave valore nella variabile $req.
A questo punto siamo pronti per preparare gli headers della richiesta che faremo a PayPal. Ma siccome in questi headers è anche contenuto l’url (che sarà PayPal in produzione o Sandbox in simulazione), prima verifichiamo appunto in che circostanza siamo.
if(SIMULATION) { $url = SIMULATION_URL; } else { $url = PRODUCTION_URL; }
Ora predisponiamo gli headers ed apriamo il soket.
$header = "POST /cgi-bin/webscr HTTP/1.0\r\n"; $header .= "Host: $url:443\r\n"; $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; $header .= "Content-Length: " . strlen($req) . "\r\n\r\n"; $fp = fsockopen ("ssl://$url", 443, $errno, $errstr, 30);
A questo punto possiamo inviare la richiesta e leggerne il risultato:
if (!$fp) { // HTTP ERROR // $errstr - $errno $this->sendReport(); return FALSE; } else { fputs ($fp, $header . $req); while (!feof($fp)) { $res = fgets ($fp, 1024); if (strcmp($res, "VERIFIED") == 0) { fclose ($fp); return TRUE; } else if (strcmp ($res, "INVALID") == 0) { //se la procedura non è legittima invia un email all'amministratore $this->sendReport(); fclose ($fp); return FALSE; } } }
All’inizio, controlliamo che il soket sia stato veramente aperto. In caso contrario troveremo delle indicazioni nelle variabili $errstr e $errno. Nota che stamparle a video sarà perfettamente inutile; questa pagina infatti non sarà visualizzata da nessuno. Eventualmente è possibile farci recapitare tramite email i valori di queste variabili.
Noi semplicemente, utilizzeremo il metodo sendReport() che ci indicherà che la notifica di una certa transazione ha avuto dei problemi. In seguito potremo verificare dall’account di PayPal se vi è stato veramente il pagamento; se è tutto ok potremo attivare manualmente l’account.
A questo punto verifichiamo la risposta. Se è VERIFIED il metodo ritorna TRUE, altrimenti ritornerà FALSE, dopo avere inviato un email all’amministratore.
Quindi implementiamo i due metodi molto simili per verificare che la transazione sia completa (isCompleted) e che l’email corrisponda all’email primario di PayPal (isPrimaryEmail)
private function isCompleted() { if(trim($_POST['payment_status']) === "Completed") { return TRUE; } return FALSE; } private function isPrimaryPayPalEmail() { if(SIMULATION) { $email = PRIMARY_SANDBOX_EMAIL; } else { $email = PRIMARY_PAYPAL_EMAIL; } if(trim($_POST['receiver_email']) === $email) { return TRUE; } return FALSE; }
Come puoi vedere sono molto semplici. Nota che nel secondo metodo, verifichiamo se si tratta di una simulazione o meno e a dipendenza dell’esito procediamo con il confronto del giusto email.
Andiamo ora ad inserire i due metodi astratti, in questo modo:
abstract protected function isVerifiedAmmount(); abstract protected function isNotProcessed();
Ricordo che li dichiariamo astratti in quanto la loro implementazione sarà diversa probabilmente in ogni circostanza. Ma sappiamo comunque che ci servono e che dovremo implementarli.
Ed ora non ci resta che definire il metodo che darà l’ok per l’attivazione dell’account (in questo caso, più in generale darà l’autorizzazione a procedere).
Questo metodo dovrà verificare che tutti i controlli abbiano dato esito positivo.
protected function isReadyTransaction() { if($this->isVerifiedIPN() AND $this->isPrimaryPayPalEmail() AND $this->isNotProcessed() AND $this->isVerifiedAmmount() AND $this->isCompleted()) { return TRUE; } return FALSE; }
Bene. Abbiamo completato la nostra classe parent, che ora ti riporto:
<?php require_once 'pp_config.php'; abstract class IPNListener { private function isVerifiedIPN() { $req = 'cmd=_notify-validate'; foreach ($_POST as $key => $value) { $value = urlencode(stripslashes($value)); $req .= "&$key=$value"; } //Modificando la costante SIMULATION nel file di configurazione //è possibile passare dall'ambiente di simulazione a quello di produzione if(SIMULATION) { $url = SIMULATION_URL; } else { $url = PRODUCTION_URL; } $header = "POST /cgi-bin/webscr HTTP/1.0\r\n"; $header .= "Host: $url:443\r\n"; $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; $header .= "Content-Length: " . strlen($req) . "\r\n\r\n"; $fp = fsockopen ("ssl://$url", 443, $errno, $errstr, 30); if (!$fp) { // HTTP ERROR // $errstr - $errno $this->sendReport(); return FALSE; } else { fputs ($fp, $header . $req); while (!feof($fp)) { $res = fgets ($fp, 1024); if (strcmp($res, "VERIFIED") == 0) { fclose ($fp); return TRUE; } else if (strcmp ($res, "INVALID") == 0) { //se la procedura non è legittima invia un email all'amministratore $this->sendReport(); fclose ($fp); return FALSE; } } } } private function sendReport() { if(SIMULATION) { $add = "- SIMULAZIONE -"; } else { $add = ""; } //messaggio all'amministratore $subject = "$add Problema IPN"; $message = "Si è verificato un problema nella seguente transazione:\r\n\r\n"; $message .= "Nome: " . $_POST['first_name'] . ' ' . $_POST['last_name'] . "\r\n"; $message .= "email: " . $_POST['payer_email'] . "\r\n"; $message .= "id transazione: " . $_POST['txn_id'] . "\r\n"; $message .= "oggetto: " . $_POST['transaction_subject']; mail(ADMIN_MAIL,$subject,$message,"From: " . NO_REPLY); return; } private function isCompleted() { if(trim($_POST['payment_status']) === "Completed") { return TRUE; } return FALSE; } private function isPrimaryPayPalEmail() { if(SIMULATION) { $email = PRIMARY_SANDBOX_EMAIL; } else { $email = PRIMARY_PAYPAL_EMAIL; } if(trim($_POST['receiver_email']) === $email) { return TRUE; } return FALSE; } abstract protected function isVerifiedAmmount(); abstract protected function isNotProcessed(); protected function isReadyTransaction() { if($this->isVerifiedIPN() AND $this->isPrimaryPayPalEmail() AND $this->isNotProcessed() AND $this->isVerifiedAmmount() AND $this->isCompleted()) { return TRUE; } return FALSE; } } ?>
Conclusione
In questo articolo abbiamo visto come realizzare una classe estremamente riutilizzabile. IPNListener fornisce tutti gli strumenti di base per procedere alle verifiche di una notifica ed è utilizzabile indifferentemente in simulazione ed in produzione.
Nel prossimo articolo estenderemo questa classe completandola con i metodi specifici che ci permetteranno di aggiungere il nuovo utente a pagamento avvenuto.
L’applicazione sta iniziando a prendere forma; sei pronto per lo sprint finale?
36 commenti
Trackback e pingback
[...] This post was mentioned on Twitter by yesWEBcan and mtx_maurizio, Simone D'Amico. Simone D'Amico said: Come implementare un pagamento…
[...] Le procedure generali [...]
[...] Le procedure generali [...]
[...] Le procedure generali [...]