Kulcare Docs
DeveloperCare plans

Careplan API Integration Documentation

Overview

This document provides comprehensive documentation for integrating the Careplan API into the prescription system. The prescription workflow consists of 6 tabs, each saving data to specific API endpoints.

Status: ✅ Complete - API fully integrated and operational

Table of Contents

  1. API Architecture
  2. URL Structure
  3. API Endpoints
  4. Data Transformations
  5. Implementation Plan
  6. Code Examples
  7. Testing Strategy

API Architecture

Key Design Decisions

  1. Two Separate Endpoints:

    • Vitals: Uses /patients/{patientId}/vitals endpoint (POST only)
    • All Other Tabs: Use /appointments/{appointmentId}/careplans endpoint
  2. Complete Data Submission:

    • Each save operation must send data for ALL sections, not just the modified tab
    • This ensures data consistency and prevents partial updates
  3. Optional Fields:

    • All fields in the prescription are optional
    • Allows progressive data entry across consultation
  4. Auto-Save Strategy:

    • 30-second auto-save interval when data is dirty
    • Visual loading indicators during save operations
    • No manual save buttons needed

URL Structure

/cms/prescription/{appointmentId}/{patientId}/queue?did={doctorId}
  • appointmentId: Primary identifier for the careplan (e.g., "30265")
  • patientId: Patient identifier for vitals endpoint (e.g., "23282")
  • doctorId: Doctor ID from query params (e.g., "87009")

API Endpoints

1. Get Existing Careplan

GET /appointments/{appointmentId}/careplans

Response Example:

{
  "id": 8118,
  "appointment_id": 6329,
  "complaint": [
    {
      "name": "Abdominal Distension",
      "since": "3",
      "sinceUnit": "Days"
    }
  ],
  "diagnosis": [
    {
      "name": "Abdominal pain",
      "note": "needs checkup"
    }
  ],
  "investigations": [],
  "medications": [],
  "advice": {
    "diet": ["Take rest", "Avoid cold items"],
    "dos": [],
    "donts": []
  },
  "follow_up": {
    "follow_up_date": "13-12-2024",
    "follow_up_message": "Checkup"
  }
}

2. Save Vitals (Tab 1)

Endpoint: POST /patients/{patientId}/vitals

Request Payload:

{
  "appointment_id": 6343,
  "weight": 12,
  "height": 100,
  "head_circumference": null,
  "mid_arm_circumference": null,
  "bmi": 12,
  "body_fat": null,
  "temperature": {
    "value": 98,
    "method": ""
  },
  "pulse": 80,
  "respiratory_rate": 18,
  "blood_pressure": {
    "systolic": 120,
    "diastolic": 80
  },
  "blood_sugar": null,
  "spo2": 98,
  "custom_vitals": []
}

3. Save Careplan (All Tabs)

Endpoint: POST /appointments/{appointmentId}/careplans

Request Payload Structure:

{
  "doctor_id": 87009,
  "clinic_id": 204745,
  "patient_id": 16955,
  "clinic_patient_id": 23282,
  "send_to_lab_id": null,
  "send_to_pharmacy_id": null,
  "careplan_details": {
    "complaints": [
      {
        "name": "Fever",
        "duration_value": 2,
        "duration_unit": "days"  // lowercase: "days", "weeks", "months", "years"
      }
    ],
    "diagnosis": [  // Array of strings, not objects
      "Viral Fever",
      "Upper Respiratory Infection"
    ],
    "test_list": [  // Not "investigations"
      {
        "tests": [
          {
            "basic_lab_test_id": 1515,  // or null for custom tests
            "name": "Complete Blood Count"
          }
        ],
        "notes": "Check for infection markers",
        "consultant_doctor_id": null,
        "consultant_doctor_name": null
      }
    ],
    "medicines": [  // Not "medications"
      {
        "basic_medicine_id": null,
        "name": "Paracetamol",
        "dosage": ["M", "E"],  // Array for timing
        "frequency": "1-0-1",  // Pattern format
        "duration_frequency": "days",  // lowercase
        "duration_unit": 5,  // Number
        "notes": "After Food",
        "alternate_medicine_name": "",
        "dose_quantity": "1",
        "dose_unit": "Tablet"
      }
    ],
    "advice": "{\"diet\":[\"fruits\",\"vegetables\"],\"dos\":[\"Rest\"],\"donts\":[]}",  // JSON string
    "diet": "",
    "notes": "Clinical notes here",
    "next_visit_value": 7,
    "next_visit_units": "Days",
    "next_visit_date": null,
    "is_final": false,
    "basic_language_id": 1
  }
}

Important Notes:

  • The entire payload must be wrapped in careplan_details object
  • Additional metadata fields (doctor_id, clinic_id, patient_id, etc.) are required at the root level
  • diagnosis is an array of strings, NOT objects
  • advice must be a JSON string, not an object
  • All duration units should be lowercase ("days" not "DAYS")
  • Field names: test_list (not investigations), medicines (not medications)

4. Example: Save Investigation Only

Request Payload (still requires full structure):

{
  "doctor_id": 87009,
  "clinic_id": 204745,
  "patient_id": 16955,
  "clinic_patient_id": 23282,
  "send_to_lab_id": null,
  "send_to_pharmacy_id": null,
  "careplan_details": {
    "complaints": [],
    "diagnosis": [],
    "test_list": [
      {
        "tests": [
          {
            "basic_lab_test_id": null,
            "name": "Complete Blood Count"
          },
          {
            "basic_lab_test_id": null,
            "name": "Chest X-Ray"
          }
        ],
        "notes": "Check for infection",
        "consultant_doctor_id": null,
        "consultant_doctor_name": null
      }
    ],
    "medicines": [],
    "advice": "{\"diet\":[],\"dos\":[],\"donts\":[]}",
    "diet": "",
    "notes": "",
    "next_visit_value": null,
    "next_visit_units": "Days",
    "next_visit_date": null,
    "is_final": false,
    "basic_language_id": 1
  }
}

5. Example: Save Medication Only

Request Payload (still requires full structure):

{
  "doctor_id": 87009,
  "clinic_id": 204745,
  "patient_id": 16955,
  "clinic_patient_id": 23282,
  "send_to_lab_id": null,
  "send_to_pharmacy_id": null,
  "careplan_details": {
    "complaints": [],
    "diagnosis": [],
    "test_list": [],
    "medicines": [
      {
        "basic_medicine_id": null,
        "name": "Augmentin 625 Duo Tablet",
        "dosage": ["M", "E"],
        "frequency": "1-0-1",
        "duration_frequency": "days",
        "duration_unit": 5,
        "notes": "After Food",
        "alternate_medicine_name": "",
        "dose_quantity": "1",
        "dose_unit": "Tablet"
      },
      {
        "basic_medicine_id": null,
        "name": "Supradyn Daily Multivitamin",
        "dosage": ["E"],
        "frequency": "0-0-1",
        "duration_frequency": "days",
        "duration_unit": 30,
        "notes": "After Food",
        "alternate_medicine_name": "",
        "dose_quantity": "1",
        "dose_unit": "Tablet"
      }
    ],
    "advice": "{\"diet\":[],\"dos\":[],\"donts\":[]}",
    "diet": "",
    "notes": "",
    "next_visit_value": null,
    "next_visit_units": "Days",
    "next_visit_date": null,
    "is_final": false,
    "basic_language_id": 1
  }
}

6. Example: Save Diet & Advice Only

Request Payload (still requires full structure):

{
  "doctor_id": 87009,
  "clinic_id": 204745,
  "patient_id": 16955,
  "clinic_patient_id": 23282,
  "send_to_lab_id": null,
  "send_to_pharmacy_id": null,
  "careplan_details": {
    "complaints": [],
    "diagnosis": [],
    "test_list": [],
    "medicines": [],
    "advice": "{\"diet\":[\"Increase water intake to 8-10 glasses per day\",\"Include fiber-rich foods\",\"Avoid spicy and oily food\"],\"dos\":[\"Regular exercise for 30 minutes daily\",\"Maintain proper sleep schedule\"],\"donts\":[\"Avoid smoking and alcohol\",\"Don't skip meals\"]}",
    "diet": "",
    "notes": "",
    "next_visit_value": null,
    "next_visit_units": "Days",
    "next_visit_date": null,
    "is_final": false,
    "basic_language_id": 1
  }
}

7. Example: Save Next Visit Only

Request Payload (still requires full structure):

{
  "doctor_id": 87009,
  "clinic_id": 204745,
  "patient_id": 16955,
  "clinic_patient_id": 23282,
  "send_to_lab_id": null,
  "send_to_pharmacy_id": null,
  "careplan_details": {
    "complaints": [],
    "diagnosis": [],
    "test_list": [],
    "medicines": [],
    "advice": "{\"diet\":[],\"dos\":[],\"donts\":[]}",
    "diet": "",
    "notes": "",
    "next_visit_value": 7,
    "next_visit_units": "Days",
    "next_visit_date": "25-12-2024",  // DD-MM-YYYY format
    "is_final": false,
    "basic_language_id": 1
  }
}

Data Transformations

Frontend to API Transformations

1. Frequency Mapping

const frequencyMap = {
  'od': 'DAILY',           // Once daily
  'bd': 'TWICE_DAILY',     // Twice daily
  'tds': 'THRICE_DAILY',   // Three times daily
  'qds': 'FOUR_TIMES',     // Four times daily
  'sos': 'AS_NEEDED',      // As needed
  'stat': 'IMMEDIATELY',   // Immediately
  'weekly': 'WEEKLY',      // Weekly
  'monthly': 'MONTHLY'     // Monthly
}

// API to Frontend (reverse mapping)
const apiToFrontendFrequency = {
  'DAILY': 'od',
  '1-0-0': 'od',
  '0-0-1': 'od',
  'TWICE_DAILY': 'bd',
  '1-0-1': 'bd',
  'THRICE_DAILY': 'tds',
  '1-1-1': 'tds',
  'FOUR_TIMES': 'qds',
  '1-1-1-1': 'qds'
}

2. Date Format Conversion

// Frontend (ISO) to API (DD-MM-YYYY)
const formatDateForAPI = (isoDate: string): string => {
  const date = new Date(isoDate);
  const day = date.getDate().toString().padStart(2, '0');
  const month = (date.getMonth() + 1).toString().padStart(2, '0');
  const year = date.getFullYear();
  return `${day}-${month}-${year}`;
}

// API (DD-MM-YYYY) to Frontend (ISO)
const parseAPIDate = (dateStr: string): string => {
  const [day, month, year] = dateStr.split('-');
  return new Date(`${year}-${month}-${day}`).toISOString();
}

3. Field Type Conversions

// Convert string IDs to numbers for API
const transformMedication = (med: Medicine): APIMediation => ({
  medicine_id: parseInt(med.id),
  medicine_name: med.name,
  frequency: med.frequency, // e.g., "1-0-1"
  duration: med.duration.toString(),
  duration_unit: med.durationUnit,
  dosage_form: med.route?.toUpperCase() || 'ORAL',
  note: med.instructions || '',
  qty: med.quantity?.toString() || ''
})

API to Frontend Transformations

const transformAPIResponse = (apiData: any): Prescription => {
  return {
    id: apiData.id?.toString(),
    appointmentId: apiData.appointment_id?.toString(),
    
    // Transform complaints to observations
    observations: {
      complaints: apiData.complaint?.map(c => ({
        id: generateId(),
        name: c.name,
        since: c.since,
        sinceUnit: c.sinceUnit
      })),
      diagnosis: apiData.diagnosis
    },
    
    // Transform investigations
    investigations: apiData.investigations?.map(inv => ({
      id: generateId(),
      testName: inv.name,
      clinicalNotes: inv.notes,
      category: 'general',
      status: 'ordered'
    })),
    
    // Transform medications
    medications: apiData.medications?.map(med => ({
      id: med.medicine_id?.toString(),
      name: med.medicine_name,
      frequency: parseFrequency(med.frequency),
      duration: parseInt(med.duration),
      durationUnit: med.duration_unit.toLowerCase(),
      route: med.dosage_form.toLowerCase(),
      instructions: med.note,
      quantity: parseInt(med.qty)
    })),
    
    // Transform advice
    advice: {
      dietaryAdvice: apiData.advice?.diet || [],
      lifestyleAdvice: apiData.advice?.dos || [],
      precautions: apiData.advice?.donts || []
    },
    
    // Transform follow-up
    nextVisit: apiData.follow_up ? {
      isRequired: !!apiData.follow_up.follow_up_date,
      date: apiData.follow_up.follow_up_date ? 
        parseAPIDate(apiData.follow_up.follow_up_date) : undefined,
      reason: apiData.follow_up.follow_up_message
    } : undefined
  }
}

Implementation Plan

Phase 1: API Service Layer (Day 1)

1.1 Create carePlanApi.ts

// apps/cms/src/services/carePlanApi.ts
import { apiClient } from "@kulcare/api-client"
import { Prescription, VitalsData } from "../types/prescription"

export const carePlanApi = {
  // Get existing careplan
  async getCareplan(appointmentId: string): Promise<any> {
    try {
      const response = await apiClient.get(
        `/appointments/${appointmentId}/careplans`
      )
      return response.data
    } catch (error) {
      // Return null if no careplan exists yet
      if (error.response?.status === 404) {
        return null
      }
      throw error
    }
  },

  // Save vitals (separate endpoint)
  async saveVitals(patientId: string, appointmentId: string, vitals: VitalsData): Promise<void> {
    const payload = transformVitalsForAPI(vitals, appointmentId)
    await apiClient.post(`/patients/${patientId}/vitals`, payload)
  },

  // Save careplan (all other tabs)
  async saveCareplan(appointmentId: string, prescription: Prescription): Promise<any> {
    const payload = transformPrescriptionForAPI(prescription)
    const response = await apiClient.post(
      `/appointments/${appointmentId}/careplans`,
      payload
    )
    return response.data
  }
}

Phase 2: Hook Integration (Day 2)

2.1 Update usePrescription Hook

// apps/cms/src/hooks/usePrescription.ts
export function usePrescription(appointmentId: string, patientId: string) {
  const [careplanId, setCareplanId] = useState<number | null>(null)
  const [isSaving, setIsSaving] = useState(false)
  const [lastSaved, setLastSaved] = useState<Date | null>(null)

  // Fetch existing careplan on mount
  const { data: existingCareplan } = useQuery({
    queryKey: ['careplan', appointmentId],
    queryFn: () => carePlanApi.getCareplan(appointmentId),
    staleTime: 5 * 60 * 1000 // 5 minutes
  })

  // Transform and set initial data
  useEffect(() => {
    if (existingCareplan) {
      setCareplanId(existingCareplan.id)
      const transformed = transformAPIResponse(existingCareplan)
      setLocalPrescription(transformed)
    }
  }, [existingCareplan])

  // Auto-save implementation
  const saveChanges = useCallback(async () => {
    if (!localPrescription || !isDirty) return
    
    setIsSaving(true)
    try {
      // Save vitals separately if changed
      if (dirtyTabs.has('vitals') && localPrescription.vitals) {
        await carePlanApi.saveVitals(patientId, appointmentId, localPrescription.vitals)
      }
      
      // Save complete careplan for all other changes
      const response = await carePlanApi.saveCareplan(appointmentId, localPrescription)
      
      // Store careplan ID if this is first save
      if (!careplanId && response.id) {
        setCareplanId(response.id)
      }
      
      setLastSaved(new Date())
      setIsDirty(false)
      toast.success('Changes saved')
    } catch (error) {
      toast.error('Failed to save changes')
      console.error('Save error:', error)
    } finally {
      setIsSaving(false)
    }
  }, [localPrescription, isDirty, careplanId])

  // Auto-save timer (30 seconds)
  useEffect(() => {
    if (!isDirty) return
    
    const timer = setTimeout(() => {
      saveChanges()
    }, 30000) // 30 seconds
    
    return () => clearTimeout(timer)
  }, [isDirty, saveChanges])

  return {
    prescription: localPrescription,
    updateSection,
    saveChanges,
    isSaving,
    lastSaved,
    isDirty
  }
}

Phase 3: UI Updates (Day 3)

3.1 Add Auto-Save Indicators

// apps/cms/src/components/prescription/PrescriptionTabs.tsx
export function PrescriptionTabs({ isSaving, lastSaved, isDirty }) {
  return (
    <div className="flex-1">
      {/* Tab content */}
      
      {/* Auto-save indicator */}
      <div className="fixed bottom-4 right-4 flex items-center gap-2">
        {isSaving && (
          <div className="flex items-center gap-2 bg-white shadow-lg rounded-lg px-3 py-2">
            <Loader2 className="h-4 w-4 animate-spin" />
            <span className="text-sm">Saving...</span>
          </div>
        )}
        
        {!isSaving && lastSaved && !isDirty && (
          <div className="flex items-center gap-2 text-green-600 bg-white shadow-lg rounded-lg px-3 py-2">
            <Check className="h-4 w-4" />
            <span className="text-sm">
              Saved {formatRelativeTime(lastSaved)}
            </span>
          </div>
        )}
        
        {isDirty && !isSaving && (
          <div className="text-sm text-muted-foreground bg-white shadow-lg rounded-lg px-3 py-2">
            Unsaved changes
          </div>
        )}
      </div>
    </div>
  )
}

Testing Strategy

1. Unit Tests

// Test data transformations
describe('Data Transformations', () => {
  it('should transform frontend prescription to API format', () => {
    const frontend = createMockPrescription()
    const api = transformPrescriptionForAPI(frontend)
    
    expect(api.complaint).toEqual([])
    expect(api.medications).toHaveLength(frontend.medications.length)
    expect(api.advice.diet).toEqual(frontend.advice.dietaryAdvice)
  })
  
  it('should transform API response to frontend format', () => {
    const apiResponse = createMockAPIResponse()
    const frontend = transformAPIResponse(apiResponse)
    
    expect(frontend.id).toBe(apiResponse.id.toString())
    expect(frontend.observations.complaints).toHaveLength(apiResponse.complaint.length)
  })
})

2. Integration Tests

// Test complete save flow
describe('Careplan Save Flow', () => {
  it('should save vitals to correct endpoint', async () => {
    const { result } = renderHook(() => usePrescription('123', '456'))
    
    act(() => {
      result.current.updateSection('vitals', mockVitals)
    })
    
    await act(async () => {
      await result.current.saveChanges()
    })
    
    expect(apiClient.post).toHaveBeenCalledWith(
      '/patients/456/vitals',
      expect.objectContaining({ appointment_id: 123 })
    )
  })
  
  it('should auto-save after 30 seconds', async () => {
    jest.useFakeTimers()
    const { result } = renderHook(() => usePrescription('123', '456'))
    
    act(() => {
      result.current.updateSection('medications', mockMedications)
    })
    
    act(() => {
      jest.advanceTimersByTime(30000)
    })
    
    expect(apiClient.post).toHaveBeenCalledWith(
      '/appointments/123/careplans',
      expect.any(Object)
    )
  })
})

3. E2E Tests

// Test complete prescription workflow
describe('Prescription Workflow', () => {
  it('should complete full prescription flow', async () => {
    // Navigate to prescription page
    await page.goto('/cms/prescription/123/456/queue?did=789')
    
    // Fill vitals
    await page.click('[data-tab="vitals"]')
    await page.fill('[name="weight"]', '70')
    await page.fill('[name="height"]', '170')
    
    // Wait for auto-save
    await page.waitForSelector('text=Saved', { timeout: 35000 })
    
    // Fill medications
    await page.click('[data-tab="medication"]')
    await page.click('[data-add-medication]')
    await page.fill('[name="medicine_name"]', 'Paracetamol')
    
    // Complete consultation
    await page.click('[data-complete-consultation]')
    await page.waitForURL('/cms/queue')
  })
})

Error Handling

Network Errors

const saveWithRetry = async (fn: () => Promise<any>, maxRetries = 3) => {
  let lastError
  
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn()
    } catch (error) {
      lastError = error
      
      // Don't retry on validation errors
      if (error.response?.status === 400) {
        throw error
      }
      
      // Wait before retry with exponential backoff
      await new Promise(resolve => 
        setTimeout(resolve, Math.pow(2, i) * 1000)
      )
    }
  }
  
  throw lastError
}

Validation Errors

const handleValidationError = (error: any) => {
  const validationErrors = error.response?.data?.errors || {}
  
  Object.entries(validationErrors).forEach(([field, messages]) => {
    toast.error(`${field}: ${messages.join(', ')}`)
  })
}

Concurrent Edit Protection

const checkForConcurrentEdits = async (appointmentId: string, lastModified: Date) => {
  const current = await carePlanApi.getCareplan(appointmentId)
  
  if (current && new Date(current.updated_at) > lastModified) {
    const shouldOverwrite = await confirm(
      'This prescription has been modified by another user. Overwrite their changes?'
    )
    
    if (!shouldOverwrite) {
      throw new Error('Concurrent edit detected')
    }
  }
}

Migration Strategy

Gradual Rollout

  1. Phase 1: Enable for new prescriptions only
  2. Phase 2: Migrate existing draft prescriptions
  3. Phase 3: Full rollout to all users

Feature Flag

const useCarePlanAPI = () => {
  const { flags } = useFeatureFlags()
  return flags.USE_CAREPLAN_API || false
}

// In usePrescription hook
const shouldUseRealAPI = useCarePlanAPI()

if (shouldUseRealAPI) {
  // Use real API
  return carePlanApi.getCareplan(appointmentId)
} else {
  // Use mock data
  return mockPrescriptionData
}

Performance Considerations

Debouncing Updates

const debouncedUpdate = useMemo(
  () => debounce((section: string, data: any) => {
    updateSection(section, data)
  }, 500),
  []
)

Optimistic Updates

const optimisticSave = async () => {
  // Update UI immediately
  setLastSaved(new Date())
  setIsSaving(false)
  
  try {
    // Save in background
    await saveChanges()
  } catch (error) {
    // Rollback on error
    setLastSaved(null)
    toast.error('Failed to save')
  }
}

Caching Strategy

const queryClient = useQueryClient()

// Cache careplan for 5 minutes
queryClient.setQueryDefaults(['careplan'], {
  staleTime: 5 * 60 * 1000,
  cacheTime: 10 * 60 * 1000
})

Monitoring and Analytics

Track Save Performance

const trackSaveMetrics = (startTime: number, success: boolean) => {
  const duration = Date.now() - startTime
  
  analytics.track('careplan_save', {
    duration,
    success,
    tab: currentTab,
    appointment_id: appointmentId
  })
}

Error Tracking

const logError = (error: any, context: any) => {
  console.error('Careplan save error:', error)
  
  // Send to error tracking service
  Sentry.captureException(error, {
    extra: context
  })
}

Success Criteria

  • ✅ All 6 tabs save to correct API endpoints
  • ✅ Auto-save triggers every 30 seconds when data is dirty
  • ✅ Loading indicators visible during save operations
  • ✅ No data loss on page refresh or navigation
  • ✅ Proper error handling with user-friendly messages
  • ✅ Complete consultation validates required fields
  • ✅ Performance: Save operations complete in <2 seconds
  • ✅ Reliability: 99.9% save success rate

References

On this page