FruitBankHybridApp/docs/WHOLESALE_FRAMEWORK_PLAN.md

20 KiB
Raw Blame History

Wholesale Framework — tervezési összefoglaló (handoff-mag)

Status: Proposed (Draft) — 2026-07-14 Jelleg: Ez NEM ADR és NEM végleges doksi. Provizórikus gondolkodási összefoglaló, ideiglenesen a FruitBankHybridApp docs/ mappájában. A tárgyalt Wholesale framework-repo még nem létezik, a neve változhat, és semmilyen döntés nem végleges. Amint a repo létrejön, ez a fájl a forrása a rendes struktúrának (ADR-ek + repo-doksik) — lásd a záró "Mi lesz ebből" szakaszt. Előzmény: két korábbi session terv-összegzése (app-oldal + nop-plugin-oldal) + ez a session konszolidációja.


1. Cél és kiváltó ok

A FruitBank (zöldség-gyümölcs nagyker, nopCommerce-plugin + Blazor Hybrid app) mellé indul a VeggieVoyage (veggievoyage.hu), egy közel azonos nagyker-rendszer apró eltérésekkel. Emellett több további zöldséges nagyker érdeklődő is van a rendszerre. Cél: egy közös nagyker-framework réteg, amelyből minden nagyker-termék származik, hogy egy új terméknél a lehető legkevesebbet kelljen újraírni.

Jelenleg (2026-07-14) csak a FruitBank-oldal létezik; sem a Wholesale-repo/könyvtár, sem a VeggieVoyage-projekt nincs még létrehozva.


2. Architektúra-döntés: base-class minta (maximalista pull-up)

Öröklés-alapú, NEM típus-átköltöztetés. A közös rétegbe szinte minden osztály felkerül — abstract vagy virtual formában (base class-ok, virtual method-ok/property-k). A konkrét (gyakran szinte üres) záró osztályok a termék-repóban élnek és származnak a base-ből.

  • Amit lehet, a közös rétegben implementálunk (virtual default üzleti logika); ami termék-specifikus, az a származtatottban override-ol.
  • Az eltérés a fejlesztés közben derül ki, nem előre. Default: minden felmegy közös virtual defaultként. Amikor a VeggieVoyage (vagy egy későbbi termék) fejlesztése bizonyítja, hogy valami mégsem közös, akkor az a member lemegy ("demote") a záró rétegbe. Amíg csak egy termék használ egy membert, a lehúzása senkit nem tör.
  • Ez a meglévő házi minta kiterjesztése: MgStockTakingItem<TStockTaking, TProduct> : MgEntityBase, PartnerBase → CargoPartner, MeasuringItemPalletBase → 3 pallet, AcConst abstract-constants minta.

Miért ez nyert (a mozgatásos alternatívával szemben)

Aggály Feloldás
Tábla-nevek (fbShipping…) [Table(Name=…)] a származtatott osztályon, termék-konstansból — nincs migráció, nincs runtime-konfig
AcBinary wire-compat (SignalDataType = AssemblyQualifiedName) Konkrét típusok helyben maradnak a termék-assembly-ben → AQN változatlan, nincs lockstep-kényszer
AcBinaryForcedInit (AssemblyLoadContext-quirk) Generált szerializálók a konkrét típusokra → meglévő ForceRegister érintetlen
LinqToDB NameCompatibility Termék-pluginban marad, konkrét típusokra

Következmény: a FruitBank-oldal minden pull-up lépésnél fordítás-azonos marad; a VeggieVoyage viszont "majdnem készen" indul, mert a scaffold záró osztályai szinte üresek.

Új DB + új plugin a VeggieVoyage-nek

  • A VeggieVoyage teljesen új adatbázist kap → a [Table(Name=…)] (identitás) így is a záró rétegen marad; a base hordozza az összes [Column]-t, a tábla-NÉV termék-konstans. A vv/fb prefix szabad termék-döntés, nem framework-ügy.
  • A VeggieVoyage új nopCommerce-plugin is → nem csak az üzleti logikát, hanem a plugin-infrastruktúrát is base-formában ki kell szervezni: install/uninstall, locale-resource telepítés, admin-menü regisztráció, settings-defaultok, schedule task-ok, MgwPluginStartupBase virtual DI-regisztrációkkal. A Veggie-pluginban ideálisan csak plugin.json (SystemName), a záró osztályok és a konstansok maradnak.
  • Zöldmezős schema-generálás: a friss install-nak a teljes sémát egy menetben kell felhúznia üres DB-n (a FruitBank táblái évek alatt, lépésenként jöttek létre). Ellenőrizendő, hogy a meglévő install/tábla-létrehozó út (MgDbContext-vonal) teljes és determinisztikus-e nulláról — GenericAttribute-kulcsok és seed-adatok is.

3. Rétegzés / célstruktúra

Rétegtérkép a legáltalánosabbtól (legmélyebb) a legspecifikusabbig:

L0   AyCode.Core (Ac*) + AyCode.Blazor         — tech-framework, domain-mentes, bármely projekt
        SignalR, AcBinary + SourceGenerator, logolás, NAV/EKÁER-protokoll, MgGrid/komponens-base-ek
L2    Mango.Nop Libraries (Mg*)                 — nop-generikus, nop-VERZIÓFÜGGŐ (4.70 mappa alatt)
L2.5  Mango.Nop.Wholesale.Core / .Services      — nagyker-domain, nop-VERZIÓFÜGGŐ, prefix: Mgw
        (Libraries alatt, 4.70) entitás-bázisok, DbTable/DbContext-bázisok, endpoint-bázisok, EKÁER-riport
L2.5  Wholesale.Common / .Common.Server         — nagyker-domain, nop-verzió-FÜGGETLEN (ÚJ repo)
        domain-modell, interfészek, enumok, DTO-bázisok, SignalR-tag core-tartomány, mérés-helperek,
        service-bázisok (a Mango.Nop DLL-jei UTÁN build-el — pont mint ma a Common-ok)
L2.5  Wholesale.Shared (Blazor, ÚJ repo)        — grid-/komponens-base-ek, SignalR-kliens base
L3    Termékek: FruitBank / VeggieVoyage / …    — vékony záró osztályok + identitás + termék-delta

Kétotthonos base-réteg — a besorolás kritériuma

A "hova kerüljön" kritérium NEM az, hogy használ-e Mango.Nop-ot (a Common-ok ma is jogosan használják), hanem hogy nop-verziófüggő-e:

  • nop-verziófüggő (Mg*/linq2db/nopCommerce-entitásokra épül, a nop belső API-ját tükrözi) → Mango.Nop.Wholesale (Libraries, verziómappa alatt).
  • nop-verzió-független nagyker-domain → Wholesale.Common / .Common.Server (új repo).

Fontos: a mai FruitBank.Common.Server "nopCommerce integration"-t is tartalmaz. Pull-upnál a nop-ot érintő részek a Mango.Nop.Wholesale.Services-be mennek, nem a Wholesale.Common.Server-be. A kritérium szigorúan "nop-verziófüggő?", nem projektnév.

Build-lánc (interleaved — ma is így épül, csak beékelődik két réteg)

AyCode → Mango.Nop Libraries → Mango.Nop.Wholesale.Core (entitás-bázisok)
       → Wholesale.Common / .Common.Server (domain- és service-bázisok, Mgw.Core-ra épülve)
       → Mango.Nop.Wholesale.Services (DbTable/DbContext/endpoint-bázisok, Wholesale.Common-ra épülve)
       → termékek (HybridApp + plugin)

Ezért a Mango.Nop.Wholesale Core/Services szétvágása kötelező, nem ízlés: a Core a Wholesale.Common ALATT, a Services FELETTE — különben körkörös függőség.

Per-app bin-profil

A referencia-minta már ma per-app build-profilos: az AyCode és Mango.Nop.Core DLL-jei bin\FruitBank\$(Configuration)\net9.0-ból jönnek (HintPath). Lesz bin\VeggieVoyage\… is. TFM-ek: Common=net9.0 (WASM-kliens is fogyasztja, szerver-only függőség nélkül), Shared=net10.0. A FruitBankHybrid.Shared.Common projekt ma üres váz — felhasználható.


4. Placement-kánon (a legfontosabb szabály)

A kód a legmélyebb rétegbe kerül, amelynek a domain-hatóköre még jogosan magába foglalja a fogalmat.

Két teszt együtt dönt:

  1. Kimenő-függőség teszt (szükséges, objektív): egy típus akkor mehet mélyebbre, ha a függőségei engedik. AyCode-ba csak az kerülhet, ami nulla nagyker/nop fogalomra hivatkozik (a using-ok és hivatkozott típusok döntik el, nem érzés). Ez adja meg, milyen mélyre mehetne a kód.
  2. Hatókör-kohézió teszt (elégséges, logikai): hova való a fogalom. Ez megállíthatja a kódot, mielőtt a függőségei alapján túl mélyre süllyedne.

Kanonikus példa — tálca/tare-aritmetika: tiszta decimal-matematika, nulla domain-függőséggel lefordulna AyCode.Core-ban. Mégis a Wholesale-be való, mert a fogalom hatóköre "nagyker-mérés". Core-ba nem azért nem kerül, mert nem fordulna, hanem mert a Core hatóköre (minden projekt) tágabb, mint a fogalom valódi közönsége.

Két hibamód

  • Túl mélyre (nagyker-fogalom Core-ban): a univerzális réteg beszennyeződik, a nagyker-fejlesztő nem is ott keresi. Ez a csábító hiba ("ez matek, mehet Core-ba"). Fordul, csak koszol → csendes hiba, ezt kell fogni.
  • Túl sekélyen (a termékben): redundancia — a következő zöldséges projekt újraírja. Ezt a framework szünteti meg.

Iránytű: a redundancia-kerülés lefelé (általánosabb felé) tol; a hatókör-kohézió a fék. A hiba-irány aszimmetrikus: túl mélyre tett domain-kód nem fordul (a rendszer önvédő); túl magasra tett specifikus kód fordul, csak koszol. Ezért a default "a legmélyebb, ami fordul", és a feljebb-tétel az, ami indoklást igényel.

Redundancia-ellensúly (hogy a DRY ne forduljon visszájára)

"Ne írd le kétszer ugyanazt a fogalmat" — NEM "vonj össze mindent, ami hasonlít". Ha két dolog csak véletlenül hasonlít (nem ugyanaz a fogalom) és közös base-be húzod, hamis csatolást hozol létre — a legrosszabb fajta redundancia-"javítás". A rule-of-two/N a védőkorlát: legalább két valódi fogyasztó bizonyítsa, hogy a közösség valódi és nem esetleges.

Identitás soha nem megy fel (checklist a záró réteghez)

  • [Table(Name=…)] tábla-nevek (termék-konstansból)
  • [AcBinarySerializable] konkrét típusok
  • SignalRTag termék-tartománybeli értékei
  • plugin SystemName, route-URL-ek, locale-fájlok, branding
  • appsettings értékek (nem a séma)

Hol él a kánon (doc-layering)

A placement-kánon kánonja a legmélyebb rétegben (AyCode.Core) él; minden alsóbb réteg copilot-instructions.md-je erre mutat + a saját befogadási kritériumát mondja ki (AyCode = "domain-mentes"; Wholesale = "nagyker-domain, nop-verzió-független"; Mango.Nop = "nop-verziófüggő"). A doksi vigyen 3-4 levezetett határeset-példát (a tare-képlet a kanonikus) — az absztrakt szabály nem elég horgony, a gondolkodásmódot a példák tanítják.


5. Zöldség-kalibráció + jövőbeli továbbbontás (elhalasztott)

Az érdeklődők egyelőre mind zöldséges nagykerek, közel azonos igénnyel. Ezért a Wholesale-réteg mai valódi hatóköre "zöldséges nagyker", nem "általános nagyker" — a "zöldséges-specifikus vs. általános-nagyker" hasítás jövőbeli, szándékosan még meg nem tett rétegvágás.

Következmények:

  • A zöldséges-specifikusság ma NEM tolja fel a kódot a termékbe. Egy kimondottan zöldséges logika (érettség, osztályozás, zöldség-mérés) jogosan a framework-rétegé — bármely másik zöldséges nagyker újra használná. Óvakodni attól, hogy "ez túl zöldség-specifikus, hagyjuk a termékben" — ez kiéheztetné a framework-öt.
  • NE annotáld előre a "zöldséges vs. általános" varratot — nincs második nem-zöldséges domain, ami bizonyítaná, tehát bármilyen jelölés spekuláció (ugyanaz a korai-triázs-csapda). A jövőbeli nem-zöldséges nagyker fogja felfedni a varratot, ugyanúgy, ahogy a Veggie-scaffold felfedi a termék-hardcode-ot.
  • Ha egyszer jön nem-zöldséges nagyker: ugyanaz a mechanikus pull-up egy szinttel lejjebb (pl. Trade/Distribution réteg), a Wholesale maga válik vékony leszármazottá. Mivel minden abstract/virtual, ez is fordítás-azonos lesz.
  • A rétegnév lehet jövő-álló ("Wholesale", prefix Mgw), de a doksinak ki kell mondania a mai zöldség-kalibrációt, hogy egy jövőbeli session ne nézze design-hibának és ne kezdjen kéretlenül általánosítani.

6. nopCommerce-verzió probléma + karantén-stratégia

Kiváltó félelem: a nopCommerce design sok hibája miatt gyakran nop-kódot kell másolni/átírni → nagyon nehéz a FruitBank upgrade-je. A VeggieVoyage viszont már új nop-verzióval készülne. Kockázat: ha a catalog-táblák változtak, az gond lehet.

A rétegzés maga a válasz — csak explicit szabályként kell kimondani:

  1. A verziófüggés karanténban van. A Mango.Nop Libraries + Mango.Nop.Wholesale definíció szerint nop-verziófüggő (ezért él verziómappa alatt). Új nop-verziónál ez a két réteg forkol (4.90/Libraries, 4.90/…Wholesale), és a per-app bin-profil ezt kiszolgálja: a FruitBank a 4.80-as fork ellen build-el, a VeggieVoyage a 4.90-es ellen — ugyanabból a Wholesale-forrásból. Feltétel: a Mg*/Mgw típusok publikus felülete forrás-kompatibilis maradjon a forkok között. A Mango.Nop réteg így anti-korrupciós réteg: a nop churn a fork belsejében marad, a Wholesale nem látja.

  2. Másolt/átírt nop-kód (a tényleges upgrade-fájdalom): jól megjelölt helyre (pl. NopOverrides/ a verziófüggő rétegben), mindegyik mellé kötelező jegyzet: miért kellett, melyik nop-fájlból, mit kell újranézni port-nál. Így a 4.90-port checklist lesz, nem régészet.

  3. Catalog-tábla drift: (a) minden catalog-hozzáférés a verziófüggő rétegben, a Wholesale csak interfészt/DTO-t lát; (b) a GenericAttribute-minta (IsMeasurable, Tare, AverageWeight, IncomingQuantity, NetWeight mind GA-ban, nem catalog-oszlopban) verzió-rezisztens — a GA a nop legstabilabb mechanizmusa. A kitettség arra szűkül, amit tényleg a Product/Order/Customer natív oszlopaiból olvastok.

  4. A két nagy műveletet ne egyszerre. Előbb pull-up a mostani nop-verzión (FruitBank fordítás-azonos, a Veggie-scaffold ugyanazon a verzión bizonyítja a framework-öt), utána a verziófüggő réteg portja az új verzióra. Egyszerre futtatva minden hiba kétesélyes lenne ("framework-bug vagy port-bug?").

  5. Reframe: a VeggieVoyage kifizeti a FruitBank upgrade-adósságát. Ha a Veggie kikényszeríti a Mango.Nop(+Wholesale) új-verziós portját, a FruitBank későbbi upgrade-je már csak "bin-profil átállítás a kész forkra + termékrétegbeli maradék".

Konkrét vizsgálat, amivel a catalog-félelem végessé tehető (még nem futott le):

  • (a) Kitettség-térkép: végigmenni a plugin + Libraries kódon, mely nop-entitásokat/service-eket/natív oszlopokat használtok közvetlenül.
  • (b) Séma-diff a cél nop-verzióval, a ténylegesen használt táblákra (a nop upgrade-migrációiból). → Cél nop-verzió megadása után elvégezhető.

7. Eltérés-taxonómia: settings vs. override (rule-of-N)

Több terméknél a "rule-of-two" rule-of-N lesz. Ökölszabály:

  • parametrikus eltérés → settings/config (EkaerSettings-minta, DI mögé)
  • strukturális eltérés → virtual override
  • identitás → abstract const a záró rétegen

Ha ugyanazt a metódust minden termék máshogy override-olja, az annak a jele, hogy paraméternek kellett volna lennie. Minél több megy settings-be, annál kevesebb kódot kell egy új terméknél megírni. Nem kell előre kitalált settings-taxonómia — demote-to-settings on evidence, ugyanaz a workflow, mint a kódnál: minden fent marad közös defaultként, és az első valódi termék-eltérésnél dől el, override vagy setting lesz.

Demote-ablak szűkül N-nel: amíg egy termék használ egy membert, a lehúzása fájdalommentes; a 2. fogyasztónál már közös és fent marad. Ezért a Veggie-fázisban agresszíven demote-olni.

Termék-scaffold = template. A Veggie "új termék felhúzása" maga is termék: minden scaffold-lépést checklistként dokumentálni a Wholesale-repo doksijába — a 3. terméknél már ez A termék-létrehozási folyamat. (Nem kell most dotnet-template; a jól dokumentált Veggie-scaffold az első draft.)

Framework-evolúció N fogyasztóval: minden wholesale-változás N terméket érint. Per-app bin-profil jó izoláció (mindenki a saját build-jét pinneli), de wire-formátumot (AcBinary) és base-séma [Column]-okat érintő változásnál CONSUMERS.md szerinti végigfuttatás kell (P1).

SignalRTag-tartomány kiosztás (hogy két termék soha ne ütközzön közös infrastruktúrán): közös core-tartomány + termék-tartományok, pl. FruitBank 09999, VeggieVoyage 1000019999, 3. termék 20000+…


8. Pilotban igazolandó mechanizmusok (a maximalista minta ezekre tesz fel mindent)

Az első pull-up ELŐTT explicit tesztelendő:

  1. [Association] keresztreferenciák öröklésnél — interfész-kulcsú társítás (OtherKey = nameof(IShipping.Id)) VAGY a navigációk a leszármazottban maradnak (base = skalárok + FK int-ek), entitásonként döntve.
  2. AcBinary öröklött property-kkel — a MeasuringItemPalletBase minta szerint; az első költözőnél explicit teszt.
  3. [SignalR] attribútum öröklött kontroller-metóduson — az AyCode dispatcher végpont-felderítése lássa a base-metódusokat.
  4. Razor @typeparam a DevExpress template-ekkel — hol megy, hol marad vékony app-razor.

9. Migrációs fázisterv ("pull-up", lépésenként, FruitBank végig fordítás-azonos)

  1. Skeleton repo + per-app bin-profil bekötés + mechanikus bázis-kiemelés (interfészek, enumok, konstans-bázisok) — compile+wire-identical.
  2. Pilot: a legkisebb/legtisztább gráf végigvitele (jelölt: Cargo — legkisebb; vagy EKÁER-lánc — legfrissebb). Itt bizonyítjuk a 8. szakasz mechanizmusait.
  3. Data-réteg bázisok (DbContext/DbTable).
  4. Service-ek domain-enként (mérés → preorder → cargo → invoicing), FruitBank-osztályok vékony leszármazottá.
  5. VeggieVoyage-plugin scaffold záró-osztályokkal — itt bukik ki a maradék hardcode.
  6. Blazor Shared (grid-/komponens-base-ek).
  7. Verziófüggő réteg portja az új nop-verzióra (a pull-up UTÁN, külön lépés).

10. Meta-protokoll teendők (amikor a repo létrejön)

  • Új repo: .github/copilot-instructions.md + @repo blokk, repo-prefix (self-service a REPO_PREFIXES.md szerint; korábbi javaslatok: nop-oldali libre MGNOPWHOLE, a nop-független Wholesale repóra pl. MGWHOLE — eldöntendő), layer-szám.
  • Class-prefix: Mgw (Mango Wholesale) — collision-check lefutott, tiszta (a Ws, Wh/Whs, Nk elvetve). A doksi tisztázza: a prefix jövő-álló, de a mai tartalom zöldség-kalibrált.
  • Doc-layering: a defining layerrel a doksik is költöznek — a FruitBank-plugin PREORDER/ doksija + a PREO/EKAER/ORDD topic-kódok a wholesale-registry-be; a termék-pluginban pointer + delta marad.
  • CONSUMERS.md a Wholesale-repóban (FruitBank, VeggieVoyage, …).
  • LLMP-DEC bejegyzés az új repo létéről (workspace-szignifikáns).
  • ADR-ek a végleges struktúrában (lásd lent).

11. Nyitott kérdések

  • Wholesale-repo neve és pontos szeparációja (egy repo Common + Common.Server + Shared, vagy több?).
  • Cél nop-verzió a VeggieVoyage-hez (ez indítja a 6/(b) séma-diffet).
  • Repo-prefix(ek) véglegesítése.
  • Generic-paraméter-robbanás mély gráfoknál (MgwShippingDocumentBase<TShipping, TPartner, TShippingItem, …>) — a házi minta elfogadta, de ez az ára; entitásonként figyelni.
  • Mango.Nop vs Wholesale határ entitásonként: ami bármely nop-projektnek jó → Mango.Nop; ami nagyker-domain → Wholesale.

12. Mi lesz ebből (amint van repo)

Ez a fájl a forrás; a rendes struktúrába szétszedve:

  • ADR-ek (<repo>/docs/adr/NNNN-*.md, Nygard-stílus):
    • base-class-pull-up-vs-type-move (2. szakasz)
    • wholesale-layer-split-and-nop-version-quarantine (3. + 6. szakasz)
    • placement-canon (4. szakasz) — vagy CONVENTIONS.md-be, a repo szokása szerint
  • Repo-doksik: ARCHITECTURE (rétegtérkép + build-lánc), CONVENTIONS (placement-kánon + tare-példa + zöldség-kalibráció), CONSUMERS.md, termék-scaffold checklist.
  • LLMP-DEC sor: az új repo léte + a protokoll-relevanciák.
  • A FruitBank-oldali doc-layering pointer-ek frissítése.

Ez a WHOLESALE_FRAMEWORK_PLAN.md a szétszedés után archiválható vagy törölhető (a tartalma a végleges helyekre kerül).