Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
455 changes: 455 additions & 0 deletions src/Apps/NA/ShopifyNA/app/Architecture.md

Large diffs are not rendered by default.

359 changes: 359 additions & 0 deletions src/Apps/NA/ShopifyNA/app/TestMatrix.md

Large diffs are not rendered by default.

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');
Comment thread
onbuyuka marked this conversation as resolved.
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)');
Comment thread
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
Comment thread
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)
Comment thread
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;
Comment thread
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.");
Comment thread
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;
}
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;
}
Loading
Loading