Combine expense activity history and AI-assisted policy evaluation - #10292
Closed
Prangshuman Das (t-prda) wants to merge 64 commits into
Closed
Combine expense activity history and AI-assisted policy evaluation#10292Prangshuman Das (t-prda) wants to merge 64 commits into
Prangshuman Das (t-prda) wants to merge 64 commits into
Conversation
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
…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
…aluate The per-line policy status collapsed two very different situations into Cleared: a line that was evaluated against real policies and passed, and a line that simply has no applicable policy to run. The frontend renders the same badge for both and cannot tell them apart. Add a new Expense Policy Status value "No Policies" and derive it from whether any enabled, category-applicable policy exists for the line - independently of whether a check has run. The signal is stable: a line with no applicable policy reports No Policies before and after the orchestrator marks it evaluated, so it never masquerades as Cleared. Staleness is still evaluated first, so removing the last applicable policy after evaluation surfaces as Needs Recheck (unchanged), not a silent drop to No Policies. Update the affected status tests (the ones whose lines have no applicable policy now expect No Policies; those with a real policy stay Cleared) and add NoPoliciesIsDistinctFromClearedAcrossLifecycle covering the full No Policies -> Not Evaluated -> Stale -> Cleared transition. Note: the generated translation file gains a "No Policies" caption unit on next build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 075e44b0-4e81-40e9-9740-d92d9c533fbd
Re-check policy currency inside the release+submit transaction so a report
cannot enter approval with a stale or unevaluated policy state. Between the
client's snapshot read of the report's policy state and the actual
release+submit, a line can go stale (a policy or the line changed) or a newly
added policy can leave a line unevaluated; without this gate such a report
would be submitted without a current evaluation (currency spec race R2).
CheckPendingApprovalStatus now calls CheckPoliciesUpToDate, which - only when
AI policy evaluation ("Evaluate Policies") is enabled - rejects submit if any
line's GetPolicyStatus is Stale or Not Evaluated. The error carries a stable
(PolicyEvaluationNotCurrent) token so the submitting client can reliably
distinguish "re-evaluate and retry" from other failures. When the toggle is
off, submit proceeds unchanged.
Covers both submit paths (release+submit and submit-only) via the shared
CheckPendingApprovalStatus. Adds tests for the stale block, the not-evaluated
block, and the toggle-off passthrough.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 075e44b0-4e81-40e9-9740-d92d9c533fbd
Split the policy-currency submit error into a translatable message and a separate Locked token label, so the (PolicyEvaluationNotCurrent) marker the submitting client matches on is never translated - no translator comment needed. Behavior and the emitted text are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 075e44b0-4e81-40e9-9740-d92d9c533fbd
Resolve conflicts with main's Spend Request + Travelers feature: - app.json: merge idRanges, add new 7123-7126 block for renumbered pages - ExpenseReportLine: keep main's fields 100/101 (Spend Request), keep our policy fields 102-104, renumber "Has Policy Violation" 101 -> 105 - Renumber colliding pages: 7099->7123, 7101->7124, 7102->7125, 7103->7126 - Renumber test codeunit "Expense Policy Flag Test" 148338 -> 148339 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 075e44b0-4e81-40e9-9740-d92d9c533fbd
- Expense Policy.OnModify: read the committed pre-modify image by primary key
instead of xRec (unreliable in this runtime, per the documented workaround in
Expense Report Line.PolicyRelevantFieldChanged). Fixes
MovingPolicyToAnotherCategoryStalesOldCategoryLine: moving a policy out of a
line's category now correctly re-stales the old-category line (was dropping to
No Policies instead of Needs Recheck).
- SubmitBlockedWhenPolicyEvaluationStaleAndAiPolicyEnabled: capture the report No.
before the release call; the by-ref header record is left with a changed key on
the error path, so the post-asserterror Get must use the captured No.
- HasCurrentPolicyViolation: SetAutoCalcFields("Is Current") before FindSet
instead of per-row CalcFields (review: per-row FlowField anti-pattern).
- CheckPoliciesUpToDate: SetLoadFields on the submit line scan (review: partial
record for the policy-status check).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 075e44b0-4e81-40e9-9740-d92d9c533fbd
Test fix: - PerformManualReleaseAndPendingApproval now captures the report No. before Codeunit.Run and re-reads the header by that stored key. The by-ref record's key is no longer reliable after the release run, which caused SubmitBlockedWhenPolicyEvaluationStaleAndAiPolicyEnabled to fail with a "header does not exist" error (a G/L value) instead of surfacing the gate token. Review comments: - Privacy (AS0016): add field-level DataClassification to every new Normal field on Expense Policy Flag, Posted Exp. Policy Flag, Expense Policy (CustomerContent) and the Expense Agent Setup "Evaluate Policies" toggle (SystemMetadata). - Performance: batch the current-flag lookup in Exp. Policies To Eval Builder (BuildForLine/HasOutstandingPolicies) into a single keyed read instead of an N+1 Get per policy; add an Applicable key to Expense Policy for the Subject Type/Enabled/Category filter shape. - Accessibility: default the two policy-flag list pages to newest-first (descending Flagged At, new key) and replace the mismatched "Ambiguous" style with a computed Unfavorable style only on flagged rows. - Web services: mark Expense Policies API Editable = false to match its read-only contract. - Testing: PolicyFlagsCopiedToPostedExpenseReport now verifies the posting confirmation text via a dedicated handler and asserts the queue is drained. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 075e44b0-4e81-40e9-9740-d92d9c533fbd
Three CI failures on the policy-evaluation branch:
1. SubmitBlockedWhenPolicyEvaluationStaleAndAiPolicyEnabled — the test did
uncommitted setup then asserted the submit gate errors. asserterror rolls
back all uncommitted changes, deleting the report, so the post-asserterror
Get failed with GL00000209. Commit the stale precondition before the
asserterror so only the rejected submit rolls back (mirrors production and
the established pattern in this file).
2. Reverted the misdiagnosed capture-No.-before-Run change in
PerformManualReleaseAndPendingApproval — it was not the cause of failure 1.
3. CapabilitiesPolicyEvaluation{Enabled,Disabled}ViaAPI returned 401. Adding
these two web-service round-trips pushed the codeunit from 3 to 5 rapid
Basic-auth OData calls; the 4th/5th get throttled to 401. Convert both to
provider-level assertions (the codebase's dominant pattern, see
ExpenseProjectsAPITest) and fold the "policy row exposed via API" coverage
into the existing Consolidated round-trip, keeping the codeunit at 3
round-trips.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 075e44b0-4e81-40e9-9740-d92d9c533fbd
- Exp. Policy To Eval Buffer: declare field-level DataClassification on all Normal fields instead of relying on the table-level default (privacy). - Select the purpose-built keys explicitly with SetCurrentKey so the reads use them: InvalidateAffectedReportLines -> PolicyInvalidation; the two applicability scans (ExpPoliciesToEvalBuilder, ExpenseReportLine) -> Applicable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 075e44b0-4e81-40e9-9740-d92d9c533fbd
Build the submit-gate message into a Text variable before raising it, so the Error call receives a single value instead of an inline concatenation. Keeps the translatable message label and the locked (PolicyEvaluationNotCurrent) marker separate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 075e44b0-4e81-40e9-9740-d92d9c533fbd
…-policy-evaluation-251762
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Apply partial-record reads and mediate activity table access through the API and factbox pages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Clean prior marked fixtures and use a cleanup-safe posted source while retaining real API lifecycle actions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Remove retention registration from the foundation and track install, upgrade, refresh, permission, and audit semantics separately. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Select query-backed keys, lock wire-token errors, avoid UI handlers, and restore setup backups before disabled-isolation tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Grant direct read through Expense Mgmt. Read like the other factbox source tables. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Remove the API page from human Expense Management object permissions; the FactBox remains available there. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Parse the requested capability row instead of assuming every other capability is enabled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Use JsonObject text and boolean accessors for capability-state assertions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Reset reused JSON token and object variables before reading each capability row. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Derive submitted, resubmitted, and posted compliance from existing report rule violations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Use the existing report rule violation count instead of introducing a parallel compliance enum, and update the activity API and tests to expose that snapshot. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Successful submission, resubmission, and posting already guarantee that Business Central rule validation passed, so the connector can derive compliance without persisting another field. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Persist the report line count used by report summaries while retaining an explicitly named attached receipt count for audit and future scenarios. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Stop lowercasing the response before case-sensitive JSON lookup and safely ignore rows that do not expose the expected capability fields. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Follow API page conventions, keep user-facing validation translatable, narrow cleanup reads, and make read-only transport assertions less brittle. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Allow the activity management codeunit to resolve protected User records while keeping the read permission code-mediated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Include the field used by ModifyAll so localized builds do not introduce AA0242 JIT-load warnings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Register the new table for evaluation data and mark actor identifiers and names as Personal. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f498ec59-4b34-4965-85d0-ff489a7ef6e5
Preserve both capabilities, assign AI-assisted policy evaluation ordinal 5, and move Expense Policy Flags to unique page ID 7110 within the combined app range. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f498ec59-4b34-4965-85d0-ff489a7ef6e5
Prangshuman Das (t-prda)
temporarily deployed
to
triage
August 17, 2026 09:26 — with
GitHub Actions
Inactive
Move Expense Policy Flag Test to codeunit 148340 and Expense Policy Flags to globally free page 7107. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f498ec59-4b34-4965-85d0-ff489a7ef6e5
Contributor
Author
|
Closing this superseded draft integration PR; the upstream changes have since been handled through merged BCApps main and NAV uptake builds. |
Prangshuman Das (t-prda)
deleted the
copilot/combine-bcapps-prs-10255-10031-f498ec59
branch
August 18, 2026 11:20
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
Integration branch combining #10255 (expense activity history) and #10031 (AI-assisted policy evaluation) for conflict and compatibility review.
Issues
Conflict resolution
ActivityLogcapability ordinal 4 and assignedAiAssistedPolicyEvaluationordinal 5.7073–7126.Expense Activity Log FactBoxat page 7123 and movedExpense Policy Flagsto unique page 7107.app.jsonrange.Linked PRs
Standalone PR Add AI-assisted policy evaluation backend to Expense Agent #10031 NAV uptake: https://dynamicssmb2.visualstudio.com/Dynamics%20SMB/_git/NAV/pullrequest/252701
Combined NAV uptake: https://dynamicssmb2.visualstudio.com/Dynamics%20SMB/_git/NAV/pullrequest/252702
Expense Policy Flag Test uses free in-range codeunit 148340 instead of conflicting 148339.
Validation
app.json