Kulcare Docs
DeveloperBilling03 api reference

Admin Terminology Management APIs

This document provides integration specifications for the Admin Terminology Management system APIs.

Base Configuration

Authentication

All terminology management endpoints require super admin authentication.

// Headers required for all requests
const headers = {
  'Authorization': `Bearer ${superAdminToken}`,
  'Content-Type': 'application/json'
}

Base URL

/admin/terminology

API Endpoints

Available Endpoints

  • GET /admin/terminology - Dashboard overview
  • GET /admin/terminology/review - Manual review interface
  • GET /admin/terminology/search - Search medicines
  • GET /admin/terminology/health - System health check
  • PATCH /admin/terminology/:id - Update medicine codes

1. Terminology Dashboard

Endpoint: GET /admin/terminology

Description: Retrieve terminology system overview with statistics and recent activity.

Request:

const response = await fetch('/admin/terminology', {
  method: 'GET',
  headers: headers
});

Response:

{
  "success": true,
  "message": "Terminology dashboard data fetched",
  "data": {
    "stats": {
      "total_medicines": 197,
      "coded_medicines": 0,
      "pending_review": 0,
      "fhir_ready": 0,
      "by_country": [
        {
          "country": "IN",
          "total": 0,
          "coded": 0,
          "coverage": 0
        },
        {
          "country": "US",
          "total": 197,
          "coded": 0,
          "coverage": 0.0
        },
        {
          "country": "AE",
          "total": 0,
          "coded": 0,
          "coverage": 0
        }
      ],
      "coding_quality": {
        "avg_confidence": null,
        "high_confidence": 0,
        "low_confidence": 197,
        "manual_verified": 0
      }
    },
    "recent_coding": [
      {
        "id": 123,
        "name": "Medicine Name",
        "coding_status": "complete",
        "coding_confidence": 0.95,
        "last_coded_at": "2025-10-03T10:30:00.000Z"
      }
    ],
    "pending_review": [
      {
        "id": 124,
        "name": "Medicine Name",
        "manufacturer": "Manufacturer Name",
        "coding_status": "pending",
        "coding_confidence": 0.0
      }
    ]
  }
}

2. Manual Review Interface

Endpoint: GET /admin/terminology/review

Description: Retrieve medicines requiring manual review with filtering and pagination.

Query Parameters:

  • page (integer): Page number (default: 1)
  • per_page (integer): Items per page (default: 20, max: 100)
  • country (string): Filter by country code (IN/US/AE)
  • confidence_range (string): Filter by confidence (low/medium/high)
    • low: confidence < 0.3
    • medium: confidence between 0.3 and 0.7
    • high: confidence > 0.7
  • status (string): Filter by coding status (pending/partial/complete/failed)
  • prescription_usage (string): Filter by prescription usage (prescribed)

Request:

const params = new URLSearchParams({
  page: 1,
  per_page: 20,
  country: 'US',
  confidence_range: 'low'
});

const response = await fetch(`/admin/terminology/review?${params}`, {
  method: 'GET',
  headers: headers
});

Response:

{
  "success": true,
  "message": "Review data fetched",
  "data": {
    "medicines": [
      {
        "id": 196,
        "name": "Aagnil Syrup",
        "manufacturer": "Haledew Remedies",
        "country": "US",
        "med_type": "Syrup",
        "prescription_type": "otc",
        "snomed_code": null,
        "country_specific_code": null,
        "ndc_code": null,
        "coding_status": "pending",
        "coding_confidence": 0.0,
        "coding_source": null,
        "last_coded_at": null,
        "prescription_count": 0,
        "fhir_ready": false
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 20,
      "total_count": 197,
      "total_pages": 10
    },
    "filters": {
      "country": "US",
      "confidence_range": "low",
      "status": null,
      "prescription_usage": null
    }
  }
}

3. Update Medicine Codes

Endpoint: PATCH /admin/terminology/:id

Description: Manually update terminology codes for a specific medicine.

Path Parameters:

  • id (integer): Medicine ID

Request Body (at least one field required):

{
  "snomed_code": "387458008",
  "country_specific_code": "RX789012",
  "ndc_code": "54321-987-65",
  "coding_status": "complete",
  "coding_confidence": 0.98
}

Validation Rules:

  • Required: At least one coding field must be provided with a non-empty value
  • Valid coding fields: snomed_code, country_specific_code, ndc_code, coding_status, coding_confidence
  • coding_confidence: Must be between 0.0 and 1.0 (inclusive)
  • coding_status: Must be one of: pending, partial, complete, failed
  • String fields cannot be empty or contain only whitespace
  • Automatically sets:
    • coding_source to "manual"
    • last_coded_at to current timestamp
    • If codes are provided without confidence: defaults to 0.9 confidence and "complete" status

Request:

const medicineId = 196;
const updateData = {
  snomed_code: "387458008",
  country_specific_code: "RX789012",
  ndc_code: "54321-987-65",
  coding_status: "complete",
  coding_confidence: 0.98
};

const response = await fetch(`/admin/terminology/${medicineId}`, {
  method: 'PATCH',
  headers: headers,
  body: JSON.stringify(updateData)
});

Success Response:

{
  "success": true,
  "message": "Medicine codes updated successfully",
  "data": {
    "medicine": {
      "id": 196,
      "name": "Aagnil Syrup",
      "snomed_code": "387458008",
      "country_specific_code": "RX789012",
      "ndc_code": "54321-987-65",
      "coding_status": "complete",
      "coding_confidence": 0.98,
      "coding_source": "manual",
      "last_coded_at": "2025-10-03T10:30:00.000Z"
    },
    "fhir_ready": true,
    "coding_summary": {
      "snomed_code": "387458008",
      "country_specific_code": "RX789012",
      "ndc_code": "54321-987-65",
      "status": "complete",
      "confidence": 0.98,
      "source": "manual",
      "last_coded_at": "2025-10-03T10:30:00.000Z"
    }
  }
}

Validation Error Examples:

{
  "success": false,
  "errors": [
    {
      "source": "request",
      "title": "Request At least one coding field must be provided with a non-empty value (snomed_code, country_specific_code, ndc_code, coding_status, or coding_confidence)"
    }
  ]
}
{
  "success": false,
  "errors": [
    {
      "source": "coding_confidence",
      "title": "Coding Confidence must be between 0.0 and 1.0"
    }
  ]
}
{
  "success": false,
  "errors": [
    {
      "source": "coding_status",
      "title": "Coding Status must be one of: pending, partial, complete, failed"
    }
  ]
}

4. Search Medicines

Endpoint: GET /admin/terminology/search

Description: Search medicines by name, SNOMED code, or other terminology codes.

Query Parameters:

  • q (string, required): Search query (minimum 2 characters)
  • country (string): Filter by country code (IN/US/AE)
  • limit (integer): Maximum results (default: 10, max: 50)

Request:

const params = new URLSearchParams({
  q: 'Aagnil',
  country: 'US',
  limit: 10
});

const response = await fetch(`/admin/terminology/search?${params}`, {
  method: 'GET',
  headers: headers
});

Response:

{
  "success": true,
  "message": "Search completed",
  "data": {
    "type": "search",
    "data": {
      "query": "Aagnil",
      "limit": 10,
      "results": [
        {
          "id": 196,
          "name": "Aagnil Syrup",
          "manufacturer": "Haledew Remedies",
          "country": "US",
          "coding_status": "complete",
          "coding_confidence": 0.98,
          "snomed_code": "387458008",
          "country_specific_code": "RX789012",
          "ndc_code": "54321-987-65",
          "coding_summary": {
            "snomed_code": "387458008",
            "country_specific_code": "RX789012",
            "ndc_code": "54321-987-65",
            "status": "complete",
            "confidence": 0.98,
            "source": "manual",
            "last_coded_at": "2025-09-15T20:08:33.349Z"
          },
          "fhir_ready": true,
          "prescription_count": 0
        }
      ],
      "total_found": 1
    }
  }
}

5. System Health Check

Endpoint: GET /admin/terminology/health

Description: Retrieve comprehensive system health status for terminology services.

Request:

const response = await fetch('/admin/terminology/health', {
  method: 'GET',
  headers: headers
});

Response:

{
  "success": true,
  "message": "System health check completed",
  "data": {
    "type": "health",
    "data": {
      "timestamp": "2025-09-15T20:03:06.910Z",
      "overall_status": "unhealthy",
      "components": {
        "database": {
          "status": "healthy",
          "response_time_ms": 9.01,
          "total_medicines": 197,
          "coded_medicines": 0,
          "coding_coverage": 0.0
        },
        "terminology_apis": {
          "overall_status": "degraded",
          "apis": {
            "nrces_snomed": {
              "status": "unhealthy",
              "error": "Failed to open TCP connection to snowstorm.nrces.in:443",
              "response_time_ms": 1.43,
              "last_checked": "2025-09-15T20:03:06.920Z"
            },
            "rxnorm": {
              "status": "healthy",
              "response_time_ms": 1264.89,
              "last_checked": "2025-09-15T20:03:08.185Z"
            },
            "fda_ndc": {
              "status": "healthy",
              "response_time_ms": 1286.37,
              "last_checked": "2025-09-15T20:03:09.472Z"
            }
          },
          "healthy_count": 2,
          "total_count": 3
        },
        "job_queues": {
          "status": "healthy",
          "queue_size": 0,
          "processing": 0,
          "retrying": 0,
          "dead": 0,
          "total_pending": 0
        },
        "coding_quality": {
          "status": "unhealthy",
          "coding_coverage": 0.0,
          "average_confidence": 0,
          "failure_rate": 0.0,
          "metrics": {
            "total_medicines": 197,
            "coded_medicines": 0,
            "high_confidence": 1,
            "low_confidence": 196,
            "failed_coding": 0
          }
        },
        "data_freshness": {
          "status": "healthy",
          "last_terminology_update": "2025-09-15T20:01:28.866Z",
          "days_since_update": 0.0,
          "recent_imports": 197,
          "recent_updates": 197
        }
      },
      "alerts": [
        {
          "severity": "warning",
          "component": "database",
          "message": "Low coding coverage: 0.0% (threshold: 85.0%)"
        },
        {
          "severity": "warning",
          "component": "apis",
          "message": "Some APIs are down: nrces_snomed"
        },
        {
          "severity": "warning",
          "component": "quality",
          "message": "Coding quality issues detected"
        }
      ],
      "metrics": {
        "uptime": 99.9,
        "throughput": 1,
        "error_rate": 0.0,
        "api_performance": {
          "avg_response_time_ms": 150,
          "success_rate": 99.5
        }
      }
    }
  }
}

Error Handling

All APIs follow a consistent error response format:

{
  "success": false,
  "errors": [
    {
      "source": "field_name",
      "title": "Error message description"
    }
  ]
}

Common Error Scenarios

Medicine Not Found:

{
  "success": false,
  "errors": [
    {
      "source": "medicine",
      "title": "Medicine not found"
    }
  ]
}

Search Query Too Short:

{
  "success": false,
  "errors": [
    {
      "source": "query",
      "title": "Query must be at least 2 characters"
    }
  ]
}

HTTP Status Codes

  • 200 OK: Successful request
  • 401 Unauthorized: Invalid or missing authentication token
  • 403 Forbidden: Insufficient permissions (requires super admin)
  • 404 Not Found: Medicine or resource not found
  • 422 Unprocessable Entity: Validation errors in request data
  • 500 Internal Server Error: Server-side errors

Integration Examples

React Integration Example

// hooks/useTerminologyAPI.js
import { useState, useEffect } from 'react';

export const useTerminologyDashboard = () => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchDashboard = async () => {
      try {
        const response = await fetch('/admin/terminology', {
          headers: {
            'Authorization': `Bearer ${getAuthToken()}`,
            'Content-Type': 'application/json'
          }
        });
        
        const result = await response.json();
        
        if (result.success) {
          setData(result.data);
        } else {
          setError(result.errors?.[0]?.title || 'Unknown error');
        }
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchDashboard();
  }, []);

  return { data, loading, error };
};

// components/TerminologyDashboard.jsx
import React from 'react';
import { useTerminologyDashboard } from '../hooks/useTerminologyAPI';

const TerminologyDashboard = () => {
  const { data, loading, error } = useTerminologyDashboard();

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div className="terminology-dashboard">
      <h1>Terminology Management</h1>
      
      <div className="stats-grid">
        <div className="stat-card">
          <h3>Total Medicines</h3>
          <p>{data.stats.total_medicines.toLocaleString()}</p>
        </div>
        
        <div className="stat-card">
          <h3>Coded Medicines</h3>
          <p>{data.stats.coded_medicines.toLocaleString()}</p>
        </div>
        
        <div className="stat-card">
          <h3>FHIR Ready</h3>
          <p>{data.stats.fhir_ready.toLocaleString()}</p>
        </div>
        
        <div className="stat-card">
          <h3>Pending Review</h3>
          <p>{data.stats.pending_review.toLocaleString()}</p>
        </div>
      </div>

      <div className="country-breakdown">
        <h2>Coverage by Country</h2>
        {data.stats.by_country.map(country => (
          <div key={country.country} className="country-stat">
            <span>{country.country}</span>
            <span>{country.coded}/{country.total}</span>
            <span>{country.coverage}%</span>
          </div>
        ))}
      </div>
    </div>
  );
};

Medicine Review Component

// components/MedicineReview.jsx
import React, { useState, useEffect } from 'react';

const MedicineReview = () => {
  const [medicines, setMedicines] = useState([]);
  const [filters, setFilters] = useState({
    country: '',
    confidence_range: '',
    page: 1
  });
  const [pagination, setPagination] = useState({});

  const fetchMedicines = async () => {
    const params = new URLSearchParams(
      Object.fromEntries(
        Object.entries(filters).filter(([key, value]) => value !== '')
      )
    );
    
    const response = await fetch(`/admin/terminology/review?${params}`, {
      headers: {
        'Authorization': `Bearer ${getAuthToken()}`,
        'Content-Type': 'application/json'
      }
    });
    
    const result = await response.json();
    if (result.success) {
      setMedicines(result.data.medicines);
      setPagination(result.data.pagination);
    }
  };

  useEffect(() => {
    fetchMedicines();
  }, [filters]);

  const updateMedicine = async (medicineId, updateData) => {
    const response = await fetch(`/admin/terminology/${medicineId}`, {
      method: 'PATCH',
      headers: {
        'Authorization': `Bearer ${getAuthToken()}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(updateData)
    });
    
    const result = await response.json();
    if (result.success) {
      // Refresh the list
      fetchMedicines();
    } else {
      console.error('Update failed:', result.errors);
    }
  };

  return (
    <div className="medicine-review">
      <div className="filters">
        <select 
          value={filters.country} 
          onChange={(e) => setFilters({...filters, country: e.target.value, page: 1})}
        >
          <option value="">All Countries</option>
          <option value="IN">India</option>
          <option value="US">United States</option>
          <option value="AE">UAE</option>
        </select>
        
        <select 
          value={filters.confidence_range} 
          onChange={(e) => setFilters({...filters, confidence_range: e.target.value, page: 1})}
        >
          <option value="">All Confidence</option>
          <option value="low">Low (&lt; 0.3)</option>
          <option value="medium">Medium (0.3-0.7)</option>
          <option value="high">High (&gt; 0.7)</option>
        </select>
      </div>

      <div className="medicines-list">
        {medicines.map(medicine => (
          <MedicineCard 
            key={medicine.id} 
            medicine={medicine} 
            onUpdate={updateMedicine}
          />
        ))}
      </div>

      <div className="pagination">
        <button 
          disabled={pagination.page === 1}
          onClick={() => setFilters({...filters, page: pagination.page - 1})}
        >
          Previous
        </button>
        
        <span>Page {pagination.page} of {pagination.total_pages}</span>
        
        <button 
          disabled={pagination.page === pagination.total_pages}
          onClick={() => setFilters({...filters, page: pagination.page + 1})}
        >
          Next
        </button>
      </div>
    </div>
  );
};

// Medicine card component for updating individual medicines
const MedicineCard = ({ medicine, onUpdate }) => {
  const [editing, setEditing] = useState(false);
  const [formData, setFormData] = useState({
    snomed_code: medicine.snomed_code || '',
    country_specific_code: medicine.country_specific_code || '',
    ndc_code: medicine.ndc_code || '',
    coding_status: medicine.coding_status || 'pending',
    coding_confidence: medicine.coding_confidence || ''
  });

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    // Filter out empty values
    const updateData = Object.fromEntries(
      Object.entries(formData).filter(([key, value]) => value !== '')
    );
    
    await onUpdate(medicine.id, updateData);
    setEditing(false);
  };

  return (
    <div className="medicine-card">
      <div className="medicine-info">
        <h3>{medicine.name}</h3>
        <p>Manufacturer: {medicine.manufacturer}</p>
        <p>Country: {medicine.country}</p>
        <p>Status: {medicine.coding_status}</p>
        <p>Confidence: {(medicine.coding_confidence * 100).toFixed(1)}%</p>
        <p>FHIR Ready: {medicine.fhir_ready ? 'Yes' : 'No'}</p>
      </div>
      
      {editing ? (
        <form onSubmit={handleSubmit} className="edit-form">
          <input
            type="text"
            placeholder="SNOMED Code"
            value={formData.snomed_code}
            onChange={(e) => setFormData({...formData, snomed_code: e.target.value})}
          />
          <input
            type="text"
            placeholder="Country Code (RxNorm/CDCI)"
            value={formData.country_specific_code}
            onChange={(e) => setFormData({...formData, country_specific_code: e.target.value})}
          />
          <input
            type="text"
            placeholder="NDC Code"
            value={formData.ndc_code}
            onChange={(e) => setFormData({...formData, ndc_code: e.target.value})}
          />
          <select
            value={formData.coding_status}
            onChange={(e) => setFormData({...formData, coding_status: e.target.value})}
          >
            <option value="pending">Pending</option>
            <option value="partial">Partial</option>
            <option value="complete">Complete</option>
            <option value="failed">Failed</option>
          </select>
          <input
            type="number"
            step="0.01"
            min="0"
            max="1"
            placeholder="Confidence (0.0-1.0)"
            value={formData.coding_confidence}
            onChange={(e) => setFormData({...formData, coding_confidence: e.target.value})}
          />
          <div className="form-actions">
            <button type="submit">Save</button>
            <button type="button" onClick={() => setEditing(false)}>Cancel</button>
          </div>
        </form>
      ) : (
        <button onClick={() => setEditing(true)}>Edit Codes</button>
      )}
    </div>
  );
};

PATCH API Testing

Example Test Cases

// Test 1: Update single field
const response1 = await fetch('/admin/terminology/196', {
  method: 'PATCH',
  headers: headers,
  body: JSON.stringify({ "coding_status": "complete" })
});

// Test 2: Update multiple fields
const response2 = await fetch('/admin/terminology/196', {
  method: 'PATCH',
  headers: headers,
  body: JSON.stringify({
    "snomed_code": "387458008",
    "country_specific_code": "RX789012",
    "ndc_code": "54321-987-65",
    "coding_status": "complete",
    "coding_confidence": 0.98
  })
});

// Test 3: Invalid request (should fail)
const response3 = await fetch('/admin/terminology/196', {
  method: 'PATCH',
  headers: headers,
  body: JSON.stringify({}) // Empty body - will be rejected
});

// Test 4: Invalid confidence (should fail)
const response4 = await fetch('/admin/terminology/196', {
  method: 'PATCH',
  headers: headers,
  body: JSON.stringify({ "coding_confidence": 1.5 }) // > 1.0 - will be rejected
});

Security Considerations

  1. Authentication: All endpoints require super admin authentication
  2. Input Validation: Comprehensive validation on all parameters and request bodies
  3. SQL Injection Protection: Parameterized queries throughout
  4. Empty/Whitespace Validation: Prevents meaningless updates
  5. Audit Logging: All terminology changes are logged with user information
  6. Rate Limiting: Prevents API abuse

Performance Notes

  • Pagination: Always use pagination for large datasets in review endpoint
  • Filtering: Use country and confidence filters to reduce payload sizes
  • Caching: Dashboard data is cached; search results may be cached client-side
  • Database Queries: Optimized with proper includes and indexing

Background Job System

Automatic Terminology Coding

The system uses Sidekiq background jobs for automatic terminology coding:

Job: TerminologyCodingJob

  • Location: parts/core/services/terminology_coding_job.rb
  • Queue: :terminology
  • Retry Policy: 3 attempts with exponential backoff
  • Processing: Batch processing with 0.1s delay between medicines for API rate limiting

When Jobs are Triggered:

  1. Medicine Import: Automatically queued when medicines are imported via rake tasks
  2. Manual Processing: Via rake terminology:process_uncoded to batch process uncoded medicines
  3. Priority Processing: Via rake terminology:process_priority for high-priority medicines

Important Configuration: Ensure config/environments/development.rb has:

config.active_job.queue_adapter = :sidekiq

Without this, jobs will run in-memory (AsyncAdapter) instead of queueing to Sidekiq.

Monitoring:

  • Sidekiq UI: http://localhost:3000/sidekiq/queues
  • Check terminology queue status in health endpoint
  • View job logs for processing details

Rake Tasks

Available terminology management rake tasks:

# Process all uncoded medicines (batch size: 100)
rake terminology:process_uncoded

# Process high-priority medicines (prescribed medicines first)
rake terminology:process_priority

# Process medicines by country
rake terminology:process_country COUNTRY=US

# Get statistics and available tasks
rake terminology:stats

# Generate detailed quality report
rake terminology:quality_report

# Export medicines for manual review (saves CSV to tmp/)
rake terminology:export_for_review

# Import manual coding corrections
rake terminology:import_corrections CORRECTIONS_FILE=path/to/corrections.csv

# Validate existing medicine codes
rake terminology:validate_codes

# Fix common coding issues
rake terminology:fix_coding_issues

Implementation Notes

This API specification reflects the current implementation and has been tested with real data. All examples show actual request/response formats from the working system.

Current Limitations

  1. API Availability: Some external terminology APIs may be unavailable:

    • NRCES SNOMED (India): snowstorm.nrces.in is currently down
    • RxNorm (US): Active and functional
    • FDA NDC (US): Active and functional
  2. Coding Success Rate: Automatic coding success varies by medicine name complexity:

    • Generic/standard names: Higher success rate (>70% confidence)
    • Proprietary/complex names: Lower success rate (<30% confidence)
    • Manual review recommended for low-confidence results
  3. Response Format: Update endpoint returns full medicine object and coding summary (not just success message)

Best Practices

  1. Manual Review Workflow:

    • Filter by confidence_range=low to find medicines needing attention
    • Verify codes against official terminology sources
    • Use coding_confidence to indicate certainty of manual coding
  2. Batch Processing:

    • Use rake tasks for large-scale coding operations
    • Monitor Sidekiq queue to track progress
    • Check health endpoint to verify system status
  3. Error Handling:

    • Always check success field in responses
    • Handle validation errors gracefully with user feedback
    • Log failed coding attempts for investigation

On this page