Back to Journal

Eight invoicing bugs, one day

Listen to this articleAI narration
0:00 / 0:00

Today was a bugfix marathon on a freelancer invoicing platform. Eight tickets ranging from state management issues to missing admin message visibility. Each fix taught something about React patterns, API design decisions, and the importance of historical data preservation.

The Settings Reset Bug

The bug report: tax and pension settings would revert to previous values after saving. Users could see their changes applied momentarily, then watch them disappear.

The State Reset ProblemUser Inputtax: 30%handleUpdate()API + dispatchWrong Payloadpayload: dataState Mangleduser data at root levelReducer expected {user: data} but received {data} - properties spread at wrong level

The culprit was in the SalarySettings component. The handleUpdate function was dispatching actions with the data directly as the payload. The fix was wrapping that data in a user object before dispatching. The reducer expected the payload to have a user key containing the settings. Without it, the data spread directly into the root state, corrupting the structure. On next render, the component read from the now-malformed state and displayed stale values.

Reducer State StructureBefore (Broken)state = {user: { ... }, // originaltax: 30, // spread at root!pension: 5, // wrong level!invoices: [...],}After (Correct)state = {user: {tax: 30, // correct levelpension: 5, // nested properly},invoices: [...],

The fix was a single line, but understanding why it failed required tracing the data flow from component through dispatch to reducer.

Invoice Sorting By Wrong Date

Dashboard invoices were supposed to show the most recently sent first. Instead, they were sorted by updatedAt - which changed whenever any field was modified, not when the invoice was actually sent.

The fix was changing the API request parameter from ordering by updatedAt descending to ordering by sentAt descending. The difference matters: an invoice sent January 1st but edited January 8th would appear at the top with updatedAt sorting. Users expected chronological send order.

Historical Subscription Display

This one was subtle. The invoice summary showed "PRO" or "3.95%" based on the user's current subscription status - not what they had when the invoice was created.

The Subscription Timing ProblemJanuary: PRO UserSends invoice #100March: Flex UserDowngraded planViews Invoice #100Shows "3.95%" - wrong!Solution: Use proEligibleAtSend field from invoiceBackend captures subscription status at send time

The backend already had the answer - a proEligibleAtSend boolean stored on each invoice. The invoice helper was using the isProUser hook which checked current subscription. The fix was replacing that with the proEligibleAtSend field from the invoice data itself, which preserved the historical status. Small change, correct behavior.

Displaying Admin Messages

The big feature request: when an invoice fails to send, users saw "Ej skickad" (Not sent) but no explanation. The backend had both errorCode and adminMessage fields - they just weren't displayed.

Admin Message FlowBackend (Already Existed)Invoice EntityerrorCode, adminMessageSerialization GroupsInvoice:Collection/ItemAPI ResponseFields exposed ✓Frontend (Added Today)NotSentDetailsNew componentTranslation Keys19 error codesAuto-expanddefaultOpen propBackend had the data. Frontend just needed to display it.

A new NotSentDetails component was created. It takes an invoice as a prop, extracts the errorCode and adminMessage fields, and renders them in a styled container with an alert icon. If neither field exists, it returns null. The error code gets translated using the internationalization system with a fallback to "Unknown error" if the code isn't found.

The backend had 19 different error codes that needed translations. These cover scenarios like invalid currency, invalid organization number, missing recipient email, missing payment provider registration, missing recipient name, invalid address, invalid city, invalid invoice email, invalid zip code, recipient name too long, e-invoice required, missing recipient address, invalid VAT number, e-invoice connection pending, customer blacklisted, invalid characters, and invalid attachments.

One refinement: rows with NOT_SENT status now auto-expand to show the error immediately. This required adding a defaultOpen prop to the BaseRow component. The component initializes its open state and chevron icon based on this prop. When an invoice row is rendered, it checks if the status is NOT_SENT and passes that as the defaultOpen value.

Smaller Fixes

Invoice Preview Reference

The preview showed company name instead of contact name for "Er referens" (Your reference). The fix was changing from displaying the customer's name field to displaying the customer's contactName field.

Also changed the button text from "Klicka för att zooma in" (Click to zoom in) to "Se förhandsvisning" (See preview) - clearer intent.

Mobile Menu Referral Card

Added the referral sidebar card to the mobile "More" menu. A simple import and component addition.

Payment Info Removal

Removed a block of payment method text from the settings page that was confusing users.

Salary Widget Updates

Three changes to the salary balance widget. First, changed button text from "Payout" to "Direktutbetalning" (Direct payment). Second, moved the button from the bottom to the header row. Third, fixed the date format - there was a hardcoded Polish locale being used for some reason. Removing that locale parameter let it use the default.

Salary Widget Layout ChangeBeforeLönesaldo15 000 SEKPayoutAfterLönesaldoDirektutbetalning15 000 SEKSenaste: 2026-01-08

The Pattern

Common Bug Patterns TodayState StructurePayload at wrong levelMissing wrapper objects#948Time ConfusionupdatedAt vs sentAtCurrent vs historical state#944, #939Data VisibilityAPI has data, UI doesn't showWrong field displayed#927, #931Most bugs were "the data exists, it's just not connected right"Backend had errorCode, adminMessage, proEligibleAtSend, contactName - all exposed via API

Files Modified

Eleven files were modified in the frontend. The Invoice Row component received the NotSentDetails component and auto-expand logic. BaseRow got the new defaultOpen prop. The Mobile menu component received the referral card. SalarySettings had the state update fix. The Invoice Preview component got the customer reference field fix. The invoice helper started using proEligibleAtSend instead of the current user status. The Balance widget received the button repositioning and date format fix. The dashboard widgets updated the sort order. PlanManagement had the payment info removed. Both Swedish and English translation files got the 19 new error code translations.

Lessons

First, trace the data flow. The settings bug was one line, but finding it required following dispatch to reducer to state to component.

Second, historical data preservation matters. Timestamps and status fields captured at creation time - like sentAt and proEligibleAtSend - exist for a reason. Use them instead of current state.

Third, API fields are cheap to expose. The backend already had adminMessage and errorCode in the serialization groups. Zero backend work needed.

Fourth, consistent component patterns pay off. Adding defaultOpen to BaseRow was the right abstraction. The component didn't need to know why a row should be open - just that it should be.

Fifth, translation files are documentation. The 19 error codes in the Swedish and English JSON files now serve as a reference for what can go wrong with invoice sending.

Eight tickets closed. The platform's a bit less frustrating to use.