Skip to content

Combine expense activity history and AI-assisted policy evaluation - #10292

Closed
Prangshuman Das (t-prda) wants to merge 64 commits into
mainfrom
copilot/combine-bcapps-prs-10255-10031-f498ec59
Closed

Combine expense activity history and AI-assisted policy evaluation#10292
Prangshuman Das (t-prda) wants to merge 64 commits into
mainfrom
copilot/combine-bcapps-prs-10255-10031-f498ec59

Conversation

@t-prda

@t-prda Prangshuman Das (t-prda) commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Integration branch combining #10255 (expense activity history) and #10031 (AI-assisted policy evaluation) for conflict and compatibility review.

Issues

Conflict resolution

  • Kept ActivityLog capability ordinal 4 and assigned AiAssistedPolicyEvaluation ordinal 5.
  • Retained both capability-provider branches and both test suites.
  • Combined the Expense Agent app range as 7073–7126.
  • Kept Expense Activity Log FactBox at page 7123 and moved Expense Policy Flags to unique page 7107.
  • Verified page 7107 occurs exactly once and is covered by the merged app.json range.

Linked PRs

Validation

  • No merge conflict markers
  • No duplicate same-type object IDs in the Expense Agent app
  • Page 7107 occurs once and is covered by app.json
  • AL build and tests (not run; manual validation requested)

encimita and others added 30 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
…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
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
Prangshuman Das (t-prda) and others added 21 commits August 14, 2026 12:04
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
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
@t-prda

Copy link
Copy Markdown
Contributor Author

Closing this superseded draft integration PR; the upstream changes have since been handled through merged BCApps main and NAV uptake builds.

@t-prda
Prangshuman Das (t-prda) deleted the copilot/combine-bcapps-prs-10255-10031-f498ec59 branch August 18, 2026 11:20
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.

Integration review: activity history and AI-assisted policy evaluation

2 participants