-
Notifications
You must be signed in to change notification settings - Fork 444
Shopify Copilot Tax Matching Agent: human-in-the-loop review + tests (stack 3/3) #10409
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Onat Buyukakkus (onbuyuka)
wants to merge
6
commits into
features/445769-2-agent-core
from
features/445769-3-hitl
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
8c506d0
Shopify Copilot Tax Matching Agent: human-in-the-loop review experien…
onbuyuka d0cfad6
Address PR review: copyright headers, Shopify-branded Sales Order sur…
onbuyuka 79de325
Affix base-table members with Shpfy: update review UI, tests, and docs
onbuyuka 43a988e
Sync telemetry table with the uptake/verbosity changes
onbuyuka afeeb7c
Sync telemetry table: internal/AOAI-failure traces scoped to publisher
onbuyuka 0c07abf
Address review: log the tax-review-open usage event only on success
onbuyuka File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
292 changes: 292 additions & 0 deletions
292
src/Apps/NA/ShopifyNA/app/src/Tax Matching Agent/Codeunits/ShpfyTMANotify.Codeunit.al
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,292 @@ | ||
| // ------------------------------------------------------------------------------------------------ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. See License.txt in the project root for license information. | ||
| // ------------------------------------------------------------------------------------------------ | ||
|
|
||
| namespace Microsoft.Integration.Shopify; | ||
|
|
||
| using Microsoft.Sales.Document; | ||
| using System.Environment.Configuration; | ||
| using System.Telemetry; | ||
|
|
||
| /// <summary> | ||
| /// Codeunit Shpfy TMA Notify (ID 30476). | ||
| /// Owns the tax review notifications (on the BC Sales Order and the Shopify Order) | ||
| /// and the review-page drills. The notifications are stateless: whether to prompt is | ||
| /// derived live from the Sales Header marker, the originating Shopify order's | ||
| /// Tax Match Reviewed flag, and the per-user My Notifications toggle — there is no | ||
| /// dedicated notification table. | ||
| /// </summary> | ||
| codeunit 30476 "Shpfy TMA Notify" | ||
| { | ||
| Access = Internal; | ||
| InherentPermissions = X; | ||
| InherentEntitlements = X; | ||
|
|
||
| var | ||
| FeatureNotificationIdLbl: Label '{e9d8c7b6-a5f4-4e32-9d10-cb87a65f43e2}', Locked = true; | ||
| OrderNotificationIdLbl: Label '{a7c3f1e2-9b4d-4c8a-8e6f-2d1b0a9c8e7d}', Locked = true; | ||
| NotifMsgLbl: Label 'Tax Matching Agent set Tax Area %1 on this Shopify order. Review before posting.', Comment = '%1 = Tax Area Code'; | ||
| OrderNotifMsgLbl: Label 'Tax Matching Agent set Tax Area %1 on this Shopify order. Review the matched tax jurisdictions.', Comment = '%1 = Tax Area Code'; | ||
| OrderNotifConflictMsgLbl: Label 'Tax Matching Agent set Tax Area %1 on this Shopify order, but one or more tax rates differ from Business Central. Review the highlighted rates before creating the sales document.', Comment = '%1 = Tax Area Code'; | ||
| ShowDecisionsActionLbl: Label 'Show Tax Match Decisions'; | ||
| OrderReviewActionLbl: Label 'Review'; | ||
| MarkReviewedActionLbl: Label 'Mark as reviewed'; | ||
| DisableActionLbl: Label 'Don''t show again'; | ||
| MyNotificationCaptionLbl: Label 'Shopify Tax Matching Agent review prompt'; | ||
| MyNotificationDescriptionLbl: Label 'Shows a one-time prompt on each Sales Order where the Tax Matching Agent populated tax fields from a Shopify order, so you can review the AI-generated decisions before posting.'; | ||
| OrderMyNotifCaptionLbl: Label 'Shopify Tax Matching Agent order review prompt'; | ||
| OrderMyNotifDescriptionLbl: Label 'Shows a prompt on a Shopify order whose tax was matched by the Tax Matching Agent, so you can review and approve the AI-generated tax match.'; | ||
|
|
||
| /// <summary> | ||
| /// Fires the review prompt on the Sales Order when the Tax Matching Agent populated its tax fields and | ||
| /// the originating Shopify order has not yet been reviewed. Stateless — whether to prompt | ||
| /// comes from the order's Tax Match Reviewed flag plus the per-user My | ||
| /// Notifications toggle, so no per-user row is stored. | ||
| /// </summary> | ||
| procedure SendForCurrentSalesHeader(SalesHeader: Record "Sales Header") | ||
| var | ||
| OrderHeader: Record "Shpfy Order Header"; | ||
| MyNotifications: Record "My Notifications"; | ||
| TMARegister: Codeunit "Shpfy TMA Register"; | ||
| FeatureTelemetry: Codeunit "Feature Telemetry"; | ||
| Notif: Notification; | ||
| begin | ||
| if not GuiAllowed() then | ||
| exit; | ||
| if not FindOrderForReview(SalesHeader, OrderHeader) then | ||
| exit; | ||
| if OrderHeader."Tax Match Reviewed" then | ||
| exit; | ||
|
|
||
| if MyNotifications.WritePermission() then | ||
| MyNotifications.InsertDefault(GetFeatureNotificationId(), MyNotificationCaptionLbl, MyNotificationDescriptionLbl, true); | ||
| if not MyNotifications.IsEnabled(GetFeatureNotificationId()) then | ||
| exit; | ||
|
|
||
| Notif.Id := GetFeatureNotificationId(); | ||
| Notif.Message(StrSubstNo(NotifMsgLbl, SalesHeader."Tax Area Code")); | ||
| Notif.Scope := NotificationScope::LocalScope; | ||
| Notif.SetData('SalesHeaderSystemId', Format(SalesHeader.SystemId)); | ||
| Notif.AddAction(ShowDecisionsActionLbl, Codeunit::"Shpfy TMA Notify", 'OpenShopifyOrder'); | ||
| Notif.AddAction(MarkReviewedActionLbl, Codeunit::"Shpfy TMA Notify", 'MarkReviewed'); | ||
| Notif.AddAction(DisableActionLbl, Codeunit::"Shpfy TMA Notify", 'DisableForUser'); | ||
| Notif.Send(); | ||
|
|
||
| FeatureTelemetry.LogUsage('0000UMT', TMARegister.FeatureName(), 'tax review notification sent'); | ||
| end; | ||
|
|
||
| procedure OpenShopifyOrder(Notif: Notification) | ||
| var | ||
| SalesHeader: Record "Sales Header"; | ||
| OrderMgt: Codeunit "Shpfy Order Mgt."; | ||
| TMARegister: Codeunit "Shpfy TMA Register"; | ||
| FeatureTelemetry: Codeunit "Feature Telemetry"; | ||
| VariantRec: Variant; | ||
| begin | ||
| if not TryGetSalesHeader(Notif, SalesHeader) then | ||
| exit; | ||
|
|
||
| // Prefer the Tax Match Review page; fall back to the raw Shopify order. | ||
| if RunReviewForSalesHeader(SalesHeader) then | ||
| FeatureTelemetry.LogUsage('0000UMU', TMARegister.FeatureName(), 'tax review opened') | ||
| else begin | ||
| VariantRec := SalesHeader; | ||
| OrderMgt.ShowShopifyOrder(VariantRec); | ||
| FeatureTelemetry.LogUsage('0000UN9', TMARegister.FeatureName(), 'tax review fell back to the Shopify order (review page could not be resolved)'); | ||
|
onbuyuka marked this conversation as resolved.
|
||
| end; | ||
| end; | ||
|
|
||
| /// <summary> | ||
| /// Sends the actionable review prompt on the Shopify order itself. The order page calls | ||
| /// this once per order per page session. Clicking Review opens the Tax Match | ||
|
onbuyuka marked this conversation as resolved.
|
||
| /// Review page. | ||
| /// </summary> | ||
| procedure SendOrderReviewNotification(OrderHeader: Record "Shpfy Order Header") | ||
| var | ||
| MyNotifications: Record "My Notifications"; | ||
| TMARegister: Codeunit "Shpfy TMA Register"; | ||
| FeatureTelemetry: Codeunit "Feature Telemetry"; | ||
| Notif: Notification; | ||
| begin | ||
| if not GuiAllowed() then | ||
| exit; | ||
|
|
||
| if MyNotifications.WritePermission() then | ||
| MyNotifications.InsertDefault(GetOrderNotificationId(), OrderMyNotifCaptionLbl, OrderMyNotifDescriptionLbl, true); | ||
| if not MyNotifications.IsEnabled(GetOrderNotificationId()) then | ||
| exit; | ||
|
|
||
| Notif.Id := GetOrderNotificationId(); | ||
| if OrderHeader."Tax Rate Conflict" then | ||
| Notif.Message(StrSubstNo(OrderNotifConflictMsgLbl, OrderHeader."Tax Area Code")) | ||
| else | ||
| Notif.Message(StrSubstNo(OrderNotifMsgLbl, OrderHeader."Tax Area Code")); | ||
| Notif.Scope := NotificationScope::LocalScope; | ||
| Notif.SetData('ShpfyOrderSystemId', Format(OrderHeader.SystemId)); | ||
| Notif.AddAction(OrderReviewActionLbl, Codeunit::"Shpfy TMA Notify", 'OpenReviewForOrder'); | ||
| Notif.AddAction(DisableActionLbl, Codeunit::"Shpfy TMA Notify", 'DisableOrderNotifForUser'); | ||
| Notif.Send(); | ||
|
|
||
| FeatureTelemetry.LogUsage('0000UMV', TMARegister.FeatureName(), 'tax order review notification sent'); | ||
| end; | ||
|
|
||
| procedure OpenReviewForOrder(Notif: Notification) | ||
| var | ||
| OrderHeader: Record "Shpfy Order Header"; | ||
| TMARegister: Codeunit "Shpfy TMA Register"; | ||
| FeatureTelemetry: Codeunit "Feature Telemetry"; | ||
| SystemIdText: Text; | ||
| OrderSystemId: Guid; | ||
| begin | ||
| SystemIdText := Notif.GetData('ShpfyOrderSystemId'); | ||
| if SystemIdText = '' then | ||
| exit; | ||
| if not Evaluate(OrderSystemId, SystemIdText) then | ||
| exit; | ||
| if not OrderHeader.GetBySystemId(OrderSystemId) then | ||
| exit; | ||
|
|
||
| RunReviewPage(OrderHeader); | ||
|
|
||
| FeatureTelemetry.LogUsage('0000UMW', TMARegister.FeatureName(), 'tax order review opened'); | ||
| end; | ||
|
|
||
| procedure DisableOrderNotifForUser(Notif: Notification) | ||
| var | ||
| MyNotifications: Record "My Notifications"; | ||
| begin | ||
| if MyNotifications.WritePermission() then | ||
| if not MyNotifications.Disable(GetOrderNotificationId()) then | ||
| MyNotifications.InsertDefault(GetOrderNotificationId(), OrderMyNotifCaptionLbl, OrderMyNotifDescriptionLbl, false); | ||
| end; | ||
|
|
||
| /// <summary> | ||
| /// Opens the Tax Match Review page for the Shopify order that produced the given | ||
| /// Sales Header (resolved via Shpfy Order Header."Sales Order No."). Returns false when no | ||
| /// such order exists, so callers can fall back to another surface. | ||
| /// </summary> | ||
| internal procedure RunReviewForSalesHeader(SalesHeader: Record "Sales Header"): Boolean | ||
| var | ||
| OrderHeader: Record "Shpfy Order Header"; | ||
| begin | ||
| if not FindOrderForReview(SalesHeader, OrderHeader) then | ||
| exit(false); | ||
|
|
||
| RunReviewPage(OrderHeader); | ||
| exit(true); | ||
| end; | ||
|
|
||
| /// <summary> | ||
| /// The single entry point that opens the Tax Match Review page for a Shopify | ||
| /// order. All review surfaces (order-page action, order-page notification, Sales Order | ||
| /// action, Sales Order notification) resolve their Shpfy Order Header and route through | ||
| /// here, so the page is opened one consistent way. | ||
| /// </summary> | ||
| internal procedure RunReviewPage(var OrderHeader: Record "Shpfy Order Header") | ||
| begin | ||
| OrderHeader.SetRecFilter(); | ||
| Page.Run(Page::"Shpfy TMA Review", OrderHeader); | ||
| end; | ||
|
|
||
| procedure MarkReviewed(Notif: Notification) | ||
|
onbuyuka marked this conversation as resolved.
|
||
| var | ||
| SalesHeader: Record "Sales Header"; | ||
| OrderHeader: Record "Shpfy Order Header"; | ||
| TMARegister: Codeunit "Shpfy TMA Register"; | ||
| FeatureTelemetry: Codeunit "Feature Telemetry"; | ||
| begin | ||
| if not TryGetSalesHeader(Notif, SalesHeader) then | ||
| exit; | ||
| if not FindOrderForReview(SalesHeader, OrderHeader) then | ||
| exit; | ||
| if OrderHeader."Tax Match Reviewed" then | ||
| exit; | ||
|
|
||
| OrderHeader."Tax Match Reviewed" := true; | ||
|
onbuyuka marked this conversation as resolved.
|
||
| OrderHeader.Modify(); | ||
|
|
||
| FeatureTelemetry.LogUsage('0000UMX', TMARegister.FeatureName(), 'tax review notification marked reviewed'); | ||
| end; | ||
|
|
||
| /// <summary> | ||
| /// Reverses an approval: clears the order's Tax Match Reviewed flag so the order is | ||
| /// held for review again (it is created only once re-approved), and un-verifies the | ||
| /// agent-created Tax Jurisdictions the order used so they return to their provisional state. | ||
| /// Exposed as internal so the review page's Undo Approval action and tests both drive the | ||
| /// same state change. | ||
| /// </summary> | ||
| internal procedure UndoApproval(var OrderHeader: Record "Shpfy Order Header") | ||
| var | ||
| TMAMatcher: Codeunit "Shpfy TMA Matcher"; | ||
| TMARegister: Codeunit "Shpfy TMA Register"; | ||
| FeatureTelemetry: Codeunit "Feature Telemetry"; | ||
| begin | ||
| if not OrderHeader."Tax Match Reviewed" then | ||
| exit; | ||
|
|
||
| OrderHeader."Tax Match Reviewed" := false; | ||
| OrderHeader.Modify(); | ||
|
|
||
| TMAMatcher.UnverifyAgentJurisdictions(OrderHeader); | ||
|
|
||
| FeatureTelemetry.LogUsage('0000UN7', TMARegister.FeatureName(), 'tax match approval undone'); | ||
| end; | ||
|
|
||
| procedure DisableForUser(Notif: Notification) | ||
| var | ||
| MyNotifications: Record "My Notifications"; | ||
| TMARegister: Codeunit "Shpfy TMA Register"; | ||
| FeatureTelemetry: Codeunit "Feature Telemetry"; | ||
| begin | ||
| if MyNotifications.WritePermission() then | ||
| if not MyNotifications.Disable(GetFeatureNotificationId()) then | ||
| MyNotifications.InsertDefault(GetFeatureNotificationId(), MyNotificationCaptionLbl, MyNotificationDescriptionLbl, false); | ||
| MarkReviewed(Notif); | ||
|
|
||
| FeatureTelemetry.LogUsage('0000UMY', TMARegister.FeatureName(), 'tax review notification disabled per user'); | ||
| end; | ||
|
|
||
| /// <summary> | ||
| /// Resolves the originating Shopify order for a Sales Header via | ||
| /// Shpfy Order Header."Sales Order No.". Returns false when the Sales Header has no | ||
| /// number or no linked Shopify order. | ||
| /// </summary> | ||
| local procedure FindOrderForReview(SalesHeader: Record "Sales Header"; var OrderHeader: Record "Shpfy Order Header"): Boolean | ||
| begin | ||
| if SalesHeader."No." = '' then | ||
| exit(false); | ||
| OrderHeader.SetCurrentKey("Sales Order No."); | ||
|
onbuyuka marked this conversation as resolved.
|
||
| OrderHeader.SetRange("Sales Order No.", SalesHeader."No."); | ||
| exit(OrderHeader.FindFirst()); | ||
| end; | ||
|
|
||
| local procedure TryGetSalesHeader(Notif: Notification; var SalesHeader: Record "Sales Header"): Boolean | ||
| var | ||
| SystemIdText: Text; | ||
| SystemId: Guid; | ||
| begin | ||
| SystemIdText := Notif.GetData('SalesHeaderSystemId'); | ||
| if SystemIdText = '' then | ||
| exit(false); | ||
| if not Evaluate(SystemId, SystemIdText) then | ||
| exit(false); | ||
| exit(SalesHeader.GetBySystemId(SystemId)); | ||
| end; | ||
|
|
||
| local procedure GetFeatureNotificationId(): Guid | ||
| var | ||
| FeatureNotificationId: Guid; | ||
| begin | ||
| Evaluate(FeatureNotificationId, FeatureNotificationIdLbl); | ||
| exit(FeatureNotificationId); | ||
| end; | ||
|
|
||
| local procedure GetOrderNotificationId(): Guid | ||
| var | ||
| OrderNotificationId: Guid; | ||
| begin | ||
| Evaluate(OrderNotificationId, OrderNotificationIdLbl); | ||
| exit(OrderNotificationId); | ||
| end; | ||
| } | ||
96 changes: 96 additions & 0 deletions
96
src/Apps/NA/ShopifyNA/app/src/Tax Matching Agent/Page Extensions/ShpfyTMAOrder.PageExt.al
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| // ------------------------------------------------------------------------------------------------ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. See License.txt in the project root for license information. | ||
| // ------------------------------------------------------------------------------------------------ | ||
|
|
||
| namespace Microsoft.Integration.Shopify; | ||
|
|
||
| /// <summary> | ||
| /// PageExtension Shpfy TMA Order (ID 30479) extends Shpfy Order (page 30113). | ||
| /// Surfaces the tax match on the Shopify order: an actionable notification and a | ||
| /// review action (review icon) that opens the Tax Match Review page (page 30471), | ||
| /// where the resolved Tax Area and per-line Tax Jurisdiction Codes are shown with AI | ||
| /// confidence indicators and the match is approved. The action is captioned "Review and | ||
| /// Approve Tax Match" while approval is still pending (the order is held — the shop | ||
| /// requires review or there is a rate conflict — and it is not yet approved) and "Review | ||
| /// Tax Match" otherwise. A held order's Sales Document is not created until the match | ||
| /// is approved on the review page. | ||
| /// </summary> | ||
| pageextension 30479 "Shpfy TMA Order" extends "Shpfy Order" | ||
| { | ||
| actions | ||
| { | ||
| addafter(CancelOrder) | ||
| { | ||
| action(ShpfyReviewAndApproveTaxMatch) | ||
| { | ||
| ApplicationArea = All; | ||
| Caption = 'Review and Approve Tax Match'; | ||
| Image = SparkleFilled; | ||
| ToolTip = 'Opens the tax match review for this order. Because this order is held for review, the Sales Document is not created until you approve the match on the review page.'; | ||
| Visible = ShowReviewEntry and not Rec."Tax Match Reviewed" and (ReviewRequired or Rec."Tax Rate Conflict" or Rec."Tax Match Incomplete"); | ||
|
|
||
| trigger OnAction() | ||
| begin | ||
| OpenReviewPage(); | ||
| end; | ||
| } | ||
| action(ShpfyReviewTaxMatch) | ||
| { | ||
| ApplicationArea = All; | ||
| Caption = 'Review Tax Match'; | ||
| Image = SparkleFilled; | ||
| ToolTip = 'Opens the tax match review for this order, where you can see the resolved Tax Area and the per-line Tax Jurisdiction Codes that the Tax Matching Agent matched.'; | ||
| Visible = ShowReviewEntry and (Rec."Tax Match Reviewed" or not (ReviewRequired or Rec."Tax Rate Conflict" or Rec."Tax Match Incomplete")); | ||
|
|
||
| trigger OnAction() | ||
| begin | ||
| OpenReviewPage(); | ||
| end; | ||
| } | ||
| } | ||
| addfirst(Category_Process) | ||
| { | ||
| actionref(ShpfyReviewAndApproveTaxMatch_Promoted; ShpfyReviewAndApproveTaxMatch) { } | ||
| actionref(ShpfyReviewTaxMatch_Promoted; ShpfyReviewTaxMatch) { } | ||
| } | ||
| } | ||
|
|
||
| trigger OnAfterGetCurrRecord() | ||
| var | ||
| Shop: Record "Shpfy Shop"; | ||
| TMANotify: Codeunit "Shpfy TMA Notify"; | ||
| TMAEvents: Codeunit "Shpfy TMA Events"; | ||
| begin | ||
| if Shop.Get(Rec."Shop Code") then begin | ||
| TaxMatchingAgentEnabled := Shop."Tax Matching Agent Enabled"; | ||
| ReviewRequired := TMAEvents.IsHeldForReviewPreference(Rec, Shop); | ||
| end else begin | ||
| TaxMatchingAgentEnabled := false; | ||
| ReviewRequired := false; | ||
| end; | ||
|
|
||
| ShowReviewEntry := TaxMatchingAgentEnabled and Rec."Tax Match Applied"; | ||
|
|
||
| if ShowReviewEntry and (not Rec."Tax Match Reviewed") and (NotifiedOrderId <> Rec."Shopify Order Id") then begin | ||
| TMANotify.SendOrderReviewNotification(Rec); | ||
| NotifiedOrderId := Rec."Shopify Order Id"; | ||
| end; | ||
| end; | ||
|
|
||
| var | ||
| TaxMatchingAgentEnabled: Boolean; | ||
| ReviewRequired: Boolean; | ||
| ShowReviewEntry: Boolean; | ||
| NotifiedOrderId: BigInteger; | ||
|
|
||
| local procedure OpenReviewPage() | ||
| var | ||
| OrderHeader: Record "Shpfy Order Header"; | ||
| TMANotify: Codeunit "Shpfy TMA Notify"; | ||
| begin | ||
| OrderHeader := Rec; | ||
| TMANotify.RunReviewPage(OrderHeader); | ||
| CurrPage.Update(false); | ||
| end; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.