Back to Journal

Fixing the Carasent invoice flow

Today started with a bug report that led down a rabbit hole. Cases with freecard numbers (frikort - Swedish healthcare free cards) were being registered incorrectly in Carasent. What seemed like a small flow issue turned out to be a fundamental misunderstanding of the business logic, plus an entirely missing invoice lifecycle.

The client's patient portal integrates with Carasent/Webdoc for healthcare journaling. When a practitioner closes a case, we need to create the proper documentation in Carasent - either a simple note or a full visit with booking, registration, and billing.

The Original Problem

Cases with freecard numbers were going through the note-only flow instead of creating proper visits. The billing department noticed the discrepancy when reconciling invoices.

The existing flow decision logic looked like this:

// The original (broken) logic
const useNoteFlow = isZ719 || isZ760 || hasFreeCardNumber;

This seemed reasonable at first glance - freecard patients don't pay, so skip the invoice flow. Wrong.

The MisunderstandingWhat We ThoughtZ71.9 → Note flowZ76.0 → Note flowHas freecard → Note flowEverything else → Normal flow"Freecard = no payment = no visit needed"What's Actually TrueZ71.9 → Note flow (always)Z76.0 → Note flow (always)Has freecard → Normal flow!Everything else → Normal flow"Freecard affects payment, not documentation"

The business requirement: freecard patients still need proper visit documentation. The freecard simply means the invoice gets marked as paid differently - the visit itself is identical. Clinical documentation requirements don't change based on payment method.

Tracing the Flow

The sign-and-close endpoint orchestrates everything. Here's the full flow for a normal (non-note) case:

Complete Carasent Visit Flow1. Get PatientgetCarasentPatient()2. Create BookingcreateOrGetBooking()3. Register VisitregisterCarasentVisit()4. Create InvoiceNEW5. Journal EntrycreateJournalEntry()6. Sign RecordsignCarasentRecord()7. Mark PaidmarkInvoiceAsPaid()8. Close CaseDatabase updateWhat Was MissingBefore: Steps 4 and 7 didn't existVisits were created but never billedNo invoice = no billing recordAfter: Complete billing lifecycleInvoice created after visitMarked paid after signingGreen boxes = new functionality added today

The TypeScript Type Issue

Before even fixing the flow logic, there was a prerequisite bug. The freeCardNumber field wasn't in the TypeScript type definition:

export type UserCase = {
  caseId: number;
  userId: number;
  conditionId: number;
  practitionerId: number;
  createdAt: string;
  description: string;
  updatedAt: string;
  closedAt: null | string;
  closed: boolean;
  // freeCardNumber was missing!
  freeCardNumber?: string | null; 
};

Without this, the frontend was silently dropping the freecard value. The type system should have caught this - but the backend was returning any typed responses. A reminder that loose types hide bugs.

Simplifying the Flow Decision

The corrected logic is embarrassingly simple:

// Before: Complex, wrong
const useNoteFlow = isZ719 || isZ760 || (hasFreeCard && !isOtherDiagnosis); 
 
// After: Simple, correct
const useNoteFlow = isZ719 || isZ760; 
Flow Decision LogicDiagnosis CodeZ71.9, Z76.0, or otherZ71.9 / Z76.0Note Flow (IDs 41-44)Everything ElseNormal Flow (IDs 34-37)Freecard statusdoesn't affect this

Z71.9 is "Health advice" - no visit needed, just documentation. Z76.0 is "Repeat prescription" - same deal. Everything else, regardless of payment method, requires the full visit flow.

Implementing the Invoice Functions

The Carasent library was missing invoice functionality entirely. Two new functions:

export const createCarasentInvoice = async (
  token: string,
  clinicId: string,
  visitId: string,
  totalPayment: string = "100"
): Promise<CarasentInvoiceResponse> => {
  if (!visitId) {
    throw new Error("visitId is required to create an invoice");
  }
 
  console.log("[Carasent] Creating invoice for visit:", visitId);
 
  const result = await carasentFetch<CarasentInvoiceResponse[] | CarasentInvoiceResponse>(
    token,
    `/v1/clinics/${clinicId}/invoices`,
    {
      method: "POST",
      body: JSON.stringify({ visitId, totalPayment }),
    }
  );
 
  // API quirk: returns array even for single invoice
  const invoice = Array.isArray(result) ? result[0] : result; 
 
  if (!invoice?.invoiceId) {
    throw new Error("Failed to create invoice - no invoiceId in response");
  }
 
  return invoice;
};

The array handling was a late addition - the first version returned undefined because we assumed a single object response. API documentation was... optimistic.

export const markInvoiceAsPaid = async (
  token: string,
  clinicId: string,
  invoiceId: string,
  totalPayment: string = "100",
  paymentMethod: string = "4" // 4 = "Paid invoice"
): Promise<CarasentInvoiceResponse> => {
  console.log("[Carasent] Marking invoice as paid:", invoiceId);
 
  return carasentFetch<CarasentInvoiceResponse>(
    token,
    `/v1/clinics/${clinicId}/invoices/${invoiceId}`,
    {
      method: "PATCH",
      body: JSON.stringify({ paid: true, totalPayment, paymentMethod }),
    }
  );
};

Payment method "4" corresponds to "Paid invoice" in Carasent's system. The naming conventions aren't intuitive but they're consistent.

The Freecard Edge Case

Here's where it gets interesting. Marking an invoice as paid can fail for freecard patients - they might have freecard status registered in Carasent that we don't know about. The invoice marking becomes a best-effort operation:

if (invoiceId) {
  try {
    console.log("[Sign & Close] Step 7: Marking invoice as paid...");
    await markInvoiceAsPaid(bearer, clinicId, invoiceId, "100", "4");
    console.log("[Sign & Close] ✓ Invoice marked as paid");
  } catch (invoiceError) {
    console.warn( 
      "[Sign & Close] ⚠ Failed to mark invoice as paid (may have freecard):", 
      invoiceError instanceof Error ? invoiceError.message : invoiceError 
    ); 
    // Continue execution - don't fail the entire flow
  }
}
Invoice Payment Edge CaseInvoice CreatedinvoiceId existsRecord SignedClinical docs donetry {markInvoiceAsPaid()}SuccessInvoice marked paidcatch {console.warn()Continue flowBoth paths lead to case closure - no blocking on payment failure

The key insight: invoice payment failure shouldn't abort the entire case closure. The visit documentation is complete. The billing discrepancy can be handled administratively. Crashing the flow helps nobody.

Hardcoding patientTypeId

While debugging, another issue surfaced. The patientTypeId was being pulled from environment variables:

// Before: env var lookup
const patientTypeName = process.env.CARASENT_PATIENT_TYPE_NAME; 
const patientTypeId = await getPatientTypeId(bearer, clinicId, patientTypeName); 
 
// After: hardcoded
const patientTypeId = 2; 

Patient type "2" is the only one we use. The env var lookup added complexity without benefit - if the type ever changes, we'd need code changes anyway. Environment variables are for secrets and environment-specific values, not constants.

This change was replicated in the utskick/send route too - anywhere we touched Carasent.

The Log Trail

After fixes, the logs tell the complete story:

[Sign & Close] Step 1: Getting patient...
[Sign & Close] ✓ Patient found: 12345
[Sign & Close] Step 2: Getting booking...
[Sign & Close] ✓ Booking ready: B-67890
[Sign & Close] Step 3: Registering visit...
[Sign & Close] ✓ Visit registered: V-11111
[Sign & Close] Step 4b: Creating invoice...
[Sign & Close] ✓ Invoice created: I-22222
[Sign & Close] Step 5: Creating journal entry...
[Sign & Close] ✓ Journal entry created: R-33333
[Sign & Close] Step 6: Signing record...
[Sign & Close] ✓ Record signed
[Sign & Close] Step 7: Marking invoice as paid...
[Sign & Close] ✓ Invoice marked as paid
[Sign & Close] Step 8: Closing case in database...
[Sign & Close] ✓ Flow complete

Each step is numbered, each success is marked. When something fails, the log shows exactly where.

Verifying the Fix

The client exported their Carasent visits for January 1-7. Before today's fix, cases with freecard numbers showed up as notes instead of visits. After deploying:

SELECT
  c.caseId,
  c.description,
  c.closedAt,
  c.freeCardNumber
FROM `Case` c
WHERE c.closedAt >= '2026-01-01 00:00:00'
  AND c.closedAt <= '2026-01-07 23:59:59'
ORDER BY c.closedAt DESC;

Cases closed post-deployment now have proper visit IDs in Carasent. The billing department confirmed invoices are appearing correctly.

Files Modified

src/types.ts                    - Added freeCardNumber field
src/components/.../CloseCaseModal.tsx - Fixed flow logic
src/app/api/cases/[caseId]/sign-and-close/route.ts - Flow + invoice
src/app/api/utskick/send/route.ts - Hardcoded patientTypeId
src/lib/carasent.ts            - Added invoice functions

Lessons

  1. Business logic beats clever code - The original conditional tried to optimize for payment scenarios. The business requirement was simpler: diagnosis determines flow, payment is separate.

  2. Type definitions are documentation - The missing freeCardNumber field was a silent failure. TypeScript only helps when types are complete.

  3. APIs lie - The invoice endpoint returns an array. The documentation implied otherwise. Always handle both cases.

  4. Graceful degradation - Invoice marking can fail for legitimate reasons. Warn and continue instead of blocking the entire flow.

  5. Log everything - Numbered steps with explicit success markers make debugging trivial. The extra console.log calls pay for themselves on the first production issue.

The fix was deployed by afternoon.