Przejdź do głównej zawartości

Wzorzec adapter

Ksefnik komunikuje się z KSeF API wyłącznie przez interfejs KsefAdapter. Dzięki temu implementacja klienta jest wymienialna — od symulatora offline po własny wrapper na istniejący kod.

Różne sytuacje wymagają różnych implementacji klienta KSeF:

  • Produkcja — prawdziwe API z autentykacją, retry i cache
  • Testy — symulator offline bez sieci
  • Vitest — mock z predefiniowanymi odpowiedziami
  • Migracja — wrapper na istniejący klient (np. ksefapi.pl)

Twardy coupling do jednego klienta HTTP uniemożliwiłby te scenariusze.

interface KsefAdapter {
fetchInvoices(opts: FetchInvoicesOpts): Promise<Invoice[]>
sendInvoice(input: SendInvoiceInput): Promise<SendInvoiceResult>
getUpo(ksefReference: string): Promise<UpoResult>
}

Trzy metody — to cały kontrakt. Każda implementacja musi je spełnić. Nic więcej.

Wewnątrz @ksefnik/core adapter produkcyjny składa się z warstw:

KsefAdapter (interfejs publiczny)
↓
RetryLayer → automatyczny retry z exponential backoff
↓
CacheLayer → cache odpowiedzi (TTL)
↓
SessionManager → zarządzanie sesją KSeF (init/terminate)
↓
KsefClient → HTTP client (wewnętrzny, niepubliczny)

Użytkownik widzi tylko KsefAdapter. Warstwy wewnętrzne to detal implementacyjny.

Adapter wstrzykiwany jest przez createKsefnik():

const ksefnik = createKsefnik({
config: { nip: '1234567890', environment: 'production' },
adapter: myAdapter, // ← dependency injection
})

Brak adaptera = brak komunikacji z KSeF. Metody fetch(), send() i getUpo() rzucą Error.

import { createSimulatorAdapter } from '@ksefnik/simulator'
const ksefnik = createKsefnik({
config: { nip: '1234567890', environment: 'test' },
adapter: createSimulatorAdapter({ scenario: 'happy-path' }),
})
// Działa offline — odpowiedzi z predefiniowanych scenariuszy
const invoices = await ksefnik.invoices.fetch({
from: '2025-01-01',
to: '2025-12-31',
})

Kluczowa zaleta: ten sam kod biznesowy działa z dowolnym adapterem:

// Ten sam kod — różne adaptery
async function reconcileLastMonth(adapter: KsefAdapter) {
const ksefnik = createKsefnik({
config: { nip: '1234567890', environment: 'production' },
adapter,
})
const invoices = await ksefnik.invoices.fetch({
from: '2025-03-01',
to: '2025-03-31',
})
const txs = await ksefnik.bank.list()
return ksefnik.reconciliation.run(invoices, txs)
}
// Produkcja
await reconcileLastMonth(productionAdapter)
// Test
await reconcileLastMonth(createSimulatorAdapter({ scenario: 'happy-path' }))