[Shopify] Copilot Tax Matching + Tax Area/Tax Liable/Tax Exempt support#7453
Draft
onbuyuka wants to merge 33 commits into
Draft
[Shopify] Copilot Tax Matching + Tax Area/Tax Liable/Tax Exempt support#7453onbuyuka wants to merge 33 commits into
onbuyuka wants to merge 33 commits into
Conversation
Import the order-level taxExempt flag from Shopify GraphQL API and add Tax Area mapping during order import. Tax Area Code and Tax Liable are now resolved in MapTaxArea (called during order mapping) and stored on the order header, then carried to the sales document in ProcessOrder. - New fields on Shpfy Order Header: Tax Area Code (1070), Tax Liable (1080), Tax Exempt (1090) - Add taxExempt to GetOrderHeader GraphQL query - Parse taxExempt from JSON in ShpfyImportOrder - New MapTaxArea procedure in ShpfyOrderMapping, called from both B2C and B2B mapping paths. Respects Tax Exempt: if the order is exempt, Tax Area Code is set but Tax Liable stays false - ProcessOrder reads Tax Area Code and Tax Liable from the order header instead of calling FindTaxArea at document creation time - Display Tax Area Code, Tax Liable, Tax Exempt on the Order page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Refund credit memos were missing Tax Liable (only Tax Area Code was set) and refund pages had no tax visibility. - Add FlowFields on Shpfy Refund Header (110-112) for Tax Area Code, Tax Liable, and Tax Exempt from the parent order - Fix ShpfyCreateSalesDocRefund: read Tax Area Code and Tax Liable from the order header (consistent with ProcessOrder) instead of calling FindTaxArea, and set both on the credit memo - Add Tax group on Refund page with Tax Area Code, Tax Liable, Tax Exempt (read-only, inherited from parent order via FlowFields) - Add Tax Lines navigation action on Refund page, reusing the existing Shpfy Order Tax Lines page filtered to the parent order's lines Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- UnitTestCreateSalesDocumentTaxLiable: verifies Tax Liable flows from Shpfy Tax Area through MapTaxArea to the sales document - UnitTestCreateSalesDocumentTaxExempt: verifies Tax Exempt order gets Tax Area Code but Tax Liable stays false - UnitTestCrMemoInheritsTaxAreaAndTaxLiable: verifies credit memo from refund inherits Tax Area Code and Tax Liable from parent order - Update CreateTaxArea helper to set Tax Liable = true on Shpfy Tax Area Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
LLM-powered tax jurisdiction matching for Shopify orders. Uses Azure OpenAI (GPT-4.1) to match free-text Shopify tax line descriptions to BC Tax Jurisdictions during order import, then finds or creates Tax Areas from the matched jurisdictions. Architecture: - ShpfyCopilotTaxRegister (30470): Capability registration - ShpfyCopilotTaxMatcher (30471): Gather data, call AOAI, apply matches - ShpfyTaxAreaBuilder (30472): Find/create Tax Area from jurisdictions - ShpfyCopilotTaxEvents (30473): OnAfterMapShopifyOrder subscriber - ShpfyTaxMatchFunction (30474): AOAI Function interface (tool def) - ShpfyCopilotTaxInstall (30475): Install trigger Features: - Sync LLM call during order import (no user interaction) - Forced function calling with structured JSON output - Temperature 0 for deterministic results - Auto-create jurisdictions/areas with admin opt-in - Guards: Tax Exempt, capability active, feature enabled per shop - Includes RAI overview, architecture doc, and test matrix Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add the event and field that the Copilot Tax Matching app depends on: - OnAfterMapShopifyOrder integration event in ShpfyOrderEvents, fired at the end of DoMapping after all order mapping is complete. The Copilot app subscribes to this to trigger LLM-based tax matching. - Fire the event in ShpfyOrderMapping.DoMapping after line mapping. - Tax Jurisdiction Code field (10) on Shpfy Order Tax Line table, with TableRelation to Tax Jurisdiction. The Copilot matcher writes the matched jurisdiction code to each tax line. - Display Tax Jurisdiction Code on the Shpfy Order Tax Lines page. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Copilot app uses public events and standard APIs — it does not need access to internal members of the Shopify connector. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move Copilot Tax Matching folders out of the main Shopify workspace into a dedicated ShopifyCopilot workspace that includes the connector app, Copilot app, and Copilot test app. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shorter folder name — the Shpfy prefix is in the app/object names, not needed on the folder. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract all inline string literals in StrSubstNo calls to Label variables in ShpfyCTMVerify. Also re-remove internalsVisibleTo entries that were re-added by linter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Documentation: - Architecture.md: fix "non-invasive" claim (we do modify the connector), update execution flow diagram to show MapTaxArea in both B2C/B2B paths and Tax Exempt guard, fix folder path reference from ShpfyCopilotTaxMatching to CopilotTaxMatching - RAI-Overview.md: add Tax Exempt as a guard condition - TestMatrix.md: add G5 (Tax Exempt guard), renumber G5-G7 to G6-G8 Code: - Add missing using Microsoft.Finance.SalesTax to ShpfyOrderHeader - Suppress AA0181 warnings on OrderHeader.Find() calls in test codeunits (GuardTest, MatchTest, TaxAreaTest) - Remove unnecessary begin/end in TaxAreaTest - Remove unused Item variable in TestLibrary Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
AL Documentation Audit
Documentation gaps were detected in the following apps:
- Shopify-Connector-Test: 0% documentation coverage
- Shopify-Copilot-Tax-Matching-Tests: 0% documentation coverage
- Shopify-Copilot-Tax-Matching: 0% documentation coverage
To generate documentation, run /al-docs init or /al-docs update using GitHub Copilot CLI or Claude Code.
This review is for awareness to help keep documentation in sync with code changes. It is okay to dismiss this request.
…res/445769-ShpfyTaxCopilot # Conflicts: # src/Apps/W1/Shopify/App/src/Order handling/Tables/ShpfyOrderHeader.Table.al # src/Apps/W1/Shopify/Test/Order Handling/ShpfyOrdersAPITest.Codeunit.al
RAI-driven changes so automated tax-jurisdiction matching is traceable, visibly marked, and actively surfaced for human review without breaking auto-create-sales-doc or background sync flows. Three pillars: - Activity Log entries (Type=AI) per matched tax line and per resolved tax area, anchored on the Shpfy entities so the platform AI confidence indicator renders on the relevant fields - Persistent 'Copilot Tax Match Applied' marker on Shpfy Order Header that propagates to BC Sales Header via the existing OnAfterCreateSalesHeader event - One-time non-blocking notification on first Sales Order open with three actions (drill into Shopify order, mark reviewed, suppress per-user via MyNotifications) Other changes: - Tax Lines list embedded as a ListPart on the Shpfy Order page so the platform AI indicators render on the Tax Jurisdiction Code cell (standalone list pages don't render the indicator) - Move Tax Jurisdiction Code field from the connector to the Copilot app TableExt; the connector itself never read or wrote it - EnsureTaxDetail now runs on every successful match (was gated on Auto Create Tax Jurisdictions). It looks for the latest Tax Detail valid at the order date and either reuses it (rate matches), inserts a new bracket (no valid one exists), or leaves the existing bracket alone and emits telemetry 0000SHK on rate divergence -- no silent rate propagation Tests: - New ShpfyCTHITLTest codeunit covering marker propagation, the notification queue, MarkReviewed, Activity Log helper invocation, and the Capitalize confidence mapping - Seven new YAML scenarios closing documented gaps: G5 (tax-exempt guard), G6 (no order lines), JC5 (description = code), TD3 (rate-divergence preservation), TD4 (different tax group), TD7 (multiple jurisdictions), TD8 (effective date) - Library + verifier additions: taxExempt setup field, createdJurisdictionDescriptionEqualsCode check, optional effectiveDate assertion on taxDetailExists Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pattern: variable name = object name minus the Shpfy prefix. When the resulting name would collide with a BC standard type, prefix with Shopify (e.g. Tax Area vs Shpfy Tax Area -> ShopifyTaxArea). Renames applied across files added/modified by this work: - ShpfyCopilotTaxRegister -> CopilotTaxRegister - Matcher -> CopilotTaxMatcher - TestLib -> CTMTestLibrary - Verify -> CTMVerify - Events -> CopilotTaxEvents - Notify -> CopilotTaxNotify - ActivityLog -> CTActivityLog - TaxLine (Record "Shpfy Order Tax Line") -> OrderTaxLine - ShopifyOrderHeader -> OrderHeader - ShopifyOrderMgt -> OrderMgt - Notification / NotificationRow (Record) -> CopilotTaxNotification - ShpfyCopilotTaxMatchingEnabled -> CopilotTaxMatchingEnabled - ShpfyTaxArea (in MapTaxArea) -> ShopifyTaxArea (BC clash with Tax Area) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the matcher writes a Tax Detail for a matched jurisdiction × item tax group, also seed one for the Shop's Shipping Charges Account tax group at the same Shopify rate. Closes the divergence between BC and Shopify totals when Copilot creates a new Tax Jurisdiction and the shipping sales line otherwise lands with no Tax Detail row. Includes a small variable rename in ShpfyCopilotTaxEvents (OrderHeader -> ShopifyOrderHeader) and adds the S1-S6 shipping-tax scenario file to CTM-UNIT. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…res/445769-ShpfyTaxCopilot
…res/445769-ShpfyTaxCopilot
Per RAI feedback: make the human review step configurable per shop, default to blocking, and let merchants opt out to non-blocking as an explicit choice. - New shop setting Copilot Tax Match Review Required (Boolean, default true) on the Shpfy Shop card - New per-order Copilot Tax Match Reviewed flag, set by the new Approve Copilot Tax Match action on the Shopify Order page (promoted in the Process group right after Cancel Order) - New OnBeforeCreateSalesHeader subscriber blocks Sales Document creation when the shop requires review and the order has not yet been approved. In a GUI session it surfaces a clear error pointing the user at the Approve action and the shop toggle; background flows silently set Handled := true to avoid noise in the job-queue log. - Markers are reset together when the matcher reruns (e.g. user manually cleared Tax Area Code), restarting the review cycle. - HandleSalesHeaderCreated suppresses the Sales Order notification when the user already approved (blocking-mode flow) so it does not fire as a redundant prompt right after their explicit approval. - Architecture.md and RAI-Overview.md updated with the four-pillar HITL framing (audit / badge / configurable blocking / notification). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…res/445769-ShpfyTaxCopilot
onbuyuka
force-pushed
the
features/445769-ShpfyTaxCopilot
branch
from
May 14, 2026 23:25
a1c045b to
eaaf886
Compare
…res/445769-ShpfyTaxCopilot # Conflicts: # src/Apps/W1/Shopify/App/src/Order Refunds/Pages/ShpfyRefund.Page.al
…b.com/microsoft/BCApps into features/445769-ShpfyTaxCopilot
CI "Verify App Changes" flagged the test app's objects as out-of-range (test objects must live in 130000..149999). Renumber all 7 test codeunits from 30490-30497 to 134713-134719 (repo-wide unique) and update the test app idRange to 134713-134732, the CTM-UNIT.xml CodeunitID references, and the Architecture.md test-app range note. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 36d85827-a99c-43f3-b2dd-7bba1bcc83e1
…hop card deps Feedback from review: - Shop Card: dependent Copilot tax fields are now disabled until their prerequisite is set (Auto Create Jurisdictions/Areas + Review Required require Copilot Tax Matching Enabled; Tax Area Naming Pattern also requires Auto Create Tax Areas). - New Copilot Tax Match Review page (30471): per-order Card summarizing the resolved Tax Area (with AI confidence indicator), ship-to context, and the tax lines with matched Tax Jurisdiction Codes. Hosts the Review and Approve (blocking) / Review (non-blocking) action, both with the Copilot SparkleFilled icon. Approving sets Copilot Tax Match Reviewed and syncs the linked Sales Header notification row. - Shopify Order page: removed the embedded tax lines ListPart; added Review and Approve / Review entry actions (Copilot icon) that open the review page, plus an actionable order-page review notification. - Sales Order "Show Copilot Tax Decisions" now opens the review page. - ListPart gains SetTaxLineFilter so the review page scopes it to one order. - Docs (Architecture.md, RAI-Overview.md, TestMatrix.md) updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 36d85827-a99c-43f3-b2dd-7bba1bcc83e1
…ns, UX polish Review feedback follow-ups on top of the HITL rework: - Shop Card: dependent Copilot tax fields now bind Enabled directly to Rec (Auto Create Jurisdictions/Areas + Review Required require Copilot Tax Matching Enabled; Naming Pattern also requires Auto Create Tax Areas), dropping the helper/globals. - Copilot Tax Match Review page (30471): per-line "applies-to item" columns, a single canonical opener (RunReviewPage) that all four review entry points route through, single Approve action (Approve icon), and OnQueryClosePage that warns when closing with a pending approval. Removed the summary/totals group. Renamed the first group caption to "Overview". - Shpfy Order page action: situational caption (Review and Approve Copilot Tax Match while approval pending, else Review Copilot Tax Match). - BC Sales Order page action renamed to Review Copilot Tax Match; both page actions and the review-drill actions share the Copilot SparkleFilled icon. - Dropped the Shpfy Copilot Tax Notification table: the Sales Order review prompt is now derived live from the order's Copilot Tax Match Reviewed flag + per-user My Notifications (fixes the processor-vs-viewer keying bug). Removed QueueNotificationFor/SyncReviewedFromOrder; consolidated order resolution into FindOrderForReview. - Updated HITL unit tests and docs (Architecture, RAI-Overview, TestMatrix). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 36d85827-a99c-43f3-b2dd-7bba1bcc83e1
Cleanup pass: update user-facing text that still referred to the removed "Approve Copilot Tax Match" action, and remove a leftover empty folder. - ShpfyCopilotTaxEvents: the review-required blocking error now directs the user to Review Copilot Tax Match and approve on the review page. - ShpfyCTOrderHeader / ShpfyCopilotTaxShop: field tooltips now describe the review-page approval flow. - ShpfyCopilotTaxNotify: corrected SendOrderReviewNotification docstring (the once-per-session dedupe lives on the order page, not the procedure). - Removed the empty src/Tables folder left after dropping the notification table. No behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 36d85827-a99c-43f3-b2dd-7bba1bcc83e1
…table review flow Rate conflicts (a matched jurisdiction whose BC Tax Detail rate differs from Shopify's) now match the jurisdiction and build the Tax Area, but set a stored "Copilot Tax Rate Conflict" flag (the single source of truth) that holds the order for review in both blocking and non-blocking modes. - Review page: editable Tax Jurisdiction Code, BC Rate % column with green/red styling vs Shopify's rate, Overview-tab guidance on conflict, Approve (rebuilds Tax Area + rechecks/flips the conflict flag) shown only while held, Undo Approval (before the Sales Document exists), and revert of unapproved edits on close. - Gate + notifications + order-page action caption all derive from the stored flag. - Matcher: ReapplyFromAssignedLines, TryGetEffectiveItemRate; conflict no longer blocks matching. - Tests: new Shpfy CT Rate Conflict Test (recheck/flip, gate, undo); TD3 rewritten; extracted IsSalesDocumentCreationHeld + UndoApproval for testability. - Moved global var sections to the top per connector convention; filled telemetry tags; docs (Architecture/RAI/TestMatrix) updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 36d85827-a99c-43f3-b2dd-7bba1bcc83e1
…tes local - FixReportToJurisdictions: only adjust jurisdictions auto-created in this run, and only when their Report-to is blank, so existing admin-maintained hierarchies are never overwritten (was iterating every matched jurisdiction). - Review page Approve: error and keep the order held if no Tax Area can be resolved for the selected jurisdictions (was releasing against the stale pre-edit area). - Extract ShouldAttemptMatch guard + add unit tests (enabled / existing Tax Area idempotency / tax-exempt); replaces vacuous guard coverage. - Stop tracking RAI-Overview.md (kept local via .gitignore); docs updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 36d85827-a99c-43f3-b2dd-7bba1bcc83e1
…al without it) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 36d85827-a99c-43f3-b2dd-7bba1bcc83e1
…class Shopify shipping lines carry their own tax lines (stored on Shpfy Order Tax Line with Parent Id = "Shopify Shipping Line Id"). Match them like product-line tax lines instead of inferring the shipping tax from product lines. - Matcher: gather shipping-charge tax lines for the LLM; resolve the tax group per line (item vs shop's Shipping-Charges-Account group) via GetTaxGroupCodeForTaxLine; seed each line's Tax Detail from its own rate; a shipping rate conflict now holds the order like a product-line one. Removed the SeedShippingTaxDetail inference and the warning-only 0000UMS path. ReapplyFromAssignedLines/TryGetEffectiveItemRate cover shipping lines. - Review page/part: include shipping-charge tax lines in the scoped filter, snapshot, revert, unmatched and approve checks; show the shipping title for shipping lines. - Tests: unit tests for shipping bracket seeding + shipping rate conflict; test library builds shipping charges from YAML; reworked ShippingTax AITest scenarios (S1-S7) and extended verify to scan shipping tax lines. - Docs updated (Architecture flow/data-model/telemetry, TestMatrix shipping section). Depends on the connector persisting shipping-line tax lines (PR #9537). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 36d85827-a99c-43f3-b2dd-7bba1bcc83e1
… Applies-to
- FixReportToJurisdictions now sets Report-to on any matched jurisdiction whose
Report-to is blank (including the state, which reports to itself), instead of
only jurisdictions auto-created in the current run. Fixes pre-existing
jurisdictions (e.g. from earlier runs) being left with a blank Report-to.
- Order tax lines part shows shipping-charge Applies-to as "Shipping charge: {title}"
so shipping tax lines are distinguishable from product lines.
- Add deterministic (non-LLM) unit tests covering the Report-to rollup and
preservation of admin-maintained hierarchies.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 36d85827-a99c-43f3-b2dd-7bba1bcc83e1
- Architecture.md: FixReportToJurisdictions sets Report-to on any matched jurisdiction whose Report-to is blank (incl. pre-existing ones and the state itself), not only jurisdictions auto-created in the current run. - TestMatrix.md: add JC6 (Report-to on a pre-existing blank jurisdiction) and list the new Report-to unit tests under the Rate Conflict Test coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 36d85827-a99c-43f3-b2dd-7bba1bcc83e1
…ackfill - Review page tax lines part: add a "Use Shopify Rate" action that creates/updates a Tax Detail (effective the order's document date, at Shopify's rate) so BC posts what the customer paid. It warns that this changes shared BC tax setup beyond this order, is enabled only on a real rate conflict, and updates a same-date bracket in place. New matcher procedure SeedTaxDetailFromShopifyRate; telemetry 0000UNP. - Shop config defaults: add InitValue=true to "Auto Create Tax Areas" (was documented true but had no InitValue) and backfill the InitValue-backed defaults onto shops that already exist when the app is installed. Backfill lives in a new upgrade codeunit (Shpfy Copilot Tax Upgrade, 30478), guarded by upgrade tag MS-445769-CopilotTaxShopDefaults, and is invoked from both the install and upgrade triggers so it runs exactly once per company. The two opt-in booleans stay false. - Deterministic unit tests: Use Shopify Rate seeding (new + same-date), and the Shop defaults backfill (applied + opt-in fields untouched). Docs updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 36d85827-a99c-43f3-b2dd-7bba1bcc83e1
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Adds LLM-powered tax jurisdiction matching for Shopify orders, foundational tax field support across orders and refunds, and a human-in-the-loop (HITL) review layer — a configurable blocking approval gate (default on) plus audit trail, persistent badge, active notification, and a rate-conflict guard — so every Copilot decision is traceable, visibly marked, and gated for review without silently breaking auto-create-sales-doc or background sync flows. Product-line and shipping-charge tax lines are matched as first-class citizens.
What this PR does
When a Shopify order is imported, it carries free-text tax line descriptions (e.g. "NEW YORK STATE TAX at 4%") on both product lines and shipping charges. Business Central needs structured Tax Jurisdiction codes and a Tax Area to apply correct tax rules. Today this mapping is manual. This PR automates it using Azure OpenAI, adds the underlying tax infrastructure to the connector, and wraps the AI decisions in a human-in-the-loop review layer — holding back Sales Document creation until a user approves by default, and always holding when Business Central's own tax rate would differ from what the customer paid on Shopify — aligned with RAI feedback that automated tax matching should never run silently.
Areas of change
1. Standard connector — Tax fields and mapping (6 files)
ShpfyOrderHeader.Table.alGetOrderHeader.graphqltaxExemptto GraphQL queryShpfyImportOrder.Codeunit.altaxExemptfrom Shopify JSONShpfyOrderMapping.Codeunit.alMapTaxAreaprocedure (called in B2C + B2B paths),OnAfterMapShopifyOrderevent + fireShpfyProcessOrder.Codeunit.alShpfyOrder.Page.alTax Area mapping now happens during order mapping (upstream) rather than during sales document creation (downstream). This means Copilot and other extensions can modify the Tax Area via the
OnAfterMapShopifyOrderevent before the sales document is created.MapTaxArearespects Tax Exempt: if the Shopify order hastaxExempt = true(e.g. POS cashier disabled tax), the Tax Area Code is still set for reporting but Tax Liable stays false.2. Standard connector — Refund tax propagation (3 files)
ShpfyRefundHeader.Table.alShpfyCreateSalesDocRefund.Codeunit.alShpfyRefund.Page.al3. Standard connector — Integration points for Copilot (1 file)
ShpfyOrderEvents.Codeunit.alOnAfterMapShopifyOrderintegration eventThe Copilot subscriber also reuses the existing
OnAfterCreateSalesHeaderevent to propagate the HITL marker. TheTax Jurisdiction Codefield onShpfy Order Tax Lineis not in the connector — it lives in the Copilot app's TableExt since the connector itself never reads or writes it.4. Copilot Tax Matching app (
CopilotTaxMatching/app/)Separate app (ID range 30470-30499) that subscribes to
OnAfterMapShopifyOrder,OnBeforeCreateSalesHeader, andOnAfterCreateSalesHeader. Architecture:Key design decisions:
match_tax_jurisdictionstool, not free texthigh/mediummatches applied directly;low(new jurisdictions) only with admin opt-inShpfy Order Tax Linekeyed byShopify Shipping Line Id) are matched and seeded exactly like product-line tax lines; each line seeds its own Tax Detail bracket from its own rate and tax group (a shipping line uses the Shop's Shipping Charges Account tax group)Objects: 9 codeunits (Register, Matcher, Builder, Events, TaxMatchFunction, Install, Upgrade, Notify, Activity Log), 4 table extensions (Shop, Order Header, Sales Header, Order Tax Line), 4 page extensions (Shop Card, Sales Order, Order, Order Tax Lines), 1 Card page (Copilot Tax Match Review), 1 ListPart page (Tax Lines, embedded on the review page), 1 enum extension, 1 permission set. Config UI is 5 fields on the Shop Card page (dependent fields disabled until their prerequisite is set). The notifications are stateless — no dedicated table. New Shop config fields are backfilled onto pre-existing shops on install/upgrade via a shared, upgrade-tag-guarded routine (see Shop config defaults below).
5. Human-in-the-loop (configurable blocking, default on)
Five pillars deliver review of every Copilot decision, centered on a dedicated Copilot Tax Match Review page (a per-order Card summarizing the resolved Tax Area and each tax line's matched Tax Jurisdiction Code, with the platform AI confidence indicators):
Copilot Tax Match Review RequiredBoolean (defaulttrue) and a per-orderCopilot Tax Match ReviewedBoolean gate Sales Document creation.ShpfyCopilotTaxEventssubscribes toOnBeforeCreateSalesHeaderand setsHandled := truewhen the order was Copilot-matched, has not yet been approved, and (the shop requires review OR the order has aCopilot Tax Rate Conflict) — so the connector (auto-create, manual action, and background job) skips Sales Doc creation and the order stays a Shopify order pending review. The Shpfy Order page exposes a Review and Approve Copilot Tax Match action (captioned just Review Copilot Tax Match once approved or when not held) that opens the review page; clicking Approve there rebuilds the Tax Area from the current line jurisdictions, re-detects rate conflicts, flipsCopilot Tax Match Reviewed, and the next process run creates the document. Clearing the shop toggle opts into non-blocking mode (but a rate conflict still holds).Copilot Tax Rate ConflictBoolean (order header field 30478) is the single source of truth for a rate conflict: it is set at match time when a matched line's existing BC Tax Detail rate (for that line's tax group, valid as of the order date) differs from Shopify's, and refreshed whenever the match is re-applied on Approve. The gate, the notifications, the order-page action caption, and the review-page guidance + Approve visibility all read this one flag, so they can never disagree. A rate conflict holds the order in both blocking and non-blocking mode — Business Central's own rate is never auto-posted in place of what the customer paid on Shopify without a human accepting it. This applies uniformly to product-line and shipping-charge tax lines.OnQueryClosePagewarns on a pending approval. On a rate conflict the reviewer has a third option beyond accepting BC's rate or changing the jurisdiction: a Use Shopify Rate action on the tax lines part creates/updates a Tax Detail (effective the order's document date, at Shopify's rate) so BC posts what the customer paid. It confirms first that this changes shared BC tax setup (not just this order), is enabled only on a real rate difference, and updates a same-date bracket in place; after it runs the row turns green and Approve then clears the conflict. (The tax lines ListPart was moved here off the Shopify Order Card.)Activity Logentry of TypeAI(viaActivity Log Builder), carrying the LLM's confidence, reasoning, and a drill-back URL. On a rate conflict, the per-line entry explains the difference. The platform renders these as small AI confidence indicators next to the corresponding fields on the review page.Copilot Tax Match AppliedBoolean propagates from the Shpfy Order Header onto the BC Sales Header (read-only field + Review Copilot Tax Match action on the Sales Order page). Actionable, dismissible BC Notifications prompt review: on the Shopify Order page (both modes, once per order/session) and on the Sales Order page (non-blocking mode). Each opens the review page and can be suppressed per-user (MyNotifications.Disable) via its own GUID. The Sales Order prompt is derived live from the originating order'sCopilot Tax Match Reviewedflag +My Notifications(no per-user table).Default is blocking. The conservative default puts a human gate before any Copilot-matched Sales Document is created — honoring RAI feedback. High-throughput merchants can opt into non-blocking automation explicitly per shop, where the review page + audit trail + badge + notification keep the AI's decisions from ever being silent — and a rate conflict still forces review regardless of the toggle.
The Shop Card fields have dependencies: Auto Create Jurisdictions/Areas and Review Required are disabled until Copilot Tax Matching Enabled is on, and the Tax Area Naming Pattern is disabled until Auto Create Tax Areas is on.
6. Tax Detail seeding + rate-conflict behavior
On every successful match, each tax line seeds its own Tax Detail for
(jurisdiction × the line's tax group)— a product-line tax line uses the order line item's Tax Group Code; a shipping-charge tax line uses the Shop's Shipping Charges Account Tax Group Code. For each, it looks for the latest Tax Detail withEffective Date <= order datefor that jurisdiction + tax group + tax type:0000UMR, and flag the order with a rate conflict (Copilot Tax Rate Conflict). The jurisdiction is still matched and the Tax Area is still built, but the order is held for human review so a person accepts BC's rate or corrects the detail — auto-inserting on divergence would silently propagate Shopify's rate to every posting after that date, which is admin territory.Report-to rollup: when more than one jurisdiction is matched,
FixReportToJurisdictionssets Report-to to the state-level (first) jurisdiction on every matched jurisdiction whose Report-to is still blank — including pre-existing jurisdictions and the state itself (which reports to itself). A jurisdiction that already has a Report-to (an admin-maintained hierarchy) is left untouched.Shop config defaults (install/upgrade)
The Shop config fields carry their defaults as field
InitValues (Auto Create Tax Areas = true,Tax Area Naming Pattern = 'SHPFY-',Copilot Tax Match Review Required = true), which only apply to shops created after the app is installed. Because Shopify shops usually already exist when this app is added,Shpfy Copilot Tax Upgrade(30478) backfills those three fields onto existing shops from anInit()'d record. The backfill is guarded by an upgrade tag so it runs exactly once per company, and is invoked from both the install trigger (OnInstallAppPerCompany) and the upgrade trigger (OnUpgradePerCompany); the tag is intentionally not registered inOnGetPerCompanyUpgradeTags, so a fresh install still backfills pre-existing shops (mirroring howShpfy Installerguards its Cue/retention setup). The two opt-in booleans (Copilot Tax Matching Enabled,Auto Create Tax Jurisdictions) intentionally default tofalseand are left untouched. (Auto Create Tax Areaspreviously had noInitValuedespite being documented as defaulting to true — now fixed.)7. Copilot test app (
CopilotTaxMatching/test/)Two layers. AI Test Toolkit data-driven tests (YAML scenarios iterated by the framework, real LLM calls, no mocking):
Plain unit tests (standard test runner, no LLM, no toolkit):
Shpfy CT HITL Test(134716) — marker propagation,MarkReviewed,DisableForUser, Activity Log helpers,Capitalizeconfidence mappingShpfy CT Rate Conflict Test(134720) — rate-conflict recheck/flip on approve, the creation gate (IsSalesDocumentCreationHeld) in blocking and non-blocking mode, the business guards (ShouldAttemptMatch), Undo Approval, shipping bracket seeding + shipping rate conflict, and the Report-to rollup on re-apply (blank Report-to set, admin-set Report-to preserved)8. Standard connector tests (3 tests)
UnitTestCreateSalesDocumentTaxLiableUnitTestCreateSalesDocumentTaxExemptUnitTestCrMemoInheritsTaxAreaAndTaxLiable9. Documentation (in
CopilotTaxMatching/app/)Architecture.md— execution flow (incl. HITL writes, rate-conflict handling, shipping tax lines, Report-to rollup, Tax Detail seeding), object inventory, LLM config, data model, integration points, telemetryTestMatrix.md— full test scenario inventory + the Automated Test Coverage mapHow to review
Architecture.mdfor the big picture — the HITL + rate-conflict sections are the primary RAI surfaceShpfyOrderMapping.Codeunit.al(MapTaxArea + event),ShpfyProcessOrder.Codeunit.al(simplified tax handling),ShpfyCreateSalesDocRefund.Codeunit.al(bug fix)ShpfyCopilotTaxMatcher.Codeunit.al(LLM logic, per-line Tax Detail seeding, rate-conflict detection, Report-to rollup, shipping-charge handling),ShpfyCopilotTaxEvents.Codeunit.al(guards, gate, propagation),ShpfyTaxAreaBuilder.Codeunit.alShpfyCopilotTaxReview.Page.al(Approve/Undo, editable jurisdiction, green/red rates),ShpfyCopilotTaxNotify.Codeunit.al,ShpfyCTActivityLog.Codeunit.al, page extensions on Sales Order and Shpfy Order pagesShpfyCTMMatchTest.Codeunit.al(LLM-driven),ShpfyCTHITLTest.Codeunit.al+ShpfyCTRateConflictTest.Codeunit.al(non-LLM behavior),CTM-TS-*.yaml(data-driven scenarios)Test plan
0000UMRemitted0000UNPemittedAuto Create Tax Areas,Tax Area Naming Pattern, andCopilot Tax Match Review Requireddefaults; the two opt-in booleans stay offFixes AB#445769