Skip to content

Add expense activity history foundation - #10255

Open
Prangshuman Das (t-prda) wants to merge 42 commits into
mainfrom
feature/expense-activity-log-foundation
Open

Add expense activity history foundation#10255
Prangshuman Das (t-prda) wants to merge 42 commits into
mainfrom
feature/expense-activity-log-foundation

Conversation

@t-prda

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

Copy link
Copy Markdown
Contributor

Summary

  • add a single append-only expense activity table whose source reference moves from the active report to the posted report while its logical subject remains stable
  • record Created, Submitted, Resubmitted, Recalled, Approved, Rejected, Reopened by Approver, and Posted lifecycle events with actor, role, comments, financial snapshots, categories, and receipt count
  • expose read-only report-scoped and Expense User role-scoped APIs, plus active/posted/manager factboxes and the ActivityLog capability
  • add source deletion cleanup, posting reassignment, unit/API/posting tests, and a cleanup-safe activity API E2E scenario

Validation

  • the AL tests passed in a manually published local environment before rebasing
  • active report, posted report, submitter history, and approver history APIs were exercised against local scenarios
  • unscoped activity and user history without historyActorRole were verified to fail closed
  • API integration tests are listed in the disabled-test manifest because the current CI environment does not support their required API isolation

Fixes AB#644172

Related: AB#639704

Replaces #10237 to reset stale review and CI history after the design was finalized.

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
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Give the test cleanup explicit activity permissions and use a deterministic approver overload for reopen coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Reserve globally free activity page IDs alongside BCApps PR 10031.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Keep public configured/default approver checks separate from explicit internal actor validation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Use standard agent API permissions, defer unused policy status, simplify factbox fields, and retain only query-backed indexes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Move cleanup-safe lifecycle coverage into the API test codeunit and remove the persistent scenario test file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Allow retention-policy refresh to execute independently of Expense Management role assignment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Grant execute through the agent object permission set instead of inherent permissions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Remove the test-only overload and isolate the current-user approver mapping in the unit test.

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
Keep transactional cleanup under full test permissions instead of elevating the shared library.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Register activity retention on install and extend Retention Pol. View so refresh cannot fail on missing execute permission.

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
@t-prda
Prangshuman Das (t-prda) requested a review from a team August 14, 2026 12:03
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
@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Performance}$

The code filters the Expense record by SystemId and then calls FindFirst(). This is a direct single-record lookup, so GetBySystemId() is the cheaper and clearer access pattern than a filtered FindFirst().

Suggested fix (apply manually — could not be anchored as a one-click suggestion):

        if Expense.GetBySystemId(ExpenseId) then begin

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.33.4

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Performance}$

The code filters Posted Expense Report Header on its full primary key field "No." and then calls FindFirst(). Get() expresses the direct primary-key seek without a filtered-search shape.

Suggested fix (apply manually — could not be anchored as a one-click suggestion):

            if PostedExpenseReportHeader.Get(Rec."Last Posting No.") then

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.33.4

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Performance}$

The code filters Posted Expense Report Header on its full primary key field "No." and then calls FindFirst(). Get() expresses the direct primary-key seek without a filtered-search shape.

Suggested fix (apply manually — could not be anchored as a one-click suggestion):

            if PostedExpenseReportHeader.Get(Rec."Last Posting No.") then

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.33.4

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Style}$

The changed file is named ExpenseManagementObjects.permissionset.al, but the style convention requires the type segment to use the AL object-type name PermissionSet in <ObjectName>.<ObjectType>.al. The lowercase permissionset suffix breaks the standard file-to-object mapping that grep, symbol search, and other tooling rely on.

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.33.4

Comment thread src/Apps/W1/ExpenseAgent/app/src/APIs/ExpenseUsersAPI.Page.al
@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟠\ High\ Severity\ —\ Performance}$

ProcessExpenseReportLines iterates every expense-report line, and that per-line path calls CreateSalesDocument, which then does SalesHeader.FindFirst() and SalesLine.FindLast() to locate or extend an open invoice. Posting a report with many billable lines therefore re-scans sales tables once per line, creating the N+1 lookup shape this rule warns about. Cache the target invoice and next line number by (customer, currency, posting date) or batch/group the billable lines before entering the posting loop.

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.33.4

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
@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Style}$

This permission-set file is still named ExpenseManagementObjects.permissionset.al, but the style rule requires the canonical object-type suffix PermissionSet in ..al. Keeping the lowercase suffix breaks the filesystem-to-object-type mapping that grep, explorers, and other tooling rely on; rename the file to ExpenseManagementObjects.PermissionSet.al while touching this object.

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.33.4

@t-prda

Copy link
Copy Markdown
Contributor Author

Review follow-up for the unanchored findings:

  • The disabled-test observation (#issuecomment-5293183036), capability-interface observation (#issuecomment-5293504058), and caller-supplied approval identity observation (#issuecomment-5293504560) duplicate inline threads that now contain the detailed rationale. The API tests require isolation-disabled execution; the small capability dispatcher is intentional; and approval authorization is a pre-existing concern that should be handled as a separate security change.
  • The Expense Ledger Entry permission (#issuecomment-5293590572), reimbursement-notification ErrorInfo (#issuecomment-5293600219), active/posted header classification (#issuecomment-5293803760 and #issuecomment-5293803956), existing Get/FindFirst patterns (#issuecomment-5293842749, #issuecomment-5293842945, and #issuecomment-5293843155), permission-set filename casing (#issuecomment-5293843744 and #issuecomment-5294432209), and sales-posting N+1 behavior (#issuecomment-5293895097) are pre-existing code unrelated to the activity-history changes in this PR. They are not being changed here to avoid unrelated scope expansion.

The actionable activity-history findings are being handled separately and remain open until the local fixes are reviewed and pushed.

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
@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟠\ High\ Severity\ —\ Interfaces}$

"Expense Capabilities Provider" selects capability-specific behaviour with a case over the "Expense Capability" enum. This means every new capability requires editing the central dispatcher again, which is the enum-branching anti-pattern the interfaces guidance calls out. Model the enum as one that implements an interface, move each capability's enablement logic into implementation codeunits, and dispatch through an interface variable instead of extending this case block.

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.33.4

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Style}$

The changed file path src/Apps/W1/ExpenseAgent/app/src/Permissions/ExpenseManagementObjects.permissionset.al does not follow the <ObjectName>.<ObjectType>.al convention from the style guidance: the object-type segment should use the canonical PermissionSet casing. Rename it to ExpenseManagementObjects.PermissionSet.al so tooling and file listings can identify the AL object type consistently.

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.33.4

@t-prda

Copy link
Copy Markdown
Contributor Author

Agentic PR Review - Round 1

Recommendation: Request Changes

What this PR does

This adds an append-oriented expense activity timeline, lifecycle snapshots, active-to-posted source reassignment, scoped APIs, FactBoxes, capabilities, permissions, and tests. The storage model, event ordering, deletion flow, and transaction placement are coherent, but the current implementation does not preserve agent provenance, enforce committed history reads, or keep table access behind the scoped surfaces.

Suggestions

S1 - Preserve agent creation provenance
The API-created report branch is always marked as User. Record Agent for that branch while keeping the Expense User as the actor.

S2 - Expose only committed activity history
Posting writes Posted before later posting steps can fail, but this API uses the default read isolation. A concurrent request can return an entry that is later rolled back; read history with ReadCommitted.

S3 - Record agent lifecycle provenance correctly
The internal Submit, Approve, and Reject paths serve Expense Agent API actions, but both logging helpers hard-code User. Pass the initiator from each caller and keep the supplied Expense User as the actor.

S4 - Keep activity reads code-mediated
Both end-user permission paths grant direct read access to the raw activity table. Use indirect read permissions and elevate only the scoped API and FactBox pages.

Risk assessment and necessity

Risk: This is audit and approval history. The current code can persist false initiator data, expose rolled-back or partly reassigned timelines to concurrent API reads, and let assigned roles bypass report and actor-role scopes through direct table access. The schema is additive and the lifecycle tests are broad, but these gaps affect the trust boundary of the new history.

Necessity: The feature is coherent and needed for expense traceability and approver history. The one-table model and source reassignment are appropriate, but the provenance, committed-read, and mediated-permission blockers must be fixed before the history can be trusted.


[AI-PR-REVIEW] version=1 promptVersion=1 system=github pr=10255 round=1 by=t-prda at=2026-08-14T15:36:06Z lastSha=3d37d18afd4e73b9b58448d1399e35db2a6a91d9 reviewKey=ccfefcb1a82ae0bd8ede40f7f0753173d0c35f9993e7528738536b95b0c3a872 suggestions=S1@96dea907,S2@287c7e2e,S3@36433d89,S4@a38182c7

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
MonicaPAhuja
MonicaPAhuja previously approved these changes Aug 14, 2026
@t-prda

Copy link
Copy Markdown
Contributor Author

Response to Agentic PR Review - Round 1:

  • S1/S3 (initiator provenance): keeping User is intentional for the current flows. The internal API actions represent explicit human submit/approve/send-back actions from the Expense Agent web experience; the Expense User is both the actor and the initiator. Agent is reserved for genuinely autonomous agent decisions/actions. For API-created reports we are starting with the initiating Expense User and can revisit autonomous creation provenance when that behavior is introduced.
  • S2 (ReadCommitted): intentionally not changing. History is refreshable and the activity table is append-only except at bounded lifecycle operations. We prefer non-blocking reads over lock waits/timeouts during posting; a transient in-flight view can be corrected by refresh.
  • S4 (direct table reads): intentional permission design. Human read permission is required for the FactBox without page-level elevation, while the agent data/object split follows the existing API permission model. The activity API independently rejects unscoped requests and accepts identity only from navigation filters.

The codeunit now has code-mediated tabledata User = r permission for resolving protected BC User actor names. No changes are planned for S1-S4 in this PR unless the product semantics or permission model are changed explicitly.

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
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.

2 participants