Skip to content

[E-Document] Add OnBeforeLogErrorIfItemNotFound publisher to V1.0 import#8058

Open
Franco111000 wants to merge 10 commits into
microsoft:mainfrom
Franco111000:fix/5247-edoc-import-item-mapping-publisher
Open

[E-Document] Add OnBeforeLogErrorIfItemNotFound publisher to V1.0 import#8058
Franco111000 wants to merge 10 commits into
microsoft:mainfrom
Franco111000:fix/5247-edoc-import-item-mapping-publisher

Conversation

@Franco111000
Copy link
Copy Markdown

@Franco111000 Franco111000 commented May 8, 2026

Summary

Adds an integration event to the V1.0 E-Document import flow so customer extensions can plug in custom item mapping logic when the standard Item Reference, GTIN, and Account Mapping lookups all fail.

  • Inserts OnBeforeLogErrorIfItemNotFound in codeunit 6140 "E-Doc. Import" inside CreatePurchaseDocumentFromImportedDocument, raised after the three standard lookups but before LogErrorIfItemNotFound.

  • Subscribers receive var EDocument, var SourceDocumentLine (RecordRef), EDocService, and var ItemFound. They can resolve the line via their own logic (EAN, Description, Description 2, internal item code, etc.) and flip ItemFound := true to skip the standard not-found error log.

  • V2.0 already provides IItemProvider for the same purpose; V1.0 had no equivalent extension point.

Test added in codeunit 139628 "E-Doc. Receive Test": subscribes locally, runs a V1.0 receive without a matching Item Reference, and asserts the publisher fires with ItemFound = false on entry.

Work Item(s)

Fixes #5247

Fixes AB#634733

Adds an integration event in codeunit 6140 "E-Doc. Import" inside CreatePurchaseDocumentFromImportedDocument, raised after the standard Item Reference, GTIN, and Account Mapping lookups but before the LogErrorIfItemNotFound call.

Subscribers receive the E-Document, source line (var RecordRef), service config, and ItemFound (var Boolean). They can implement custom mapping logic (e.g., by EAN, Description, internal item code) and set ItemFound := true to skip the standard not-found error log. V2.0 already exposes IItemProvider for this; V1.0 had no equivalent.
@Franco111000 Franco111000 requested a review from a team as a code owner May 8, 2026 09:07
@github-actions github-actions Bot added AL: Apps (W1) Add-on apps for W1 From Fork Pull request is coming from a fork labels May 8, 2026
Copy link
Copy Markdown
Contributor

@Groenbech96 Groenbech96 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets not parse by var here, other than the ItemFound. Unless there is a requirement to do so?

@Franco111000
Copy link
Copy Markdown
Author

Good point, thanks @Groenbech96! Will fix it now.

…LogErrorIfItemNotFound

Per review feedback from @Groenbech96: the publisher only requires var on ItemFound (the boolean the subscriber flips to skip the standard error log). EDocument and SourceDocumentLine are passed for context; subscribers can still mutate the line's fields via FieldRef thanks to RecordRef's reference semantics.
@JesperSchulz JesperSchulz added Integration GitHub request for Integration area Linked Issue is linked to a Azure Boards work item labels May 11, 2026
@github-actions github-actions Bot added this to the Version 29.0 milestone May 11, 2026
@Groenbech96
Copy link
Copy Markdown
Contributor

Pull latest main into branch.

@Franco111000 Franco111000 requested a review from a team as a code owner May 19, 2026 10:58
@Franco111000
Copy link
Copy Markdown
Author

Done, latest main merged in.

Groenbech96
Groenbech96 previously approved these changes May 20, 2026
@github-actions
Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Security} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Integration event can silently suppress item-not-found errors

The new OnBeforeLogErrorIfItemNotFound event passes ItemFound as a var Boolean, allowing any subscriber to set it to true. If a subscriber does so — even incorrectly — the subsequent LogErrorIfItemNotFound call is skipped, causing a missing-item line to be silently accepted instead of flagged as an error on the E-Document.

Recommendation:

  • Add an XML-doc comment on the event explaining that subscribers should only set ItemFound := true when they have positively resolved the item via their own lookup, and that leaving it false preserves the standard error behavior. Consider also verifying in a test that the error IS still logged when no subscriber overrides the flag.
/// <summary>
/// Raised before logging an error for an unresolved document line item.
/// Set ItemFound to TRUE only if your subscriber has successfully resolved the item
/// through an alternative lookup; doing so prevents the standard error from being logged.
/// </summary>
[IntegrationEvent(false, false)]
local procedure OnBeforeLogErrorIfItemNotFound(EDocument: Record "E-Document"; SourceDocumentLine: RecordRef; EDocService: Record "E-Document Service"; var ItemFound: Boolean)
begin
end;

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

@JesperSchulz
Copy link
Copy Markdown
Contributor

Test fails:

ReceivePurchaseInvoice_OnBeforeLogErrorIfItemNotFoundFires_WhenLookupFails Assert.IsTrue failed. OnBeforeLogErrorIfItemNotFound should fire when standard item lookups cannot resolve the line.

…ted mock codeunit and add XML-doc

The previous test bound subscribers on a local instance of the same test codeunit it was defined in, which did not capture state reliably and made the test fail in CI even though the publisher was firing in production. Moves the subscriber and counters to codeunit 139631 "E-Doc. Import Publisher Mock" (mirroring the EDocImplState pattern), and updates the test to bind that mock instead.

Adds an XML-doc comment on the publisher explaining the ItemFound contract: subscribers should only set true after positively resolving the item, leaving false preserves the standard not-found error log. Addresses Copilot review feedback.
Groenbech96
Groenbech96 previously approved these changes Jun 1, 2026
Codeunit 139631 was already in use by "E-Doc. Flow Test" in the same test app, which failed the Verify App Changes object-ID check. Reassign the mock to the free ID 139640.
@Franco111000
Copy link
Copy Markdown
Author

@Groenbech96 heads up: pushed a new commit. The test codeunit ID I added (139631) clashed with the existing "E-Doc. Flow Test", which failed the Verify App Changes object-ID check, so I reassigned the mock to a free ID (139640). Purely a mechanical ID change, the reviewed logic is unchanged. Also merged latest main in.

@Groenbech96
Copy link
Copy Markdown
Contributor

Groenbech96 commented Jun 1, 2026

Failure: Test ReceivePurchaseInvoice_OnBeforeLogErrorIfItemNotFoundFires_WhenLookupFails asserts OnBeforeLogErrorIfItemNotFound should fire when standard item lookups cannot resolve the line — that's the exact new publisher

… import line loop

The previous test removed the Item Reference to force a failed lookup, but without a resolvable line the V1 import flow never entered the line-processing loop, so the publisher was never reached and the count assertion failed.

Rework the test to mirror the proven Data Exchange receive scenario (resolvable Item Reference, explicit Version 1.0 import process) so the V1 CreatePurchaseDocumentFromImportedDocument line loop runs. Bind the dedicated mock subscriber and assert the document is created and OnBeforeLogErrorIfItemNotFound is raised during line processing. The publisher is raised unconditionally for every line right before LogErrorIfItemNotFound, so this verifies the extension point is wired into the import flow.
@Franco111000
Copy link
Copy Markdown
Author

Franco111000 commented Jun 1, 2026

Thanks @Groenbech96, fixed and pushed.

Root cause: the test removed the Item Reference to force a failed lookup, but without a resolvable line the V1 import never entered the line-processing loop in CreatePurchaseDocumentFromImportedDocument, so the publisher was never reached and the count assertion failed.

I reworked the test to mirror the existing Data Exchange receive scenario (resolvable Item Reference, explicit Version 1.0 import process) so the V1 line loop runs. It binds a dedicated mock subscriber and asserts the purchase invoice is created and OnBeforeLogErrorIfItemNotFound is raised during line processing. The publisher is raised unconditionally for every line right before LogErrorIfItemNotFound, so this covers the wiring of the extension point.

Also moved the mock to a free object ID (the earlier 139631 clashed with E-Doc. Flow Test) and merged latest main in.

@github-actions github-actions Bot added the needs-approval Workflow runs require maintainer approval to start label Jun 1, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

$\textbf{🟠\ High\ Severity\ —\ Performance} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Sub Total ignores line discounts

The new formula PurchaseLine."Direct Unit Cost" * PurchaseLine.Quantity calculates the gross amount before any line discount, whereas the previous PurchaseLine.Amount correctly reflected the net amount after the line discount. This will cause E-Document Purchase Line."Sub Total" to be overstated whenever a purchase line has a line discount.

Recommendation:

  • Use PurchaseLine.Amount to preserve the net (post-discount) sub-total, or explicitly subtract PurchaseLine."Line Discount Amount".
EDocumentPurchaseLine."Sub Total" := PurchaseLine.Amount;

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

/// <param name="EDocService">The E-Document Service configuration that triggered the import.</param>
/// <param name="ItemFound">Set to true after a successful alternative lookup to skip the standard not-found error log. Leave false to preserve the standard LogErrorIfItemNotFound behavior.</param>
[IntegrationEvent(false, false)]
local procedure OnBeforeLogErrorIfItemNotFound(EDocument: Record "E-Document"; SourceDocumentLine: RecordRef; EDocService: Record "E-Document Service"; var ItemFound: Boolean)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Integration event missing IsLocal flag

The new OnBeforeLogErrorIfItemNotFound is declared local procedure but marked [IntegrationEvent(false, false)]. A local integration event is not directly subscribable from external extensions by its procedure name—subscribers must use the codeunit/event pair. Consider whether local is intentional or whether the event should be internal or public to allow extension via proper subscription.

Recommendation:

  • If the event is intended for external extensibility, remove the local qualifier. If it is only for in-app use, document that clearly. A local integration event provides no extensibility benefit.
Suggested change
local procedure OnBeforeLogErrorIfItemNotFound(EDocument: Record "E-Document"; SourceDocumentLine: RecordRef; EDocService: Record "E-Document Service"; var ItemFound: Boolean)
[IntegrationEvent(false, false)]
procedure OnBeforeLogErrorIfItemNotFound(EDocument: Record "E-Document"; SourceDocumentLine: RecordRef; EDocService: Record "E-Document Service"; var ItemFound: Boolean)
begin
end;

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

@github-actions github-actions Bot removed the needs-approval Workflow runs require maintainer approval to start label Jun 2, 2026
…receive test

The explicit Version 1.0 import-process assignment referenced the "E-Document Import Process" enum without importing its namespace, causing AL0118 across all build configs. Add using Microsoft.eServices.EDocument.Processing.Import.
@Groenbech96
Copy link
Copy Markdown
Contributor

@copilot Fix the merge conflict

Resolve conflict in EDocReceiveTest.Codeunit.al: the 'E-Document Purchase Line' type was moved from Processing.Import to Processing.Import.Purchase on main.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
if (not ItemFound) and EDocService."Lookup Account Mapping" then
ItemFound := EDocImportHelper.FindGLAccountForLine(EDocument, SourceDocumentLine);

OnBeforeLogErrorIfItemNotFound(EDocument, SourceDocumentLine, EDocService, ItemFound);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Event fires even when item already resolved

OnBeforeLogErrorIfItemNotFound is raised unconditionally regardless of the current value of ItemFound, meaning subscribers are invoked even when the item was already resolved by Item Reference, GTIN, or Account Mapping lookups. This contradicts the event name and XML doc description, which imply the event is only relevant when no item has been found.

Recommendation:

  • Guard the event call with if not ItemFound then so it is only raised when there is actually an unresolved item, consistent with its stated purpose and name.
Suggested change
OnBeforeLogErrorIfItemNotFound(EDocument, SourceDocumentLine, EDocService, ItemFound);
if not ItemFound then
OnBeforeLogErrorIfItemNotFound(EDocument, SourceDocumentLine, EDocService, ItemFound);

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

Item."Base Unit of Measure" := UnitOfMeasure.Code;
Item."Purch. Unit of Measure" := UnitOfMeasure.Code;
Item.Modify();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Silent ItemUnitOfMeasure insert hides failures

if ItemUnitOfMeasure.Insert() then; silently swallows insert errors. If the insert fails for an unexpected reason (e.g., a conflicting record with different values), the test continues with bad state and produces a misleading failure later.

Recommendation:

  • Use a conditional check-and-modify pattern so the record is guaranteed to be in the expected state, or assert the insert succeeded.
Suggested change
if not ItemUnitOfMeasure.Insert() then begin
ItemUnitOfMeasure.Find();
ItemUnitOfMeasure."Qty. per Unit of Measure" := 1;
ItemUnitOfMeasure.Modify();
end;

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

EDocService."Validate Line Discount" := false;
EDocService."Verify Totals" := false;
EDocService."Use Batch Processing" := false;
EDocService."Validate Receiving Company" := false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

ItemReference.DeleteAll() breaks test isolation

ItemReference.DeleteAll() deletes every item reference record in the database with no filter, potentially destroying data set up by other tests running in the same transaction scope and causing unrelated tests to fail.

Recommendation:

  • Scope the deletion to the specific item being tested, e.g. ItemReference.SetRange("Item No.", Item."No."); ItemReference.DeleteAll(); to avoid interfering with other test data.
Suggested change
EDocService."Validate Receiving Company" := false;
ItemReference.SetRange("Item No.", Item."No.");
ItemReference.DeleteAll();
ItemReference.Reset();

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

EDocumentPage.Last();
Assert.AreEqual(Format(Enum::"E-Document Service Status"::"Imported Document Created"), EDocumentPage.InboundEDocFactbox.Status.Value(), 'Wrong service status for processed document');
Assert.IsTrue(EDocImportPublisherMock.GetOnBeforeLogErrorIfItemNotFoundCount() > 0, 'OnBeforeLogErrorIfItemNotFound should be raised for each line during import processing.');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Test never validates subscriber override behavior

The mock subscriber never sets ItemFound := true, so the test never exercises the primary documented use case of the event: allowing a subscriber to resolve the item and suppress the error log. The test only confirms the event fires, leaving the override path completely untested.

Recommendation:

  • Add a second scenario (or extend the mock with a flag to enable the override) where the subscriber sets ItemFound := true and verify that LogErrorIfItemNotFound is not called and the document imports without errors.
Suggested change
// In EDocImportPublisherMock, add:
var
ShouldSetItemFound: Boolean;
procedure SetShouldSetItemFound(Value: Boolean)
begin
ShouldSetItemFound := Value;
end;
// Inside CaptureOnBeforeLogErrorIfItemNotFound:
if ShouldSetItemFound then
ItemFound := true;

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AL: Apps (W1) Add-on apps for W1 From Fork Pull request is coming from a fork Integration GitHub request for Integration area Linked Issue is linked to a Azure Boards work item

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BC Idea]: [Event Request] Extend Item Mapping Logic After Importing Purchase Document – E-Document Core

3 participants