Back to Journal

Eight invoicing bugs, one day

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 (#948)

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 SalarySettings.jsx:

// Before: missing wrapper
const handleUpdate = data => {
  dispatch({type: ACTIONS.UPDATE_DATA, payload: data}) 
}
 
// After: properly structured
const handleUpdate = data => {
  dispatch({type: ACTIONS.UPDATE_DATA, payload: {user: data}}) 
}

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 (#944)

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.

// dashboard-widgets.jsx
const response = await InvoiceService.getAll({
  itemsPerPage: 3,
  page: 1,
  'order[updatedAt]': 'desc'
  'order[sentAt]': 'desc'
});

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 (#939)

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:

// invoice-helper.jsx
worknode_fee: {
  label: t('invoice.numbers.worknodeFee'),
  value: data?.commissionAmount,
  percentage: isProUser ? undefined : (worknodePercentage || undefined), 
  percentageLabel: isProUser ? 'PRO' : undefined, 
  percentage: data?.proEligibleAtSend ? undefined : (worknodePercentage || undefined), 
  percentageLabel: data?.proEligibleAtSend ? 'PRO' : undefined, 
},

The isProUser hook checked current subscription. The proEligibleAtSend field preserved history. Small change, correct behavior.

Displaying Admin Messages (#927)

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.

The new component:

function NotSentDetails({invoice}) {
  const {t} = useTranslation()
  const errorCode = invoice?.errorCode
  const adminMessage = invoice?.adminMessage
 
  if (!errorCode && !adminMessage) {
    return null
  }
 
  return <div className="bg-layer-0 rounded-md p-4 md:w-1/2 lg:max-w-[500px] gap-4">
    <div className="flex border-b border-separator-1 items-center pb-4 gap-3 tx-s font-[600]">
      <Icon name="alert" size={16} className="text-orange-500"/>
      <span>{t('invoice.details.notSentReason')}</span>
    </div>
 
    <div className="py-4">
      {errorCode && (
        <p className="tx-s text-dimmed mb-2">
          {t(`invoice.errorCode.${errorCode}`, {defaultValue: t('invoice.errorCode.unknown')})}
        </p>
      )}
      {adminMessage && (
        <p className="tx-s text-dimmed">
          {adminMessage}
        </p>
      )}
    </div>
  </div>
}

The backend had 19 different error codes. Each needed a translation:

"errorCode": {
  "unknown": "Unknown error",
  "0": "Unknown error",
  "1": "Invalid currency",
  "2": "Invalid organization number",
  "3": "Recipient email address is missing",
  "4": "User is missing Pinebucket registration",
  "5": "Recipient name is missing",
  "6": "Invalid recipient address",
  "7": "Invalid recipient city",
  "9": "Invalid invoice email for recipient",
  "11": "Invalid zip code for recipient",
  "12": "Recipient name is too long",
  "13": "E-invoice is required",
  "14": "Recipient address is missing",
  "15": "Invalid VAT registration number",
  "16": "E-invoice connection pending",
  "17": "Customer is blacklisted",
  "18": "Invalid characters in invoice",
  "19": "Invalid attachments"
}

One refinement: rows with NOT_SENT status now auto-expand to show the error immediately:

<BaseRow
  summary={summary}
  onOpen={() => getDetails(invoice)}
  details={detailsComponent}
  actions={actions}
  actionsVariant="dropdown"
  defaultOpen={invoice.status === InvoiceStatus.NOT_SENT} 
/>

This required adding a defaultOpen prop to the BaseRow component:

export default function BaseRow({
  summary,
  actions,
  onOpen = null,
  details,
  actionsVariant = 'buttons',
  defaultOpen = false
}) {
  const [isOpen, setIsOpen] = useState(defaultOpen) 
  const [icon, setIcon] = useState(defaultOpen ? 'chevron_up' : 'chevron_down') 
  // ...
}

Smaller Fixes

Invoice Preview Reference (#931)

The preview showed company name instead of contact name for "Er referens" (Your reference):

// Before: wrong field
<strong>{recipient?.customer?.name || '-'}</strong> 
 
// After: correct field
<strong>{recipient?.customer?.contactName || '-'}</strong> 

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. Simple addition:

import ReferralSidebarCard from '@/components/Referral/SidebarCard.jsx';
 
// In the Mobile menu component
<MyPlan />
<ReferralSidebarCard /> 

Payment Info Removal (#946)

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

Salary Widget Updates (#928)

Three changes to the salary balance widget:

  1. Changed button text from "Payout" to "Direktutbetalning" (Direct payment)
  2. Moved button from bottom to header row
  3. Fixed date format - removed hardcoded 'pl-PL' locale:
// Before: Polish locale (why?)
formatDate(lastSalary?.date, 'pl-PL') 
 
// After: default locale
formatDate(lastSalary?.date) 
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

Frontend:
├── src/components/Invoice/Row.jsx        - NotSentDetails component, auto-expand
├── src/components/Base/BaseRow.jsx       - defaultOpen prop
├── src/components/Menu/Mobile.jsx        - Referral card
├── src/pages/Settings/Salary/SalarySettings.jsx - State update fix
├── src/pages/Invoice/Preview.jsx         - Customer reference field
├── src/hooks/invoice-helper.jsx          - proEligibleAtSend usage
├── src/widgets/Balance.jsx               - Button position, date format
├── src/widgets/dashboard-widgets.jsx     - Sort by sentAt
├── src/pages/Settings/PlanManagement.jsx - Remove payment info
├── src/i18n/locales/sv.json              - Swedish translations
└── src/i18n/locales/en.json              - English translations

Lessons

  1. Trace the data flow - The settings bug was one line, but finding it required following dispatch → reducer → state → component.

  2. Historical data preservation - Timestamps and status fields captured at creation time (sentAt, proEligibleAtSend) exist for a reason. Use them instead of current state.

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

  4. Consistent component patterns - 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.

  5. Translation files are documentation - The 19 error codes in sv.json/en.json 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.