A megelőzés után a már felhalmozódott adat takarítása: 70 duplikált névcsoport,
146 rekord, ebből 76 beolvasztandó.
Két parancs, mert a kivezetés három környezeten megy (local -> d2d -> éles), és
minden környezetnek SAJÁT duplikátum-készlete van - a döntési lapot ezért ott
kell újragenerálni, nem beégetett ID-listával dolgozunk.
- producers:dedupe-report — döntési xlsx a megrendelőnek. A megtartandó a
legtöbb TERMÉKKEL rendelkező rekord (döntetlennél több rendelési tétel, majd
régebbi rekord); a végleges nevet a megrendelő hagyja jóvá, mert 48 csoport
csak kis/nagybetűben tér el, és az írásmód üzleti döntés.
- producers:dedupe — alapból csak kimutatás, --apply hajt végre.
A lapot FEJLÉCNÉV alapján olvassa, nem oszlopbetű szerint, így a megrendelő
beszúrhat oszlopot vagy átrendezheti a lapot anélkül, hogy eltörne.
Hiányos csoportot és ismeretlen azonosítót visszautasít.
- Visszafordíthatóság: a jelentés SORONKÉNT tárolja a régi producer_id-t, mert
a fordított leképezés azokat a sorokat is átírná, amelyek eredetileg is a
megtartott rekordra mutattak. --rollback ebből állít vissza.
- Tartós nyom a jelentésfájltól függetlenül: a beolvasztott rekord archive +
canSee=0 lesz, és a note-jába kerül, hova olvadt be.
- A két nagy táblán nincs index a producer_id-n, ezért táblánként EGY UPDATE
fut CASE leképezéssel - 76 külön WHERE 76 teljes scant jelentene.
Mért eredmény a d2d másolaton: 27 mp alatt 314 termék + 27 272 archív + 115
árlista-sor átírva; a vizsgált árlistán a "Módosult" sorok 126 -> 23, a
gyártó-diffek 114 -> 2. Visszagörgetés után minden szám visszaállt, és az
újraszámolt diff ismét 126 - az ok-okozat mindkét irányban igazolt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A rendszerben 70 duplikált gyártónév halmozódott fel (146 rekord, 1166-ból),
mert a legacy import karakterre pontos egyezést követelt: a "Danone " nem
találta meg a meglévő "Danone"-t, és felvett egy újat - a nyers, trimmeletlen
Excel-értékkel. Gyártóhoz nincs admin felület, ez az egyetlen keletkezési út.
Következménye kettős: az árlista feldolgozó valódi változás nélkül is
módosulást jelez (a vizsgált fájlban 126 "Módosult" sorból 103 emiatt), a
Termék mennyiség statisztika pedig egy gyártóra szűrve a másik ID alá könyvelt
tételeket kihagyja a riportból.
- új App\Support\NameNormalizer: a normalizálás egyetlen definíciója. Három
független pont használja (legacy import, új feldolgozó, jövőbeli összevonó
parancs); ha ezek elcsúsznak, az újra duplikált törzsadatot szül.
- PriceListService::getProducerByName(): a karakterre pontos találat továbbra
is elsőbbséget élvez, de ha nincs, jön a normalizált egyeztetés. Duplikátum
esetén a legrégebbi rekordot adja vissza - a termékek arra mutatnak.
- PriceListService::addNewProducer(): trimmel mentés előtt
- getProducerLookupMap(): kihagyja az archive/deleted státuszú gyártókat.
Enélkül a beolvasztott rekord visszakerülne a feloldásba, és a takarítás
hatástalan lenne. Ma nem változtat semmin, csak felkészít.
Az új feldolgozó lookupjának tie-breakjéhez SZÁNDÉKOSAN nem nyúltam: egy
sorral eltüntetné a hamis "Módosult" jelzéseket, de az a funkcióban fedné el
az adathibát. A javulás a tényleges adattakarításból jöjjön.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bunzl árlistájához szükséges egy eddig hiányzó mértékegység. A "Legkisebb
eladási egység" / "Mennyiségi egység" mezőt két párhuzamos árlista-import
validálja külön-külön (a régi PriceListService.php ProductUnitEnum-mal, az új
PricelistFileProcessService.php saját PricelistSellerUnitEnum-mal), mindkettő
végül ugyanabba a products.sellerUnit / products.amountUnit DB enum oszlopba
ír. A puszta enum-bővítés önmagában nem lett volna elég: a DB oszlop
korlátozta volna a végrehajtást, csak ott, a legkésőbbi ponton bukva el.
- ProductUnitEnum: új 'm' case (régi pipeline validációja + a DB oszlopokat
generáló forrás).
- PricelistSellerUnitEnum: új 'm' case (új pipeline validációja).
- Migráció: products.sellerUnit/amountUnit és order_archives_items ugyanezen
oszlopainak DB enum bővítése 'm'-mel, ProductUnitEnum::getKeys()-ből
generálva, hogy a PHP enum és a DB oszlop ne csúszhasson el egymástól. A
down() az 'm' értékű sorokat NULL-ra állítja visszagörgetéskor, különben a
szűkített enum miatt az ALTER hibára futna. A "Mértékegység" (productUnit)
oszlopot szándékosan nem bővítettem, a jegy csak a másik két mezőt említi.
- schema:dump frissítve.
5 új teszt mindkét pipeline validációjára és a DB enum oszlopra.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A DeliveryCalendarService 4. prioritása a WorkCalendarService::isWorkDay()-t
hívta, ami a naptárban nem szereplő napoknál a hétvégére esett vissza. Így egy
hétvégét is tartalmazó sablon szombat/vasárnap mezőit sosem érte el a
kiértékelés (5. prioritás), és csak egyedi felülbírálással lehetett hétvégi
szállítást engedni.
- WorkCalendarService: új isHoliday(), ami csak a work_calendars ünnepnap
rekordjaira ad true-t, a naptári hétvégére nem. Az isWorkDay() változatlan.
- DeliveryCalendarService: a 4. prioritás mostantól isHoliday()-t használ, így
a hétvégéről a heti sablon dönt. Ünnepnap továbbra is tilt.
- NextDeliveryDateCalculatorService: a szállítási nap keresése kikerült az
isWorkDay() blokkból, különben a hétvége a következő szállítási nap
számításánál továbbra is kimaradt volna. Az átfutási időbe az új
countsTowardLeadTime() szerint a sablon hétvégi napjai is beleszámítanak,
a H-P sablonok viselkedése változatlan.
- DeliveryCalendarWidget: a szürke "inaktív" festés csak a nem szállítási
hétvégékre megy rá. Mellékesen a szállítási napok ciklusa $date->addDay()-jel
mutálta a kollekció Carbon objektumait, ez copy()-ra javítva.
7 új teszt, köztük a H-P sablon regresszió-őre.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A d2d-n a megrendelő fájljának megnyitásakor a sorok táblája eltört:
TextColumn::getDescriptionBelow(): Return value must be of type
Htmlable|string|null, array returned.
Nem UI-hiba volt, hanem adatvesztés. Ha egy cellán belül több formázású
szövegrész van (pl. a terméknév egy része félkövér), a PhpSpreadsheet
getValue()-ja RichText objektumot ad vissza. A json_encode ezt ÜRESRE
serializálja (a szövegrészek protected propertyben vannak), így a payloadba
{} kerül, ami visszaolvasva üres tömb - a cella tartalma nyomtalanul eltűnt,
a felület pedig tömböt kapott string helyett.
- normalizeCellValue(): a cella értéke a payloadba kerülés ELŐTT skalárrá
alakul (RichText -> getPlainText, dátum -> szöveg, tömb -> összefűzött
szöveg, __toString-es objektum -> string)
- LinesRelationManager::payloadText(): a MÁR ELMENTETT rekordokban maradt
tömbök nem törhetik el a nézetet - a beolvasási javítás azokat
visszamenőleg nem gyógyítja meg
- isBlankPayloadValue(): a kötelező mező ellenőrzése is_string() alapú volt,
ezért az üres tömböt KITÖLTÖTTNEK látta - egy elveszett terméknév némán
átment a validáláson. Ugyanez a védelem a Hooreyca-blokk vizsgálatában és a
végrehajtás mezőkonverziójában is (utóbbi különben szó szerint "Array"
néven hozta volna létre a terméket).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A paratest FÁJLONKÉNT osztja szét a munkát, ezért egyetlen 28 tesztes fájlból
nem tudott párhuzamosítani: egy processz vitte a teljes 137 mp-et, a másik öt
üresjáratban állt. A --parallel emiatt korábban egyenesen lassabb volt
(136 -> 143 mp), a teljes suite-on pedig csak 11%-ot hozott.
A felosztás témák szerint történt, de mért időkre kalibrálva, hogy egyik fájl
se domináljon (a leghosszabb a Builder, ~35 mp):
AccessTest 6 teszt ~7 mp jogosultság, feature flag, 403
GitTest 5 teszt ~18 mp GitRepository, ChangeSetAnalyzer
BuilderTest 9 teszt ~35 mp csomagépítés, _TEENDOK, _torles.sh, verzió
PageTest 4 teszt ~21 mp commitlista, keresés, kiválasztás
CreationTest 2 teszt ~32 mp felületről indított csomagolás, kirakottnak jelölés
DownloadTest 2 teszt ~25 mp naplóbejegyzés, ZIP letöltés, útvonal-ellenőrzés
A két legdrágább teszt (21,7 és 20,4 mp) szándékosan külön fájlba került,
együtt hagyva ők adnák a padlót.
A közös segédfüggvények a DeploymentTestHelpers.php-ba kerültek. Ez nem
*Test.php, ezért a phpunit nem szedi fel teszt-fájlként. A minta-repót takarító
afterAll() helyére register_shutdown_function lépett: az afterAll fájlonként
futna le, és a következő fájl alól húzná ki a mintát.
Mért eredmény (6 mag): ezek a tesztek sorosan 137,00 mp, párhuzamosan
67,88 mp. Teljes suite párhuzamosan 149,78 -> 77,52 mp.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A --parallel eddig két okból nem működött volna:
1. A setUp() a parent::setUp() ELŐTT hívja a refreshApplication()-t, hogy a DB
nevét a RefreshDatabase előtt ellenőrizhesse. A Laravel viszont a
ParallelTesting::callSetUpTestCaseCallbacks()-ot csak az "if (! $this->app)"
ágban futtatja (InteractsWithTestCaseLifecycle), ami így kimaradt - a
processzenkénti adatbázis sosem jött létre, minden processz a közös
d2dTest-et migrálta volna egyszerre. A callbacket most magunk hívjuk meg;
nem párhuzamos futásnál no-op.
2. A névellenőrzés pontos "d2dtest" egyezést várt, és eltérésnél nem hibát
dobott, hanem VISSZAÍRTA a configot d2dTest-re - vagyis csendben közös
adatbázisra terelte volna a processzeket.
A név mostantól a /^d2dtest(_test_\d+)?$/ mintát követi, ami a Laravel
TestDatabases::testDatabase() elnevezését fedi le. Szándékosan nem
str_starts_with($db, 'd2d'): az átengedné az éles adatbázist is. Az éles d2d
elleni azonnali exit(1) változatlan.
Ellenőrizve: tiszta lapról a párhuzamos futás létrehozta a d2dtest_test_1..6
adatbázisokat, a sima futás változatlanul működik.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A temp git repó eddig tesztenként épült újra 12 git processzel, ami Windowson
önmagában ~4 mp volt tesztenként. Mostantól futásonként EGYSZER épül fel, a
tesztek pedig másolatot kapnak róla (0,138 mp másolás + 0,177 mp takarítás).
Minden teszt saját, írható példányon dolgozik, így a "working tree elrontása"
és a builder-tesztek továbbra is írhatnak a repóba.
Mérés a teljes fájlon, azonos gépen, 28/28 zölddel: 384,05 mp -> 268,38 mp.
A régi deploymentCleanup a Windows read-only .git/objects fájljain elbukott és
a hibát csendben elnyelte, ezért árva mappákat hagyott a temp-ben (164 gyűlt
össze). Az új, facade nélküli deploymentDeleteDirectory nem hagy maga után
semmit, és az afterAll a booteolt alkalmazáson kívül is tud takarítani.
A minta útvonalában getmypid() van, hogy a tervezett --parallel futásnál is
processzenként külön mintát használjunk.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A 2026-08-17-i eset általánosítása: a d2d/e2e Docker Swarmban ugyanazt a kódot több
service futtatja közös volume-ról, és a queue:work hosszan futó folyamat a betöltött
osztályokat a memóriájában tartja. A fájlmásolás után a worker RÉGI kóddal dolgozta fel
a jobot - a felület jónak látszott, a háttérfeladat némán a régi viselkedést hozta.
- config: 'runtime' minta (app/, config/, database/, routes/, bootstrap/, composer.lock),
mert a restart nem csak a jobok változásakor kell; targetenként a service nevek és a
Swarm manager elérése
- a felületen figyelmeztetés, a teendőkben a konkrét ssh + docker service update parancs
- a restart szándékosan a cache ürítés UTÁN: a frissen induló worker különben a régi,
cache-elt konfigot töltené be
- a d2d schedulernél jelezzük, hogy 0 replikán fut (a restart üresjárat lehet), a t2t-nél
pedig azt, hogy queue service még nincs, de a névkonvenció alapján t2t_queue lesz
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- A célkörnyezet alapértelmezése a d2d (azt frissítjük sűrűbben). A config targets
tömbjének sorrendje adja az alapértelmezést, ezért csak a sorrend cserélődött.
- A "Környezetek állapota" panel betöltéskor magától lekérdezi a deploy-version.json-t,
percenkénti cache-sel (a hibás választ is cache-eljük, hogy egy nem válaszoló
környezet ne lassítsa minden kattintásnál a felületet). A Frissítés gomb kényszerít.
- A panel tömörebb, és valóban két hasábos: a md:grid-cols-2 nincs benne a lefordított
CSS-ben, ezért saját, media query-s stílussal oldjuk meg - így build nélkül is működik.
- A commitlistában a hash mellett jelöljük, melyik környezet áll azon a commiton:
zöld a szervertől lekérdezett, szürke a napló szerinti állapot.
Teszt: a beforeEach Http::fake() catch-all stubja minden URL-re illeszkedik és a factory
az első találatot adja vissza, ezért a konkrét választ váró teszt új factory-t kap.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Éles használat közben derült ki: a csomagba került egy seeder, de a _TEENDOK.md nem
szólt róla. A seeder felmásolása önmagában nem csinál semmit, tehát csendben kimaradt
egy lépés - pont az a hibatípus, ami ellen az egész eszköz készült.
- config: új attention csoport a database/seeders/* útvonalakra
- a felületen figyelmeztetés, a teendőkben a konkrét osztálynévvel:
php artisan db:seed --class=<Osztály>
- mellé egy feltételes cache-lépés: a Pennant purge Eloquent mentésnél NEM kell (a
FeatureFlagObserver::saved() elvégzi), csak query builderes írás után; a laratrust
cache pedig csak szerepkör/jogosultság hozzárendelés változásakor, mert a saját
cache-ét a felhasználóhoz kötve tárolja
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A Livewire::test() a HttpException-t válasszá alakítja és nem dobja tovább
(RequestBroker: withoutExceptionHandling([HttpException::class])), ezért a toThrow()
sosem teljesült volna. A jogosultság-ellenőrzés valódi belépési pontja az oldal URL-je,
így a teszt most GET-tel kéri le és assertForbidden()-nel ellenőriz.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A csomagolás mostantól nyomot hagy: melyik környezetre mi készült, mi lett kirakva, és
mennyi a lemaradás - ez az, ami a "melyik verzió fut az e2e-n" kérdést kiveszi a fejből.
- deployment_packages tábla + modell (BaseAuditable): target, tartomány, darabszámok,
mappa/ZIP útvonal, deployed_at. A csomagolás és a tényleges kirakás két külön esemény.
- Targetenkénti előtöltés: a kezdő commit az adott környezetre utoljára kirakottként
megjelölt csomag to_commit-je. Rebase után nem létező hash esetén inkább üres marad,
mint hogy hamis tartományt mutasson.
- "Környezetek állapota" panel: napló szerinti állapot + commit-lemaradás
(git rev-list --count), és gombra a szerver deploy-version.json-jának lekérdezése.
Ha a kettő eltér, az elmaradt vagy félbemaradt feltöltés jele. Szándékosan gombra fut,
nem minden rendereléskor: így nem indul kimenő kérés magától.
- ZIP a kész mappából (a fájllista a zip létrejötte előtt készül, így nem csomagolja
magát), letöltés a naplóból. A DB-ből jövő útvonalat kiírás előtt a kimeneti mappához
kötjük, hogy egy módosított rekord se tehessen letölthetővé tetszőleges fájlt.
- _torles.sh: alapból dry run, --confirm kell a törléshez, app-gyökér ellenőrzéssel,
abszolút útvonal és .. kiszűrésével, LF sorvéggel és BOM nélkül. Lefuttatva ellenőrizve.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A kijelölt tartományból elkészül a feltölthető mappa: deploy_<target>_<időbélyeg>, benne
a fájlok eredeti relatív útvonalon, hogy a feltöltés mappa-összeolvasztás legyen.
- GitRepository::archiveTo(): git archive --format=zip + ZipArchive kicsomagolás, ~100
fájlonként darabolva a parancssor-limit miatt. A tartalom a git objektumtárból jön, nem a
working tree-ből: így nem szivárog ki commitolatlan módosítás, és a .gitattributes eol=lf
miatt LF sorvéggel kerül a Linux célgépre. Üres pathspec esetén nem hívunk gitet, mert az
a TELJES fát csomagolná.
- DeploymentPackageBuilder: _MANIFEST.json/.txt (sha1 + méret fájlonként), _TORLENDO.txt a
törölt és az átnevezett fájlok régi útvonalával, _TEENDOK.md a diffből származó lépésekkel
(migrate, composer install, asset-figyelmeztetés, verzió-ellenőrzés), és public/
deploy-version.json a kirakott commit hashével.
- A git archive által kihagyott fájlok (pl. .gitattributes export-ignore) külön "missing"
listára kerülnek - csendben hiányzó fájl a manuális deploynál a legrosszabb hiba.
- Felületen fájlonkénti kézi kivétel, megerősítéses csomagolás gomb, eredmény-kártya.
- DeploymentGuard::ensureEnvironmentAllowed(): a builder felhasználó nélkül is ellenőrzi az
env kapcsolót és a stage whitelistet, így konzolról indítva sem fut le egy szerveren.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- a topbar menüpont a most bemásolt git ikont használja; a fájl ico_gitpng néven
érkezett, átnevezve ico_git.png-re, hogy illeszkedjen az ico_*.png konvencióhoz,
amiből a nav az útvonalat építi
- a git log a törzset (%b) is elkéri, és a keresés a tárgysor mellett ebben is keres:
a hash-re ritkán keres ember, az érdemi leírás viszont gyakran a törzsben van
- a --shortstat sora a törzs után érkezik, ezért a parse leválasztja róla, különben
a "N files changed" szöveg a törzsbe és a keresésbe is beszivárogna
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Két commit közötti fájlok összegyűjtését előkészítő felület első fázisa: célkörnyezet
választás (e2e/d2d), commitlista, diff-előnézet figyelmeztetésekkel. Ez a fázis még
semmit nem ír a fájlrendszerre, a csomagolás a phase2-ben érkezik.
- DeploymentGuard: .env kapcsoló + hardkódolt stage whitelist + developer szerep +
feature flag; a flag szándékosan nem biztonsági réteg, csak láthatóság-vezérlés
- GitRepository: csak olvasó wrapper, argumentum-tömbös Symfony Process (nincs shell),
hash- és referencia-validáció, core.quotePath=false az ékezetes útvonalakhoz
- ChangeSetAnalyzer: kizárási lista, törlendő/másolandó szétválasztás (átnevezésnél
mindkettő), figyelmeztetések migrációra, composer.lock-ra és a nem verziókövetett
fordított assetekre
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Egy beszállítóhoz egyszerre legfeljebb egy nyitott végrehajtás tartozhat: egy
execution_failed fájl után a termékek félig frissített állapotban vannak, egy új
import erre a kevert alapra rétegződne rá (és a validálás is ehhez számolná a
diffeket); két párhuzamos végrehajtás pedig nem determinisztikus eredményt adna.
- új PricelistGuard service: egyetlen igazságforrás a blokkoláshoz
- mind a NÉGY belépési pont véd: modern Filament create, régebbi
PriceListProcessor oldal, legacy Admin\PriceListController import, konzol
parancs. A legacy import a legfontosabb - az közvetlenül ír a products
táblába, megkerülve a modult, és a beszállítókat ma még nagyrészt ott kezelik.
- az `inprogress` önmagában nem blokkol: az előfeldolgozás és a validálás
egyetlen terméket sem ír, csak a már elindult végrehajtás számít
- a beszállító nem tűnik el a select listából, hanem konkrét magyarázatot kap a
felhasználó arról, melyik fájl blokkol és miért
- blokkolt beszállítónál másik fájl jóváhagyása sem indítható
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- PricelistExecutionJob + új PricelistRevertJob: $tries=1, $timeout, failed()
hook. A service catch ága csak PHP exceptiont fog el; worker timeout vagy
memórialimit esetén a fájl inprogress-ben ragadna, döntési gomb nélkül.
- heartbeat backstop a kill -9 esetére, ahol a failed() sem fut: minden chunk ír
a rekordba, így az updated_at a szívverés (isExecutionStuck). Ezzel garantált,
hogy minden kísérlet véges időn belül terminális állapotba jut - enélkül a
kényszerlezárás sem nyílna ki soha.
- kompenzáló visszaállítás R1-R4: árak törlése, termékek visszaállítása a
snapshotból, létrehozott termékek kivonása (soft delete, NEM fizikai törlés,
mert lehet rájuk hivatkozás), árlista + fájl lezárása
- konfliktuskezelés: a végrehajtás óta kézzel módosított terméket kihagyjuk és
jelentjük, nem írjuk felül vakon
- FIX phase5/3: a snapshot csak az írás ELŐTTI updated_at-et tárolta, amihez
képest a termék a végrehajtás után mindig eltér - a konfliktus-ellenőrzés így
minden sort kihagyott volna. Most a saját írásunk utáni updated_at is bekerül.
- kényszerlezárás: csak legalább egy terminális hibába futott visszaállítási
kísérlet után, flag + developer szerepkör mögött, kötelező indoklással
- döntéstámogató panel: mi történt már meg az importból, hogy a felhasználó ne
vakon válasszon a Folytatás és a Visszavonás között
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A sleep(1) szimuláció helyén valódi import: árlista fejrekord -> új gyártók ->
új termékek -> termékfrissítés -> árak -> lezárás.
- szándékosan nincs egyetlen átfogó tranzakció: fázisonként/chunkonként
tranzakciózunk, az atomicitást az adja, hogy az árlista E6-ig draft marad,
tehát félbeszakadáskor sem kerül ki félkész ár a felhasználók felé
- minden fázis idempotencia-jelölője az az artefaktum, amit maga hoz létre
(price_list_id / producer_id / product_id / applied_snapshot / pivot sor),
így a megszakadt futás folytatható, nem kezd elölről
- E4 a termék TÉNYLEGES, írás előtti állapotát menti az applied_snapshot-ba,
ugyanabban a tranzakcióban - ebből áll majd vissza a Visszavonás
- új EXECUTION_FIELD_MAP: a PRODUCT_FIELD_MAP csak a diff-számításhoz kellett,
az import a legacy importPriceList()-tel azonos, bővebb mezőkört ír
- hiányzó opcionális oszlop (pl. Akció) nem írja felül a termék meglévő értékét
- updateStepStatus: az Execution lépés 'failed' státusza execution_failed
fájlstátuszra képződik le, a többi lépésé marad fail
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- PricelistFile::canBeApproved() / hasExecutionStarted(): a jóváhagyás feltételei
egy helyen, hogy a felületi gomb és a service ugyanazt ellenőrizze
- approve(): a státusz MÉG A DISPATCH ELŐTT billen át, különben a queue-latency
alatt a gomb látszana és egy második kattintás párhuzamos importot indítana
- reject(): új 'rejected' lépésstátusz - nem 'failed', mert azt az
updateStepStatus fail fájlstátuszra fordítaná, az elutasítás viszont closed
- a döntés (ki, mikor, milyen indokkal) a file_meta.approval-ba kerül, mert a
BaseAuditable a nevével ellentétben nem naplóz
- Edit gomb szűkítése hasExecutionStarted()-tel három helyen, köztük az
EditPricelistFile::afterSave()-ben, ami a teljes láncot újraindítja
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>