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 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:
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; 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
}
}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
-
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.
-
Type definitions are documentation - The missing
freeCardNumberfield was a silent failure. TypeScript only helps when types are complete. -
APIs lie - The invoice endpoint returns an array. The documentation implied otherwise. Always handle both cases.
-
Graceful degradation - Invoice marking can fail for legitimate reasons. Warn and continue instead of blocking the entire flow.
-
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.