Häufige Fehler bei Finanztransaktionen vermeiden

MoneyPHP: Internationale Transaktionen leicht gemacht
Keine Kommentare

Ein Softwarefehler bei der Risikoberechnung verursachte 217 Millionen Dollar an Investorenverlusten [1]. Es handelte sich um einen Fehler in grundlegenden Dezimaloperationen, die von einem Softwareentwickler programmiert wurden. Das zeigt uns, wie wichtig und verantwortungsvoll die Arbeit eines Entwicklers ist. Sind Sie sicher, dass Sie wissen, wie Sie solche Fehler in Ihren eigenen PHP-Anwendungen vermeiden können?

Ich arbeite bereits seit 12 Jahren in der Softwareentwicklungsbranche und bei jedem Projekt, in das ich involviert war, ging es um die Verarbeitung von Finanztransaktionen. Ob es sich um einen Webshop, ein Nachrichtenportal, eine Affiliate-Marketing-Plattform oder ein internationales Zahlungsportal gehandelt hat – jedes Projekt hat einige monetäre Berechnungen durchgeführt. Es ist offensichtlich eine verantwortungsvolle Arbeit, und doch kommt es immer wieder vor, dass das Rad auf falsche Weise neu erfunden werden soll. Ist die richtige Verarbeitung von Finanztransaktionen so schwierig? Gibt es zuverlässige und verifizierte Lösungen, die wir wählen können? Wir werden uns weiter in dieses Thema vertiefen, indem wir einen sehr grundlegenden Schritt ausführen – die Erstellung einer Quittung für einen Kunden.

Geld in aller Welt

Bei über 200 Währungen auf der Welt gibt es viele Stolpersteine, die bei der Entwicklung einer Finanz-App beachtet werden müssen. Selbst wenn nur eine Währung verwendet werden soll, steckt der Teufel oftmals im Detail. Besonders schwierig wird es, wenn man sich an internationale Märkte heranwagen will. Fangen wir von vorne an. Wie schreibt und liest man Geldbeträge? Die meisten Währungen haben Untereinheiten, z. B. entspricht 1 Euro 100 Cent, daher ist es praktisch, Dezimalbrüche zu verwenden. Das Beispiel von 1 Euro und 23 Cent kann so etwa mit 1,23 EUR oder 1,23 € ausgedrückt werden.

Ein Cent lässt sich nicht halbieren, sodass von Zeit zu Zeit Rundungen vorgenommen werden müssen. Sollen zu 1,23 € 19 Prozent Steuern hinzugerechnet werden, ist das Ergebnis zunächst 1,4637 – nun muss entschieden werden, wie damit weiter verfahren werden soll. Auf- oder abrunden? Je nach örtlichem Gesetz könnten Sie am Ende entweder 1,46 oder 1,47 erhalten. Diese Unklarheit ist das Erste, was mit Product Ownern, Branchenexperten oder Anwälten geklärt werden muss. Diese Gespräche sollten niemals allein geführt werden – Ihre Aufgabe ist es, geschäftliche Anforderungen umzusetzen, nicht sie zu erraten!

Interessant wird es, wenn man verschiedene Währungen verarbeiten muss. Dann muss nicht nur Euro von Dollar unterschieden werden – es geht auch um die korrekte Umrechnung. Genauso wie Kilogramm und Pfund nicht direkt addieren werden können, können Euro und Dollar nicht zusammengezählt werden, ohne den Wechselkurs zwischen diesen beiden Währungen zu kennen. Die Kurse sind sicherlich jederzeit irgendwo im Internet zu finden, sie müssen jedoch von einer vertrauenswürdigen Quelle stammen, um die richtigen Berechnungen anstellen zu können.

Wie können die angesprochenen Probleme nun möglichst elegant gelöst werden, ohne dafür jedes Mal das Rad neu erfinden zu müssen? Wenn Millionen von Entwicklern auf der ganzen Welt mit ähnlichen Problemen zu kämpfen haben, kann davon ausgegangen werden, dass es einige gebrauchsfertige Lösungen gibt.

Warum Sie nicht alles selbst entwickeln sollten

Auf den ersten Blick sieht die Berechnung einer Quittung einfach aus: Man muss nur ein paar Zahlen addieren, richtig? Aber wie kann garantiert werden, dass die Berechnungen korrekt sind? Leider machen es uns die Computer nicht so einfach. Die CPUs wissen nicht, was eine Dezimalzahl ist. Wenn Sie einer Variablen eine Zahl wie 1,23 zuweisen, gibt es keine Garantie, dass eine CPU genau diesen Wert beibehält. Das liegt daran, dass CPUs und Programmiersprachen den IEEE-754-Standard verwenden, um Zahlen als Brüche zu speichern. Es sind reelle Zahlen, und der Datentyp heißt Floating Point [2] (oder kurz Float). Als der IEEE-Standard in den 1980er Jahren geschaffen wurde, verfügten Computer nur über sehr begrenzte Rechenleistung und Speicher. Die Ingenieure wollten einen möglichst großen Bereich von reellen Zahlen in nur 32 Bit unterbringen. Aus diesem Grund entschieden sie sich dafür, einen binären Typ statt eines Dezimaltyps zu verwenden. Jede Float Number wird als 2 ^ Exponent x Mantisse dargestellt. Wird dieser Wert in eine Dezimalzahl umgewandelt, ergibt sich sehr wahrscheinlich ein Präzisionsfehler.

Solche kleinen Fehler sind für wissenschaftliche Berechnungen in Ordnung, aber für die Finanzwelt völlig inakzeptabel. Leider ist die einzige Möglichkeit, mit PHP präzise Dezimalrechnungen durchzuführen, die Verwendung einer Bibliothek wie BC Math oder PHP Decimal. Sie ist nicht in die Sprache selbst integriert. Ein weiteres Problem, insbesondere für internationale Webanwendungen, ist die Unterscheidung von Währungen. Wenn eine Reihe von Zahlen verwechselt wird, ohne eine Idee von deren Währungen zu haben, werden die Berechnungen grundlegend falsch sein. Wenn die Prinzipien der objektorientierten Programmierung angewendet werden, sollte es ziemlich einfach sein, eine dedizierte Datenstruktur für Währungen und Finanzen zu erstellen.

Das Money-Pattern

In seinem Buch von 2002 schlug Martin Fowler eine Money-Struktur [3] vor, um einen Geldbetrag mit einer Währung zu kombinieren. Mit OOP können Sie diesen Wert in einem Objekt zusammenfassen und so sicherstellen, dass die Berechnungen durch die Klasse Money geschützt sind. Das Money-Pattern wurde für mehrere Sprachen wie PHP, Java und JavaScript implementiert. Es liegt nahe, dass die PHP-Implementierung MoneyPHP [4] heißt. Es handelt sich um eine ausgereifte Library, die es seit bereits seit fast zehn Jahren gibt. Listing 1 enthält ein Beispiel für die Verwendung.

use Money\Money;


// create an object of 1,23 EUR

$net = new Money(123, new Currency('EUR'));


// add 19% of tax, round up

$gross = $net->multiply('1.19', Money::ROUND_UP);


// output is 147, which means 1,47 EUR

echo $gross->getAmount();

Mit MoneyPHP können wir uns bei drei Dingen sicher sein: korrekte Dezimalberechnungen, Vermeidung von Fehlern bei der Verwendung mehrerer Währungen und Vermeidung versehentlicher Wertüberschreibungen, da Money ist ein unveränderliches Objekt ist. Jedes Mal, wenn eine Addition, Subtraktion oder eine andere Operation durchgeführt wird, erhalten wir eine neue Instanz der Money-Klasse. Dadurch werden Bugs vermieden, insbesondere wenn Ihre Money-Objekte zwischen mehreren Klassen wechseln, die die komplexe Geschäftslogik Ihrer Anwendung implementieren.

Intern speichert ein Money-Objekt den Betrag immer als String, d. h. in der kleinsten Untereinheit einer Währung. Für Euro ist das der Betrag in Cent, im obigen Beispiel entsprechen 1,23 € 123 Cent. Wenn also ein Eurobetrag vom Benutzer eingegeben wird, muss dieser nur mit 100 multipliziert oder das Dezimaltrennzeichen entfernt werden, um das Money-Objekt zu erstellen. Bei Währungen wie dem japanischen Yen oder dem ungarischen Forint ist es jedoch komplizierter. Der Yen hat aktuell keine Untereinheiten (sehr wohl aber in der Vergangenheit), daher erwartet MoneyPHP den Betrag in Yen. In Forint sind keine Münzen kleiner als 5 Ft im Umlauf, aber die 1/100-Untereinheit wird immer noch für Berechnungen verwendet. Wer Forint verwendet, muss sie für MoneyPHP immer noch mit 100 multiplizieren. Für die Verwendung von verschiedenen Währungen muss daher immer auch auf Details wie die eben erwähnten geachtet werden. Hilfestellung kann die ISO-4217-Liste der Währungen geben [5].

Die Währung richtig formatieren

Wenn wir dem Benutzer einen Geldbetrag darstellen, müssen wir die Sprache und Region berücksichtigen, die der Benutzer bevorzugt. Es reicht nicht aus, ein einfaches echo wie im ersten Listing zu verwenden. Verschiedene Regionen haben unterschiedliche Formatierungsstandards für Zahlen und Währungen. Einige Leute verwenden einen Punkt anstelle eines Kommas, um den Bruchteil zu trennen. Andere setzen die Währung vor den Betrag. Tabelle 1 zeigt einige Beispiele dafür.

Sprache und Region Beispiel eines Betrags
Deutsch (Deutschland) 12.345,67 €
Niederländisch (Niederlande) € 12.345,67
Polnisch (Polen) 12 345,67 €

Tabelle 1: Beispiele für Währungsformate in verschiedenen Sprachen und Regionen

Die meisten Money-Bibliotheken wie MoneyPHP bieten eine an den lokalen Gegebenheiten orientierte Formatierung der Geldbeträge an. Sie kombinieren Zahlen- und Währungsformatierungsregeln, die für ein Land und eine Sprache spezifisch sind (Listing 2).

use Money\Money;
use Money\Currencies\ISOCurrencies;
use Money\Currency;
use Money\Formatter\IntlMoneyFormatter;
use Money\Money;

// we need a repository of ISO standard currencies
$currencies = new ISOCurrencies();

// create an object of 1,23 EUR
$money = new Money(123, new Currency('EUR'));

// use a PHP "intl" number formatter for Germany...
$numberFormatter = new \NumberFormatter('de_DE', \NumberFormatter::CURRENCY);

// ...together with MoneyPHP formatter
$moneyFormatter = new IntlMoneyFormatter($numberFormatter, $currencies);

// output: 1,23 €
echo $moneyFormatter->format($money);

Im Beispiel in Listing 2 führt MoneyPHP ein Konzept von Währungsrepositorys ein. Bisher hatten wir nur einen Währungscode verwendet, aber jetzt muss der Formatter genau wissen, wie viele Stellen nach dem Dezimalzeichen für eine bestimmte Währung erwartet werden. Die Entwickler von MoneyPHP wollten sich nicht nur auf die Standard-ISO-Währungen beschränken. Wir werden vorerst das ISO-Repository verwenden, könnten allerdings auch ein eigenes implementieren, zum Beispiel für Kryptowährungen.

Ein weiterer Anwendungsfall eines Formatters für Money ist, Daten an ein API zu senden, das als Eingabe eine Dezimalzahl erwartet. Einige APIs könnten dem Schema.org-Standard PriceSpecification [6] folgen, der eine Dezimalzahl mit einem Punkt erwartet. Statt sich nun selbst eine Formatierung auszudenken, können Sie die Klasse DecimalMoneyFormatter verwenden, die in Listing 3 zu sehen ist.

use Money\Currency;
use Money\Formatter\DecimalMoneyFormatter;
use Money\Money;

// create an object of 1,23 EUR
$money = new Money(123, new Currency('EUR'));

// use a decimal number formatter
$decimalFormatter = new DecimalMoneyFormatter($currencies);

// output: 1.23
echo $decimalFormatter->format($money);

Integration von Backend- und Frontend-Layern

Bis jetzt haben wir eine gute Lösung gefunden, um Währungen in PHP zu speichern und sie an einen Benutzer auszugeben. Die meisten Webanwendungen verwenden jedoch mehrere Systeme. Ein Benutzer beginnt seine Reise mit dem Frontend-Layer, der dann eine Verbindung zum Backend herstellt, außerdem gibt es dann normalerweise eine Datenbank, zum Beispiel MySQL. Die Daten wandern hin und her – und auf jeder Ebene besteht die Gefahr, dass ein Teil der monetären Daten verloren geht. Aus diesem Grund müssen diese Daten mit besonderer Vorsicht verwaltet werden, angefangen bei dem Zeitpunkt, zu dem die Benutzereingaben erhalten wurden.

Nehmen wir an, es gibt einen Administrator, der einem Webshop Produkte hinzufügt und deren Preise definiert. Das einfachste Szenario wäre es, wenn ein HTML-Formular direkt per POST-Methode an das PHP-Skript gesendet wird. In diesem Fall erhalten wir den Preis in der Regel als lokalisierte Zahl, z. B. 1,23. Um ein Money-Objekt zu erstellen, muss es korrekt geparst werden.

Ein schwerwiegender Fehler, den ich an dieser Stelle mal gesehen habe, bestand darin, diesen Wert in ein Float umzuwandeln, dann mit 100 zu multiplizieren und schließlich das Ergebnis in ein Int zu konvertieren. Da der Float-Typ nicht präzise war und Int immer abgerundet wurde, ist ein Preis nicht auf 4,10 € festlegbar – er wird immer auf 4,09 € geändert. Um solche Fehler zu vermeiden, werden wir einen speziellen Money-Parser verwenden (Listing 4).

use Money\Currency;
use Money\Currencies\ISOCurrencies;
use Money\Parser\IntlLocalizedDecimalParser;

// we need a repository of ISO standard currencies
$currencies = new ISOCurrencies();

// use a PHP "intl" number formatter for Germany...
$numberFormatter = new \NumberFormatter('de_DE', \NumberFormatter::DECIMAL);

// ...together with MoneyPHP parser
$moneyParser = new IntlLocalizedDecimalParser($numberFormatter, $currencies);

// parse user input as euro
$money = $moneyParser->parse('1.234,56', new Currency('EUR'));

// output: 123456
echo $money->getAmount();

Natürlich kann der Frontend-Layer viel komplizierter sein als das; auf dieser Ebene können ja auch Client-seitige Validierungen oder sogar Berechnungen in JavaScript durchgeführt werden. Wie bereits erwähnt, gibt es bereits Implementierungen des Money-Patterns für JavaScript. Hier kann zwischen verschiedenen Bibliotheken wie currency.js, Dinero.js oder Money.js gewählt werden, das ist jedoch nicht Gegenstand dieses Artikels.

Im umgekehrten Fall müssen Sie aber möglicherweise ein Money-Objekt an JSON serialisieren und diese Daten an den Frontend-Layer senden. Die Money-Klasse implementiert das JsonSerializable-Interface, Sie müssen also nur die Funktion json_encode() verwenden, um die JSON-Datei zu empfangen, wie in Listing 5 gezeigt.

use Money\Money;

// create an object of 1,23 EUR
$net = new Money(123, new Currency('EUR'));

// output: {"amount":"123","currency":"EUR"}
echo json_encode($net);

Wenn Sie monetäre Daten zwischen Systemen austauschen, ist es immer entscheidend, zu wissen, ob beide Systeme die gleiche Einheit verwenden. Manchmal kann ein API den Betrag in Euro und ein anderes in Cent erwarten. Diese Diskrepanzen können zu Verwirrung und schwerwiegenden Fehlern führen. Es ist besser, aussagekräftigere Feldnamen wie amountDecimal oder amountInteger anstelle eines einfachen amount zu verwenden.

Geldbeträge in einer SQL-Datenbank verwalten

Für die SQL-Datenbanken ist die Lösung schwieriger, weil tatsächlich keine von ihnen das Money-Pattern implementiert. Einige Datenbanksysteme haben monetäre Datentypen, die eine global festgelegte Währung verwenden, aber das ist nicht das, was wir anstreben. Wir wollen beliebige Währungen im System speichern, ohne sie durcheinander zu bringen. Selbst wenn unsere Anwendung nur eine Währung verwendet, besteht die Chance, dass eines Tages eine neue Währung in unserem Land eingeführt wird. Wir müssen aussagekräftige Werte in der Datenbank speichern, um Probleme bei der Migration oder anderen Operationen, die eine Währungsumrechnung beinhalten, zu vermeiden.

Eine Lösung wäre, den Betrag in einer Spalte zu speichern, normalerweise vom Typ DECIMAL oder BIGINT (auf keinen Fall FLOAT oder DOUBLE!), und dann eine CHAR(3)-Spalte zu verwenden, um den ISO-Währungscode zu speichern. Das erlaubt es, einige Berechnungen direkt in der Datenbank durchzuführen, aber gleichzeitig ist es einfacher, die Währungen zu verwechseln. Eine andere Lösung besteht also darin, eine einzige VARCHAR-Spalte zu verwenden, um einen kodierten Wert wie EUR 123 zu speichern. Dann muss man den String in PHP dekodieren. So oder so müssen Sie darauf achten, entweder Euro oder Cent konsequent zu verwenden (Kasten: „Systeme ohne Fehler integrieren“).

Es ist nicht leicht zu entscheiden, wie groß der Datentyp für die Speicherung der Währung sein soll. Ein unsignierter 32-Bit INT-Typ würde Beträge über 4 Milliarden Euro ermöglichen, was mehr als genug erscheint. Allerdings haben Währungen in der Welt einen unterschiedlichen Wert, und sie können einer Hyperinflation unterliegen [7]. Im Jahr 2009 kostete in Simbabwe ein US-Dollar 1025 einer lokalen Währung. Ja, das ist die Zahl 1 mit 25 Nullen. Solche Extremfälle können schnell zu einem Integer Overflow führen. Sie sollten regelmäßig überprüfen, ob Ihre Systeme die aktuellen Geldbeträge noch verarbeiten können.

Ein Rechnungsmodell erstellen

Es spielt keine Rolle, ob Sie es eine Quittung, eine Rechnung oder einen Beleg nennen – der Punkt ist derselbe. Wir möchten ein juristisches Dokument erstellen, um den Kauf eines Kunden zu bestätigen. Die Struktur dieses Dokuments wird normalerweise durch das Gesetz festgelegt. Einige Felder, wie die Namen von Käufer und Verkäufer und eine Gesamtsumme, sind obligatorisch. Einige zusätzliche Felder können optional sein, aber sie können dennoch wertvolle Daten enthalten. Um anzufangen, greifen Sie sich einfach eine zufällig auf Ihrem Schreibtisch oder in Ihrer Brieftasche liegende Quittung und beginnen Sie mit der Untersuchung ihres Inhalts.

Dieselbe Quittung wird höchstwahrscheinlich in mehreren Formen vorliegen. Ein Kunde wird sie auf einer Webseite direkt nach einem Kauf und später in einem Abschnitt wie „“>Meine Bestellungen“ sehen. Vielleicht wird er oder sie eine E-Mail-Bestätigung mit einem PDF-Anhang erhalten. Wir haben mindestens vier Stellen, an denen ähnliche Daten präsentiert werden.

Es ist praktisch, eine Abstraktion des Dokuments zu erstellen. Lassen Sie uns eine Klasse namens Receipt entwickeln (Listing 6), die alle vom Gesetz, von Ihrer Firma und den Kunden geforderten Informationen enthält. An diesem Punkt werden wir uns nicht um SQL, JSON, HTML oder das PDF kümmern. Es geht nur um eine reine Datenstruktur mit einigen implementierten grundlegenden Verhaltensweisen, also ein abstraktes Modell.

use Money\Currency;
use Money\Money;

final class Contractor
{
  private string $name;
  private string $address;
  // constructor and getters below...
}

final class ReceiptItem
{
  private Money $unitPrice;
  private int $quantity;
  private int $tax;

  public function __construct(Money $unitPrice, int $quantity, int $tax)
  {
    // verify data correctness
    if ($quantity < 0 || $tax < 0) {
      throw new \Exception('Quantity and tax cannot be negative');
    }
    $this->unitPrice = $unitPrice;
    $this->quantity = $quantity;
    $this->tax = $tax;
  }

  // calculate total value of an item/row
  public function getTotalPrice(): Money
  {
    return $this->unitPrice
      ->multiply($this->quantity)
      ->multiply('1.' . $this->tax, Money::ROUND_UP);
  }
}

final class Receipt
{
  private Currency $currency;
  private Contractor $seller;
  private Contractor $buyer;
  private \DateTimeImmutable $issueDate;
  private \DateTimeImmutable $dueDate;
  /** @var array|ReceiptItem[] */
  private array $items = [];

  public function __construct(Currency $currency)
  {
    $this->currency = $currency;
  }

  public function addItem(Money $unitPrice, int $quantity, int $tax)
  {
    // check if the currencies match
    if (!$this->currency->equals($unitPrice->getCurrency())) {
      throw new \Exception('Currencies must match');
    }
    $this->items[] = new ReceiptItem($unitPrice, $quantity, $tax);
  }

  public function getTotalAmount(): Money
  {
    $sum = new Money(0, $this->currency);
    foreach ($this->items as $item) {
      $sum = $sum->add($item->getTotalPrice());
    }
    return $sum;
  }

  // other getters and setters below...
}

Im Beispiel in Listing 6 haben wir nun eine einfache Receipt-Klasse erstellt, die grundlegende Verkäufer- und Käuferdaten, das Ausstellungsdatum des Dokuments, das Fälligkeitsdatum der Zahlung und eine Liste von Artikeln (Zeilen) enthält. Der Beleg wird auf einer einzigen Währung basieren, daher haben wir eine zusätzliche Prüfung hinzugefügt, um sicherzustellen, dass alle Artikel dieselbe Währung haben. Es gibt eine Methode, mit der der zu zahlende Gesamtbetrag berechnet wird. Es ist sehr einfach, Tests für eine so einfache Klasse zu schreiben, die keine Verbindungen zu externen Systemen hat. Wir verwenden dafür PHPUnit [8] (Listing 7).

use Money\Currency;
use Money\Money;
use PHPUnit\Framework\TestCase;

final class ReceiptTest extends TestCase
{
  public function testTotalAmount(): void
  {
    // given
    $euro = new Currency('EUR');
    $receipt = new Receipt($euro);
    $receipt->addItem(new Money(123, $euro), 10, 19);

    // when
    $total = $receipt->getTotalAmount();

    // then
    self::assertEquals('1464', $total->getAmount());
    self::assertTrue($euro->equals($total->getCurrency()));
  }
}

Der Test ist sehr einfach. Er legt ein Receipt-Objekt mit Euro als Währung an und fügt dann einen Artikel mit einem Stückpreis von 1,23 € mal 10 Stück hinzu. Der Steuersatz für diesen Artikel beträgt 19 Prozent, also berechnet der Test den Gesamtbetrag, der 14,64 € betragen sollte.

Jetzt können wir jede noch verbleibende Logik zur Berechnung von Steuern, Zwischensummen und allen anderen Beträgen, die wir auf der Rechnung vorlegen müssen, testen und umsetzen. Im Allgemeinen wollen wir all diese Finanzlogik in unserer Receipt-Klasse einkapseln. Aus diesem Grund akzeptieren wir die berechneten Werte nicht als Benutzereingabe. Wir möchten sicherstellen, dass unsere Receipt-Objekte immer einen gültigen Zustand enthalten.

Um Ihr Receipt-Objekt mit anderen Systemen austauschbar zu machen, müssen Sie eine zusätzliche Infrastrukturschicht implementieren. Um das Objekt als JSON zu kodieren oder aus einem JSON-String zu dekodieren, erstellen wir nun unsere eigenen Serialisierer und Deserialisierer. Die Kodierungsarbeit kann jedoch auch durch die Implementierung der JsonSerializable-Schnittstelle in der Receipt-Klasse erfolgen. Die resultierenden Daten sollten zum Senden an den Frontend-Layer bereit sein (Listing 8).

final class Receipt implements JsonSerializable
{
  // prepare an array that will be encoded to JSON
  public function jsonSerialize(): array
  {
    return [
      'currency' => $this->currency->getCode(),
      'totalAmountInteger' => $this->getTotalAmount()->getAmount(),
      // more fields here...
    ];
  }
}

Eine andere Klasse, die als repository bezeichnet wird, wäre dann dafür zuständig, die Quittung in einer SQL-Datenbank zu speichern und sie wieder abzurufen. Sie können entweder auf bestehende ORM-Implementierungen wie Doctrine oder Eloquent zurückgreifen oder die Datenbanklogik selbst implementieren. Es hängt davon ab, wie sauber die Receipt-Klasse sein soll. Mit Doctrine würden Sie nur einige Anmerkungen zu dieser Klasse hinzufügen und ein separates Repository implementieren. Bei Eloquent müsste die Receipt-Klasse das Active-Record-Muster implementieren und damit eine Menge ändern. Das würde unsere Grundvoraussetzung nicht mehr erfüllen, dass die Receipt-Klasse nicht zu sehr an die Datenbank gebunden werden soll.

Zwischen Währungen umrechnen

Wenn Ihre Anwendung für internationale Märkte funktionieren soll, müssen Sie möglicherweise zwischen verschiedenen Währungen umrechnen. Selbst in der Europäischen Union hat nicht jedes Land den Euro. Einige Kunden ziehen es vielleicht vor, eine Rechnung in ihrer Landeswährung zu erhalten und oft ist es erforderlich, den umgerechneten Betrag irgendwo auf Papier zu deklarieren. Möglicherweise müssen Sie Ihrer Receipt-Klasse weitere Felder hinzufügen. Zuerst müssen Sie die aktuellen Wechselkurse von einem externen API abrufen. Es gibt eine Vielzahl von Diensten, aus denen Sie wählen können, darunter kostenlose und kostenpflichtige Angebote zum Beispiel Fixer, die Europäische Zentralbank oder Open Exchange Rates. Da die Währungsumrechnung eher ein Backend-Job sein sollte, werden wir den MoneyPHP Converter verwenden. Mit etwas Hilfe der Swap-Bibliothek werden wir die Wechselkurse von dem von Ihnen gewählten API abrufen und den Converter in Listing 9 mit Daten füttern. Weitere Tipps zum Umgang mit verschiedenen Währungen erhalten Sie im Kasten „Wie geht man mit Wechselkursen um?“.

use Money\Converter;
use Money\Currencies\ISOCurrencies;
use Money\Currency;
use Money\Exchange\SwapExchange;
use Money\Money;
use Swap\Builder;

// prepare the API client with your credentials
$swap = (new Builder())
-> add('some-api', ['access_key' =>  'your-key'])
-> build();

// build a currency exchange object
$exchange = new SwapExchange($swap);

// build a converter working on ISO currencies
$converter = new Converter(new ISOCurrencies(), $exchange);

// convert euros to dollars
$eur = new Money(123, new Currency('EUR'));
$usd = $converter-> convert($eur, new Currency('USD'));

Normalerweise ist es ausreichend, die komplette Wechselkurstabelle einmal pro Minute oder sogar einmal pro Stunde abzurufen und in Ihrer eigenen Datenbank zu speichern, damit Sie das API nicht überlasten. Besprechen Sie das Intervall für das Abrufen der Wechselkurse mit Ihren Domänenexperten. Speichern Sie immer das genaue Datum und die genaue Uhrzeit, die von dem von Ihnen verwendeten API angegeben werden. Außerdem sollten Sie bei jeder Währungsumrechnung in der Lage sein, nachzuweisen, welcher Kurs verwendet wurde und von welchem Zeitpunkt dieser stammt. Denken Sie daran, Datentypen zu verwenden, die groß genug sind, um jede beliebige Währung zu speichern, die Sie benötigen, und die präzise genug sind, die Wechselkurse zu speichern. Sie benötigen in der Regel vier Stellen nach dem Dezimalpunkt. Verwenden Sie zuverlässige Dezimaltypen und weder Float noch Double.

HTML- und PDF-Rechnungen rendern

Nach dem Kauf von Waren in Ihrem Webshop erwarten die meisten Kunden eine Bestätigung, eine Quittung oder eine Rechnung. Diese kann direkt nach dem Kauf, in einer E-Mail oder einer angehängten PDF-Datei angezeigt werden. Ähnliche Daten werden an mehreren Stellen dupliziert. Vielleicht können Sie einige HTML-Fragmente wiederverwenden, um Codeduplikation zu vermeiden. Zum Beispiel werden die Kundendaten oder die Produktliste höchstwahrscheinlich überall gleich sein.

Ihre Anwendung verwendet wahrscheinlich bereits eine Template-Engine wie Twig oder Blade, sodass wir hier nicht auf die Generierung des HTML-Codes eingehen werden. Wenn Sie einige praktische Methoden in Ihrer Receipt-Klasse vorbereitet haben, sollte es jetzt einfach sein, alle relevanten Daten an den richtigen Stellen der HTML-Vorlage einzubetten.

In diesem Stadium sollten Sie keine Berechnungen oder Entscheidungen innerhalb von HTML durchführen – das liegt in der Verantwortung der Receipt-Klasse! Wenn einige Teile des Dokuments an Bedingungen gebunden sind, implementieren Sie diese Logik in Ihrem Modell. Das Template darf nur einfache Fragen an das Modell stellen, z. B.: „Soll ich diesen Abschnitt anzeigen oder nicht?“

Sobald Sie ein HTML-Dokument vorbereitet haben, sollte es einfach sein, daraus eine PDF-Quittung zu erstellen. Es gibt viele Lösungen, um HTML direkt in ein PDF zu konvertieren. Quittungen haben normalerweise einfache Layouts, so dass zum Beispiel die dompdf-Bibliothek [9] ausreicht. Sie verfügt über eine eigene Rendering-Engine, die den größten Teil der Syntax von HTML5 und CSS 2.1 versteht (Listing 10).

$dompdf = new Dompdf\Dompdf();

// load the HTML document and set paper size
$dompdf->loadHtml($html);
$dompdf->setPaper('A4', 'portrait');

// render the document and get the PDF contents
$dompdf->render();
$pdf = $dompdf->output();

Das erstellte PDF testen

Auch wenn wir für unsere Receipt-Klasse Unit-Tests implementiert haben, sollten wir noch sicherstellen, dass unsere HTML-Vorlagen korrekt sind. Wenn Sie mit mehreren Geldbeträgen und mehreren Begriffen wie „Summe“, „Zwischensumme“ und so weiter arbeiten, könnten sich Fehler einschleichen, die vor dem Finanzamt schwer zu erklären sind. Das Testen von Dokumenten ist viel schwieriger als das Schreiben von Unit-Tests für Klassen. Eine Abschlussquittung kombiniert Geschäftsdaten mit HTML-Markup und vielleicht sogar einigen CSS-Tricks. Wenn Sie über eine saubere Struktur für das HTML-Template nachdenken, haben Sie deshalb den Vorteil, dass das Testen einfacher ist.

Der beste Weg, ein HTML-Template mit PHP zu testen, ist die Verwendung der gebündelten DOM-Erweiterung. DOM steht für Document Object Model und ist ein Standardmechanismus zur Darstellung einer HTML-Dokumentstruktur im Speicher. Nach dem Rendern einer Vorlage erstellen Sie also ein DOMDocument-Objekt und verwenden dann XPath-Abfragen, um alle Elemente nachzuschlagen, die wichtige Daten enthalten. Listing 11 zeigt das Vorgehen.

use PHPUnit\Framework\TestCase;

final class TemplateTest extends TestCase
{
  public function testTemplateRendersProperly(): void
  {
    // given
    $html = '<html><body><address class="seller">Foobar GmbH</address></body></html>';
    $document = new \DOMDocument();
    $document->loadHTML($html);
    $xpath = new \DOMXPath($document);

    // when
    $element = $xpath->query('//address[@class="seller"]')->item(0);

    // then
    self::assertEquals('Foobar GmbH', $element->textContent);
  }
}

Um sicherzustellen, dass das Layout Ihrer endgültigen Quittung korrekt ist, kann ein weiterer Test entweder einen Screenshot des fertigen PDFs erfassen und ihn mit dem erwarteten Bild vergleichen oder Text aus einem rechteckigen Bereich extrahieren, um zu sehen, ob ein erwarteter Datenblock richtig positioniert wurde. Auf diese Weise können Sie sicherstellen, dass sich die Kundendaten irgendwo in der rechten oberen Ecke befinden, die Produktliste irgendwo in der Mitte und so weiter.

Es ist nicht einfach, Text aus einer PDF-Datei zu extrahieren, da die physische Reihenfolge der Elemente in einer PDF-Datei oft von der im HTML abweicht. Eine PDF-Datei hat keinen Textfluss; sie arbeitet mit festen Inhaltsblöcken. Sie können dafür mit dem Werkzeug pdftotext aus dem Paket poppler-utils [10] oder mit Sébastien Malots PdfParser [11] experimentieren. Der Kasten „Sollte man Drittanbietertools vertrauen?” gibt einige Infos zur Verwendung solcher Tools.

Nun könnte noch die Frage aufkommen, warum man die fertigen PDF-Dateien testen sollten, wenn man eine beliebte Bibliothek verwendet, um sie aus HTML zu generieren. Die Frage ist: Wie sehr vertrauen Sie den Lösungen, die von anderen Leuten vorbereitet wurden? Haben Sie überprüft, wie gut die Bibliothek getestet wird? Sind Sie sicher, dass ein Upgrade der PDF-Bibliothek Ihr Layout nicht beeinträchtigt? Das ist mir nach einem kleineren Upgrade eines anderen PDF-Tools passiert.
Denken Sie daran, dass diese Programme von Leuten gemacht werden, die Fehler machen wie alle anderen auch. Ihre Kunden werden allerdings Ihnen die Schuld an den Fehlern anderer geben. Sie sind für die Werkzeuge, die Sie in Ihren Projekten verwenden, verantwortlich.

Fazit

Die Entwicklung von E-Commerce- und Finanzanwendungen ist eine verantwortungsvolle Aufgabe. Sie wollen sich oder Ihre Kunden nicht dem Risiko aussetzen, Geld und Ansehen zu verlieren. Sie können die Zuverlässigkeit Ihres Codes erheblich verbessern, indem Sie einige Grundregeln bei der Geldverarbeitung anwenden. Es gibt viele einsatzbereite Lösungen, die Ihnen die Arbeit erleichtern, sodass es nicht sinnvoll ist, das Rad neu zu erfinden.

Links & Literatur

[1] https://www.theregister.com/2011/09/22/software_bug_fine/

[2] https://floating-point-gui.de

[3] Fowler, Martin: „Patterns of Enterprise Application Architecture“, Addison-Wesley, 2002

[4] https://moneyphp.org

[5] https://www.iso.org/iso-4217-currency-codes.html

[6] https://schema.org/PriceSpecification

[7] https://de.wikipedia.org/wiki/Hyperinflation

[8] https://phpunit.de

[9] https://github.com/dompdf/dompdf

[10] https://poppler.freedesktop.org

[11] https://www.pdfparser.org

Unsere Redaktion empfiehlt:

Relevante Beiträge

Abonnieren
Benachrichtige mich bei
guest
0 Comments
Inline Feedbacks
View all comments
X
- Gib Deinen Standort ein -
- or -