PharmaAutopilot
...

Java SDK

Artefakt: dev.msv3:msv3-sdk

Das Java SDK bietet typsicheren Zugriff auf das MSV3 Smart Gateway ab Java 17+. Keine externen Abhaengigkeiten -- nur die Java-Standardbibliothek.


Installation

Maven

<dependency>
    <groupId>dev.msv3</groupId>
    <artifactId>msv3-sdk</artifactId>
    <version>1.0.0</version>
</dependency>

Gradle

implementation 'dev.msv3:msv3-sdk:1.0.0'

Erfordert Java 17+. Das SDK hat keine Laufzeit-Abhaengigkeiten ueber die Standardbibliothek hinaus (nutzt java.net.http.HttpClient).


Grundeinrichtung

import dev.msv3.sdk.Msv3Client;

Msv3Client client = Msv3Client.builder()
    .apiKey(System.getenv("MSV3_API_KEY"))  // "pk_live_..." oder "pk_test_..."
    .build();
Builder-MethodeTypStandardBeschreibung
apiKey(String)StringerforderlichIhr Gateway-API-Schluessel
baseUrl(String)Stringhttps://api.msv3gateway.example.comUeberschreibung fuer Tests
timeout(int)int30Anfrage-Timeout in Sekunden

Zugangsdaten

MSV3-Zugangsdaten werden pro Aufruf uebergeben, nie im Client gespeichert:

import dev.msv3.sdk.Msv3Credentials;

Msv3Credentials creds = new Msv3Credentials(
    "noweda",
    "Now00079800",
    System.getenv("NOWEDA_PASS")
);

Msv3Credentials ist ein Java-Record mit den Feldern wholesaler, username und password.


Ressourcen

RessourceZugriffBeschreibung
ConnectionResourceclient.connection()Grosshaendler-Konnektivitaet testen
WholesalersResourceclient.wholesalers()Grosshaendler auflisten und registrieren
AvailabilityResourceclient.availability()Echtzeit-Bestandsabfragen
OrdersResourceclient.orders()Bestellungen aufgeben und verfolgen
ContractsResourceclient.contracts()Vertragsdaten und Lieferfenster
DeliveriesResourceclient.deliveries()Lieferbenachrichtigungen
ReturnsResourceclient.returns()Retouren-Autorisierung und Ankuendigungen
DocumentsResourceclient.documents()PDF-Dokumente herunterladen
WebhooksResourceclient.webhooks()Webhook-Abonnement-Verwaltung

Verbindung testen

import dev.msv3.sdk.model.ConnectionTestResponse;

ConnectionTestResponse result = client.connection().test(creds);
System.out.println("Verbunden: " + result.connected());     // true
System.out.println("Grosshaendler: " + result.wholesaler()); // "noweda"
System.out.println("Latenz: " + result.latencyMs() + "ms");  // 142

Verfuegbarkeit

client.availability().check(creds, request)

import dev.msv3.sdk.enums.DemandType;
import dev.msv3.sdk.model.AvailabilityRequest;
import dev.msv3.sdk.model.AvailabilityResponse;
import dev.msv3.sdk.model.AvailabilityItemResponse;

AvailabilityResponse result = client.availability().check(creds,
    AvailabilityRequest.builder()
        .addItem("761271", 5)
        .addItem("10203595", 1, DemandType.DIRECT)
        .build()
);

for (AvailabilityItemResponse item : result.items()) {
    if (item.available()) {
        var d = item.deliveries().get(0);
        System.out.printf("PZN %s: %d Stueck, ETA %s%n",
            item.pzn(), d.quantity(), d.estimatedAt());
    }
    if (item.substitution() != null) {
        System.out.printf("  Ersetzt durch PZN %s%n",
            item.substitution().replacementPzn());
    }
}

client.availability().bulk(creds, pzns)

import dev.msv3.sdk.model.BulkAvailabilityResponse;

BulkAvailabilityResponse bulk = client.availability().bulk(creds,
    List.of("761271", "4211896", "10203595", "99999999")
);
System.out.println("Auf Lager: " + bulk.availablePzns());

Bestellungen

client.orders().create(creds, request)

import dev.msv3.sdk.enums.DeliveryPreference;
import dev.msv3.sdk.model.OrderRequest;
import dev.msv3.sdk.model.OrderResponse;

OrderResponse order = client.orders().create(creds, OrderRequest.builder()
    .addItem("761271", 3, DeliveryPreference.BACKORDER)
    .addItem("10203595", 1)
    .build()
);

System.out.println("Status: " + order.status());        // "confirmed"
System.out.println("Request ID: " + order.requestId());

for (var item : order.items()) {
    System.out.printf("PZN %s: %d/%d bestaetigt%n",
        item.pzn(), item.quantityConfirmed(), item.quantityOrdered());
}

Liefervorgaben: NORMAL, BACKORDER, GROUPED, DISPOSITION

Dry Run

OrderResponse order = client.orders().create(creds,
    OrderRequest.builder()
        .addItem("761271", 3)
        .build(),
    true  // dryRun
);
// order.status() == "dry_run"

Nachtmodus

if ("queued_night_mode".equals(order.status())) {
    System.out.println("Grosshaendler im Nachtmodus. Bestellung vorgemerkt.");
}

client.orders().status(creds, orderId)

import dev.msv3.sdk.model.OrderStatusResponse;

OrderStatusResponse status = client.orders().status(creds, order.requestId());

switch (status.status()) {
    case "available" ->
        System.out.printf("Bestellung %s: %d Artikel%n",
            status.orderId(), status.items().size());
    case "expired" ->
        System.out.println("Bestellantwort nicht mehr verfuegbar");
    case "unknown" ->
        System.out.println("Grosshaendler kennt diese Bestell-ID nicht");
}

Vertraege

import dev.msv3.sdk.model.ContractsResponse;

ContractsResponse contract = client.contracts().get(creds);

System.out.println("Kunden-ID: " + contract.customerId());
System.out.println("Bulk-Verfuegbarkeit: " + contract.capabilities().bulkAvailability());
System.out.println("Substitution: " + contract.capabilities().substitution());

for (var window : contract.deliveryWindows()) {
    System.out.printf("  %s schliesst um %s%n", window.day(), window.closesAt());
}

Lieferungen

import dev.msv3.sdk.model.DeliveryListResponse;

// Unbestaetigte Lieferavise abrufen
DeliveryListResponse response = client.deliveries().list(creds);

for (var delivery : response.deliveries()) {
    System.out.printf("Lieferung %s (%s)%n",
        delivery.trackingNumber(), delivery.date());
    for (var item : delivery.items()) {
        System.out.printf("  PZN %s: %d geliefert%n",
            item.pzn(), item.quantityDelivered());
    }
}

// Bestaetigen
List<String> trackingNumbers = response.deliveries().stream()
    .map(d -> d.trackingNumber())
    .toList();
client.deliveries().confirm(creds, trackingNumbers);

Retouren

import dev.msv3.sdk.enums.ReturnReason;
import dev.msv3.sdk.model.ReturnRequest;
import dev.msv3.sdk.model.ReturnResponse;

ReturnResponse ret = client.returns().request(creds, ReturnRequest.builder()
    .addItem("TRACK-001", "761271", 2, ReturnReason.DAMAGED)
    .build()
);

System.out.println("Return ID: " + ret.returnId());
for (var pos : ret.positions()) {
    System.out.printf("  PZN %s: %s%n", pos.pzn(), pos.status());
}

Webhooks

import dev.msv3.sdk.model.WebhookSubscription;

// Webhook erstellen
WebhookSubscription webhook = client.webhooks().create(
    "https://example.com/hooks/msv3",
    "noweda",
    List.of("delivery.received", "order.status_changed")
);

// WICHTIG: signingSecret jetzt speichern - wird nicht erneut angezeigt
System.out.println("Webhook ID: " + webhook.id());
System.out.println("Secret: " + webhook.signingSecret());

Signatur-Verifizierung

import dev.msv3.sdk.Msv3WebhookVerifier;

// Servlet / Spring-Controller
String payload = request.getReader().lines().collect(Collectors.joining());
String signature = request.getHeader("X-MSV3-Signature");
String secret = System.getenv("MSV3_WEBHOOK_SECRET");

if (!Msv3WebhookVerifier.verify(payload, signature, secret)) {
    response.sendError(401, "Invalid signature");
    return;
}

// Payload ist verifiziert - jetzt parsen und verarbeiten

Die Methode Msv3WebhookVerifier.verify() akzeptiert sowohl String als auch byte[] als Payload.


Fehlerbehandlung

import dev.msv3.sdk.exception.*;

try {
    OrderResponse order = client.orders().create(creds,
        OrderRequest.builder().addItem("761271", 3).build());

} catch (Msv3AuthException e) {
    System.err.println("Authentifizierung fehlgeschlagen: " + e.getMessage());

} catch (Msv3BadRequestException e) {
    System.err.println("Ungueltige Anfrage: " + e.getMessage());

} catch (Msv3ProductUnavailableException e) {
    System.err.printf("Abgelehnt (Code %s): %s%n", e.errorCode(), e.getMessage());

} catch (Msv3RateLimitException e) {
    int wait = e.retryAfter() != null ? e.retryAfter() : 60;
    System.err.printf("Rate-Limit. Warte %ds...%n", wait);

} catch (Msv3WholesalerUnavailableException e) {
    System.err.printf("Grosshaendler %s nicht erreichbar%n", e.wholesaler());

} catch (Msv3ApiException e) {
    System.err.printf("API-Fehler %d [%s]: %s%n",
        e.status(), e.errorType(), e.getMessage());
}
ExceptionHTTP-StatusBeschreibung
Msv3AuthException401Ungueltiger API-Key oder MSV3-Zugangsdaten
Msv3BadRequestException400Ungueltige Anfrageparameter
Msv3NotFoundException404Grosshaendler oder Ressource nicht gefunden
Msv3ProductUnavailableException422Bestellung vom Grosshaendler abgelehnt
Msv3RateLimitException429Rate-Limit ueberschritten
Msv3WholesalerUnavailableException502Grosshaendler nicht erreichbar

Alle Exceptions erweitern Msv3ApiException (ein RuntimeException). Gemeinsame Methoden:

MethodeTypBeschreibung
status()intHTTP-Statuscode
errorType()StringMaschinenlesbarer Fehlercode
getMessage()StringMenschenlesbare Beschreibung
wholesaler()StringGrosshaendler-ID (kann null sein)
errorCode()StringRoher MSV3-Fehlercode (kann null sein)
requestId()StringRequest-ID aus dem X-Request-Id-Header

Retry-Strategie

Das SDK wiederholt nicht automatisch. Implementieren Sie Retry-Logik in Ihrer Anwendung:

import dev.msv3.sdk.exception.Msv3WholesalerUnavailableException;

static OrderResponse createOrderWithRetry(
        Msv3Client client, Msv3Credentials creds,
        OrderRequest request, int maxAttempts) {
    for (int attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
            return client.orders().create(creds, request);
        } catch (Msv3WholesalerUnavailableException e) {
            if (attempt < maxAttempts) {
                long delay = (long) Math.pow(2, attempt - 1) * 1000;
                System.err.printf("Versuch %d fehlgeschlagen, erneut in %dms...%n",
                    attempt, delay);
                try { Thread.sleep(delay); } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw e;
                }
            } else {
                throw e;
            }
        }
    }
    throw new IllegalStateException("unreachable");
}

Vollstaendiges Beispiel

import dev.msv3.sdk.Msv3Client;
import dev.msv3.sdk.Msv3Credentials;
import dev.msv3.sdk.enums.DeliveryPreference;
import dev.msv3.sdk.exception.Msv3ApiException;
import dev.msv3.sdk.model.*;

public class QuickStart {
    public static void main(String[] args) {
        Msv3Client client = Msv3Client.builder()
            .apiKey(System.getenv("MSV3_API_KEY"))
            .build();

        Msv3Credentials creds = new Msv3Credentials(
            "noweda",
            System.getenv("MSV3_USERNAME"),
            System.getenv("MSV3_PASSWORD")
        );

        try {
            // 1. Verbindung testen
            var connection = client.connection().test(creds);
            System.out.printf("Verbunden (%dms)%n", connection.latencyMs());

            // 2. Verfuegbarkeit pruefen
            var availability = client.availability().check(creds,
                AvailabilityRequest.builder()
                    .addItem("761271", 3)
                    .build()
            );
            if (!availability.items().get(0).available()) {
                System.out.println("PZN 761271 nicht verfuegbar");
                return;
            }

            // 3. Bestellung mit Nachlieferung aufgeben
            var order = client.orders().create(creds, OrderRequest.builder()
                .addItem("761271", 3, DeliveryPreference.BACKORDER)
                .build()
            );
            System.out.println("Bestellstatus: " + order.status());
            System.out.println("Request ID: " + order.requestId());

        } catch (Msv3ApiException e) {
            System.err.printf("MSV3-Fehler %d [%s]: %s%n",
                e.status(), e.errorType(), e.getMessage());
        }
    }
}