PharmaAutopilot
...

TypeScript SDK

Package: @msv3/sdk

Das TypeScript SDK ist der primaere Weg, das MSV3 Smart Gateway in Node.js- und Browser-kompatiblen Umgebungen zu integrieren.


Installation

npm install @msv3/sdk

Erfordert Node.js 18+ (nutzt die native fetch-API). Funktioniert mit TypeScript 5+ und JavaScript ESM/CommonJS.


Grundeinrichtung

import { Msv3Client } from '@msv3/sdk';

const client = new Msv3Client({
  apiKey: process.env.MSV3_API_KEY!, // 'pk_live_...' oder 'pk_test_...'
});
OptionTypStandardBeschreibung
apiKeystringerforderlichIhr Gateway-API-Schluessel
baseUrlstringhttps://api.msv3gateway.example.comUeberschreibung fuer Tests
timeoutnumber30000Anfrage-Timeout in Millisekunden

Zugangsdaten

MSV3-Zugangsdaten (Grosshaendler-Benutzername + Passwort) werden pro Aufruf uebergeben, nicht im Client gespeichert.

import type { Msv3Credentials } from '@msv3/sdk';

const creds: Msv3Credentials = {
  wholesaler: 'noweda',
  username: process.env.MSV3_USERNAME!,
  password: process.env.MSV3_PASSWORD!,
};

Ressourcen

RessourceEigenschaftBeschreibung
ConnectionResourceclient.connectionGrosshaendler-Konnektivitaet testen
WholesalersResourceclient.wholesalersGrosshaendler auflisten und registrieren
AvailabilityResourceclient.availabilityEchtzeit-Bestandsabfragen
OrdersResourceclient.ordersBestellungen aufgeben und verfolgen
ContractsResourceclient.contractsVertragsdaten und Lieferfenster
DeliveriesResourceclient.deliveriesLieferbenachrichtigungen
ReturnsResourceclient.returnsRetouren-Autorisierung und Ankuendigungen
DocumentsResourceclient.documentsPDF-Dokumente herunterladen
WebhooksResourceclient.webhooksWebhook-Abonnement-Verwaltung

Verbindung testen

const result = await client.connection.test(creds);
// result.connected   → true
// result.wholesaler  → 'noweda'
// result.latency_ms  → 142

Verfuegbarkeit

client.availability.check(creds, body)

const result = await client.availability.check(creds, {
  items: [
    { pzn: '761271', quantity: 5 },
    { pzn: '10203595', quantity: 1, demand_type: 'direct' },
  ],
});

for (const item of result.items) {
  if (item.available) {
    const d = item.deliveries[0];
    console.log(`PZN ${item.pzn}: ${d.quantity} Stueck, ETA ${d.estimated_at}`);
  }
  if (item.substitution) {
    console.log(`  Ersetzt durch PZN ${item.substitution.replacement_pzn}`);
  }
}

client.availability.bulk(creds, body)

const { available_pzns } = await client.availability.bulk(creds, {
  pzns: ['761271', '4211896', '10203595', '99999999'],
});
console.log('Auf Lager:', available_pzns);

Bestellungen

client.orders.create(creds, body, options?)

const order = await client.orders.create(creds, {
  items: [
    { pzn: '761271', quantity: 3, delivery_preference: 'backorder' },
    { pzn: '10203595', quantity: 1 },
  ],
});

console.log(`Status: ${order.status}`);      // 'confirmed'
console.log(`Request ID: ${order.request_id}`);

for (const item of order.items) {
  console.log(`PZN ${item.pzn}: ${item.quantity_confirmed}/${item.quantity_ordered}`);
}

Liefervorgaben: 'normal' (Standard), 'backorder', 'grouped', 'disposition'

Dry Run

const order = await client.orders.create(
  creds,
  { items: [...] },
  { dryRun: true },
);
// order.status === 'dry_run'

Nachtmodus

if (order.status === 'queued_night_mode') {
  console.log('Grosshaendler im Nachtmodus. Bestellung fuer naechstes Fenster vorgemerkt.');
}

client.orders.status(creds, orderId)

const status = await client.orders.status(creds, order.request_id);

switch (status.status) {
  case 'available':
    console.log(`Bestellung ${status.order_id}: ${status.items.length} Artikel`);
    break;
  case 'expired':
    console.log('Bestellantwort nicht mehr verfuegbar');
    break;
  case 'unknown':
    console.log('Grosshaendler kennt diese Bestell-ID nicht');
    break;
}

Lieferungen

// Unbestaetigte Lieferavise abrufen
const { deliveries } = await client.deliveries.list(creds);

for (const delivery of deliveries) {
  console.log(`Lieferung ${delivery.tracking_number} (${delivery.date})`);
  for (const item of delivery.items) {
    console.log(`  PZN ${item.pzn}: ${item.quantity_delivered} geliefert`);
  }
}

// Bestaetigen
const tracking_numbers = deliveries.map(d => d.tracking_number);
await client.deliveries.confirm(creds, { tracking_numbers });

Webhooks

// Webhook erstellen
const webhook = await client.webhooks.create({
  url: 'https://example.com/hooks/msv3',
  wholesaler: 'noweda',
  events: ['delivery.received', 'order.status_changed'],
});

// WICHTIG: signing_secret jetzt speichern - wird nicht erneut angezeigt
console.log(`Webhook ID: ${webhook.id}`);
console.log(`Secret: ${webhook.signing_secret}`);

Signatur-Verifizierung

import { verifyWebhookSignature } from '@msv3/sdk';

// Express-Beispiel
app.post('/hooks/msv3', (req, res) => {
  const payload = req.body;         // Roh-String - NICHT zuerst parsen
  const signature = req.headers['x-msv3-signature'] ?? '';
  const secret = process.env.MSV3_WEBHOOK_SECRET!;

  if (!verifyWebhookSignature(payload, signature, secret)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(payload);
  console.log(`Event: ${event.event} von ${event.wholesaler}`);
  res.send('OK');
});

Fehlerbehandlung

import {
  Msv3ApiError,
  Msv3AuthError,
  Msv3BadRequestError,
  Msv3ProductUnavailableError,
  Msv3RateLimitError,
  Msv3WholesalerUnavailableError,
} from '@msv3/sdk';

try {
  const order = await client.orders.create(creds, { items: [...] });
} catch (err) {
  if (err instanceof Msv3AuthError) {
    console.error('Authentifizierung fehlgeschlagen:', err.message);
  } else if (err instanceof Msv3ProductUnavailableError) {
    console.error(`Abgelehnt (Code ${err.code}):`, err.message);
  } else if (err instanceof Msv3RateLimitError) {
    const retryAfter = err.retryAfter ?? 60;
    console.warn(`Rate-Limit. Erneut in ${retryAfter}s`);
  } else if (err instanceof Msv3WholesalerUnavailableError) {
    console.error(`Grosshaendler ${err.wholesaler} nicht erreichbar`);
  } else if (err instanceof Msv3ApiError) {
    console.error(`API-Fehler ${err.status}: ${err.message}`);
  }
}
EigenschaftTypBeschreibung
statusnumberHTTP-Statuscode
typestringMaschinenlesbarer Fehlercode
messagestringMenschenlesbare Beschreibung
wholesalerstring | undefinedGrosshaendler-ID
codestring | undefinedRoher MSV3-Fehlercode
retryAfternumber | undefinedWartezeit in Sekunden (nur 429)
requestIdstringRequest-ID aus dem X-Request-Id-Header

Retry-Strategie

Das SDK wiederholt automatisch bei 503 mit exponentiellem Backoff (bis zu 3 Versuche, ab 500ms). Bei 4xx-Fehlern wird nicht wiederholt.

Fuer 502 (Grosshaendler nicht erreichbar) implementieren Sie eigene Retry-Logik:

async function createOrderWithRetry(
  client: Msv3Client,
  creds: Msv3Credentials,
  items: any[],
  maxAttempts = 3,
) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await client.orders.create(creds, { items });
    } catch (err) {
      if (err instanceof Msv3WholesalerUnavailableError && attempt < maxAttempts) {
        const delay = 1000 * Math.pow(2, attempt - 1);
        console.warn(`Versuch ${attempt} fehlgeschlagen, erneut in ${delay}ms...`);
        await new Promise(r => setTimeout(r, delay));
      } else {
        throw err;
      }
    }
  }
}

Vollstaendiges Beispiel

import { Msv3Client, Msv3ApiError } from '@msv3/sdk';

const client = new Msv3Client({ apiKey: process.env.MSV3_API_KEY! });

const creds = {
  wholesaler: 'noweda',
  username: process.env.MSV3_USERNAME!,
  password: process.env.MSV3_PASSWORD!,
};

async function run() {
  // 1. Verbindung testen
  const connection = await client.connection.test(creds);
  console.log(`Verbunden (${connection.latency_ms}ms)`);

  // 2. Verfuegbarkeit pruefen
  const availability = await client.availability.check(creds, {
    items: [{ pzn: '761271', quantity: 3 }],
  });
  if (!availability.items[0].available) {
    console.log('PZN 761271 nicht verfuegbar');
    return;
  }

  // 3. Bestellung mit Nachlieferung aufgeben
  const order = await client.orders.create(creds, {
    items: [{ pzn: '761271', quantity: 3, delivery_preference: 'backorder' }],
  });
  console.log(`Bestellstatus: ${order.status}`);
  console.log(`Request ID: ${order.request_id}`);
}

run().catch((err) => {
  if (err instanceof Msv3ApiError) {
    console.error(`MSV3-Fehler ${err.status}: ${err.message}`);
  } else {
    throw err;
  }
});