Testing the Carasent integration has been painful. Every time a developer needs to verify a booking, invoice, or record signature, they have to log in, navigate to a case, fill out the close modal, and submit. If something fails halfway through, they start over. Today's work eliminates that friction entirely.
The goal: standalone CLI scripts that call each Carasent API step individually, plus combined flows that chain them together — all runnable from the terminal with yarn carasent:* commands.
Why Not Just Use curl
The existing carasent.ts library already handles OAuth token acquisition, patient lookup with PNR masking, booking type resolution based on county codes, midnight-crossing edge cases, and invoice array response normalization. Rewriting that in shell scripts would mean duplicating logic that's already tested in production. The scripts need to import from carasent.ts directly.
The Module-Scope Problem
Here's where it gets interesting. The carasent.ts library reads environment variables at module scope — line 47 through 62. Constants like CARASENT_AUTH_URL and CARASENT_CLIENT_ID are evaluated the moment the module is imported.
In Next.js, .env is loaded automatically before any server code runs. In a standalone tsx script, it isn't. The naive approach — import dotenv, then import carasent — fails because ESM static imports are hoisted. Both imports execute before any top-level code runs.
The fix: dynamic imports. Load .env synchronously at the top of each script, then use await import() inside the async main function. By the time the carasent module evaluates, process.env is already populated:
import { loadEnv, runScript, printSuccess } from "../lib/helpers.js";
loadEnv(); // Runs before anything else
async function main() {
// Dynamic import — carasent.ts reads process.env NOW, not at hoist time
const { fetchCarasentToken } = await import("../../../src/lib/carasent.js");
const token = await fetchCarasentToken();
printSuccess("Token acquired", token);
}
runScript("Fetch Carasent Token", main);A dedicated tsconfig.scripts.json extends the root config with nodenext module resolution, since the root uses bundler mode which doesn't work outside Next.js.
Individual Steps and Combined Flows
Twelve individual scripts cover every Carasent API operation. Three combined flows chain them together, mirroring the production routes exactly.
All three flows support a --dry-run flag. In dry-run mode, the script fetches a real token and looks up the patient (read-only), then prints what the remaining mutating steps would do without calling the APIs. Useful for verifying credentials and patient data before committing to real bookings.
Error handling mirrors production exactly. Invoice creation and payment marking are wrapped in try-catch and continue on failure — everything else aborts the flow.
Verification
The simplest test confirms the entire chain works:
$ yarn carasent:token
============================================================
Fetch Carasent Token
============================================================
OK Token acquired
{
"access_token": "adc625c8200d1f9e57ec...",
"expires_in": 3600,
"token_type": "Bearer"
}
Total: 231msMissing arguments produce clear errors instead of cryptic failures:
$ yarn carasent:find-patient
ERROR: --pnr is required (Patient personal number (YYYYMMDDNNNN))Files Modified
One existing file changed: carasent.ts gained an export on the CarasentInvoiceResponse type so flow scripts can reference it. Seventeen new files created across scripts/carasent/ — twelve individual step scripts, three combined flow scripts, a shared helpers module, and a README. A tsconfig.scripts.json was added at the project root. Fifteen new yarn script entries in package.json. One new devDependency: tsx for TypeScript execution outside Next.js.
Lessons
First, reuse production code. The scripts call the same functions the UI calls. No test doubles, no mocks, no drift. If the API contract changes, the scripts break in the same way the app would.
Second, module evaluation order matters. ESM hoisting makes "import then configure" impossible with static imports. Dynamic imports are the clean solution — not hacks like mutating modules after import.
Third, dry-run modes pay for themselves immediately. Being able to verify credentials and patient existence without creating real bookings in Carasent saves cleanup time and avoids polluting the EHR with test data.