Search by

tietge / silverstripe-shipping

moritz-sauer-13

Versandabwicklung für SilverShop: Sendungen, Etiketten, Statuswechsel und Sendungsverfolgung im Kundenkonto — mit austauschbaren Paketdiensten.

Package info

git.innomedia.de/Tietge/silverstripe-shipping

Type:silverstripe-vendormodule

pkg:composer/tietge/silverstripe-shipping

Statistics

Installs: 0

Dependents: 2

Suggesters: 0

1.0.0 2026-08-06 08:23 UTC

This package is auto-updated.

Last update: 2026-09-17 08:00:58 UTC


README

Versandabwicklung für SilverShop mit austauschbaren Paketdiensten. Aus einer bezahlten Bestellung heraus per Knopfdruck im Backend ein Versandetikett erzeugen, das PDF geschützt ablegen, den Kunden benachrichtigen und ihm Sendungsnummer samt Verfolgungslink im Konto zeigen.

Dieses Modul kennt keinen einzigen Paketdienst. Welcher das Etikett liefert, entscheidet ein Provider-Modul, das sich selbst anmeldet — DHL, DPD und UPS gibt es fertig, weitere sind eine Klasse.

Installation

composer require tietge/silverstripe-shipping
vendor/bin/sake db:build --flush

Abhängigkeiten: silverstripe/framework ^6, silvershop/core ^6, lekoala/silverstripe-cms-actions ^2. Nicht innomedia/shopextensions — das Modul setzt es weder voraus noch fasst es an.

Allein ist es funktionsfähig, aber nutzlos: Ohne Provider steht kein Dienstleister zur Wahl. Mindestens eines der Provider-Module installieren.

Ohne Paketdienst ausprobieren

Für die Einrichtung liegt ein Testdienst bei, der ein lesbares PDF im Etikettenformat erzeugt, ohne irgendwo anzurufen. Damit lässt sich die ganze Kette prüfen — Knopf, Vorgaben, Ablage, geschützter Download, Statuswechsel, Kundenmail —, bevor ein einziger echter Zugang existiert. Er meldet sich nicht von selbst an:

Tietge\Shipping\Carrier\CarrierRegistry:
  carriers:
    - Tietge\Shipping\Carrier\DemoCarrier

Er verhält sich absichtlich unbequem: fehlt der Empfängeradresse die Hausnummer, druckt er trotzdem und gibt eine Beanstandung zurück — genau wie DHL bei einer nicht leitcodierbaren Adresse. Vor dem Produktivgang wieder austragen.

Was es mitbringt

ShipmentEine Sendung: ein Etikett, ein Paket, eine Sendungsnummer. Eine Bestellung hat mehrere
Reiter „Versand" an der BestellungErzeugte Sendungen, Vorgaben fürs nächste Etikett, Lieferanschrift
Knopf „Versandetikett erzeugen"In der Aktionsleiste des Bestellformulars
Etikett-AblageGeschützt in assets/.protected, Download nur mit CMS-Rechten
StatuswechselOrder.Status → Sent beim Etikettendruck, löst die Kundenmail aus
StatusmailDeutsche Vorlage mit Sendungsnummer und Verfolgungslink, ersetzt silvershops
Reiter „Versand" in der SiteConfigAbsender, Vorgaben, je Provider ein Unterreiter
Testseite versandtest/<dienst>„Verbindung testen" je Dienstleister
Storno je SendungNur dort, wo der Dienst es kann

Projektseitig zu erledigen

1. Statusmail freischalten. Ohne diese Zeile passiert beim Statuswechsel nichts:

SilverShop\Model\Order:
  log_status:
    - Sent

2. Sendungen im Konto anzeigen. Das Modul liefert die Daten, das Template gehört ins Projekt:

<% if $HasVisibleShipments %>
    <% loop $VisibleShipments %>
        $CarrierTitle — <a href="$TrackingUrl">$TrackingNumber</a>
    <% end_loop %>
<% end_if %>

VisibleShipments filtert stornierte heraus. In der Kaufbestätigung ist die Liste immer leer — Etiketten entstehen erst später im Backend.

3. Mail-Template — nichts zu tun. Das Modul bringt eine deutsche Vorlage mit Sendungsnummer und Verfolgungslink mit und hängt sie über den Hook updateStatusChangeEmail ein; silvershops englische Vorlage kommt nicht mehr zum Zug. Wer sie anpassen will, legt Tietge/Shipping/Order_StatusEmail.ss im Theme ab.

Bewusst nicht über eine gleichnamige Datei unter silvershops Pfad: bei gleichem Pfad entscheidet ModuleManifest.module_priority, welche der beiden Vorlagen gewinnt — und beide Module stehen dort im Sammeltopf $other_modules. Das hätte geheißen: auf dem einen Server kommt die Mail mit Sendungsnummer, auf dem nächsten ohne, bei identischem Stand.

Die Schnittstelle

Ein Provider implementiert Tietge\Shipping\Carrier\CarrierInterface und meldet sich an:

Tietge\Shipping\Carrier\CarrierRegistry:
  carriers:
    - Meine\Firma\MeinCarrier

Das ist der ganze Plug-&-Play-Mechanismus. Ob der Dienst im Backend erscheint, entscheidet danach allein isConfigured().

Die Value Objects sind reine PHP-Objekte ohne ORM-Bezug:

  • LabelRequest — Absender, Empfänger, Produktcode, Gewicht, optional Maße. Einheiten sind festgelegt: Gramm und Millimeter, beide als Integer. Das ist keine Formalie — DPD rechnet in 10-Gramm-Einheiten, UPS in Kilogramm und Zentimetern. Ein gemeinsames Feld ohne festgeschriebene Einheit wäre ein Fehler mit unmittelbarer Kostenwirkung. Jeder Adapter rechnet genau einmal um, an einer Stelle.
  • LabelResult — Sendungsnummer, Etiketteninhalt, Storno-Kennung, Warnungen, Dateiendung.
  • CarrierException — trägt einen Übersetzungsschlüssel samt Platzhaltern, keinen fertigen Satz. Übersetzt wird an der Ausgabestelle.

Zwei Entwurfsentscheidungen, die beim Bauen teuer erkauft wurden

Ein LabelResult bedeutet: ein Etikett liegt vor. Die Trennlinie ist ausdrücklich nicht der HTTP-Status. DHL liefert 200 mit gültigem Etikett und Beanstandungen („Hausnummer nicht gefunden"); DPD legt Fehler in eine formal erfolgreiche Antwort und kann dort sogar eine Paketscheinnummer ohne PDF zurückgeben — die Nummer ist dann verbraucht. Entsteht kein Etikett, wirft der Adapter. Gibt es trotz Etikett etwas zu sagen, gehört das in getWarnings().

Die Dateiendung kommt aus dem Ergebnis, nicht aus einer Annahme. Das Modul nannte einmal jede Datei .pdf. Beim ersten Nicht-PDF lag eine unlesbare Datei unter einem PDF-Namen in der Ablage — leere Druckseite, kein Fehler, keine Meldung. getFileExtension() verhindert das; Shipment::isPrintableLabel() entscheidet, ob der Druckdialog aufgeht oder ein Download angeboten wird.

Konfiguration

Tietge\Shipping\Service\ShipmentService:
  label_folder: 'Versandetiketten'   # Ordner unter assets/
  sent_status: 'Sent'                # Zielstatus beim Etikettendruck

Tietge\Shipping\Model\Shipment:
  printable_label_extensions: ['pdf', 'gif', 'png', 'jpg', 'jpeg']

Absenderanschrift, Verpackungszuschlag und Etikettenformat stehen im CMS, nicht in YAML — sie ändern sich, ohne dass jemand an Dateien sollte.

Caching

Die Konto-Templates sind bewusst ohne <% cached %> gebaut („die Bestellung gehört einem einzelnen Kunden"). Es sind daher keine cares-Einträge in cache.yml nötig.

Tests

composer install
vendor/bin/phpunit                      # alles
vendor/bin/phpunit --testsuite unit     # ohne Kernel, Sekundenbruchteile

Zwei Suiten mit verschiedenen Voraussetzungen. unit prüft die Value Objects und die reine Rechenlogik als gewöhnliche PHPUnit-Tests — sie brauchen weder Kernel noch Datenbank und laufen auch in einem Modul-Checkout ohne Projekt drumherum. Schwerpunkt sind die Stellen, an denen ein Fehler Geld kostet und trotzdem nicht auffällt: die Zerlegung der Straßenzeile, die Dateiendung des Etiketts, die Alles-oder-nichts-Regel der Maße und das Gewicht einer Bestellposition.

integration läuft über SapphireTest und braucht Config und Injector — dort steht das Verhalten des CarrierRegistry.

Was es bewusst nicht tut

Retourenetiketten, Packstation/Postfiliale, Auslandsversand mit Zollinhaltserklärung, Versandart-Auswahl an der Kasse, Live-Sendungsverfolgung. Das Datenmodell ist auf die Verfolgung vorbereitet (TrackingStatusText, TrackingPolledOn, DeliveredOn), der Abruf fehlt.