Add French E-Document lifecycle messages - #10426
Add French E-Document lifecycle messages#10426Milica Đukić (djukicmilica) wants to merge 9 commits into
Conversation
|
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' |
|
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") |
There was a problem hiding this comment.
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
| DefaultImplementation = IConsentManager = "Consent Manager Default Impl.", | ||
| IMessageSender = "E-Doc. Msg. Transport Default", | ||
| IMessageResponseHandler = "E-Doc. Msg. Transport Default"; |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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]) |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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
| page 10972 "FR E-Invoice Refusal Dialog" | ||
| { | ||
| Caption = 'Refuse E-Invoice'; | ||
| PageType = StandardDialog; |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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]) |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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
| asserterror FREInvoiceLifecycleWorker.Run(FREInvoiceLifecycle); | ||
| FREInvoiceLifecycle.Get(FREInvoiceLifecycle."Entry No."); | ||
| MessageEntryNo := FREInvoiceLifecycle."E-Document Message Entry No."; | ||
| FREInvoiceLifecycleError.Run(FREInvoiceLifecycle); |
There was a problem hiding this comment.
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.
| 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
| asserterror FREInvoiceLifecycleWorker.Run(FREInvoiceLifecycle); | ||
| FREInvoiceLifecycle.Get(FREInvoiceLifecycle."Entry No."); | ||
| FREInvoiceLifecycleError.Run(FREInvoiceLifecycle); |
There was a problem hiding this comment.
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.
| 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'; |
There was a problem hiding this comment.
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.", |
There was a problem hiding this comment.
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
|
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. |
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
CollectedandNegative Collectedpayment lifecycle capture, CDAR XML generation, service routing, retries, and VAT breakdown storage.Submitted,Accepted, and technicalRejectedlifecycle response parsing, payload retention, duplicate protection, connector API, and manual XML import.AcceptedandRefusedresponses for incoming purchase invoices, including reason validation and asynchronous response polling.