bcquality/custom/knowledge/integration/deduplicate-inbound-messages-with-an-idempotency-check.good.al
Tharanga Chandrasekara 07140e2223 Add new action skills for AL testing and documentation
- Introduced `al-test-writer` to generate AL test codeunits for production objects based on TDD principles.
- Added `al-userguide-test-writer` to create test codeunits from user guide steps, mapping actions and assertions.
- Implemented `bc-extension-test-guide` to generate a comprehensive TEST_GUIDE.md for Business Central extensions, covering various categories.
- Created `bc-webclient-runner` to automate UI testing of the Business Central web client, capturing screenshots and asserting UI states.
- Developed `page-scripting-e2e` to produce a recording plan for Page Scripting, ensuring a structured approach to browser-level testing.
2026-06-07 12:26:47 +12:00

67 lines
3.2 KiB
AL

// Best practice: deduplicate on the source system's stable id (External Reference + Type)
// BEFORE staging, backed by a unique key. A replay returns the prior result instead of
// being processed again. The internal Message ID is never the dedup key, because it is
// freshly generated per insert and so could never match a repeat.
table 50140 "Integration Message"
{
DataClassification = CustomerContent;
fields
{
field(1; "Message ID"; Guid) { Caption = 'Message ID'; }
field(2; Direction; Enum "Integration Direction") { Caption = 'Direction'; }
field(3; "Type"; Code[40]) { Caption = 'Type'; }
field(4; Status; Enum "Integration Status") { Caption = 'Status'; }
// The source-controlled stable id. This, not Message ID, is what dedup keys on.
field(5; "External Reference"; Text[100]) { Caption = 'External Reference'; }
}
keys
{
key(PK; "Message ID") { Clustered = true; }
// UNIQUE idempotency key: a second concurrent insert of the same delivery fails at
// the database, so dedup holds even under a race, not only on the explicit lookup.
key(Idempotency; "External Reference", "Type") { Unique = true; }
}
}
codeunit 50141 "Inbound Dedup"
{
procedure Stage(ExternalRef: Text[100]; MsgType: Code[40]; Payload: Text): Guid
var
Existing: Record "Integration Message";
IntegrationMessage: Record "Integration Message";
begin
// The idempotency lookup: a single indexed read on the unique key.
Existing.SetRange("External Reference", ExternalRef);
Existing.SetRange(Type, MsgType);
if Existing.FindFirst() then begin
case Existing.Status of
Existing.Status::Resolved:
// Already processed. Return the prior result; do NOT do the work again.
exit(Existing."Message ID");
Existing.Status::"In Progress":
// A run is already handling this exact external reference. Reject the
// second one rather than process the same message concurrently.
Error('Message %1 of type %2 is already in progress', ExternalRef, MsgType);
end;
// Any other prior state (for example Failed): return the existing row so the
// resolution flow handles it, instead of minting a duplicate.
exit(Existing."Message ID");
end;
// No prior message exists: stage a genuinely new one.
IntegrationMessage.Init();
IntegrationMessage."Message ID" := CreateGuid(); // internal id, never the dedup key
IntegrationMessage.Direction := IntegrationMessage.Direction::Inbound;
IntegrationMessage."External Reference" := ExternalRef;
IntegrationMessage.Type := MsgType;
IntegrationMessage.Status := IntegrationMessage.Status::New;
IntegrationMessage.SetRequest(Payload);
// If a concurrent request slipped past the lookup, the unique key makes THIS Insert
// fail rather than create a duplicate. Either way, the side effect runs at most once.
IntegrationMessage.Insert(true);
exit(IntegrationMessage."Message ID");
end;
}