Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
3f61e55
Add expense activity history foundation
t-prda Aug 13, 2026
5a92237
Fix expense activity review issues
t-prda Aug 13, 2026
2becc02
Resolve activity page object IDs
t-prda Aug 13, 2026
259268e
Resolve localized activity codeunit IDs
t-prda Aug 13, 2026
29f330a
Harden activity history access
t-prda Aug 13, 2026
3da9c13
Resolve activity API test object ID
t-prda Aug 13, 2026
5bbe88d
Document activity API preload
t-prda Aug 13, 2026
ad5c4db
Use committed reads for activity API
t-prda Aug 13, 2026
928440a
Index retention and parse capability JSON
t-prda Aug 13, 2026
0104770
Make persistent activity scenarios rerunnable
t-prda Aug 13, 2026
58ae2aa
Add activity and posted-report query keys
t-prda Aug 13, 2026
a36355e
Rename activity event key
t-prda Aug 13, 2026
6284d35
Use default activity API read isolation
t-prda Aug 13, 2026
775467b
Stabilize activity tests under restricted permissions
t-prda Aug 14, 2026
311cc8b
Avoid expense policy page ID clashes
t-prda Aug 14, 2026
9d17c0e
Preserve report-specific reopen authorization
t-prda Aug 14, 2026
f5291a1
Align activity API and table patterns
t-prda Aug 14, 2026
31f533f
Consolidate activity API E2E coverage
t-prda Aug 14, 2026
71778e1
Entitle activity retention subscriber
t-prda Aug 14, 2026
59de18f
Align retention subscriber permissions
t-prda Aug 14, 2026
df5ed7c
Test public reopen authorization path
t-prda Aug 14, 2026
d34aae0
Merge latest BCApps main
t-prda Aug 14, 2026
f6530cd
Restore permissions before expense test cleanup
t-prda Aug 14, 2026
5bcd231
Grant retention users activity subscriber access
t-prda Aug 14, 2026
ef68098
Defer activity retention policy
t-prda Aug 14, 2026
766f9f2
Align activity API reads and E2E fixtures
t-prda Aug 14, 2026
a4b631f
Align activity factbox permissions
t-prda Aug 14, 2026
cdc74f6
Keep activity API agent-only
t-prda Aug 14, 2026
9d2ee7e
Fix reopen test variable scope
t-prda Aug 14, 2026
b93fe54
Assert individual expense capability states
t-prda Aug 14, 2026
f9fd11f
Read capability JSON values directly
t-prda Aug 14, 2026
08bbc11
Clear capability JSON loop state
t-prda Aug 14, 2026
e001f2c
Snapshot report compliance in activity history
t-prda Aug 14, 2026
7d21351
Verify activity compliance API field
t-prda Aug 14, 2026
9c060a2
Snapshot report rule violations in activity history
t-prda Aug 14, 2026
e536a0b
Remove redundant activity compliance data
t-prda Aug 14, 2026
c31f31e
Snapshot expense and attached receipt counts
t-prda Aug 14, 2026
f906af6
Preserve capability API JSON property names
t-prda Aug 14, 2026
3d37d18
Address activity API review feedback
t-prda Aug 14, 2026
25d0e34
Grant activity logger access to BC users
t-prda Aug 14, 2026
c91d245
Load expense report number during cleanup
t-prda Aug 14, 2026
71d5453
Classify expense activity log sensitivity
t-prda Aug 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Apps/W1/ExpenseAgent/app/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@
},
{
"from": 7073,
"to": 7103
"to": 7123
}
],
"features": [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------
namespace Microsoft.ExpenseAgent;

page 7122 "Expense Activity Log API"
{
APIGroup = 'expense';
APIPublisher = 'microsoft';
APIVersion = 'beta';
EntityCaption = 'Expense Activity Log Entry';
EntitySetCaption = 'Expense Activity Log Entries';
EntityName = 'expenseActivityLogEntry';
EntitySetName = 'expenseActivityLogEntries';
PageType = API;
Comment thread
t-prda marked this conversation as resolved.
DelayedInsert = true;
SourceTable = "Expense Activity Log Entry";
ODataKeyFields = SystemId;
DataAccessIntent = ReadOnly;
Editable = false;
InsertAllowed = false;
ModifyAllowed = false;
DeleteAllowed = false;
Extensible = false;
AboutText = 'Provides activity history when scoped through an expense report, posted expense report, or expense user. Direct unscoped access is not allowed. Expense user history requires the historyActorRole filter.';

layout
{
area(Content)
{
repeater(General)
{
field(id; Rec.SystemId)
{
Caption = 'ID';
}
field(entryNumber; Rec."Entry No.")
{
Caption = 'Entry Number';
}
field(sourceTableId; Rec."Source Table ID")
{
Caption = 'Source Table ID';
}
field(sourceId; Rec."Source Record System ID")
{
Caption = 'Source ID';
}
field(subjectTableId; Rec."Subject Table ID")
{
Caption = 'Subject Table ID';
}
field(subjectId; Rec."Subject System ID")
{
Caption = 'Subject ID';
}
field(documentNumber; Rec."Document No.")
{
Caption = 'Document Number';
}
field(documentDescription; Rec."Document Description")
{
Caption = 'Document Description';
}
field(eventType; Rec."Event Type")
{
Caption = 'Event Type';
}
field(occurredAt; Rec."Occurred At")
{
Caption = 'Occurred At';
}
field(initiatedBy; Rec."Initiated By")
{
Caption = 'Initiated By';
}
field(actorRole; Rec."Actor Role")
{
Caption = 'Actor Role';
}
field(actorTableId; Rec."Actor Table ID")
{
Caption = 'Actor Table ID';
}
field(actorId; Rec."Actor Record System ID")
{
Caption = 'Actor ID';
}
field(actorDisplayName; Rec."Actor Display Name")
{
Caption = 'Actor Display Name';
}
field(comment; Rec.Comment)
{
Caption = 'Comment';
}
field(amountLCY; Rec."Amount (LCY)")
{
Caption = 'Amount (LCY)';
}
field(currencyLCY; CurrencyLCY)
{
Caption = 'Currency (LCY)';
}
field(nonRefundableAmountLCY; Rec."Non-Refundable Amount (LCY)")
{
Caption = 'Non-Refundable Amount (LCY)';
}
field(reimbursableAmount; Rec."Reimbursable Amount")
{
Caption = 'Reimbursable Amount';
}
field(reimbursableAmountLCY; Rec."Reimbursable Amount (LCY)")
{
Caption = 'Reimbursable Amount (LCY)';
}
field(refundableAmount; Rec."Refundable Amount")
{
Caption = 'Refundable Amount';
}
field(refundableAmountLCY; Rec."Refundable Amount (LCY)")
{
Caption = 'Refundable Amount (LCY)';
}
field(reimbursementCurrencyCode; ReimbursementCurrencyCode)
{
Caption = 'Reimbursement Currency Code';
}
field(reimbursementCurrencyFactor; Rec."Reimbursement Currency Factor")
{
Caption = 'Reimbursement Currency Factor';
}
field(categories; Rec.Categories)
{
Caption = 'Categories';
}
field(attachedReceiptCount; Rec."Attached Receipt Count")
{
Caption = 'Attached Receipt Count';
}
field(expenseCount; Rec."Expense Count")
{
Caption = 'Expense Count';
}
field(historyActorRole; Rec."History Actor Role Filter")
{
Caption = 'History Actor Role';
}
}
}
}

var
CurrencyHelper: Codeunit "Expense API Currency Helper";
CurrencyLCY: Code[10];
HistoryScopeApplied: Boolean;
ReimbursementCurrencyCode: Code[10];
HistoryActorRoleRequiredErr: Label 'The historyActorRole filter must be specified as Submitter or Approver.';
ActivityScopeRequiredErr: Label 'Activity log entries must be requested through an expense report, posted expense report, or expense user.';

trigger OnInit()
Comment thread
t-prda marked this conversation as resolved.
var
ExpenseAgentAPIValidation: Codeunit "Expense Agent API Validation";
begin
ExpenseAgentAPIValidation.VerifyAgentAccess();
end;

trigger OnOpenPage()
Comment thread
t-prda marked this conversation as resolved.
begin
// Avoid JIT load consistency errors by including fields read in OnAfterGetRecord in the initial record buffer.
Rec.AddLoadFields("Reimbursement Currency Code");
end;
Comment thread
t-prda marked this conversation as resolved.
Comment thread
t-prda marked this conversation as resolved.

trigger OnAfterGetRecord()
begin
Clear(CurrencyLCY);
Clear(ReimbursementCurrencyCode);
if Rec."Event Type" in [Rec."Event Type"::Submitted, Rec."Event Type"::Resubmitted, Rec."Event Type"::Posted] then begin
CurrencyLCY := CurrencyHelper.GetCurrencyCodeForAPI('');
ReimbursementCurrencyCode := CurrencyHelper.GetCurrencyCodeForAPI(Rec."Reimbursement Currency Code");
end;
end;

trigger OnFindRecord(Which: Text): Boolean
begin
ApplyHistoryScope();
exit(Rec.Find(Which));
end;

local procedure ApplyHistoryScope()
var
OriginalFilterGroup: Integer;
HasHistoryActorFilters: Boolean;
HasSourceFilters: Boolean;
HistoryActorRoleFilter: Text;
begin
if HistoryScopeApplied then
exit;

OriginalFilterGroup := Rec.FilterGroup();
HistoryActorRoleFilter := Rec.GetFilter("History Actor Role Filter");
Rec.FilterGroup(4);
HasSourceFilters :=
(Rec.GetFilter("Source Table ID") <> '') and
(Rec.GetFilter("Source Record System ID") <> '');
HasHistoryActorFilters :=
(Rec.GetFilter("History Actor Table ID Filter") <> '') and
(Rec.GetFilter("History Actor System ID Filter") <> '');
if HistoryActorRoleFilter = '' then
HistoryActorRoleFilter := Rec.GetFilter("History Actor Role Filter");
Rec.FilterGroup(0);
if HasHistoryActorFilters then begin
Comment thread
t-prda marked this conversation as resolved.
if HistoryActorRoleFilter <> '' then begin
Rec.SetCurrentKey("Occurred At", "Entry No.");
Rec.Ascending(false);
Rec.SetRange("History Subject Match", true)
end else
Error(HistoryActorRoleRequiredErr);
end else
if HasSourceFilters then begin
Rec.SetCurrentKey("Source Table ID", "Source Record System ID", "Occurred At", "Entry No.");
Rec.Ascending(false);
end else
Error(ActivityScopeRequiredErr);
Rec.FilterGroup(OriginalFilterGroup);
HistoryScopeApplied := true;
end;

}
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,14 @@ page 6928 "Expense Reports API"
EntitySetName = 'expenseReportLines';
SubPageLink = "Document No." = field("No.");
}
part(activityLogEntries; "Expense Activity Log API")
{
Caption = 'Activity Log Entries';
EntityName = 'expenseActivityLogEntry';
EntitySetName = 'expenseActivityLogEntries';
SubPageLink = "Source Table ID" = const(Database::"Expense Report Header"),
"Source Record System ID" = field(SystemId);
}
}
}
}
Expand Down Expand Up @@ -459,4 +467,5 @@ page 6928 "Expense Reports API"
ActionContext.AddEntityKey(Rec.FieldNo(SystemId), Rec.SystemId);
ActionContext.SetResultCode(WebServiceActionResultCode::Updated);
end;

}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ page 6918 "Expense Users API"
SubPageLink = "Expense User No." = field("No.");
}

part(activityHistory; "Expense Activity Log API")
Comment thread
t-prda marked this conversation as resolved.
{
EntityName = 'expenseActivityLogEntry';
EntitySetName = 'expenseActivityLogEntries';
SubPageLink = "History Actor Table ID Filter" = const(Database::"Expense User"),
"History Actor System ID Filter" = field(SystemId);
}

part(approverView; "Approver View API")
{
EntityName = 'approverView';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,14 @@ page 6957 "Posted Expense Reports API"
EntitySetName = 'postedExpenseReportLines';
SubPageLink = "Document No." = field("No.");
}
part(activityLogEntries; "Expense Activity Log API")
{
Caption = 'Activity Log Entries';
EntityName = 'expenseActivityLogEntry';
EntitySetName = 'expenseActivityLogEntries';
SubPageLink = "Source Table ID" = const(Database::"Posted Expense Report Header"),
"Source Record System ID" = field(SystemId);
}
}
}
}
Expand Down
Loading
Loading