Skip to content

Add AI-assisted policy evaluation backend to Expense Agent - #10031

Draft
encimita wants to merge 8 commits into
mainfrom
copilot/expense-agent-policy-evaluation-251762
Draft

Add AI-assisted policy evaluation backend to Expense Agent#10031
encimita wants to merge 8 commits into
mainfrom
copilot/expense-agent-policy-evaluation-251762

Conversation

@encimita

@encimita encimita commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Fixes AB#626515

Motivation

Expenses have hard rules in code and soft policies evaluated with AI. This moves policy-flag management to the backend so clients don't need bespoke APIs to configure policies, while still allowing them to read violations and detect whether the capability is supported.

This supersedes NAV PR 251762 after Expense Agent moved from NAV to BCApps.

Design

Subject: Expense Report Line - The report line is the sole policy subject. It is a creation-time snapshot of an expense, and its content and child details determine policy relevance.

Status and version model - GetPolicyStatus() returns Not Evaluated, Stale, Flagged, or Cleared. Policy Eval Version tracks the report line generation and is bumped by relevant line edits, child changes, and applicable policy changes. Evaluated Policy Version records the generation last evaluated by the agent. Version 0 is a normal version, not a sentinel.

Lines with no applicable policies are treated as Cleared without requiring an evaluation pass.

Expense Policy Flag - Flags retain evaluation history using (Subject System Id, Policy System Id, Subject Version, Policy Version). The server stamps Subject Version from the report line, and Policy Version snapshots the policy version. Is Current indicates whether the policy has changed since the flag was written. Duplicate evaluations for the same versions fail by design.

Expense Policy - Policies use (Subject Type, Line No.) as the primary key, with category as a secondary key and a bumpable version field. Adding, changing, or deleting an applicable policy invalidates evaluated report lines. A blank category applies to all categories.

Invalidation - Relevant report-line edits, participant/itemization/per-diem changes, and applicable policy changes bump the subject version. Relevant-field detection reads the committed pre-modify image from the database rather than relying on xRec.

Submitting a report is non-invalidating because release and approval state changes only affect the header.

API changes

  • Add markPoliciesEvaluated to Expense Report Lines API.
  • Add the read/insert expensePolicyFlags subpage scoped to the current subject version.
  • Add read-only policyStatus and policiesEvaluatedAt.
  • Add the nested read-only policiesToEvaluate endpoint for policies still requiring evaluation at the current subject and policy versions.
  • Add submit-with-comment overloads to Expense Reports API.
  • Add a read-only Posted Expense Policy Flags API nested under Posted Expense Report Lines.
  • Add AiAssistedPolicyEvaluation to the Expense Capability enum.

Posting and setup

Posting copies current-version flags to the immutable Posted Expense Policy Flag table for audit. Deleting an open report line removes its flags.

The setup wizard includes an Evaluate Policies option gated by AI credits and opens Expense Policies modally.

Testing

The migrated changes include regression coverage for policy status, line and child invalidation, policy add/change/delete invalidation, evaluation history, duplicate evaluation protection, submit safety, posting, and the policiesToEvaluate endpoint.

The original NAV implementation compiled and its focused policy-flag suite passed on a live NST. The migrated BCApps branch has not yet been compiled or run through the BCApps pipeline.

encimita and others added 2 commits August 6, 2026 14:12
Port the net ExpenseAgent changes from NAV PR 251762 into the migrated BCApps application. This adds policy status and versioning, policy flags and evaluation APIs, posted audit flags, capability and setup support, invalidation, and regression coverage.

The source tip is 7faae6b94d8781e601692053b8af327a2e07181d, based on e8970c249048b9595dc4db1fc580628df3e70a8d. The authoritative post-migration BCApps pin is 32c3034.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3873b235-f26b-4236-ad16-adc33538f26e
Apply the missing follow-up delta from NAV PR 251762 so never-evaluated expense lines with no applicable policy report Cleared, with regression coverage and isolated policy test data.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3873b235-f26b-4236-ad16-adc33538f26e
@github-actions github-actions Bot added AL: Apps (W1) Add-on apps for W1 Integration GitHub request for Integration area labels Aug 6, 2026

@encimita encimita left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Review completed against backend, testing, and code-diet lenses. Four blocking defects and four suggestions are attached inline.

Comment thread src/Apps/W1/ExpenseAgent/app/src/MasterData/Tables/ExpensePolicy.Table.al Outdated
Comment thread src/Apps/W1/ExpenseAgent/app/src/MasterData/Tables/ExpensePolicy.Table.al Outdated
Comment thread src/Apps/W1/ExpenseAgent/app/src/APIs/ExpensePolicyFlagsAPI.Page.al
Comment thread src/Apps/W1/ExpenseAgent/app/src/Expense/Pages/ExpensePolicyFlags.Page.al Outdated
@github-actions github-actions Bot added this to the Version 29.0 milestone Aug 7, 2026
encimita and others added 6 commits August 7, 2026 13:39
…seded flags, builder filter)

Fixes four reviewer comments on the policy-evaluation model:

- #1 Invalidate both old and new scope on policy change. OnModify now
  re-checks the previous (xRec) category/subject-type scope as well as the
  new one, so moving, retargeting, or disabling a policy stales the lines it
  used to cover instead of leaving them incorrectly Current.
- #2 Add a covering PolicyInvalidation key (Expense Category, Policies
  Evaluated At) so the invalidation scan is seekable. Policies change rarely,
  so the per-line write cost is accepted rather than reworking the model.
- #3 Only count current violations. GetPolicyStatus / the API now use
  HasCurrentPolicyViolation, which ignores non-compliant flags whose policy
  version is superseded (Is Current = false); such flags stay as history but
  no longer keep a line Flagged. hasPolicyViolation on the API mirrors this.
- #6 Push the category-or-blank applicability rule into the policies-to-
  evaluate query filter instead of re-checking it in the loop.

Adds tests: MovingPolicyToAnotherCategoryStalesOldCategoryLine (#1) and
SupersededFlagDoesNotKeepLineFlagged (#3).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 075e44b0-4e81-40e9-9740-d92d9c533fbd
Addresses PR #10031 review comments #4 and #5.

#4 - Guard MarkPoliciesEvaluated (API bound action): refuse to mark a
line evaluated while an applicable policy still lacks a verdict for the
current version. Adds Exp. Policies To Eval Builder.HasOutstandingPolicies
(sharing SetApplicablePolicyFilter with BuildForLine, no duplication) and
an error on the Expense Report Lines API action. The table method stays
unguarded so table-mechanics tests are unaffected.

#5 - Make policy flags a validated, immutable evaluation record. The flag
table OnInsert now rejects unknown report lines, unknown/disabled policies,
and policies that do not apply to the line category, and snapshots subject
and policy version from live records. The Expense Policy Flags API is now
create + read-only (ModifyAllowed = false, DeleteAllowed = false).

Tests: OutstandingPolicyIsDetectedUntilFlagged (#4); rewrote
SupersededFlagDoesNotKeepLineFlagged to force the non-current state via a
raw insert; added FlagInsertRejects{UnknownReportLine,DisabledPolicy,
InapplicablePolicy} (#5).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 075e44b0-4e81-40e9-9740-d92d9c533fbd
… + posted)

Addresses PR #10031 review comment #7: pages 7099/7101 were an orphaned UI
surface with no host. Instead of removing them, wire them up as the in-product
policy drilldown, and mirror the same surface for posted documents.

Open documents (Expense Report line subpage):
- The existing Policy Status field is now shown only when the
  AiAssistedPolicyEvaluation capability is enabled for the org.
- Drilling into the field opens the evaluated policies for that line.
- Page 7099 "Expense Policy Flags" is promoted from ListPart to a filtered
  List (Evaluated Policies) with Compliant/category/reason/policy-text columns
  and a card drilldown; 7101 stays the per-flag detail card.

Posted documents (Posted Expense Report line subpage):
- Adds Posted Expense Report Line.GetPolicyStatus(), deriving the status from
  the immutable posted flags (non-compliant -> Flagged, any -> Cleared, none ->
  Not Evaluated). Currency is intentionally not re-checked so the audit trail is
  stable when live policies change later.
- Adds a capability-gated Policy Status field with the same drilldown.
- New pages 7109 "Posted Exp. Policy Flags" (List) and 7110 "Posted Exp. Policy
  Flag Card", mirroring the open-document pages over "Posted Exp. Policy Flag".
- Registers the two new pages in the objects permission set.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 075e44b0-4e81-40e9-9740-d92d9c533fbd
Follow-up review notes on the flag tables (open + posted).

- Remove the "Policy Line No." field from "Expense Policy Flag" and
  "Posted Exp. Policy Flag" and from their API pages and detail cards. It was
  a non-stored FlowField lookup that duplicated Policy System Id (identity) and
  Policy Text (display), so it carried no information the flag didn't already
  have. The policies-to-evaluate buffer keeps its own Policy Line No. - that is
  a different table and out of scope here.
- Rename the flag's "Description" field to "Reason" on both tables, the two API
  pages (property description -> reason), and the list/card pages. The field
  always held the reason the policy was flagged; the name now says so.

Frontend contract note: the expensePolicyFlags / postedExpensePolicyFlags OData
entities lose policyLineNo and rename description -> reason. To be coordinated
with the frontend.

Left unchanged (by design): "Subject Type" stays in the Expense Policy primary
key. The "Expense Policy Subject" enum is Extensible and Line No. is sequenced
per subject type, so keeping it in the key preserves that and avoids a breaking
key change for no functional gain; flags reference policies by SystemId, not the
key.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 075e44b0-4e81-40e9-9740-d92d9c533fbd
Make Subject Type the leading component of the primary key on both
Expense Policy Flag and Posted Exp. Policy Flag. Subject identity is
conceptually (Subject Type, Subject System Id); keying on the pair is a
defensive guard against cross-table SystemId reuse and prepares the
tables for additional values of the extensible Expense Policy Subject
enum. Update all primary-key-based Get calls (duplicate-evaluation
check, FlagExists idempotency probe, and the deletion-history test) to
pass Subject Type in the new leading position.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 075e44b0-4e81-40e9-9740-d92d9c533fbd
Page id 7110 should not be consumed. Page ids 7106 and 7107 were free
(only used by a table and a codeunit respectively; AL object ids are
per-type), so move the posted flag card to 7106. Only the object
declaration referenced the number; the list page CardPageId and the
permission set reference it by name, so no other change is needed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 075e44b0-4e81-40e9-9740-d92d9c533fbd
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.

1 participant