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”.
Jak się zapisać
Dział zatytułowany „Jak się zapisać”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(), ]]);}Koperta
Dział zatytułowany „Koperta”| Getter | Znaczenie |
|---|---|
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().
Dwa zapisy i nie są wymienne
Dział zatytułowany „Dwa zapisy i nie są wymienne”| Zapis | Rodzaj | Ty |
|---|---|---|
p2lab.stockly.… — kropka po nazwie dostawcy | hook | odpowiadasz |
p2lab_stockly.… — podkreślenie | fakt | obserwujesz |
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.
Popyt — czego chce zamówienie i co je pokrywa
Dział zatytułowany „Popyt — czego chce zamówienie i co je pokrywa”| Zdarzenie | Kiedy | Kluczowy ładunek |
|---|---|---|
p2lab_stockly.demand.declared | popyt pozycji zostaje zapisany po raz pierwszy | quantity, allocatedQuantity, shortfallQuantity, warehouseIds[], productId, orderedProductId |
p2lab_stockly.demand.changed | popyt albo pokrycie istniejącej pozycji się zmieniły | powyższe plus previousQuantity, previousAllocated, previousShortfall |
p2lab_stockly.demand.deducted | towar fizycznie zszedł z półki na to zamówienie | totalQuantity, lines[] z warehouseId / binLocationId / batchId / handlingUnitId |
p2lab_stockly.demand.released | zamówienie przestało czegokolwiek chcieć | releasedQuantity, reasonCode (cancel / delete / edit), returnMode |
p2lab_stockly.sourcing.decided | pozycja została przypisana do węzła, przeniesiona do innego albo nie dało się jej przypisać wcale | chosenWarehouseId, 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.
Zamówienia oczekujące — co jest należne
Dział zatytułowany „Zamówienia oczekujące — co jest należne”| Zdarzenie | Kiedy | Kluczowy ładunek |
|---|---|---|
p2lab_stockly.backorder.opened | to, co należy się pozycji, wzrosło | shortfallQuantity (nowa suma), previousShortfall (0 = prawdziwe otwarcie), totalOutstanding |
p2lab_stockly.backorder.covered | sztuki zostały przypisane czekającej pozycji | coveredQuantity, remainingShortfall, triggerReason (receipt / transfer / release / correction) |
p2lab_stockly.backorder.closed | pozycji nie należy się już nic | closedQuantity, 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.
Towar — co przyszło
Dział zatytułowany „Towar — co przyszło”| Zdarzenie | Kiedy | Kluczowy ładunek |
|---|---|---|
p2lab_stockly.goods.received | przyjęcie zamówienia zakupu zostało zaksięgowane | purchaseOrderId, 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ą”| Zdarzenie | Kiedy | Kluczowy ładunek |
|---|---|---|
p2lab_stockly.stock.moved | towar przyszedł, wyszedł albo się przemieścił | movementType, quantity, delta, quantityAfter, from / to {warehouseId, binLocationId, lpCode}, batchId, expiresAt, orderId, purchaseOrderId, operationId |
p2lab_stockly.stock.availability_changed | zmienił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.
Praca — co zrobili ludzie w budynku
Dział zatytułowany „Praca — co zrobili ludzie w budynku”| Zdarzenie | Kiedy | Kluczowy ładunek |
|---|---|---|
p2lab_stockly.work.pick_wave_planned | praca została przekazana kompletującemu | fala i jej pozycje |
p2lab_stockly.work.short_pick_reported | kompletujący zastał bin pusty albo niepełny | expectedQuantity, foundQuantity, missingQuantity, reasonCode, stockCorrected, varianceHeld, reallocatedQuantity |
p2lab_stockly.work.short_pick_withdrawn | to zgłoszenie zostało wycofane — towar jednak był | expectedQuantity, missingQuantity, restoredQuantity, earmarkRestored |
p2lab_stockly.work.pick_line_completed | sztuki trafiły do pojemnika | quantity, quantityPicked, quantityRequired, isComplete, pickLineId |
p2lab_stockly.work.stocktake_committed | sesja kontroli stanu została zatwierdzona | lineCount, varianceLineCount, totalVarianceUnits, lines[], linesTruncated |
p2lab_stockly.work.quality_decided | partia z kwarantanny została dopuszczona albo odrzucona | decision, quantity, batchNumber, failAction, reason |
p2lab_stockly.work.transfer_state_changed | przewóz wyruszył, dojechał, został zamknięty albo anulowany | fromState, 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.
Wysyłka — co opuściło budynek
Dział zatytułowany „Wysyłka — co opuściło budynek”| Zdarzenie | Kiedy | Kluczowy ładunek |
|---|---|---|
p2lab_stockly.shipping.shipment_booked | jedna paczka dostała numer przesyłki | orderNumber, parcelId, sequence, trackingCode, carrierProfile, weightKg, parcelCount, remainingParcels |
p2lab_stockly.shipping.order_shipped | każda paczka zamówienia ma numer przesyłki | orderNumber, parcelCount, trackingCodes[], trackingCodeList |
p2lab_stockly.shipping.shipment_failed | przewoźnik odmówił, a próby się wyczerpały | orderNumber, parcelId, sequence, carrierProfile, attempts, error |
p2lab_stockly.shipping.batch_handed_over | kierowca zabrał zestaw paczek i podpisał odbiór | code, 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.
Zwroty — co wróciło
Dział zatytułowany „Zwroty — co wróciło”| Zdarzenie | Kiedy | Kluczowy ładunek |
|---|---|---|
p2lab_stockly.returns.announced | ktoś zgłosił zwrot; nic się jeszcze nie ruszyło | returnNumber, orderId, type (withdrawal / complaint / cancellation), source (storefront / admin / system), contactEmail, requestedResolution, lineCount, totalQuantity, reasonCodes[] |
p2lab_stockly.returns.received | towar dotarł i został zaksięgowany w strefie zwrotów | returnNumber, warehouseId, binLocationId, receivedQuantity, announcedQuantity, outstandingQuantity, complete, lines[] |
p2lab_stockly.returns.dispositioned | ktoś zdecydował, co dalej ze sztukami | returnNumber, lineId, productId, disposition (restock / scrap / hold), quantity, reasonCode, liability, remainingOnHold, documentComplete |
p2lab_stockly.returns.refunded | pieniądze wróciły do klienta | returnNumber, 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.
Spójność
Dział zatytułowany „Spójność”| Zdarzenie | Kiedy |
|---|---|
p2lab_stockly.stock_integrity_critical | skan znalazł więcej otwartych znalezisk krytycznych niż ustawiony próg |
p2lab_stockly.external_stock_write_detected | ktoś 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().
Zdarzenia po nazwie klasy
Dział zatytułowany „Zdarzenia po nazwie klasy”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.
| Klasa | Kiedy |
|---|---|
P2Lab\Stockly\Event\StockChangedEvent | tani sygnał „ten produkt się zmienił, odczytaj go ponownie” |
P2Lab\Stockly\Event\BeforeStockOperationEvent | przed dodaniem, zdjęciem albo przewozem — hook odmowy, opisany w punktach decyzyjnych |
Gwarancje i to, czego nie ma
Dział zatytułowany „Gwarancje i to, czego nie ma”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ń.
Flow Builder i webhooki App System
Dział zatytułowany „Flow Builder i webhooki App System”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.
Zgodność
Dział zatytułowany „Zgodność”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().
- Punkty decyzyjne — zdarzenia, na które się odpowiada, zamiast je obserwować
- Jak położyć zapas w binie — co publikuje zdarzenia o zapasie