Skip to content

[Shopify] Copilot Tax Matching + Tax Area/Tax Liable/Tax Exempt support#7453

Draft
onbuyuka wants to merge 33 commits into
mainfrom
features/445769-ShpfyTaxCopilot
Draft

[Shopify] Copilot Tax Matching + Tax Area/Tax Liable/Tax Exempt support#7453
onbuyuka wants to merge 33 commits into
mainfrom
features/445769-ShpfyTaxCopilot

Conversation

@onbuyuka

@onbuyuka onbuyuka commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

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)

File Change
ShpfyOrderHeader.Table.al New fields: Tax Area Code (1070), Tax Liable (1080), Tax Exempt (1090)
GetOrderHeader.graphql Add taxExempt to GraphQL query
ShpfyImportOrder.Codeunit.al Parse taxExempt from Shopify JSON
ShpfyOrderMapping.Codeunit.al New MapTaxArea procedure (called in B2C + B2B paths), OnAfterMapShopifyOrder event + fire
ShpfyProcessOrder.Codeunit.al Read Tax Area Code + Tax Liable from order header (instead of calling FindTaxArea at doc creation time)
ShpfyOrder.Page.al Display Tax Area Code, Tax Liable, Tax Exempt

Tax 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 OnAfterMapShopifyOrder event before the sales document is created.

MapTaxArea respects Tax Exempt: if the Shopify order has taxExempt = 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)

File Change
ShpfyRefundHeader.Table.al FlowFields 110-112: Tax Area Code, Tax Liable, Tax Exempt (from parent order)
ShpfyCreateSalesDocRefund.Codeunit.al Set Tax Liable alongside Tax Area Code on credit memos (bug fix — was only setting Tax Area Code)
ShpfyRefund.Page.al Tax group (3 fields) + Tax Lines navigation action (reuses existing Order Tax Lines page)

3. Standard connector — Integration points for Copilot (1 file)

File Change
ShpfyOrderEvents.Codeunit.al New OnAfterMapShopifyOrder integration event

The Copilot subscriber also reuses the existing OnAfterCreateSalesHeader event to propagate the HITL marker. The Tax Jurisdiction Code field on Shpfy Order Tax Line is 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, and OnAfterCreateSalesHeader. Architecture:

OnAfterMapShopifyOrder event
  -> Events (30473): guards (enabled? capability active? not exempt? no tax area yet?)
  -> Matcher (30471): gather tax lines (product-line AND shipping-charge) + jurisdictions
                      + address -> call GPT-4.1 -> parse matches
  -> ApplyMatches: write jurisdiction codes to tax lines, create jurisdictions if allowed,
                   seed a Tax Detail bracket per line from its own rate + tax group, and
                   flag a rate conflict when BC's existing rate differs from Shopify's
  -> FixReportToJurisdictions: point every matched jurisdiction with a blank Report-to at
                   the state-level jurisdiction (admin-maintained hierarchies left untouched)
  -> Builder (30472): find or create Tax Area from matched jurisdictions -> set on order header
  -> Activity Log (30477): per-line + per-area Activity Log entries (Type=AI)
  -> Set "Copilot Tax Match Applied" + "Copilot Tax Rate Conflict" on order header

OnBeforeCreateSalesHeader event  [blocking gate]
  -> Events (30473): if Copilot-matched + not yet approved + (shop requires review
                     OR order has a rate conflict) -> Handled := true (skip Sales Doc
                     creation; order waits until the user approves on the review page)

OnAfterCreateSalesHeader event
  -> Events (30473): propagate Copilot Tax Match Applied marker to Sales Header (badge only)

Review surfaces (all open the Copilot Tax Match Review page, 30471)
  -> Shpfy Order page (30479): Review and Approve / Review Copilot Tax Match action
                               + actionable "Review" notification fired on open
  -> Sales Order page (30476): Review Copilot Tax Match action + one notification
                               (non-blocking mode), derived live from the order's
                               Reviewed flag + My Notifications (no table)

Key design decisions:

  • Forced function calling — LLM must return structured JSON via match_tax_jurisdictions tool, not free text
  • Temperature 0 — deterministic results for identical inputs
  • Confidence levelshigh/medium matches applied directly; low (new jurisdictions) only with admin opt-in
  • First-class shipping — shipping-charge tax lines (stored on Shpfy Order Tax Line keyed by Shopify 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)
  • No PII sent — only tax line titles, jurisdiction codes/descriptions, and city/state/country

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):

  • Configurable blocking review (default on) — a per-shop Copilot Tax Match Review Required Boolean (default true) and a per-order Copilot Tax Match Reviewed Boolean gate Sales Document creation. ShpfyCopilotTaxEvents subscribes to OnBeforeCreateSalesHeader and sets Handled := true when the order was Copilot-matched, has not yet been approved, and (the shop requires review OR the order has a Copilot 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, flips Copilot 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).
  • Rate-conflict guard — the Copilot Tax Rate Conflict Boolean (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.
  • Review page — the single canonical review-and-adjust surface. Shows the resolved Tax Area (with AI confidence indicator), the ship-to context, and the tax lines ListPart. Each line shows the item (or shipping charge) it taxes, Shopify's rate, the Business Central Tax Detail rate that would apply as of the order date, and the matched Tax Jurisdiction Code (with confidence indicators). The Tax Jurisdiction Code is editable so a reviewer can correct or complete a match, and the row is highlighted green when the two rates agree and red when they differ. Approve (shown only while the order is held and not yet reviewed) rebuilds the Tax Area, re-seeds missing Tax Detail brackets, re-detects conflicts, and is blocked while any line is still unmatched; it errors (without releasing the order) if no Tax Area can be resolved. An Undo Approval action reverses an approval while the order is still held and no Sales Document exists yet. Jurisdiction edits take effect only on Approve and are reverted if the page is closed without approving; OnQueryClosePage warns 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.)
  • Audit trail — every matched tax line and the resulting Tax Area each get a System Application Activity Log entry of Type AI (via Activity 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.
  • Persistent badge + active notifications — a Copilot Tax Match Applied Boolean 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's Copilot Tax Match Reviewed flag + 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 with Effective Date <= order date for that jurisdiction + tax group + tax type:

  • No valid bracket exists → insert one at the order date with Shopify's rate (pure seeding).
  • Bracket exists, rate matches Shopify → exit silently. Already correct.
  • Bracket exists, rate differs from Shopify → leave the existing (admin-maintained) rate untouched, emit telemetry 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, FixReportToJurisdictions sets 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 an Init()'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 in OnGetPerCompanyUpgradeTags, so a fresh install still backfills pre-existing shops (mirroring how Shpfy Installer guards its Cue/retention setup). The two opt-in booleans (Copilot Tax Matching Enabled, Auto Create Tax Jurisdictions) intentionally default to false and are left untouched. (Auto Create Tax Areas previously had no InitValue despite 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):

  • J1-J8: Jurisdiction matching (exact, partial, auto-create, medium confidence, duplicates)
  • H1-H12: Hard/adversarial matching (similar codes across states, truncated titles, 15+ distractors, Canadian HST/GST/PST, French tax abbreviations, geographic disambiguation)
  • JC1-JC6: Jurisdiction creation details (country/region, report-to hierarchy incl. pre-existing blank Report-to, description = code)
  • TD1-TD8: Tax detail creation including effective date and rate-divergence preservation
  • RD1-RD9: Rate divergence / conflict (item + multi-line conflicts, held in blocking and non-blocking mode, resolve-then-approve, edit discarded on close, Undo Approval)
  • S1-S7: Shipping-charge tax lines (bracket seeded at the shipping line's own rate, new-jurisdiction, untaxed shipping, empty/shared tax group, multiple charges, shipping rate conflict holds the order)
  • TA1-TA11: Tax area find/create (exact match, superset/subset, collision, naming patterns)
  • G1-G8: Guard / early-exit scenarios (Copilot disabled, tax area pre-set, tax exempt, no order lines, result=false)
  • F1-F6: End-to-end flows

Plain unit tests (standard test runner, no LLM, no toolkit):

  • Shpfy CT HITL Test (134716) — marker propagation, MarkReviewed, DisableForUser, Activity Log helpers, Capitalize confidence mapping
  • Shpfy 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)

Test Codeunit What it verifies
UnitTestCreateSalesDocumentTaxLiable 139608 Tax Liable flows from Shpfy Tax Area → MapTaxArea → sales document
UnitTestCreateSalesDocumentTaxExempt 139608 Tax Exempt order gets Tax Area Code but Tax Liable stays false
UnitTestCrMemoInheritsTaxAreaAndTaxLiable 139611 Credit memo inherits Tax Area Code + Tax Liable from parent order

9. 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, telemetry
  • TestMatrix.md — full test scenario inventory + the Automated Test Coverage map

How to review

  1. Start with Architecture.md for the big picture — the HITL + rate-conflict sections are the primary RAI surface
  2. Connector changes: ShpfyOrderMapping.Codeunit.al (MapTaxArea + event), ShpfyProcessOrder.Codeunit.al (simplified tax handling), ShpfyCreateSalesDocRefund.Codeunit.al (bug fix)
  3. Copilot core: 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.al
  4. HITL layer: ShpfyCopilotTaxReview.Page.al (Approve/Undo, editable jurisdiction, green/red rates), ShpfyCopilotTaxNotify.Codeunit.al, ShpfyCTActivityLog.Codeunit.al, page extensions on Sales Order and Shpfy Order pages
  5. Tests: ShpfyCTMMatchTest.Codeunit.al (LLM-driven), ShpfyCTHITLTest.Codeunit.al + ShpfyCTRateConflictTest.Codeunit.al (non-LLM behavior), CTM-TS-*.yaml (data-driven scenarios)

Test plan

  • Compile Shopify connector app
  • Compile Copilot Tax Matching app + test app
  • Run connector unit tests (codeunit 139608, 139611)
  • Run Copilot unit tests (codeunit 134716 HITL, 134720 rate conflict) — non-LLM behavioral coverage
  • Run Copilot AI Test Suite (CTM-UNIT) — YAML-driven scenarios with real LLM calls, incl. shipping (S*) and rate divergence (RD*)
  • Manual: import a Shopify order with product + shipping tax lines → verify Tax Area Code + Tax Liable set, per-line Tax Detail seeded from each line's own rate/tax group, Activity Log entries appear, marker set on Shpfy Order Header
  • Manual: create Sales Document from the order → verify marker propagates, "Review Copilot Tax Match" action visible, one-time notification fires on first open
  • Manual: rate-conflict order (existing Tax Detail at a different rate) → order is held in both blocking and non-blocking mode, the divergent line shows red on the review page, Approve rebuilds + releases, Undo Approval re-holds; telemetry 0000UMR emitted
  • Manual: on a rate-conflict line, click Use Shopify Rate → confirm warning shown, Tax Detail created/updated at the document date with Shopify's rate, row turns green, Approve clears the conflict; telemetry 0000UNP emitted
  • Manual: install/upgrade into a company that already has shops → existing shops get Auto Create Tax Areas, Tax Area Naming Pattern, and Copilot Tax Match Review Required defaults; the two opt-in booleans stay off
  • Manual: tax-exempt POS order → Copilot skipped, no marker, no Activity Log entries
  • Manual: process refund → credit memo gets Tax Area Code + Tax Liable from parent order

Fixes AB#445769

onbuyuka and others added 11 commits March 30, 2026 17:54
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>
@github-actions github-actions Bot added the AL: Apps (W1) Add-on apps for W1 label Mar 30, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

onbuyuka and others added 8 commits April 27, 2026 10:14
…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>
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>
@onbuyuka
onbuyuka force-pushed the features/445769-ShpfyTaxCopilot branch from a1c045b to eaaf886 Compare May 14, 2026 23:25
onbuyuka and others added 4 commits July 6, 2026 09:06
…res/445769-ShpfyTaxCopilot

# Conflicts:
#	src/Apps/W1/Shopify/App/src/Order Refunds/Pages/ShpfyRefund.Page.al
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
@github-actions github-actions Bot added this to the Version 29.0 milestone Jul 13, 2026
onbuyuka and others added 3 commits July 16, 2026 15:21
…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
@JesperSchulz JesperSchulz added the Integration GitHub request for Integration area label Jul 16, 2026
onbuyuka and others added 7 commits July 17, 2026 16:02
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AL: Apps (W1) Add-on apps for W1 Integration GitHub request for Integration area

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants