Thomas Bandt

Über mich | Kontakt | Archiv

PayPal-Integration (mit ASP.NET)

Ich habe in den letzten Tagen ein PayPal-Zahlungsmodul für unsere Shop-Software entwickelt - nachfolgend finden sich ein paar (natürlich subjektive) Erfahrungswerte und Tipps & Tricks zum Thema.

Workflow - ja wie denn nun?

Bevor man überhaupt eine Zeile Code schreibt, sollte man sich Gedanken über den eigentlichen Workflow machen. Von PayPal selbst bekommt man dazu keine große Unterstützung, beispielsweise in Form einer anschaulichen Referenzimplementierung. Bleibt also der Blick zu anderen Shoppingsystemen. Dabei herausgekommen sind im Prinzip zwei gangbare Lösungen, die ich nachfolgend kurz beschreibe.

Möglicher Workflow - Variante 1

Kunde beginnt Checkout und wählt …

  1. Vorkasse, Nachnahme, Bankeinzug usw.
    1. Er bestätigt den Vorgang, die Bestellung wird gespeichert und fertiggestellt, Bestellnummer erzeugt, Mails verschickt.
  2. PayPal
    1. Er bestätigt den Vorgang, die Bestellung wird abgespeichert und auf „Pending“ gesetzt. Anschließend wird ihm der PayPal-Button zum Bezahlen angezeigt. Nun gibt es 4 Möglichkeiten für nachfolgende Schritte:
      1. Er führt die Zahlung gleich durch und wird von PayPal wieder zurück in den Shop geleitet. Die Bestellung wird abgeschlossen, Bestellnummer generiert, Mails verschickt.
      2. Er führt die Zahlung gleich durch und kehrt nicht zum Shop zurück, bsp. wegen technischer Probleme auf seiner Seite. PayPal sendet im Hintergrund unabhängig vom Kunden eine Zahlungsbenachrichtigung an den Shop (IPN) -> die Bestellung wird abgeschlossen, Bestellnummer generiert, Mails verschickt.
      3. Er führt die Bezahlung niemals durch, die Bestellung bleibt auf „Pending“ stehen und wird nicht abgeschlossen.
      4. Er bricht den Vorgang bei PayPal ab und wird zurück zum Shop geleitet. Hier wird die Bestellung auf „Aborted“ gesetzt und, wenn die Session noch aktiv ist, der Kunde zurück zur Auswahl der Zahlungsweise geführt. Er kann nun den Bestellvorgang erneut abschließen, wobei ein neuer Bestelldatensatz erzeugt wird, der alte ist damit eine "Leiche", die aber keine Auswirkungen hat.

Vorteile:

  1. Es werden wirklich nur Bestellungen abgeschlossen, bei denen die Zahlung auch wirklich durchgeführt wurde.
  2. Damit entstehen auch keine Lücken im Nummernkreis der Rechnungen, da immer nur dann die fortlaufende Rechnungsnummer erhöht wird, wenn eine Bestellung abgeschlossen wird. Das Wesen und der Vorteil von PayPal ist ja die sofortige Bezahlung, die man somit weitestgehend sicherstellen kann.
  3. Der Kunde hat die Möglichkeit, sich auf der PayPal-Seite noch einmal umzuentscheiden und doh noch ein anderes Zahlungsmittel zu wählen.

Nachteile:

  1. Im Prinzip ist die Zahlung via PayPal auch nichts anderes als die Zahlung via Vorkasse – d.h. man kann beide Zahlungsweisen im Prozess durchaus gleich behandeln. Praktisch ist aber ein Klick auf den "Abschließen"-Button, der bei den anderen Verfahren zum verbindlichen Kauf führt, nur ein weiterer Schritt. Erst mit der Zahlung bei PayPal kommt die Bestellung faktisch zustande.
  2. Da die Rechnungsnummer erst nach Abschluss erstellt wird, kann diese auch nicht an PayPal übergeben und dem Kunden somit als Referenznummer angezeigt werden.
  3. Eine spätere Zahlung ist nur möglich, wenn der Kunde sich den Link zu PayPal speichert oder man ihm separat noch eine Zahlungsaufforderung per E-Mail sendet. Das ist evtl. für "Heavy-User" eher eine unnötige Einschränkung, zumal es Szenarien gibt, in denen man bewusst erst später zahlen möchte.

Fazit:

Ein durchaus gangbarer Weg, vor allem dann, wenn man Bestelleingänge ohne Zahlungsdurchführung bei PayPal unbedingt vermeiden will. Denn erzwingt man diese Zahlung nicht, siehe Variante 2, kann es durchaus sein, dass der Käufer den Betrag schuldig bleibt.

Möglicher Workflow- Variane 2

  1. Kunde schließt Bestellprozess durch die Bestätigung immer ab:
    1. Bestellung wird gespeichert.
    2. Rechnungsnummer wird generiert.
    3. E-Mails werden versandt.
      1. Wenn PayPal gewählt, wird ein entsprechender Zahlungs-Link in der Mail angegeben. Das ist nun möglich, da die eindeutige Rechnungsnummer zum Zeitpunkt der PayPal-Zahlung bereits bekannt ist. Diese Rechnungsnummer kann nun auch an PayPal übermittelt werden und Kunden wie Zahlungsempfänger als Referenz zur Zuordnung von Zahlung zu Bestellung dienen.
  2. Wenn PayPal als Zahlungsweise gewählt wurde, wird nach dem Abschluss der Bestellung ein offizieller PayPal-Button angezeigt, über den der Kunde die Bezahlung bei PayPal durchführen kann. Darüber hinaus kann dieser Button auch im Kundenmenü in der Bestellhistorie, sofern vorhanden, angezeigt werden, wenn die Bezahlung noch nicht erfolgt ist. Hierbei gibt es nun folgende Möglichkeiten:
    1. Der Kunde führt die Zahlung erfolgreich durch und wird zurück zum Shop geleitet. Die von PayPal übermittelten Zahlungsinformationen (PDT) werden der Bestellung zugeordnet und gespeichert. Dem Kunden wird Bestätigungs-Seite angezeigt.
    2. Der Kunde führt die Zahlung erfolgreich durch, wird aber nicht zum Shop geleitet. PayPal sendet im Hintergrund automatisch eine Benachrichtigung an den Shop, dass die Bezahlung durchgeführt wurde (IPN). Die von PayPal übermittelten Zahlungsinformationen werden der Bestellung zugeordnet und gespeichert.
    3. Der Kunde zahlt nicht, oder bricht den Bezahlvorgang bei PayPal ab und kehrt nicht zum Shop zurück. Nun kann er in seinem Kundenkonto die Zahlung erneut veranlassen oder den Link in der E-Mail benutzen.
    4. Der Kunde bricht den Zahlungsvorgang bei PayPal ab und kehrt zum Shop zurück. Im Gegensatz zu Variante 1 kann er aber hier nun den Bestellprozess nicht erneut starten, da die Bestellung ja bereits final abgeschlossen wurde. Ihm wird stattdessen ein geeigneter Text samt Information und Zahlungsaufforderung sowie ggf. wieder Der PayPal-Button angezeigt.

Vorteile:

  1. Die Bestellung wird immer abgeschlossen, die Rechnungsnummer immer erzeugt und dem Kunden auch gleich die Bestätigungs-E-Mail zugesandt, in der man ihm auch einen Link zu PayPal zur späteren Zahlung anbieten kann.
  2. Der Klick von der Bestätigung zur Zahlung im Checkout-Prozess ist absolut verbindlich, wie bei den anderen üblichen Zahlungsweisen auch.
  3. Der Kunde kann vollkommen frei entscheiden, wann er die Zahlung tätigt und ist nicht gezwungen sie innerhalb des Bestellprozesses abzuschließen. Das kann für "Heavy-User" sinnvoll sein, aber auch für Leute deren PayPal-Konto z.B. gerade nicht aktiv oder gedeckt ist, oder für das Sie schlicht die Zugangsdaten nicht zur Hand haben. Bei Vorkasse per Banküberweisung kann man den Zeitpunkt der Zahlung ja auch selbst auswählen.

Nachteile:

  1. Keine verbindliche Zahlungsanweisung bis Bestellabschluss, wie man sie z.B. bei Implementierung einer Kreditkartenzahlung sicherstellen kann.

Fazit:

Obwohl etablierte Shoppingsysteme wie beispielsweise xt:commerce auf Variante 1 setzen, halte ich die Zweite für die wesentlich kundenfreundlichere Wahl. Der Kunde kann wie bei anderen Zahlungsweisen auch frei entscheiden, wann er die Zahlung tätigt und für den Verkäufer entstehen prinzipiell keine relevanten Nachteile (außer dass er nicht sofort sein Geld bekommt ...).

Die technische Umsetzung (mit ASP.NET)

Es gilt wie immer zu beachten: viele Wege führen nach Rom. Was sich für mich als nerviges Sackgässchen entpuppte, war das von PayPal angebotene SDK für ASP.NET. Selbiges ist augenscheinlich noch .NET 1.1-Code, den ich mit Visual Studio 2008 nicht kompilieren konnte. Ich habe mich daher relativ schnell entschieden, darauf zu verzichten, und PayPal über die Formularlösung zu implementieren, wie das überall anders auch der Fall ist.

Generell habe ich ein wenig den Eindruck gewonnen, als würde man keinen großartigen Support bekommen, vor allem nicht als ASP.NET-Entwickler, der Fokus liegt hier wohl eher auf dem PHP-Lager - verständlich bei den dort verfügbaren Shopping-Lösungen, die ja vor allem in Deutschland weit verbreitet sind.

Ich will und werde an dieser Stelle nun nicht tief ins Detail gehen oder gar die komplette Implementierung veröffentlichen. Stattdessen greife ich ein paar Punkte auf, die meiner Meinung nach wichtig sind.

1. Einbindung der PayPal HTML-Formulare

Ich habe wie geschrieben die so genannte Standard-Zahlungslösung [4] ohne Nutzung der Webservices usw. verwendet. Das bedeutet, dass man die Daten wie Rechnungsnummer, Betrag usw. usf. per POST an PayPal sendet, im einfachsten Fall sieht das so aus:

   1:  <form action="https://www.paypal.com/cgi-bin/webscr" method="post">
   2:     <input type="hidden" name="cmd" value="_xclick" />
   3:     <input type="hidden" name="business"
   4:        value="paypal@blumentag.com" />
   5:     <input type="hidden" name="item_name"
   6:        value="Ihre Blumentag-Bestellung" />
   7:     <input type="hidden" name="amount" value="3.00" />
   8:     <input type="submit" value="PayPal" />
   9:  </form>

So genial das Modell der ASP.NET-WebForms bisweilen ist, hier zeigt es einem als Entwickler regelmäßig den Stinkefinger. Denn in einem WebForm ist nur ein einziges serverseitiges Formular erlaubt, also:

   1:  <form runat="server"></form>

Da wir heute üblicherweise mit Masterpages und verschachtelten UserControls an verschiedenen Stellen einer Seite arbeiten, gibt es eigentlich in der Praxis keine andere Möglichkeit, als das serverseitige Formular soweit außen um die Seite zu legen, wie nur irgendwie möglich. Meistens landet es also schon kurz nach dem Body-Tag.

Da HTML-Formulare aber nun leider nicht verschachtelt werden können, gibt es ein Problem: wie bekommt man nun das PayPal-Formular in der Seite unter?

Mögliche Lösungen:

  1. Wenn es das Layout irgendwie hergibt, positioniert man das Formular außerhalb des Server-Forms.
  2. Man positioniert das PayPal-Formular außerhalb des Server-Forms und schickt es per JavaScript ab.
  3. Man killt das ServerForm [1] einfach im Fall der Anzeige des PayPal-Formulars raus.
  4. Man verwendet genau für diesen einen Fall eine andere MasterPage, die identisch zur sonst verwendeten ist, aber as serverseitige Formular anders positioniert.

Ich habe mich für 4. entschieden, weil es a) relativ flexibel ist und b) übrige Formulare, z.B. ein Login-Formular in der Sidebar, welche PostBacks nutzen, noch am Leben lässt. Wie man eine MasterPage dynamisch austauscht bzw. setzt habe ich unter [2] beschrieben. Alternativ könnte man auch verschachtelte MasterPages benutzen ([3]).

2. Von der Theorie in die Praxis - richtig testen

Zum Testen hat sich PayPal etwas Gutes einfallen lassen - sie haben ihre komplette Umgebung inkl. allen Apis in einer Testumgebung (Sandbox [5]) nachgebaut und bieten diese an. Man kann sich dort verschiedenste Business- und Kundenkonten einrichten und so Zahlungen senden und empfangen, stornieren usw. usf. - inkl. E-Mail-Versand und allem drum und dran. D.h. man ist nicht gezwungen echtes Geld hin und her zu schicken.

Das muss man aber gleichwohl, wenn die Shoplösung online geht und live getestet werden muss. Hier bietet es sich an, im Shop ein Produkt mit einem Betrag von 0,01 Euro sowie eine kostenlose Versandmethode anzulegen, über die man dann quasi Bestellungen im Gesamtwert von 1 Cent ordern kann. Witzigerweise zieht PayPal beim Empfänger den einen Cent wirklich als Gebühren ein ;-).

Zum Testen mit der Sandbox muss man in der aktuellen Shopsession auch im PayPal Developer Center authentifiziert sein, da alle Transaktionen in die und von der Sandbox im eigenen Benutzerkontext laufen. Einen Account bekommt man unter [6]

3. PDT und IPN - was ist das und wie funktioniert es

Amis stehen ja ungemein auf Abkürzungen, also kurze Aufklärung: PDT steht bei PayPal für die "Übertragung der Zahlungsdaten", IPN für die "Sofortige Zahlungsbestätigung".

Was ist der Unterschied?

Es gibt inhaltlich eigentlich keinen - auf beiden Wegen gelangen Daten von der Bestellung von PayPal zurück zur Shop-Software. Den Unterschied macht die Art bzw. der Zeitpunkt der Sendung. PDT erfolgt immer dann, wenn der Kunde eine Transaktion bei PayPal abschließt und dann von PayPal zurück zum Shop geleitet wird, oder diesen über einen speziellen Link selbst wieder aufruft. In dem Moment erhält man von PayPal eine Transaktionsnummer, über die man die Kaufinformationen von PayPal anfordern kann, gleich mehr dazu.

IPN wiederum ist völlig unabhängig von der Session des Kunden. D.h. der kann die Transaktion abschließen und dann nicht zum Shop zurückkehren, also eine andere URL aufrufen, den Zurück-Button des Browsers verwenden oder einfach tot vom Stuhl fallen. Was man natürlich nicht hoffen will ;-).

Denn IPN sendet an eine vorab im Verkäufer-Kundenkonto definierte bzw. bei der Transaktion im Formular mit übergebene URL Daten, um Informationen zu dieser Bestellung zu übermitteln. Sendet dann der Shop seinerseits den Http-Status-Code 200 zurück, ist die Geschichte abgeschlossen - tut er das nicht, bsp. weil etwas schiefgelaufen ist in der Shopsoftware und ein Fehler übermittelt wird, versucht es PayPal noch bis zu 4 Tage lang - und zwar im 10-Minuten-Takt.

Im Prinzip würde es also ausreichen nur IPN zu integrieren, allerdings habe ich während der Entwicklung dieser Tage selbst erlebt, wie die PayPal-Server über mehrere Stunden nur sehr schwer bis gar nicht erreichbar waren, und die IPN-Calls mitunter erst eine geschlagene Stunde nach Transaktionsende im Shop eingingen. Schon deshalb baut man besser auch PDT ein, weil man so die sofortige Rückmeldung von PayPal hat und seinerseits evtl. die Bestellung abschließen kann (wenn man z.B. oben die 1. Workflow-Variante verwendet).

Hilfreich bei Fehlern oder lahmen Response-Zeiten von PayPal ist im Übrigen der "Live Site Status" [8], hier gibt PayPal recht transparent laufende Voränge bekannt.

4. PDT - Tipps zur Implementierung

Wie der Dokumentation zu entnehmen ist, erwartet PayPal für PDT die Zusendung einiger Parameter per POST, und sendet dann seinerseits in der Antwort die dazugehörigen Parameter. In der ersten Zeile steht dabei, ob die Anfrage erfolgreich war, oder nicht. Für die Auswertung kann man folgende Klasse verwenden:

   1:  public class PayPalPDT
   2:  {
   3:   
   4:      public PayPalPDT()
   5:      {
   6:          Answer = new NameValueCollection();
   7:      }
   8:   
   9:      public string TransactionID { get; set; }
  10:      public string IdentificationToken { get; set; }
  11:      public NameValueCollection Answer { get; set; }
  12:      public string Url { get; set; }
  13:   
  14:      public void Send()
  15:      {
  16:   
  17:          NameValueCollection formData = new NameValueCollection();
  18:          formData.Add("cmd", "_notify-synch");
  19:          formData.Add("tx", TransactionID);
  20:          formData.Add("at", IdentificationToken);
  21:   
  22:          WebClient client = new WebClient();
  23:   
  24:          byte[] message = PrepareMessage(formData, "iso-8859-1");
  25:          string answer = Encoding.Default.GetString(client.UploadData(Url, "POST", message));
  26:   
  27:          string[] rows = answer.Split('\n');
  28:   
  29:          for (int i = 0; i < rows.Length; i++)
  30:          {
  31:              if (i == 0)
  32:              {
  33:                  Answer.Add("Status", rows[i]);
  34:              }
  35:              else if (!string.IsNullOrEmpty(rows[i]) && rows[i].Trim().Length > 0)
  36:              {
  37:                  string[] nameValue = rows[i].Split('=');
  38:                  Answer.Add(nameValue[0], HttpUtility.UrlDecode(nameValue[1]).Trim());
  39:              }
  40:          }
  41:   
  42:      }
  43:   
  44:      private static byte[] PrepareMessage(NameValueCollection data, string encoding)
  45:      {
  46:          Encoding enc = Encoding.GetEncoding(encoding);
  47:          StringBuilder builder = new StringBuilder();
  48:          foreach (string name in data)
  49:          {
  50:              string encodedName = HttpUtility.UrlEncode(name, enc);
  51:              string encodedValue = HttpUtility.UrlEncode(data[name], enc);
  52:   
  53:              builder.Append(encodedName);
  54:              builder.Append('=');
  55:              builder.Append(encodedValue);
  56:              builder.Append('&');
  57:   
  58:          }
  59:          builder.Remove(builder.Length - 1, 1);
  60:          byte[] bytes = Encoding.ASCII.GetBytes(builder.ToString());
  61:          return bytes;
  62:      }
  63:   
  64:  }

5. IPN - Tipps zur Implementierung

Bei IPN ist das Verfahren einfach: man bekommt an die definierte URL, am besten verwendet man einen HttpHandler, Daten via POST gesendet. Diese muss man 1:1 + dem Parameter cmd mit Wert "_notify-validate" zurück an PayPal senden, und bekommt dann ein "VERIFIED" oder eben nicht.

Das Senden der Post-Message kann man z.B. wieder mit Hilfe der WebClient-Klasse machen:

WebClient client = new WebClient();
string message = Context.Request.Form.ToString() + "&cmd=_notify-validate";
string status = Encoding.Default.GetString(client.UploadData("http://...", "POST", Encoding.Default.GetBytes(message)));

Die übermittelten Parameter, die sich im Übrigen in der Variablen-Referenz finden [9], kann man wie folgt auseinandernehmen und dann einzeln abrufen:

   1:  NameValueCollection parameters = new NameValueCollection();
   2:  string[] rows = message.Split('&');
   3:  for (int i = 0; i < rows.Length; i++)
   4:  {
   5:      if (!string.IsNullOrEmpty(rows[i]) && rows[i].Trim().Length > 0)
   6:      {
   7:          string[] nameValue = rows[i].Split('=');
   8:          parameters.Add(nameValue[0], HttpUtility.UrlDecode(nameValue[1]).Trim());
   9:      }
  10:  }

Abruf der Rechnungsnummer: parameters["invoice"]

6. Sonstiges - Buttons und Links

Im PayPal-Kunden-Login für Business-Kunden finden sich lediglich Buttons und Grafiken zur Information über PayPal für die Endkunden im Shop, einen "Kaufen-Button" findet man hier nur im Zusammenhang mit dem Express-Kauf. Es hat eine Weile gedauert bis ich zufällig die richtige Site gefunden habe, nämlich die für die "Jetzt kaufen"-Buttons [7].

Wählt man hier den Punkt "Button-Verschlüsselung" ab, gibt einem der Generator noch etwas Nettes aus: nämlich einen Link, den man beispielsweise in den Kaufbestätigungs-E-Mails an die Endkunden schicken kann, und über die sie dann bezahlen können. Es ist im Übrigen die gleiche URL, wie sie auch für die POST-Aufrufe verwendet wird, d.h. man kann auch die gleichen Parameter, z.B. für Rechnungsnummer (&invoice=) oder das eigene Freifeld (&custom=) anhängen.

Fazit

PayPal kann einen ganz schön ins Schwitzen bringen, aber wenn man einmal drin ist und den richtigen Weg für sich gefunden hat, macht es auch Spaß, weil sie gerade mit der Sandbox und dem letztendlich wirklich ausgereiften Workflow innerhalb der PayPal-Site gute Voraussetzungen liefern. Ich hoffe dass mit diesem kurzen Überblick der eine oder andere den Einstieg etwas schneller finden kann.

Link-Verzeichnis:

  1. Jeremy Schneider - Hide form tag, but leave content
  2. MasterPage on the fly ändern
  3. ASP.NET 3.5 - Verschachtelte Masterpages
  4. Referenz der Standard-Variablen
  5. PayPal Sandbox
  6. PayPal Developer Center
  7. "Jetzt kaufen"- und "Jetzt bezahlen"-Buttons und E-Mail-Links erzeugen.
  8. PayPal Live Site Status
  9. Variablenreferenz für IPN/PDT


« Zurück  |  Weiter »