Skip to content

Add French E-Document lifecycle messages - #10426

Closed
Milica Đukić (djukicmilica) wants to merge 9 commits into
mainfrom
feature/fr-edocument-messages-v2
Closed

Add French E-Document lifecycle messages#10426
Milica Đukić (djukicmilica) wants to merge 9 commits into
mainfrom
feature/fr-edocument-messages-v2

Conversation

@djukicmilica

@djukicmilica Milica Đukić (djukicmilica) commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Why

French electronic invoices require lifecycle communication beyond the parent E-Document processing status. Suppliers must report payment events, platforms must return technical and business statuses, and buyers must be able to accept or refuse incoming invoices without overwriting the invoice's own processing state.

This change models those exchanges as child E-Document messages, keeping generic transport in W1 and French AFNOR/CDAR semantics in the E-Reporting FR app.

Summary

  • Added generic synchronous and asynchronous child-message transport APIs, context, status handling, and integration interfaces in E-Document Core.
  • Added French Collected and Negative Collected payment lifecycle capture, CDAR XML generation, service routing, retries, and VAT breakdown storage.
  • Added incoming Submitted, Accepted, and technical Rejected lifecycle response parsing, payload retention, duplicate protection, connector API, and manual XML import.
  • Added buyer Accepted and Refused responses for incoming purchase invoices, including reason validation and asynchronous response polling.
  • Added lifecycle, parsed response, and buyer-response history pages while reusing the W1 Messages FactBox for the common child-message timeline.
  • Added focused W1 and FR test coverage for transport, routing, status mapping, failure handling, retries, and buyer decisions.

@djukicmilica
Milica Đukić (djukicmilica) requested a review from a team as a code owner August 19, 2026 22:55
@github-actions github-actions Bot added AL: Apps (W1) Add-on apps for W1 Team: Other GitHub request for other area than SCM, Finance or Integration Ownership: Needs Review Ownership is Other, low confidence, or needs manual correction labels Aug 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Could not find a linked ADO work item. Please link one by using the pattern 'AB#' followed by the relevant work item number. You may use the 'Fixes' keyword to automatically resolve the work item when the pull request is merged. E.g. 'Fixes AB#1234'

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟠\ High\ Severity\ —\ Error\ Handling}$

CheckSIRENNotEmpty, CheckSIRETNotEmpty, CheckSellerCountryCode, and CheckBuyerElectronicAddress all raise plain Error(...) for recoverable setup gaps even though the code already knows the exact record the user must fix (Company Information or the current Customer). This is a dead-end dialog where a Show-it ErrorInfo could navigate directly to the record that needs correction.

Knowledge:

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

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.34.4

EDocMessage.Modify();
end;

local procedure InitializeMessageContext(EDocMessage: Record "E-Document Message"; var EDocMessageContext: Codeunit "E-Doc. Message Context")

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{🟠\ High\ Severity\ —\ Error\ Handling}$

These new branches surface developer-facing invariants with plain client-visible errors instead of routing them as internal diagnostics. In EDocMessageMgt, MessagePayloadErr and MessageSendingErr expose internal message entry numbers and transport-contract state to end users; the same anti-pattern appears in FREInvoiceBuyerResponseMgt.Codeunit.al, where the defensive UnsupportedResponseTypeErr branch is raised as a plain Error. Use ErrorInfo with ErrorType::Internal for these internal failures so the detail goes to telemetry instead of the user dialog.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.34.4

Comment on lines +15 to +17
DefaultImplementation = IConsentManager = "Consent Manager Default Impl.",
IMessageSender = "E-Doc. Msg. Transport Default",
IMessageResponseHandler = "E-Doc. Msg. Transport Default";

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{🟠\ High\ Severity\ —\ Interfaces}$

The persisted extensible enum "Service Integration" now adds IMessageSender and IMessageResponseHandler with only DefaultImplementation fallbacks. That does not cover an orphaned stored ordinal from an uninstalled enumextension value on "E-Document Service". In that state, EDocMessageMgt assigns "Service Integration V2" to the new interfaces and can fail with a technical runtime error instead of the controlled default transport error. Add UnknownValueImplementation for the new message interfaces so unknown ordinals resolve safely.

Suggested change
DefaultImplementation = IConsentManager = "Consent Manager Default Impl.",
IMessageSender = "E-Doc. Msg. Transport Default",
IMessageResponseHandler = "E-Doc. Msg. Transport Default";
DefaultImplementation = IConsentManager = "Consent Manager Default Impl.",
IMessageSender = "E-Doc. Msg. Transport Default",
IMessageResponseHandler = "E-Doc. Msg. Transport Default";
UnknownValueImplementation = IMessageSender = "E-Doc. Msg. Transport Default",
IMessageResponseHandler = "E-Doc. Msg. Transport Default";

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.34.4


local procedure FindOutgoingEDocument(InvoiceID: Text; var EDocument: Record "E-Document")
begin
EDocument.SetRange("Document No.", InvoiceID);

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{🟠\ High\ Severity\ —\ Performance}$

The new FindOutgoingEDocument lookup filters "E-Document" by "Document No." and Direction, but "E-Document" has no key whose leading fields cover that access pattern. On a large E-Document table, this FindFirst/Next uniqueness check can degrade into a broad scan; add a key for this lookup and select it with SetCurrentKey, or switch the lookup to an already indexed identifier.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.34.4

field(10970; "FR Sender Platform ID"; Text[50])
{
Caption = 'FR Sender Platform ID';
DataClassification = SystemMetadata;

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{🟠\ High\ Severity\ —\ Privacy}$

The new "FR Sender Platform ID" and "FR Sender Platform Name" fields are classified as SystemMetadata even though their tooltips describe identifying data about an approved external platform (organization identity). BCQuality treats organization-identifying values as data that must not be under-classified as SystemMetadata.

Suggested fix (apply manually — could not be anchored as a one-click suggestion):

        field(10970; "FR Sender Platform ID"; Text[50])
        {
            Caption = 'FR Sender Platform ID';
            DataClassification = OrganizationIdentifiableInformation;
            ToolTip = 'Specifies the identifier of the French approved platform that sends lifecycle messages.';
        }
        field(10971; "FR Sender Platform Scheme"; Code[4])
        {
            Caption = 'FR Sender Platform Scheme';
            DataClassification = SystemMetadata;
            InitValue = '0238';
            ToolTip = 'Specifies the identifier scheme of the French approved platform.';
        }
        field(10972; "FR Sender Platform Name"; Text[100])
        {
            Caption = 'FR Sender Platform Name';
            DataClassification = OrganizationIdentifiableInformation;
            ToolTip = 'Specifies the name of the French approved platform that sends lifecycle messages.';
        }

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.34.4


IncludedPermissionSets = "E-Reporting FR Read";

Permissions = tabledata "FR E-Invoice Lifecycle" = im,

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{🟠\ High\ Severity\ —\ Security}$

The assignable "E-Reporting FR Edit" role grants direct write access (im/ri/rim) to the FR lifecycle, VAT, response, and buyer-response tables even though this feature is designed to update those records through the import/worker/response codeunits. Direct table write grants let assignees bypass code-mediated validation and alter regulatory message state through any writable surface (e.g. RapidStart, OData). Change the table write permissions to indirect so writes remain mediated by the app code.

Suggested fix (apply manually — could not be anchored as a one-click suggestion):

    Permissions = tabledata "FR E-Invoice Lifecycle" = im,
                  tabledata "FR E-Invoice Lifecycle VAT" = i,
                  tabledata "FR E-Invoice Lifecycle Resp." = i,
                  tabledata "FR E-Invoice Buyer Response" = im,
                  codeunit "FR E-Invoice Lifecycle Import" = X,
                  codeunit "FR E-Inv. Buyer Resp. Mgt." = X,
                  codeunit "FR E-Invoice Lifecycle Worker" = X,
                  codeunit "FR E-Invoice Lifecycle Error" = X,
                  codeunit "FR E-Invoice Lifecycle Mgt." = X;

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.34.4

DataClassification = SystemMetadata;
ToolTip = 'Specifies the identifier of the French approved platform that sends lifecycle messages.';
}
field(10971; "FR Sender Platform Scheme"; Code[4])

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{🔴\ Critical\ Severity\ —\ Upgrade}$

The new field "FR Sender Platform Scheme" is added to the existing "E-Document Service" table with InitValue = '0238', but this app ships no upgrade codeunit to back-fill existing service rows. Pre-upgrade records will keep the pre-upgrade blank value, so existing French service setups can later fail when lifecycle processing relies on this field. A non-default InitValue on a field added to an existing table needs an upgrade routine for existing rows.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.34.4

Caption = 'E-Invoice Buyer Responses';
Editable = false;
InherentPermissions = X;
PageType = List;

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\ —\ Accessibility}$

FR E-Inv. Buyer Responses is a history list page (UsageCategory = History) but it does not set a descending default order, so users land on older buyer responses first instead of the most recent ones. Historical pages should open with the newest records first.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.34.4

using Microsoft.eServices.EDocument.Processing.Message;
using System.Utilities;

codeunit 10987 "FR E-Invoice Lifecycle Import"

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\ —\ Breaking\ Changes}$

FR E-Invoice Lifecycle Import is exposed as a public codeunit even though this change only uses it from the app itself (the E-Documents page action and tests). That turns an implementation-detail importer into a supported external contract unnecessarily; make the codeunit internal instead.

Suggested fix (apply manually — could not be anchored as a one-click suggestion):

codeunit 10987 "FR E-Invoice Lifecycle Import"
{
    Access = Internal;
    InherentEntitlements = X;
    InherentPermissions = X;
    Permissions = tabledata "FR E-Invoice Lifecycle Resp." = ri;

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.34.4

Comment on lines +7 to +10
page 10972 "FR E-Invoice Refusal Dialog"
{
Caption = 'Refuse E-Invoice';
PageType = StandardDialog;

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\ —\ Breaking\ Changes}$

FR E-Invoice Refusal Dialog is a same-app modal helper page, but it is left public by default. Keeping helper UI objects public creates an accidental extension surface that other apps can bind to; mark the page internal instead.

Suggested change
page 10972 "FR E-Invoice Refusal Dialog"
{
Caption = 'Refuse E-Invoice';
PageType = StandardDialog;
page 10972 "FR E-Invoice Refusal Dialog"
{
Access = Internal;
Caption = 'Refuse E-Invoice';
PageType = StandardDialog;

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.34.4

begin
OriginalLifecycleVAT.SetRange("Lifecycle Entry No.", OriginalOccurrenceEntryNo);
if not OriginalLifecycleVAT.FindSet() then
Error(OriginalVATBreakdownErr, OriginalOccurrenceEntryNo);

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{🟠\ High\ Severity\ —\ Error\ Handling}$

Failing to find VAT breakdown rows for an already-captured original lifecycle occurrence is an internal invariant failure, but this path raises a plain client-visible Error with the raw occurrence entry number. Raise ErrorInfo with ErrorType::Internal so the detailed diagnostic goes to telemetry while the user gets a generic failure.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.34.4

EDocument.Get(EDocMessage."E-Document Entry No.");
EDocumentService.Get(EDocMessage.Service);
if EDocumentService."Service Integration V2" = EDocumentService."Service Integration V2"::"No Integration" then
Error(NoMessageIntegrationErr, EDocumentService.Code);

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\ —\ Error\ Handling}$

Sending a message through a service whose integration is not configured is a recoverable setup problem on a known E-Document Service record, but this path raises a plain Error with no Show-it action. Raise an ErrorInfo that navigates the user to the service record so they can configure the integration directly.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.34.4

{
fields
{
field(10970; "FR Sender Platform ID"; Text[50])

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{🟠\ High\ Severity\ —\ Privacy}$

The new FR Sender Platform ID and FR Sender Platform Name fields are classified as SystemMetadata even though they store an external platform's identifier and name. That under-classifies organization-identifying data, which the app later copies into lifecycle records and outbound messages.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.34.4

/// </summary>
/// <param name="MessageEntryNo">The entry number of the E-Document message whose payload to load.</param>
/// <param name="TempBlob">The codeunit that receives the message payload.</param>
procedure GetMessageBlob(MessageEntryNo: Integer; var TempBlob: Codeunit "Temp Blob")

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\ —\ Security}$

The new public E-Document Message API exposes GetMessageBlob, SendMessage, and GetMessageResponse as entry-number-based wrappers over E-Doc. Message Mgt., which runs with read/write permission on E-Document Message and E-Doc. Data Storage. That lets any extension that is granted execute permission on this API read stored message payloads or drive transport state for arbitrary message rows without proving ownership of the target message. Keep these operations internal to trusted companion apps, or add explicit authorization/ownership validation before dereferencing the message entry number.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.34.4

Comment on lines +1191 to +1194
asserterror FREInvoiceLifecycleWorker.Run(FREInvoiceLifecycle);
FREInvoiceLifecycle.Get(FREInvoiceLifecycle."Entry No.");
MessageEntryNo := FREInvoiceLifecycle."E-Document Message Entry No.";
FREInvoiceLifecycleError.Run(FREInvoiceLifecycle);

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{🟠\ High\ Severity\ —\ Testing}$

LifecycleWorkerRetriesSameMessageAfterSendFailure uses asserterror without checking which error was raised. After the worker has already created and committed the child message, any unrelated send-path failure can satisfy the negative assertion and still let the later state checks pass, so the test no longer proves that the retry logic is driven by the expected connector failure.

Suggested change
asserterror FREInvoiceLifecycleWorker.Run(FREInvoiceLifecycle);
FREInvoiceLifecycle.Get(FREInvoiceLifecycle."Entry No.");
MessageEntryNo := FREInvoiceLifecycle."E-Document Message Entry No.";
FREInvoiceLifecycleError.Run(FREInvoiceLifecycle);
asserterror FREInvoiceLifecycleWorker.Run(FREInvoiceLifecycle);
Assert.ExpectedError('French lifecycle message sending failed.');
FREInvoiceLifecycle.Get(FREInvoiceLifecycle."Entry No.");
MessageEntryNo := FREInvoiceLifecycle."E-Document Message Entry No.";
FREInvoiceLifecycleError.Run(FREInvoiceLifecycle);

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.34.4

Comment on lines +1239 to +1241
asserterror FREInvoiceLifecycleWorker.Run(FREInvoiceLifecycle);
FREInvoiceLifecycle.Get(FREInvoiceLifecycle."Entry No.");
FREInvoiceLifecycleError.Run(FREInvoiceLifecycle);

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{🟠\ High\ Severity\ —\ Testing}$

LifecycleWorkerRejectsNonSentConnectorStatus also leaves its asserterror unpinned. The shared message-send path accepts Pending Response as a valid transport result, so this test currently proves only that something failed, not that the worker rejected the non-Sent status it is supposed to guard.

Suggested change
asserterror FREInvoiceLifecycleWorker.Run(FREInvoiceLifecycle);
FREInvoiceLifecycle.Get(FREInvoiceLifecycle."Entry No.");
FREInvoiceLifecycleError.Run(FREInvoiceLifecycle);
asserterror FREInvoiceLifecycleWorker.Run(FREInvoiceLifecycle);
Assert.ExpectedError('returned status Pending');
FREInvoiceLifecycle.Get(FREInvoiceLifecycle."Entry No.");
FREInvoiceLifecycleError.Run(FREInvoiceLifecycle);

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.34.4

{
Caption = 'FR Sender Platform Scheme';
DataClassification = SystemMetadata;
InitValue = '0238';

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{🔴\ Critical\ Severity\ —\ Upgrade}$

InitValue = '0238' was added on a new field in a tableextension over the existing E-Document Service table, but this app ships no upgrade routine to back-fill existing service records. On version upgrade, pre-existing rows will keep the datatype default (''), not '0238', even though the new lifecycle code later reads and validates this field for FR services. Add an upgrade step that sets FR Sender Platform Scheme to '0238' for existing relevant service rows, guarded by an upgrade tag.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.34.4

Extensible = true;
Access = Public;
DefaultImplementation = IConsentManager = "Consent Manager Default Impl.";
DefaultImplementation = IConsentManager = "Consent Manager Default Impl.",

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\ —\ Web\ Services}$

This PR introduces production callers for EDocumentMessageAPI.SendMessage()/GetMessageResponse(), but the only Service Integration values that now implement IMessageSender/IMessageResponseHandler are test-only mocks. Real integrations still fall back to the new default transport, which raises "does not support sending E-Document messages", so the new French buyer-response/lifecycle flows can pass tests and then fail at runtime for actual services. Add message-transport implementations to the production integrations that should carry these messages, or block these flows for integrations that only support document send/receive.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.34.4

@djukicmilica

Copy link
Copy Markdown
Contributor Author

Superseded by #10437, which is now the authoritative implementation for AB#637593.

#10437 intentionally uses a smaller normalized message model. The broader functionality explored here, including VAT allocation storage, sender-platform configuration, dedicated retry/history surfaces, manual XML import, and asynchronous buyer-response polling, is not part of the replacement and should be tracked in separate follow-up work items if still required.

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 Ownership: Needs Review Ownership is Other, low confidence, or needs manual correction Team: Other GitHub request for other area than SCM, Finance or Integration

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant