Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b613f0f
Enhance contact search functionality and improve user instructions
tomasevicst Aug 6, 2026
2868cdf
Enhance contact handling by improving error messages and refining ema…
tomasevicst Aug 7, 2026
b0c3487
Update codeunit number for SOA Contact Search implementation
tomasevicst Aug 7, 2026
47d11fb
Fix codeunit number for SOA Contact Search implementation
tomasevicst Aug 7, 2026
4dc593f
Enhance SOA Send Reply functionality by adding BCC handling, improvin…
tomasevicst Aug 7, 2026
2dd4ee7
Enhance SOA functionality by adding contact override trust validation…
tomasevicst Aug 7, 2026
87e956c
Enhance SOA functionality by adding validation for message access and…
tomasevicst Aug 8, 2026
625ba05
Merge branch 'main' into bugs/640657_SOA_Mapped_Contact_Enhancement
tomasevicst Aug 8, 2026
4efb73e
Refactor contact email handling to use ReadCommitted isolation level …
tomasevicst Aug 8, 2026
36c2579
Merge branch 'main' into bugs/640657_SOA_Mapped_Contact_Enhancement
tomasevicst Aug 11, 2026
8d91b13
Refactor authorization checks in SOA Reply and Filters to use central…
tomasevicst Aug 11, 2026
3be80eb
Update telemetry logging to include specific action identifier in SOA…
tomasevicst Aug 11, 2026
a78c47f
Enhance SOA Contact handling and email reply logic
tomasevicst Aug 11, 2026
44c9fb7
Rename parameter in OnBeforeFindRecord procedure for clarity
tomasevicst Aug 11, 2026
32c6d92
Refactor error message for missing mapped contact email to improve cl…
tomasevicst Aug 11, 2026
d4ba661
Rename parameter in FindRecordContactFromList procedure for consistency
tomasevicst Aug 12, 2026
4609d17
Merge branch 'main' into bugs/640657_SOA_Mapped_Contact_Enhancement
tomasevicst Aug 12, 2026
f15d955
Add "E-Mail 2" key to Contact table across multiple layers
tomasevicst Aug 12, 2026
1246059
Fix error handling in Contact email validation to exit gracefully
tomasevicst Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -143,25 +143,26 @@
"steps_include_numbering": "true",
"steps": [
{
"value": "Navigate to the contact list page and use the search function to find the contact.",
"value": "Navigate to the contact list page. **If there is only one contact record displayed, select it directly without using the search function. If there are multiple contacts, use the search function to find the correct contact.**",
"steps_include_numbering": "true",
"steps": [
"{% if page.id == 5052 -%}",
"Use information available to you from the conversation history one by one, starting with the email address, sender's name, company name, phone number, etc.",
"Do not select a contact without performing a search first.",
"If there are multiple contacts, do not select a contact without performing a search first.",
"{% endif -%}"
]
},
{
"value": "If the contact is not found, navigate to the customer list page and use the search function to find the customer.",
"value": "If the contact is not found, navigate to the customer list page. **If there is only one customer record displayed, select it directly without using the search function. If there are multiple customers, use the search function to find the correct customer.**",
"steps_include_numbering": "true",
"steps": [
"{% if page.id == 22 -%}",
"Use information available to you from the conversation history one by one, starting with the email address, sender's name, company name, phone number, etc.",
"Do not select a customer without performing a search first.",
"If there are multiple customers, do not select a customer without performing a search first.",
"{% endif -%}"
]
},
"**Important:** Once you have selected a contact or customer record from the search results, proceed with sales quote creation even if the contact's or customer's name or email address does not exactly match the conversation history. Incoming emails can be mapped to another contact for response routing. The selected record is authoritative; do not request assistance only because of this mismatch.",
"If neither the contact nor the customer is found, then request for assistance."
]
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------
namespace Microsoft.Agent.SalesOrderAgent;

using Microsoft.CRM.Contact;
using System.Agents;

codeunit 4412 "SOA Contact Search Impl"
{
Access = Internal;
EventSubscriberInstance = Manual;
InherentEntitlements = X;
InherentPermissions = X;

var
AgentTaskID: BigInteger;

internal procedure SetAgentTaskID(NewAgentTaskID: BigInteger)
begin
AgentTaskID := NewAgentTaskID;
end;

[EventSubscriber(ObjectType::Page, Page::"Contact List", OnBeforeFindRecord, '', false, false)]
local procedure FindRecordContactFromList(var Rec: Record Contact; Which: Text; var Found: Boolean; var IsHandled: Boolean)
Comment thread
tomasevicst marked this conversation as resolved.
Outdated
begin
Comment thread
tomasevicst marked this conversation as resolved.
FindRecordContact(Rec, Which, Found, IsHandled);
end;

local procedure FindRecordContact(var Rec: Record Contact; Which: Text; var Found: Boolean; var IsHandled: Boolean)
var
AgentTaskMessage: Record "Agent Task Message";
SOATaskContactOverride: Record "SOA Task Contact Override";
OriginalFilterGroup: Integer;
begin
if AgentTaskID = 0 then
exit;

AgentTaskMessage.SetLoadFields(ID);
AgentTaskMessage.SetRange("Task ID", AgentTaskID);
AgentTaskMessage.SetRange(Type, AgentTaskMessage.Type::Input);
AgentTaskMessage.SetFilter(Status, '<>%1&<>%2', AgentTaskMessage.Status::Discarded, AgentTaskMessage.Status::Rejected);
AgentTaskMessage.SetCurrentKey("Task ID", SystemCreatedAt);
AgentTaskMessage.Ascending(false);
if not AgentTaskMessage.FindFirst() then
exit;

if not SOATaskContactOverride.Get(AgentTaskID, AgentTaskMessage.ID) then
exit;

OriginalFilterGroup := Rec.FilterGroup();
Rec.FilterGroup(11);
Comment thread
tomasevicst marked this conversation as resolved.
Rec.SetRange("No.", SOATaskContactOverride."Contact No.");
Rec.FilterGroup(OriginalFilterGroup);
Found := Rec.Find(Which);
IsHandled := true;
end;
}
Original file line number Diff line number Diff line change
Expand Up @@ -419,12 +419,7 @@ page 4404 "SOA Email Message"
exit(true);
end;

Contact.SetFilter("E-Mail", SOAFiltersImpl.GetSafeFromEmailFilter(EmailAddress));
ContactCount := Contact.Count();
if not Contact.FindFirst() then
exit(false);

exit(true);
exit(SOAFiltersImpl.FindContactByEmail(Contact, EmailAddress, ContactCount));
end;

local procedure GetSOAEmail(var AgentTaskMessage: Record "Agent Task Message"): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#pragma warning disable AS0007
namespace Microsoft.Agent.SalesOrderAgent;

using Microsoft.CRM.Contact;
using System.Agents;
using System.Email;
using System.Telemetry;
Expand All @@ -24,7 +25,11 @@ codeunit 4419 "SOA Send Reply"
AgentMessage: Codeunit "Agent Message";
Email: Codeunit Email;
EmailMessage: Codeunit "Email Message";
BCCRecipients: List of [Text];
CCRecipients: List of [Text];
ToRecipients: List of [Text];
Body: Text;
MappedContactEmail: Text;
Subject: Text;
begin
Rec.Get(Rec."Task ID", Rec.ID);
Expand All @@ -36,11 +41,23 @@ codeunit 4419 "SOA Send Reply"

Subject := StrSubstNo(EmailSubjectTxt, InputAgentTaskMessage."Task ID");
Body := AgentMessage.GetText(Rec);
EmailMessage.CreateReplyAll(Subject, Body, true, InputAgentTaskMessage."External ID");
MappedContactEmail := GetMappedContactEmail(InputAgentTaskMessage);
Comment thread
tomasevicst marked this conversation as resolved.
Comment thread
tomasevicst marked this conversation as resolved.

if MappedContactEmail <> '' then begin
ToRecipients.Add(MappedContactEmail);
GetOriginEmailRecipients(InputAgentTaskMessage, CCRecipients, BCCRecipients);
Comment thread
tomasevicst marked this conversation as resolved.
Outdated
EmailMessage.CreateReply(ToRecipients, Subject, Body, true, InputAgentTaskMessage."External ID", CCRecipients, BCCRecipients);
end else
EmailMessage.CreateReplyAll(Subject, Body, true, InputAgentTaskMessage."External ID");

AddMessageAttachments(EmailMessage, Rec);

if not Email.ReplyAll(EmailMessage, SOASetup."Email Account ID", SOASetup."Email Connector") then
Error(EmailReplyFailedErr);
if MappedContactEmail <> '' then begin
if not Email.Reply(EmailMessage, SOASetup."Email Account ID", SOASetup."Email Connector") then
Error(EmailReplyFailedErr);
end else
if not Email.ReplyAll(EmailMessage, SOASetup."Email Account ID", SOASetup."Email Connector") then
Error(EmailReplyFailedErr);

AgentMessage.SetStatusToSent(Rec."Task ID", Rec.ID);
end;
Expand All @@ -53,6 +70,76 @@ codeunit 4419 "SOA Send Reply"
EmailSubjectTxt: Label 'Sales order agent reply to task %1', Comment = '%1 = Agent Task id';
EmailReplyFailedErr: Label 'The email reply could not be sent.';
InvalidReplyMessageErr: Label 'Only reviewed output messages can be sent as replies.';
InvalidMappedContactErr: Label 'The contact mapping for this message is no longer valid. Choose another contact before sending the reply.';
MappedContactEmailMissingErr: Label 'The mapped contact %1 does not have a primary email address. Add an email address to the contact or choose another contact before sending the reply.', Comment = '%1 = Contact No.';
MultipleAlternateEmailMappingsErr: Label 'The sender''s alternate email address is assigned to more than one contact. Remove the duplicate alternate email mappings before sending the reply.';

local procedure GetMappedContactEmail(InputAgentTaskMessage: Record "Agent Task Message"): Text
var
SOATaskContactOverride: Record "SOA Task Contact Override";
Contact: Record Contact;
SOAFiltersImpl: Codeunit "SOA Filters Impl.";
Comment thread
tomasevicst marked this conversation as resolved.
ContactCount: Integer;
begin
if SOATaskContactOverride.Get(InputAgentTaskMessage."Task ID", InputAgentTaskMessage.ID) then begin
Comment thread
tomasevicst marked this conversation as resolved.
if SOATaskContactOverride."Contact No." = '' then
ErrorMappedContact(InvalidMappedContactErr);

Contact.SetLoadFields("E-Mail");
if not Contact.Get(SOATaskContactOverride."Contact No.") then
ErrorMappedContact(InvalidMappedContactErr);
if Contact."E-Mail" = '' then
Comment thread
tomasevicst marked this conversation as resolved.
ErrorMappedContact(StrSubstNo(MappedContactEmailMissingErr, Contact."No."));

exit(Contact."E-Mail");
end;

// Only the alternate email represents a persistent mapping; primary email matches keep the existing Reply All behavior.
Comment thread
tomasevicst marked this conversation as resolved.
if SOAFiltersImpl.FindContactByAlternateEmail(Contact, InputAgentTaskMessage.From, ContactCount) then begin
Comment thread
tomasevicst marked this conversation as resolved.
if ContactCount > 1 then
ErrorMappedContact(MultipleAlternateEmailMappingsErr);
if Contact."E-Mail" = '' then
ErrorMappedContact(StrSubstNo(MappedContactEmailMissingErr, Contact."No."));

exit(Contact."E-Mail");
end;

if ContactCount > 0 then
Comment thread
tomasevicst marked this conversation as resolved.
Outdated
ErrorMappedContact(InvalidMappedContactErr);

exit('');
end;

local procedure ErrorMappedContact(ErrorMessage: Text)
Comment thread
tomasevicst marked this conversation as resolved.
Outdated
var
MappedContactErrorInfo: ErrorInfo;
begin
MappedContactErrorInfo.Message(ErrorMessage);
Error(MappedContactErrorInfo);
end;

local procedure GetOriginEmailRecipients(InputAgentTaskMessage: Record "Agent Task Message"; var CCRecipients: List of [Text]; var BCCRecipients: List of [Text])
Comment thread
tomasevicst marked this conversation as resolved.
Outdated
var
SOAEmail: Record "SOA Email";
EmailInbox: Record "Email Inbox";
OriginEmailMessage: Codeunit "Email Message";
begin
SOAEmail.SetLoadFields("Email Inbox ID");
Comment thread
tomasevicst marked this conversation as resolved.
SOAEmail.SetRange("Task ID", InputAgentTaskMessage."Task ID");
SOAEmail.SetRange("Task Message ID", InputAgentTaskMessage.ID);
if not SOAEmail.FindFirst() then
Comment thread
tomasevicst marked this conversation as resolved.
exit;

EmailInbox.SetLoadFields("Message Id");
if not EmailInbox.Get(SOAEmail."Email Inbox ID") then
exit;

if not OriginEmailMessage.Get(EmailInbox."Message Id") then
exit;

OriginEmailMessage.GetRecipients(Enum::"Email Recipient Type"::Cc, CCRecipients);
OriginEmailMessage.GetRecipients(Enum::"Email Recipient Type"::Bcc, BCCRecipients);
end;

local procedure AddMessageAttachments(var EmailMessage: Codeunit "Email Message"; var AgentTaskMessage: Record "Agent Task Message")
var
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ codeunit 4398 "SOA Task Message"
SentAgentTaskMessage: Record "Agent Task Message";
SOATaskContactOverride: Record "SOA Task Contact Override";
OverrideContact: Record Contact;
SOAFiltersImpl: Codeunit "SOA Filters Impl.";
ContactCount: Integer;
begin
Clear(ToAddress);
if OutputAgentTaskMessage.Type <> OutputAgentTaskMessage.Type::Output then
Expand All @@ -110,29 +112,31 @@ codeunit 4398 "SOA Task Message"
end;
end;

if SOAFiltersImpl.FindContactByEmail(OverrideContact, SentAgentTaskMessage.From, ContactCount) and (ContactCount = 1) then
if OverrideContact."E-Mail" <> '' then begin
ToAddress := OverrideContact."E-Mail";
exit(true);
end;

ToAddress := SentAgentTaskMessage.From;
exit(true);
end;

internal procedure MessageRequiresReview(SOASetup: Record "SOA Setup"; EmailInbox: Record "Email Inbox"; IsFirstMessageInTask: Boolean): Boolean
var
Contact: Record Contact;
SOAFiltersImpl: Codeunit "SOA Filters Impl.";
SOAInputMessageReview: Enum "SOA Input Message Review";
begin
// If we have the same review setting for both registered and unregistered senders,
// then we can skip trying to find the contact.
if SOASetup."Known Sender In. Msg. Review" = SOASetup."Unknown Sender In. Msg. Review" then
SOAInputMessageReview := SOASetup."Known Sender In. Msg. Review"
else begin
else
// Check if the sender is a registered contact
Contact.SetFilter("E-Mail", SOAFiltersImpl.GetSafeFromEmailFilter(EmailInbox."Sender Address"));
Contact.ReadIsolation := IsolationLevel::ReadCommitted;
if Contact.IsEmpty() then
if not SOAFiltersImpl.ContactExistsByEmail(EmailInbox."Sender Address") then
SOAInputMessageReview := SOASetup."Unknown Sender In. Msg. Review"
else
SOAInputMessageReview := SOASetup."Known Sender In. Msg. Review";
end;

case SOAInputMessageReview of
SOAInputMessageReview::"All Messages":
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------
namespace Microsoft.Agent.SalesOrderAgent;

using Microsoft.CRM.Contact;

pageextension 4411 "SOA Contact List Ext" extends "Contact List"
{
layout
{
addafter("E-Mail")
{
field("SOA E-Mail 2"; Rec."E-Mail 2")
{
ApplicationArea = Basic, Suite;
Caption = 'Email 2';
ToolTip = 'Specifies an alternative email address for the contact.';
Visible = IsAgentSession;
}
Comment thread
tomasevicst marked this conversation as resolved.
}
}

trigger OnOpenPage()
var
SOAKPITrackAll: Codeunit "SOA - KPI Track All";
AgentTaskID: BigInteger;
begin
IsAgentSession := SOAKPITrackAll.IsOrderTakerAgentSession(AgentTaskID);
end;

var
IsAgentSession: Boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ pagecustomization "SOA Contact List" customizes "Contact List"
{
Visible = true;
}
modify("SOA E-Mail 2")
{
Visible = true;
}
modify("Fax No.")
{
Visible = true;
Expand Down
Loading
Loading