Building a self-service GL journal importer for Unit 4 ERPx
Finance teams working with Unit 4 ERPx face a recurring problem: importing general ledger journal entries from external systems. The traditional path is manual data entry or CSV uploads through the native ERPx interface — both error-prone, both slow, and neither gives meaningful pre-submission feedback. When a journal carries hundreds of lines across multiple cost centres and projects, a single mistyped account code can reject the whole batch, leaving the team to diagnose problems inside a spreadsheet without any clear pointer to what went wrong.
The Journal Importer is a purpose-built web application that fixes this. It accepts Excelerator Postback spreadsheets, validates every line against live ERPx data before submission, and posts balanced journals directly to the ERPx GL journal API. What was previously a fragile, multi-step manual process becomes a guided three-step workflow: upload, preview, import.
The problem space
Excelerator Postback files are structured Excel workbooks generated by external payroll and reporting systems. They follow a specific shape: declare directives carry metadata (client, batch identifier, currency); an update_columns row establishes the column mapping; and update_data rows contain the journal lines. Each line carries an account code, up to seven dimension values (cost centre, project, week number, and others), debit and credit amounts, a description, and sequencing information.
The catch is that these files are produced outside ERPx and may contain stale data. Account codes get deactivated. Cost centres get renamed. Periods close. Without validation, the finance team discovers these issues only after submission, when ERPx rejects the batch with error messages that are difficult to trace back to specific spreadsheet rows.
Architecture and stack
The application is built on .NET 10 and runs on AWS serverless infrastructure. The frontend is a Blazor WebAssembly application running entirely in the browser, talking to a backend API hosted on AWS Lambda. Static WASM files are served from S3 via CloudFront; API requests route to a Lambda function URL running a Native AOT Kestrel binary.
Native Ahead-of-Time compilation is central to the backend's performance. The Lambda binary compiles to roughly 30MB of native code with no JIT warmup penalty, delivering consistent sub-100ms cold starts. That matters for a tool finance teams use in bursts during month-end close, when Lambda instances may have scaled to zero between requests.
The Lambda sits behind AWS Lambda Web Adapter, which translates function URL events into standard HTTP requests. The same Kestrel-based ASP.NET Core application runs identically in local development (via .NET Aspire) and in production — eliminating the class of bugs that come from environment-specific code paths.
Security model
The application implements defence in depth across three layers. At the infrastructure level, CloudFront with AWS WAF applies rate limiting and managed rule sets, while a resource policy restricts the Lambda function URL to accept requests only from the CloudFront distribution. An origin verification header injected by CloudFront and validated by custom middleware prevents direct access to the function URL even if its address becomes known.
At the application level, Microsoft Entra ID provides OpenID Connect authentication. The Blazor WASM client uses MSAL to authenticate users against the organisation's tenant, and the API validates JWT Bearer tokens on every request. Only authenticated users within the Entra ID tenant can access the tool.
Upload and parsing pipeline
When a user selects a spreadsheet, the browser uploads it directly to S3 via a presigned PUT URL. This sidesteps Lambda's 6MB request payload limit and allows files of any reasonable size. The presigned URL pattern keeps credentials out of the browser while granting time-limited, single-use upload permissions.
The parser uses Sylvan.Data.Excel, a high-performance streaming Excel reader that processes workbooks without loading them entirely into memory. It handles the Excelerator Postback format by scanning for declare directives, building a dynamic column mapping from the update_columns row, and extracting typed values from each data row. The parser handles multiple date formats, decimal parsing with culture-invariant rules, and graceful fallback for the type mismatches that are common in generated spreadsheets.
Validation engine
Validation is where the Journal Importer delivers its primary value. The validator runs structural checks, business rule validation, and live ERPx lookups in a single pass.
Structural validation confirms every line has an account code, description, cost centre, week number, voucher number, and a non-zero amount. It verifies that sequence numbers within each transaction are contiguous, start from zero, and contain no duplicates or gaps.
Business rule validation ensures journals are balanced — total debits must equal total credits across both base and currency amounts. The validator also handles VAT-inclusive tax codes, where the VAT amount must be included in the balance calculation. All lines within a journal must share the same accounting period and transaction date, preventing cross-period posting errors that ERPx would otherwise catch only after submission.
The most valuable layer queries ERPx in real time. The validator fetches the current chart of accounts, cost centres, projects, and open periods for the target company, then checks every line against this live data. An account code that existed last month but has since been deactivated is flagged before the user attempts to post. A cost centre that was valid in the source system but doesn't exist in ERPx is caught immediately. The tool also validates that the target period is open for posting, preventing submissions ERPx would reject.
These lookup calls run concurrently — accounts, cost centres, projects, open periods, and week mappings fetched in parallel to minimise validation latency.
Preview and concurrency control
After validation, users see a detailed preview showing every journal line with its resolved display names from ERPx. Lines with errors display inline messages explaining exactly what is wrong and on which row. This preview step exists as an explicit gate: users must review the data before committing to an import.
A concurrency control mechanism prevents two users from importing to the same company simultaneously. An S3-based advisory lock with a 10-second TTL guards the preview and import stages. The active browser tab extends this lock every five seconds via a periodic timer. If the user switches tabs, the system continues extending the lock for a 60-second grace period before allowing it to expire. When a blocked user encounters an active lock, the interface polls every five seconds and acquires the lock as soon as it becomes available.
This design acknowledges the realities of browser-based applications: tab visibility changes frequently and users may close tabs without explicit cleanup. The short TTL ensures abandoned locks expire quickly, while the grace period prevents unnecessary lock churn during normal multitasking.
Import and results
When the user confirms the import, the validated journal is assembled into an ERPx-compatible payload and posted to the GL journal API via OAuth-authenticated HTTP calls. The ERPx client uses Microsoft.Extensions.Http.Resilience for retry policies and circuit breaking, handling transient failures without surfacing raw HTTP errors to the user.
Results are stored as JSON in S3 and retrieved via presigned GET URLs, respecting Lambda's response payload limits. The UI displays per-line outcomes, showing exactly which lines posted successfully and which encountered issues during the ERPx write.
Local development with .NET Aspire
The development experience uses .NET Aspire to orchestrate the API and Web projects as separate resources. A single command starts both services with correct configuration: the WASM app connects to the API via a configured base URL, and CORS headers are configured for cross-port requests. In production, both sit behind the same CloudFront domain, eliminating CORS entirely.
The Aspire AppHost provides structured logging, distributed tracing via OpenTelemetry, and resource management through a local dashboard. The same OpenTelemetry instrumentation that aids local debugging ships to Datadog in production via a Lambda extension, giving the team consistent observability across environments.
Testing strategy
The test suite uses xUnit v3 with a focus on behaviour verification rather than implementation coupling. WireMock.Net simulates ERPx HTTP responses, allowing integration tests to exercise the full validation and posting pipeline without a live ERPx instance. Verify.XunitV3 provides snapshot testing for complex response structures, catching unintended changes to parser output or validation messages. ClosedXML generates test spreadsheets programmatically, ensuring test data remains version-controlled and reproducible.
Why this shape of tool matters
The Journal Importer is small. It does one thing. It costs almost nothing to run when idle, and it scales instantly during peak usage. But it eliminates an entire category of operational friction — the post-submission rejection loop that used to swallow hours of a finance team's month-end close.
This is the shape of work HawkBS most often takes on: a focused, productised piece of engineering that sits between an off-the-shelf system and the way your business actually wants to use it. .NET 10, serverless, AOT-compiled where it earns its keep, validated against live data, and shipped with the observability and testing infrastructure to keep it healthy past launch.