Test environment was acting up again. Invoices showing twice, OAuth tokens expiring mid-request, and the "send invoice" button doing nothing. Three separate issues that needed untangling.
The Symptoms
Three issues presented simultaneously.
First, duplicate invoices in the dashboard - one invoice showing twice. Second, invoices stuck at "sending" with a yellow background in the UI that never progressed. Third, Fortnox OAuth failures with invalid_grant errors when syncing payments.
Each symptom had a different root cause. Untangling them required understanding how the invoice system actually works.
Issue 1: The Duplicate Invoice Mystery
The dashboard showed 2 invoices but Fortnox only had 1. Database query confirmed only 1 invoice record existed. So where was the duplicate coming from?
The InvoiceDataProvider was designed to merge invoices from two databases - the primary and a legacy "G2" database. This worked in production where they're separate. But in test, both environment variables pointed to the same database.
The provider queried the same database twice and merged identical results, creating duplicates.
The Fix
First attempt: set G2_DATABASE_URL to empty string via Lambda environment variables. That broke the app - Symfony's container failed to build because the env var was referenced in services.yaml and doctrine.yaml.
The proper fix required two changes.
First, I added a guard around the G2 database query in InvoiceDataProvider.php. The original code always queried the G2 database by creating a new entity manager with the G2 database URL and running the query. The updated code initializes an empty G2 results array, then checks if the G2 database URL is not empty before attempting the query. If the URL exists, it creates the entity manager, builds the query to select invoices joined with recipients filtered by owner and not deleted, orders by creation date descending, and executes it. The whole thing is wrapped in a try-catch that logs any errors without crashing.
Second, I updated serverless.yml to set an empty G2 URL for test. Instead of referencing the SSM parameter for the G2 database URL, the test environment now just has an empty string. The env var must exist because Symfony requires it, but it can be empty because our code now handles that case.
Issue 2: OAuth Token Configuration Drift
The invalid_grant error appeared when running the payment sync command. The error message indicated the refresh_token doesn't exist or is invalid for the client.
Investigation revealed the test Lambda was using hardcoded Fortnox credentials from serverless.yml. The test environment had literal strings for the client ID and secret with a TODO comment saying to change them. Meanwhile, production was correctly using SSM parameters to fetch the credentials.
We'd created SSM parameters for test with new Fortnox credentials, but the Lambda wasn't reading them. The fix was consistency: changing the test environment to use the same SSM parameter pattern as production, just with the test prefix in the parameter path instead of the prod prefix.
After deployment, the OAuth flow worked with the correct credentials.
Issue 3: NoxFinans Doesn't Exist in Test
With OAuth fixed, invoices created in Fortnox but failed to send. The POST request to the NoxFinans invoices endpoint returned a 400 error saying "Endpoint not available".
NoxFinans is Fortnox's invoice delivery and factoring service - it sends invoices to customers and handles collection. Fortnox doesn't provide NoxFinans for test accounts.
This isn't a bug - it's an environment limitation. The workaround for test involves several manual steps. First, the invoice creates normally via API. Then you manually send the invoice in the Fortnox UI by clicking "Skicka". Next, manually book the invoice in Fortnox by clicking "Bokför". Then update the WorkNode database to set the status to booked_in_fortnox. Finally, run the payment sync command.
Understanding the Full Invoice Lifecycle
To properly debug, I mapped every state transition and its handler:
Why Payment Sync Wasn't Working
The process-invoice-payments command only looks for invoices in booked_in_fortnox or partially_paid status. Our test invoice was stuck at created_in_fortnox because NoxFinans failed.
After manually updating the status in the database with an UPDATE statement setting the status to booked_in_fortnox for that specific invoice ID, running the command still showed "no payments".
The issue: Fortnox "Inbetalningar" (payments) are separate from marking an invoice as paid. You need to actually create a payment record in Fortnox linked to the invoice, not just mark the invoice status as paid in the UI.
Cron Jobs: Disabled in Test
A final discovery: all scheduled jobs are disabled for test. The serverless.yml has a cronEnabled setting that's false for test and true for prod.
This includes the process-invoice-payments command that runs every 30 minutes, the payslip processing that runs hourly, the recipient verification emails that run hourly, and the invoice processing that runs every 45 minutes.
Enabling them for test would be straightforward but risks side effects - the test environment shares the production Mailgun DSN, so verification emails would go to real recipients.
For now, manual invocation via Lambda console is the safer approach. You can use the AWS CLI to invoke the console function with the command name as the payload, specifying the raw-in-base64-out format and the eu-north-1 region.
Summary: Three Bugs, Three Fixes
The duplicate invoices issue was caused by G2_DATABASE_URL pointing to the same database as DATABASE_URL. The fix was setting G2_DATABASE_URL to an empty string and adding a guard in the provider.
The OAuth failures were caused by hardcoded credentials that weren't reading from SSM. The fix was changing to use the SSM parameter references with the test prefix.
The invoices stuck sending issue was caused by NoxFinans being unavailable in test. The workaround is a manual workflow in the Fortnox UI.
Test Environment Checklist
For future invoice testing in this environment, follow these steps. First, create the invoice - this works automatically. Second, send the invoice manually in Fortnox by clicking "Skicka". Third, book the invoice manually in Fortnox by clicking "Bokför". Fourth, register the payment by navigating to Bokföring, then Inbetalningar, and creating a new payment. Fifth, update the database status to booked_in_fortnox. Sixth, run the payment sync command.
The infrastructure is correct - the test environment just lacks NoxFinans, which is a Fortnox account limitation, not a code issue.
Files Modified
Two files were changed. In InvoiceDataProvider.php in the Service/Invoice directory, I added an empty check guard around the G2 database query. In serverless.yml, I changed the Fortnox client ID and secret to use SSM parameters and set G2_DATABASE_URL to an empty string for the test environment.
The code changes are minimal but the debugging journey revealed the full invoice lifecycle - useful context for any future Fortnox integration work.