No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
MoritzSauer d04bc8df45 Korrekturen aus dem Code-Review
Drei Befunde mit praktischer Wirkung:

* Die Hausnummer ueberlebt das Kuerzen der Anschriftenzeile. Bisher schnitt
  `text()` von rechts ab und traf bei ueber 35 Zeichen genau die Nummer; wegen
  `nonvalidate` beanstandet UPS das nicht, das Etikett entsteht und das Paket
  ist unzustellbar. Reicht der Platz nicht, wird jetzt der Strassenname gekuerzt.
* Der Verbindungstest verwirft das Token, bevor er eines holt. Vorher las er bis
  zu vier Stunden lang nur den eigenen Zwischenspeicher und bestaetigte damit den
  vorherigen Test statt den Zugang.
* Die E-Mail-Adresse wird geprueft und bei Ueberlaenge weggelassen statt
  gekuerzt. UPS nimmt 50 Zeichen und lehnt sonst die ganze Sendung ab; eine
  abgeschnittene Adresse waere ausserdem nicht die gekuerzte richtige, sondern
  eine falsche.

Dazu Hygiene:

* `describeErrors()` stand zweimal byte-gleich im Modul und liegt jetzt als
  Trait `DescribesUpsErrors` an einer Stelle. Es liest zusaetzlich die
  OAuth2-Standardform (`error` / `error_description`) — bisher blieb davon nur
  ein nacktes „Unauthorized" uebrig — und vertraegt Antworten, die von der
  erwarteten Form abweichen.
* Ein leeres `expires_in` ergab eine Token-Lebensdauer von 60 Sekunden. `?:`
  statt `??`.
* Gewichte unter 5 g rundeten auf `0.00`; jetzt auf 0,01 kg begrenzt.
* `PDF_LABEL` wird als Dateiendung ausdruecklich auf `pdf` abgebildet, statt
  sich auf die Pruefung in `LabelResult::getFileExtension()` zu verlassen.
* `UpsClient` und `AccessTokenProvider` werden ueber `create()` erzeugt, damit
  das `Injectable` nicht nur Zierde ist.
* `psr/log` aus der composer.json entfernt — nicht benutzt.
* Der Kommentar zu `defaultLifetime` behauptete eine Obergrenze; ein beim
  Ablegen mitgegebener Wert ueberschreibt sie, und der Provider gibt immer einen mit.
* Toter README-Verweis auf `docs/versandmodul-ups-einrichtung.md` entfernt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 10:14:35 +02:00
_config Korrekturen aus dem Code-Review 2026-08-06 10:14:35 +02:00
lang UPS ueber die Shipping API v2409 2026-08-05 16:19:01 +02:00
src Korrekturen aus dem Code-Review 2026-08-06 10:14:35 +02:00
.gitattributes UPS ueber die Shipping API v2409 2026-08-05 16:19:01 +02:00
.gitignore UPS ueber die Shipping API v2409 2026-08-05 16:19:01 +02:00
composer.json Korrekturen aus dem Code-Review 2026-08-06 10:14:35 +02:00
LICENSE UPS ueber die Shipping API v2409 2026-08-05 16:19:01 +02:00
README.md Korrekturen aus dem Code-Review 2026-08-06 10:14:35 +02:00

tietge/silverstripe-shipping-ups

UPS für tietge/silverstripe-shipping — Etiketten über die UPS Shipping API v2409 (REST/JSON, OAuth2 Client Credentials).

Handgeschrieben gegen die offizielle Shipping.yaml aus UPS-API/api-documentation. Zwei REST-Aufrufe, keine SDK-Abhängigkeit.

Installation

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

Braucht einen PSR-18-Client — php-http/guzzle7-adapter genügt.

Zugangsdaten

Der Zugang ist reine Selbstbedienung — kein Antrag, keine Abnahme, keine Wartezeit. Damit ist UPS von den angebundenen Diensten der einzige, der ohne Vertragsvorlauf produktiv nutzbar ist.

  1. Auf developer.ups.com anmelden, Apps → Add Apps.
  2. „I want to integrate UPS shipping capabilities into my business" wählen. Das ist der Schalter für Client Credentials — die andere Variante wäre der Authorization-Code-Fluss für Anwendungen, die im Namen fremder UPS-Konten versenden, und bräuchte eine Callback-URL.
  3. Callback-URL leer lassen. Der Client-Credentials-Fluss kennt keine Redirect-URI.
  4. Produkte: Shipping (Pflicht), Tracking (für später gleich mitnehmen). Rating nicht — der Versandtarif dieses Moduls ist bewusst dienstleisterunabhängig.
Tietge\Shipping\Ups\UpsCarrier:
  sandbox: true
  client_id: ''
  client_secret: ''
  account_number: ''     # 6-stellige „Shipper Number"

Kontonummer ≠ Client ID. Die Client ID identifiziert die App, die Kontonummer das Versandkonto; UPS prüft beide gegeneinander. Sie steht auf ups.com unter „Account Summary" oder oben rechts auf jeder UPS-Rechnung und ist nicht rein numerisch (etwa A1B2C3).

Weitere Schalter

  services: ['11']                    # 11 = UPS Standard (DE/EU-Straßendienst)
  shipment_description: 'Buecher'     # Warenbeschreibung aufs Etikett (Vorgabe: 'Warenlieferung')
  api_version: 'v2409'                # laut Spezifikation der einzige gültige Wert

Weitere Servicecodes, nur wenn vertraglich freigeschaltet: 07 Express, 08 Expedited, 54 Express Plus, 65 Express Saver, 70 Access Point Economy, 74 Express 12:00.

Etikettenformat

Im CMS unter Website-Einstellungen → Shop → Versand → UPS. Beide Wahlmöglichkeiten liefern ein PDF; sie unterscheiden sich in der Seitengröße:

Auswahl Seitengröße Wofür
PDF auf Blatt 612 × 792 pt (Letter) Bürodrucker; Etikett sitzt gedreht auf dem Blatt
PDF in Etikettengröße 288 × 432 pt = 4 × 6 Zoll Etikettendrucker, ohne Skalierung

Zwei Stellen, an denen die Spezifikation danebenliegt

Beide am laufenden Dienst gemessen, beide mit praktischer Wirkung:

1. PDF als Etikettenformat. Das Anfrageschema nennt für LabelImageFormat nur GIF, ZPL, EPL und SPL; PDF steht ausschließlich bei der LabelRecovery-Schnittstelle. Der Versandaufruf nimmt es trotzdem an und liefert ein echtes PDF.

2. LabelStockSize bei PDF. Laut Schema gilt der Block nur für Thermoformate. Tatsächlich entscheidet er auch beim PDF über die Seitengröße — ohne ihn ein Letter-Blatt, mit ihm exakt 4 × 6 Zoll.

Ohne Punkt 1 wäre UPS der einzige Dienst mit GIF-Dateien geworden, und das Basismodul hätte umgebaut werden müssen. Ohne Punkt 2 müsste jeder Etikettendrucker ein Letter-PDF herunterrechnen, was Barcodes unscharf macht.

Kein ZPL im Angebot

UPS kann ZPL, das Modul bietet es nicht an. ZPL ist eine Druckersprache, kein Dokument — ein Browser kann die Steuerbefehle weder anzeigen noch drucken. Damit sie beim Drucker ankommen, braucht es ein Hilfsprogramm auf dem Arbeitsplatz (bei Zebra: Browser Print, localhost:9100), das von Hand je Rechner zu installieren ist, ein bestätigtes Zertifikat verlangt und nur mit Zebra-Druckern funktioniert.

Das Etiketten-PDF erreicht dasselbe Ergebnis ohne all das. Für ein Modul, das auch anderswo laufen soll, ist das der einzige Weg ohne Voraussetzungen.

Testumgebung

wwwcie.ups.com (CIE) — dieselben Zugangsdaten wie produktiv, es unterscheidet sich nur der Host. Kein Antrag, keine getrennte Nutzerverwaltung.

Zwei Dinge sehen dort anders aus und sind kein Fehler:

  • Auf dem Etikett steht „SAMPLE", die Barcodefelder tragen Platzhaltertexte.
  • Die Sendungsnummer kommt als 1ZXXXXXXXXXXXXXXXX zurück.

Weil es keine echten Sendungsnummern gibt, lässt sich das Storno in der CIE nicht prüfen — UPS antwortet mit „No shipment found within the allowed void period" (190102). Produktiv funktioniert es (DELETE /shipments/{version}/void/cancel/{nummer}, bis zum Tagesabschluss).

Eigenheiten

  • Zahlen reisen als Zeichenketten. Weight, Length, Height sind im Schema durchweg type: string; ein echter Zahlwert wird abgewiesen.
  • Gewicht in KGS, Maße in CM. Das Basismodul führt Gramm und Millimeter.
  • Die Kontonummer steht zweimal im Auftrag — als ShipperNumber beim Absender und unter PaymentInformation. UPS trennt „wer versendet" von „wer zahlt".
  • ShipFrom wird mitgeschickt, obwohl es entfallen dürfte: Fehlt es, nimmt UPS die Absenderanschrift und tarifiert von dort.
  • Ein fehlendes api_version erzeugt einen nackten 404 — die Fassung steckt im Pfad. Das Modul hat dafür einen harten Rückfall.