Expense Agent: Add Corporate Card interfaces and import flow - #10057
Expense Agent: Add Corporate Card interfaces and import flow#10057Alexander Yakunin (Alexander-Ya) wants to merge 21 commits into
Conversation
| ShowCaption = true; | ||
| Enabled = false; | ||
|
|
||
| field(TotalBatches; GetTotalBatches()) |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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)] |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
| Expense.SetRange("Expense User No.", CorpCard."Expense User No."); | ||
| Expense.SetRange("Status", Expense."Status"::Open); | ||
|
|
||
| if not Expense.FindSet() then | ||
| exit(false); |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
| 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)); |
| procedure RunImport() | ||
| var | ||
| CorpCardFeedMgt: Codeunit EACorpCardFeedMgt; | ||
| ErrorMsg: Text; | ||
| begin | ||
| ErrorMsg := ''; | ||
| CorpCardFeedMgt.RunAllEnabledProviders(); | ||
| end; |
| // Calculate score: Perfect match = 100, decreased by date/amount variance | ||
| MatchScore := 100 - (DateDiff * 5) - ((AmountDiff / MaxAmountDiff) * 10); |
| CorpCardTrans."Merchant Norm" := NormalizedName; | ||
| if MatchedCategory <> '' then | ||
| CorpCardTrans.MCC := ''; |
| 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.'; |
| 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 |
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
What I tested and the outcome (required — be specific: scenarios, commands, screenshots for UI changes)
Risk & compatibility