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?
Articoli di questa guida
36 commenti
Trackback e pingback
-
Tweets that mention Come implementare un pagamento online: Le procedure generali (3/6) | Your Inspiration Web -- Topsy.com
[...] This post was mentioned on Twitter by yesWEBcan and mtx_maurizio, Simone D'Amico. Simone D'Amico said: Come implementare un pagamento… -
Come implementare un pagamento online: chiarirsi le idee (2/6) | Your Inspiration Web
[...] Le procedure generali [...] -
Come implementare un pagamento online: Preparazione (1/6) | Your Inspiration Web
[...] Le procedure generali [...] -
Come implementare un pagamento online: Testare l’applicazione (5/6) | Your Inspiration Web
[...] Le procedure generali [...]
Io sono prontissimo allo sprint, e non vedo l’ora…
Ottimo articolo..!!
Innanzitutto complimenti per la serie di articoli.. Davvero molto interessanti.
Solo un dubbio, a parte la possibilità di utilizzare il file configurazione e quindi personalizzare l’IPN per ogni evento, perche non utilizzare l’IPN messo a disposizione da Paypal qui: https://www.paypaltech.com/sg2/ ?
pensi che abbia dei limiti o è troppo complesso?
Grazie per i complimenti.
Quei codici vanno bene come base; ma poi vanno riadattati alle situazioni che sono sempre diverse.
Quello che sto presentando in questa guida é un sistema più complesso ma altamente riutulizzabile.
Ciao Maurizio,
la guida è veramente interessante. Ho ripetuto i passi utilizzando un account paypal business. Facendo le prove, però, non mi permette di pagare con Carta di Credito, ma solo iscrivendomi a Paypal o disponendo di un conto attivo Paypal.
Ho sbagliato qualcosa (dimmi di si) o è così che funziona?
Ciao
Ma sei su sandbox o su paypal?
Ciao Maurizio,
mio riferivo a Paypal. Comunque ho cercato sul forum di Paypal ed ho trovato la soluzione. Il tutto era dovuto all’indirizzo mail non certificato.
Ancora complimenti per la guida.
Ciao
ok allora tutto risolto
Che significa “apriamo il socket” ?
Anche se il concetto di socket é un po’ più complesso, si può riassumere dicendo che “aprire un socket” significa “stabilire una connessione”
Ciao Maurizio,
ho provato a contattarti su tuo sito ma non ho ricevuto risposta.
Posto qui viesto che rispondi più facilmente.
Ero già alle prese con lo studio delle API di Paypal in inglese,
e notando che stavi realizzando una guida per fare ciò che volevo
ho ritenuto opportuno seguirti per partire da una base ITALIANA più esperta di me.
Sarà pur vero che scrivo php da oltre 6 anni e che da qualche mese
ho iniziato (colpa del poco tempo) a pensare e scrivere in OO anzichè in procedurale,
però ritengo da come scrivi, e chi è del mestiere lo capisce subito da piccoli particolari, che la tua esperienza in termini di programmazione è assai superiore alla mia.
Detto questo ho 2 domande da porti:
1) Impostando l’indirizzo per l’IPN si è vincolati ad un solo prodotto.
Se volessi utilizzare lo stesso account paypal per più prodotti?
L’indirizzo indicato ovviamente non può essere utilizzato per i vari scopi.
Come rendere l’indirizzo per la notifica dinamico, affichè io possa predisporre,
un file lettore che faccia quel che mi serve?
2) Collegandomi a “de la pena” in tema di socket, questa connessione stabilita serve ad inviare un input ad una determinata pagina che compie qualcosa?
A buona ragione. Che senso ha porre domande relative all’ articolo in forma privata?
1) Nell’ultimo articolo di questa serie viene appunto presentato come implementare dinamicamente il bottone di pagamento con tutti i parametri necessari, compreso l’url del listener
2) La procedura di verifica della legittimità di una richiesta prevede che il listener invii la richiesta ricevuta a paypal; é per questo che si attiva una connessione, per “chiedere” a paypal: “guarda un po’ questa richiesta, me l’hai mandata tu?” E paypal risponde.
Li per li non ci ho pensato.
L’articolo 6 non l’avevo ancora visto.
Un’ultima cosa:
Come mai non mi arrivano quasi mai le IPN al listener?
Di tutte le prove che ho fatto sempre con le stesse impostazioni
solo un paio di volte mi è arrivata l’email da sendReport per avvisarmi che
c’è un problema:
Si è verificato un problema nella seguente transazione:
Nome:
email:
id transazione:
oggetto:
non vengono lette nemmeno le variabili.
PS. non ho minimanente toccato il codice.
Ho iniziato facendo il copia ed incolla.
Le mie modifiche faro quando serve. se serve.
aggiungo…
non ha nemmeno caricato l’utente nel database.
Siccome non hai visto l’articolo 6, forse non avrai nemmeno visto l’articolo 5 (testare l’applicazione) :-)
Ciao Mauriio, credo che ci sia un promlema con il metodo: isVerifiedIPN(), ho effettuato varie prove e non va, ma cambiando il codice :
protected function isReadyTransaction()
{
if(//$this->isVerifiedIPN() AND
$this->isPrimaryPayPalEmail() AND
$this->isNotProcessed() AND
$this->isVerifiedAmmount() AND
$this->isCompleted())
{
return TRUE;
}
return FALSE;
}
}
in:
protected function isReadyTransaction()
{
if(//$this->isVerifiedIPN() AND
$this->isPrimaryPayPalEmail() AND
$this->isNotProcessed() AND
$this->isVerifiedAmmount() AND
$this->isCompleted())
{
return TRUE;
}
return FALSE;
}
}
in questo modo va. è possibile che il codice non comunichi con sandbox o paypal?
Ciao.
scusa ho sbagliato prima a scrivere:
da:
if($this->isVerifiedIPN() AND
a:
if(//$this->isVerifiedIPN() AND
ovvero non gli faccio eseguire il primo controllo. così va
Ciao Giovanni,
come ho già detto molte volte, per me fare un debug alla cieca é impossibile; quindi posso solo darti dei suggerimenti:
E’ possibile che vi sia un errore nell’impostazione del servizio su paypal.
Oppure che vi siano dei problemi di configurazione enl server (estensioni non abilitate, ecc…)
Controlla le impostazioni su paypal e guarda il log degli errori del server
hai perfettamente ragione tu, era un problema legato al server (devevo abilitare la funzione fsockopen), quindi ora è ok!!
ciao Maurizio, ho provato tutto è va bene.
Vorrei capire come fare per passare ora a Paypal e quindi abbandonare la simulazione.
devo cambiare:
define(“SIMULATION”, 1);
in
define(“SIMULATION”, 0);
così??
Grazie.
Esattamente così
ciao Maurizo,
io ho seguito la guida passo passo , ma riscontro il problema che non inserisce i dati nel mio database “localhost” “root” “xxx” “paypal”. non capisco come mai , potresti aiutarmi ?
salve a tutti ha un po di tempo che lo script in questione non mi funziona più
il problema sta qui:
}
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;
}
è come se $res fosse vuoto o mancante mentre se scrivo if (strcmp($res, “”) == 0) viene processato tutto.
questo è l’errore che mi da:
HTTP/1.0 302 Found
vorrei capire se un problema solo mio o generale
grazie
Maurizio, l’errore nel post precedente me lo dava perchè avevo impostato la porta 80, cambiando la porta con la 443 mi da invece questo errore:
HTTP/1.0 500 Server closed connection without sending any data back
Resource id #4
Ho contattato il mio host server e mi hanno garantito che la porta 443 è aperta e che non ci sono firewall che bloccano le comunicazioni con l’esterno.
Puoi fare una prova tu per capire se è un problema di script o di sandbox?
il codice che uso è uguale al tuo esempio e prima mi funzionava anche sulla porta 80
grazie.
ciao maurizio grazie della tua stupenda guida(come sempre)
ho un problema nel far capire a paypal che ho ricevuto il pagamento in pratica nella cronologia ipn mi dice “tentativo in corso”
facendo varie prove tipo sostituendo
ssl://www.sandbox.paypal.com
con
http://www.sandbox.paypal.com
sembra funzionare, nel senso che nella cronologia mi dice finalmente “invio effettuato”
pero’ non mi fa piu’ i controlli verified o invalid
devo necessariamente usare ssl://www.sandbox.paypal.com ?
se si è un problema del mio provider (aruba hosting condiviso) ?
grazie ciao
con ssl://www.sandbox..ecc
mi dice
IPN delivery failed. HTTP error code 500: Internal Server Error
nel test tool
aiuto
Ciao Franco,
premetto (come ho già detto più volte) che per me è assolutamente impossibile capire cosa ci sia che non funziona senza poter disporre direttamente dell’appilcazione. Ciò detto, è sicuramente impossibile fare una transazione al di fuori di un protocollo sicuro.
Aggiungo una cosa che ripeto allo sfinimento.
Basta Aruba, basta altervista, se si vogliono fare cose serie ci vuole anche un hosting serio.
Andresi a scalare l’Everest a piedi nudi?
Ciao Maurizio con vari test ho capito perchè non ricevevo nessun email di errore IPN etc..
Dopo aver modificato questo codice:
$fp = fsockopen (“ssl://$url”, 443, $errno, $errstr, 30);
in:
$fp = fsockopen (“https://$url”, 443, $errno, $errstr, 30); //Ho modificato il protocollo da ssl a https
mi sono arrivate le email di errore nel IPN, a questo punto ho cercato di capire come mai c’era un errore e ho fatto altri test e sono riuscito a vedere in quale parte del codice era il problema, ed era nel THEN dell’IF (!$fp), quindi a questo punto ho visualizzato la variabile $errstr e mi esce scritto: Unable to find the socket transport “https” – did you forget to enable it when you configured PHP.
Come posso risolvere questo problema ? (Utilizzo Edi Host, ti ringrazio per avermelo consigliato mi trovo benissimo)
Ciao maurizio,
Sto impazzendo da più di quarantotto ore, ho seguito il tuo procedimento e l’ho confrontato anche con altre fonti (e ho lo stesso problema anche in un altro modo), in pratica sembra che mi funzioni tutto ma lo strcmp non trova ne VERIFIED ne INVALID, sapresti dirmi com’è possibile? E’ un problema comune? Perchè io non trovo riscontri.
ho letto con piacere la tua guida per implementare un servizio di pagamento su un sito internet.
Ho seguito passo passo quanto descritto e sono arrivato alla fine senza problemi, ho testato il tutto e risulta funzionante al 100%.
Quello che non ho capito è come passare alla produzione, o meglio come faccio a fare in modo che il pulsante del form non punti più a http://www.sandbox.paypal.com, ma bensì a http://www.paypal.com?
Ho cambiato il valore 1 in 0 nel file pp_config.php, ma nonostante ciò quando clicco sul pulsante di pagamento vengo comunque rindirizzato al sito sandbox!
Devo cambiare il pulsante?
Ciao Maurizio, ciao a tutti…
Ho provato e riprovato il tuo codice ma non mi funziona….
Lo sto testando su altervista: può essere che ci sia un problema nell’utilizzo della funzione “fsockopen”? Cercando su google o sul gith di paypal https://github.com/paypal/ipn-code-samples/blob/master/paypal_ipn.php mi pare di capire che sarebbe meglio usare “curl”.
Qualcuno ha provato il codice di Maurizio su altervista? vi funziona? su xoom? help!
Grazie,
Davide
Ciao Maurizio dato che sono alle prime armi, volevo chiederti un aiuto in una modifica al tuo script, tu per la connessione utilizzi “fsockopen” purtroppo l’hosting che ospita il mio sito non permette la comunicazione tramite “https” e mi hanno suggerito di usare il cURL.
Quindi potresti farmi vedere come sostituire con il cURL? Grazie.