Skip to content

Expense Agent: Add Corporate Card interfaces and import flow - #10057

Open
Alexander Yakunin (Alexander-Ya) wants to merge 21 commits into
mainfrom
migrate/ado-pr-251529
Open

Expense Agent: Add Corporate Card interfaces and import flow#10057
Alexander Yakunin (Alexander-Ya) wants to merge 21 commits into
mainfrom
migrate/ado-pr-251529

Conversation

@Alexander-Ya

@Alexander-Ya Alexander Yakunin (Alexander-Ya) commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

What & why

Problem
Expense Agent did not have a complete corporate card import flow for provider setup, staging, matching, and sample-driven validation. Additional cleanup was needed for setup isolation, sample payload handling, object naming consistency, and test coverage.

Solution
This PR introduces the corporate card feature set for Expense Agent, including:

Corporate card provider, staging, mapping, validation, and import orchestration
Data Exchange based parsing flow and provider registration
Matching, post-import processing, and reporting helpers
Corporate card setup integration and role center/page integrations
Sample payloads for CSV/XML including Level 3 scenarios
New interfaces and renamed setup codeunits for API-driven flow
Permission sets and related app metadata updates
Tests
Added/updated tests for:

Corporate card setup
Basic import flow
Level 3 VAT scenarios
Phase 3 behavior
Supporting VAT test updates

Migration Notes
Source Azure PR: 251529
Commit history replayed from source branch (non-merge commits)
One additional normalization commit was added to adapt legacy path layout from App/Internal/Apps/ExpenseAgent/app to current BCApps layout under src/Apps/W1/ExpenseAgent/app.

Validation
Cherry-pick conflicts resolved across:
app metadata id ranges
setup codeunit/content merges
file rename and file-location conflicts due to path/layout differences
Branch is clean and pushed to origin

Linked work

Fixes AB#630366

How I validated this

  • I read the full diff and it contains only changes I intended.
  • I built the affected app(s) locally with no new analyzer warnings.
  • I ran the change in Business Central and confirmed it behaves as expected.
  • I added or updated tests for the new behavior, or explained below why none are needed.

What I tested and the outcome (required — be specific: scenarios, commands, screenshots for UI changes)

Risk & compatibility

@github-actions github-actions Bot added AL: Apps (W1) Add-on apps for W1 Integration GitHub request for Integration area labels Aug 7, 2026
@github-actions github-actions Bot added this to the Version 29.0 milestone Aug 7, 2026
ShowCaption = true;
Enabled = false;

field(TotalBatches; GetTotalBatches())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

EACorpCardStatisticsFactbox computes 11 statistics inline through Count/CalcSums-based helper calls on the render path. Because this is a FactBox on the dashboard, the page must wait for all of those read-only calculations before the UI is fully painted, which matches the synchronous statistics pattern this guidance says to move to page background tasks.

Knowledge:

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


AuditSubscribers.LogImportStarted(CorpCardProvider.Code, CorpCardBatch."Batch No.");

CorpCardProviderImpl.Download(CorpCardBatch);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

EACorpCardImportOrch.RunProvider calls CorpCardProviderImpl.Download, ParseToStaging, and Ack back-to-back with no TryFunction/error boundary around them. If any of those provider calls raises an error (network failure, malformed source, HTTP error), the codeunit call unwinds without ever reaching the code that sets CorpCardBatch.Status/"Ended DT" or calls LogImportCompleted/LogImportFailed. The batch row is left permanently in Status::Started with no "Ended DT", so operators lose visibility into the failure and any 'last batch in progress' checks elsewhere would see a stuck batch indefinitely. This is a cross-cutting orchestration/error-handling/reliability gap that spans the import pipeline, not a single leaf's concern; wrap the provider calls so a failure still finalizes the batch record (Status::Failed, "Ended DT", and an audit/telemetry call) before the error propagates, or before it is swallowed.

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

ToolTip = 'Setup employee posting groups.';
}
}
group("Corporate Card")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

$\textbf{🔴\ Critical\ Severity\ —\ AppSource}$

The PR exposes new Corporate Card setup and usage paths from the Expense Management role center and Expense User page, but no assignable app role covers the required EACorpCard* tabledata permissions. "Expense Mgmt. Read/Edit/Admin" still grant only the existing Expense* data, while the new EACorpCardRead/Edit/Admin sets are all non-assignable and are not included by any public role. A non-SUPER user can therefore reach the new Corporate Card UI and then fail on normal setup/usage, which violates the AppSource requirement to cover setup and usage without SUPER.

Knowledge:

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

TableRelation = Currency.Code;
ToolTip = 'Specifies the currency code for the card.';
}
field(7; Blocked; Boolean)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

$\textbf{🟠\ High\ Severity\ —\ Data\ Modeling}$

The new EACorpCard.Blocked field is inert because the consuming corporate-card flows never enforce it at the point of use. EACorpCardMatchMgt, EACorpCardEnhancedMatchMgt, and EACorpCardExpWriter all read the card with Get("Card Id") and continue without EACorpCard.TestField(Blocked, false), so a blocked card can still match expenses and auto-create drafts.

Knowledge:

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

TableRelation = "VAT Business Posting Group".Code;
ToolTip = 'Specifies the VAT Business Posting Group to which the MCC is mapped.';
}
field(4; Blocked; Boolean)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

$\textbf{🟠\ High\ Severity\ —\ Data\ Modeling}$

The new EACorpCardMCCMap.Blocked field is not enforced where the mapping is consumed. EACorpCardMCCMgt reads MCC rows and returns the mapped category without MCCMap.TestField(Blocked, false), and EACorpCardExpWriter uses that result during draft creation, so blocked MCC mappings can still drive automatic categorization.

Knowledge:

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

exit(DelChr(ValueTxt, '<>', ' '));
end;

[IntegrationEvent(false, false)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Two new publishers use ad-hoc names that do not encode where they fire: "OnAfterCreateDataExch" is raised at the end of Download, and "OnProvideSourceContent" is raised from InjectSourceContent without a before/after host-routine naming pattern. Rename these publishers to position-based names so subscribers can infer the firing point from the event contract.

Knowledge:

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

ExpenseAgentSetup: Record "Expense Agent Setup";
MerchantNorm: Codeunit EACorpCardMerchantNorm;
EnhancedMatchMgt: Codeunit EACorpCardEnhancedMatchMgt;
ExpWriter: Codeunit EACorpCardExpWriter;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

EACorpCardPostImportOrch hard-wires EACorpCardExpWriter as a concrete codeunit collaborator even though this PR introduces EACorpCardExpWriterInterface. That removes the injection seam for post-import orchestration, so tests cannot substitute a lightweight writer implementation and the orchestrator is coupled to one concrete writer. Depend on Interface EACorpCardExpWriterInterface and inject the implementation through a parameter or setter instead of declaring Codeunit EACorpCardExpWriter directly.

Knowledge:

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

var
CorpCardDataExchProv: Codeunit EACorpCardDataExchProv;
begin
case CorpCardProvider."Feed Type" of

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Provider dispatch still happens through a case over the persisted Feed Type enum even though this PR introduces EACorpCardProviderInterface. Adding a new provider variant will still require editing this resolver instead of letting the enum map each value to its implementation and dispatch through the interface automatically. Model EACorpCardFeedType as an enum that implements EACorpCardProviderInterface and assign the enum value to the interface variable rather than branching here.

Knowledge:

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

exit('');

DataExch."File Content".CreateInStream(InStr);
while not InStr.EOS do begin

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

ReadCardIdFromSourcePayload and ReadTagValueByOccurrenceFromSourcePayload build the XML payload with repeated XmlTxt += XmlLineTxt inside a stream loop. On import-sized payloads this reallocates the growing Text on every iteration; use TextBuilder and convert once after the stream has been read.

Knowledge:

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

if CorpCardTrans."Provider Trans Id" = '' then
CorpCardTrans."Provider Trans Id" := CopyStr(GetDataExchFieldValueByColumnName(DataExch."Entry No.", CorpCardProvider."Data Exch Def Code", CorpCardProvider."Data Exch Map Code", CurrentLineNo, 'ProviderTransId'), 1, MaxStrLen(CorpCardTrans."Provider Trans Id"));

if CorpCardTrans."Provider Trans Id" = '' then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

BackfillMandatoryMappedFields falls back to ReadProviderTransIdFromSourcePayload, ReadCardIdFromSourcePayload, and ReadTagValueByOccurrenceFromSourcePayload inside the per-transaction path, and each helper rereads DataExch."File Content" from the start. Cache the payload or pre-index the fallback tags once per batch so missing-field fallbacks do not rescan the entire source for every row.

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

Comment on lines +113 to +117
Expense.SetRange("Expense User No.", CorpCard."Expense User No.");
Expense.SetRange("Status", Expense."Status"::Open);

if not Expense.FindSet() then
exit(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

FuzzyMerchantMatch loops over the wide Expense table but only reads "Merchant Name" and "No." from each candidate row. Without SetLoadFields, every matching import loads the full Expense record for each comparison.

Suggested change
Expense.SetRange("Expense User No.", CorpCard."Expense User No.");
Expense.SetRange("Status", Expense."Status"::Open);
if not Expense.FindSet() then
exit(false);
Expense.SetRange("Expense User No.", CorpCard."Expense User No.");
Expense.SetRange("Status", Expense."Status"::Open);
Expense.SetLoadFields("No.", "Merchant Name");
if not Expense.FindSet() then
exit(false);

Knowledge:

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

ReportNo := ExpenseReportHeader."No.";

// Add all matching expenses to the report
if Expense.FindSet() then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

CreateReportFromCorpCardExpenses filters the target Expense set up front, then iterates it only to assign the same report number and call Modify(). That is an equivalent bulk assignment and should be issued as one ModifyAll on the filtered set.

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

        Expense.ModifyAll("Expense Report No.", ReportNo);

Knowledge:

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


internal procedure LogImportStarted(ProviderCode: Code[20]; BatchNo: Integer)
begin
Session.LogMessage('0000UCS', StrSubstNo(ImportStartedMsg, ProviderCode, BatchNo), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategory());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

The new Session.LogMessage calls in EACorpCardAuditSubscribers build free-text telemetry with StrSubstNo from report numbers, employee/user IDs, rejection reasons, and import error text. Those values are sent verbatim in the telemetry message string even though the calls pass DataClassification::SystemMetadata, so the new corporate-card telemetry leaks identifying or customer content into publisher telemetry.

Knowledge:

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

RequestHeaders: HttpHeaders;
begin
RequestMessage.Method := 'GET';
RequestMessage.SetRequestUri(CorpCardProvider."API Endpoint");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

The API import sends EACorpCardProvider."API Endpoint" directly to HttpRequestMessage.SetRequestUri() with no Uri.AreURIsHaveSameHost() or Uri.IsValidURIPattern() check. Because the endpoint is persisted in a table field, anyone who can alter that row can retarget the server-side request to an internal or attacker-controlled host and turn the import into an SSRF primitive.

Knowledge:

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

JQMgt: Codeunit EACorpCardJQMgt;
begin
JQMgt.ScheduleProviderImport(Rec.Code, 1440, 080000T, Today());
Message('Provider %1 scheduled for daily import.', Rec.Code);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

The new provider actions call Message/Confirm with hard-coded user-facing format strings at lines 257, 264, and 270 instead of object-scope Labels. That bypasses the translation pipeline for these UI texts; move each literal into a Label and keep passing the runtime parameters separately.

Knowledge:

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


internal procedure LogImportFailed(ProviderCode: Code[20]; BatchNo: Integer; ErrorMsg: Text)
begin
Session.LogMessage('0000UCU', StrSubstNo(ImportFailedMsg, ProviderCode, BatchNo, CopyStr(ErrorMsg, 1, 250)), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategory());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

LogImportFailed emits an import-failure event with TelemetryScope::ExtensionPublisher, but a provider import failure is a customer-actionable operational signal. Keeping it publisher-only hides the failure from environment telemetry that tenant operators use for diagnostics and alerting; use TelemetryScope::All here.

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

        Session.LogMessage('0000UCU', StrSubstNo(ImportFailedMsg, ProviderCode, BatchNo, CopyStr(ErrorMsg, 1, 250)), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::All, 'Category', TelemetryCategory());

Knowledge:

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

Initialize();
SetProviderSourcePayload(CorpCardXmlProviderCodeTok, GetMalformedXmlPayload(), MalformedXmlFileNameTok);

asserterror CorpCardFeedMgt.RunImport(CorpCardXmlProviderCodeTok);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

$\textbf{🔴\ Critical\ Severity\ —\ Testing}$

This negative test uses asserterror but only checks that some error text was produced. Any unrelated failure in the import path would still make the test pass, so the test can go green without proving that malformed XML fails for the expected reason.

Knowledge:

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

CorpCardProvider."Auth Type"::ApiKey:
Headers.Add('x-api-key', SecretValue);
CorpCardProvider."Auth Type"::OAuth2:
Headers.Add('Authorization', SecretStrSubstNo('Bearer %1', SecretValue));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

$\textbf{🟡\ Medium\ Severity\ —\ Web\ Services}$

OAuth2-configured providers will send a literal masked string as the Authorization header value, because the SecretText format template used to build the header contains no substitution placeholder for the resolved secret and never actually inserts the token. This makes every OAuth2 API import fail authentication. The format template needs a placeholder so the secret value is inserted before the request is sent.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds an end-to-end Corporate Card capability to the Expense Agent app (provider setup + import orchestration + staging/validation + matching + UI/role center integration), along with sample payloads and new/updated integration tests to validate the flow (including Level 3 VAT scenarios).

Changes:

  • Introduces Corporate Card domain objects (tables/enums/interfaces/codeunits) and UI pages for providers, batches, transactions, exceptions, and dashboards.
  • Extends Expense Agent setup + permissions + privacy classification handling to include Corporate Card configuration and entities.
  • Adds sample CSV/XML payloads and new integration tests covering setup idempotency, basic import, Level 3 VAT behavior, and Phase 3/negative cases.

Reviewed changes

Copilot reviewed 79 out of 79 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/Apps/W1/ExpenseAgent/test/src/LibraryExpense.Codeunit.al Adds reusable helper to seed VAT posting group in setup for tests.
src/Apps/W1/ExpenseAgent/test/src/Helper/EACorpCardTestLib.Codeunit.al New test library for corp card setup/import helpers and assertions.
src/Apps/W1/ExpenseAgent/test/src/ExpensePostingVATTest.Codeunit.al Refactors VAT posting group setup to shared helper.
src/Apps/W1/ExpenseAgent/test/src/EACorpCardSetupTests.Codeunit.al New tests validating default corp card setup idempotency + seeded payload.
src/Apps/W1/ExpenseAgent/test/src/EACorpCardPhase3Tests.Codeunit.al New tests for malformed payload handling and additional provider formats.
src/Apps/W1/ExpenseAgent/test/src/EACorpCardL3VATTests.Codeunit.al New tests for Level 3 VAT detail import and draft creation behavior.
src/Apps/W1/ExpenseAgent/test/src/EACorpCardImportBasicTests.Codeunit.al New tests for basic CSV import and duplicate rerun behavior.
src/Apps/W1/ExpenseAgent/app/src/Setup/Tables/ExpenseAgentSetup.Table.al Adds corp card setup fields (create mode, tolerances, default provider).
src/Apps/W1/ExpenseAgent/app/src/Setup/Pages/ExpenseAgentSetup.Page.al Exposes corp card setup fields + actions to apply defaults/demo.
src/Apps/W1/ExpenseAgent/app/src/Setup/Codeunits/ExpenseAgentPrivacySubs.Codeunit.al Adds corp card tables to data classification eval normalization list.
src/Apps/W1/ExpenseAgent/app/src/Setup/Codeunits/EACreateCorpCardL3Demo.Codeunit.al Seeds Level 3 demo provider, data exchange mapping, and sample payload.
src/Apps/W1/ExpenseAgent/app/src/Setup/Codeunits/CreateExpenseVATRates.Codeunit.al Makes VAT seeding more tolerant of missing customer-owned categories.
src/Apps/W1/ExpenseAgent/app/src/Setup/Codeunits/CreateExpenseAgentSetup.Codeunit.al Updates setup seeding (payment method labels) and permissions.
src/Apps/W1/ExpenseAgent/app/src/Permissions/ExpenseManagementObjects.permissionset.al Adds corp card objects/pages/codeunits to the expense objects permission set.
src/Apps/W1/ExpenseAgent/app/src/MasterData/Pages/ExpenseUser.Page.al Adds navigation action to corporate cards filtered by expense user.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Tables/EACorpCardTransDetail.Table.al New table for Level 3 tax/detail lines per transaction.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Tables/EACorpCardTrans.Table.al New table for imported corporate card transactions (incl. dedup key).
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Tables/EACorpCardProvider.Table.al New table for provider configuration incl. payload storage and cleanup.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Tables/EACorpCardMerchantRule.Table.al New table for merchant normalization rules.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Tables/EACorpCardMCCMap.Table.al New table mapping MCC to expense categories / VAT posting group.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Tables/EACorpCardException.Table.al New table for import/validation exceptions.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Tables/EACorpCardBatch.Table.al New table representing import batches and counters.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Tables/EACorpCard.Table.al New table for card-to-user/provider links.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Permissions/EACorpCardRead.PermissionSet.al New internal read permission set for corp card feature.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Permissions/EACorpCardEdit.PermissionSet.al New internal edit permission set for corp card feature.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Permissions/EACorpCardAdmin.PermissionSet.al New internal admin permission set for corp card feature.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Pages/EACorpCardTransList.Page.al New list page for browsing transactions + actions to open details/expense.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Pages/EACorpCardStatisticsFactbox.Page.al New factbox with recent import/matching statistics.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Pages/EACorpCardProviders.Page.al New provider list page with payload upload/clear/import/scheduling helpers.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Pages/EACorpCardMerchantRules.Page.al New page for maintaining merchant normalization rules.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Pages/EACorpCardMCCMap.Page.al New page for MCC-to-category/VAT mapping maintenance.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Pages/EACorpCardJQScheduleSubpage.Page.al New listpart showing job queue schedule entries for a provider.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Pages/EACorpCardJQSchedule.Page.al New schedule page hosting the schedule listpart.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Pages/EACorpCardExceptions.Page.al New list page for viewing corp card exceptions.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Pages/EACorpCardDetails.Page.al New list page for Level 3 detail lines by transaction.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Pages/EACorpCardDashboardFactbox.Page.al New dashboard part showing recent batches.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Pages/EACorpCardDashboard.Page.al New role center-style dashboard for corp card imports.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Pages/EACorpCardCards.Page.al New list page for corporate cards + navigation to transactions.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Pages/EACorpCardBatches.Page.al New list page for batches + actions to run matching and navigate.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Interfaces/EACorpCardProviderInterface.Interface.al New provider interface for download/parse/ack lifecycle.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Interfaces/EACorpCardExpWriterInterface.Interface.al New interface for writing expenses from transactions.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Enums/EACorpCardTransStatus.Enum.al New enum for transaction lifecycle status.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Enums/EACorpCardMatchType.Enum.al New enum describing match strategy/type.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Enums/EACorpCardFeedType.Enum.al New enum describing provider feed types (CSV/XML/ISO/CAMT/API/etc.).
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Enums/EACorpCardExcpType.Enum.al New enum describing exception categories.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Enums/EACorpCardCreateMode.Enum.al New enum controlling draft creation strategy.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Enums/EACorpCardBatchStatus.Enum.al New enum for batch run state.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Enums/EACorpCardAuthType.Enum.al New enum for provider auth type.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Codeunits/EACorpCardValidateMgt.Codeunit.al New transaction validation helper for mandatory fields.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Codeunits/EACorpCardReportMgt.Codeunit.al New report aggregation helpers for corp card expenses.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Codeunits/EACorpCardProvReg.Codeunit.al New provider implementation resolver/registry.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Codeunits/EACorpCardPostImportOrch.Codeunit.al New post-import orchestration for normalization/matching/draft creation.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Codeunits/EACorpCardMerchantNorm.Codeunit.al New merchant normalization logic based on configured rules.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Codeunits/EACorpCardMCCMgt.Codeunit.al New MCC mapping seeding + validation helpers.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Codeunits/EACorpCardMatchMgt.Codeunit.al New basic matching logic using user/date/amount tolerances.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Codeunits/EACorpCardMapMgt.Codeunit.al New mapping helpers + Data Exch event subscribers for payload sourcing/field normalization.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Codeunits/EACorpCardJQRunner.Codeunit.al New Job Queue runner intended to execute scheduled imports.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Codeunits/EACorpCardJQMgt.Codeunit.al New job queue scheduling management for provider imports.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Codeunits/EACorpCardImportOrch.Codeunit.al New import orchestrator coordinating provider lifecycle into batches.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Codeunits/EACorpCardFeedMgt.Codeunit.al New entrypoint for running imports for one/all enabled providers.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Codeunits/EACorpCardExpWriter.Codeunit.al New draft expense creation from transactions + Level 3 VAT specs creation.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Codeunits/EACorpCardEnhancedMatchMgt.Codeunit.al New enhanced matching (exact/fuzzy/employee-only) with scoring.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Codeunits/EACorpCardDENoop.Codeunit.al No-op mapping codeunit for Data Exch flow.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Codeunits/EACorpCardDedupMgt.Codeunit.al New duplicate detection helper.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Codeunits/EACorpCardAuditSubscribers.Codeunit.al New telemetry/audit logging for corp card operations.
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Codeunits/EACorpCardApprovalMgt.Codeunit.al New approval/report workflow helpers (submit/release/reject).
src/Apps/W1/ExpenseAgent/app/src/CorporateCard/Codeunits/EACorpCardApiSourceProv.Codeunit.al New API payload source provider (Key Vault auth + content download).
src/Apps/W1/ExpenseAgent/app/src/Common/Pages/ExpenseManagementRoleCenter.Page.al Adds Corporate Card actions to role center navigation.
src/Apps/W1/ExpenseAgent/app/samples/CorporateCard/CorpCardISO20022Sample.xml Adds ISO20022 sample payload.
src/Apps/W1/ExpenseAgent/app/samples/CorporateCard/CorpCard-Sample-Level3.xml Adds Level 3 sample payload.
src/Apps/W1/ExpenseAgent/app/samples/CorporateCard/CorpCard-Sample-60.csv Adds CSV sample payload (60 rows).
src/Apps/W1/ExpenseAgent/app/appsourcecop.json Adds AppSourceCop prefix configuration.
src/Apps/W1/ExpenseAgent/app/app.json Extends ID ranges and includes samples as resource folder.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +359 to +370
var
Count: Integer;
Remaining: Text;
LineTxt: Text;
NewLinePos: Integer;
HeaderHandled: Boolean;
CRChar: Char;
begin
Remaining := PayloadTxt;
CRChar := 13;
while Remaining <> '' do begin
NewLinePos := StrPos(Remaining, Format(10));
Comment on lines +15 to +22
procedure RunImport()
var
CorpCardFeedMgt: Codeunit EACorpCardFeedMgt;
ErrorMsg: Text;
begin
ErrorMsg := '';
CorpCardFeedMgt.RunAllEnabledProviders();
end;
Comment on lines +86 to +87
// Calculate score: Perfect match = 100, decreased by date/amount variance
MatchScore := 100 - (DateDiff * 5) - ((AmountDiff / MaxAmountDiff) * 10);
Comment on lines +28 to +30
CorpCardTrans."Merchant Norm" := NormalizedName;
if MatchedCategory <> '' then
CorpCardTrans.MCC := '';
Comment on lines +56 to +76
var
Client: HttpClient;
RequestMessage: HttpRequestMessage;
ResponseMessage: HttpResponseMessage;
RequestHeaders: HttpHeaders;
begin
RequestMessage.Method := 'GET';
RequestMessage.SetRequestUri(CorpCardProvider."API Endpoint");

RequestMessage.GetHeaders(RequestHeaders);
RequestHeaders.Add('Accept', 'application/xml, text/xml, text/csv, application/json');
AddAuthHeaders(RequestHeaders, CorpCardProvider);

if not Client.Send(RequestMessage, ResponseMessage) then
Error(ApiTransportFailedErr, CorpCardProvider.Code);

if not ResponseMessage.IsSuccessStatusCode() then
Error(ApiHttpFailedErr, CorpCardProvider.Code, ResponseMessage.HttpStatusCode(), ResponseMessage.ReasonPhrase());

ResponseMessage.Content.ReadAs(Result);
ContentType := '';
field(Pattern; Rec.Pattern)
{
ApplicationArea = Basic, Suite;
ToolTip = 'Specifies the regex pattern to match merchant names.';
Caption = 'Merchant Normalization Rules';
ApplicationArea = Basic, Suite;
RunObject = Page "EACorpCardMerchantRules";
ToolTip = 'Setup regex patterns for merchant name normalization.';
Comment on lines +35 to +37
Expense.SetRange("Expense User No.", ExpenseUserNo);
Expense.SetFilter(Status, '%1|%2', Expense.Status::Open, Expense.Status::Released);
Expense.SetFilter("Expense Report No.", ''); // Not yet added to a report
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 ExpenseManagement Integration GitHub request for Integration area

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants