Przejdź do głównej zawartości

Zdarzenia

Stockly publikuje to, co dzieje się w magazynie, żeby integracja nie musiała odpytywać jego tabel — ani, co gorsza, zapisywać product.stock, który moduł posiada i nadpisuje.

Katalog niżej pogrupowany jest w płaszczyzny. bin/console p2lab-stockly:events wypisuje żywy katalog wraz z liczbą nasłuchujących, co jest też najszybszą odpowiedzią na pytanie „czy mój nasłuchujący w ogóle jest podpięty”.

Zapisz się po nazwie zdarzenia, a ładunek czytaj przez gettery sprawdzane po nazwie metody. Żadnego use klasy ze Stockly, żadnej zależności w composer.json; wtedy wtyczka działa i gdy Stockly nie ma, i gdy jest, i w wersjach, w których jakaś klasa się przeniosła.

public static function getSubscribedEvents(): array
{
return ['p2lab_stockly.backorder.opened' => 'onBackorderOpened'];
}
public function onBackorderOpened(object $event): void
{
if (!method_exists($event, 'getPayload')) {
return;
}
$data = $event->getPayload();
// $data['orderId'], $data['lineItemId'], $data['shortfallQuantity'], …
}

Każde zdarzenie niesie tę samą kopertę, więc uniwersalny most (webhook, kolejka, dziennik audytu) nie potrzebuje wiedzy o żadnej konkretnej klasie i działa dalej dla zdarzeń dodanych później:

public function forward(object $event): void
{
if (!method_exists($event, 'getPayload') || !method_exists($event, 'getName')) {
return;
}
$this->http->post($this->url, ['json' => [
'event' => $event->getName(),
'id' => $event->getEventId(), // klucz do odsiewania duplikatów
'at' => $event->getOccurredAt()->format(DATE_ATOM),
'data' => $event->getPayload(),
]]);
}
GetterZnaczenie
getName()stała nazwa publiczna, np. p2lab_stockly.demand.declared
getEventId()unikalne 32 znaki szesnastkowe — użyj do odsiewania duplikatów
getSchemaVersion()podnoszona tylko wtedy, gdy klucz ładunku znika albo zmienia znaczenie
getOccurredAt()DateTimeImmutable
getCorrelationId()spina jedną całość pracy (identyfikator zamówienia, zamówienia zakupu)
getActor()['type' => user|storefront|cli|system, 'id' => ?string, 'name' => ?string]
getPayload()cały fakt, jako wartości proste i tablice nadające się do JSON-a
getContext()Context Shopware

Ładunki to wartości proste, nulle i tablice z nich, nigdy encje, struktury czy obiekty DateTime; daty są napisami ISO-8601. Każdy identyfikator to 32 małe znaki szesnastkowe bez myślników, gotowe do UNHEX().

ZapisRodzajTy
p2lab.stockly.…kropka po nazwie dostawcyhookodpowiadasz
p2lab_stockly.…podkreśleniefaktobserwujesz

Nazwy z kropką należą do hooków wysyłki i druku: p2lab.stockly.parcels.plan, p2lab.stockly.label.print i pokrewne, opisane w Integracji ze Stockly.

ZdarzenieKiedyKluczowy ładunek
p2lab_stockly.demand.declaredpopyt pozycji zostaje zapisany po raz pierwszyquantity, allocatedQuantity, shortfallQuantity, warehouseIds[], productId, orderedProductId
p2lab_stockly.demand.changedpopyt albo pokrycie istniejącej pozycji się zmieniłypowyższe plus previousQuantity, previousAllocated, previousShortfall
p2lab_stockly.demand.deductedtowar fizycznie zszedł z półki na to zamówienietotalQuantity, lines[] z warehouseId / binLocationId / batchId / handlingUnitId
p2lab_stockly.demand.releasedzamówienie przestało czegokolwiek chciećreleasedQuantity, reasonCode (cancel / delete / edit), returnMode
p2lab_stockly.sourcing.decidedpozycja została przypisana do węzła, przeniesiona do innego albo nie dało się jej przypisać wcalechosenWarehouseId, previousWarehouseId, rule, ruleSource, degraded, candidateCount, candidates[], triggerSource, requirement, settingsFingerprint

Na declared i changed zawsze zachodzi quantity = allocatedQuantity + shortfallQuantity.

sourcing.decided wysyłane jest tylko wtedy, gdy wynik jest wart zgłoszenia: węzeł się zmienił, decyzja została podjęta w trybie awaryjnym albo popytu nie dało się przypisać. Powtórzona identyczna decyzja podbija tylko licznik na wierszu przypisania, więc nasłuchujący widzi historię trasowania, a nie szum edycji zamówienia.

productId to produkt, który niesie zapas; orderedProductId to ten z pozycji zamówienia. Różnią się, gdy współpracująca wtyczka przekierowuje zapas, na przykład zestaw pobierający ze składników.

ZdarzenieKiedyKluczowy ładunek
p2lab_stockly.backorder.openedto, co należy się pozycji, wzrosłoshortfallQuantity (nowa suma), previousShortfall (0 = prawdziwe otwarcie), totalOutstanding
p2lab_stockly.backorder.coveredsztuki zostały przypisane czekającej pozycjicoveredQuantity, remainingShortfall, triggerReason (receipt / transfer / release / correction)
p2lab_stockly.backorder.closedpozycji nie należy się już nicclosedQuantity, closeReason (fulfilled / cancelled)

covered wysyłane jest także przy pokryciu częściowym; liczy się remainingShortfall. Po pełnym pokryciu przychodzi closed.

Pozycja, której wiersz przypisania został skasowany w całości, nie daje closed w ogóle, ponieważ zniknął wiersz, na którym żył brak. Na ten przypadek obserwuj demand.released.

ZdarzenieKiedyKluczowy ładunek
p2lab_stockly.goods.receivedprzyjęcie zamówienia zakupu zostało zaksięgowanepurchaseOrderId, supplierId, warehouseId, totalQuantity, items[] z ilością, binem, partią, terminem, nośnikiem i kosztem

Jedno zdarzenie na przyjęcie, nie na pozycję, bo przyjęcie jest jedną decyzją. Podział siedzi w items[].

Zapas — co się ruszyło i co to zrobiło z dostępnością

Dział zatytułowany „Zapas — co się ruszyło i co to zrobiło z dostępnością”
ZdarzenieKiedyKluczowy ładunek
p2lab_stockly.stock.movedtowar przyszedł, wyszedł albo się przemieściłmovementType, quantity, delta, quantityAfter, from / to {warehouseId, binLocationId, lpCode}, batchId, expiresAt, orderId, purchaseOrderId, operationId
p2lab_stockly.stock.availability_changedzmieniło się to, co da się sprzedaćpreviousAtp, currentAtp, shelfTotal, shortfallTotal, isNegative, previousAvailable, currentAvailable

Kierunek siedzi w from / to: przyjęcie ma from = null i dodatnią delta, wydanie ma to = null i ujemną, a przewóz ma oba końce i delta = 0, bo suma magazynu się nie zmieniła.

Przyjęcie, odkładanie, liczenie i paletyzacja to wszystko stock.moved z innym movementType. Jeden fakt, jedno zdarzenie: filtruj po typie, zamiast szukać osobnego zdarzenia na każdą operację.

availability_changed jest celowo osobne. Rezerwacja zmienia dostępność bez ruchu towaru, a przełożenie z binu do binu rusza towar, nie tykając dostępności. Jest odsiewane per produkt na jedną całość pracy, więc currentAtp jest zawsze liczbą zatwierdzoną, nigdy pośrednią. Może być ujemne i jest publikowane bez przycinania: sprzedaż trzech sztuk więcej, niż jest na stanie, to odpowiedź, a nie uszkodzenie danych.

ZdarzenieKiedyKluczowy ładunek
p2lab_stockly.work.pick_wave_plannedpraca została przekazana kompletującemufala i jej pozycje
p2lab_stockly.work.short_pick_reportedkompletujący zastał bin pusty albo niepełnyexpectedQuantity, foundQuantity, missingQuantity, reasonCode, stockCorrected, varianceHeld, reallocatedQuantity
p2lab_stockly.work.short_pick_withdrawnto zgłoszenie zostało wycofane — towar jednak byłexpectedQuantity, missingQuantity, restoredQuantity, earmarkRestored
p2lab_stockly.work.pick_line_completedsztuki trafiły do pojemnikaquantity, quantityPicked, quantityRequired, isComplete, pickLineId
p2lab_stockly.work.stocktake_committedsesja kontroli stanu została zatwierdzonalineCount, varianceLineCount, totalVarianceUnits, lines[], linesTruncated
p2lab_stockly.work.quality_decidedpartia z kwarantanny została dopuszczona albo odrzuconadecision, quantity, batchNumber, failAction, reason
p2lab_stockly.work.transfer_state_changedprzewóz wyruszył, dojechał, został zamknięty albo anulowanyfromState, toState, sourceWarehouseId, targetWarehouseId, lines[]

short_pick_reported wysyłane jest niezależnie od tego, czy bramka rozbieżności poprawiła półkę; sama obserwacja jest warta opublikowania. short_pick_withdrawn jest osobnym zdarzeniem, a nie zgłoszeniem z odwróconymi liczbami: kto zareagował na brak, musi zostać powiadomiony, że brak został wycofany, a restoredQuantity i earmarkRestored mówią, co dokładnie zaksięgowała rekompensata.

stocktake_committed wysyłane jest nawet wtedy, gdy nic się nie różniło, ponieważ czyste liczenie to liczba, na której mierzy się dokładność. Obcina lines[] do 500, największe rozbieżności najpierw, i mówi o tym w linesTruncated.

pick_line_completed dotyczy wyłącznie przyjętych skanów; duplikaty i konflikty nie publikują nic, bo nie zaliczyły niczyjej pracy.

ZdarzenieKiedyKluczowy ładunek
p2lab_stockly.shipping.shipment_bookedjedna paczka dostała numer przesyłkiorderNumber, parcelId, sequence, trackingCode, carrierProfile, weightKg, parcelCount, remainingParcels
p2lab_stockly.shipping.order_shippedkażda paczka zamówienia ma numer przesyłkiorderNumber, parcelCount, trackingCodes[], trackingCodeList
p2lab_stockly.shipping.shipment_failedprzewoźnik odmówił, a próby się wyczerpałyorderNumber, parcelId, sequence, carrierProfile, attempts, error
p2lab_stockly.shipping.batch_handed_overkierowca zabrał zestaw paczek i podpisał odbiórcode, carrierProfile, warehouseId, shipDate, parcelCount, orderCount, manifestExternalId, closedByName

remainingParcels jest tym, co pozwala wyrazić wysyłkę częściową: maszyna stanów dostaw Shopware ma stan „wysłane częściowo”, a bez licznika Flow mógłby powiedzieć tylko „wysłane”, co nie jest prawdą do zaksięgowania ostatniej paczki.

order_shipped może zostać wysłane więcej niż raz dla tego samego zamówienia i tak ma być. Dołożenie paczki do wysłanego już zamówienia i zaksięgowanie jej znów spełnia warunek, a to jest druga wysyłka, a nie usterka. Flow reagujący na to zdarzenie powinien wymusić przejście stanu, bo inaczej drugie uruchomienie zawiedzie na zamówieniu, które już jest w stanie docelowym.

batch_handed_over celowo nie niesie jednego zamówienia, ponieważ zestaw obejmuje tyle zamówień, ile zmieściło się na wózku. Automatyzacja per zamówienie należy do dwóch pozostałych.

ZdarzenieKiedyKluczowy ładunek
p2lab_stockly.returns.announcedktoś zgłosił zwrot; nic się jeszcze nie ruszyłoreturnNumber, orderId, type (withdrawal / complaint / cancellation), source (storefront / admin / system), contactEmail, requestedResolution, lineCount, totalQuantity, reasonCodes[]
p2lab_stockly.returns.receivedtowar dotarł i został zaksięgowany w strefie zwrotówreturnNumber, warehouseId, binLocationId, receivedQuantity, announcedQuantity, outstandingQuantity, complete, lines[]
p2lab_stockly.returns.dispositionedktoś zdecydował, co dalej ze sztukamireturnNumber, lineId, productId, disposition (restock / scrap / hold), quantity, reasonCode, liability, remainingOnHold, documentComplete
p2lab_stockly.returns.refundedpieniądze wróciły do klientareturnNumber, orderId, amount (ta wpłata; ujemna przy obciążeniu zwrotnym), refundedTotal, refundState, source (manual / payment), comment

returns.refunded nie wynika z żadnego z pozostałych trzech. Reklamacja bywa rutynowo przyjęta, rozstrzygnięta i zamknięta, kiedy przelew jest wciąż do zrobienia, i dlatego pieniądze mają własną oś statusu. Wysyłane jest raz na wpłatę, więc reklamacja rozliczona w dwóch ratach wyśle je dwa razy: amount to ta rata, a refundedTotal to stan reklamacji po niej.

source i type są w ładunku, bo ten sam dokument powstaje trzema drogami: kreator w sklepie, operator odbierający telefon i ścieżka anulowania, w której nie ma człowieka w ogóle. Flow wysyłający „przyjęliśmy zgłoszenie zwrotu” chce pierwszej z nich, a nie dwóch pozostałych.

announcedQuantity jedzie obok receivedQuantity celowo: różnica między nimi to przypadek zwyczajny i zwykle to właśnie on jest wart reakcji.

ZdarzenieKiedy
p2lab_stockly.stock_integrity_criticalskan znalazł więcej otwartych znalezisk krytycznych niż ustawiony próg
p2lab_stockly.external_stock_write_detectedktoś zapisał product.stock z zewnątrz, a strażnik to odnotował

external_stock_write_detected niesie productId, expectedValue (co trzymało Stockly), foundValue (co zostawił piszący), delta, source (dal dla zapisu przez API albo DAL, sql dla zapisu wprost w bazie) oraz policyApplied (off / correct / restore). Jedno wysłanie na produkt na całość pracy, po zatwierdzeniu transakcji. Zobacz obce zapisy.

stock_integrity_critical jest pisane pod Flow Builder i niesie wartości proste zamiast koperty; każde inne zdarzenie na tej stronie odpowiada na getPayload().

Dwa zdarzenia są starsze od schematu nazewnictwa i są wysyłane pod pełną nazwą klasy. To się nie zmieni, bo wtyczki są już na nie zapisane.

KlasaKiedy
P2Lab\Stockly\Event\StockChangedEventtani sygnał „ten produkt się zmienił, odczytaj go ponownie”
P2Lab\Stockly\Event\BeforeStockOperationEventprzed dodaniem, zdjęciem albo przewozem — hook odmowy, opisany w punktach decyzyjnych

Fakty są publikowane po zatwierdzeniu transakcji. To, co opisuje zdarzenie, już się wydarzyło i nie zostanie później wycofane.

Wadliwy nasłuchujący nie zepsuje magazynu. Wyjątki z nasłuchujących na fakty są logowane i połykane. Symfony nie izoluje jednak nasłuchujących od siebie, więc rzucający wyjątek odcina tych, którzy stoją w kolejce za nim. Nasłuchujący na decyzje działają odwrotnie: ich wyjątki idą w górę, bo wpływ na wynik jest tam całym sensem.

Nie ma gwarancji dostarczenia. Zdarzenia to wywołania funkcji w procesie. Jeśli proces padnie między zatwierdzeniem a wysłaniem, zmiana jest w bazie, a zdarzenie nie dotarło do nikogo, i nie zostanie powtórzone później, bo uzgadnianie jest idempotentne i następny przebieg nie widzi nic do zrobienia. Integracja, która nie może się rozminąć z magazynem, potrzebuje własnego okresowego uzgodnienia, nie samych zdarzeń.

Nie ma gwarancji kolejności między płaszczyznami. W jednej całości pracy zdarzenie o zamówieniu oczekującym i zdarzenie o popycie mogą przyjść w dowolnej kolejności. getCorrelationId() razem z getOccurredAt() pozwala je pogrupować i posortować.

Nasłuchujący nie jest w transakcji Stockly. Wycofanie jego zapisów nie wycofa zapisów Stockly.

Odczyt nie publikuje niczego. Raporty i listy nie wysyłają zdarzeń.

Dwadzieścia trzy zdarzenia są oferowane jako wyzwalacze Flow Buildera, więc sklep może powiesić na nich mail, webhook albo tag bez pisania kodu: każde powyższe poza pięcioma, które wysyłane są per pozycja, per ruch albo per skan, czyli poza demand.declared, demand.changed, sourcing.decided, stock.moved i work.pick_line_completed.

Te same zdarzenia trafiają do webhooków App System. Shopware traktuje każde zdarzenie zgodne z Flow jako podpinalne, więc aplikacja może się na nie zapisać, a dostarczanie idzie przez własny dziennik webhooków i kolejkę rdzenia, z ponawianiem i sprzątaniem. Dla systemu zewnętrznego to najpewniejsza droga i nie potrzebuje niczego od tej wtyczki.

Listą rządzą dwie zasady i obie istnieją po to, żeby była uczciwa.

Cykl życia oferowany jest w całości. opened / covered / closed chodzą razem, a deducted chodzi z released. Pół cyklu życia w Flow Builderze jest gorsze niż nic: pozwala zbudować automatyzację, która mówi klientowi, że towar jest w drodze, po tym jak poprosił o zwrot pieniędzy.

Zdarzenia o dużej częstotliwości zostają poza listą. Wymienione wyżej pięć wysyłane jest per pozycja zamówienia, per ruch i per skan. Zaoferowanie ich w panelu kończy się automatyzacją wysyłającą tysiąc maili dziennie. Kto naprawdę ich potrzebuje, pisze wtyczkę i zapisuje się po nazwie, gdzie żadnego limitu nie ma. stock.availability_changed jest na liście mimo ruchliwej płaszczyzny i powodem jest odsiewanie duplikatów: wysyła się, gdy dostępność faktycznie się zmieniła, a nie gdy cokolwiek się wydarzyło.

Nazwy zdarzeń nigdy się nie zmieniają. Konstruktory zyskują wyłącznie opcjonalne parametry na końcu. getPayload() może bez uprzedzenia zyskać klucze; klucz znika albo zmienia znaczenie tylko wraz z podniesieniem getSchemaVersion().