Case studyVerSyncComarch Optima

Case study: synchronizacja produktów i cen między Comarch Optimą a panelem B2B

Robert Mońka ·

To nie jest demo. To nie jest proof of concept. To jest wdrożenie produkcyjne, które działa na żywych danych firmy handlowej — codziennie, od zamówień po faktury.

Kapelanczyk to firma produkcyjno-handlowa. Panel B2B rozwijany od lat, Comarch ERP Optima jako system magazynowo-księgowy. Dwa odrębne systemy, dwie bazy danych, jedno wymaganie: synchronizacja danych — produkty i ceny muszą się zgadzać po obu stronach.

Ten artykuł opisuje, jak Optima API i VerSync realizują tę synchronizację — z konkretnymi konfiguracjami, logiką cen i mechanizmami bezpieczeństwa. Każdy element, który tu widzisz, jest dokładnie tym samym kodem, który dostają klienci WebArm.

Architektura rozwiązania opiera się na dwukierunkowej wymianie danych pomiędzy systemami Comarch ERP Optima i panelem B2B. Synchronizacja dwukierunkowa zapewnia, że zmiany wprowadzone w jednym systemie są automatycznie widoczne w drugim. VerSync wykorzystuje synchronizację inkrementalną — przesyła jedynie te dane, które uległy zmianie, co przyspiesza proces i ogranicza obciążenie bazy. Każda operacja synchronizacji tworzy kronikę zmian (dziennik zmian), pozwalającą na precyzyjne odtworzenie stanu danych i przywracanie poprzednich wersji. Automatyczne tworzenie kopii zapasowych danych odbywa się przy każdym cyklu synchronizacji, a historia wersji umożliwia rollback do dowolnego punktu w czasie.

Synchronizacja danych między Comarch Optima a panelem B2B

Architektura

Comarch ERP Optima (MSSQL)
        ↓ odczyt SQL / zapis COM
   Optima API (REST)

     VerSync

  Panel B2B (MariaDB)

Pipeline kapelanczykPHP-optimaKapelanczykRM łączy dwa connectory:

connectors:
  kapelanczykPHP:
    type: kapelanczyk
    table: product
    primary_key: id
    cursor_column: time_stamp
    batch_size: 100

  optimaKapelanczykRM:
    type: optima
    base_url: "http://127.0.0.1:5000"
    firma: "Kapelanczyk RM"
    auth:
      type: api_key
      header: X-Api-Key
      token: "${OPTIMA_RM_API_KEY}"
    timeout: 15m
    rate_limit:
      requests_per_second: 3
      burst_size: 20

entity_mappings:
  - entity_type: product
    poll_interval: 30s
    init_batch_cooldown: 3s

conflict_resolution:
  strategy: lww
  source_priority:
    optimaKapelanczykRM: 1
    kapelanczykPHP: 2
  • kapelanczykPHP — connector do bazy MariaDB panelu B2B, kursor po time_stamp, batch po 100 rekordów
  • optimaKapelanczykRM — connector REST do Optima API z rate limitem 3 req/s, timeout 15 minut na operację COM

Source of truth to panel B2B (kapelanczykPHP). W przypadku konfliktu — np. jednoczesnej edycji tego samego produktu po obu stronach — wygrywa wersja z panelu. Conflict resolution: LWW (Last Write Wins) z priorytetem: Optima = 1, PHP = 2 (wyższy numer = wyższy priorytet).

Tworzenie nowych rekordów jest jednokierunkowe. W sekcji filters connector PHP ma create_policy: deny (nie tworzy nowych rekordów z Optimy), a Optima ma create_policy: allow — nowe produkty mogą powstawać w panelu B2B i trafiać do Optimy, ale nie odwrotnie.

Wyzwanie: produkty się nie zgadzają

Pozornie prosta synchronizacja produktów okazuje się złożona, gdy przyjrzysz się danym po obu stronach.

Częściowe pokrycie. Optima zawiera materiały, produkty gotowe, usługi, opakowania. Panel B2B potrzebuje tylko produktów handlowych. Nie da się zsynchronizować „wszystkiego” — potrzebny jest precyzyjny filtr.

Filtrowanie w pipeline definiujesz jako klauzulę where po stronie source connectora:

filters:
  connectors:
    kapelanczykPHP:
      where: "brand_id = 2 AND childmodel_type IN (0, 3) AND status > -3"
      create_policy: deny
    optimaKapelanczykRM:
      create_policy: allow
  • brand_id = 2 — produkty marki Versanis
  • childmodel_type IN (0, 3) — produkty proste i zestawy, bez wariantów i wirtualnych
  • status > -3 — wyklucza usunięte

VerSync stosuje where po stronie source connectora, więc do synchronizacji trafiają tylko rekordy spełniające wszystkie kryteria.

Rozbieżność kodów. Historycznie pole kod w Optimie nie odpowiada polu model w panelu. Match key łączy model (PHP) z polem kod (Optima). Dodatkowa komplikacja: Optima przechowuje kody wielkimi literami, panel — nie.

match_key: model
source_of_truth: kapelanczykPHP

fields:
  - canonical: model
    connectors:
      kapelanczykPHP:
        column: model
      optimaKapelanczykRM:
        field: kod
        writable: true
    sync: bidirectional
    role: match_key
    transform: to_upper

  - canonical: model_to_numerkat
    connectors:
      kapelanczykPHP:
        column: model
      optimaKapelanczykRM:
        field: numerKat
        writable: true
    sync: kapelanczykPHP->optimaKapelanczykRM
    role: copy
    transform: to_upper

Transform to_upper normalizuje obie wartości przed porównaniem. Pole model jest matchowane z kod w Optimie i dodatkowo kopiowane jednokierunkowo do numerKat — to osobne pole w Optimie (numer katalogowy), które musi być spójne z kodem panelu.

Trzy cenniki z różną logiką synchronizacji

Trzy cenniki, trzy strategie

Synchronizacja cen to najciekawsza część tego pipeline’u, ponieważ obejmuje automatyczną synchronizację i aktualizowanie danych cenowych w czasie rzeczywistym pomiędzy Comarch ERP Optima a panelem B2B. Pipeline używa trzech cenników, każdy z inną logiką synchronizacji.

CennikInitWatchLogika
ZakupuOptima → PHPOptima → PHPoptima_price_netto
HurtowaPHP → OptimaDwukierunkowaoptima_price_netto
DetalicznaPHP → OptimaDwukierunkowaoptima_price_brutto_stawka

Jak to działa? Pole sync kontroluje kierunek w trybie watch (i domyślnie w init). Pole initial_sync_direction nadpisuje kierunek wyłącznie dla init. Gdy initial_sync_direction jest pominięte — init używa tego samego kierunku co watch.

Konfiguracja cenników w product.yaml:

prices:
  - canonical: purchase_price
    precision: 2
    kapelanczykPHP:
      source: product_price
      product_list_id: 0
    optimaKapelanczykRM:
      cennik_numer: 1
      logic: optima_price_netto
      cennik_nazwa: zakupu
      waluta: PLN
    sync: optimaKapelanczykRM->kapelanczykPHP

  - canonical: wholesale_price
    precision: 2
    kapelanczykPHP:
      source: product_price
      product_list_id: 1
    optimaKapelanczykRM:
      cennik_numer: 2
      logic: optima_price_netto
      cennik_nazwa: hurtowa
      waluta: PLN
    initial_sync_direction: kapelanczykPHP->optimaKapelanczykRM
    sync: bidirectional

  - canonical: retail_price
    precision: 2
    kapelanczykPHP:
      source: product.suggested_retail_price
    optimaKapelanczykRM:
      cennik_numer: 5
      logic: optima_price_brutto_stawka
      cennik_nazwa: detaliczna
      waluta: PLN
    initial_sync_direction: kapelanczykPHP->optimaKapelanczykRM
    sync: bidirectional

Cennik zakupu (purchase_price)

Ceny zakupu istnieją tylko w Optimie (cennik nr 1) — to ceny dostawców, wprowadzane ręcznie lub z dokumentów PZ. Pipeline pobiera je do panelu B2B (product_list_id: 0) jako informację referencyjną. Kierunek jednokierunkowy: sync: optimaKapelanczykRM->kapelanczykPHP — ten sam w init i watch, bo nie ma initial_sync_direction.

Cennik hurtowy (wholesale_price)

Cennik hurtowy (cennik nr 2 w Optimie, product_list_id: 1 w panelu) to główny cennik sprzedażowy. Watch (sync: bidirectional) działa dwukierunkowo. Init ma nadpisany kierunek: initial_sync_direction: kapelanczykPHP->optimaKapelanczykRM — przy pierwszej synchronizacji ceny hurtowe lecą z panelu do Optimy (wyrównanie początkowe).

Cennik detaliczny (retail_price)

Cennik detaliczny (cennik nr 5 w Optimie, suggested_retail_price w panelu) to ceny brutto. Ten sam pattern co hurtowy: sync: bidirectional + initial_sync_direction: kapelanczykPHP->optimaKapelanczykRM. Tu pojawia się konwersja VAT — logika optima_price_brutto_stawka automatycznie przelicza brutto/netto.

Reguły przeliczeniowe z prices.yaml:

price_rules:
  optima_price_netto:
    read: wartoscNetto
    to_internal: input
    write: wartoscNetto
    to_external: input

  optima_price_brutto_stawka:
    read: wartoscBrutto
    to_internal: round(input / (1 + vat/100), 2)
    write: wartoscBrutto
    to_external: round(input * (1 + vat/100), 2)
    vars:
      vat:
        from: record
        key: stawka

optima_price_netto to pass-through — wartość netto bez konwersji. optima_price_brutto_stawka przelicza brutto na netto (i odwrotnie) ze stawką VAT z pola stawka karty towaru.

Dwufazowa automatyczna synchronizacja

Automatyzacja procesów synchronizacji produktów jest podzielona na dwie fazy, podobnie jak w typowej integracji sklepu internetowego z Comarch ERP Optima:

Faza 1: brand Versanis — produkty katalogowe, dostępne w ofercie publicznej. Mniejszy zbiór, stabilniejsze dane, rzadsze zmiany. Ta faza działa jako pierwsza, żeby ustabilizować bazowy katalog.

Faza 2: brand Kapelanczyk — produkty klientów, zamówienia specjalne, konfiguracje indywidualne. Większy zbiór, częstsze zmiany, wyższe ryzyko konfliktów.

Rozdzielenie ma sens operacyjny: jeśli faza 2 napotka problem (np. nowy typ produktu, nieoczekiwany format danych), faza 1 działa niezależnie. Izolacja awarii.

Wydajność: odczyt z SQL, zapis przez COM

Comarch ERP Optima nie ma oficjalnego REST API. Standardowy dostęp do danych to COM — technologia z lat 90., jednowątkowa, wrażliwa na timeout i wycieki pamięci. Optima API od WebArm rozwiązuje ten problem architekturą dwuwarstwową (szczegółowe porównanie COM vs REST).

Odczyt: bezpośrednio z MSSQL

Endpoint /api/sync/towary omija COM całkowicie i jest kluczowy dla automatycznej synchronizacji stanów magazynowych z Comarch ERP Optima. Czyta dane towarów bezpośrednio z bazy SQL Server Optimy, gdzie dane są przechowywane, z cursor-based pagination. Konfiguracja tego endpointu w pipeline:

endpoints:
  fetch_changes:
    method: GET
    path: "/api/sync/towary"
    query_params:
      modifiedSince: "{cursor}"
    pagination:
      mode: cursor
      page_size: 100
      has_more_path: hasMore
      next_cursor_path: nextCursor

VerSync odpytuje GET /api/sync/towary?modifiedSince={cursor}&take=100 — dostaje tylko towary zmienione od ostatniego cyklu, po 100 rekordów na stronę.

Batch read dla wielu towarów jednocześnie:

endpoints:
  fetch_by_match_keys:
    method: POST
    path: "/api/towary/batch/read"
    request:
      keys_field: kody
      key_type: string
POST /api/towary/batch/read
Content-Type: application/json

{
  "kody": ["PROD-001", "PROD-002", "PROD-003"]
}

Oba endpointy korzystają z SQL — szybki odczyt, brak ograniczeń COM, pełna wydajność bazy danych.

Zapis: zawsze przez COM

Zapisy do Optimy muszą przechodzić przez COM. To nie jest wybór architektoniczny — to wymóg Comarch. COM waliduje dane biznesowe: sprawdza stawki VAT, oblicza wartości, aktualizuje powiązane tabele. Bezpośredni zapis do SQL pominąłby tę walidację i mógłby uszkodzić bazę.

Mechanizmy bezpieczeństwa Optima API

COM jest niestabilny z natury. Optima API ma wbudowane zabezpieczenia, konfigurowane w appsettings.json, które zapewniają że dane są chronione, a nad bezpieczeństwem systemu czuwają dedykowane mechanizmy:

{
  "ComQueueCapacity": 64,
  "MemoryGuardPrivateMb": 1200,
  "MemoryGuardRecycleAfterSeconds": 120,
  "ComHangWatchdogSeconds": 180,
  "ComHangWatchdogPollSeconds": 5,
  "OverloadShieldEnabled": true,
  "OverloadBlockThreshold": 8
}
  • MemoryGuard — próg MemoryGuardPrivateMb: 1200. Gdy proces COM przekroczy 1200 MB, sesja jest recyklowana zanim dojdzie do crash. Recycle po 120 sekundach stabilnej pracy
  • ComHangWatchdog — timeout ComHangWatchdogSeconds: 180. Jeśli operacja COM się zawiesi, watchdog ją przerywa co 5 sekund sprawdzając stan
  • OverloadShield — middleware blokujący żądania gdy aktywnych operacji > OverloadBlockThreshold: 8
  • Bounded channel queueComQueueCapacity: 64, single STA thread. Kolejka operacji COM z ograniczoną pojemnością

Mechanizmy bezpieczeństwa VerSync

Po stronie VerSync:

  • In-memory channel queue z backpressure — gdy kolejka jest pełna, nowe operacje dostają błąd natychmiast (fail-fast), zamiast czekać w nieskończoność
  • Init batch — batch po 100 rekordów, cooldown 3 sekundy między batchami (init_batch_cooldown: 3s). Chroni bazę docelową przed zalaniem
  • Outbox relay — guaranteed delivery. Każda operacja zapisu trafia najpierw do outboxa. Dopiero po potwierdzeniu dostarczenia jest usuwana
  • MemoryPressureChecker — VerSync odpytuje guard state Optima API i wstrzymuje operacje, gdy API raportuje memory pressure
  • Kopie zapasowe danych — VerSync przechowuje historię synchronizacji i snapshoty, co umożliwia przywracanie wielu wersji danych i planowe tworzenie kopii zapasowych niezależnie od głównego procesu synchronizacji

Bezpieczeństwo operacyjne i kopii zapasowych

Synchronizacja danych produkcyjnych wymaga mechanizmów bezpieczeństwa na każdym etapie. Automatyczne tworzenie kopii zapasowych danych przy każdym cyklu synchronizacji zapewnia ochronę i możliwość przywracania informacji w razie niepowodzenia operacji.

Dry-run

Przed każdą nową konfiguracją — dry-run. Flaga --dry-run uruchamia pełny cykl synchronizacji bez zapisywania zmian:

versync init --pipeline kapelanczykPHP-optimaKapelanczykRM --dry-run

VerSync porównuje oba systemy i generuje raport z polami ReconcileSummary:

{
  "entity_type": "product",
  "left_source": "kapelanczykPHP",
  "right_source": "optimaKapelanczykRM",
  "summary": {
    "total_left": 847,
    "total_right": 912,
    "matched": 835,
    "identical": 801,
    "divergent": 34,
    "only_left": 12,
    "only_right": 77
  }
}

matched — rekordy z pasującym match key po obu stronach. divergent — matchowane, ale z różnicami w polach. only_left / only_right — rekordy bez odpowiednika po drugiej stronie. Zero niespodzianek.

Single-record test

Flaga --record filtruje synchronizację do jednego rekordu po match key:

versync init --pipeline kapelanczykPHP-optimaKapelanczykRM --record "PROD-001" --dry-run

Rollback i przywracanie

Każda operacja synchronizacji tworzy snapshot, który może być wykorzystany do przywracania danych w przypadku awarii lub utraty informacji. W razie problemu — szybkie przywracanie do stanu sprzed konkretnego cyklu, zarówno z lokalnych kopii zapasowych, jak i z historii wersji w bazie VerSync.

Monitoring

Metryki synchronizacji dostępne w Grafanie (dodatkowe case studies i przewodniki znajdziesz na blogu o integracji z Comarch ERP Optima):

  • Liczba zsynchronizowanych rekordów per cykl
  • Czas trwania cyklu (odczyt + zapis)
  • Błędy z pełnym kontekstem (jaki rekord, jaki błąd, kiedy)
  • Zużycie pamięci COM i VerSync

Podsumowanie

Synchronizacja produktów i cen między Comarch Optimą a panelem B2B działa na produkcji — z trzema cennikami, dwufazowym pipeline’em i mechanizmami bezpieczeństwa na każdym poziomie.

To dokładnie ten sam kod i ta sama architektura, którą dostają klienci WebArm. Żadnych demo, żadnych uproszczonych wersji — pełny produkt, przetestowany na żywych danych.


Chcesz zsynchronizować produkty lub ceny z Optimy ze swoim systemem? Sprawdź odpowiedzi na najczęstsze pytania lub skontaktuj się z nami.


Robert Mońka — WebArm

Robert Mońka

Programista, właściciel firmy produkcyjno-handlowej, lider zespołu WebArm. Wraz z zespołem rozwija Optima API i VerSync — REST API i synchronizatory do Comarch ERP Optima używane też w jego własnych firmach.

LinkedIn →

Masz pytania o integrację z Comarch Optima?