diff --git a/samples/BCAgents/SalesReturnAgent/app/.resources/Instructions/InstructionsV1.md b/samples/BCAgents/SalesReturnAgent/app/.resources/Instructions/InstructionsV1.md
new file mode 100644
index 00000000..60b38d68
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/app/.resources/Instructions/InstructionsV1.md
@@ -0,0 +1,36 @@
+You are acting as a Sales Returns Agent. You receive return requests from customers via email. You are responsible for handling the tasks in this area like creating credit memos and handling customer requests. The following are the responsibilities, processing guidelines, and instructions you need to follow.
+
+# **Communication**
+- Your replies are sent back to the customer who emailed you. Write them as responses to the customer.
+- When you encounter processing issues (e.g. cannot verify an invoice, items do not match, data discrepancies), do not reply to the customer. Instead, request assistance from the Business Central user so they can investigate.
+- When requesting approval to post a credit memo, request assistance from the Business Central user — not the customer.
+
+# **Guidelines**
+1. Do not post any draft credit memo's without asking for approval from the Business Central user.
+2. If customer number is not provided but there is a customer name or an email, first go to the customers list and search either by name or email and memorize the customer number.
+3. If no unit of measure can be looked up, try with PCS.
+
+# **Instructions**
+## **Validating the original invoice**
+1.0. Before creating a credit memo, verify the original sale:
+1.0.1. If a sales invoice is attached to the message, do not trust it at face value. Extract the customer, items, quantities, and date from the attachment, then search posted sales invoices in the system for a matching record. Only proceed if you find a posted invoice that matches. If the attachment does not match any posted invoice, treat it as unverified and follow step 1.0.4.
+1.0.2. If a sales invoice number or date is referenced in the message, look it up in posted sales invoices and confirm the items and quantities. If the referenced invoice number does not exist, request assistance from the Business Central user.
+1.0.3. If no invoice is provided but the customer and items are known, search posted sales invoices for that customer to find a matching transaction.
+1.0.4. If no matching invoice or shipment can be found, request assistance from the Business Central user explaining what was searched and what could not be verified. Do not create a credit memo.
+1.0.5. If a matching invoice is found but the items the customer wants to return are not in that invoice, request assistance from the Business Central user. Do not create a credit memo.
+1.0.6. If multiple invoices could match and you cannot determine which one, request assistance from the Business Central user.
+
+## **Handling returns**
+1.1. Create a draft credit memo for that customer with the items they want to be returned.
+1.1.1. Fill in the relevant lines. If the credit memo has been created as a correction of an existing invoice, ensure that the quantities and items match what is being returned. If the customer is doing a partial return, this should be reflected by removing the credit memo lines that are not being returned.
+1.1.2. Set the "Applies-to Doc. No." on the credit memo to link it to the original posted invoice.
+1.1.3. Fill out the work description with what is being returned and justification why. Also add any text that the customer may have provided as the reason.
+
+## **Requesting review and approval to post**
+1.2. After the draft credit memo is created, request a review of the credit memo from the Business Central user. You should also request to approve posting it. Do not post the credit memo until the user has reviewed it.
+1.2.1. If the Business Central user declines the review or requests changes, update the credit memo according to their feedback and request a new review.
+1.2.2. Continue this review cycle until the Business Central user approves the credit memo.
+
+## **Posting and responding to the customer**
+1.3. Once the Business Central user has approved the credit memo, post it.
+1.4. After posting, print the credit memo and attach the PDF to a reply to the customer, summarizing what was returned and why.
diff --git a/samples/BCAgents/SalesReturnAgent/app/ExtensionLogo.png b/samples/BCAgents/SalesReturnAgent/app/ExtensionLogo.png
new file mode 100644
index 00000000..689824d3
Binary files /dev/null and b/samples/BCAgents/SalesReturnAgent/app/ExtensionLogo.png differ
diff --git a/samples/BCAgents/SalesReturnAgent/app/Integration/SalesRetAgentInstall.Codeunit.al b/samples/BCAgents/SalesReturnAgent/app/Integration/SalesRetAgentInstall.Codeunit.al
new file mode 100644
index 00000000..2e9bc6ad
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/app/Integration/SalesRetAgentInstall.Codeunit.al
@@ -0,0 +1,64 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace SalesReturnAgent.Integration;
+
+using SalesReturnAgent.Setup;
+using System.Agents;
+using System.AI;
+
+codeunit 53702 "Sales Ret. Agent Install"
+{
+ Subtype = Install;
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ trigger OnInstallAppPerDatabase()
+ var
+ SalesRetAgentSetupRec: Record "Sales Ret. Agent Setup";
+ begin
+ RegisterCapability();
+
+ if not SalesRetAgentSetupRec.FindSet() then
+ exit;
+
+ repeat
+ InstallAgent(SalesRetAgentSetupRec);
+ until SalesRetAgentSetupRec.Next() = 0;
+ end;
+
+ local procedure InstallAgent(var SalesRetAgentSetupRec: Record "Sales Ret. Agent Setup")
+ begin
+ InstallAgentInstructions(SalesRetAgentSetupRec);
+ end;
+
+ local procedure InstallAgentInstructions(var SalesRetAgentSetupRec: Record "Sales Ret. Agent Setup")
+ var
+ Agent: Codeunit Agent;
+ SalesRetAgentSetup: Codeunit "Sales Ret. Agent Setup";
+ begin
+ Agent.SetInstructions(SalesRetAgentSetupRec."User Security ID", SalesRetAgentSetup.GetInstructions());
+ end;
+
+ [EventSubscriber(ObjectType::Page, Page::"Copilot AI Capabilities", 'OnRegisterCopilotCapability', '', false, false)]
+ local procedure OnRegisterCopilotCapability()
+ begin
+ RegisterCapability();
+ end;
+
+ local procedure RegisterCapability()
+ var
+ CopilotCapability: Codeunit "Copilot Capability";
+ LearnMoreUrlTok: Label 'https://go.microsoft.com/fwlink/?linkid=2350506', Locked = true;
+ begin
+ if not CopilotCapability.IsCapabilityRegistered(Enum::"Copilot Capability"::"Sales Return Agent") then
+ CopilotCapability.RegisterCapability(
+ Enum::"Copilot Capability"::"Sales Return Agent",
+ Enum::"Copilot Availability"::Preview,
+ "Copilot Billing Type"::"Microsoft Billed",
+ LearnMoreUrlTok);
+ end;
+}
diff --git a/samples/BCAgents/SalesReturnAgent/app/Integration/SalesRetCopilotCapability.EnumExt.al b/samples/BCAgents/SalesReturnAgent/app/Integration/SalesRetCopilotCapability.EnumExt.al
new file mode 100644
index 00000000..cc77ab46
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/app/Integration/SalesRetCopilotCapability.EnumExt.al
@@ -0,0 +1,16 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace SalesReturnAgent.Integration;
+
+using System.AI;
+
+enumextension 53700 "Sales Ret. Copilot Capability" extends "Copilot Capability"
+{
+ value(53700; "Sales Return Agent")
+ {
+ Caption = 'Sales Return Agent';
+ }
+}
diff --git a/samples/BCAgents/SalesReturnAgent/app/Setup/KPI/SalesRetAgentKPI.Page.al b/samples/BCAgents/SalesReturnAgent/app/Setup/KPI/SalesRetAgentKPI.Page.al
new file mode 100644
index 00000000..ea40ea84
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/app/Setup/KPI/SalesRetAgentKPI.Page.al
@@ -0,0 +1,63 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace SalesReturnAgent.Setup.KPI;
+
+page 53701 "Sales Ret. Agent KPI"
+{
+ PageType = CardPart;
+ ApplicationArea = All;
+ Caption = 'Sales Return Agent Summary';
+ SourceTable = "Sales Ret. Agent KPI";
+ Editable = false;
+ Extensible = false;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ layout
+ {
+ area(Content)
+ {
+ cuegroup(KeyMetrics)
+ {
+ Caption = 'Key Performance Indicators';
+
+ field(CreditMemosCreated; Rec."Credit Memos Created")
+ {
+ Caption = 'Credit Memos Created';
+ ToolTip = 'Specifies the number of sales credit memos created by the agent.';
+ }
+ }
+ }
+ }
+
+ trigger OnOpenPage()
+ begin
+ GetRelevantAgent();
+ end;
+
+ ///
+ /// Retrieves the relevant agent's KPI record for display.
+ /// This page is launched via IAgentMetadata.GetSummaryPageId(). The platform sets a filter on the
+ /// "User Security ID" field before opening the page, so the source record may not be fully populated
+ /// on open - the filter is evaluated here to resolve and load the correct record.
+ ///
+ local procedure GetRelevantAgent()
+ var
+ UserSecurityIDFilter: Text;
+ begin
+ if IsNullGuid(Rec."User Security ID") then begin
+ UserSecurityIDFilter := Rec.GetFilter("User Security ID");
+ if not Evaluate(Rec."User Security ID", UserSecurityIDFilter) then
+ Error(AgentDoesNotExistErr);
+ end;
+
+ if not Rec.Get(Rec."User Security ID") then
+ Rec.Insert();
+ end;
+
+ var
+ AgentDoesNotExistErr: Label 'The agent does not exist. Please check the configuration.';
+}
diff --git a/samples/BCAgents/SalesReturnAgent/app/Setup/KPI/SalesRetAgentKPI.Table.al b/samples/BCAgents/SalesReturnAgent/app/Setup/KPI/SalesRetAgentKPI.Table.al
new file mode 100644
index 00000000..d59ea7ee
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/app/Setup/KPI/SalesRetAgentKPI.Table.al
@@ -0,0 +1,43 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace SalesReturnAgent.Setup.KPI;
+
+table 53701 "Sales Ret. Agent KPI"
+{
+ Access = Internal;
+ Caption = 'Sales Ret. Agent KPI';
+ DataClassification = SystemMetadata;
+ InherentEntitlements = RIMX;
+ InherentPermissions = RIMX;
+ ReplicateData = false;
+ DataPerCompany = false;
+
+ fields
+ {
+ // This field is part of the IAgentMetadata.GetSummaryPageId() contract.
+ // The platform filters on "User Security ID" when opening the summary page,
+ // so it must be the primary key of this table.
+ field(1; "User Security ID"; Guid)
+ {
+ Caption = 'User Security ID';
+ ToolTip = 'Specifies the unique identifier for the agent user.';
+ Editable = false;
+ }
+ field(10; "Credit Memos Created"; Integer)
+ {
+ Caption = 'Credit Memos Created';
+ ToolTip = 'Specifies the number of sales credit memos created by the agent.';
+ DataClassification = CustomerContent;
+ }
+ }
+ keys
+ {
+ key(Key1; "User Security ID")
+ {
+ Clustered = true;
+ }
+ }
+}
diff --git a/samples/BCAgents/SalesReturnAgent/app/Setup/KPI/SalesRetAgentKPILogging.Codeunit.al b/samples/BCAgents/SalesReturnAgent/app/Setup/KPI/SalesRetAgentKPILogging.Codeunit.al
new file mode 100644
index 00000000..fb101977
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/app/Setup/KPI/SalesRetAgentKPILogging.Codeunit.al
@@ -0,0 +1,65 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace SalesReturnAgent.Setup.KPI;
+
+using Microsoft.Sales.Document;
+using SalesReturnAgent.Setup;
+using System.Agents;
+
+///
+/// Subscribes to business events to automatically log KPI metrics
+/// for the Sales Return Agent.
+///
+/// - Credit Memos Created: incremented each time the agent creates a sales credit memo.
+///
+codeunit 53704 "Sales Ret. Agent KPI Logging"
+{
+ Access = Internal;
+ EventSubscriberInstance = StaticAutomatic;
+ SingleInstance = true;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ [EventSubscriber(ObjectType::Table, Database::"Sales Header", OnAfterInsertEvent, '', false, false)]
+ local procedure OnAfterInsertSalesHeader(var Rec: Record "Sales Header"; RunTrigger: Boolean)
+ var
+ AgentSession: Codeunit "Agent Session";
+ SalesRetAgentSetup: Codeunit "Sales Ret. Agent Setup";
+ AgentUserSecurityId: Guid;
+ AgentMetadataProvider: Enum "Agent Metadata Provider";
+ begin
+ if not AgentSession.IsAgentSession(AgentMetadataProvider) then
+ exit;
+
+ if AgentMetadataProvider <> Enum::"Agent Metadata Provider"::"Sales Return Agent" then
+ exit;
+
+ if Rec."Document Type" <> Rec."Document Type"::"Credit Memo" then
+ exit;
+
+ if not SalesRetAgentSetup.TryGetAgent(AgentUserSecurityId) then
+ exit;
+
+ if UserSecurityId() <> AgentUserSecurityId then
+ exit;
+
+ UpdateKPI(AgentUserSecurityId, 1);
+ end;
+
+ local procedure UpdateKPI(AgentUserSecurityId: Guid; CreatedIncrement: Integer)
+ var
+ SalesRetAgentKPI: Record "Sales Ret. Agent KPI";
+ begin
+ if not SalesRetAgentKPI.Get(AgentUserSecurityId) then begin
+ SalesRetAgentKPI.Init();
+ SalesRetAgentKPI."User Security ID" := AgentUserSecurityId;
+ SalesRetAgentKPI.Insert();
+ end;
+
+ SalesRetAgentKPI."Credit Memos Created" += CreatedIncrement;
+ SalesRetAgentKPI.Modify();
+ end;
+}
diff --git a/samples/BCAgents/SalesReturnAgent/app/Setup/Metadata/SalesRetAgentFactory.Codeunit.al b/samples/BCAgents/SalesReturnAgent/app/Setup/Metadata/SalesRetAgentFactory.Codeunit.al
new file mode 100644
index 00000000..aaf61277
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/app/Setup/Metadata/SalesRetAgentFactory.Codeunit.al
@@ -0,0 +1,54 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace SalesReturnAgent.Setup.Metadata;
+
+using SalesReturnAgent.Setup;
+using System.Agents;
+using System.AI;
+using System.Reflection;
+using System.Security.AccessControl;
+
+codeunit 53700 SalesRetAgentFactory implements IAgentFactory
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure GetDefaultInitials(): Text[4]
+ begin
+ exit(SalesRetAgentSetup.GetInitials());
+ end;
+
+ procedure GetFirstTimeSetupPageId(): Integer
+ begin
+ exit(SalesRetAgentSetup.GetSetupPageId());
+ end;
+
+ procedure ShowCanCreateAgent(): Boolean
+ var
+ SalesRetAgentSetupRec: Record "Sales Ret. Agent Setup";
+ begin
+ exit(SalesRetAgentSetupRec.IsEmpty());
+ end;
+
+ procedure GetCopilotCapability(): Enum "Copilot Capability"
+ begin
+ exit("Copilot Capability"::"Sales Return Agent");
+ end;
+
+ procedure GetDefaultProfile(var TempAllProfile: Record "All Profile" temporary)
+ begin
+ SalesRetAgentSetup.GetDefaultProfile(TempAllProfile);
+ end;
+
+ procedure GetDefaultAccessControls(var TempAccessControlTemplate: Record "Access Control Buffer" temporary)
+ begin
+ SalesRetAgentSetup.GetDefaultAccessControls(TempAccessControlTemplate);
+ end;
+
+ var
+ SalesRetAgentSetup: Codeunit "Sales Ret. Agent Setup";
+}
diff --git a/samples/BCAgents/SalesReturnAgent/app/Setup/Metadata/SalesRetAgentMetadata.Codeunit.al b/samples/BCAgents/SalesReturnAgent/app/Setup/Metadata/SalesRetAgentMetadata.Codeunit.al
new file mode 100644
index 00000000..f3dfc464
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/app/Setup/Metadata/SalesRetAgentMetadata.Codeunit.al
@@ -0,0 +1,44 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace SalesReturnAgent.Setup.Metadata;
+
+using SalesReturnAgent.Setup;
+using System.Agents;
+
+codeunit 53701 SalesRetAgentMetadata implements IAgentMetadata
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure GetInitials(AgentUserId: Guid): Text[4]
+ begin
+ exit(SalesRetAgentSetup.GetInitials());
+ end;
+
+ procedure GetSetupPageId(AgentUserId: Guid): Integer
+ begin
+ exit(SalesRetAgentSetup.GetSetupPageId());
+ end;
+
+ procedure GetSummaryPageId(AgentUserId: Guid): Integer
+ begin
+ exit(SalesRetAgentSetup.GetSummaryPageId());
+ end;
+
+ procedure GetAgentTaskMessagePageId(AgentUserId: Guid; MessageId: Guid): Integer
+ begin
+ exit(Page::"Agent Task Message Card");
+ end;
+
+ procedure GetAgentAnnotations(AgentUserId: Guid; var Annotations: Record "Agent Annotation")
+ begin
+ Clear(Annotations);
+ end;
+
+ var
+ SalesRetAgentSetup: Codeunit "Sales Ret. Agent Setup";
+}
diff --git a/samples/BCAgents/SalesReturnAgent/app/Setup/Metadata/SalesRetMetadataProvider.EnumExt.al b/samples/BCAgents/SalesReturnAgent/app/Setup/Metadata/SalesRetMetadataProvider.EnumExt.al
new file mode 100644
index 00000000..3be1539f
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/app/Setup/Metadata/SalesRetMetadataProvider.EnumExt.al
@@ -0,0 +1,17 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace SalesReturnAgent.Setup.Metadata;
+
+using System.Agents;
+
+enumextension 53701 "Sales Ret. Metadata Provider" extends "Agent Metadata Provider"
+{
+ value(53701; "Sales Return Agent")
+ {
+ Caption = 'Sales Return Agent';
+ Implementation = IAgentFactory = SalesRetAgentFactory, IAgentMetadata = SalesRetAgentMetadata;
+ }
+}
diff --git a/samples/BCAgents/SalesReturnAgent/app/Setup/Profile/SRAccountReceivables.PageCust.al b/samples/BCAgents/SalesReturnAgent/app/Setup/Profile/SRAccountReceivables.PageCust.al
new file mode 100644
index 00000000..17b442b5
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/app/Setup/Profile/SRAccountReceivables.PageCust.al
@@ -0,0 +1,26 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace SalesReturnAgent.Setup.Profile;
+
+using Microsoft.Finance.RoleCenters;
+
+pagecustomization "SR Account Receivables" customizes "Account Receivables"
+{
+ ClearLayout = true;
+ ClearActions = true;
+
+ actions
+ {
+ modify(Customers)
+ {
+ Visible = true;
+ }
+ modify("Posted Sales Invoices")
+ {
+ Visible = true;
+ }
+ }
+}
diff --git a/samples/BCAgents/SalesReturnAgent/app/Setup/Profile/SRAgent.Profile.al b/samples/BCAgents/SalesReturnAgent/app/Setup/Profile/SRAgent.Profile.al
new file mode 100644
index 00000000..64431a59
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/app/Setup/Profile/SRAgent.Profile.al
@@ -0,0 +1,23 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace SalesReturnAgent.Setup.Profile;
+
+using Microsoft.Finance.RoleCenters;
+
+profile "SR Agent"
+{
+ Caption = 'Sales Return Agent (Copilot)';
+ Enabled = false;
+ ProfileDescription = 'Functionality for the Sales Return Agent to efficiently create credit memos for customer returns.';
+ Promoted = false;
+ RoleCenter = "Account Receivables";
+ Customizations =
+ "SR Account Receivables",
+ "SR Posted Sales Invoices",
+ "SR Sales Credit Memo",
+ "SR Sales Cr. Memo Subform",
+ "SR Customer List";
+}
diff --git a/samples/BCAgents/SalesReturnAgent/app/Setup/Profile/SRCustomerList.PageCust.al b/samples/BCAgents/SalesReturnAgent/app/Setup/Profile/SRCustomerList.PageCust.al
new file mode 100644
index 00000000..2409f043
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/app/Setup/Profile/SRCustomerList.PageCust.al
@@ -0,0 +1,46 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace SalesReturnAgent.Setup.Profile;
+
+using Microsoft.Sales.Customer;
+
+pagecustomization "SR Customer List" customizes "Customer List"
+{
+ ClearLayout = true;
+ ClearActions = true;
+ ClearViews = true;
+ InsertAllowed = false;
+ ModifyAllowed = false;
+ DeleteAllowed = false;
+
+ layout
+ {
+ modify("No.")
+ {
+ Visible = true;
+ }
+ modify(Name)
+ {
+ Visible = true;
+ }
+ modify("Phone No.")
+ {
+ Visible = true;
+ }
+ modify("Balance (LCY)")
+ {
+ Visible = true;
+ }
+ addafter("Phone No.")
+ {
+ field("SR E-Mail"; Rec."E-Mail")
+ {
+ ApplicationArea = All;
+ Visible = true;
+ }
+ }
+ }
+}
diff --git a/samples/BCAgents/SalesReturnAgent/app/Setup/Profile/SRPostedSalesInvoices.PageCust.al b/samples/BCAgents/SalesReturnAgent/app/Setup/Profile/SRPostedSalesInvoices.PageCust.al
new file mode 100644
index 00000000..5206ed1f
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/app/Setup/Profile/SRPostedSalesInvoices.PageCust.al
@@ -0,0 +1,58 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace SalesReturnAgent.Setup.Profile;
+
+using Microsoft.Sales.History;
+
+pagecustomization "SR Posted Sales Invoices" customizes "Posted Sales Invoices"
+{
+ ClearLayout = true;
+ ClearActions = true;
+ ClearViews = true;
+ InsertAllowed = false;
+ ModifyAllowed = false;
+ DeleteAllowed = false;
+
+ layout
+ {
+ modify("No.")
+ {
+ Visible = true;
+ }
+ modify("Sell-to Customer No.")
+ {
+ Visible = true;
+ }
+ modify("Sell-to Customer Name")
+ {
+ Visible = true;
+ }
+ modify("Posting Date")
+ {
+ Visible = true;
+ }
+ modify("Due Date")
+ {
+ Visible = true;
+ }
+ modify("Amount")
+ {
+ Visible = true;
+ }
+ modify("Amount Including VAT")
+ {
+ Visible = true;
+ }
+ }
+
+ actions
+ {
+ modify(CreateCreditMemo_Promoted)
+ {
+ Visible = true;
+ }
+ }
+}
diff --git a/samples/BCAgents/SalesReturnAgent/app/Setup/Profile/SRSalesCrMemoSubform.PageCust.al b/samples/BCAgents/SalesReturnAgent/app/Setup/Profile/SRSalesCrMemoSubform.PageCust.al
new file mode 100644
index 00000000..3b3cb091
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/app/Setup/Profile/SRSalesCrMemoSubform.PageCust.al
@@ -0,0 +1,49 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace SalesReturnAgent.Setup.Profile;
+
+using Microsoft.Sales.Document;
+
+pagecustomization "SR Sales Cr. Memo Subform" customizes "Sales Cr. Memo Subform"
+{
+ ClearLayout = true;
+ ClearActions = true;
+ InsertAllowed = true;
+ ModifyAllowed = true;
+ DeleteAllowed = true;
+
+ layout
+ {
+ modify(Type)
+ {
+ Visible = true;
+ }
+ modify("No.")
+ {
+ Visible = true;
+ }
+ modify(Description)
+ {
+ Visible = true;
+ }
+ modify(Quantity)
+ {
+ Visible = true;
+ }
+ modify("Unit of Measure Code")
+ {
+ Visible = true;
+ }
+ modify("Unit Price")
+ {
+ Visible = true;
+ }
+ modify("Line Amount")
+ {
+ Visible = true;
+ }
+ }
+}
diff --git a/samples/BCAgents/SalesReturnAgent/app/Setup/Profile/SRSalesCreditMemo.PageCust.al b/samples/BCAgents/SalesReturnAgent/app/Setup/Profile/SRSalesCreditMemo.PageCust.al
new file mode 100644
index 00000000..2041191e
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/app/Setup/Profile/SRSalesCreditMemo.PageCust.al
@@ -0,0 +1,90 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace SalesReturnAgent.Setup.Profile;
+
+using Microsoft.Sales.Document;
+
+pagecustomization "SR Sales Credit Memo" customizes "Sales Credit Memo"
+{
+ ClearLayout = true;
+ ClearActions = true;
+ InsertAllowed = true;
+ ModifyAllowed = true;
+ DeleteAllowed = false;
+
+ layout
+ {
+ modify("No.")
+ {
+ Visible = true;
+ }
+ modify("Sell-to Customer No.")
+ {
+ Visible = true;
+ }
+ modify("Sell-to Customer Name")
+ {
+ Visible = true;
+ }
+ modify("Posting Description")
+ {
+ Visible = true;
+ }
+ modify("Posting Date")
+ {
+ Visible = true;
+ }
+ modify("Document Date")
+ {
+ Visible = true;
+ }
+ modify("External Document No.")
+ {
+ Visible = true;
+ }
+ modify("Applies-to Doc. Type")
+ {
+ Visible = true;
+ }
+ modify("Applies-to Doc. No.")
+ {
+ Visible = true;
+ }
+ modify(WorkDescription)
+ {
+ Visible = true;
+ }
+ modify(SalesLines)
+ {
+ Visible = true;
+ }
+ }
+
+ actions
+ {
+ modify(Post)
+ {
+ Visible = true;
+ }
+ modify(Release)
+ {
+ Visible = true;
+ }
+ modify(Reopen)
+ {
+ Visible = true;
+ }
+ modify(DocAttach)
+ {
+ Visible = true;
+ }
+ modify(TestReport)
+ {
+ Visible = true;
+ AboutText = 'Generate PDF Test Report';
+ }
+ }
+}
diff --git a/samples/BCAgents/SalesReturnAgent/app/Setup/SalesRetAgentSetup.Codeunit.al b/samples/BCAgents/SalesReturnAgent/app/Setup/SalesRetAgentSetup.Codeunit.al
new file mode 100644
index 00000000..15941406
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/app/Setup/SalesRetAgentSetup.Codeunit.al
@@ -0,0 +1,146 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace SalesReturnAgent.Setup;
+
+using SalesReturnAgent.Setup.KPI;
+using System.Agents;
+using System.Reflection;
+using System.Security.AccessControl;
+
+codeunit 53703 "Sales Ret. Agent Setup"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure TryGetAgent(var AgentUserSecurityId: Guid): Boolean
+ var
+ SalesRetAgentSetupRec: Record "Sales Ret. Agent Setup";
+ begin
+ if SalesRetAgentSetupRec.FindFirst() then begin
+ AgentUserSecurityId := SalesRetAgentSetupRec."User Security ID";
+ exit(true);
+ end;
+
+ exit(false);
+ end;
+
+ procedure GetInitials(): Text[4]
+ begin
+ exit(AgentInitialsLbl);
+ end;
+
+ procedure GetSetupPageId(): Integer
+ begin
+ exit(Page::"Sales Ret. Agent Setup");
+ end;
+
+ procedure GetSummaryPageId(): Integer
+ begin
+ exit(Page::"Sales Ret. Agent KPI");
+ end;
+
+ procedure GetOrCreateDefaultAgent(): Guid
+ var
+ TempAgentSetupBuffer: Record "Agent Setup Buffer";
+ AgentSetup: Codeunit "Agent Setup";
+ AgentUserSecurityId: Guid;
+ begin
+ if TryGetAgent(AgentUserSecurityId) then
+ exit(AgentUserSecurityId);
+
+ AgentSetup.GetSetupRecord(
+ TempAgentSetupBuffer,
+ AgentUserSecurityId,
+ Enum::"Agent Metadata Provider"::"Sales Return Agent",
+ AgentNameTok + ' - ' + CompanyName(),
+ DefaultDisplayNameTok,
+ AgentSummaryLbl);
+
+ exit(SaveAgent(TempAgentSetupBuffer));
+ end;
+
+ internal procedure SaveAgent(var TempAgentSetupBuffer: Record "Agent Setup Buffer"): Guid
+ var
+ AgentSetup: Codeunit "Agent Setup";
+ AgentUserSecurityId: Guid;
+ IsNewAgent: Boolean;
+ begin
+ IsNewAgent := IsNullGuid(TempAgentSetupBuffer."User Security ID");
+
+ if IsNewAgent then begin
+ // Create: always save new agent
+ TempAgentSetupBuffer."Agent Metadata Provider" := Enum::"Agent Metadata Provider"::"Sales Return Agent";
+ AgentUserSecurityId := AgentSetup.SaveChanges(TempAgentSetupBuffer);
+ Agent.SetInstructions(AgentUserSecurityId, GetInstructions());
+ EnsureSetupExists(AgentUserSecurityId);
+ end else begin
+ // Update: only save if changes were made
+ AgentUserSecurityId := TempAgentSetupBuffer."User Security ID";
+ if AgentSetup.GetChangesMade(TempAgentSetupBuffer) then begin
+ AgentUserSecurityId := AgentSetup.SaveChanges(TempAgentSetupBuffer);
+ Agent.SetInstructions(AgentUserSecurityId, GetInstructions());
+ end;
+ end;
+
+ exit(AgentUserSecurityId);
+ end;
+
+ procedure EnsureSetupExists(UserSecurityID: Guid)
+ var
+ SalesRetAgentSetupRec: Record "Sales Ret. Agent Setup";
+ begin
+ if not SalesRetAgentSetupRec.Get(UserSecurityID) then begin
+ SalesRetAgentSetupRec."User Security ID" := UserSecurityID;
+ SalesRetAgentSetupRec.Insert();
+ end;
+ end;
+
+ [NonDebuggable]
+ procedure GetInstructions(): SecretText
+ var
+ Instructions: Text;
+ begin
+ Instructions := NavApp.GetResourceAsText('Instructions/InstructionsV1.md');
+ exit(Instructions);
+ end;
+
+ procedure GetDefaultProfile(var TempAllProfile: Record "All Profile" temporary)
+ var
+ CurrentModuleInfo: ModuleInfo;
+ begin
+ NavApp.GetCurrentModuleInfo(CurrentModuleInfo);
+ Agent.PopulateDefaultProfile(DefaultProfileTok, CurrentModuleInfo.Id, TempAllProfile);
+ end;
+
+ procedure GetDefaultAccessControls(var TempAccessControlBuffer: Record "Access Control Buffer" temporary)
+ begin
+ Clear(TempAccessControlBuffer);
+ TempAccessControlBuffer."Company Name" := CopyStr(CompanyName(), 1, MaxStrLen(TempAccessControlBuffer."Company Name"));
+ TempAccessControlBuffer.Scope := TempAccessControlBuffer.Scope::System;
+ TempAccessControlBuffer."App ID" := BaseApplicationAppIdTok;
+ TempAccessControlBuffer."Role ID" := D365ReadPermissionSetTok;
+ TempAccessControlBuffer.Insert();
+
+ TempAccessControlBuffer.Init();
+ TempAccessControlBuffer."Company Name" := CopyStr(CompanyName(), 1, MaxStrLen(TempAccessControlBuffer."Company Name"));
+ TempAccessControlBuffer.Scope := TempAccessControlBuffer.Scope::System;
+ TempAccessControlBuffer."App ID" := BaseApplicationAppIdTok;
+ TempAccessControlBuffer."Role ID" := D365SalesPermissionSetTok;
+ TempAccessControlBuffer.Insert();
+ end;
+
+ var
+ Agent: Codeunit Agent;
+ DefaultProfileTok: Label 'SR AGENT', Locked = true;
+ AgentInitialsLbl: Label 'SR', MaxLength = 4;
+ BaseApplicationAppIdTok: Label '437dbf0e-84ff-417a-965d-ed2bb9650972', Locked = true;
+ D365ReadPermissionSetTok: Label 'D365 READ', Locked = true;
+ D365SalesPermissionSetTok: Label 'D365 SALES', Locked = true;
+ AgentNameTok: Label 'Sales Return Agent', Locked = true;
+ DefaultDisplayNameTok: Label 'Sales Return Agent', Locked = true;
+ AgentSummaryLbl: Label 'Creates credit memos for customer returns, populates work descriptions with return justifications, and generates PDF summaries.';
+}
diff --git a/samples/BCAgents/SalesReturnAgent/app/Setup/SalesRetAgentSetup.Page.al b/samples/BCAgents/SalesReturnAgent/app/Setup/SalesRetAgentSetup.Page.al
new file mode 100644
index 00000000..feb32a3b
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/app/Setup/SalesRetAgentSetup.Page.al
@@ -0,0 +1,152 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace SalesReturnAgent.Setup;
+
+using System.Agents;
+using System.AI;
+
+#pragma warning disable AL0906
+page 53700 "Sales Ret. Agent Setup"
+#pragma warning restore AL0906
+{
+ PageType = ConfigurationDialog;
+ Extensible = false;
+ ApplicationArea = All;
+ IsPreview = true;
+ Caption = 'Set up Sales Return Agent';
+ InstructionalText = 'The Sales Return Agent creates credit memos for customer returns, populates work descriptions with return justifications, and generates PDF summaries.';
+ AdditionalSearchTerms = 'Sales Return Agent, Agent';
+ SourceTable = "Sales Ret. Agent Setup";
+ SourceTableTemporary = true;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ layout
+ {
+ area(Content)
+ {
+ part(AgentSetupPart; "Agent Setup Part")
+ {
+ ApplicationArea = All;
+ UpdatePropagation = Both;
+ }
+ group(GetStarted)
+ {
+ Caption = 'Get started';
+ group(HowItWorksGroup)
+ {
+ Caption = 'How it works';
+ InstructionalText = 'Assign tasks to the Sales Return Agent by sending messages. The agent receives return requests from customers via email and creates credit memos accordingly.';
+ }
+ group(LearnMore)
+ {
+ Caption = 'Learn more';
+ InstructionalText = 'To learn more about the Sales Return Agent''s capabilities and how to prepare the data for the agent to complete its tasks, see the documentation article.';
+
+ field(OpenDocumentation; OpenDocumentationTxt)
+ {
+ ShowCaption = false;
+ Editable = false;
+ ToolTip = 'Open the Sales Return Agent documentation article in your browser.';
+
+ trigger OnDrillDown()
+ begin
+ Hyperlink(AgentLearnMoreUrlTok);
+ end;
+ }
+ }
+ }
+ }
+ }
+ actions
+ {
+ area(SystemActions)
+ {
+ systemaction(OK)
+ {
+ Caption = 'Update';
+ Enabled = IsUpdated;
+ ToolTip = 'Apply the changes to the agent setup.';
+ }
+
+ systemaction(Cancel)
+ {
+ Caption = 'Cancel';
+ ToolTip = 'Discards the changes and closes the setup page.';
+ }
+ }
+ }
+
+ trigger OnOpenPage()
+ begin
+ if not AzureOpenAI.IsEnabled(Enum::"Copilot Capability"::"Sales Return Agent") then
+ Error(SalesRetAgentNotEnabledErr);
+
+ IsUpdated := false;
+ InitializePage();
+ end;
+
+ trigger OnAfterGetRecord()
+ begin
+ InitializePage();
+ end;
+
+ trigger OnAfterGetCurrRecord()
+ begin
+ IsUpdated := IsUpdated or CurrPage.AgentSetupPart.Page.GetChangesMade();
+ end;
+
+ trigger OnModifyRecord(): Boolean
+ begin
+ IsUpdated := true;
+ end;
+
+ trigger OnQueryClosePage(CloseAction: Action): Boolean
+ var
+ SalesRetAgentSetup: Codeunit "Sales Ret. Agent Setup";
+ begin
+ if CloseAction = CloseAction::Cancel then
+ exit(true);
+
+ CurrPage.AgentSetupPart.Page.GetAgentSetupBuffer(TempAgentSetupBuffer);
+ Rec."User Security ID" := SalesRetAgentSetup.SaveAgent(TempAgentSetupBuffer);
+ exit(true);
+ end;
+
+ local procedure InitializePage()
+ var
+ AgentSetup: Codeunit "Agent Setup";
+ begin
+ if Rec.IsEmpty() then
+ Rec.Insert();
+
+ CurrPage.AgentSetupPart.Page.GetAgentSetupBuffer(TempAgentSetupBuffer);
+ if TempAgentSetupBuffer.IsEmpty() then
+ AgentSetup.GetSetupRecord(
+ TempAgentSetupBuffer,
+ Rec."User Security ID",
+ Enum::"Agent Metadata Provider"::"Sales Return Agent",
+ AgentNameLbl + ' - ' + CompanyName(),
+ DefaultDisplayNameLbl,
+ AgentSummaryLbl);
+
+ CurrPage.AgentSetupPart.Page.SetAgentSetupBuffer(TempAgentSetupBuffer);
+ CurrPage.AgentSetupPart.Page.Update(false);
+
+ IsUpdated := IsUpdated or CurrPage.AgentSetupPart.Page.GetChangesMade();
+ end;
+
+ var
+ TempAgentSetupBuffer: Record "Agent Setup Buffer";
+ AzureOpenAI: Codeunit "Azure OpenAI";
+ IsUpdated: Boolean;
+ SalesRetAgentNotEnabledErr: Label 'The Sales Return Agent capability is not enabled in Copilot capabilities.\\Please enable the capability before setting up the agent.';
+ AgentNameLbl: Label 'Sales Return Agent';
+ DefaultDisplayNameLbl: Label 'Sales Return Agent';
+ AgentSummaryLbl: Label 'Creates credit memos for customer returns, populates work descriptions with return justifications, and generates PDF summaries.';
+ OpenDocumentationTxt: Label 'Learn more';
+ AgentLearnMoreUrlTok: Label 'https://go.microsoft.com/fwlink/?linkid=2350506', Locked = true;
+}
diff --git a/samples/BCAgents/SalesReturnAgent/app/Setup/SalesRetAgentSetup.Table.al b/samples/BCAgents/SalesReturnAgent/app/Setup/SalesRetAgentSetup.Table.al
new file mode 100644
index 00000000..17dd970e
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/app/Setup/SalesRetAgentSetup.Table.al
@@ -0,0 +1,38 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace SalesReturnAgent.Setup;
+
+table 53700 "Sales Ret. Agent Setup"
+{
+ Access = Internal;
+ Caption = 'Sales Ret. Agent Setup';
+ DataClassification = CustomerContent;
+ InherentEntitlements = RIMDX;
+ InherentPermissions = RIMDX;
+ ReplicateData = false;
+ DataPerCompany = false;
+
+ fields
+ {
+ // The platform uses a field named "User Security ID" to open the setup and summary pages
+ // defined in IAgentMetadata. This field must exist with this exact name on the source table.
+ field(1; "User Security ID"; Guid)
+ {
+ Caption = 'User Security ID';
+ ToolTip = 'Specifies the unique identifier for the agent user.';
+ DataClassification = SystemMetadata;
+ Editable = false;
+ }
+ }
+
+ keys
+ {
+ key(Key1; "User Security ID")
+ {
+ Clustered = true;
+ }
+ }
+}
diff --git a/samples/BCAgents/SalesReturnAgent/app/app.json b/samples/BCAgents/SalesReturnAgent/app/app.json
new file mode 100644
index 00000000..1c2bdf92
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/app/app.json
@@ -0,0 +1,47 @@
+{
+ "id": "25341373-464d-4087-b195-b91bbcdb2045",
+ "name": "Sales Return Agent Sample",
+ "publisher": "Partner",
+ "version": "$(app_currentVersion)",
+ "brief": "An example of the sales return agent, implemented as an app via the AI development toolkit.",
+ "description": "A sample agent app implementing the sales return agent.",
+ "EULA": "https://go.microsoft.com/fwlink/?LinkId=847985",
+ "help": "https://go.microsoft.com/fwlink/?linkid=2344702",
+ "url": "https://go.microsoft.com/fwlink/?linkid=2344702",
+ "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2344702",
+ "privacyStatement": "https://go.microsoft.com/fwlink/?LinkId=724009",
+ "logo": "ExtensionLogo.png",
+ "dependencies": [],
+ "screenshots": [],
+ "platform": "$(app_platformVersion)",
+ "application": "$(app_minimumVersion)",
+ "idRanges": [
+ {
+ "from": 53700,
+ "to": 53739
+ }
+ ],
+ "resourceExposurePolicy": {
+ "allowDebugging": true,
+ "allowDownloadingSource": true,
+ "includeSourceInSymbolFile": true,
+ "applyToDevExtension": true
+ },
+ "features": [
+ "GenerateCaptions",
+ "TranslationFile",
+ "NoImplicitWith",
+ "NoPromotedActionProperties"
+ ],
+ "internalsVisibleTo": [
+ {
+ "id": "45d741e7-0c9e-46b4-828b-550ebd88da2c",
+ "name": "Sales Return Agent Sample Test",
+ "publisher": "Partner"
+ }
+ ],
+ "resourceFolders": [
+ ".resources"
+ ],
+ "target": "Cloud"
+}
diff --git a/samples/BCAgents/SalesReturnAgent/test/.resources/configuration/SR-ACCUR.xml b/samples/BCAgents/SalesReturnAgent/test/.resources/configuration/SR-ACCUR.xml
new file mode 100644
index 00000000..32a049be
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/test/.resources/configuration/SR-ACCUR.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/samples/BCAgents/SalesReturnAgent/test/.resources/datasets/SR-ACCUR-ATTACHMENTS.yaml b/samples/BCAgents/SalesReturnAgent/test/.resources/datasets/SR-ACCUR-ATTACHMENTS.yaml
new file mode 100644
index 00000000..f7f0915c
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/test/.resources/datasets/SR-ACCUR-ATTACHMENTS.yaml
@@ -0,0 +1,84 @@
+suite_setup: SR-ACCUR-SETUP
+tests:
+- name: SR-RETURN-WITH-ATTACHMENTS
+ description: Return some items with attachments. The agent should create a credit memo for the damaged chairs and lamps, get approval, post, and respond to the customer.
+ turns:
+ # 1st turn: the customer requests a return, agent creates draft and requests review.
+ - query:
+ from: return.customer02@contoso.com
+ title: "Return request — items from attached invoice"
+ message: >
+ We have received damaged chairs, and want to return all chairs. Since the chairs are matching the lamps, we need to return the lamps as well. Can you refund us and help us with the return process?
+ Let us know if you need any additional information.
+ attachments:
+ - file: datasets/attachments/SR-ScratchedChairs.png
+ - file:
+ action_type: sales-invoice
+ action_data:
+ # START: Agent specific setup
+ customer_no: SRCUST02
+ date: "$DateFormula-<-45D>$" # This is a placeholder date formula to avoid hardcoding dates.
+ lines:
+ - item_no: SRITEM02
+ description: USB Keyboard
+ quantity: 3
+ - item_no: SRITEM04
+ description: Office Chair
+ quantity: 5
+ - item_no: SRITEM05
+ description: Desk Lamp
+ quantity: 2
+ - item_no: SRITEM06
+ description: Premium Office Chair
+ quantity: 1
+ # END: Agent specific setup
+ expected_data:
+ intervention_request:
+ type: ReviewRecord # Support Assistance, ReviewRecord, and ReviewMessage.
+ # START: Agent specific validation
+ credit_memo_created: true
+ credit_memo_customer: SRCUST02
+ credit_memo_posted: false
+ credit_memo_lines:
+ - item_no: SRITEM04
+ quantity: 5
+ unit_price: 150
+ line_amount: 750
+ - item_no: SRITEM05
+ quantity: 2
+ unit_price: 40
+ line_amount: 80
+ - item_no: SRITEM06
+ quantity: 1
+ unit_price: 300
+ line_amount: 300
+ credit_memo_total: 1130
+ work_description_contains: "scratch"
+ # END: Agent specific validation
+
+ # 2nd turn: the Business Central user approves the credit memo. The agent posts it and responds to the customer.
+ - query:
+ intervention:
+ instruction: "Approved. Please post the credit memo."
+ expected_data:
+ intervention_request:
+ type: ReviewMessage
+ # START: Agent specific validation
+ credit_memo_created: true
+ credit_memo_customer: SRCUST02
+ credit_memo_posted: true
+ credit_memo_lines:
+ - item_no: SRITEM04
+ quantity: 5
+ unit_price: 150
+ line_amount: 750
+ - item_no: SRITEM05
+ quantity: 2
+ unit_price: 40
+ line_amount: 80
+ - item_no: SRITEM06
+ quantity: 1
+ unit_price: 300
+ line_amount: 300
+ credit_memo_total: 1130
+ # END: Agent specific validation
diff --git a/samples/BCAgents/SalesReturnAgent/test/.resources/datasets/SR-ACCUR-INVOICE-LOOKUP.yaml b/samples/BCAgents/SalesReturnAgent/test/.resources/datasets/SR-ACCUR-INVOICE-LOOKUP.yaml
new file mode 100644
index 00000000..e99eab21
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/test/.resources/datasets/SR-ACCUR-INVOICE-LOOKUP.yaml
@@ -0,0 +1,155 @@
+suite_setup: SR-ACCUR-SETUP
+tests:
+
+# Scenario 1: Invoice is attached as a PDF (generated from posted invoice data)
+- name: SR-RETURN-WITH-INVOICE-ATTACHED
+ description: >
+ Customer attaches the original sales invoice PDF.
+ Agent should cross-reference and create a credit memo for the requested items.
+ turns:
+ - query:
+ from: return.customer02@contoso.com
+ title: "Return request with invoice attached"
+ message: >
+ Hi, we received damaged Office Chairs and would like to return them.
+ I've attached the original invoice for reference. Please process
+ the return for all 5 Office Chairs (SRITEM04).
+ attachments:
+ - file:
+ action_type: sales-invoice
+ action_data:
+ customer_no: SRCUST02
+ date: "$DateFormula-<-45D>$"
+ lines:
+ - item_no: SRITEM02
+ description: USB Keyboard
+ quantity: 3
+ - item_no: SRITEM04
+ description: Office Chair
+ quantity: 5
+ - item_no: SRITEM05
+ description: Desk Lamp
+ quantity: 2
+ - item_no: SRITEM06
+ description: Premium Office Chair
+ quantity: 1
+ expected_data:
+ intervention_request:
+ type: ReviewRecord
+ credit_memo_created: true
+ credit_memo_customer: SRCUST02
+ credit_memo_posted: false
+ credit_memo_lines:
+ - item_no: SRITEM04
+ quantity: 5
+ unit_price: 150
+ line_amount: 750
+ credit_memo_total: 750
+ work_description_contains: "damage"
+
+# Scenario 2: Invoice referenced by date in the message
+- name: SR-RETURN-WITH-INVOICE-REFERENCE
+ description: >
+ Customer references the original invoice by date.
+ Agent should find the matching posted invoice and create a credit memo.
+ turns:
+ - query:
+ from: return.customer01@contoso.com
+ title: "Return -- referencing original invoice"
+ message: >
+ Hi, we need to return 2 Wireless Mouse (SRITEM01). They stopped
+ working after a week. The original invoice was from about
+ $DateFormula-<-60D>$ (60 days ago).
+ Our account number is SRCUST01.
+ expected_data:
+ intervention_request:
+ type: ReviewRecord
+ credit_memo_created: true
+ credit_memo_customer: SRCUST01
+ credit_memo_posted: false
+ credit_memo_lines:
+ - item_no: SRITEM01
+ quantity: 2
+ unit_price: 15
+ line_amount: 30
+ credit_memo_total: 30
+ work_description_contains: "stopped working"
+
+# Scenario 3: No invoice provided, agent finds a matching one
+- name: SR-RETURN-NO-INVOICE-AGENT-FINDS-MATCH
+ description: >
+ Customer does not provide or reference an invoice.
+ A matching posted invoice exists. Agent should find it and proceed.
+ turns:
+ - query:
+ from: return.customer01@contoso.com
+ title: "Return request"
+ message: >
+ Hi, we'd like to return 1 USB Keyboard (SRITEM02). The keys
+ are sticking. Our account number is SRCUST01.
+ expected_data:
+ intervention_request:
+ type: ReviewRecord
+ credit_memo_created: true
+ credit_memo_customer: SRCUST01
+ credit_memo_posted: false
+ credit_memo_lines:
+ - item_no: SRITEM02
+ quantity: 1
+ unit_price: 30
+ line_amount: 30
+ credit_memo_total: 30
+ work_description_contains: "sticking"
+
+# Scenario 4: No matching invoice found -- agent should NOT create a credit memo
+- name: SR-RETURN-NO-INVOICE-NO-MATCH
+ description: >
+ Customer requests a return for an item that was never sold to them.
+ No matching posted invoice exists. Agent should ask for proof of purchase.
+ turns:
+ - query:
+ from: return.customer01@contoso.com
+ title: "Return request"
+ message: >
+ Hi, we'd like to return 3 Monitor Stands (SRITEM03). They
+ don't fit our desks. Our account number is SRCUST01.
+ expected_data:
+ intervention_request:
+ type: Assistance
+ credit_memo_created: false
+
+# Scenario 5: Customer references a non-existent invoice by ID
+- name: SR-RETURN-INVOICE-ID-NOT-FOUND
+ description: >
+ Customer references a specific invoice number that does not exist
+ in the system. Agent should request assistance from the BC user.
+ turns:
+ - query:
+ from: return.customer01@contoso.com
+ title: "Return request with invoice reference"
+ message: >
+ Hi, we need to return 2 Wireless Mouse (SRITEM01). They're
+ defective. Our invoice number is PSI-99999 and our account
+ number is SRCUST01.
+ expected_data:
+ intervention_request:
+ type: Assistance
+ credit_memo_created: false
+
+# Scenario 6: Customer references a valid invoice but items are not in it
+- name: SR-RETURN-INVOICE-ITEMS-MISMATCH
+ description: >
+ Customer references an invoice that exists but wants to return items
+ that are not in that invoice. Agent should request assistance.
+ turns:
+ - query:
+ from: return.customer01@contoso.com
+ title: "Return request -- items not on invoice"
+ message: >
+ Hi, we'd like to return 2 Monitor Stands (SRITEM03). We believe
+ they were on our invoice from about $DateFormula-<-60D>$
+ (60 days ago). Our account number is SRCUST01.
+ expected_data:
+ intervention_request:
+ type: Assistance
+ credit_memo_created: false
diff --git a/samples/BCAgents/SalesReturnAgent/test/.resources/datasets/SR-ACCUR-P0.yaml b/samples/BCAgents/SalesReturnAgent/test/.resources/datasets/SR-ACCUR-P0.yaml
new file mode 100644
index 00000000..bf1d39c2
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/test/.resources/datasets/SR-ACCUR-P0.yaml
@@ -0,0 +1,92 @@
+suite_setup: SR-ACCUR-SETUP
+tests:
+- name: SR-SINGLE-ITEM-FULL-RETURN
+ description: Single item type return — customer returns all mouses from the order.
+ turns:
+ - query:
+ from: return.customer03@contoso.com
+ title: "Create credit memo for returned items"
+ message: "Hi, we'd like to return all 5 Wireless Mouse. They're defective — the scroll wheels stopped working after a few days. Our account number is SRCUST03."
+ expected_data:
+ intervention_request:
+ type: ReviewRecord
+ credit_memo_created: true
+ credit_memo_customer: SRCUST03
+ credit_memo_posted: false
+ credit_memo_lines:
+ - item_no: SRITEM01
+ quantity: 5
+ unit_price: 15
+ line_amount: 75
+ credit_memo_total: 75
+ work_description_contains: "defective"
+
+- name: SR-SINGLE-ITEM-PARTIAL-RETURN
+ description: Single item type return — customer returns only some of the mouses from the order.
+ turns:
+ - query:
+ from: return.customer03@contoso.com
+ title: "Create credit memo for returned items"
+ message: "Hi, we'd like to return 2 Wireless Mouse. They're defective — the scroll wheel stopped working after 3 days. Our account number is SRCUST03."
+ expected_data:
+ intervention_request:
+ type: ReviewRecord
+ credit_memo_created: true
+ credit_memo_customer: SRCUST03
+ credit_memo_posted: false
+ credit_memo_lines:
+ - item_no: SRITEM01
+ quantity: 2
+ unit_price: 15
+ line_amount: 30
+ credit_memo_total: 30
+ work_description_contains: "defect"
+- name: SR-MULTI-ITEM-FULL-RETURN
+ description: Multi-item order — customer returns all mouses and all keyboards.
+ turns:
+ - query:
+ from: return.customer04@contoso.com
+ title: "Create credit memo for returned items"
+ message: "Hi, we need to return all 5 Wireless Mouse and all 3 USB Keyboard. They were all damaged during shipping. Our account number is SRCUST04."
+ expected_data:
+ intervention_request:
+ type: ReviewRecord
+ credit_memo_created: true
+ credit_memo_customer: SRCUST04
+ credit_memo_posted: false
+ credit_memo_lines:
+ - item_no: SRITEM01
+ quantity: 5
+ unit_price: 15
+ line_amount: 75
+ - item_no: SRITEM02
+ quantity: 3
+ unit_price: 30
+ line_amount: 90
+ credit_memo_total: 165
+ work_description_contains: "damage"
+
+- name: SR-MULTI-ITEM-PARTIAL-RETURN
+ description: Multi-item order — customer returns some mouses and some keyboards but not all.
+ turns:
+ - query:
+ from: return.customer04@contoso.com
+ title: "Create credit memo for returned items"
+ message: "Hi, we need to return 3 of the 5 Wireless Mouse and 1 of the 3 USB Keyboard. They are defective — the buttons stopped clicking on the mouses and one keyboard has a broken spacebar. Our account number is SRCUST04."
+ expected_data:
+ intervention_request:
+ type: ReviewRecord
+ credit_memo_created: true
+ credit_memo_customer: SRCUST04
+ credit_memo_posted: false
+ credit_memo_lines:
+ - item_no: SRITEM01
+ quantity: 3
+ unit_price: 15
+ line_amount: 45
+ - item_no: SRITEM02
+ quantity: 1
+ unit_price: 30
+ line_amount: 30
+ credit_memo_total: 75
+ work_description_contains: "defect"
diff --git a/samples/BCAgents/SalesReturnAgent/test/.resources/datasets/SR-ACCUR-POSTING.yaml b/samples/BCAgents/SalesReturnAgent/test/.resources/datasets/SR-ACCUR-POSTING.yaml
new file mode 100644
index 00000000..309a6345
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/test/.resources/datasets/SR-ACCUR-POSTING.yaml
@@ -0,0 +1,130 @@
+suite_setup: SR-ACCUR-SETUP
+tests:
+- name: SR-SINGLE-ITEM-RETURN-POSTED
+ description: Single item return — credit memo created, user approves, agent posts and responds to customer.
+ turns:
+ # The first turn: the customer requests to return some items. The agent creates a draft credit memo and requests review.
+ - query:
+ from: return.customer01@contoso.com
+ title: "Create credit memo for returned items"
+ message: "Hi, we'd like to return 2 Wireless Mouse (SRITEM01). They're defective — the scroll wheel stopped working after 3 days. Our account number is SRCUST01."
+ expected_data:
+ intervention_request:
+ type: ReviewRecord
+ credit_memo_created: true
+ credit_memo_customer: SRCUST01
+ credit_memo_posted: false
+ credit_memo_lines:
+ - item_no: SRITEM01
+ quantity: 2
+ unit_price: 15
+ line_amount: 30
+ credit_memo_total: 30
+ work_description_contains: "defective"
+ # The second turn: the Business Central user approves the credit memo. The agent posts it and responds to the customer.
+ - query:
+ intervention:
+ instruction: "Approved. Please post the credit memo."
+ expected_data:
+ intervention_request:
+ type: ReviewMessage
+ credit_memo_created: true
+ credit_memo_customer: SRCUST01
+ credit_memo_posted: true
+ credit_memo_lines:
+ - item_no: SRITEM01
+ quantity: 2
+ unit_price: 15
+ line_amount: 30
+ credit_memo_total: 30
+
+- name: SR-SINGLE-ITEM-RETURN-POSTED-ALT
+ description: Single item return alt — credit memo created, user approves, agent posts and responds to customer.
+ turns:
+ # The first turn: the customer requests to return some items. The agent creates a draft credit memo and requests review.
+ - query:
+ from: return.customer01@contoso.com
+ title: "Create credit memo for returned items"
+ message: "Hi, we'd like to return 2 Wireless Mouse (SRITEM01). They're defective — the scroll wheel stopped working after 3 days. Our account number is SRCUST01."
+ expected_data:
+ intervention_request:
+ type: ReviewRecord
+ credit_memo_created: true
+ credit_memo_customer: SRCUST01
+ credit_memo_posted: false
+ credit_memo_lines:
+ - item_no: SRITEM01
+ quantity: 2
+ unit_price: 15
+ line_amount: 30
+ credit_memo_total: 30
+ work_description_contains: "defective"
+ # The second turn: the Business Central user approves the credit memo. The agent posts it and responds to the customer.
+ - query:
+ intervention:
+ instruction: "Approved"
+ expected_data:
+ intervention_request:
+ type: ReviewMessage
+ credit_memo_created: true
+ credit_memo_customer: SRCUST01
+ credit_memo_posted: true
+ credit_memo_lines:
+ - item_no: SRITEM01
+ quantity: 2
+ unit_price: 15
+ line_amount: 30
+ credit_memo_total: 30
+
+- name: SR-SINGLE-ITEM-RETURN-DECLINED-AND-RESUBMITTED
+ description: Credit memo created, user requests changes, agent updates and re-requests review, then user approves and agent posts.
+ turns:
+ # Turn 1: customer requests a return, agent creates draft and requests review.
+ - query:
+ from: return.customer01@contoso.com
+ title: "Create credit memo for returned items"
+ message: "Hi, we'd like to return 2 Wireless Mouse (SRITEM01). They're defective. Our account number is SRCUST01."
+ expected_data:
+ intervention_request:
+ type: ReviewRecord
+ credit_memo_created: true
+ credit_memo_customer: SRCUST01
+ credit_memo_posted: false
+ credit_memo_lines:
+ - item_no: SRITEM01
+ quantity: 2
+ unit_price: 15
+ line_amount: 30
+ credit_memo_total: 30
+ # Turn 2: user declines and requests a quantity change. Agent updates and re-requests review.
+ - query:
+ intervention:
+ instruction: "The quantity is wrong. The customer actually wants to return 3 Wireless Mouse, not 2. Please update the credit memo and resubmit for review."
+ expected_data:
+ intervention_request:
+ type: ReviewRecord
+ credit_memo_created: true
+ credit_memo_customer: SRCUST01
+ credit_memo_posted: false
+ credit_memo_lines:
+ - item_no: SRITEM01
+ quantity: 3
+ unit_price: 15
+ line_amount: 45
+ credit_memo_total: 45
+ # Turn 3: user approves, agent posts and responds to customer.
+ - query:
+ intervention:
+ instruction: "Approved. Please post the credit memo."
+ expected_data:
+ intervention_request:
+ type: ReviewMessage
+ credit_memo_created: true
+ credit_memo_customer: SRCUST01
+ credit_memo_posted: true
+ credit_memo_lines:
+ - item_no: SRITEM01
+ quantity: 3
+ unit_price: 15
+ line_amount: 45
+ credit_memo_total: 45
diff --git a/samples/BCAgents/SalesReturnAgent/test/.resources/datasets/attachments/SR-ScratchedChairs.png b/samples/BCAgents/SalesReturnAgent/test/.resources/datasets/attachments/SR-ScratchedChairs.png
new file mode 100644
index 00000000..2fe0d130
Binary files /dev/null and b/samples/BCAgents/SalesReturnAgent/test/.resources/datasets/attachments/SR-ScratchedChairs.png differ
diff --git a/samples/BCAgents/SalesReturnAgent/test/.resources/datasets/suite_setup/SR-ACCUR-SETUP.yaml b/samples/BCAgents/SalesReturnAgent/test/.resources/datasets/suite_setup/SR-ACCUR-SETUP.yaml
new file mode 100644
index 00000000..f1312d5f
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/test/.resources/datasets/suite_setup/SR-ACCUR-SETUP.yaml
@@ -0,0 +1,103 @@
+name: SR-ACCUR-SETUP
+description: Setup suite for testing handling of attachments in sales return scenarios. It creates customers, items, and posted sales invoices that will be used in the tests.
+suite_setup:
+ setup_actions:
+ # First, create the customers.
+ - action_type: create_customers
+ action_data:
+ # START: Agent specific setup
+ - "No.": SRCUST01
+ Name: Return Customer 01
+ Email: return.customer01@contoso.com
+ Address: 123 Main Street
+ City: Seattle
+ "Post Code": "98101"
+ "Country/Region Code": US
+ - "No.": SRCUST02
+ Name: Return Customer 02
+ Email: return.customer02@contoso.com
+ Address: 456 Oak Avenue
+ City: Portland
+ "Post Code": "97201"
+ "Country/Region Code": US
+ - "No.": SRCUST03
+ Name: Return Customer 03
+ Email: return.customer03@contoso.com
+ Address: 789 Pine Road
+ City: San Francisco
+ "Post Code": "94102"
+ "Country/Region Code": US
+ - "No.": SRCUST04
+ Name: Return Customer 04
+ Email: return.customer04@contoso.com
+ Address: 321 Elm Boulevard
+ City: Denver
+ "Post Code": "80201"
+ "Country/Region Code": US
+ # END: Agent specific setup
+
+ # Then, create the items.
+ - action_type: create_items
+ action_data:
+ # START: Agent specific setup
+ - "No.": SRITEM01
+ Description: Wireless Mouse
+ "Unit of Measure Code": PCS
+ "Unit Price": 15
+ - "No.": SRITEM02
+ Description: USB Keyboard
+ "Unit of Measure Code": PCS
+ "Unit Price": 30
+ - "No.": SRITEM03
+ Description: Monitor Stand
+ "Unit of Measure Code": PCS
+ "Unit Price": 150
+ - "No.": SRITEM04
+ Description: Office Chair
+ "Unit of Measure Code": PCS
+ "Unit Price": 150
+ - "No.": SRITEM05
+ Description: Desk Lamp
+ "Unit of Measure Code": PCS
+ "Unit Price": 40
+ - "No.": SRITEM06
+ Description: Premium Office Chair
+ "Unit of Measure Code": PCS
+ "Unit Price": 300
+ # END: Agent specific setup
+
+ # Finally, create posted sales invoices for the customers, which will be referenced in the tests.
+ - action_type: create_posted_sales_invoices
+ action_data:
+ # START: Agent specific setup
+ - customer_no: SRCUST01
+ date: "$DateFormula-<-60D>$" # This is a placeholder data formula to avoid hardcoding dates.
+ lines:
+ - item_no: SRITEM01
+ quantity: 5
+ - item_no: SRITEM02
+ quantity: 3
+ - customer_no: SRCUST02
+ date: "$DateFormula-<-45D>$"
+ lines:
+ - item_no: SRITEM02
+ quantity: 3
+ - item_no: SRITEM04
+ quantity: 5
+ - item_no: SRITEM05
+ quantity: 2
+ - item_no: SRITEM06
+ quantity: 1
+ - customer_no: SRCUST03
+ date: "$DateFormula-<-30D>$"
+ lines:
+ - item_no: SRITEM01
+ quantity: 5
+ - customer_no: SRCUST04
+ date: "$DateFormula-<-30D>$"
+ lines:
+ - item_no: SRITEM01
+ quantity: 5
+ - item_no: SRITEM02
+ quantity: 3
+ # END: Agent specific setup
\ No newline at end of file
diff --git a/samples/BCAgents/SalesReturnAgent/test/ExtensionLogo.png b/samples/BCAgents/SalesReturnAgent/test/ExtensionLogo.png
new file mode 100644
index 00000000..689824d3
Binary files /dev/null and b/samples/BCAgents/SalesReturnAgent/test/ExtensionLogo.png differ
diff --git a/samples/BCAgents/SalesReturnAgent/test/Libraries/SREventHandler.Codeunit.al b/samples/BCAgents/SalesReturnAgent/test/Libraries/SREventHandler.Codeunit.al
new file mode 100644
index 00000000..66111bbc
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/test/Libraries/SREventHandler.Codeunit.al
@@ -0,0 +1,14 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace SalesReturnAgent.Test.Libraries;
+
+codeunit 53748 "SR Event Handler"
+{
+ Access = Internal;
+ EventSubscriberInstance = Manual;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+}
diff --git a/samples/BCAgents/SalesReturnAgent/test/Libraries/SRTestLibrary.Codeunit.al b/samples/BCAgents/SalesReturnAgent/test/Libraries/SRTestLibrary.Codeunit.al
new file mode 100644
index 00000000..afb0e214
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/test/Libraries/SRTestLibrary.Codeunit.al
@@ -0,0 +1,584 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace SalesReturnAgent.Test.Libraries;
+
+using Microsoft.Inventory.Item;
+using Microsoft.Sales.Customer;
+using Microsoft.Sales.Document;
+using Microsoft.Sales.History;
+using System.TestTools.AITestToolkit;
+using System.TestTools.TestRunner;
+
+codeunit 53746 "SR Test Library"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ ///
+ /// Creates customers from the suite setup data.
+ ///
+ procedure CreateCustomers(TestSetup: Codeunit "Test Input Json")
+ var
+ ActionData: Codeunit "Test Input Json";
+ I: Integer;
+ begin
+ if not TryGetActionData(TestSetup, CreateCustomersTok, ActionData) then
+ exit;
+
+ for I := 0 to ActionData.GetElementCount() - 1 do
+ CreateCustomer(ActionData.ElementAt(I));
+ end;
+
+ ///
+ /// Creates items from the suite setup data.
+ ///
+ procedure CreateItems(TestSetup: Codeunit "Test Input Json")
+ var
+ ActionData: Codeunit "Test Input Json";
+ I: Integer;
+ begin
+ if not TryGetActionData(TestSetup, CreateItemsTok, ActionData) then
+ exit;
+
+ for I := 0 to ActionData.GetElementCount() - 1 do
+ CreateItem(ActionData.ElementAt(I));
+ end;
+
+ ///
+ /// Creates credit memo test data for the current turn.
+ ///
+ procedure CreateCreditMemoTestData(TestSetup: Codeunit "Test Input Json")
+ var
+ ActionData: Codeunit "Test Input Json";
+ begin
+ if not TryGetActionData(TestSetup, CreatePostedShipmentTok, ActionData) then
+ exit;
+
+ // Posted shipment data is set up so the agent has context for the return.
+ // The actual credit memo creation is done by the agent during the test.
+ end;
+
+ ///
+ /// Creates posted sales invoices from the suite setup data.
+ ///
+ procedure CreatePostedSalesInvoices(TestSetup: Codeunit "Test Input Json")
+ var
+ ActionData: Codeunit "Test Input Json";
+ I: Integer;
+ begin
+ if not TryGetActionData(TestSetup, CreatePostedSalesInvoicesTok, ActionData) then
+ exit;
+
+ for I := 0 to ActionData.GetElementCount() - 1 do
+ CreatePostedSalesInvoice(ActionData.ElementAt(I));
+ end;
+
+ ///
+ /// Validates that a credit memo was created with the expected data.
+ ///
+ procedure ValidateCreditMemoCreated(var ErrorReason: Text): Boolean
+ var
+ ExpectedData: Codeunit "Test Input Json";
+ Element: Codeunit "Test Input Json";
+ ElementExists: Boolean;
+ CreditMemoCreated: Boolean;
+ CreditMemoPosted: Boolean;
+ begin
+ ExpectedData := AITTestContext.GetExpectedData();
+
+ Element := ExpectedData.ElementExists(CreditMemoCreatedTok, ElementExists);
+ if ElementExists then
+ CreditMemoCreated := Element.ValueAsBoolean()
+ else
+ exit(true); // No validation needed
+
+ Element := ExpectedData.ElementExists(CreditMemoPostedTok, ElementExists);
+ if ElementExists then
+ CreditMemoPosted := Element.ValueAsBoolean();
+
+ if CreditMemoPosted then
+ exit(ValidatePostedCreditMemo(ExpectedData, CreditMemoCreated, ErrorReason));
+
+ exit(ValidateUnpostedCreditMemo(ExpectedData, CreditMemoCreated, ErrorReason));
+ end;
+
+ local procedure ValidateUnpostedCreditMemo(ExpectedData: Codeunit "Test Input Json"; CreditMemoCreated: Boolean; var ErrorReason: Text): Boolean
+ var
+ SalesHeader: Record "Sales Header";
+ SalesLine: Record "Sales Line";
+ LinesData: Codeunit "Test Input Json";
+ Element: Codeunit "Test Input Json";
+ ExpectedCustomer: Text;
+ ExpectedItemNo: Text;
+ ExpectedUOM: Text;
+ ExpectedQuantity: Integer;
+ ElementExists: Boolean;
+ LinesExist: Boolean;
+ HasExpectedUOM: Boolean;
+ I: Integer;
+ begin
+ SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::"Credit Memo");
+ if not CreditMemoCreated then begin
+ if not SalesHeader.IsEmpty() then begin
+ ErrorReason := CreditMemoShouldNotExistLbl;
+ exit(false);
+ end;
+ exit(true);
+ end;
+
+ if SalesHeader.IsEmpty() then begin
+ ErrorReason := CreditMemoNotCreatedLbl;
+ exit(false);
+ end;
+
+ // Validate customer
+ Element := ExpectedData.ElementExists(CreditMemoCustomerTok, ElementExists);
+ if ElementExists then begin
+ ExpectedCustomer := Element.ValueAsText();
+ SalesHeader.SetRange("Sell-to Customer No.", CopyStr(ExpectedCustomer, 1, 20));
+ if SalesHeader.IsEmpty() then begin
+ ErrorReason := StrSubstNo(WrongCustomerLbl, ExpectedCustomer);
+ exit(false);
+ end;
+ end;
+
+ SalesHeader.FindFirst();
+
+ // Resolve expected UOM (applies to all lines)
+ Element := ExpectedData.ElementExists(ExpectedUomTok, HasExpectedUOM);
+ if HasExpectedUOM then
+ ExpectedUOM := Element.ValueAsText();
+
+ // Validate lines
+ LinesData := ExpectedData.ElementExists(CreditMemoLinesTok, LinesExist);
+ if LinesExist then begin
+ SalesLine.SetRange("Document Type", SalesLine."Document Type"::"Credit Memo");
+ SalesLine.SetRange("Document No.", SalesHeader."No.");
+ SalesLine.SetRange(Type, SalesLine.Type::Item);
+
+ for I := 0 to LinesData.GetElementCount() - 1 do begin
+ Element := LinesData.ElementAt(I).Element(ItemNoTok);
+ ExpectedItemNo := Element.ValueAsText();
+ ExpectedQuantity := LinesData.ElementAt(I).Element(QuantityTok).ValueAsInteger();
+
+ SalesLine.SetRange("No.", CopyStr(ExpectedItemNo, 1, 20));
+ if SalesLine.IsEmpty() then begin
+ ErrorReason := StrSubstNo(ItemLineNotFoundLbl, ExpectedItemNo);
+ exit(false);
+ end;
+
+ SalesLine.FindFirst();
+ if SalesLine.Quantity <> ExpectedQuantity then begin
+ ErrorReason := StrSubstNo(WrongQuantityLbl, ExpectedItemNo, ExpectedQuantity, SalesLine.Quantity);
+ exit(false);
+ end;
+
+ Element := LinesData.ElementAt(I).ElementExists(UnitPriceTok, ElementExists);
+ if ElementExists then
+ if SalesLine."Unit Price" <> Element.ValueAsDecimal() then begin
+ ErrorReason := StrSubstNo(WrongUnitPriceLbl, ExpectedItemNo, Element.ValueAsDecimal(), SalesLine."Unit Price");
+ exit(false);
+ end;
+
+ Element := LinesData.ElementAt(I).ElementExists(LineAmountTok, ElementExists);
+ if ElementExists then
+ if SalesLine."Line Amount" <> Element.ValueAsDecimal() then begin
+ ErrorReason := StrSubstNo(WrongLineAmountLbl, ExpectedItemNo, Element.ValueAsDecimal(), SalesLine."Line Amount");
+ exit(false);
+ end;
+
+ if HasExpectedUOM then
+ if SalesLine."Unit of Measure Code" <> CopyStr(ExpectedUOM, 1, 10) then begin
+ ErrorReason := StrSubstNo(WrongUomLbl, ExpectedUOM, SalesLine."Unit of Measure Code", ExpectedItemNo);
+ exit(false);
+ end;
+ end;
+
+ // Verify no extra lines beyond what is expected
+ SalesLine.SetRange("No.");
+ if SalesLine.Count() <> LinesData.GetElementCount() then begin
+ ErrorReason := StrSubstNo(LineCountMismatchLbl, LinesData.GetElementCount(), SalesLine.Count());
+ exit(false);
+ end;
+ end;
+
+ // Validate credit memo total
+ Element := ExpectedData.ElementExists(CreditMemoTotalTok, ElementExists);
+ if ElementExists then begin
+ SalesHeader.CalcFields(Amount);
+ if SalesHeader.Amount <> Element.ValueAsDecimal() then begin
+ ErrorReason := StrSubstNo(WrongTotalLbl, Element.ValueAsDecimal(), SalesHeader.Amount);
+ exit(false);
+ end;
+ end;
+
+ exit(true);
+ end;
+
+ local procedure ValidatePostedCreditMemo(ExpectedData: Codeunit "Test Input Json"; CreditMemoCreated: Boolean; var ErrorReason: Text): Boolean
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ SalesCrMemoLine: Record "Sales Cr.Memo Line";
+ LinesData: Codeunit "Test Input Json";
+ Element: Codeunit "Test Input Json";
+ ExpectedCustomer: Text;
+ ExpectedItemNo: Text;
+ ExpectedUOM: Text;
+ ExpectedQuantity: Integer;
+ ElementExists: Boolean;
+ LinesExist: Boolean;
+ HasExpectedUOM: Boolean;
+ I: Integer;
+ begin
+ if not CreditMemoCreated then begin
+ ErrorReason := PostedCreditMemoConflictLbl;
+ exit(false);
+ end;
+
+ if SalesCrMemoHeader.IsEmpty() then begin
+ ErrorReason := PostedCreditMemoNotFoundLbl;
+ exit(false);
+ end;
+
+ // Validate customer
+ Element := ExpectedData.ElementExists(CreditMemoCustomerTok, ElementExists);
+ if ElementExists then begin
+ ExpectedCustomer := Element.ValueAsText();
+ SalesCrMemoHeader.SetRange("Sell-to Customer No.", CopyStr(ExpectedCustomer, 1, 20));
+ if SalesCrMemoHeader.IsEmpty() then begin
+ ErrorReason := StrSubstNo(WrongCustomerLbl, ExpectedCustomer);
+ exit(false);
+ end;
+ end;
+
+ SalesCrMemoHeader.FindLast();
+
+ // Resolve expected UOM (applies to all lines)
+ Element := ExpectedData.ElementExists(ExpectedUomTok, HasExpectedUOM);
+ if HasExpectedUOM then
+ ExpectedUOM := Element.ValueAsText();
+
+ // Validate lines
+ LinesData := ExpectedData.ElementExists(CreditMemoLinesTok, LinesExist);
+ if LinesExist then begin
+ SalesCrMemoLine.SetRange("Document No.", SalesCrMemoHeader."No.");
+ SalesCrMemoLine.SetRange(Type, SalesCrMemoLine.Type::Item);
+
+ for I := 0 to LinesData.GetElementCount() - 1 do begin
+ Element := LinesData.ElementAt(I).Element(ItemNoTok);
+ ExpectedItemNo := Element.ValueAsText();
+ ExpectedQuantity := LinesData.ElementAt(I).Element(QuantityTok).ValueAsInteger();
+
+ SalesCrMemoLine.SetRange("No.", CopyStr(ExpectedItemNo, 1, 20));
+ if SalesCrMemoLine.IsEmpty() then begin
+ ErrorReason := StrSubstNo(ItemLineNotFoundLbl, ExpectedItemNo);
+ exit(false);
+ end;
+
+ SalesCrMemoLine.FindFirst();
+ if SalesCrMemoLine.Quantity <> ExpectedQuantity then begin
+ ErrorReason := StrSubstNo(WrongQuantityLbl, ExpectedItemNo, ExpectedQuantity, SalesCrMemoLine.Quantity);
+ exit(false);
+ end;
+
+ Element := LinesData.ElementAt(I).ElementExists(UnitPriceTok, ElementExists);
+ if ElementExists then
+ if SalesCrMemoLine."Unit Price" <> Element.ValueAsDecimal() then begin
+ ErrorReason := StrSubstNo(WrongUnitPriceLbl, ExpectedItemNo, Element.ValueAsDecimal(), SalesCrMemoLine."Unit Price");
+ exit(false);
+ end;
+
+ Element := LinesData.ElementAt(I).ElementExists(LineAmountTok, ElementExists);
+ if ElementExists then
+ if SalesCrMemoLine."Line Amount" <> Element.ValueAsDecimal() then begin
+ ErrorReason := StrSubstNo(WrongLineAmountLbl, ExpectedItemNo, Element.ValueAsDecimal(), SalesCrMemoLine."Line Amount");
+ exit(false);
+ end;
+
+ if HasExpectedUOM then
+ if SalesCrMemoLine."Unit of Measure Code" <> CopyStr(ExpectedUOM, 1, 10) then begin
+ ErrorReason := StrSubstNo(WrongUomLbl, ExpectedUOM, SalesCrMemoLine."Unit of Measure Code", ExpectedItemNo);
+ exit(false);
+ end;
+ end;
+
+ // Verify no extra lines beyond what is expected
+ SalesCrMemoLine.SetRange("No.");
+ if SalesCrMemoLine.Count() <> LinesData.GetElementCount() then begin
+ ErrorReason := StrSubstNo(LineCountMismatchLbl, LinesData.GetElementCount(), SalesCrMemoLine.Count());
+ exit(false);
+ end;
+ end;
+
+ // Validate credit memo total
+ Element := ExpectedData.ElementExists(CreditMemoTotalTok, ElementExists);
+ if ElementExists then begin
+ SalesCrMemoHeader.CalcFields(Amount);
+ if SalesCrMemoHeader.Amount <> Element.ValueAsDecimal() then begin
+ ErrorReason := StrSubstNo(WrongTotalLbl, Element.ValueAsDecimal(), SalesCrMemoHeader.Amount);
+ exit(false);
+ end;
+ end;
+
+ exit(true);
+ end;
+
+ ///
+ /// Validates that no credit memo was created (for error scenarios).
+ ///
+ procedure ValidateNoCreditMemoCreated(var ErrorReason: Text): Boolean
+ var
+ SalesHeader: Record "Sales Header";
+ begin
+ SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::"Credit Memo");
+ if not SalesHeader.IsEmpty() then begin
+ ErrorReason := CreditMemoShouldNotExistLbl;
+ exit(false);
+ end;
+ exit(true);
+ end;
+
+ ///
+ /// Validates that the work description contains expected text.
+ ///
+ procedure ValidateWorkDescription(var ErrorReason: Text): Boolean
+ var
+ SalesHeader: Record "Sales Header";
+ ExpectedData: Codeunit "Test Input Json";
+ Element: Codeunit "Test Input Json";
+ WorkDescription: Text;
+ ExpectedText: Text;
+ ElementExists: Boolean;
+ begin
+ ExpectedData := AITTestContext.GetExpectedData();
+ Element := ExpectedData.ElementExists(WorkDescContainsTok, ElementExists);
+ if not ElementExists then
+ exit(true);
+
+ ExpectedText := Element.ValueAsText();
+
+ SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::"Credit Memo");
+ if not SalesHeader.FindFirst() then begin
+ ErrorReason := CreditMemoNotCreatedLbl;
+ exit(false);
+ end;
+
+ WorkDescription := SalesHeader.GetWorkDescription();
+ if StrPos(WorkDescription, ExpectedText) = 0 then begin
+ ErrorReason := StrSubstNo(WorkDescMissingTextLbl, ExpectedText, WorkDescription);
+ exit(false);
+ end;
+
+ exit(true);
+ end;
+
+ ///
+ /// Deletes all sales credit memos and their lines.
+ ///
+ procedure DeleteAllSalesCreditMemos()
+ var
+ SalesHeader: Record "Sales Header";
+ SalesLine: Record "Sales Line";
+ begin
+ SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::"Credit Memo");
+ SalesHeader.DeleteAll(false);
+
+ SalesLine.SetRange("Document Type", SalesLine."Document Type"::"Credit Memo");
+ SalesLine.DeleteAll(false);
+ end;
+
+ ///
+ /// Deletes any unposted sales invoices as a general cleanup safety measure.
+ ///
+ procedure DeleteAllUnpostedSalesInvoices()
+ var
+ SalesHeader: Record "Sales Header";
+ SalesLine: Record "Sales Line";
+ begin
+ SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::Invoice);
+ SalesHeader.DeleteAll(false);
+
+ SalesLine.SetRange("Document Type", SalesLine."Document Type"::Invoice);
+ SalesLine.DeleteAll(false);
+ end;
+
+ local procedure CreateCustomer(TestInputJson: Codeunit "Test Input Json")
+ var
+ Customer: Record Customer;
+ Element: Codeunit "Test Input Json";
+ ElementExists: Boolean;
+ CustomerNo: Code[20];
+ begin
+ Element := TestInputJson.ElementExists('No.', ElementExists);
+
+ if ElementExists then begin
+ CustomerNo := CopyStr(Element.ValueAsText(), 1, MaxStrLen(CustomerNo));
+ if not Customer.Get(CustomerNo) then begin
+ LibrarySales.CreateCustomer(Customer);
+ Customer.Rename(CustomerNo);
+ end;
+ end else
+ LibrarySales.CreateCustomer(Customer);
+
+ Element := TestInputJson.ElementExists('Name', ElementExists);
+ if ElementExists then
+ Customer.Validate(Name, CopyStr(Element.ValueAsText(), 1, MaxStrLen(Customer.Name)));
+
+ Element := TestInputJson.ElementExists('Email', ElementExists);
+ if ElementExists then
+ Customer.Validate("E-Mail", Element.ValueAsText());
+
+ Element := TestInputJson.ElementExists('Address', ElementExists);
+ if ElementExists then
+ Customer.Validate(Address, CopyStr(Element.ValueAsText(), 1, MaxStrLen(Customer.Address)));
+
+ Element := TestInputJson.ElementExists('City', ElementExists);
+ if ElementExists then
+ Customer.Validate(City, CopyStr(Element.ValueAsText(), 1, MaxStrLen(Customer.City)));
+
+ Element := TestInputJson.ElementExists('Post Code', ElementExists);
+ if ElementExists then
+ Customer.Validate("Post Code", CopyStr(Element.ValueAsText(), 1, MaxStrLen(Customer."Post Code")));
+
+ Element := TestInputJson.ElementExists('Country/Region Code', ElementExists);
+ if ElementExists then
+ Customer.Validate("Country/Region Code", CopyStr(Element.ValueAsText(), 1, MaxStrLen(Customer."Country/Region Code")));
+
+ Customer.Modify();
+ Commit();
+ end;
+
+ local procedure CreateItem(TestInputJson: Codeunit "Test Input Json")
+ var
+ Item: Record Item;
+ Element: Codeunit "Test Input Json";
+ ElementExists: Boolean;
+ ItemNo: Code[20];
+ begin
+ Element := TestInputJson.ElementExists('No.', ElementExists);
+
+ if ElementExists then begin
+ ItemNo := CopyStr(Element.ValueAsText(), 1, MaxStrLen(ItemNo));
+ if not Item.Get(ItemNo) then begin
+ LibraryInventory.CreateItem(Item);
+ Item.Rename(ItemNo);
+ end;
+ end else
+ LibraryInventory.CreateItem(Item);
+
+ Element := TestInputJson.ElementExists('Description', ElementExists);
+ if ElementExists then
+ Item.Validate(Description, CopyStr(Element.ValueAsText(), 1, MaxStrLen(Item.Description)));
+
+ Element := TestInputJson.ElementExists('Unit of Measure Code', ElementExists);
+ if ElementExists then
+ Item.Validate("Base Unit of Measure", CopyStr(Element.ValueAsText(), 1, 10));
+
+ Element := TestInputJson.ElementExists('Unit Price', ElementExists);
+ if ElementExists then
+ Item.Validate("Unit Price", Element.ValueAsDecimal());
+
+ Item.Modify();
+ Commit();
+ end;
+
+ local procedure CreatePostedSalesInvoice(TestInputJson: Codeunit "Test Input Json")
+ var
+ SalesHeader: Record "Sales Header";
+ SalesLine: Record "Sales Line";
+ LinesData: Codeunit "Test Input Json";
+ LineElement: Codeunit "Test Input Json";
+ Element: Codeunit "Test Input Json";
+ ElementExists: Boolean;
+ CustomerNo: Code[20];
+ ItemNo: Code[20];
+ I: Integer;
+ begin
+ Element := TestInputJson.ElementExists('customer_no', ElementExists);
+ if not ElementExists then
+ exit;
+
+ CustomerNo := CopyStr(Element.ValueAsText(), 1, MaxStrLen(CustomerNo));
+ LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Invoice, CustomerNo);
+
+ Element := TestInputJson.ElementExists('date', ElementExists);
+ if ElementExists then begin
+ SalesHeader.Validate("Posting Date", Element.ValueAsDate());
+ SalesHeader.Modify(true);
+ end;
+
+ LinesData := TestInputJson.ElementExists('lines', ElementExists);
+ if ElementExists then
+ for I := 0 to LinesData.GetElementCount() - 1 do begin
+ LineElement := LinesData.ElementAt(I);
+
+ ItemNo := CopyStr(LineElement.Element('item_no').ValueAsText(), 1, MaxStrLen(ItemNo));
+ LibrarySales.CreateSalesLine(
+ SalesLine, SalesHeader,
+ Enum::"Sales Line Type"::Item, ItemNo,
+ LineElement.Element('quantity').ValueAsDecimal());
+ end;
+
+ LibrarySales.PostSalesDocument(SalesHeader, true, true);
+ Commit();
+ end;
+
+ local procedure TryGetActionData(TestSetup: Codeunit "Test Input Json"; ActionType: Text; var ActionData: Codeunit "Test Input Json"): Boolean
+ var
+ ActionsArray: Codeunit "Test Input Json";
+ ActionsExist: Boolean;
+ I: Integer;
+ begin
+ ActionsArray := TestSetup.ElementExists(SetupActionsTok, ActionsExist);
+ if not ActionsExist then
+ exit(false);
+
+ for I := 0 to ActionsArray.GetElementCount() - 1 do
+ if ActionsArray.ElementAt(I).Element(ActionTypeTok).ValueAsText() = ActionType then begin
+ ActionData := ActionsArray.ElementAt(I).Element(ActionDataTok);
+ exit(true);
+ end;
+
+ exit(false);
+ end;
+
+ var
+ AITTestContext: Codeunit "AIT Test Context";
+ LibraryInventory: Codeunit "Library - Inventory";
+ LibrarySales: Codeunit "Library - Sales";
+ CreditMemoNotCreatedLbl: Label 'Expected a credit memo to be created but none was found.';
+ CreditMemoShouldNotExistLbl: Label 'Expected no credit memo to be created but one was found.';
+ PostedCreditMemoNotFoundLbl: Label 'Expected a posted credit memo but none was found.';
+ PostedCreditMemoConflictLbl: Label 'credit_memo_posted is true but credit_memo_created is false — conflicting expected data.';
+ WrongCustomerLbl: Label 'Expected credit memo for customer %1 but none was found.', Comment = '%1 = customer number';
+ ItemLineNotFoundLbl: Label 'Expected credit memo line for item %1 but none was found.', Comment = '%1 = item number';
+ WrongQuantityLbl: Label 'Expected quantity %2 for item %1 but found %3.', Comment = '%1 = item number, %2 = expected quantity, %3 = actual quantity';
+ WrongUnitPriceLbl: Label 'Expected unit price %2 for item %1 but found %3.', Comment = '%1 = item number, %2 = expected unit price, %3 = actual unit price';
+ WrongLineAmountLbl: Label 'Expected line amount %2 for item %1 but found %3.', Comment = '%1 = item number, %2 = expected line amount, %3 = actual line amount';
+ WrongTotalLbl: Label 'Expected credit memo total %1 but found %2.', Comment = '%1 = expected total, %2 = actual total';
+ WorkDescMissingTextLbl: Label 'Expected work description to contain "%1" but it did not. Found: "%2"', Comment = '%1 = expected text, %2 = actual text';
+ WrongUomLbl: Label 'Expected unit of measure %1 but found %2 for item %3.', Comment = '%1 = expected UOM, %2 = actual UOM, %3 = item number';
+ SetupActionsTok: Label 'setup_actions', Locked = true;
+ ActionTypeTok: Label 'action_type', Locked = true;
+ ActionDataTok: Label 'action_data', Locked = true;
+ CreateCustomersTok: Label 'create_customers', Locked = true;
+ CreateItemsTok: Label 'create_items', Locked = true;
+ CreatePostedShipmentTok: Label 'create_posted_shipment', Locked = true;
+ CreatePostedSalesInvoicesTok: Label 'create_posted_sales_invoices', Locked = true;
+ CreditMemoCreatedTok: Label 'credit_memo_created', Locked = true;
+ CreditMemoCustomerTok: Label 'credit_memo_customer', Locked = true;
+ CreditMemoPostedTok: Label 'credit_memo_posted', Locked = true;
+ CreditMemoLinesTok: Label 'credit_memo_lines', Locked = true;
+ CreditMemoTotalTok: Label 'credit_memo_total', Locked = true;
+ LineCountMismatchLbl: Label 'Expected %1 credit memo line(s) but found %2.', Comment = '%1 = expected count, %2 = actual count';
+ ItemNoTok: Label 'item_no', Locked = true;
+ QuantityTok: Label 'quantity', Locked = true;
+ UnitPriceTok: Label 'unit_price', Locked = true;
+ LineAmountTok: Label 'line_amount', Locked = true;
+ WorkDescContainsTok: Label 'work_description_contains', Locked = true;
+ ExpectedUomTok: Label 'expected_uom', Locked = true;
+}
diff --git a/samples/BCAgents/SalesReturnAgent/test/Setup/SRResourceProvider.Codeunit.al b/samples/BCAgents/SalesReturnAgent/test/Setup/SRResourceProvider.Codeunit.al
new file mode 100644
index 00000000..d1113dd7
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/test/Setup/SRResourceProvider.Codeunit.al
@@ -0,0 +1,122 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace SalesReturnAgent.Test.Setup;
+
+using Microsoft.Sales.Document;
+using Microsoft.Sales.History;
+using System.IO;
+using System.TestLibraries.Agents;
+using System.TestTools.TestRunner;
+using System.Utilities;
+
+codeunit 53749 "SR Resource Provider" implements IAgentTestResourceProvider
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure GetResource(ResourcePath: Text; var ResourceInStream: InStream; var FileName: Text[250]; var MIMEType: Text[100])
+ var
+ FileManagement: Codeunit "File Management";
+ begin
+ FileName := CopyStr(FileManagement.GetFileName(ResourcePath), 1, MaxStrLen(FileName));
+ NavApp.GetResource(ResourcePath, ResourceInStream);
+ MIMEType := GetMIMEType(FileName);
+ end;
+
+ procedure GenerateResource(GeneratorName: Text; GeneratorData: Codeunit "Test Input Json"; var ResourceInStream: InStream; var FileName: Text[250]; var MIMEType: Text[100])
+ begin
+ case GeneratorName of
+ SalesInvoiceTok:
+ GenerateSalesInvoicePdf(GeneratorData, ResourceInStream, FileName, MIMEType);
+ else
+ Error(UnknownFileGeneratorErr, GeneratorName);
+ end;
+ end;
+
+ local procedure GenerateSalesInvoicePdf(GeneratorData: Codeunit "Test Input Json"; var ResourceInStream: InStream; var FileName: Text[250]; var MIMEType: Text[100])
+ var
+ SalesHeader: Record "Sales Header";
+ SalesLine: Record "Sales Line";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ LinesData, LineElement, Element : Codeunit "Test Input Json";
+ RecRef: RecordRef;
+ OutStream: OutStream;
+ CustomerNo: Code[20];
+ ItemNo: Code[20];
+ PostedInvoiceNo: Code[20];
+ ElementExists: Boolean;
+ I: Integer;
+ begin
+ CustomerNo := CopyStr(GeneratorData.Element(CustomerNoTok).ValueAsText(), 1, MaxStrLen(CustomerNo));
+
+ LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Invoice, CustomerNo);
+
+ Element := GeneratorData.ElementExists(DateTok, ElementExists);
+ if ElementExists then begin
+ SalesHeader.Validate("Posting Date", Element.ValueAsDate());
+ SalesHeader.Validate("Document Date", Element.ValueAsDate());
+ SalesHeader.Modify(true);
+ end;
+
+ LinesData := GeneratorData.Element(LinesTok);
+ for I := 0 to LinesData.GetElementCount() - 1 do begin
+ LineElement := LinesData.ElementAt(I);
+ ItemNo := CopyStr(LineElement.Element(ItemNoTok).ValueAsText(), 1, MaxStrLen(ItemNo));
+
+ LibrarySales.CreateSalesLine(
+ SalesLine, SalesHeader,
+ SalesLine.Type::Item, ItemNo,
+ LineElement.Element(QuantityTok).ValueAsInteger());
+
+ Element := LineElement.ElementExists(DescriptionTok, ElementExists);
+ if ElementExists then begin
+ SalesLine.Validate(Description, CopyStr(Element.ValueAsText(), 1, MaxStrLen(SalesLine.Description)));
+ SalesLine.Modify(true);
+ end;
+ end;
+
+ PostedInvoiceNo := LibrarySales.PostSalesDocument(SalesHeader, true, true);
+ SalesInvoiceHeader.Get(PostedInvoiceNo);
+ SalesInvoiceHeader.SetRecFilter();
+ RecRef.GetTable(SalesInvoiceHeader);
+ TempBlob.CreateOutStream(OutStream);
+
+ if not Report.SaveAs(Report::"Standard Sales - Invoice", '', ReportFormat::Pdf, OutStream, RecRef) then
+ Error(FailedToGenerateFileErr, SalesInvoiceTok);
+
+ TempBlob.CreateInStream(ResourceInStream);
+
+ FileName := CopyStr(StrSubstNo(InvoiceFileNameLbl, PostedInvoiceNo), 1, MaxStrLen(FileName));
+ MIMEType := PdfMimeTypeTok;
+ end;
+
+ local procedure GetMIMEType(FileNameValue: Text[250]): Text[100]
+ begin
+ if FileNameValue.EndsWith('.png') then
+ exit('image/png');
+ if FileNameValue.EndsWith('.jpg') or FileNameValue.EndsWith('.jpeg') then
+ exit('image/jpeg');
+ if FileNameValue.EndsWith('.pdf') then
+ exit(PdfMimeTypeTok);
+ exit('application/octet-stream');
+ end;
+
+ var
+ TempBlob: Codeunit "Temp Blob";
+ LibrarySales: Codeunit "Library - Sales";
+ CustomerNoTok: Label 'customer_no', Locked = true;
+ DateTok: Label 'date', Locked = true;
+ LinesTok: Label 'lines', Locked = true;
+ ItemNoTok: Label 'item_no', Locked = true;
+ QuantityTok: Label 'quantity', Locked = true;
+ DescriptionTok: Label 'description', Locked = true;
+ SalesInvoiceTok: Label 'sales-invoice', Locked = true;
+ PdfMimeTypeTok: Label 'application/pdf', Locked = true;
+ InvoiceFileNameLbl: Label 'Invoice_%1.pdf', Locked = true, Comment = '%1 = document number';
+ UnknownFileGeneratorErr: Label 'Unknown file generator: %1', Comment = '%1 = generator name';
+ FailedToGenerateFileErr: Label 'Failed to generate file for generator: %1', Comment = '%1 = generator name';
+}
diff --git a/samples/BCAgents/SalesReturnAgent/test/Setup/SRTestsInstall.Codeunit.al b/samples/BCAgents/SalesReturnAgent/test/Setup/SRTestsInstall.Codeunit.al
new file mode 100644
index 00000000..6b930cee
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/test/Setup/SRTestsInstall.Codeunit.al
@@ -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 SalesReturnAgent.Test.Setup;
+
+using System.TestTools.AITestToolkit;
+
+codeunit 53747 "SR Tests Install"
+{
+ Subtype = Install;
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ trigger OnInstallAppPerCompany()
+ begin
+ LoadResources();
+ end;
+
+ internal procedure LoadResources()
+ var
+ DatasetPaths: List of [Text];
+ TestSuitePaths: List of [Text];
+ ResourcePath: Text;
+ begin
+ // Load Datasets
+ DatasetPaths := NavApp.ListResources('*.yaml');
+ foreach ResourcePath in DatasetPaths do
+ SetupDataInput(ResourcePath);
+
+ // Load Test Suites
+ TestSuitePaths := NavApp.ListResources('*.xml');
+ foreach ResourcePath in TestSuitePaths do
+ SetupTestSuite(ResourcePath);
+ end;
+
+ local procedure SetupDataInput(FilePath: Text)
+ var
+ AITALTestSuiteMgt: Codeunit "AIT AL Test Suite Mgt";
+ FileName: Text;
+ ResInStream: InStream;
+ begin
+ FileName := FilePath.Substring(FilePath.LastIndexOf('/') + 1);
+
+ NavApp.GetResource(FilePath, ResInStream, TextEncoding::UTF8);
+ AITALTestSuiteMgt.ImportTestInputs(FileName, ResInStream);
+ end;
+
+ local procedure SetupTestSuite(FilePath: Text)
+ var
+ AITALTestSuiteMgt: Codeunit "AIT AL Test Suite Mgt";
+ XMLSetupInStream: InStream;
+ begin
+ NavApp.GetResource(FilePath, XMLSetupInStream);
+ AITALTestSuiteMgt.ImportAITestSuite(XMLSetupInStream);
+ end;
+}
diff --git a/samples/BCAgents/SalesReturnAgent/test/Tests/SRAccuracyTest.Codeunit.al b/samples/BCAgents/SalesReturnAgent/test/Tests/SRAccuracyTest.Codeunit.al
new file mode 100644
index 00000000..ab33b909
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/test/Tests/SRAccuracyTest.Codeunit.al
@@ -0,0 +1,130 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace SalesReturnAgent.Test;
+
+using SalesReturnAgent.Setup;
+using SalesReturnAgent.Test.Libraries;
+using SalesReturnAgent.Test.Setup;
+using System.Agents;
+using System.TestLibraries.Agents;
+using System.TestLibraries.Utilities;
+using System.TestTools.AITestToolkit;
+using System.TestTools.TestRunner;
+
+codeunit 53745 "SR Accuracy Test"
+{
+ Subtype = Test;
+ TestType = AITest; // Defines the test as an AI Eval.
+ TestPermissions = Disabled;
+ RequiredTestIsolation = Disabled;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ local procedure Initialize()
+ var
+ SalesRetAgentSetup: Codeunit "Sales Ret. Agent Setup";
+ AgentTestContext: Codeunit "Agent Test Context";
+ begin
+ if not Initialized then begin
+ // Gets whether an agent has already been selected in the AI Eval Suite page.
+ // If not, get or create a default agent.
+ AgentTestContext.GetAgentUserSecurityID(AgentUserSecurityId);
+ if IsNullGuid(AgentUserSecurityId) then
+ AgentUserSecurityId := SalesRetAgentSetup.GetOrCreateDefaultAgent();
+
+ // Run the per-suite test data setup only once.
+ SetupPerSuiteTestData();
+ Initialized := true;
+ Commit();
+ end;
+
+ // Ensure the agent is active.
+ LibraryAgent.EnsureAgentIsActive(AgentUserSecurityId);
+
+ // Clear any existing data that might interfere with the test.
+ SRTestLibrary.DeleteAllSalesCreditMemos();
+ SRTestLibrary.DeleteAllUnpostedSalesInvoices();
+ Commit();
+ end;
+
+ [Test]
+ procedure TestAccuracy()
+ var
+ AgentTask: Record "Agent Task";
+ SREventHandler: Codeunit "SR Event Handler";
+ SRResourceProvider: Codeunit "SR Resource Provider";
+ TurnSuccessful: Boolean;
+ ErrorReason: Text;
+ ContinueWithNextTurn: Boolean;
+ AgentStatusErr: Label 'The agent task did not complete successfully. Task status: %1.', Comment = '%1 = task status';
+ begin
+ BindSubscription(SREventHandler);
+ Initialize();
+
+ repeat
+ Clear(ErrorReason);
+ SetupTurnData();
+
+ TurnSuccessful := LibraryAgent.RunTurnAndWait(AgentUserSecurityId, AgentTask, SRResourceProvider);
+
+ if TurnSuccessful then
+ TurnSuccessful := ValidateTurn(ErrorReason)
+ else
+ ErrorReason := StrSubstNo(AgentStatusErr, AgentTask.Status);
+
+ ContinueWithNextTurn := LibraryAgent.FinalizeTurn(AgentTask, TurnSuccessful, ErrorReason);
+ until not ContinueWithNextTurn;
+
+ Assert.IsTrue(TurnSuccessful, ErrorReason);
+ end;
+
+ local procedure ValidateTurn(var ErrorReason: Text): Boolean
+ begin
+ if not SRTestLibrary.ValidateCreditMemoCreated(ErrorReason) then
+ exit(false);
+
+ if not SRTestLibrary.ValidateWorkDescription(ErrorReason) then
+ exit(false);
+
+ exit(true);
+ end;
+
+ local procedure SetupTurnData()
+ var
+ TurnSetup: Codeunit "Test Input Json";
+ TurnSetupExists: Boolean;
+ begin
+ TurnSetupExists := AITTestContext.GetTurnSetup(TurnSetup);
+ if not TurnSetupExists then
+ exit;
+
+ SRTestLibrary.CreateCreditMemoTestData(TurnSetup);
+ end;
+
+ local procedure SetupPerSuiteTestData()
+ var
+ SuiteTestSetup: Codeunit "Test Input Json";
+ begin
+ if AITTestContext.IsSuiteSetupDone() then
+ exit;
+
+ SuiteTestSetup := AITTestContext.GetEvalSuiteSetupDataInput();
+
+ SRTestLibrary.CreateCustomers(SuiteTestSetup);
+ SRTestLibrary.CreateItems(SuiteTestSetup);
+ SRTestLibrary.CreatePostedSalesInvoices(SuiteTestSetup);
+
+ AITTestContext.SetEvalSuiteSetupCompleted();
+ end;
+
+ var
+ Assert: Codeunit "Library Assert";
+ LibraryAgent: Codeunit "Library - Agent";
+ SRTestLibrary: Codeunit "SR Test Library";
+ AITTestContext: Codeunit "AIT Test Context";
+ AgentUserSecurityId: Guid;
+ Initialized: Boolean;
+}
diff --git a/samples/BCAgents/SalesReturnAgent/test/app.json b/samples/BCAgents/SalesReturnAgent/test/app.json
new file mode 100644
index 00000000..a60599c4
--- /dev/null
+++ b/samples/BCAgents/SalesReturnAgent/test/app.json
@@ -0,0 +1,47 @@
+{
+ "id": "45d741e7-0c9e-46b4-828b-550ebd88da2c",
+ "name": "Sales Return Agent Sample Test",
+ "publisher": "Partner",
+ "brief": "Sample tests demonstrating how to implement tests for the Sales Return agent.",
+ "description": "Sample automation tests demonstrating how to implement tests for the Sales Return agent.",
+ "version": "$(app_currentVersion)",
+ "privacyStatement": "https://go.microsoft.com/fwlink/?LinkId=724009",
+ "EULA": "https://go.microsoft.com/fwlink/?LinkId=847985",
+ "help": "https://go.microsoft.com/fwlink/?LinkId=849257",
+ "url": "https://go.microsoft.com/fwlink/?LinkId=724011",
+ "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?LinkId=849257",
+ "logo": "ExtensionLogo.png",
+ "screenshots": [],
+ "application": "$(app_minimumVersion)",
+ "platform": "$(app_platformVersion)",
+ "dependencies": [
+ {
+ "id": "517f890c-b49f-47de-8120-d0327974b89d",
+ "name": "AI Development Toolkit - Evaluation",
+ "publisher": "Microsoft",
+ "version": "$(app_minimumVersion)"
+ },
+ {
+ "id": "25341373-464d-4087-b195-b91bbcdb2045",
+ "name": "Sales Return Agent Sample",
+ "publisher": "Partner",
+ "version": "$(app_minimumVersion)"
+ }
+ ],
+ "target": "Cloud",
+ "features": [
+ "GenerateCaptions",
+ "TranslationFile",
+ "NoImplicitWith",
+ "NoPromotedActionProperties"
+ ],
+ "idRanges": [
+ {
+ "from": 53745,
+ "to": 53749
+ }
+ ],
+ "resourceFolders": [
+ ".resources"
+ ]
+}