- PHP 90.3%
- Scheme 9.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Etikettenrechte, ein stilles Falschgewicht und die Vorlage der Statusmail — dazu die Tests, die bisher ganz fehlten. Rechte an der Etikettenablage. getLabelLink() verliess sich darauf, dass File::getURL() die Freigabe nur erteilt, wenn der Aufrufer die Datei sehen darf. Gefragt wird dort aber File::canView(), nicht Shipment::canView(), und bei geerbter Dateiberechtigung gibt FileDefaultPermissions::canView() bedingungslos true zurueck. Es haette genuegt, dass ein Projekt-Template $LabelLink im Kundenkonto ausgibt. Jetzt entscheidet canView() der Sendung, die Freigabe wird danach ausdruecklich erteilt. Der Ordner wird zusaetzlich einmalig auf OnlyTheseUsers festgenagelt: bisher hing der Schutz allein daran, dass niemand die Dateien veroeffentlicht. Gewicht von Varianten. Der Service las Product()->Weight und uebersah, dass Varianten in SilverShop ein eigenes Weight haben, das Variation\OrderItem:: Weight() bevorzugt. Ein Shop mit 0,5-l- und 1-l-Fassung derselben Produktseite bekam still das Produktgewicht aufs Etikett — auffallen tut das erst auf der Frachtrechnung. Produkt und Dienstleister. Das Produkt-Dropdown fuehrt die Produkte aller Dienste. Wer dort ein DPD-Produkt waehlte und den Dienstleister leer liess, druckte DHL mit dessen erstem Produkt, wortlos. Der Dienst wird jetzt aus dem Produkt abgeleitet (bei mehrdeutigen Codes bewusst nicht geraten), ein widerspruechliches Paar wirft. Vorlage der Statusmail. Sie lag unter silvershops Pfad; welche der beiden gewinnt, entschied die Modulreihenfolge. Jetzt haengt sie ueber den Hook updateStatusChangeEmail und liegt unter Tietge/Shipping/. Ihre 16 i18n-Schluessel sind aus dem fremden Namensraum ShopEmail heraus und in lang/de.yml nachgetragen; vier tote Eintraege sind raus. Verbrauchte Sendungsnummern. Scheitert nach createLabel() die Ablage oder das Speichern, ist das Etikett bezahlt und die Nummer verbraucht. Bisher blieb davon keine Spur — jetzt ein critical-Log mit Nummer und Rohantwort. Tests. Zwei Suiten: unit ohne Kernel fuer die Value Objects und die Rechenlogik, integration ueber SapphireTest fuer das Verzeichnis. 58 Tests laufen gruen; CarrierRegistryTest ist geschrieben, aber mangels Projekt mit Framework 6, silvershop und PHPUnit zugleich hier nicht ausgefuehrt. Kleinigkeiten. LabelResult.php enthielt ein echtes NUL-Byte in der trim()-Zeichenliste — git fuehrte die Datei damit als binaer, ohne Diff und ohne Blame. Ausserdem: Injectable an ConnectionTestResult entfernt (privater Konstruktor), Registry protokolliert Tippfehler und Schluesselkollisionen statt sie zu verschlucken, Convert::raw2att() auf den Link-Attributen, Rueckfrage vor dem kostenpflichtigen Etikett, zwei irrefuehrende Docblocks korrigiert. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
| _config | ||
| lang | ||
| src | ||
| templates/Tietge/Shipping | ||
| tests | ||
| .gitattributes | ||
| .gitignore | ||
| composer.json | ||
| LICENSE | ||
| phpunit.xml.dist | ||
| README.md | ||
tietge/silverstripe-shipping
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
Shipment |
Eine Sendung: ein Etikett, ein Paket, eine Sendungsnummer. Eine Bestellung hat mehrere |
| Reiter „Versand" an der Bestellung | Erzeugte Sendungen, Vorgaben fürs nächste Etikett, Lieferanschrift |
| Knopf „Versandetikett erzeugen" | In der Aktionsleiste des Bestellformulars |
| Etikett-Ablage | Geschützt in assets/.protected, Download nur mit CMS-Rechten |
| Statuswechsel | Order.Status → Sent beim Etikettendruck, löst die Kundenmail aus |
| Statusmail | Deutsche Vorlage mit Sendungsnummer und Verfolgungslink, ersetzt silvershops |
| Reiter „Versand" in der SiteConfig | Absender, Vorgaben, je Provider ein Unterreiter |
Testseite versandtest/<dienst> |
„Verbindung testen" je Dienstleister |
| Storno je Sendung | Nur 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.