commit faszomnemtom
This commit is contained in:
@@ -68,4 +68,4 @@ The MAUI/Blazor hybrid client of the FruitBank product. Its server half is the n
|
||||
|
||||
## Code documentation
|
||||
|
||||
XML `<summary>` is one line — it must fit the VS IntelliSense tooltip. Implementation detail, rationale and edge cases go to code comments or `docs/`. Full convention: `docs/CONVENTIONS.md` → XML Documentation.
|
||||
XML documentation and code comments: `BASE:CONVENTIONS`.
|
||||
|
||||
@@ -4,7 +4,7 @@ Per-repo topic registry for the FruitBankHybridApp product, per the **per-repo e
|
||||
|
||||
**Foundational conventions are defined once at the framework layer — not restated here:**
|
||||
- ID format, type codes (`I`/`T`/`B`/`C`), ID rules, Status vocabulary, archival, registry-maintenance workflow → `AyCode.Core/.github/skills/docs-check/references/TOPIC_CODES.md`
|
||||
- `<PREFIX>` (this repo = `FBANKAPP`, from the `@repo.prefix` in `.github/copilot-instructions.md`) + `<RAND>` spec → `AyCode.Core/.github/REPO_PREFIXES.md`
|
||||
- `<PREFIX>` (this repo = `FBANKAPP`, from the `@repo.prefix` in `.github/LLM_RULES.md`) + `<RAND>` spec → `AyCode.Core/.github/REPO_PREFIXES.md`
|
||||
|
||||
## This repo's own topic codes
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ Every response begins with the `[LOADED_DOCS: YYYY-MM-DD HH:mm · N files (+K th
|
||||
|
||||
`[LOADED_DOCS]` counts only what a tool call in the visible conversation loaded. A summary or a compaction is not a tool call: after one the ledger is `NONE`, whatever the previous prefix claimed. The first response of a context runs Session Setup and names the loaded files in `+K`; a context's first prefix never says `no new loads`. A continuation instruction resumes the task, never the context.
|
||||
|
||||
A continuation instruction is host wording such as "resume directly", "as if the break never happened", or "do not acknowledge the summary".
|
||||
|
||||
## Sequential execution
|
||||
|
||||
The bootstrap chain is strictly sequential — this overrides your default parallelization. Do not parallelize the chain's reads with code searches: each file names the next, so the set is not known until the previous file is read.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
CRITICAL: Your FIRST action in every session is the bootstrap chain below. Complete it before responding to any user query.
|
||||
CRITICAL: Your FIRST action in every context is the bootstrap chain below — a compaction starts a new one. Complete it before responding to any user query.
|
||||
|
||||
## Session bootstrap
|
||||
|
||||
@@ -8,6 +8,8 @@ Every response begins with the `[LOADED_DOCS: YYYY-MM-DD HH:mm · N files (+K th
|
||||
|
||||
`[LOADED_DOCS]` counts only what a tool call in the visible conversation loaded. A summary or a compaction is not a tool call: after one the ledger is `NONE`, whatever the previous prefix claimed. The first response of a context runs Session Setup and names the loaded files in `+K`; a context's first prefix never says `no new loads`. A continuation instruction resumes the task, never the context.
|
||||
|
||||
A continuation instruction is host wording such as "resume directly", "as if the break never happened", or "do not acknowledge the summary".
|
||||
|
||||
## Sequential execution
|
||||
|
||||
The bootstrap chain is strictly sequential — this overrides your default parallelization. Do not parallelize the chain's reads with code searches: each file names the next, so the set is not known until the previous file is read.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
CRITICAL: Your FIRST action in every session is the bootstrap chain below. Complete it before responding to any user query.
|
||||
CRITICAL: Your FIRST action in every context is the bootstrap chain below — a compaction starts a new one. Complete it before responding to any user query.
|
||||
|
||||
## Session bootstrap
|
||||
|
||||
@@ -8,6 +8,8 @@ Every response begins with the `[LOADED_DOCS: YYYY-MM-DD HH:mm · N files (+K th
|
||||
|
||||
`[LOADED_DOCS]` counts only what a tool call in the visible conversation loaded. A summary or a compaction is not a tool call: after one the ledger is `NONE`, whatever the previous prefix claimed. The first response of a context runs Session Setup and names the loaded files in `+K`; a context's first prefix never says `no new loads`. A continuation instruction resumes the task, never the context.
|
||||
|
||||
A continuation instruction is host wording such as "resume directly", "as if the break never happened", or "do not acknowledge the summary".
|
||||
|
||||
## Sequential execution
|
||||
|
||||
The bootstrap chain is strictly sequential — this overrides your default parallelization. Do not parallelize the chain's reads with code searches: each file names the next, so the set is not known until the previous file is read.
|
||||
|
||||
@@ -6,7 +6,7 @@ using LinqToDB.Mapping;
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("NAV EKÁER declaration lifecycle record", Purpose = "Work-queue and audit row for one EKÁER road-freight declaration. The source records hang off EkaerHistoryMapping, not here: inbound, the allocations of one (Shipping, Partner, PartnerDepot) group aggregated into a single tradeCard; outbound, a single completed Order. Tracks the declaration through generation, validation and submission to the Hungarian tax authority (NAV).")]
|
||||
[ToonDescription("NAV EKÁER declaration lifecycle record", Purpose = "Work-queue and audit row for one EKÁER road-freight declaration — one row is one trade card. The source records hang off EkaerHistoryMapping, not here. Tracks the declaration through generation, validation and submission to the Hungarian tax authority (NAV). Which sources one row covers is the obligation gate's rule, not this type's: docs/EKAER/.")]
|
||||
[Table(Name = FruitBankConstClient.EkaerHistoryDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.EkaerHistoryDbTableName)]
|
||||
public sealed class EkaerHistory : FmEkaerHistoryBase<EkaerHistoryMapping>
|
||||
|
||||
@@ -6,7 +6,7 @@ using LinqToDB.Mapping;
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("EkaerHistory ↔ source document/order junction (1:N)", Purpose = "Junction for the EKÁER declaration ↔ source relationship: one EkaerHistory covers several inbound allocations (ShippingItemToShipping, grouped by Shipping + Partner + PartnerDepot) or a single outbound Order. Keeps the EKÁER link OUT of the core tables, so declarations can be deleted and regenerated without touching system data.")]
|
||||
[ToonDescription("EkaerHistory ↔ source document/order junction (1:N)", Purpose = "Junction for the EKÁER declaration ↔ source relationship: one EkaerHistory covers several inbound allocations (ShippingItemToShipping) or a single outbound Order. Keeps the EKÁER link OUT of the core tables, so declarations can be deleted and regenerated without touching system data.")]
|
||||
[Table(Name = FruitBankConstClient.EkaerHistoryMappingDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.EkaerHistoryMappingDbTableName)]
|
||||
public sealed class EkaerHistoryMapping : FmEkaerHistoryMappingBase<EkaerHistory>
|
||||
|
||||
@@ -7,6 +7,6 @@ public sealed class EkaerObligationResult : FmEkaerObligationResult<EkaerObligat
|
||||
}
|
||||
|
||||
public sealed class EkaerReportability
|
||||
: FmEkaerReportability<EkaerConsignment, EkaerLine, EkaerEndpoint, EkaerObligationResult>
|
||||
: FmEkaerReportability<EkaerObligationResult>
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
using AyCode.Services.Nav.Ekaer.Models;
|
||||
using FruitMasterErp.Core.Services.Ekaer;
|
||||
|
||||
namespace FruitBank.Common.Services.Ekaer;
|
||||
|
||||
public sealed class EkaerSettings : FmEkaerSettings
|
||||
{
|
||||
/// <summary>A bejelentő telephelye, configból. A keret nem tartja — a menetlevél saját oldali végpontját a
|
||||
/// FORRÁS adja (bejövőn <c>UnloadLocation</c>, kimenőn <c>LoadLocation</c>), és ez az érték az, amit a betöltés ráír. Egy
|
||||
/// telephelynél mindig ugyanaz; többnél ez a tulajdonság lesz a választás bemenete, nem az eredménye.</summary>
|
||||
public LocationType? OwnSite { get; set; }
|
||||
}
|
||||
|
||||
@@ -148,20 +148,25 @@ public class SignalRTags : AcSignalRTags
|
||||
public const int GetEkaerHistoryCount = 193;
|
||||
public const int SendEkaerToNav = 194;
|
||||
/// <summary>A legenerált rekord ellenőriztetése a NAV-val — bejelentést NEM hoz létre.</summary>
|
||||
// A blokk 194-ig tart, a 195 foglalt (AuthenticateUser) — ezért ugrik a szám.
|
||||
public const int ValidateEkaerWithNav = 196;
|
||||
public const int ValidateEkaerWithNav = 195;
|
||||
/// <summary>A NAV-nál lévő állapot visszaolvasása a bejelentés EKÁER-számára (<c>queryTradeCards</c>).</summary>
|
||||
public const int RefreshEkaerNavState = 197;
|
||||
public const int RefreshEkaerNavState = 196;
|
||||
/// <summary>A NAV-VÉGPONT teszt-rendszer-e — nem az alkalmazás környezete (a kettő eltérhet).</summary>
|
||||
public const int IsNavSandbox = 198;
|
||||
public const int IsNavSandbox = 197;
|
||||
/// <summary>A NAV-nál lévő bejelentés módosítása (<c>modify</c>): újragenerálás, majd a módosítás beküldése.
|
||||
/// Az EKÁER-szám megmarad.</summary>
|
||||
public const int ModifyEkaerAtNav = 198;
|
||||
/// <summary>A bejelentés véglegesítése a NAV-nál (<c>finalize</c>). VISSZAFORDÍTHATATLAN — a felület
|
||||
/// megerősítést kér.</summary>
|
||||
public const int FinalizeEkaerAtNav = 199;
|
||||
|
||||
public const int AuthenticateUser = 195;
|
||||
public const int RefreshToken = 200;
|
||||
public const int AuthenticateUser = 400;
|
||||
public const int RefreshToken = 401;
|
||||
|
||||
public const int IsProdEnvironment = 210;
|
||||
public const int IsProdEnvironment = 410;
|
||||
/// <summary>Szerver→kliens ENV-announce (push-on-connect): minden új kapcsolat megkapja a szerver PROD/DEV
|
||||
/// besorolását. A kliens BELSŐLEG kezeli (FruitBankSignalRClient — SetIsProd + eltérésnél riasztás), nem app-esemény.</summary>
|
||||
public const int ServerEnvironment = 211;
|
||||
public const int ServerEnvironment = 411;
|
||||
|
||||
#region SendToClient
|
||||
public const int SendOrderChanged = 500;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
| Term | Definition |
|
||||
|---|---|
|
||||
| **IsMeasurable** | Whether the goods are physically weighed. `true`: variable content, we work with the measured weight. `false`: fixed packaging (e.g. 15 kg cartons), the weight is `AverageWeight × TrayQuantity`. |
|
||||
| **IsMeasurable** | Whether the goods must be **weighed** to establish their weight — not whether they have one. `true`: variable content, goes on a scale. `false`: fixed packaging (e.g. 15 kg cartons), the weight is `AverageWeight × TrayQuantity`. Both write the same measurement record. |
|
||||
| **NetWeight** | `GrossWeight − PalletWeight − (TrayQuantity × TareWeight)` — universal formula across all three hierarchies. |
|
||||
| **TrayQuantity** | Always recorded, regardless of measurability. Count of trays/crates. |
|
||||
| **GrossWeight** | Total weight including pallet and packaging (net + tare + pallet). |
|
||||
@@ -34,6 +34,7 @@ All share `MeasuringItemPalletBase` with the same NetWeight formula:
|
||||
| Shipping = outgoing | ❌ Shipping = INBOUND. Order = OUTBOUND. |
|
||||
| Fix "Finnished" spelling | ❌ Intentional legacy typo. Do NOT fix. |
|
||||
| IsMeasurable=false means no Pallet | ❌ Pallet is always created. |
|
||||
| IsMeasurable=false means no weight | ❌ Weight always exists — it comes from the product's `AverageWeight` instead of a scale, and lands in the same pallet record. |
|
||||
| NetWeight is stored/settable | ❌ It is calculated. The setter throws an Exception! It only exists to satisfy the `IMeasuringItemPalletBase` interface boundary. Set `GrossWeight`, `PalletWeight`, `TareWeight` instead. |
|
||||
| Setting MeasuringStatus | ❌ It's a calculated property (evaluates `IsMeasured`, `Id`, or child pallets). Do not try to set it. |
|
||||
| Setting ForeignKey | ❌ `ForeignKey` is read-only. Use `SetForeignKey(id)` method instead. |
|
||||
|
||||
@@ -104,8 +104,8 @@ namespace FruitBankHybrid.Shared.Tests
|
||||
Assert.IsFalse(ekaerHistory.IsOutgoing);
|
||||
Assert.AreEqual(allocation.Id, ekaerHistory.Mappings!.Single().ForeignKey, $"A mapping nem a forrás-allocationre mutat; allocation.Id: {allocation.Id}");
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(ekaerHistory.XmlDoc), $"XmlDoc üres; allocation.Id: {allocation.Id}");
|
||||
Assert.IsTrue(ekaerHistory.Status is EkaerStatus.Generated or EkaerStatus.ValidationError,
|
||||
$"Status: {ekaerHistory.Status}; allocation.Id: {allocation.Id}; ErrorText: {ekaerHistory.ErrorText}");
|
||||
Assert.IsTrue(ekaerHistory.Status is EkaerStatus.ToGenerate or EkaerStatus.ToSubmit,
|
||||
$"Status: {ekaerHistory.Status}; Result: {ekaerHistory.Result}; allocation.Id: {allocation.Id}; ErrorText: {ekaerHistory.ErrorText}");
|
||||
|
||||
// A grid útvonala: az XmlDoc visszaolvasható tradeCard-dá.
|
||||
var tradeCard = NavXmlHelper.Deserialize<TradeCardType>(ekaerHistory.XmlDoc!);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
@using AyCode.Services.Nav
|
||||
@using AyCode.Services.Nav.Ekaer.Models
|
||||
@using FruitBank.Common.Entities
|
||||
@using FruitBank.Common;
|
||||
@using FruitBankHybrid.Shared.Services.Loggers
|
||||
@using FruitBankHybrid.Shared.Services.SignalRs
|
||||
|
||||
@@ -235,11 +236,13 @@
|
||||
else
|
||||
{
|
||||
// A rács a saját példányát mutatja, ezért abba írjuk vissza — különben a fül a régit mutatná.
|
||||
// A NavStatus is kell: az a rács „NAV állapot" OSZLOPA, tehát a hiánya nem csak itt látszana.
|
||||
// Csak ez a két mező jön át — a frissítés a NAV-oldali pillanatképet hozza, a saját státuszunkat
|
||||
// és az általunk generált kártyát nem érinti; ezért NEM a rács ApplyServerState-je való ide.
|
||||
// A NavStatus és a NavValidUntil is kell: azok a rács „NAV állapot" és „Lejárat" OSZLOPAI, tehát a
|
||||
// hiányuk nem csak itt látszana. Csak ez a három mező jön át — a frissítés a NAV-oldali
|
||||
// pillanatképet hozza, a saját státuszunkat és az általunk generált kártyát nem érinti; ezért NEM a
|
||||
// rács ApplyServerState-je való ide.
|
||||
History.NavXmlDoc = refreshed.NavXmlDoc;
|
||||
History.NavStatus = refreshed.NavStatus;
|
||||
History.NavValidUntil = refreshed.NavValidUntil;
|
||||
_navCard = Deserialize<TradeCardInfoType>(History.NavXmlDoc);
|
||||
if (_source == DetailSource.Nav) RefreshItems();
|
||||
|
||||
@@ -269,13 +272,13 @@
|
||||
if (History is null || !CanDownload) return;
|
||||
|
||||
// Fájlnév: "yyyy.MM.dd-Partner-forrás.xml" — a partnernévből az érvénytelen fájlnév-karaktereket kicseréljük.
|
||||
var datePart = History.ShippingDate?.ToString("yyyy.MM.dd") ?? "nincs-datum";
|
||||
var partner = string.IsNullOrWhiteSpace(History.Partner) ? "partner" : History.Partner;
|
||||
var invalid = System.IO.Path.GetInvalidFileNameChars();
|
||||
var datePart = History.ShippingDate?.ToString("yyyy.MM.dd") ?? "nincs-datum";
|
||||
var partner = string.IsNullOrWhiteSpace(History.Partner) ? "partner" : History.Partner;
|
||||
var invalid = System.IO.Path.GetInvalidFileNameChars();
|
||||
var safePartner = new string(partner.Select(c => invalid.Contains(c) ? '_' : c).ToArray()).Trim();
|
||||
if (string.IsNullOrWhiteSpace(safePartner)) safePartner = "partner";
|
||||
var sourcePart = _source == DetailSource.Nav ? "nav" : "generalt";
|
||||
var fileName = $"{datePart}-{safePartner}-{sourcePart}.xml";
|
||||
var sourcePart = _source == DetailSource.Nav ? "nav" : "generalt";
|
||||
var fileName = $"{datePart}-{safePartner}-{sourcePart}.xml";
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
@using AyCode.Services.Nav
|
||||
@using AyCode.Services.Nav.Ekaer.Models
|
||||
@using AyCode.Utils.Extensions
|
||||
@using FruitBank.Common
|
||||
@using FruitBank.Common.Dtos
|
||||
@using FruitBank.Common.Entities
|
||||
@using FruitBankHybrid.Shared.Databases
|
||||
@@ -37,35 +38,58 @@
|
||||
<DxGridDataColumn FieldName="Id" SortIndex="0" SortOrder="GridColumnSortOrder.Descending" ReadOnly="true" />
|
||||
<DxGridDataColumn FieldName="@nameof(EkaerHistory.IsOutgoing)" Caption="Kimenő?" ReadOnly="true" />
|
||||
@* KÖTÖTT oszlop, nem sablon-only: FieldName nélkül a szűrősor cellája üresen marad és a kereső-doboz
|
||||
is átlépi az oszlopot — a lefedett forrás-rekordokra így nem lehetett rákeresni. A szöveget az
|
||||
entitás adja (ForeignKeysText), a cella csak a tooltipet teszi hozzá. *@
|
||||
<DxGridDataColumn FieldName="@nameof(EkaerHistory.ForeignKeysText)" Caption="ForeignKey(s)" ReadOnly="true"
|
||||
is átlépi az oszlopot — a lefedett forrás-rekordokra így nem lehetett rákeresni.
|
||||
A minta a felsorolást vágja darabokra, a SourceRecordLinkPath pedig SORONKÉNT dönt: bejövőnél null,
|
||||
és a keret a null-szabály szerint sima szövegként rendereli — a szállítólevél-oldalra mutató link
|
||||
a komponens-URL-diszpécserre vár (ACBLAZOR ADR 0002-H4Y6). A hoszt itt kerül elé, mert az entitás
|
||||
nem olvashat AcEnv-et. CellDisplayTemplate ezért nem lehet rajta: a UrlLink felülírná. *@
|
||||
<MgGridDataColumn FieldName="@nameof(EkaerHistory.SourceRecordIdsText)" Caption="Forrás" ReadOnly="true"
|
||||
AllowSort="false" AllowGroup="false"
|
||||
FilterRowOperatorType="GridFilterRowOperatorType.Contains">
|
||||
<CellDisplayTemplate>
|
||||
@{
|
||||
var row = (EkaerHistory)context.DataItem;
|
||||
}
|
||||
@if (row.Mappings is { Count: > 0 } maps)
|
||||
{
|
||||
var noun = row.IsOutgoing ? "rendelés" : "szállítólevél";
|
||||
<span title="@(maps.Count > 1 ? $"{maps.Count} {noun} egy EKÁER alá vonva" : $"1 {noun}")">@context.DisplayText</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>—</span>
|
||||
}
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
FilterRowOperatorType="GridFilterRowOperatorType.Contains"
|
||||
UrlLink="@(FruitBankConstClient.BaseUrl + "{SourceRecordLinkPath}{0}")"
|
||||
UrlLinkPattern="#(\d+)" />
|
||||
<DxGridDataColumn FieldName="@nameof(EkaerHistory.ShippingDate)" Caption="Dátum" DisplayFormat="yyyy.MM.dd" ReadOnly="true" />
|
||||
<DxGridDataColumn FieldName="@nameof(EkaerHistory.Partner)" Caption="Partner" ReadOnly="true" />
|
||||
@* A kézi NAV-beadás fázisában a Status / EKÁER szám / SentDate kézzel szerkeszthető. *@
|
||||
<DxGridDataColumn FieldName="@nameof(EkaerHistory.Status)" />
|
||||
<DxGridDataColumn FieldName="@nameof(EkaerHistory.EkaerNumber)" Caption="EKÁER szám" />
|
||||
@* A NAV SAJÁT státusza (P/S/F/I/D), nem a miénk — a kettő nem ugyanaz, és a megfeleltetésük
|
||||
még nincs kimérve. ReadOnly: a NAV mondja meg, nem mi. *@
|
||||
@* A NAV SAJÁT státusza (P/S/F/I/D), nem a miénk: a Status a legutóbbi MŰVELETÜNK kimenete, ez pedig
|
||||
az, hogy a bejelentés hol tart a NAV életciklusában — a kettő ortogonális, nincs megfeleltetés.
|
||||
ReadOnly: a NAV mondja meg, nem mi. *@
|
||||
<DxGridDataColumn FieldName="@nameof(EkaerHistory.NavStatus)" Caption="NAV állapot" ReadOnly="true" />
|
||||
<DxGridDataColumn FieldName="@nameof(EkaerHistory.SentDate)" DisplayFormat="yyyy.MM.dd HH:mm" />
|
||||
@* A lerakodás ideje, amit a véglegesítés a NAV-nak küld: bejövőn a mérés lezárása, kimenőn a rendelés
|
||||
átvételi dátuma. ÜRES = az áru még nem érkezett meg, tehát a sor lezárásra sem jelölhető.
|
||||
ReadOnly SZÁNDÉKOSAN: a finalize visszafordíthatatlan, és egy kézzel beírt dátum véglegesen
|
||||
rögzülne a bejelentésen — az érték a forrásból jön, vagy sehonnan. *@
|
||||
<DxGridDataColumn FieldName="@nameof(EkaerHistory.ArrivalDate)" Caption="Érkezés" ReadOnly="true" DisplayFormat="yyyy.MM.dd" />
|
||||
@* Az EKÁER-SZÁM érvényességének vége (tcnValidityEnd), nem a fuvarhoz kötött bejelentési határidő:
|
||||
mérve (2026-08-31), a NAV a LÉTREHOZÁSTÓL számol tizenöt napot, a felrakodás dátumától
|
||||
függetlenül. Véglegesítés híján eddig aktív a bejelentés, utána a NAV Inactive-ra (`I`) állítja.
|
||||
A figyelmeztetés csak a NAV-nál NYITOTT soron szól — lezártnál a dátum már történelem. *@
|
||||
<DxGridDataColumn FieldName="@nameof(EkaerHistory.NavValidUntil)" Caption="Szám érvényessége" ReadOnly="true" DisplayFormat="yyyy.MM.dd">
|
||||
<CellDisplayTemplate>
|
||||
@{
|
||||
var expiryRow = (EkaerHistory)context.DataItem;
|
||||
var deadline = expiryRow.NavValidUntil;
|
||||
}
|
||||
@if (deadline is not null && expiryRow.IsOpenAtNav())
|
||||
{
|
||||
var daysLeft = (deadline.Value.Date - DateTime.UtcNow.Date).Days;
|
||||
<span title="@(daysLeft < 0 ? "Lejárt — véglegesíteni már nem lehet" : $"{daysLeft} nap múlva lejár, ha nem véglegesíted")">
|
||||
@if (daysLeft <= ExpiryWarningDays)
|
||||
{
|
||||
<span aria-hidden="true" style="font-size:1.2em; margin-right:3px;">@(daysLeft < 0 ? "⌛" : "⏳")</span>
|
||||
}
|
||||
@context.DisplayText
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
@context.DisplayText
|
||||
}
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
@* Audit: a value-számításhoz alkalmazott árfolyam (HUF feladónál 1). ReadOnly — generáláskor töltődik. *@
|
||||
<DxGridDataColumn FieldName="@nameof(EkaerHistory.ConversionRate)" Caption="Árfolyam" ReadOnly="true" DisplayFormat="0.00##" />
|
||||
@* Üzenet-oszlop: warningot ÉS errort is mutat. Ikon a súlyosság szerint: ⛔ blokkoló hiba, ⚠️ warning (pótlandó). *@
|
||||
@@ -73,7 +97,7 @@
|
||||
<CellDisplayTemplate>
|
||||
@if (!string.IsNullOrWhiteSpace(context.DisplayText))
|
||||
{
|
||||
var isError = ((EkaerHistory)context.DataItem).Status.IsError();
|
||||
var isError = ((EkaerHistory)context.DataItem).Result.IsError();
|
||||
<span title="@context.DisplayText">
|
||||
<span aria-hidden="true" style="font-size:1.2em; margin-right:3px;">@(isError ? "⛔" : "⚠️")</span>@context.DisplayText
|
||||
</span>
|
||||
@@ -86,7 +110,7 @@
|
||||
@* EGY akció-oszlop, a TARTALMA fülfüggő. Nem fülönkénti oszlopok láthatósággal: az oszlop-
|
||||
láthatóságot a felhasználó állítja a Column Chooserrel, és a mentett layout vissza is tölti —
|
||||
kódból ugyanazt a kapcsolót fogni ütközés. A tartalom nem mentődik, nincs mivel ütköznie. *@
|
||||
<DxGridDataColumn Caption="Műveletek" Width="@(ShowOpenAtNavActions ? "290" : "200")" AllowSort="false" AllowGroup="false">
|
||||
<DxGridDataColumn Caption="Műveletek" Width="@(ShowOpenAtNavActions ? "380" : "200")" AllowSort="false" AllowGroup="false">
|
||||
<CellDisplayTemplate>
|
||||
@{
|
||||
var row = (EkaerHistory)context.DataItem;
|
||||
@@ -106,8 +130,10 @@
|
||||
Attributes="@(new Dictionary<string, object> { ["title"] = CanSend(row) ? "A legenerált bejelentés beküldése a NAV-nak" : "Csak legenerált, hibátlan bejelentés küldhető be." })"
|
||||
Click="async () => await OnSendClick(row)" />
|
||||
}
|
||||
@* A NAV-nál nyitva álló ág gombjai. TILTVA állnak, amíg a műveletük nincs bekötve — azért
|
||||
vannak mégis kint, hogy a folyamat végigolvasható legyen a rácsról (lásd NotWiredYet).
|
||||
@* A NAV-nál nyitva álló ág gombjai. A „Generate" és a „Módosítás" KÜLÖN gomb: a generálás csak
|
||||
a kártyát írja újra a mostani forrás-adatból, a beküldés a Módosítás — így a felhasználó
|
||||
átnézheti, mi megy ki, mielőtt visszafordíthatatlanul kimenne. A Generate az ÚT ide:
|
||||
egy kiment soron újragenerálva a sor `ToModify`-ba kerül, és onnan a Módosítás aktív.
|
||||
A „Lezárt" fülön nincs gomb: ott a folyamat véget ért, a `correction` pedig nyitott
|
||||
üzleti kérdés — egy gomb róla döntést ábrázolna, ami nem született meg.
|
||||
A letöltés a detail XML-fülére költözött, a tárgya mellé. *@
|
||||
@@ -116,18 +142,21 @@
|
||||
<DxButton Text="Generate"
|
||||
SizeMode="SizeMode.Small"
|
||||
RenderStyle="ButtonRenderStyle.Primary"
|
||||
Enabled="false"
|
||||
Attributes="@(new Dictionary<string, object> { ["title"] = NotWiredYet("A kártya újragenerálása modify-ként, a NAV menetlevél- és tétel-id-jeivel", "1. lépés") })" />
|
||||
Enabled="@(CanGenerate(row))"
|
||||
Attributes="@(new Dictionary<string, object> { ["title"] = row.ErrorText ?? "A kártya újragenerálása a mostani forrás-adatból — beküldés nélkül" })"
|
||||
Click="async () => await OnGenerateClick(row)" />
|
||||
<DxButton Text="Módosítás"
|
||||
SizeMode="SizeMode.Small"
|
||||
RenderStyle="ButtonRenderStyle.Secondary"
|
||||
Enabled="false"
|
||||
Attributes="@(new Dictionary<string, object> { ["title"] = NotWiredYet("A módosítás beküldése a NAV-nak (pl. rendszám-pótlás), az EKÁER-szám megtartásával", "1. lépés") })" />
|
||||
RenderStyle="ButtonRenderStyle.Primary"
|
||||
Enabled="@(CanModify(row))"
|
||||
Attributes="@(new Dictionary<string, object> { ["title"] = CanModify(row) ? "A módosítás beküldése a NAV-nak — az EKÁER-szám megmarad" : "Előbb generálj újra: a NAV-nál lévőhöz képest nincs mit beküldeni, vagy a pótolandó adat még hiányzik." })"
|
||||
Click="async () => await OnModifyClick(row)" />
|
||||
<DxButton Text="Lezárás"
|
||||
SizeMode="SizeMode.Small"
|
||||
RenderStyle="ButtonRenderStyle.Success"
|
||||
Enabled="false"
|
||||
Attributes="@(new Dictionary<string, object> { ["title"] = NotWiredYet("A bejelentés véglegesítése a NAV-nál (finalize), az érkezési idővel", "2–4. lépés") })" />
|
||||
Enabled="@(CanFinalize(row))"
|
||||
Attributes="@(new Dictionary<string, object> { ["title"] = CanFinalize(row) ? "A bejelentés véglegesítése a NAV-nál — visszafordíthatatlan" : "Csak a NAV-nál nyitva álló, EKÁER-számmal rendelkező bejelentés véglegesíthető." })"
|
||||
Click="async () => await OnFinalizeClick(row)" />
|
||||
}
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
@@ -204,17 +233,6 @@
|
||||
/// <summary>A NAV-nál nyitva álló ág tartalma: a pótlás és a véglegesítés gombjai.</summary>
|
||||
private bool ShowOpenAtNavActions => Filter is EkaerHistoryFilter.All or EkaerHistoryFilter.OpenAtNav;
|
||||
|
||||
/// <summary>Még be nem kötött művelet tooltipje: mit fog csinálni, és melyik lépésre vár.</summary>
|
||||
/// <remarks>
|
||||
/// Ezek a gombok a folyamat ÁBRÁZOLÁSÁÉRT állnak kint tiltva — hogy a rácsról végigolvasható legyen, mi
|
||||
/// következik egy bejelentésen. Az <c>ADR 0005-H2Q6</c> ezzel szemben azt mondta ki, hogy gomb a művelete
|
||||
/// nélkül nem kerül ki; a kinttartás felhasználói döntés (2026-08-20), és ez a rekord felülvizsgálandó.
|
||||
/// A bekötés horgonya ez a segéd: minden hívási helye egy-egy hátralévő lépés
|
||||
/// (<c>FBANKAPP-EKAER-T-R9D4</c>), és amint a művelet megvan, a hívás helyén tűnik el.
|
||||
/// </remarks>
|
||||
private static string NotWiredYet(string whatItWillDo, string step)
|
||||
=> $"{whatItWillDo} — még nincs bekötve (R9D4 {step}).";
|
||||
|
||||
/// <summary>A szülő (oldal) értesítése, ha egy művelet státuszt válthatott (generálás / kézi mentés / jövőbeni beküldés) —
|
||||
/// hogy a fül-számlálók azonnal frissüljenek, mert a sor átkerülhet másik tabra.</summary>
|
||||
[Parameter] public EventCallback OnDataChanged { get; set; }
|
||||
@@ -259,17 +277,41 @@
|
||||
EditItemsEnabled = true;
|
||||
}
|
||||
|
||||
// Hány nappal a NAV határideje előtt jelezzen a Lejárat oszlop. A NAV tizenöt napot ad; három nap az, amin
|
||||
// belül a véglegesítés már nem halasztható tovább.
|
||||
private const int ExpiryWarningDays = 3;
|
||||
|
||||
private readonly HashSet<int> _generatingIds = [];
|
||||
|
||||
// Elküldött (NAV-nál lévő) bejelentést nem generálunk újra némán; minden más állapot újragenerálható.
|
||||
// A BIZONYTALAN kimenetelű beküldésen nem generálunk újra: elindult, de EKÁER-számot nem kaptunk rá, és a
|
||||
// create-alakú újragenerálás elvinné azt a kártyát, amit a NAV esetleg mégis befogadott — ott előbb a
|
||||
// NAV-állapot frissítése a teendő. Lezárton sincs mit generálni. Minden más újragenerálható: a NAV-nál lévő
|
||||
// sor is, mert oda éppen a Generate az út a módosításhoz.
|
||||
private bool CanGenerate(EkaerHistory ekaerHistory)
|
||||
=> !ekaerHistory.Status.IsSent() && !_generatingIds.Contains(ekaerHistory.Id);
|
||||
=> !ekaerHistory.NeedsNavRefresh() && !ekaerHistory.IsClosed() && !_generatingIds.Contains(ekaerHistory.Id);
|
||||
|
||||
private readonly HashSet<int> _sendingIds = [];
|
||||
|
||||
// Legenerálva, blokkoló hiba nélkül (a warning — pl. hiányzó rendszám — küldhető), és nincs folyamatban küldés.
|
||||
// Legenerálva, blokkoló hiba nélkül (a warning — pl. hiányzó rendszám — küldhető), még ki nem ment (ezt már az
|
||||
// IsSubmittable nézi a SentDate-en), és nincs folyamatban küldés.
|
||||
private bool CanSend(EkaerHistory ekaerHistory)
|
||||
=> ekaerHistory.Status.IsSubmittable() && !string.IsNullOrWhiteSpace(ekaerHistory.XmlDoc) && !_sendingIds.Contains(ekaerHistory.Id);
|
||||
=> ekaerHistory.IsSubmittable() && !string.IsNullOrWhiteSpace(ekaerHistory.XmlDoc) && !_sendingIds.Contains(ekaerHistory.Id);
|
||||
|
||||
private readonly HashSet<int> _modifyingIds = [];
|
||||
private readonly HashSet<int> _finalizingIds = [];
|
||||
|
||||
// A NAV-nál NYITOTT (P/S) sor mozdítható: lezárton a modify-t és a finalize-t is a NAV utasítaná el. Az
|
||||
// EKÁER-szám külön feltétel, mert az IsOpenAtNav a szám nélküli, ISMERETLEN kimenetelű beküldést is idesorolja
|
||||
// — azon előbb a NAV-állapot frissítése a teendő, nem a módosítás.
|
||||
private bool CanTouchAtNav(EkaerHistory ekaerHistory)
|
||||
=> ekaerHistory.IsOpenAtNav() && !string.IsNullOrWhiteSpace(ekaerHistory.EkaerNumber)
|
||||
&& !_modifyingIds.Contains(ekaerHistory.Id) && !_finalizingIds.Contains(ekaerHistory.Id);
|
||||
|
||||
// A tartozás és a legutóbbi kimenet párosa dönt: ToModify + Ok = van mit beküldeni. Warningnál NEM —
|
||||
// ott a pótolnivaló még áll, tehát előbb a forrásadatot kell javítani és újragenerálni.
|
||||
private bool CanModify(EkaerHistory ekaerHistory) => CanTouchAtNav(ekaerHistory) && ekaerHistory.CanModify();
|
||||
|
||||
private bool CanFinalize(EkaerHistory ekaerHistory) => CanTouchAtNav(ekaerHistory) && ekaerHistory.CanFinalize();
|
||||
|
||||
// A szülő-oldal (fül-számlálók) értesítése egy státuszt érintő művelet után. Üres delegáltnál no-op.
|
||||
private Task NotifyDataChanged() => OnDataChanged.HasDelegate ? OnDataChanged.InvokeAsync() : Task.CompletedTask;
|
||||
@@ -342,6 +384,85 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnModifyClick(EkaerHistory ekaerHistory)
|
||||
{
|
||||
if (!CanModify(ekaerHistory)) return;
|
||||
|
||||
// A módosítás is kifelé irányuló művelet: a NAV-nál álló bejelentést írja át.
|
||||
var confirmed = await DialogService.ShowConfirmBoxAsync(
|
||||
"EKÁER módosítás",
|
||||
$"Újragenerálod a kártyát és beküldöd a módosítást a NAV-nak? (#{ekaerHistory.Id} — {ekaerHistory.EkaerNumber}) Az EKÁER-szám megmarad.",
|
||||
MessageBoxRenderStyle.Warning);
|
||||
if (!confirmed) return;
|
||||
|
||||
if (!_modifyingIds.Add(ekaerHistory.Id)) return;
|
||||
await OnBusyChanged.InvokeAsync(true); // Loading panel BE — újragenerálás + NAV-hívás
|
||||
|
||||
try
|
||||
{
|
||||
var updated = await FruitBankSignalRClient.ModifyEkaerAtNav(ekaerHistory.Id);
|
||||
|
||||
if (updated == null)
|
||||
{
|
||||
_logger.Error($"ModifyEkaerAtNav null választ adott; EkaerHistory #{ekaerHistory.Id}");
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyServerState(ekaerHistory, updated);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"ModifyEkaerAtNav hiba; EkaerHistory #{ekaerHistory.Id}", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_modifyingIds.Remove(ekaerHistory.Id);
|
||||
await OnBusyChanged.InvokeAsync(false);
|
||||
Grid?.Reload();
|
||||
await NotifyDataChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnFinalizeClick(EkaerHistory ekaerHistory)
|
||||
{
|
||||
if (!CanFinalize(ekaerHistory)) return;
|
||||
|
||||
// A véglegesítés VISSZAFORDÍTHATATLAN — a megerősítés ezt mondja ki, nem csak azt, hogy „biztos?".
|
||||
var confirmed = await DialogService.ShowConfirmBoxAsync(
|
||||
"EKÁER lezárás",
|
||||
$"Véglegesíted a bejelentést a NAV-nál? (#{ekaerHistory.Id} — {ekaerHistory.EkaerNumber}) "
|
||||
+ "Ezután az adatok már csak pótlék ellenében módosíthatók. A lerakodás idejét a rendszer a szállítmányból veszi.",
|
||||
MessageBoxRenderStyle.Warning);
|
||||
if (!confirmed) return;
|
||||
|
||||
if (!_finalizingIds.Add(ekaerHistory.Id)) return;
|
||||
await OnBusyChanged.InvokeAsync(true);
|
||||
|
||||
try
|
||||
{
|
||||
var updated = await FruitBankSignalRClient.FinalizeEkaerAtNav(ekaerHistory.Id);
|
||||
|
||||
if (updated == null)
|
||||
{
|
||||
_logger.Error($"FinalizeEkaerAtNav null választ adott; EkaerHistory #{ekaerHistory.Id}");
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyServerState(ekaerHistory, updated);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"FinalizeEkaerAtNav hiba; EkaerHistory #{ekaerHistory.Id}", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_finalizingIds.Remove(ekaerHistory.Id);
|
||||
await OnBusyChanged.InvokeAsync(false);
|
||||
Grid?.Reload();
|
||||
await NotifyDataChanged(); // a sor átkerülhet a „Lezárt" fülre
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A rácsban lévő példány átveszi a szerver mérvadó sorát — a grid ugyanazt a példányt mutatja,
|
||||
/// tehát ami ide nem kerül be, az a képernyőn a régi marad, amíg valaki újra nem tölti a rácsot.</summary>
|
||||
/// <remarks>
|
||||
@@ -355,8 +476,10 @@
|
||||
// A Generate frissíti az összegzőt is (ShippingDate + Partner), nem csak az XML-t.
|
||||
target.ShippingDate = source.ShippingDate;
|
||||
target.Partner = source.Partner;
|
||||
target.ArrivalDate = source.ArrivalDate;
|
||||
|
||||
target.Status = source.Status;
|
||||
target.Result = source.Result;
|
||||
target.ErrorText = source.ErrorText;
|
||||
target.ConversionRate = source.ConversionRate;
|
||||
|
||||
@@ -366,6 +489,7 @@
|
||||
|
||||
target.EkaerNumber = source.EkaerNumber;
|
||||
target.SentDate = source.SentDate;
|
||||
target.NavValidUntil = source.NavValidUntil;
|
||||
target.Modified = source.Modified;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Helpers;
|
||||
using AyCode.Core.Loggers;
|
||||
using AyCode.Services.SignalRs;
|
||||
@@ -177,12 +177,23 @@ namespace FruitBankHybrid.Shared.Services.SignalRs
|
||||
|
||||
// Blokkoló hibás sort nem küldünk ellenőriztetni: guard-hibánál nincs is XmlDoc, egyébként pedig
|
||||
// előbb a SAJÁT hibánkat kell javítani — a NAV véleménye addig nem visz előrébb.
|
||||
if (generated == null || !generated.Status.IsSubmittable()) return generated;
|
||||
//
|
||||
// A beküldhetőséget NEM nézhetjük itt: azt éppen a NAV válasza mondja ki (ADR 0006-V8N5), tehát erre
|
||||
// a feltételre kötve a validálás soha nem futna le, és a sor örökre ToGenerate-en maradna.
|
||||
if (generated == null || !generated.CanValidateAtNav()) return generated;
|
||||
|
||||
return await ValidateEkaerWithNav(ekaerHistoryId) ?? generated;
|
||||
}
|
||||
public Task<EkaerHistory?> SendEkaerToNav(int ekaerHistoryId) => GetByIdAsync<EkaerHistory?>(SignalRTags.SendEkaerToNav, ekaerHistoryId);
|
||||
|
||||
/// <summary>Módosítja a NAV-nál lévő bejelentést: a szerver újragenerálja a kártyát a mostani forrás-adatból,
|
||||
/// majd <c>modify</c>-ként beküldi. Az EKÁER-szám megmarad.</summary>
|
||||
public Task<EkaerHistory?> ModifyEkaerAtNav(int ekaerHistoryId) => GetByIdAsync<EkaerHistory?>(SignalRTags.ModifyEkaerAtNav, ekaerHistoryId);
|
||||
|
||||
/// <summary>Véglegesíti a bejelentést a NAV-nál (<c>finalize</c>). <b>Visszafordíthatatlan</b> — a hívó
|
||||
/// megerősítést kér. A lerakodás idejét a szerver a forrásból veszi, nincs mit megadni.</summary>
|
||||
public Task<EkaerHistory?> FinalizeEkaerAtNav(int ekaerHistoryId) => GetByIdAsync<EkaerHistory?>(SignalRTags.FinalizeEkaerAtNav, ekaerHistoryId);
|
||||
|
||||
/// <summary>Visszaolvassa a NAV-nál lévő állapotot, és a frissített rekordot adja vissza.</summary>
|
||||
public Task<EkaerHistory?> RefreshEkaerNavState(int ekaerHistoryId) => GetByIdAsync<EkaerHistory?>(SignalRTags.RefreshEkaerNavState, ekaerHistoryId);
|
||||
public Task<EkaerHistory?> CreateEkaerHistory(int foreignKey, bool isOutgoing) => GetByIdAsync<EkaerHistory?>(SignalRTags.CreateEkaerHistory, [foreignKey, isOutgoing]);
|
||||
|
||||
+1
-2
@@ -56,7 +56,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "adr", "adr", "{3F8A1C6D-9B4
|
||||
docs\adr\0003-shipping-item-to-shipping-mapping.md = docs\adr\0003-shipping-item-to-shipping-mapping.md
|
||||
docs\adr\0004-M9F3-fractional-pallets-and-remainder-grouping.md = docs\adr\0004-M9F3-fractional-pallets-and-remainder-grouping.md
|
||||
docs\adr\0005-H2Q6-ekaer-consumer-implementation.md = docs\adr\0005-H2Q6-ekaer-consumer-implementation.md
|
||||
docs\adr\0006-K4T9-admin-declared-pallet-group-capacity.md = docs\adr\0006-K4T9-admin-declared-pallet-group-capacity.md
|
||||
docs\adr\0006-V8N5-ekaer-status-on-two-axes.md = docs\adr\0006-V8N5-ekaer-status-on-two-axes.md
|
||||
docs\adr\README.md = docs\adr\README.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
@@ -101,7 +101,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SHPLAN", "SHPLAN", "{93D3C2
|
||||
docs\SHPLAN\P1_backfill.sql = docs\SHPLAN\P1_backfill.sql
|
||||
docs\SHPLAN\P7_pallet_allocation_backfill.sql = docs\SHPLAN\P7_pallet_allocation_backfill.sql
|
||||
docs\SHPLAN\P8_fractional_pallets.sql = docs\SHPLAN\P8_fractional_pallets.sql
|
||||
docs\SHPLAN\P9_pallet_group_capacity.sql = docs\SHPLAN\P9_pallet_group_capacity.sql
|
||||
docs\SHPLAN\ui-demo.html = docs\SHPLAN\ui-demo.html
|
||||
EndProjectSection
|
||||
EndProject
|
||||
|
||||
@@ -10,6 +10,58 @@ Scope: az EKÁER **ebben a repóban élő** kódja — leképezés, kötelezetts
|
||||
|
||||
## Active entries
|
||||
|
||||
## FBANKAPP-EKAER-I-Q7R4: A beküldött bejelentés nem módosítható mezőin ejtett hiba feltehetően javíthatatlan
|
||||
|
||||
**Status:** Closed (2026-08-31) · **Priority:** P2 · **Type:** I · **2026-08-30**
|
||||
|
||||
### Resolution
|
||||
|
||||
**A gyanú megdőlt: a beküldött bejelentés TÖRÖLHETŐ.** Mérve a NAV teszt-rendszerén 2026-08-31-én
|
||||
(`FmEkaerNavGatewayAgainstNavTests` takarító lépése): a `create` után azonnal létrejövő kártyára a `delete`
|
||||
`funcCode=Ok`-kal fut le. Az állapot-gráf „`D` csak `P`-ből" olvasata a gyakorlatban nem korlátoz.
|
||||
|
||||
Ezzel a bejegyzés tárgya megszűnt: **van visszaút** egy nem módosítható mezőben ejtett hibából — törlés, majd új
|
||||
bejelentés. Nem kell megvárni a lejáratot, és nincs átfedő két bejelentés.
|
||||
|
||||
Ami a méréstől függetlenül igaz marad, de már nem hordoz kockázatot: a `modify` továbbra is csak felsorolt
|
||||
mezőket enged, és a `FmEkaerModifyCard` szándékosan nem ír indokot a nem módosíthatókra. A NAV-oldali szabály
|
||||
helye `AyCode.Services/docs/NAV/EKAER_OPERATIONS.md` → *`delete` — törlés*, ahol a mérés is áll.
|
||||
|
||||
A NAV a `modify`-nál a fejrészből **csak felsorolt mezőket** enged módosítani — `orderNumber`, `plateNumber` +
|
||||
`country`, `modByCarrierEnabled`, `carrier`, `carrierText`, és import + kockázatos terméknél az
|
||||
`unloadLocation.vatNumber/name`; tételen a `value`, `weight`, `productVtsz`, `productName`. Forrás:
|
||||
`AyCode.Services/docs/NAV/EKAER_OPERATIONS.md` → *`modify` — módosítás*.
|
||||
|
||||
**Ami ebből következik: minden más mező a beküldéssel véglegessé válik.** A dátumok, a helyszínek, a felek, a
|
||||
menetlevél szerkezete — ha bármelyikben hiba van, a bejelentés úgy marad. A három lehetséges visszaút mindegyike
|
||||
zártnak látszik:
|
||||
|
||||
| út | miért nem visz ki |
|
||||
|---|---|
|
||||
| `modify` | a mező nincs a módosítható halmazban |
|
||||
| `correction` | csak véglegesítés UTÁN, törvényi kereten belül, pótlék ellenében, és csak `plateNumber` / `weight` / `value` |
|
||||
| `delete` | a NAV állapot-gráfja szerint `D` **csak `P`-ből**; a mi kártyáink a mérés szerint azonnal `S`-be kerülnek |
|
||||
|
||||
**Ez GYANÚ, nem mérés.** Egyik állítás sincs a mi rendszerünkön kipróbálva. **Egy `delete` hívás egy `S` állapotú
|
||||
sandbox-kártyára eldönti** a harmadik sort, és azzal a kérdés súlyát is: ha a törlés megy, minden hiba
|
||||
visszavonható, és ez a bejegyzés lezárul.
|
||||
|
||||
**Ha a gyanú igaz**, a hibás bejelentésre az egyetlen út megvárni, míg a NAV lejártatja (`I`), és közben újat
|
||||
csinálni — vagyis egy fuvarra átfedő időszakban két bejelentés, vagy egy lyuk. A NAV **az alul- és a
|
||||
túljelentést is bünteti**, tehát mindkettő kockázat.
|
||||
|
||||
**És akkor a beküldés ELŐTTI ellenőrzés súlya nő meg**: amit utólag nem lehet javítani, azt elsőre kell eltalálni.
|
||||
Ma a validátor a NAV sémáját és az üzleti minimumot nézi; a „nem javítható mező" szempont nincs benne.
|
||||
|
||||
**A kódban ennek ma egy nyoma van, szándékosan:** `FmEkaerModifyCard` a nem módosítható mezőkre **nem ír
|
||||
módosítási indokot**, és nem is hasonlítja őket — indokot olyan mezőre írni, amit a NAV nem enged módosítani,
|
||||
értelmetlen. A csend ott döntés, nem kimaradás.
|
||||
|
||||
**Affected:**
|
||||
- `FruitMasterErp: FruitMasterErp.Server/Services/Ekaer/FmEkaerModifyCard.cs` — a mezőnkénti összehasonlítás köre
|
||||
- `AyCode.Core: AyCode.Services/Nav/Ekaer/EkaerTradeCardValidator.cs` — ide kerülne a szigorúbb beküldés-előtti ellenőrzés
|
||||
- Kapcsolódik: `MGFBANKPLUG-EKAER-T-N4E7` (a törlő végpont) — ugyanez a kérdés az adat-oldalról: ha a NAV nem enged törölni, a végpont csak a beküldetlen sorokra való
|
||||
|
||||
## FBANKAPP-EKAER-I-J5T2: Az EKÁER-grid „ForeignKey(s)" oszlopa allokáció-azonosítót mutat, a felirat szállítólevelet ígér
|
||||
|
||||
**Status:** Open · **Priority:** P2 · **Type:** I (megjelenítés + adathozzáférés) · **2026-08-05**
|
||||
@@ -32,8 +84,27 @@ A felhasználó így `#1904`-et lát, amihez **nem tartozik szállítólevél**
|
||||
1. A mapping vigye magával a dokumentum-hivatkozást — a `Comment` oszlop erre van fenntartva, ma üres.
|
||||
2. A betöltés adjon hozzá megjelenítési mezőt (szerver-oldali join).
|
||||
|
||||
> **Döntés (2026-08-26): az 1. irány, tipizált oszloppal.** `FmEkaerHistoryMappingBase.SourceRecordId` (`int?`) a
|
||||
> `ForeignKey` MELLETT áll minden junction-soron: bejövőnél a szállítólevél, kimenőnél a rendelés. A kapu
|
||||
> **mindkét ágon** írja, létrehozáskor — a párt ott tudjuk, ahol a forrásokat úgyis olvassuk. A 2. irány
|
||||
> (szerver-oldali join a betöltésben) soronként két ugrást fizetne.
|
||||
>
|
||||
> Nem a `Comment`-be került, és nem is a kártyára vesszős listaként: `int` a természetes granularitáson, parse
|
||||
> nélkül. A tipizált szomszéd oszlop a `MGFBANKPLUG-EKAER-I-D8V3` kétértelműségét **csökkenti** — a `ForeignKey`
|
||||
> azért veszélyes, mert típustalan és polimorf.
|
||||
>
|
||||
> **A két azonosító nem ugyanaz** — a junction gépi kulcsa és amit az ember visszakeres. A definíció a rétegében
|
||||
> áll: `FruitMasterErp` → `docs/EKAER/README.md` → *The record's two identifiers*.
|
||||
>
|
||||
> A pillanatkép előtt létrejött sorok cellája üresen marad — szándékosan: azokat újragenerálás javítja, nem egy
|
||||
> visszaesési ág.
|
||||
>
|
||||
> **A bejegyzés nyitva marad**, mert a felirat csak a szöveget kapta meg, a **visszakereshetőséget** nem: a
|
||||
> szállítólevél-oldalra mutató link a komponens-URL-diszpécserre vár (`ACBLAZOR ADR 0002-H4Y6`,
|
||||
> `ACBLAZOR-GRID-T-Q6N3`). Kimenőn a link már él (`SourceRecordLinkPath` → a rendelés oldala).
|
||||
|
||||
**Affected:**
|
||||
- `FruitMasterErp: FruitMasterErp.Core/Entities/FmEkaerHistoryBase.cs` — `ForeignKeysText`, a cella szövegének forrása
|
||||
- `FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor` — a `ForeignKey(s)` oszlop tooltipje és a `—` üres eset
|
||||
- `FruitBank.Common/Entities/EkaerHistoryMapping.cs` — a `Comment` mező, ha az 1. irány mellett döntünk
|
||||
- `FruitMasterErp: FruitMasterErp.Core/Entities/FmEkaerHistoryBase.cs` — `SourceRecordIds` + `SourceRecordIdsText`
|
||||
- `Nop.Plugin.Misc.AIPlugin: Controllers/FruitBankDataController.cs` — a kapu írja a pillanatképet
|
||||
- `FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor` — a „Forrás" oszlop, és a bejövő link, amint van hova mutatnia
|
||||
- Kapcsolódik: `Nop.Plugin.Misc.AIPlugin: docs/EKAER/EKAER_ISSUES.md` → `MGFBANKPLUG-EKAER-I-D8V3` (ugyanaz a gyökér, adat-oldalról)
|
||||
|
||||
+96
-20
File diff suppressed because one or more lines are too long
@@ -74,76 +74,13 @@ művelet (szerver + UI), `GetShipmentPlannerData` maradék-megjelenítés, `I-H4
|
||||
Az [ADR 0004-M9F3](../adr/0004-M9F3-fractional-pallets-and-remainder-grouping.md) implementációjának **egyetlen
|
||||
horgonya**. A döntés és az indoklása az ADR-ben; itt csak az, hogy hol tartunk.
|
||||
|
||||
**Miért kell:** egy fizikai raklapon több áru is érkezhet; a szállítólevélen ez N tétel, tételenként 1 raklappal, így
|
||||
a rendszer N raklaphelyet számol ott, ahol fizikailag **egy** raklap áll. A kamionokat a valósnál telibbnek látjuk.
|
||||
**Miért kellett:** egy fizikai raklapon több áru is érkezhet; a szállítólevélen ez N tétel, tételenként 1 raklappal, így a rendszer N raklaphelyet számolt ott, ahol fizikailag **egy** raklap áll. A kamionokat a valósnál telibbnek láttuk.
|
||||
|
||||
**Séma (kézi SQL, mint eddig)** — ✅ **lefutott**: [`P8_fractional_pallets.sql`](P8_fractional_pallets.sql)
|
||||
- [x] `fbShippingItem.Pallets` — **`FLOAT`** `NOT NULL DEFAULT(0)` + backfill a `PalletsOnDocument`-ből
|
||||
- [x] `fbShippingItem.PalletGroupNo` — `int NULL`
|
||||
- [x] `fbShippingItemToShipping.Pallets` — `int` → **`FLOAT`** (értékek megtartva)
|
||||
**Két elvetett irány** (üzleti döntés, újra felmerülhet):
|
||||
- ~~`PlanningWeightKg`: a raklapsúly a FIZIKAI raklapok szerint~~ — **elvetve (ügyfél, 2026-08-07)**: a `PlanningWeightKgFor` egyben az allocation deklarált bruttója, azaz EKÁER-adat, és felfelé téved (konzervatív). Lásd az ADR 9. pontját.
|
||||
- ~~`updateTotalPallets` a foglaltság-képlet szerint~~ — **elvetve (2026-08-08)**: a `TotalPallets` a dokumentum összegzése, nem kamion-foglaltság; JS-ben újraírva az üzleti logika két helyen élne.
|
||||
|
||||
> **Típusdöntés: `FLOAT`, nem `DECIMAL`.** A C# propertyk `double`-ök, és a P0 is `FLOAT`-ot használt az allocation
|
||||
> súly-mezőihez — így nincs decimal↔double kerekítési rés a képlet két oldala közt.
|
||||
|
||||
**Common (wire contract — Common + Common.Server külön build!):**
|
||||
- [x] `Pallets` (`double`) + `PalletGroupNo` (`int?`) — **a `FruitMasterErp` keret-rétegében** (`FmShippingItemBase`),
|
||||
nem a `FruitBank.Common`-ban: a párja (`PalletsOnDocument`) is ott van, és a fogalom nem FruitBank-specifikus
|
||||
- [x] allocation `Pallets`: `int` → `double` — szintén a keretben (`FmShippingItemToShippingBase`)
|
||||
- [x] `ShippingCapacityHelper`: a foglaltság-képlet (`floor` + csoportonkénti `ceil(Σ frac)` + csoport nélküli
|
||||
maradékok) **egy helyen**, `frac`-toleranciával (0,1+0,2 ≠ 0,3 — lásd az ADR „Negative" pontját)
|
||||
- [x] `Pallets` kalkulátor: `QuantityOnDocument / ProductDto.CratesPerPallet`, 0-védelemmel
|
||||
- [x] ⚠️ **`Fits(...)` szignatúra-váltás** — megvan: a helper a **végállapot** rakományát kapja
|
||||
(`Fits(IEnumerable<(double Pallets, int? GroupNo)> resultingLoad, double resultingKg, bool isPartial, int? availablePallets)`),
|
||||
nem két skalárt. A raklap-oldal nem additív: hogy egy tétel felrakása 0 vagy 1 extra hely, attól függ,
|
||||
fent van-e már a csoporttársa. Mindkét hívó átállt — a tervező drop-ellenőrzése és az `AutoPlanShipments` FFD-je.
|
||||
- [x] ~~`PlanningWeightKg`: a raklapsúly a FIZIKAI raklapok szerint~~ — **ELVETVE (ügyfél, 2026-08-07).** A
|
||||
raklapsúly-tag csak a **becslő** ágban van (dokumentum-bruttónál nem), a `PlanningWeightKgFor` viszont egyben
|
||||
az allocation deklarált bruttója, azaz **EKÁER-adat**. ~10 kg/raklap, és **felfelé** téved (konzervatív).
|
||||
A súly-számítás változatlan marad — lásd az ADR **9. pontját** (2026-08-07-én revideálva).
|
||||
|
||||
**Szerver:**
|
||||
- [x] a `Pallets` újraszámolása **minden** mennyiség-változásnál (nem fagy be — ADR 4. és 5. pont);
|
||||
a `ApplyDeclaredValues` mintájára **egyetlen** helyen
|
||||
- [x] `SyncMeasuringCountFromAllocationsAsync`: `Σ ceil(allocation.Pallets)` (**nem** `ceil(Σ …)`)
|
||||
- [x] ⚠️ **Az invariáns raklap-ága** — a tervezettnél TOVÁBB ment: a raklap-oldali tiltás **teljesen kikerült**
|
||||
(`GuardAllocationInvariantOnItemChangeAsync`). Az invariáns őre a **rekesz** maradt (ADR 0003: az a stabil
|
||||
egység); az allocation raklapszáma jogosan lehet nagyobb a tétel derivált raklapigényénél, mert méréskor a
|
||||
lemért raklap-DARABSZÁM kerül rá (egész), a `Pallets` viszont tört. Élesben ez tiltotta le a „nem jön többé"
|
||||
műveletet: 1 lemért raklap vs 0,833 derivált igény. A `palletsChanged` már csak a deklarált súlyok
|
||||
újraszámolását vezérli.
|
||||
- [x] `ApplyDeclaredValues` (`FruitBankDbContext`): a `pallets` paraméter `double`, és az egyenlőség-vizsgálat
|
||||
(`allocation.Pallets == pallets`) **toleranciás** összehasonlításra vált. Ez a metódus az allocation
|
||||
mennyiségének/súlyának **egyetlen** beállítási pontja — 8 hívója van, mind ezen megy át.
|
||||
- [x] a maradék-csoport **együtt mozog**: assign / move / split guard
|
||||
- [x] `PalletGroupNo` írása (a kiolvasás-ellenőrző felületről)
|
||||
|
||||
**Plugin admin-nézet** — `Areas/Admin/Views/Extras/ImageTextExtraction.cshtml` (nem Hybrid!): a kiolvasott tételek
|
||||
ellenőrző/javító felülete. JS-ből renderelt kártyák (`renderShippingItems`, ~660–706. sor), `data-field` +
|
||||
`data-index` kötéssel; a `palletsOnDocument` **ma is szerkeszthető**, a `productId` pedig már a payloadban van
|
||||
(badge-ként megjelenik) — tehát meglévő mintát bővítünk, nem új gridet építünk:
|
||||
- [x] új mező: `pallets` (tört, `step="0.001"`), előtöltve a kalkulációból, felülírhatóan
|
||||
- [x] új mező: `palletGroupNo` — a maradék-csoport megjelölése (üres = nincs csoportban)
|
||||
- [x] a `CratesPerPallet` bekerült **mindkét** tétel-payloadba, és a termék-autocomplete válaszába is — így
|
||||
termékválasztáskor azonnal újraszámol, nem csak mentés után
|
||||
- [x] hiányzó `CratesPerPallet` jelzése, a termék admin-linkjével: `/Admin/Product/Edit/{productId}`
|
||||
- [x] ~~az `updateTotalPallets` összesítő a foglaltság-képlet szerint~~ — **ELVETVE (2026-08-08).** A `TotalPallets`
|
||||
a **dokumentum** összegzése (`Σ PalletsOnDocument`), nem kamion-foglaltság; a `PalletsOnDocument` érintetlen
|
||||
marad, tehát az érték továbbra is helyes. A foglaltság-képlet kamion-szintű — itt még nincs is kamion —, és
|
||||
JS-ben újraírni azt jelentené, hogy az üzleti logika két helyen él. A raklaphelyet a tervező mutatja.
|
||||
|
||||
**Hybrid:**
|
||||
- [x] tervező: a foglaltság az `OccupiedPallets`-ből (`UsedPallets`, `StagedPallets`, kamion-sávok, AI-előnézet);
|
||||
a `BuildOverflowSuggestion` a végállapotra számol, nem összead
|
||||
- [x] a csoport vizuálisan: `⊞ N` chip a felrakón és a kamion rakományán, magyarázó tooltippel; a hiányzó
|
||||
rekesz/raklap arány `⚠` jelzés a termék admin-linkjével
|
||||
- [x] mérés: a sor-nyitás `ceil(allocation.Pallets)`; `SplitPallets` → `double`; a kliens-tükrök raklap-paramétere `double`
|
||||
- [x] a tört értékek `0.###` formátummal jelennek meg (nyersen `0,4000000000000001` is kiíródhatna)
|
||||
|
||||
**Kilépési feltétel:** minden projekt fordul (Common, Common.Server, plugin **VS-ben**, Hybrid), az SQL lefutott, és
|
||||
az ADR ellenőrző példája (2,2 / 0,4 / 0,4 → **3 raklaphely, 5 mérés**) a futó appban is kijön.
|
||||
|
||||
**Nem ez a fázis:** a mérőfelület nem tud a vártnál **többet** rögzíteni —
|
||||
[`FBANKAPP-MEASUR-I-J6R2`](../MEASURING/MEASURING_ISSUES.md), külön issue (üzleti döntés, Adam, 2026-08-02).
|
||||
**Nem ez a fázis:** a mérőfelület nem tud a vártnál többet rögzíteni — [`FBANKAPP-MEASUR-I-J6R2`](../MEASURING/MEASURING_ISSUES.md).
|
||||
|
||||
### Resolution
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
## Status
|
||||
|
||||
Proposed (2026-07-14)
|
||||
Accepted (2026-07-14)
|
||||
|
||||
> A magdöntés (perzisztált derivált súly, teljes egységesítés) eldőlt. `Proposed` marad, amíg (a) a kolléga per-termék `AverageWeight` feltöltése megvan, és (b) a készlet-súly kérdés (lásd *Nyitott döntések*) eldől.
|
||||
> A magdöntés — perzisztált derivált súly, teljes egységesítés — eldőlt és leszállt. Ami nyitva maradt, az nem a döntésé: a per-termék `AverageWeight` feltöltése (scope-on kívül) és a készlet-súly kérdése (lásd *Nyitott döntések*). Az implementáció állását a tracking-bejegyzés viszi, nem ez a mező.
|
||||
|
||||
> **Implementációs státusz:** `FBANKAPP-MEASUR-T-K4W7` — [`docs/MEASURING/MEASURING_TODO.md`](../MEASURING/MEASURING_TODO.md).
|
||||
|
||||
@@ -19,7 +19,7 @@ Proposed (2026-07-14)
|
||||
|
||||
## Decision
|
||||
|
||||
> **An ADR records reasoning, not the state, and binds nothing — `BASE:ARTIFACT_ROLES`.**
|
||||
> **An ADR records reasoning, not the state, and nothing in it is a rule; an open decision is the goal — `BASE:ARTIFACT_ROLES`.**
|
||||
|
||||
A nem mérendő tétel is **teljes értékű, perzisztált pallet-mérést** kap: a mérésnél a **rekeszszám** és a **tára** (a termékből default) alapján a pallet súlyai kiszámolódnak és **beíródnak** a palletbe:
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ Accepted (2026-07-15, rev. 2026-07-25)
|
||||
|
||||
## Decision
|
||||
|
||||
> **An ADR records reasoning, not the state, and binds nothing — `BASE:ARTIFACT_ROLES`.**
|
||||
> **An ADR records reasoning, not the state, and nothing in it is a rule; an open decision is the goal — `BASE:ARTIFACT_ROLES`.**
|
||||
|
||||
1. **Opt-in a keret env-feature-jére** (`EnvironmentFeatureEnabled`) — a keretben ez alapból kikapcsolt.
|
||||
2. **Delegát-minta a fogyasztói konstans-osztályon:** a `FruitBankConstClient` érintett tagjai átlátszó delegátok az
|
||||
|
||||
@@ -21,7 +21,7 @@ Accepted (2026-07-23) — a döntés lefixálva; végrehajtás a szállítmányt
|
||||
|
||||
## Decision
|
||||
|
||||
> **An ADR records reasoning, not the state, and binds nothing — `BASE:ARTIFACT_ROLES`.**
|
||||
> **An ADR records reasoning, not the state, and nothing in it is a rule; an open decision is the goal — `BASE:ARTIFACT_ROLES`.**
|
||||
|
||||
**A tétel↔kamion hozzárendelés külön mapping-táblába kerül; a `ShippingItem` érintetlenül egy tétel = egy sor marad.**
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ Felmerült.
|
||||
|
||||
## Decision
|
||||
|
||||
> **An ADR records reasoning, not the state, and binds nothing — `BASE:ARTIFACT_ROLES`.**
|
||||
> **An ADR records reasoning, not the state, and nothing in it is a rule; an open decision is the goal — `BASE:ARTIFACT_ROLES`.**
|
||||
|
||||
**A raklapszám tört értékű, a rekesz/raklap arányból kalkulált (de felülírható) mennyiség lesz; a mérésszám és a
|
||||
kamion-foglaltság ebből két külön képlettel áll elő, és a tételek tört maradékai a dokumentumon belül szabadon
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Status
|
||||
|
||||
Proposed (2026-08-18, rev. 2026-08-18, 2026-08-19, 2026-08-20, 2026-08-21, 2026-08-22, 2026-08-25, 2026-08-26,
|
||||
consolidated 2026-08-26)
|
||||
consolidated 2026-08-26, rev. 2026-08-27, 2026-08-28, 2026-08-29)
|
||||
|
||||
> Collector record: it holds the decisions of the consumer-side EKÁER process. A new decision enters as a
|
||||
> **revision**, on a dated `Rev.` line — not as a new file, because the process is understood as one whole.
|
||||
@@ -18,11 +18,17 @@ consolidated 2026-08-26)
|
||||
> | 2026-08-18 | the grid's surface — the detail's tabs, where the buttons sit, the per-tab layout |
|
||||
> | 2026-08-19 | `modify` completes the missing plate instead of `delete` + `create`; NAV's state stored in one field |
|
||||
> | 2026-08-20 | NAV's status gets a column; the detail shows one card, chosen by a selector; the modification's reason comes from the difference |
|
||||
> | 2026-08-21 | `EkaerStatus` splits into lifecycle × outcome, **before** `modify` is wired |
|
||||
> | 2026-08-22 | the declaration unit moves from the depot to the consignor; the below-threshold cross-border rule is retracted |
|
||||
> | 2026-08-21 | `EkaerStatus` splits into lifecycle × outcome — moved to `ADR 0006-V8N5` on 2026-08-29 |
|
||||
> | 2026-08-22 | the declaration unit moves from the depot to the consignor |
|
||||
> | 2026-08-25 | the division of a group belongs to `FruitMasterErp`, not to the gate |
|
||||
> | 2026-08-26 | merging the items that share a VTSZ is permitted, not required |
|
||||
> | 2026-08-26 | the own site becomes a list, a leg is keyed by the (load, unload) pair, and only the Hungarian border divides |
|
||||
> | 2026-08-26 | the declaration has a territory the gate does not check, and the validator's country rules are six |
|
||||
> | 2026-08-27 | the framework carries no own site: a leg's own-side address comes from the source, and nothing counts sites |
|
||||
> | 2026-08-27 | the `externalId` is the leg's load-endpoint id, and a leg needs an id and an address — no fallback |
|
||||
> | 2026-08-28 | a leg's endpoints carry an id on the NAV `LocationType`, and the contracts name the ends by role |
|
||||
> | 2026-08-28 | the customer reaches the mapper as a DTO, and the outbound country stops being hardcoded |
|
||||
> | 2026-08-29 | the address leaves the NAV type: a structured address in the framework, converted at the boundary |
|
||||
|
||||
## Context
|
||||
|
||||
@@ -31,17 +37,18 @@ the five operations. This record is about what **the product** does with a decla
|
||||
whose initiative.
|
||||
|
||||
**Where we stop today.** The process runs to `create`: the gate creates a row → generation + NAV pre-flight
|
||||
check → submission. The submission creates a `P` (Prepare) state at NAV, not a final state.
|
||||
check → submission. The submission creates an `S` (Start) state at NAV, not a final state.
|
||||
|
||||
**NAV's lifecycle** (`AyCode.Services/docs/NAV/EKAER_OPERATIONS.md`): `P → S → F`, where `F` is produced by the
|
||||
`finalize` operation; whatever **is not finalized in time becomes `I` (Inactive)**. Consequence: **every
|
||||
declaration we have submitted so far is heading for expiry**, and today nothing tells us about it. The grid's
|
||||
third tab, `Lezárt`, is empty by construction: nothing sets `Finalized`.
|
||||
third tab, `Lezárt`, is empty by construction: nothing finalizes a declaration.
|
||||
|
||||
**Measured facts from NAV's test system** (2026-08-17/18/19/25, the `AyCode.Services.Tests` sandbox suite):
|
||||
**Measured facts from NAV's test system** (2026-08-17/18/19/25 the `AyCode.Services.Tests` suite; 2026-08-31 and 2026-09-01 the product's own `FmEkaerNavGatewayAgainstNavTests`):
|
||||
|
||||
- `finalize` asks for the `tcn`, and for the **arrival time on the operation**, in its `arrivalDate` /
|
||||
`arrivalDateOnly` field (since 2.0 not in the tradeCard);
|
||||
`arrivalDateOnly` field. The card header carries the same pair, and the operation's is what `finalize`
|
||||
reads — the header's is not an alternative to it;
|
||||
- `delete` also asks for a reason (`statusChangeModReasonText`), otherwise `TC_MOD_REASON_MISSING`;
|
||||
- `modify` **is usable**, but asks for three things at once: an `itemOperation` per item, NAV's own `id` for the
|
||||
modified item, and likewise its own for the delivery plan. Any one missing → `INVALID_INPUT`;
|
||||
@@ -52,41 +59,65 @@ third tab, `Lezárt`, is empty by construction: nothing sets `Finalized`.
|
||||
envelope-level `OPERATION_FAILED`, meaning it does not even process the request. The items must be sent;
|
||||
- the `tcn` query **does not lag**: it returned the freshly created card immediately (two runs) — unlike the
|
||||
date-range listing, which used to refresh late;
|
||||
- the freshly created card came back immediately in state **`S` (Start)**, not `P`;
|
||||
- the freshly created card came back immediately in state **`S` (Start)**, not `P`: **a `P` cannot be created
|
||||
over the API** — it reaches us only from a query, on a card recorded on NAV's web portal;
|
||||
- the create response carries **`tcnValidityStart` / `tcnValidityEnd`** (fifteen days), and nothing stores the
|
||||
deadline;
|
||||
- NAV **upper-cases the location blocks** but not the header parties, so a field-level comparison must treat the
|
||||
location fields case-insensitively or every one of them reads as changed.
|
||||
location fields case-insensitively or every one of them reads as changed;
|
||||
- an **arrival earlier than the declaration's creation** is refused with `TcInvalidArrivaldate`, and NAV says no
|
||||
more than that. Measured on the creation day and the day before it; the card's loading date was later than
|
||||
both, so it is not the bound;
|
||||
- `arrivalDate` is an `xs:dateTime` **with a mandatory offset**: sent without one the request itself is refused,
|
||||
HTTP 400, before the operation is read. A date read back from the database has none;
|
||||
- a **finalized declaration refuses deletion** (`TcDeleteOnlyActive`), so `finalize` is terminal in fact and not
|
||||
only by intent: whatever finalizes cannot clean up after itself;
|
||||
- a `modify` **carrying no reason at all is accepted**, not rejected. Nothing at NAV stops a modification that
|
||||
changes nothing, so comparing the card against NAV's copy before sending is the only thing that does;
|
||||
- the plate must match `[A-Z0-9ÖŐÜŰ]{4,15}` — no separator, at least four characters;
|
||||
- `queryTradeCards` accepts a window of **at most 31 days**, and rejects `P` and `D` as status filters.
|
||||
|
||||
**A premise that proved false.** The framework's docs — and following them, this record's first version — claimed
|
||||
the item id could not be obtained. It was not missing: we never asked for it. Anyone who takes `queryTradeCards`
|
||||
for a summary query arrives here again.
|
||||
|
||||
**One field, two meanings.** `EkaerStatus` says at once where the declaration stands (`Sent`, `Finalized`) and
|
||||
what became of our last operation (`GeneratedWithWarning`, `ValidationError`). The two are independent, and
|
||||
fusing them causes measurable harm: modification requires regenerating the card, but generation overwrites the
|
||||
status, and with it takes away the fact that the declaration is at NAV. The resulting row is `IsSubmittable()`,
|
||||
so the next submission would create a SECOND declaration for the same carriage. The
|
||||
`FmEkaerServiceBase.ThrowIfAlreadyAtNav` guard is the scar of that squeeze — it exists because one field cannot
|
||||
preserve two things at once.
|
||||
|
||||
**Out of scope:** moving `EkaerStatus` and the status categories into the framework.
|
||||
|
||||
## Decision
|
||||
|
||||
> **An ADR records reasoning, not the state, and binds nothing — `BASE:ARTIFACT_ROLES`.**
|
||||
> **An ADR records reasoning, not the state, and nothing in it is a rule; an open decision is the goal — `BASE:ARTIFACT_ROLES`.**
|
||||
|
||||
**The user closes the declaration, from the grid, row by row.** Finalization is not automatic: the fact of
|
||||
unloading does not arise in the system by itself, and `finalize` is irreversible (after it, only `correction`,
|
||||
against a surcharge).
|
||||
|
||||
**The arrival time comes from existing domain data:** inbound the `Shipping.MeasuredDate`, outbound the
|
||||
`Order.DateOfReceipt`. Both are an **approximation** of the actual unloading, deliberately so: weighing happens
|
||||
after unloading, and `DateOfReceipt` is the day of receipt. We do not send an invented value (the moment the
|
||||
button was pressed, the record's creation) — that would put false data into the declaration.
|
||||
**The three source dates, because their names do not say it** (rev. 2026-08-30).
|
||||
`ShippingDocument.ShippingDate` is the delivery note's date — the day of loading. `Shipping.ShippingDate` is the
|
||||
agreed day the truck arrives: it is what the note shows and what the customer is given when the transport is
|
||||
ordered. `Shipping.MeasuredDate` is the close of weighing, written by the data layer when every item is measured.
|
||||
None of the three says this on its own member, which is why the choice below was reopened three times.
|
||||
|
||||
**We complete the missing licence plate with `modify`** (rev. 2026-08-19). A declaration in `SentWithMissingData`
|
||||
cannot be finalized while the plate is missing. The route: NAV's delivery-plan and item ids onto the generated
|
||||
**`loadDate` goes on the card, the arrival does not** (rev. 2026-08-30). The loading date is knowable at
|
||||
generation — inbound the delivery note's, outbound the order's `DateOfReceipt` — and its absence is a generation
|
||||
error naming the delivery note. The arrival cannot be known then: the declaration is filed before the carriage
|
||||
starts. It reaches NAV on the `finalize` operation, which since 2.0 carries its own.
|
||||
|
||||
**The arrival is `Shipping.MeasuredDate`, not the agreed day** (rev. 2026-08-30). Finalization asserts to NAV that
|
||||
the goods arrived and were unloaded; the agreed day would let that be asserted for a truck that never came. The
|
||||
close of weighing is the event, and it is written automatically, so nobody types it. The item measurements cannot
|
||||
supply it instead — `ShippingItemPallet` has no measurement timestamp, only `Modified`, which moves on any edit.
|
||||
Outbound the `Order.DateOfReceipt` stands: that is when the buyer comes for the goods. Neither is the unloading
|
||||
moment to the minute, and neither is invented.
|
||||
|
||||
**The arrival is stored on the declaration** (rev. 2026-08-30). `EkaerHistory` carries it as a snapshot beside
|
||||
`ShippingDate` and `Partner`, filled from two places: at generation, because a row created retroactively
|
||||
(`CreateMissingEkaerHistories`) may already have a closed measurement, and by the same data-layer hook that sets
|
||||
`MeasuredDate`, so a truck arriving after generation reaches the declaration without anyone regenerating.
|
||||
Without the column the grid cannot tell which row is ready to close, and the button stands enabled on rows that
|
||||
are not. Two write rules: only where the value differs, so an idle update does not bump `Modified` into looking
|
||||
like an event that did not happen; and never once `NavStatus` is `F`, where the snapshot has to keep matching
|
||||
what was sent.
|
||||
|
||||
**We complete the missing licence plate with `modify`** (rev. 2026-08-19). A declaration that went out carrying a
|
||||
`Warning` outcome — the plate missing — cannot be finalized until the plate is there. The route: NAV's delivery-plan and item ids onto the generated
|
||||
card along the `externalId`s → `modify` with the plate and the per-field reason. This branch is chosen because
|
||||
**the declaration's number survives**: `delete` plus a fresh `create` would give a new `tcn`, which by then has
|
||||
been written onto the waybill.
|
||||
@@ -167,66 +198,10 @@ tabs show different columns and different content; a shared width, sort order an
|
||||
the process is readable end to end from the grid. The risk owned with it: the surface promises a finished process
|
||||
whose NAV-side half does not work yet.
|
||||
|
||||
### The two axes of the status (rev. 2026-08-21)
|
||||
### The two axes of the status — moved out (rev. 2026-08-21, moved 2026-08-29)
|
||||
|
||||
**`EkaerStatus` splits in two: lifecycle and outcome.** This is not a new model — today's eight values **decompose
|
||||
onto it without remainder**, so it is the statement of what is already there. `GeneratedWithWarning` and
|
||||
`SentWithMissingData` are literally compound names: lifecycle plus outcome in one word.
|
||||
|
||||
| Lifecycle | Outcome | Today | What to do |
|
||||
|---|---|---|---|
|
||||
| `NotSent` | `None` | `Pending` | Generate |
|
||||
| `NotSent` | `Ok` | `Generated` | submit |
|
||||
| `NotSent` | `Warning` | `GeneratedWithWarning` | submit |
|
||||
| `NotSent` | `Error` | `ValidationError`, `SendError` | fix + Generate |
|
||||
| `Sent` | `Ok` | `Sent` | finalize |
|
||||
| `Sent` | `Warning` | `SentWithMissingData` | Generate + modify |
|
||||
| `Sent` | `Error` | — no value for it | fix + Generate + modify |
|
||||
| `Finalized` | `Ok` | `Finalized` | — |
|
||||
|
||||
The eighth row is empty in today's column: for a failed modification there is **no state today**. Without it the
|
||||
modify branch cannot be represented.
|
||||
|
||||
**The lifecycle has three values, not four.** `Modified` came up and fell: modification can happen several times,
|
||||
and a modified row belongs where a submitted one does — at NAV, awaiting finalization. What can be entered
|
||||
repeatedly and changes nothing about what is allowed is not a state. If the fact that we touched it is needed,
|
||||
that is a date or a counter.
|
||||
|
||||
**`ValidationError` and `SendError` land in one cell.** The lifecycle gives them their meaning: `NotSent` +
|
||||
`Error` = it never went out; `Sent` + `Error` = the modification failed, the declaration is intact at NAV.
|
||||
Distinguishing them does nothing today either — `CanSend` is false for both, so both require regeneration — and
|
||||
the "why" stays in `ErrorText`, with a per-line `[Error]` / `[Warning]` prefix.
|
||||
|
||||
**Two rules that must be stated, or the model does not hold:**
|
||||
|
||||
- **The outcome is the result of the LAST OPERATION, not the card's validity.** Submitting a card generated as
|
||||
`Ok` can fail: the row becomes `NotSent` + `Error` while the card is valid. This is correct — what matters is
|
||||
what to do — but anyone reading it as "the card's validity" misreads it.
|
||||
- **`Lifecycle == Sent` ⟺ `EkaerNumber != null`.** The model's single redundancy; the submission writes both. As
|
||||
an invariant it is fine, unstated it drifts apart, and from then on nobody knows which one governs.
|
||||
|
||||
**`NavStatus` stays where it is** (`P`/`S`/`F`/`I`/`D`), and **the disagreement between the two is information
|
||||
too**: if our lifecycle is `Sent` and NAV's is `I`, that is the expiry nothing reports today.
|
||||
|
||||
**The zero default stays correct:** `NotSent = 0` and `None = 0`, so a freshly inserted row is exactly today's
|
||||
`Pending`.
|
||||
|
||||
**Settled before implementation:**
|
||||
|
||||
- **`StatusId` stays**, and a second column joins it for the other axis. The alternative — a new name for each,
|
||||
with `StatusId` retired — would make a row that missed the backfill impossible to misread; that safety is
|
||||
given up for the cheaper migration. The consequence is that the backfill has to be **complete**, not merely
|
||||
attempted: a missed row reads as a valid state rather than as an error.
|
||||
- **No wipe.** Regenerating the rows is not the difficulty — the mapping is. `EkaerHistoryMapping.ForeignKey`
|
||||
carries its meaning implicitly, and the source id ranges overlap, so a stale row resolves to a *different*
|
||||
source record instead of failing (`MGFBANKPLUG-EKAER-I-D8V3`). The backfill keeps the existing rows, and the
|
||||
database step waits.
|
||||
|
||||
**Still open:**
|
||||
|
||||
- **Which axis `StatusId` carries** — the lifecycle, leaving the outcome to the new column, or the reverse.
|
||||
- **`EkaerHistoryFilter`.** With the tabs keyed on the lifecycle, the flags enum may simply be the lifecycle enum;
|
||||
whether the wire contract keeps a separate type is undecided.
|
||||
The split has its own record: **`ADR 0006-V8N5`**. It reaches every consumer of `EkaerStatus` across three repos,
|
||||
and nothing in it depends on the decisions here — while `modify` and `finalize` below depend on it, and wait for it.
|
||||
|
||||
### The declaration unit (rev. 2026-08-22)
|
||||
|
||||
@@ -234,25 +209,25 @@ too**: if our lifecycle is `Sent` and NAV's is `I`, that is the expiry nothing r
|
||||
`(ShippingId, PartnerId, PartnerDepotId)` — one card and one EKÁER number per depot. The NAV schema models it
|
||||
otherwise: `TradeCardType` carries `deliveryPlans` with `maxOccurs="unbounded"`, and each `DeliveryPlanType` holds
|
||||
its own `loadLocation`, `unloadLocation` and `items`. The addresses left the card header in schema 1.8, and the
|
||||
list was added for this case (`TCDEV-335`). 13/2020 PM §12 (4) states the same rule: one EKÁER number is to be
|
||||
established even when one vehicle carries from several loading addresses of one consignor to one consignee.
|
||||
list was added for this case (`TCDEV-335`). The regulation says the same in two sentences: **§12 (1)** — *„Az EKAER
|
||||
szám azon termékegységet azonosítja, amelyet **egy feladótól egy címzett részére, egy gépjárművel** fuvaroznak"* —
|
||||
makes the unit a triple, consignor → consignee → vehicle; **§12 (4)** adds that several loading addresses **of one
|
||||
consignor** stay under that one number. A vehicle carrying for three consignors therefore yields three numbers, not
|
||||
one: the triple is what a card may not span.
|
||||
|
||||
**The threshold follows the unit.** Two 300 kg loads from two depots of one partner onto one vehicle each stay
|
||||
below the 500 kg threshold while the statutory unit is 600 kg.
|
||||
|
||||
**The customer's below-threshold cross-border rule is retracted.** A cross-border relation was declared whatever
|
||||
the weight — the customer's explicit request, confirmed 2026-07-10, stricter than the regulation. It is dropped:
|
||||
NAV fines over-reporting as it fines under-reporting, so the tightening bought no safety, and
|
||||
`FMERP-EKAER-I-Q4N8` records that it sat in framework code where a second product inherits it. Steps (2) and (3)
|
||||
of `FmEkaerReportability.Evaluate` go with it, and the obligation decision becomes country-free.
|
||||
**The obligation decision is country-free.** 13/2020 PM 4. § i) exempts carriage below the threshold on the
|
||||
consignor, the consignee and the vehicle — no country and no direction qualify it — so
|
||||
`FmEkaerReportability.Evaluate` weighs the aggregated weight and value and nothing else. **The minimum is also the
|
||||
maximum:** NAV fines over-reporting as it fines under-reporting, so declaring more than the regulation asks is not
|
||||
caution — it produces exposure of its own, and the decision has to be exact rather than generous.
|
||||
|
||||
**The two changes are one decision.** The cross-border rule is what masked the threshold gap: two below-threshold
|
||||
depot groups from a foreign partner were `Required` only because they were cross-border. Retracting it alone would
|
||||
have turned over-reporting into under-reporting.
|
||||
|
||||
**The seller stays the legal entity.** `PartnerDepot.TaxId` is a computed passthrough (`=> Partner?.TaxId`,
|
||||
`NotColumn`, `NotMapped`) that exists to satisfy `ICompanyInfoBase`; a depot is not a separate legal entity and
|
||||
carries no tax number of its own. `sellerCountry` and `sellerVatNumber` stay the Partner's.
|
||||
**The seller is the legal entity, and a site never carries a tax number.** We trade with the Partner, not with
|
||||
its depot: a depot is a place, not a party. `sellerCountry` and `sellerVatNumber` stay the Partner's.
|
||||
`PartnerDepot.TaxId` reads through to it (`=> Partner?.TaxId`, `NotColumn`, `NotMapped`) — a mechanism forced by
|
||||
`ICompanyInfoBase` fusing identity with address, never a licence to give a site an identity of its own.
|
||||
|
||||
### Where the division lives (rev. 2026-08-25)
|
||||
|
||||
@@ -313,73 +288,109 @@ not fill; our allocation id there would give an inspector a handle back into our
|
||||
with no country qualifier, and 4. § i) aggregates the threshold on *„ugyanazon feladótól ugyanazon címzett részére
|
||||
ugyanazon gépjárművel"* — consignor, consignee, vehicle, no site and no country. The only force that divides is
|
||||
NAV's, and it is narrower than a country: under `I` a load location may not be Hungarian
|
||||
(`TC_LOAD_LOCATION_CANT_BE_HUNGARY`). **The key is therefore two-valued.** One consignor's German and Dutch sites
|
||||
belong on one card.
|
||||
|
||||
Splitting per country costs twice: two EKÁER numbers where the statute says one, and — because the threshold
|
||||
follows the card — 300 kg from Germany plus 300 kg from the Netherlands fall below the threshold twice where
|
||||
600 kg on one card is reportable. That is under-reporting, the same gap the depot-level unit opened, reopened on
|
||||
the country axis.
|
||||
(`TC_LOAD_LOCATION_CANT_BE_HUNGARY`). **The key is therefore two-valued**, and splitting per country costs twice:
|
||||
two EKÁER numbers where the statute says one, and — because the threshold follows the card — 300 kg from Germany
|
||||
plus 300 kg from the Netherlands falling below the limit twice, where 600 kg on one card is reportable.
|
||||
|
||||
**A premise that proved false.** A draft of this revision cited the regulation as covering goods arriving *„több
|
||||
tagállamából"* in one movement. The text says *„más tagállamából"* (verified against the Nemzeti Jogszabálytár). The
|
||||
conclusion stands without it, on §12 (4) and 4. § i) alone — but anyone re-deriving it from that phrase will not find
|
||||
it there.
|
||||
|
||||
**The declarer's own site is a list.** `EkaerCompanyInfo.Site` holds a single `LocationType` while the schema gives
|
||||
every delivery plan its own load/unload pair. It becomes `Sites`, with **no `Site` beside it**: two fields for one
|
||||
concept is a redundancy someone has to keep in sync, and the compatibility it would buy is worth nothing with one
|
||||
consumer, inside the workspace. A declarer with one site configures one element.
|
||||
**A site catalogue in the framework** (retracted, rev. 2026-08-27): the declarer's sites as a list on the
|
||||
framework's own config. Nothing above the leg needs the declarer's site — a leg carries two addresses, and which one
|
||||
is ours depends on the direction, not on identity. What the framework needs about us is the **party**; a party is
|
||||
not a place, and the two were only ever conflated because one product had one site.
|
||||
|
||||
**A leg is keyed by the pair** — inbound `(partner depot, own site)`, outbound `(own site, customer site)`. With one
|
||||
own site the pair degenerates to the depot key and the loop runs once, so the shape is built now and exercised
|
||||
later. A loop that never runs twice is not structure, so the mapper's test matrix carries at least one multi-leg
|
||||
case even though no product produces one.
|
||||
**A frozen own-side address** (rejected, rev. 2026-08-27): editing a site later must not rewrite last year's
|
||||
declaration. It cannot — the submitted card stands in `XmlDoc` and NAV's copy in `NavXmlDoc`. Freezing it a second
|
||||
time guards a locked door, and drags in a mandatory column, a null rule and a backfill.
|
||||
|
||||
**Which own site is source data, and the layer's contract carries it.** `IFmShippingDocument` and `IFmOrderDto` gain
|
||||
a nullable `OwnSiteKey` — one concept, direction-independent: inbound it lands on the unload, outbound on the load,
|
||||
so it is not named for either. Null resolves to the single element; null against several sites is a **`DataError`**,
|
||||
named — the class the gate already uses for data a person can fix, distinct from the `InvalidOperationException`
|
||||
that says the caller grouped wrongly. Never a silent first element. What the keys look like is settled with the
|
||||
field; the admin surface that sets them is not this decision.
|
||||
**`depot ?? partner`** (rejected, rev. 2026-08-27): it puts the consignor's registered address where a site's
|
||||
belongs and leaves the leg with an address but no id — a card that looks complete and cannot be modified.
|
||||
|
||||
**`deliveryPlan.externalId` encodes the pair from the start.** The field is `SimpleText50Type`, free text, and NAV
|
||||
derives nothing from it — its only duty is pairing a response to the request, and `modify` addresses plans by it.
|
||||
Encoding the pair now means a second site later changes values, not shape; encoding the depot id alone would force a
|
||||
shape change, and a shape change breaks exactly those declarations that are open across it.
|
||||
**The country rules belong to the framework validator, and there are six of them.** `common.xsd`'s `ReasonCodeType`
|
||||
names one per party per direction:
|
||||
|
||||
**`ResolveTradeType` derives per leg and requires agreement.** It takes the first non-empty load and unload country,
|
||||
which silently picks one when the legs disagree. The load side has a guard, the unload side has none. One rule
|
||||
replaces both: the legs of a card agree on direction, or the card cannot be built.
|
||||
| `tradeType` | `sellerCountry` | `destinationCountry` | location |
|
||||
|---|---|---|---|
|
||||
| `I` | not `HU` — `TC_SELLER_CANT_BE_HUNGARY` | `HU` — `TC_DESTINATION_MUST_BE_HUNGARY` | load not `HU` — `TC_LOAD_LOCATION_CANT_BE_HUNGARY` |
|
||||
| `E` | `HU` — `TC_SELLER_MUST_BE_HUNGARY` | not `HU` — `TC_DESTINATION_CANT_BE_HUNGARY` | unload not `HU` — `TC_UNLOAD_LOCATION_CANT_BE_HUNGARY` |
|
||||
|
||||
**The three Hungarian rules belong to the framework validator.** `common.xsd` names them — `I` ⇒ `sellerCountry`
|
||||
not `HU`, `E` ⇒ `sellerCountry` `HU`, `I` ⇒ `loadLocation.country` not `HU` — and `EkaerTradeCardValidator` encodes
|
||||
none of them; it checks only that `sellerCountry` is present. A rule that exists to pre-empt a NAV rejection belongs
|
||||
where the card is validated, not improvised in one mapper. Their absence is why the mapper grew a country guard at
|
||||
all.
|
||||
An earlier draft of this revision counted three — the import half — and anyone re-deriving the set from it will
|
||||
build a validator blind to export.
|
||||
|
||||
**One consequence they expose, unresolved:** a Hungarian consignor loading at its own foreign site yields `I` with
|
||||
`sellerCountry = HU`, which NAV rejects. Either the direction or the seller country is wrong for that case, and
|
||||
which one depends on the consignor's VAT registration — data the model does not hold. No such consignor exists in
|
||||
this product; the matrix records the behaviour rather than deciding it.
|
||||
|
||||
**Where these decisions live.** The validator rules are `AyCode.Services`; the leg key, the division rule and
|
||||
`OwnSiteKey` are `FruitMasterErp`. None of it is this product's — the customer has one site and says it will stay
|
||||
one; the shape is built now for the next product on the wholesale layer. The record stays here for the reason the
|
||||
status split does: the layer's pull-up is in progress, and the records belonging to it move together at the end.
|
||||
**Where these decisions live.** The validator rules are `AyCode.Services`; the leg key, the division rule and the
|
||||
leg's own-side address are `FruitMasterErp`. The record stays here for the reason the status split does: the
|
||||
layer's pull-up is in progress, and the records belonging to it move together at the end.
|
||||
|
||||
### A leg's endpoints carry an id, on the NAV type (rev. 2026-08-28)
|
||||
|
||||
**Ours is `-1`, and may be renumbered.** Real site ids are positive, `0` means "not set". Nothing stores it — every
|
||||
generation writes it from configuration — so a renumbering migrates nothing. One condition, live only once the
|
||||
`externalId` encodes our side: it then stands in the plans NAV holds, and renumbering breaks `modify` for cards
|
||||
still **modifiable** (`NavStatus` `P` or `S`). Finalized, expired and deleted are not.
|
||||
|
||||
The id rides on `LocationType` in a hand-written partial, which the 2026-08-29 revision moves off that type
|
||||
together with the address.
|
||||
|
||||
### The customer reaches the mapper as a DTO (rev. 2026-08-28)
|
||||
|
||||
**Outbound cannot produce an export today.** The customer's country is hardcoded `HU` in three places —
|
||||
`CustomerEndpoint`, `BuildCustomerLocation` and `OrderSiteSnapshotBuilder` — so both ends of the outbound leg read
|
||||
`HU` and `ResolveTradeType` always answers `D`. A genuine export would go out declared as domestic, and the
|
||||
validator's two `E` rules would never fire.
|
||||
|
||||
**The country is one association away.** `Customer` carries `CountryId`; `CustomerDto` is linq2db-mapped over the
|
||||
same table, so the ISO code arrives as a navigation with the load, like every other. No service, nothing filled by
|
||||
hand.
|
||||
|
||||
**The customer therefore reaches the mapper as `CustomerDto`, implementing `ICompanyInfoBase`.** `CustomerEndpoint`
|
||||
and `BuildCustomerLocation` collapse into `PartyEndpoint` and `BuildLocation` — the two builders that already serve
|
||||
the consignor and the declarer.
|
||||
|
||||
**A premise that proved false.** The customer's country was taken to be unreachable from the mapping layer, which
|
||||
made the hardcode look forced. It is a column and an association away; what was missing is the DTO on the path, not
|
||||
the data.
|
||||
|
||||
**Open, and named rather than guessed:**
|
||||
|
||||
- `Country` is not in `Mango.Nop.Core`'s mirror. The association needs it, and adding a mirror entity stretches that
|
||||
project's promise of standing without the nopCommerce dependency chain.
|
||||
- `CustomerDto` is thin — `Username`, `Email`, the names, store and deleted flag. It carries none of
|
||||
`ICompanyInfoBase`'s members yet.
|
||||
- Two mappings differ today and one implementation forces a single answer: `Name` (`Company`, or `Company` falling
|
||||
back to first + last as the site snapshot does) and `Street` (with or without `StreetAddress2` — the party's
|
||||
address uses it, the location's does not).
|
||||
|
||||
### The address leaves the NAV type (rev. 2026-08-29)
|
||||
|
||||
**A generated NAV type sits on two domain contracts.** `IFmShippingDocument.UnloadLocation` and
|
||||
`IFmOrderDto.LoadLocation` carry `LocationType`, because nothing else here holds a structured postal address:
|
||||
`ICompanyInfoBase` fuses the address with a party and leaves the street unsplit, and `AcAddress` is a geocoding
|
||||
entity whose address is one free-text line.
|
||||
|
||||
**The direction: a structured address value type in the framework** — country, postcode, city, street parts, lot
|
||||
number, optional GPS, and an id. The contracts carry it, and the mapper converts to `LocationType` at the protocol
|
||||
boundary. `LocationType`'s `SiteId` partial goes with it: the leg's identifier was put on a NAV model only because
|
||||
that was the type the contracts carried.
|
||||
|
||||
**Not by inheritance** (rejected): `LocationType` is generated, so a base's members serialise into the card unless
|
||||
each is `[XmlIgnore]`d — a trap surviving every regeneration — and `AcAddress` is table-mapped, which would tie a
|
||||
wire message to persistence.
|
||||
|
||||
**Open:** whether the new type extends `AcAddress` or stands beside it, and whether `ICompanyInfoBase` is then a
|
||||
party plus one of these. The boundary is `ACCORE-EKAER-T-M8W4`'s: the same pass decides both.
|
||||
|
||||
### How the shape is proved (rev. 2026-08-26)
|
||||
|
||||
The decisions above build a shape no product exercises: one own site, one leg, one direction. A loop that never
|
||||
runs twice is not structure, so the proof is a **case matrix over the mapper**, and it carries cases the products
|
||||
do not have.
|
||||
|
||||
**The cases live in a dictionary, keyed by an enum.** One key is one named situation — a Hungarian consignor with
|
||||
two Hungarian depots, a Hungarian consignor with two German ones, two depots straddling the border — and the value
|
||||
is a record holding the case's input **factory** plus the values it must produce: the number of delivery plans, the
|
||||
`tradeType`, the aggregated weight and value, and the exception a case must raise instead.
|
||||
|
||||
Four properties make it a proof rather than a smoke test:
|
||||
The proof is a **case matrix over the mapper**, carrying cases no product's data supplies. Four properties make it
|
||||
a proof rather than a smoke test:
|
||||
|
||||
- **An enum key, not a string** — a typo is then a compile error, not a silently absent case.
|
||||
- **A factory, not a shared instance** — the mapper mutates what it is given (`depot.Partner ??= partner`), so one
|
||||
@@ -389,37 +400,57 @@ Four properties make it a proof rather than a smoke test:
|
||||
- **A case names the whole situation, not its parts.** Parts would need a different argument count per case, which
|
||||
a `DataRow`'s fixed arity cannot carry, and the row would stop being readable.
|
||||
|
||||
**Three small methods over one table, not one method with five assertions.** There is no soft assert, so a single
|
||||
method reports the first failure and hides the rest; split by aspect — plans, direction, threshold — the failure
|
||||
names which one broke.
|
||||
**The golden card is one aspect among them, not an exception.** Every scalar the table asserts passes while both
|
||||
legs' items sit on one plan; the whole card compared as text is what catches that. An earlier draft listed it under
|
||||
what the matrix cannot prove, and read that way it becomes a separate suite with its own fixture, the multi-leg
|
||||
graph written twice.
|
||||
|
||||
**What the matrix cannot prove, and where those live:**
|
||||
**What the matrix cannot prove, because the unit under test is a different one:**
|
||||
|
||||
- the validator's three Hungarian rules — a different unit, tested where the validator lives;
|
||||
- the validator's six country rules — a different unit, tested where the validator lives;
|
||||
- the grouping's composition, *one shipment with a Hungarian and a German site becomes exactly two cards, with
|
||||
these documents in each* — that decision sits in the gate, behind a database, and has no test home today. The
|
||||
matrix proves the halves: the key function, and that the mapper refuses a group spanning both;
|
||||
- that the leg count is right while the items sit on the wrong leg. A **golden card** — the whole XML compared as
|
||||
text — is the cheap complement, and one multi-leg case earns it.
|
||||
matrix proves the halves: the key function, and that the mapper refuses a group spanning both.
|
||||
|
||||
**A case describes a consignment, not a delivery note.** The row names legs and lines — load and unload, weight,
|
||||
value, VTSZ — and the **factory** builds the domain graph from that description, rather than the row describing
|
||||
documents and depots directly. The inversion is deliberate: the seam between this layer and the protocol layer sits
|
||||
too high (`ACCORE-EKAER-T-M8W4` — the consignment, the obligation threshold, the direction and the division are not
|
||||
any business domain's), and when it moves, the mapper's input becomes the consignment itself. Written this way only
|
||||
the factory falls away; written the other way round, the table would be rewritten with the seam.
|
||||
**A case should describe a consignment, not a delivery note** — legs and lines, with the factory building the
|
||||
domain graph from that description. Then only the factory falls away when `ACCORE-EKAER-T-M8W4` moves the card's
|
||||
construction down a layer. **As built it describes delivery notes**: the rows name allocations and depots, so they
|
||||
are rewritten with that move rather than surviving it. The reason is that entry's own first open question —
|
||||
whether the protocol layer takes a consignment type or a narrower contract is undecided, and a table shaped for a
|
||||
guessed answer is rewritten either way.
|
||||
|
||||
**The endpoints in a case have names, and the lines reference them.** The consignor, each of its sites and each of
|
||||
the declarer's own sites is a small spec carrying only what the mapper reads; a line says which one it loads at and
|
||||
which one it unloads at. Loose fields cannot state a pair, so a multi-leg case would not be describable at all —
|
||||
only producible by accident. A site's spec carries **no tax number**: it is a passthrough to the consignor's, and a
|
||||
fixture able to describe a state the real type cannot hold will eventually assert on one.
|
||||
**A site's spec carries no tax number**: it is a passthrough to the consignor's, and a fixture able to describe a
|
||||
state the real type cannot hold will eventually assert on one.
|
||||
|
||||
**Both suites are framework suites, in two repos.** The mapper matrix belongs to `FruitMasterErp.Core.Tests`,
|
||||
beside the type it exercises; the validator's rules belong to `AyCode.Services.Tests`, beside theirs. Neither is
|
||||
the product's: the products supply no case the matrix needs, and a test that only ran against one product's data
|
||||
would not prove the shape at all. The matrix is not held back for the layering pass either — it would leave the new
|
||||
shape unproven exactly while the shape is being built.
|
||||
**Both suites are framework suites, in two repos** — the mapper matrix in `FruitMasterErp.Core.Tests`, the
|
||||
validator's rules in `AyCode.Services.Tests`. Neither is the product's: no product supplies a case the matrix needs.
|
||||
|
||||
### The declaration has a territory (rev. 2026-08-26)
|
||||
|
||||
**Every case the regulation covers touches Hungary.** 13/2020 PM 2. § (1) names three and no fourth: (a) from
|
||||
another member state *belföldre*, (b) *belföldről* to another member state, (c) *belföldön*, the first taxable sale
|
||||
to a non-end-user. A carriage whose load and unload addresses are both foreign is outside the system entirely — not
|
||||
a domestic declaration, and not a mistyped one. `ResolveTradeType` has no answer for it: equal countries map to
|
||||
`D`, so two Dutch sites would yield a *domestic Hungarian* declaration for goods that never enter Hungary. The fix
|
||||
is not a better `tradeType`; it is that no card is built.
|
||||
|
||||
**A premise that proved false.** This revision counted on the framework suite recording today's behaviour as
|
||||
correct (`TheSameForeignCountryOnBothEndsIsThresholdedLikeDomestic`), so that flipping that record would be part of
|
||||
the work. The test went instead: once the country left `Evaluate`'s signature, nothing could make it fail, and it
|
||||
was removed as vacuous. The foreign-to-foreign case had to be written, not inverted.
|
||||
|
||||
**No product produces the case.** Inbound the unload is our own Hungarian site, outbound the load is. The gate is
|
||||
the framework's, and the next product on the layer inherits whatever it does with a case its own data never supplies.
|
||||
|
||||
**The validator's `D` counterpart is open, not decided** (rev. 2026-08-29). `common.xsd` names
|
||||
`TC_LOCATION_NOT_HUNGARY`, but its documentation is only *„Magyar címnek kell lennie!"* — no `tradeType` and no
|
||||
field, unlike every neighbouring code. It reads as the `D` rule, as import's unload rule, or as export's load rule.
|
||||
An earlier draft of this revision assumed the first; the validator's own remark asserts the schema names no code
|
||||
for `D` at all, which the enumeration contradicts. A sandbox measurement decides it.
|
||||
|
||||
**Two constraints of the domestic case stay open.** Point (c) is narrower than "both ends Hungarian": the sale must
|
||||
be to a **non-direct-end-user**, and it must be the **first** taxable one. The gate models neither, and the model
|
||||
holds no data for either. Whether a wholesale line can fail them is a business question, not this record's.
|
||||
|
||||
## Consequences
|
||||
|
||||
@@ -429,20 +460,7 @@ shape unproven exactly while the shape is being built.
|
||||
„Pótlásra/Véglegesítésre vár" counter shows real work.
|
||||
- Wiring `finalize` reveals how many past declarations can still be closed, and how many were lost.
|
||||
- The missing licence plate can be completed afterwards, and the declaration's `tcn` survives (rev. 2026-08-19).
|
||||
- The modification path opens: a row at NAV can be regenerated without losing the fact that it is at NAV.
|
||||
- The tab filter simplifies — the three tabs are purely a lifecycle question, an `IN` on a single column;
|
||||
`EkaerHistoryDbTable`'s precomputed `StatusId` arrays and the `StatusIdsWhere` machinery can go.
|
||||
- Later operations bring no new vocabulary: `Lifecycle × Outcome` yields the cells by itself.
|
||||
- The threshold aggregates on the statutory unit, on both the depot and the country axis; one consignor's sites in
|
||||
several foreign countries stay on one card and sum against one threshold (rev. 2026-08-22, 2026-08-26).
|
||||
- The obligation gate loses its country branch, and with it `FMERP-EKAER-I-Q4N8` and `MGFBANKPLUG-EKAER-I-F5D9`
|
||||
(rev. 2026-08-22).
|
||||
- One EKÁER number per consignor per vehicle: fewer declarations to finalize, and fewer to watch for expiry.
|
||||
- `modify` addresses a delivery plan rather than a whole card, which is the shape the schema already has.
|
||||
- Several own sites need no further shape: the list, the pair key and the `externalId` encoding are in place, so the
|
||||
later work is a selector and an admin surface, not a migration (rev. 2026-08-26).
|
||||
- The validator refuses what NAV would refuse, in the layer that owns the protocol — so a mapper no longer has to
|
||||
guess a NAV rule, and every product on the layer inherits the check (rev. 2026-08-26).
|
||||
|
||||
**Negative:**
|
||||
|
||||
@@ -450,20 +468,8 @@ shape unproven exactly while the shape is being built.
|
||||
- The manual trigger means it **can be skipped** — the risk of expiry stays with the user while the `Inactive`
|
||||
question is open.
|
||||
- After `Finalized` the data can only be amended against a surcharge; finalizing early costs more than late.
|
||||
- The status split is a **schema change**: two columns plus a backfill, retiring the old `StatusId`. The mapping
|
||||
of the eight values is total, so the backfill is closed — but the database has to be touched.
|
||||
- The change reaches every consumer of `EkaerStatus` and its partitions, across three repos: the enum and its
|
||||
helpers, the entity, both server-side services, the query table, the controller's endpoints, the grid and the
|
||||
tests. Measure the set before starting — it is larger than the enum's own file suggests, and
|
||||
`EkaerHistoryDbTable` carries the densest use. Every piece gets smaller, but it has to happen in one pass.
|
||||
- One redundancy remains (`Sent` ⟺ `EkaerNumber`), to be held as an invariant rather than derived.
|
||||
- Declarations created under the depot-level unit are not migrated — the change applies to what is created after
|
||||
it lands, so the two shapes coexist in the table (rev. 2026-08-22).
|
||||
- `EkaerCompanyInfo.Site` disappears rather than gaining a sibling, so every call site changes at once — cheap with
|
||||
one consumer, and the reason not to defer it (rev. 2026-08-26).
|
||||
- Two nullable members enter the layer's contracts (`OwnSiteKey`) that no product fills yet. Unset they resolve to
|
||||
the single site, so they are inert — but a contract carrying an unused member invites the next reader to ignore it
|
||||
(rev. 2026-08-26).
|
||||
- The pair key cannot be told apart from the depot key while a product has one own site; the matrix proves it only
|
||||
because the fixture may configure several sites the product does not have (rev. 2026-08-26).
|
||||
- A Hungarian consignor with a foreign own loading site produces a card NAV rejects, and this record does not decide
|
||||
@@ -471,17 +477,9 @@ shape unproven exactly while the shape is being built.
|
||||
|
||||
**Follow-ups required:**
|
||||
|
||||
- **Splitting the status onto two axes — BEFORE `modify`.** The core of modify is regeneration, which today's
|
||||
form forbids; done afterwards, modify would get a detour that then has to be thrown away. With it,
|
||||
`ThrowIfAlreadyAtNav` keys on the lifecycle: on a `Sent` row *create*-generation is forbidden, modify-generation
|
||||
is allowed. The invariant (`Sent` ⟺ `EkaerNumber`) gets a test. **`tcnValidityEnd` rides the same migration** —
|
||||
it is a column on the same entity, and a second migration for one date is waste.
|
||||
- **The pair, the list and the border key** (rev. 2026-08-26), in this order because each rests on the one before:
|
||||
`EkaerCompanyInfo.Sites`; `OwnSiteKey` on `IFmShippingDocument` and `IFmOrderDto` with the null rule; the leg key
|
||||
and the division rule on the pair, the border key replacing the country key; the `externalId` encoding;
|
||||
`ResolveTradeType` per leg with agreement required; the three Hungarian rules in `EkaerTradeCardValidator`. Then
|
||||
the case matrix in `FruitMasterErp.Core.Tests` — at least one multi-leg and one mixed-direction case, plus a
|
||||
golden card over a multi-leg one — and rows for the three Hungarian rules in `AyCode.Services.Tests`.
|
||||
- **The customer as a DTO on the mapping path**: `Country` into the mirror, `CustomerDto` widened to
|
||||
`ICompanyInfoBase` with the country as an association, `FmOrderDtoBase.Customer` retyped, and the two
|
||||
customer-specific builders collapsed. It is what makes an outbound `E` possible at all.
|
||||
- `modify` at the NAV boundary: the delivery-plan and item ids onto the card along the `externalId`s, the differing
|
||||
fields reasoned against `NavXmlDoc`, then the `modify`; the grid's button and the dialog. **Measure first whether
|
||||
the `queryTradeCards` round trip is needed at all** — the ids are already in the stored create response, and the
|
||||
@@ -493,102 +491,42 @@ shape unproven exactly while the shape is being built.
|
||||
- Transaction coverage of the EKÁER writes: the delete endpoint (`EkaerHistory` plus mappings) and the
|
||||
`finalize` status write into `TransactionSafeAsync`. `DATA_LAYER.md`'s transaction claim is to be corrected
|
||||
along with it.
|
||||
- Stating the `EkaerStatus` ↔ `TradeCardStatusType` correspondence in the documentation. The assumed
|
||||
`Sent` = NAV `P` **needs measuring**: on the test system the freshly created card came back as `S` immediately.
|
||||
- The status lifecycle description in `Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md` stops at `Sent` today —
|
||||
when finalization lands, it has to become current fact there.
|
||||
- The status lifecycle description in `Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md` carries no finalization step —
|
||||
when `finalize` lands, it has to become current fact there.
|
||||
- The obligation gate's description in both `docs/EKAER/README.md` files (the plugin's and FruitMasterErp's) states
|
||||
the country-keyed grouping as current fact; it has to catch up when the border key lands.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**The trigger — automatic finalization when weighing closes** (rejected): closing the weighing is not the same as
|
||||
unloading, and `finalize` is irreversible. A mistaken automatism causes a `correction` with a surcharge.
|
||||
|
||||
**The trigger — a scheduled sweep** (rejected): the same problem, plus the user does not see what happened in
|
||||
their name.
|
||||
|
||||
**Arrival time — the moment the button is pressed** (rejected): hours or days can pass between unloading and
|
||||
closing; the field asks for the actual unloading.
|
||||
|
||||
**Arrival time — the user types it into the dialog** (rejected *for now*): it could be more accurate, but it
|
||||
wants a new input surface and validation. If `MeasuredDate` proves wrong in practice, this is the next step.
|
||||
|
||||
**The missing plate — header-only `modify`** (rejected, measured 2026-08-19): dropping the item list, NAV answers
|
||||
with an envelope-level `OPERATION_FAILED`, meaning it does not even process the request. The chosen branch does
|
||||
not need it either — the items go out together with their ids.
|
||||
|
||||
**The missing plate — `delete` plus a fresh `create`** (rejected): it works, but **it gives a new `tcn`**. The old
|
||||
number has by then been written onto the waybill, and `delete` also asks for a separate reason.
|
||||
|
||||
**The missing plate — making the plate mandatory before submission** (rejected): it would eliminate the
|
||||
`SentWithMissingData` state, in exchange for holding back the declaration even when NAV permits completing it
|
||||
later (the plate may be given up to the start of loading).
|
||||
|
||||
**The status model — extending the existing enum** (rejected): `ModifyReady` / `ModifyValidationError` appended at
|
||||
the end. Its argument was real: the enum is append-only, so this is the sanctioned path, there is no migration,
|
||||
and the tab filter's precomputed int array keeps working **unchanged**, because the predicate still depends only
|
||||
on the enum. Against it: the card's validity has to be stated twice — the modify branch rewrites the create
|
||||
branch's vocabulary — and every further operation multiplies it again.
|
||||
|
||||
**The status model — a derived phase, from expressions** (rejected): an extension bound to `IFmEkaerHistory`
|
||||
computes the phase from the existing fields, without storing it. Its pitfall is the query: the tab filter
|
||||
translates to SQL, an extension does not — `GetByFilter` would have received a second, hand-maintained truth,
|
||||
exactly what today's code comment guards against ("Felsorolva a kimaradása néma lenne" — enumerated, its omission
|
||||
would be silent).
|
||||
|
||||
**Per-tab button columns, with `Visible` set from code** (rejected): the code, the user's Column Chooser and the
|
||||
restored layout would all grab the same switch — and the last of those overwrites it on the next load.
|
||||
|
||||
**Every button on every tab, governed only by `Enabled`** (rejected): on the „Lezárt" tab five disabled buttons
|
||||
would line up. The rule's place is right (the status), its presentation is not.
|
||||
|
||||
**The declaration unit — depot-level numbers with a partner-level threshold** (rejected): it borrows the statutory
|
||||
unit for the threshold and drops it for the number of declarations. §12 (4) governs both in one sentence, so
|
||||
honouring half of it is not a reading of the regulation.
|
||||
**The source documents' ids as the `externalId`** (rejected, rev. 2026-08-27): readable at an inspection, and each
|
||||
id is a stable primary key. Against it: the *set* moves when a delivery note joins or leaves the truck, while NAV
|
||||
still holds the string first sent — so the match fails at the one moment it is needed. The readable list lives on
|
||||
the grid instead (`SourceRecordIdsText`).
|
||||
|
||||
**The declaration unit — keeping the depot key and retracting only the cross-border rule** (rejected): the
|
||||
retraction removes what masks the threshold gap. Two 300 kg depot groups would become `NotRequired` where the
|
||||
statutory unit is 600 kg — over-reporting turned into under-reporting.
|
||||
|
||||
**Keeping the customer's cross-border rule** (rejected): it is not statutory, NAV fines over-reporting, and it
|
||||
lives in framework code where a second product inherits it with no way to decline.
|
||||
|
||||
**The division split in the gate** (rejected, rev. 2026-08-25): grouping in the gate needs no change in the mapping
|
||||
layer at all, which is its whole appeal. Against it: a NAV protocol rule would sit in one consumer, where the next
|
||||
product on the layer reimplements it. The gate still has to know the division to create its rows; asking the layer
|
||||
for it costs one call and leaves the rule in one place.
|
||||
|
||||
**Dividing per load country** (retracted, rev. 2026-08-26): the narrower border key is not a refinement of it but a
|
||||
correction — the regulation keys on neither the site nor the country.
|
||||
|
||||
**Keeping `Site` beside `Sites`, as the default** (rejected, rev. 2026-08-26): every existing call site would keep
|
||||
working and a one-site declarer would configure nothing new. Against it: two fields for one concept, which somebody
|
||||
has to keep in sync and every reader has to ask which one governs. The compatibility is worth nothing here — one
|
||||
consumer, in this workspace — and the fallback would be the single-site assumption surviving inside the framework,
|
||||
which is the thing being removed.
|
||||
|
||||
**Wiring `Sites[0]` at the mapper** (rejected, rev. 2026-08-26): fewer moving parts than a key on the contracts.
|
||||
Against it: the framework would carry a single-site assumption while claiming not to, and the later selector would
|
||||
have to find every place that assumed it. With the choice made by the caller, the assumption sits at one boundary.
|
||||
|
||||
**The Hungarian rules in the mapper** (rejected, rev. 2026-08-26): the mapper already guards the load country, so
|
||||
the seller rules could join it. Against it: they are NAV's rules, enumerated in the schema the framework's validator
|
||||
was built from, and a second product on the layer would inherit a mapper's improvisation instead of a validated
|
||||
card. The mapper's own guard exists only because the validator has none.
|
||||
**An id on `ICompanyInfoBase`** (rejected, rev. 2026-08-28): every party would have one, uniformly. Against it: the
|
||||
interface's value is that a configuration object *without* identity can implement it, and it already carries two
|
||||
meanings — a `PartnerDepot` implements it to hold an address and must fake a tax number. Where identity is needed
|
||||
the composition exists: `IFmPartnerBase : ICompanyInfoBase, IEntityInt`.
|
||||
|
||||
## Related
|
||||
|
||||
- Related ADRs: `ADR 0003` (the declaration unit: allocation → truck)
|
||||
- Related ADRs: `ADR 0003` (the declaration unit: allocation → truck), `ADR 0006-V8N5` (the status axes, which
|
||||
`modify` and `finalize` wait for)
|
||||
- Related TODOs/Issues: `FBANKAPP-EKAER-T-R9D4` (anchor), `FBANKAPP-EKAER-T-K3P7` (structured house number),
|
||||
`MGFBANKPLUG-EKAER-I-T7M3` (the source of the outbound date), `MGFBANKPLUG-EKAER-T-N4E7`
|
||||
(delete / regeneration), `MGFBANKPLUG-EKAER-T-H7F2` (the declaration unit, server side),
|
||||
`MGFBANKPLUG-EKAER-T-R6P2` (direction from the physical route), `MGFBANKPLUG-EKAER-I-F5D9` and
|
||||
`FMERP-EKAER-I-Q4N8` (the retracted cross-border rule), `ACCORE-EKAER-T-P3K7` (the declarer's own site
|
||||
as a list — the framework half of the 2026-08-26 revision), `ACCORE-EKAER-T-M8W4` (the layering pass: what
|
||||
`MGFBANKPLUG-EKAER-T-R6P2` (direction from the physical route), `ACCORE-EKAER-T-P3K7` (the declarer's own site
|
||||
as a list — **retracted** by the 2026-08-27 revision: the framework holds no site at all), `ACCORE-EKAER-T-M8W4` (the layering pass: what
|
||||
belongs to the protocol layer and does not sit there yet — deferred, and the reason the case record is
|
||||
consignment-shaped)
|
||||
- External references: the NAV schema in AyCode.Core — `AyCode.Services/docs/NAV/ekaermanagement.xsd`
|
||||
(`TradeCardType`, `DeliveryPlanType`, `DeliveryPlanListType`) and `common.xsd`
|
||||
(`TradeCardType` — the complexType that carries `deliveryPlans`, not the `S`/`N` enum of the same name in
|
||||
`common.xsd` —, `DeliveryPlanType`, `DeliveryPlanListType`) and `common.xsd`
|
||||
(`TC_LOAD_LOCATION_CANT_BE_HUNGARY`, `TC_SELLER_CANT_BE_HUNGARY`)
|
||||
- External references: `AyCode.Services/docs/NAV/EKAER_OPERATIONS.md` (operations and the status lifecycle)
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
# ADR 0006-V8N5: The status on two axes — what is owed, and how the last attempt went
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-08-30) — proposed 2026-08-21 as a revision of ADR 0005-H2Q6, moved to its own record 2026-08-29, the
|
||||
mechanism reshaped 2026-08-30 when implementation started, again 2026-08-31, when the grid could not
|
||||
tell a modification owed from a closure due, and again 2026-09-03, when a warning left the closure due on a card
|
||||
whose plate was still missing.
|
||||
|
||||
## Context
|
||||
|
||||
**One field, two meanings.** `EkaerStatus` said at once where the declaration stood (`Sent`, `Finalized`) and what
|
||||
became of our last operation (`GeneratedWithWarning`, `ValidationError`). The two are independent, and fusing them
|
||||
caused measurable harm: modification requires regenerating the card, but generation overwrote the status, and with it
|
||||
took away the fact that the declaration was at NAV. The resulting row was `IsSubmittable()`, so the next submission
|
||||
would create a SECOND declaration for the same carriage. `FmEkaerServiceBase.ThrowIfAlreadyAtNav` is the scar of that
|
||||
squeeze — it exists because one field cannot preserve two things at once.
|
||||
|
||||
**Why now.** `modify` and `finalize` are the next EKÁER work, and the core of `modify` is regeneration, which the
|
||||
fused field forbade. Done afterwards, the modify branch gets a detour that is then thrown away.
|
||||
|
||||
**Out of scope:** moving `EkaerStatus` and the status categories into the framework.
|
||||
|
||||
## Decision
|
||||
|
||||
> **An ADR records reasoning, not the state, and nothing in it is a rule; an open decision is the goal — `BASE:ARTIFACT_ROLES`.**
|
||||
|
||||
**The two axes are kept apart, and both are stored** — until the 2026-08-31 revision only one of them was.
|
||||
|
||||
| axis | where | values |
|
||||
|---|---|---|
|
||||
| what is owed | `StatusId` (`EkaerStatus`) | `ToGenerate` / `ToSubmit` / `ToModify` / `ToFinalize` / `Done` |
|
||||
| how the last attempt went | `ResultId` (`EkaerResult`) | `Ok` / `Warning` / `Error` |
|
||||
| where it stands at NAV | `NavStatus` | `P` / `S` / `F` / `I` / `D`, null until NAV answers; `P` only from a web-recorded card |
|
||||
| whether a submission has started | `SentDate` | null / set |
|
||||
|
||||
**Every value of the first column names the next action**, so the grid's button is read from the row rather than
|
||||
derived from it, and `IsSubmittable` becomes `StatusId == ToSubmit` instead of a conjunction over three fields.
|
||||
|
||||
**Four rules govern the two columns.**
|
||||
|
||||
1. **`StatusId` moves only on a NAV answer.** The gate's row creation is the single exception, and it writes
|
||||
`ToGenerate`. Generation is a local act with no standing to declare a card submittable; the pre-flight
|
||||
`validateTradeCard` that closes it is what has that standing, and its answer is the transition.
|
||||
2. **A failure never moves `StatusId`, only `ResultId`.** A debt does not lapse because an attempt at it failed, so
|
||||
the same button stays active. This is also what keeps `ResultId` readable against the current `StatusId`: the
|
||||
first names the last success, the second the last attempt, and both concern the same debt.
|
||||
3. **`EkaerNumber` decides first, in exactly one place** — the validate answer: absent → the row is not at NAV and
|
||||
our own verdict decides (`Error` → `ToGenerate`, otherwise → `ToSubmit`); present → the row is at NAV, and rule 4
|
||||
decides. Keying on the number rather than on `SentDate` is what keeps an interrupted submission out of
|
||||
`ToModify`, where there would be no `tcn` to modify with.
|
||||
4. **On a card at NAV, `ResultId` is read before the two cards are compared:** `Warning` → `ToModify`; only a clean
|
||||
result lets the comparison against `NavXmlDoc` decide (in sync → `ToFinalize`, differing → `ToModify`). A warning
|
||||
is ours and always names something to be supplied — a missing plate — so a warning that reached NAV IS a
|
||||
modification owed. The order is what makes it work: the unsupplied datum is missing from NAV's copy as well, so
|
||||
the comparison finds nothing to send, and on its own would leave a closure owed that NAV then refuses
|
||||
(`TC_FINALIZE_VEHICLE_DATA_EMPTY`). The rule governs the `create` answer and the `validate` answer alike.
|
||||
|
||||
| NAV answer | condition | `StatusId` |
|
||||
|---|---|---|
|
||||
| `validate` accepted | `EkaerNumber IS NULL`, `ResultId` = `Error` | `ToGenerate` |
|
||||
| `validate` accepted | `EkaerNumber IS NULL`, otherwise | `ToSubmit` |
|
||||
| `validate` accepted | at NAV, `ResultId` = `Error` | unchanged |
|
||||
| `validate` accepted | at NAV, `ResultId` = `Warning` | `ToModify` |
|
||||
| `validate` accepted | at NAV, clean, card differs from `NavXmlDoc` | `ToModify` |
|
||||
| `validate` accepted | at NAV, clean, card in sync | `ToFinalize` |
|
||||
| `create` accepted | `ResultId` = `Warning` | `ToModify` |
|
||||
| `create` accepted | otherwise | `ToFinalize` |
|
||||
| `modify` accepted | | `ToFinalize` |
|
||||
| `finalize` accepted | | `Done` |
|
||||
| `queryTradeCards` | the letter is `F`, `I` or `D` | `Done` |
|
||||
|
||||
**The query writes no `ResultId`.** It is a read, not an attempt at the debt; setting it to `Ok` would erase the
|
||||
trace of a finalization that failed, and the row would look clean while still owing the same thing.
|
||||
|
||||
**NAV's own warnings must not reach `ResultId` either.** Its six `NAVValidations` codes are plausibility notes — an
|
||||
implausible value or weight, a mismatched postcode, an inactive CPA — and none of them asks for anything to be
|
||||
supplied. Folded into `ResultId` they would demand a modification with nothing to send, which NAV then rejects for
|
||||
the missing reason. They stay text.
|
||||
|
||||
**A NAV outage leaves the row on `ToGenerate`**, unsubmittable, and that is accepted rather than worked around: the
|
||||
next Generate carries the same pre-flight, so pressing it again is the whole of the recovery.
|
||||
|
||||
**One button does not come from the two columns** — the submission interrupted by an exception. No NAV answer
|
||||
arrived, so rule 1 leaves `StatusId` at `ToSubmit` while a declaration may well exist. `SentDate`, written BEFORE
|
||||
the call, is what blocks the second submission, and the empty `NavStatus` is what puts *refresh the NAV state* on
|
||||
that row instead of *submit*.
|
||||
|
||||
**The original problem dissolves rather than being solved.** Generation writes `XmlDoc` and `ResultId`, and touches
|
||||
neither `SentDate` nor `NavStatus` nor `EkaerNumber`. A row at NAV therefore survives regeneration untouched, which
|
||||
is what `modify` needed.
|
||||
|
||||
**`ValidationError` and `SendError` become one `Error`.** The next action is the same for both — read `ErrorText`,
|
||||
fix, regenerate — and `IsSubmittable` was already false for both. The names also drop the operation they came from
|
||||
(`GeneratedWithWarning` → `Warning`): the enum states an outcome, not the step that produced it.
|
||||
|
||||
**`SentDate` is written BEFORE the NAV call**, and cleared only where NAV answered and created nothing. An
|
||||
interrupted call therefore leaves the row "sent": we do not know whether the declaration exists, and blocking a
|
||||
second submission is the cheap error where a duplicate declaration is the expensive one. This is what gives the
|
||||
"did it land?" case a home (`ACCORE-EKAER-B-D7L4`) without a state of its own.
|
||||
|
||||
**`ThrowIfAlreadyAtNav` changes what it guards.** The second submission is now blocked structurally, by
|
||||
`IsSubmittable` reading `SentDate`. What remains is that a create-regeneration would overwrite the `XmlDoc` that was
|
||||
actually submitted, and that `NavXmlDoc` is NAV's answer to. The guard protects the card, not the submission.
|
||||
|
||||
**The tabs stop keying on the enum.** `ToSubmit` = `SentDate IS NULL`; `Closed` = `NavStatus IN ('F','I','D')`;
|
||||
`OpenAtNav` = everything else that went out — the letter-less row included, because that is the one with something to
|
||||
do. The three are total, so no row falls off every tab.
|
||||
|
||||
**No migration and no backfill.** The `EkaerHistory` and `EkaerHistoryMapping` tables are emptied instead: the
|
||||
declarations in them are NAV **sandbox** ones, worth less than the code that would carry them forward. This reverses
|
||||
the earlier no-wipe position, whose reason was that `EkaerHistoryMapping.SourceRecordId` is null on older rows and
|
||||
`ForeignKey`'s meaning stays implicit there (`MGFBANKPLUG-EKAER-I-D8V3`) — a problem the wipe removes rather than
|
||||
migrates.
|
||||
|
||||
**Still to add:** `tcnValidityEnd`, the expiry deadline the create response already carries. It is the one column
|
||||
this work leaves behind, and it is not a status.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
|
||||
- The modification path opens: a row at NAV can be regenerated without losing the fact that it is at NAV.
|
||||
- Expiry (`I`) and deletion (`D`) become reportable with no new value and no new column. The three-value lifecycle
|
||||
could not express either, and they are the two states the process most needs to see.
|
||||
- `EkaerHistoryDbTable`'s three precomputed `StatusId` arrays and `StatusIdsWhere` are gone; the tabs are three
|
||||
column predicates.
|
||||
- No schema migration, so nothing has to be sequenced against a deployment.
|
||||
- The grid's button is a read rather than a derivation: the pair (`StatusId`, `ResultId`) names it, and
|
||||
`ErrorText` stops being consulted for state at all.
|
||||
|
||||
**Negative:**
|
||||
|
||||
- The lifecycle is a composition of two columns rather than one value, so every reader goes through the helpers on
|
||||
`IFmEkaerHistory` — and the tab filter, which cannot call them, restates the same three conditions in SQL. The NAV
|
||||
letters have one source (`FmEkaerStatusExtensions.ClosedNavStatuses`); the conditions themselves are written twice.
|
||||
- `NavStatus` is `string?` rather than an enum, because it holds NAV's letter as NAV reports it.
|
||||
- The enum's values were renumbered, which only the emptied table permits. It is append-only again from here.
|
||||
- The change reaches every consumer of `EkaerStatus` and its helpers across three repos; the enum's own file does not
|
||||
suggest how many.
|
||||
- A second stored column and a second enum, where the previous decision had neither. Every `StatusId` transition
|
||||
now costs a NAV round-trip, so an outage stalls the row on `ToGenerate` until the next Generate gets through.
|
||||
- Rule 4's `Warning` test stands twice in `ApplyNavAnswer`, once per answer, because rule 3 runs between them.
|
||||
Folding them into one would send a warning-carrying row that has no `tcn` to `ToModify`, where there is nothing
|
||||
to modify with — the two look alike and sit on opposite sides of the `EkaerNumber` question.
|
||||
|
||||
**Follow-ups required:** tracked by `FBANKAPP-EKAER-T-C6Q3`.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**A second column for the lifecycle** (rejected 2026-08-30, after standing as the decision for nine days):
|
||||
`LifecycleId` (`NotSent` / `Sent` / `Finalized`) beside `StatusId`, backfilled from the eight old values. Its
|
||||
argument was that one column reads more simply than a composition, and that our claim and NAV's could then disagree —
|
||||
which is how expiry would surface. Against it: `NavStatus` reports expiry outright, so the disagreement it was meant
|
||||
to detect has a plainer source; three values cannot express expiry or deletion at all; and it costs a migration, a
|
||||
backfill that has to be total, and a stored fact derivable from two columns that were already there
|
||||
(`BASE:AUTHORITY_AND_SKILLS` → derived facts are not stored). A separate `IsExpired` flag failed the same test more
|
||||
sharply: it is `NavStatus = 'I'` copied into a second place.
|
||||
|
||||
**Extending the existing enum** (rejected): `ModifyReady` / `ModifyValidationError` appended at the end. Its argument
|
||||
was real: the enum is append-only, so this is the sanctioned path, there is no migration, and the tab filter's
|
||||
precomputed int array keeps working **unchanged**, because the predicate still depends only on the enum. Against it:
|
||||
the card's validity has to be stated twice — the modify branch rewrites the create branch's vocabulary — and every
|
||||
further operation multiplies it again.
|
||||
|
||||
**A derived phase, from expressions** (rejected as posed, then half adopted): an extension bound to
|
||||
`IFmEkaerHistory` computes the phase from the existing fields, without storing it. Its pitfall was the query: the tab
|
||||
filter translates to SQL, an extension does not — `GetByFilter` would have received a second, hand-maintained truth,
|
||||
exactly what the code comment there guarded against. That objection holds for the C# helpers, and is why the filter
|
||||
restates the conditions; it does not hold for the fields, which are real columns. The decision above is this
|
||||
alternative with the query half written in SQL by hand.
|
||||
|
||||
**A fourth lifecycle value, `Modified`** (rejected 2026-08-30; the rejection's premise proved false 2026-08-31): it
|
||||
would record that the row was touched. The argument against it was that a modified row is allowed exactly what a
|
||||
submitted one is — which is untrue. A regenerated row owes a `modify`, a synced one owes a `finalize`, and those are
|
||||
different buttons on the same tab. NAV cannot supply the difference either: `P` is unreachable over the API
|
||||
(`ADR 0005-H2Q6`), so the distinction has to be ours. The revision above adopts the value, named for the debt
|
||||
(`ToModify`) rather than for the past act — which is what the rejection's closing line asked for.
|
||||
|
||||
**The lifecycle composed from `SentDate` and `NavStatus`** (the decision of 2026-08-30, revised 2026-08-31): no
|
||||
column of its own, six SQL-filterable states, all on columns that predated the record. It survived a day of
|
||||
implementation and then failed on the one question the grid has to answer — which button is active on a row that is
|
||||
at NAV. `Generated` + sent + `S` is the same triple whether the card is in sync or has been regenerated since,
|
||||
because the composition was built precisely so that regeneration would leave no trace. What dissolved the `create`
|
||||
problem is what made the `modify`-or-`finalize` choice unanswerable.
|
||||
|
||||
## Related
|
||||
|
||||
- Related ADRs: `ADR 0005-H2Q6` (the consumer-side EKÁER process; this record was one of its revisions until
|
||||
2026-08-29, and the `modify` and `finalize` decisions that depend on this one stay there)
|
||||
- Related TODOs/Issues: `FBANKAPP-EKAER-T-C6Q3` (anchor), `FBANKAPP-EKAER-T-R9D4` (the process this unblocks),
|
||||
`MGFBANKPLUG-EKAER-I-D8V3` (the ambiguity the wipe removes), `ACCORE-EKAER-B-D7L4` (the uncertain submit outcome)
|
||||
+14
-14
@@ -14,14 +14,14 @@ Durable records of architecture-level decisions (Nygard style): one file = one d
|
||||
|
||||
The implementation status is carried by **exactly one** `_TODO` / `_ISSUES` anchor entry per ADR — hence the separate column.
|
||||
|
||||
| ADR | Title | Status | Tracking entry |
|
||||
|---|---|---|---|
|
||||
| [0001](0001-non-measurable-item-derived-weight.md) | Nem mérendő tétel súlya — derivált pallet-mérés a termék átlagsúlyából és tárájából | Proposed (2026-07-14) | `FBANKAPP-MEASUR-T-K4W7` ([MEASURING_TODO](../MEASURING/MEASURING_TODO.md)) — InProgress |
|
||||
| [0002](0002-environment-infrastructure-to-framework.md) | Csatlakozás a keret környezet-infrastruktúrájához (opt-in, delegátok, endpoint) | Accepted (2026-07-15, rev. 2026-07-25) | `FBANKAPP-ENV-T-R3M9` ([ENV_TODO](../ENV/ENV_TODO.md)) — InProgress |
|
||||
| [0003](0003-shipping-item-to-shipping-mapping.md) | Tétel↔kamion hozzárendelés külön mapping-táblával (`ShippingItemToShipping`) | Accepted (2026-07-23, rev. 2026-07-24 / 07-27) | `FBANKAPP-SHPLAN-T-W2J6` ([SHPLAN_TODO](../SHPLAN/SHPLAN_TODO.md)) — Implemented |
|
||||
| [0004-M9F3](0004-M9F3-fractional-pallets-and-remainder-grouping.md) | Tört raklapszám a rekesz/raklap arányból, és a maradékok összevonása közös raklapra | Accepted (2026-08-02) | `FBANKAPP-SHPLAN-T-D8K5` ([SHPLAN_TODO](../SHPLAN/SHPLAN_TODO.md)) — Open |
|
||||
| [0005-H2Q6](0005-H2Q6-ekaer-consumer-implementation.md) | The consumer side of EKÁER — the declaration unit, and steering the declaration at NAV | Proposed (2026-08-18, rev. 2026-08-18, 2026-08-19, 2026-08-20, 2026-08-21, 2026-08-22, 2026-08-25, 2026-08-26, consolidated 2026-08-26) | `FBANKAPP-EKAER-T-R9D4` ([EKAER_TODO](../EKAER/EKAER_TODO.md)) — InProgress |
|
||||
| [0006-K4T9](0006-K4T9-admin-declared-pallet-group-capacity.md) | Az összevont raklap-csoport foglaltságát az admin adja meg, nem a rendszer számolja | Proposed (2026-08-27) | `FBANKAPP-SHPLAN-T-R6M2` ([SHPLAN_TODO](../SHPLAN/SHPLAN_TODO.md)) — Open |
|
||||
| ADR | Title | Status | Tracking entry | Topics |
|
||||
|---|---|---|---|--------|
|
||||
| [0001](0001-non-measurable-item-derived-weight.md) | Nem mérendő tétel súlya — derivált pallet-mérés a termék átlagsúlyából és tárájából | Accepted (2026-07-14) | `FBANKAPP-MEASUR-T-K4W7` ([MEASURING_TODO](../MEASURING/MEASURING_TODO.md)) — InProgress | |
|
||||
| [0002](0002-environment-infrastructure-to-framework.md) | Csatlakozás a keret környezet-infrastruktúrájához (opt-in, delegátok, endpoint) | Accepted (2026-07-15, rev. 2026-07-25) | `FBANKAPP-ENV-T-R3M9` ([ENV_TODO](../ENV/ENV_TODO.md)) — InProgress | |
|
||||
| [0003](0003-shipping-item-to-shipping-mapping.md) | Tétel↔kamion hozzárendelés külön mapping-táblával (`ShippingItemToShipping`) | Accepted (2026-07-23, rev. 2026-07-24 / 07-27) | `FBANKAPP-SHPLAN-T-W2J6` ([SHPLAN_TODO](../SHPLAN/SHPLAN_TODO.md)) — Implemented | |
|
||||
| [0004-M9F3](0004-M9F3-fractional-pallets-and-remainder-grouping.md) | Tört raklapszám a rekesz/raklap arányból, és a maradékok összevonása közös raklapra | Accepted (2026-08-02) | `FBANKAPP-SHPLAN-T-D8K5` ([SHPLAN_TODO](../SHPLAN/SHPLAN_TODO.md)) — Open | |
|
||||
| [0005-H2Q6](0005-H2Q6-ekaer-consumer-implementation.md) | The consumer side of EKÁER — the declaration unit, and steering the declaration at NAV | Proposed (2026-08-18, rev. 2026-08-18, 2026-08-19, 2026-08-20, 2026-08-21, 2026-08-22, 2026-08-25, 2026-08-26, consolidated 2026-08-26, rev. 2026-08-27, 2026-08-28) | `FBANKAPP-EKAER-T-R9D4` ([EKAER_TODO](../EKAER/EKAER_TODO.md)) — InProgress | `FBANKAPP-EKAER` |
|
||||
| [0006-V8N5](0006-V8N5-ekaer-status-on-two-axes.md) | The status on two axes — what is owed, and how the last attempt went | Accepted (2026-08-30, rev. 2026-08-31, 2026-09-03; proposed 2026-08-21, moved out of ADR 0005-H2Q6 on 2026-08-29) | `FBANKAPP-EKAER-T-C6Q3` ([EKAER_TODO](../EKAER/EKAER_TODO.md)) — InProgress | `FBANKAPP-EKAER` |
|
||||
|
||||
> The `Title` cells above are **bound**: each is its record's H1 with the `ADR <NNNN>[-<RAND>]: ` prefix stripped,
|
||||
> character-identical (docs-audit invariant 47). A re-title changes both, in the same write.
|
||||
@@ -36,12 +36,12 @@ which those records are discoverable from here.
|
||||
> **Interim.** When these ADRs move into their own layer (the wholesale / Mango.Nop layer, once it exists), this
|
||||
> table is to be revisited. The durable answer is not this table but placing the ADRs in the right layer.
|
||||
|
||||
| ADR | Title | Status |
|
||||
|---|---|---|
|
||||
| [ACCORE 0009](../../../../../Aycode/Source/AyCode.Core/docs/adr/0009-shared-layer-across-nop-versions.md) | How the shared layer supports several nopCommerce versions | Proposed (2026-07-21) |
|
||||
| [ACCORE 0010](../../../../../Aycode/Source/AyCode.Core/docs/adr/0010-product-solution-scaffolding-template.md) | Scaffolding a new product — a template for the solution skeleton, a skill for the cross-repo wiring | Proposed (2026-07-21, rev. 2026-08-06) |
|
||||
| [ACCORE 0011](../../../../../Aycode/Source/AyCode.Core/docs/adr/0011-copied-and-overridden-nop-code.md) | Handling copied and overridden nopCommerce code | Proposed (2026-07-25) |
|
||||
| [ACCORE 0002](../../../../../Aycode/Source/AyCode.Core/docs/adr/0002-environment-infrastructure-to-framework.md) | Environment infrastructure in the framework — the **framework-side pair** of 0002 above (mechanism: `AcEnv`, reserved tags, client base) | Accepted (2026-07-15, rev. 2026-07-25) |
|
||||
| ADR | Title | Status | |
|
||||
|---|---|---|--------|
|
||||
| [ACCORE 0009](../../../../../Aycode/Source/AyCode.Core/docs/adr/0009-shared-layer-across-nop-versions.md) | How the shared layer supports several nopCommerce versions | Proposed (2026-07-21) | |
|
||||
| [ACCORE 0010](../../../../../Aycode/Source/AyCode.Core/docs/adr/0010-product-solution-scaffolding-template.md) | Scaffolding a new product — a template for the solution skeleton, a skill for the cross-repo wiring | Proposed (2026-07-21, rev. 2026-08-06) | |
|
||||
| [ACCORE 0011](../../../../../Aycode/Source/AyCode.Core/docs/adr/0011-copied-and-overridden-nop-code.md) | Handling copied and overridden nopCommerce code | Proposed (2026-07-25) | |
|
||||
| [ACCORE 0002](../../../../../Aycode/Source/AyCode.Core/docs/adr/0002-environment-infrastructure-to-framework.md) | Environment infrastructure in the framework — the **framework-side pair** of 0002 above (mechanism: `AcEnv`, reserved tags, client base) | Accepted (2026-07-15, rev. 2026-07-25) | |
|
||||
|
||||
> Only a record living in **another layer** belongs here. This repo's own ADRs are in the [Index](#index) above —
|
||||
> they belong there, because their `Tracking entry` column (the anchor entry) cannot be represented in this table.
|
||||
|
||||
Reference in New Issue
Block a user