mirror of
https://github.com/temetro/temetro.git
synced 2026-08-24 17:46:33 +00:00
feat: HL7/FHIR, e-prescribing & insurance claims integrations
Real, standards-compliant integration clients that the clinic points at
its own (sandbox or production) endpoints — no mock data.
backend:
- `integrations` table (per org+type) storing endpoint + encrypted
credentials (reusing the AI-key crypto) + status; Drizzle migration
- services/integrations:
- fhir.ts — FHIR R4 REST client (pull lab Observations → patient
record), HL7 v2 ORU parsing, capability-statement connection test
- eprescribe.ts — NCPDP SCRIPT NewRx message build + transmit
- claims.ts — X12 837P claim generation + 835 remittance parsing
- `/api/integrations` route: config GET/PUT (owner/admin), connection
test, and the FHIR sync / HL7 ingest / e-Rx send / claim submit actions,
RBAC-gated (lab/patient, prescription, invoice)
frontend:
- lib/integrations.ts client
- Settings → Integrations tab to configure endpoints/credentials/enable
+ test each integration
- on-page actions, shown only when the integration is enabled:
Lab page → FHIR "Sync results" card; prescription sheet → "Send to
pharmacy"; invoice sheet → "Submit claim"
Production e-Rx/claims routing requires the clinic's own Surescripts /
clearinghouse credentials; the code transmits real messages once supplied.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1103,6 +1103,36 @@
|
||||
"uploadFailedTitle": "Some files didn't upload",
|
||||
"uploadFailedBody": "The record was saved, but one or more files failed to upload. Try adding them again from the record."
|
||||
},
|
||||
"integrations": {
|
||||
"fhir": {
|
||||
"cardTitle": "Lab system (HL7/FHIR)",
|
||||
"disabledHint": "Not enabled. An owner or admin can connect a lab system in Settings → Integrations.",
|
||||
"searchPlaceholder": "Search a patient to pull results…",
|
||||
"change": "Change",
|
||||
"sync": "Sync results",
|
||||
"syncing": "Syncing…",
|
||||
"syncedTitle": "Results synced",
|
||||
"syncedBody": "Imported {{count}} result(s) for {{name}}.",
|
||||
"failedTitle": "Sync failed",
|
||||
"failedBody": "Couldn't reach the lab system. Please try again."
|
||||
},
|
||||
"eRx": {
|
||||
"send": "Send to pharmacy",
|
||||
"sending": "Sending…",
|
||||
"sentTitle": "Prescription sent",
|
||||
"sentBody": "Transmitted to the pharmacy as an NCPDP SCRIPT NewRx.",
|
||||
"failedTitle": "Couldn't send",
|
||||
"failedBody": "The pharmacy gateway rejected the message or is unreachable."
|
||||
},
|
||||
"claims": {
|
||||
"submit": "Submit claim",
|
||||
"submitting": "Submitting…",
|
||||
"submittedTitle": "Claim submitted",
|
||||
"submittedBody": "Clearinghouse status: {{status}}.",
|
||||
"failedTitle": "Couldn't submit claim",
|
||||
"failedBody": "The clearinghouse rejected the claim or is unreachable."
|
||||
}
|
||||
},
|
||||
"patientCard": {
|
||||
"notFound": "No patient found for file #{{number}}.",
|
||||
"overview": "Overview",
|
||||
@@ -1277,8 +1307,55 @@
|
||||
"records": "Records",
|
||||
"signing": "Signing",
|
||||
"careTeam": "Care team",
|
||||
"integrations": "Integrations",
|
||||
"developers": "Developers"
|
||||
},
|
||||
"integrations": {
|
||||
"loading": "Loading integrations…",
|
||||
"intro": "Connect temetro to external healthcare systems. Point each integration at your vendor's sandbox or production endpoint and supply its credentials.",
|
||||
"endpoint": "Endpoint URL",
|
||||
"credentials": "Credentials",
|
||||
"credentialsSet": "•••••••• (stored — type to replace)",
|
||||
"enable": "Enable integration",
|
||||
"enableHint": "When on, its actions appear on the relevant pages.",
|
||||
"save": "Save",
|
||||
"saving": "Saving…",
|
||||
"test": "Test connection",
|
||||
"testing": "Testing…",
|
||||
"savedTitle": "Integration saved",
|
||||
"saveFailedTitle": "Couldn't save",
|
||||
"saveFailedBody": "Something went wrong, or you don't have permission. Please try again.",
|
||||
"testOk": "Connection succeeded",
|
||||
"testFailed": "Connection failed",
|
||||
"testError": "Couldn't reach the endpoint.",
|
||||
"lastSync": "Last activity {{when}}",
|
||||
"status": {
|
||||
"connected": "Connected",
|
||||
"error": "Error",
|
||||
"unconfigured": "Not configured"
|
||||
},
|
||||
"fhir": {
|
||||
"title": "Lab system (HL7/FHIR)",
|
||||
"description": "Read lab results from a FHIR R4 server or HL7 v2 feed (e.g. a HAPI FHIR / SMART Health IT sandbox, or your lab's gateway).",
|
||||
"endpointPlaceholder": "https://hapi.fhir.org/baseR4",
|
||||
"credentialsPlaceholder": "Bearer token (optional for open sandboxes)",
|
||||
"credentialsHint": "Sent as a Bearer token, or JSON {\"token\":\"…\"}. Leave blank for public sandboxes."
|
||||
},
|
||||
"eprescribe": {
|
||||
"title": "e-Prescribing (NCPDP SCRIPT)",
|
||||
"description": "Transmit prescriptions to pharmacies as NCPDP SCRIPT NewRx messages. Production routing requires your Surescripts (or sandbox) account.",
|
||||
"endpointPlaceholder": "https://your-pharmacy-gateway.example/script",
|
||||
"credentialsPlaceholder": "JSON: {\"token\":\"…\",\"senderId\":\"…\"}",
|
||||
"credentialsHint": "JSON with your gateway token and sender id."
|
||||
},
|
||||
"claims": {
|
||||
"title": "Insurance claims (X12 837/835)",
|
||||
"description": "Submit professional claims (837P) to a clearinghouse and read remittances (835). Production requires your clearinghouse account.",
|
||||
"endpointPlaceholder": "https://your-clearinghouse.example/claims",
|
||||
"credentialsPlaceholder": "JSON: {\"token\":\"…\",\"submitterId\":\"…\",\"receiverId\":\"…\"}",
|
||||
"credentialsHint": "JSON with your clearinghouse token and submitter/receiver ids."
|
||||
}
|
||||
},
|
||||
"empty": "Nothing here yet.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// Client for the backend integrations API (HL7/FHIR labs, e-prescribing,
|
||||
// insurance claims). Config is owner/admin-only to write; status is readable by
|
||||
// any member so pages can gate their on-page actions.
|
||||
|
||||
import { apiFetch } from "@/lib/api-client";
|
||||
|
||||
export type IntegrationType = "fhir" | "eprescribe" | "claims";
|
||||
export type IntegrationStatus = "unconfigured" | "connected" | "error";
|
||||
|
||||
export type IntegrationConfig = {
|
||||
type: IntegrationType;
|
||||
endpoint: string;
|
||||
enabled: boolean;
|
||||
status: IntegrationStatus;
|
||||
hasCredentials: boolean;
|
||||
lastSyncAt: string | null;
|
||||
};
|
||||
|
||||
export function listIntegrations(): Promise<IntegrationConfig[]> {
|
||||
return apiFetch<IntegrationConfig[]>("/api/integrations");
|
||||
}
|
||||
|
||||
export function saveIntegration(
|
||||
type: IntegrationType,
|
||||
input: { endpoint?: string; enabled?: boolean; credentials?: string },
|
||||
): Promise<IntegrationConfig> {
|
||||
return apiFetch<IntegrationConfig>(`/api/integrations/${type}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export function testIntegration(
|
||||
type: IntegrationType,
|
||||
): Promise<{ ok: boolean; message: string }> {
|
||||
return apiFetch(`/api/integrations/${type}/test`, { method: "POST" });
|
||||
}
|
||||
|
||||
// Pull a patient's lab results from the FHIR server.
|
||||
export function syncFhirLabs(fileNumber: string): Promise<{ imported: number }> {
|
||||
return apiFetch("/api/integrations/fhir/sync", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ fileNumber }),
|
||||
});
|
||||
}
|
||||
|
||||
// Transmit a prescription to a pharmacy (NCPDP SCRIPT NewRx).
|
||||
export function sendEprescription(
|
||||
rxId: string,
|
||||
): Promise<{ messageId: string; status: string }> {
|
||||
return apiFetch("/api/integrations/eprescribe/send", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ rxId }),
|
||||
});
|
||||
}
|
||||
|
||||
// Submit an insurance claim for an invoice (X12 837P) and read the remittance.
|
||||
export function submitInsuranceClaim(
|
||||
invoiceId: string,
|
||||
): Promise<{ claimStatus: string; paidAmount: number; submitted: boolean }> {
|
||||
return apiFetch("/api/integrations/claims/submit", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ invoiceId }),
|
||||
});
|
||||
}
|
||||
|
||||
// Convenience hook-style fetch reused by the on-page sections: returns the
|
||||
// config for one type (or null while loading/absent).
|
||||
export async function getIntegration(
|
||||
type: IntegrationType,
|
||||
): Promise<IntegrationConfig | null> {
|
||||
try {
|
||||
const all = await listIntegrations();
|
||||
return all.find((c) => c.type === type) ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user