penambahan All case Satu sehat
This commit is contained in:
No files matched your search
@@ -149,8 +149,17 @@ SATUSEHAT_CLIENT_ID=l1ZgJGW6K5pnrqGUikWM7fgIoquA2AQ5UUG0U8WqHaq2VEyZ
|
|||||||
SATUSEHAT_CLIENT_SECRET=Al3PTYAW6axPiAFwaFlpn8qShLFW5YGMgG8w1qhexgCc7lGTEjjcR6zxa06ThPDy
|
SATUSEHAT_CLIENT_SECRET=Al3PTYAW6axPiAFwaFlpn8qShLFW5YGMgG8w1qhexgCc7lGTEjjcR6zxa06ThPDy
|
||||||
SATUSEHAT_AUTH_URL=https://api-satusehat.kemkes.go.id/oauth2/v1
|
SATUSEHAT_AUTH_URL=https://api-satusehat.kemkes.go.id/oauth2/v1
|
||||||
SATUSEHAT_BASE_URL=https://api-satusehat.kemkes.go.id/fhir-r4/v1
|
SATUSEHAT_BASE_URL=https://api-satusehat.kemkes.go.id/fhir-r4/v1
|
||||||
SATUSEHAT_CONSENT_URL=https://api-satusehat.dto.kemkes.go.id/consent/v1
|
SATUSEHAT_CONSENT_URL=https://api-satusehat.kemkes.go.id/consent/v1
|
||||||
SATUSEHAT_KFA_URL=https://api-satusehat.kemkes.go.id/kfa-v2
|
SATUSEHAT_KFA_URL=https://api-satusehat.kemkes.go.id/kfa-v2
|
||||||
|
SATUSEHAT_KYC_URL=https://api-satusehat.kemkes.go.id/kyc/v1
|
||||||
|
SATUSEHAT_KYC_CHALLENGE_CODE_URL=https://api-satusehat.kemkes.go.id/kyc/v1/challenge-code
|
||||||
|
SATUSEHAT_WEBHOOK_SECRET=token_rahasia_webhook_anda_dari_portal_dto
|
||||||
|
SATUSEHAT_DICOM_URL=https://api-satusehat.kemkes.go.id/dicom/v1/dicomWeb/studies
|
||||||
|
|
||||||
|
# .env
|
||||||
|
KYC_PRIVATE_KEY_B64="LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBb01SNDFMVUxabWNiZURBRHVtN2UvQ2dWanRTc3ZNMmJrSHhsS1pwN0VQT2M1UGlICmprMW1MQTBsZ3BqNGg5RkxNY2VHbUZIcnk3QXFmdDlDUThPb0VsUy9HRUJIeFpmRTlndDhERTZxV0lEQW51VXAKeGJQVFVlNEFhVzBsTUZ5ZVF5eXZZaS9PdVR3VXJCN2M3Y1ZlYm0wTnBrREJ3RDJYRjhwdUFPTW1VeGFPdG9LdQpia2g1WWNVcVZ1enBYWStuTXFMK1FqMUF3UGJ3TlowREpyQlp3ejJYQ2EzU3k2bWtqaEJMV3JhZzFtc29oZUM3CmQ0S21oMDZEZ1JGeFBqMDJacVlGU2l2R1FXeWcvYlZ6bFBtSEpwYVMvem1HY0plcUdXTkg0VUMvQU81YThpNVgKbUo4UmJQVTBtVkxoZHpXcmZWNGFMVEVmLzlNQjRtTFc0VnMyTVFJREFRQUJBb0lCQUFXeWlzdnlmZFMvYUJHLwpoVjFOdlRNclhqNjNacWNEQkIybjYrdWdWdmJPQkdBWXR2Q2VDWFk3a2NvTjN1WnN6bFQxZXMyU2Z5aFMvaUdNCml0Z1ZFck55RzRyWjZkVTh4SUVaL1hlZjhORmx3RFhOWGloc2o5dFVJNEY0K2MrN3c0dlN4VVVsOTdYNHRTeVgKcENtY3hGVEdZbnpES0I2bWpDQ09wdXo2LzlaalRJOXBnN2VlMitEL0MxMjM0bTU1OWgvVVVBQjEzczduNytRbAo1MGpkcUE2WHpGZEM1Wlg1TUF3YkNNNU9kM0Y4TXdpM0EvVDlrazdtRTUwL0Z4cVRDQVhFZEUybFJZTnNsRUNBCnQ5d1NQUVNIQmw2OUg5L1Z4VS9oTk5oVkkzeUZMaHFKM0xBRk42b05UVU5VcTZFMzdvVE5ZUlBCbkJ3RkYvRysKRmF2VVA0RUNnWUVBMDdJTEF2MzNoUCsyMms0WHhyakQyMDI0NFJ4MFRJWjI3YXk3bEorT2lTemFtV1FzeGhFOApFN3YvTUpZTHF0T2hzU1VwYUxTZ04vQUxDWTJ1OXJ4WkNtM1JhM1EwZk9jNm5lbEZEUzhoVHJNZ3VVMFNaakgrCkJ2cXNpazM5Nm5uTHlVRFVKR3pITy85ekpxdW1uMHNkd2VsTGd0TFZkQnRGZWVqQnZTb2NJb0VDZ1lFQXdtbmYKRUxuS3pLWlkrZlNzNmptUGtiRWhZYnovWWFuUnFNbzVZK3F3MFN4T1dGVTIveVROOEpKMWh3MXBubytpNkNONQpqd255bTljNThNM3l3MmJkWnkxVmFwWmFJNTBoeE9ldGtYdmJsaGMyZExJVDRTcThzeENvaWM2elhnTi9IWU9rCkh2SExKc2J6YnE3S2d4Nmg3VWxUTjBXUGVHNk9yYnphRXhyajI3RUNnWUVBeGozb3lmaUNubTNKRWs0blBkVXcKdEdIb1RBYVZja1JKZ3ZGTENEY1RCNnFWV3lmUU5mdFZ3elUrR0lXcWoySjMvS2ZNUzEwNjNLZFI3clJic0h0WQo5c2M0WWVRRWN5TW1aVkpaYzZzN0NiUDlaUW1qdlVmN2x4V1FGa1JMRUJUV25iU3o5UmhZeXZyQkh1bUhzNHhvClpYclBWOGVRVkpabFRYQ0dYVnJkMWdFQ2dZRUF0enVoaWRvU1VaL1c3SDYzR21yMTdaTE1xNW05K1FaZEhJQzgKWm5lai93R0xOYVhiRjg1UWF4VWV3QkxjSVIydnBLc0t2czNaWXhnTysrNmZoNXREd1NMeEJteCt1R0k0bWFUaAo2ZVNhcVhOblY4VWZCM0dJT0J6VFBFOGNydGNNejN4ckN0OUpZVnJSeE5mbzludkFraXd4eWo0OVlueVE3UjY1ClNwaGxlWUVDZ1lCd25VQ2dOMnlwWUJ6V2FwaUpKRmwvcHBRS3ZQVjdPSmZHZVVESGgvZVlhWC9IaVlDY3N3LzkKU1IxMVVza1AwMGRSakt5UERtTG1Odlh5QmRaMDFJTHhOZmFpdXBxNkNWNFlXVThRdTFtb1lMYkc4ZHJjTEpVTApuL1pMY3hycitOS2FNUzZOSzZhSWpSd2FFeVhacWUzdEM5d20xWGdRcVcyYmdHV2E4eU5FL2c9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo="
|
||||||
|
KYC_PUBLIC_KEY_B64="LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFvTVI0MUxVTFptY2JlREFEdW03ZQovQ2dWanRTc3ZNMmJrSHhsS1pwN0VQT2M1UGlIamsxbUxBMGxncGo0aDlGTE1jZUdtRkhyeTdBcWZ0OUNROE9vCkVsUy9HRUJIeFpmRTlndDhERTZxV0lEQW51VXB4YlBUVWU0QWFXMGxNRnllUXl5dllpL091VHdVckI3YzdjVmUKYm0wTnBrREJ3RDJYRjhwdUFPTW1VeGFPdG9LdWJraDVZY1VxVnV6cFhZK25NcUwrUWoxQXdQYndOWjBESnJCWgp3ejJYQ2EzU3k2bWtqaEJMV3JhZzFtc29oZUM3ZDRLbWgwNkRnUkZ4UGowMlpxWUZTaXZHUVd5Zy9iVnpsUG1ICkpwYVMvem1HY0plcUdXTkg0VUMvQU81YThpNVhtSjhSYlBVMG1WTGhkeldyZlY0YUxURWYvOU1CNG1MVzRWczIKTVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=="
|
||||||
|
|
||||||
|
|
||||||
# Swagger
|
# Swagger
|
||||||
SWAGGER_HOST=localhost:8096 # Use the mapped port for external access
|
SWAGGER_HOST=localhost:8096 # Use the mapped port for external access
|
||||||
|
|||||||
+106
-17
@@ -18,13 +18,31 @@ import (
|
|||||||
|
|
||||||
satuSehatAuth "service/internal/satusehat/reference/auth"
|
satuSehatAuth "service/internal/satusehat/reference/auth"
|
||||||
satuSehatKfa "service/internal/satusehat/reference/kfa"
|
satuSehatKfa "service/internal/satusehat/reference/kfa"
|
||||||
|
satuSehatKyc "service/internal/satusehat/reference/kyc"
|
||||||
satuSehatLocation "service/internal/satusehat/reference/location"
|
satuSehatLocation "service/internal/satusehat/reference/location"
|
||||||
satuSehatOrganization "service/internal/satusehat/reference/organization"
|
satuSehatOrganization "service/internal/satusehat/reference/organization"
|
||||||
satuSehatPatient "service/internal/satusehat/reference/patient"
|
satuSehatPatient "service/internal/satusehat/reference/patient"
|
||||||
satuSehatPractitioner "service/internal/satusehat/reference/practitioner"
|
satuSehatPractitioner "service/internal/satusehat/reference/practitioner"
|
||||||
|
"service/internal/satusehat/usecase/allergyintolerance"
|
||||||
|
"service/internal/satusehat/usecase/careplan"
|
||||||
|
clinicalimpression "service/internal/satusehat/usecase/clinicalImpression"
|
||||||
|
"service/internal/satusehat/usecase/composition"
|
||||||
satuSehatCondition "service/internal/satusehat/usecase/condition"
|
satuSehatCondition "service/internal/satusehat/usecase/condition"
|
||||||
|
"service/internal/satusehat/usecase/diagnosticreport"
|
||||||
satuSehatEncounter "service/internal/satusehat/usecase/encounter"
|
satuSehatEncounter "service/internal/satusehat/usecase/encounter"
|
||||||
|
"service/internal/satusehat/usecase/episodeofcare"
|
||||||
|
"service/internal/satusehat/usecase/imagingstudy"
|
||||||
|
"service/internal/satusehat/usecase/immunization"
|
||||||
|
"service/internal/satusehat/usecase/medication"
|
||||||
|
"service/internal/satusehat/usecase/medicationdispense"
|
||||||
|
"service/internal/satusehat/usecase/medicationrequest"
|
||||||
|
"service/internal/satusehat/usecase/medicationstatement"
|
||||||
|
"service/internal/satusehat/usecase/observation"
|
||||||
satuSehatProcedure "service/internal/satusehat/usecase/procedure"
|
satuSehatProcedure "service/internal/satusehat/usecase/procedure"
|
||||||
|
"service/internal/satusehat/usecase/questionnaireresponse"
|
||||||
|
"service/internal/satusehat/usecase/servicerequest"
|
||||||
|
"service/internal/satusehat/usecase/specimen"
|
||||||
|
"service/internal/satusehat/usecase/studies"
|
||||||
"service/pkg/logger"
|
"service/pkg/logger"
|
||||||
|
|
||||||
// roleComponent "service/internal/master/role/component"
|
// roleComponent "service/internal/master/role/component"
|
||||||
@@ -185,9 +203,79 @@ func main() {
|
|||||||
satuSehatProcedureRepo := satuSehatProcedure.NewRepository(satusehatClient)
|
satuSehatProcedureRepo := satuSehatProcedure.NewRepository(satusehatClient)
|
||||||
satuSehatProcedureSvc := satuSehatProcedure.NewService(satuSehatProcedureRepo)
|
satuSehatProcedureSvc := satuSehatProcedure.NewService(satuSehatProcedureRepo)
|
||||||
|
|
||||||
|
satuSehatKycRepo := satuSehatKyc.NewRepository(satusehatClient)
|
||||||
|
satuSehatKycSvc := satuSehatKyc.NewService(satuSehatKycRepo, cfg.SatuSehat)
|
||||||
|
|
||||||
|
// AllergyIntolerance
|
||||||
|
satuSehatAllergyIntoleranceRepo := allergyintolerance.NewRepository(satusehatClient)
|
||||||
|
satuSehatAllergyIntoleranceSvc := allergyintolerance.NewService(satuSehatAllergyIntoleranceRepo)
|
||||||
|
|
||||||
|
// CarePlan
|
||||||
|
satuSehatCarePlanRepo := careplan.NewRepository(satusehatClient)
|
||||||
|
satuSehatCarePlanSvc := careplan.NewService(satuSehatCarePlanRepo)
|
||||||
|
|
||||||
|
// ClinicalImpression
|
||||||
|
satuSehatClinicalImpressionRepo := clinicalimpression.NewRepository(satusehatClient)
|
||||||
|
satuSehatClinicalImpressionSvc := clinicalimpression.NewService(satuSehatClinicalImpressionRepo)
|
||||||
|
|
||||||
|
// Composition
|
||||||
|
satuSehatCompositionRepo := composition.NewRepository(satusehatClient)
|
||||||
|
satuSehatCompositionSvc := composition.NewService(satuSehatCompositionRepo)
|
||||||
|
|
||||||
satuSehatConditionRepo := satuSehatCondition.NewRepository(satusehatClient)
|
satuSehatConditionRepo := satuSehatCondition.NewRepository(satusehatClient)
|
||||||
satuSehatConditionSvc := satuSehatCondition.NewService(satuSehatConditionRepo)
|
satuSehatConditionSvc := satuSehatCondition.NewService(satuSehatConditionRepo)
|
||||||
|
|
||||||
|
// DiagnosticReport
|
||||||
|
satuSehatDiagnosticReportRepo := diagnosticreport.NewRepository(satusehatClient)
|
||||||
|
satuSehatDiagnosticReportSvc := diagnosticreport.NewService(satuSehatDiagnosticReportRepo)
|
||||||
|
|
||||||
|
// EpisodeOfCare
|
||||||
|
satuSehatEpisodeOfCareRepo := episodeofcare.NewRepository(satusehatClient)
|
||||||
|
satuSehatEpisodeOfCareSvc := episodeofcare.NewService(satuSehatEpisodeOfCareRepo)
|
||||||
|
|
||||||
|
// ImagingStudy
|
||||||
|
satuSehatImagingStudyRepo := imagingstudy.NewRepository(satusehatClient)
|
||||||
|
satuSehatImagingStudySvc := imagingstudy.NewService(satuSehatImagingStudyRepo)
|
||||||
|
|
||||||
|
// Immunization
|
||||||
|
satuSehatImmunizationRepo := immunization.NewRepository(satusehatClient)
|
||||||
|
satuSehatImmunizationSvc := immunization.NewService(satuSehatImmunizationRepo)
|
||||||
|
|
||||||
|
// Medication
|
||||||
|
satuSehatMedicationRepo := medication.NewRepository(satusehatClient)
|
||||||
|
satuSehatMedicationSvc := medication.NewService(satuSehatMedicationRepo)
|
||||||
|
|
||||||
|
// MedicationDispense
|
||||||
|
satuSehatMedicationDispenseRepo := medicationdispense.NewRepository(satusehatClient)
|
||||||
|
satuSehatMedicationDispenseSvc := medicationdispense.NewService(satuSehatMedicationDispenseRepo)
|
||||||
|
|
||||||
|
// MedicationRequest
|
||||||
|
satuSehatMedicationRequestRepo := medicationrequest.NewRepository(satusehatClient)
|
||||||
|
satuSehatMedicationRequestSvc := medicationrequest.NewService(satuSehatMedicationRequestRepo)
|
||||||
|
|
||||||
|
// MedicationStatement
|
||||||
|
satuSehatMedicationStatementRepo := medicationstatement.NewRepository(satusehatClient)
|
||||||
|
satuSehatMedicationStatementSvc := medicationstatement.NewService(satuSehatMedicationStatementRepo)
|
||||||
|
|
||||||
|
// Observation
|
||||||
|
satuSehatObservationRepo := observation.NewRepository(satusehatClient)
|
||||||
|
satuSehatObservationSvc := observation.NewService(satuSehatObservationRepo)
|
||||||
|
|
||||||
|
// QuestionnaireResponse
|
||||||
|
satuSehatQuestionnaireResponseRepo := questionnaireresponse.NewRepository(satusehatClient)
|
||||||
|
satuSehatQuestionnaireResponseSvc := questionnaireresponse.NewService(satuSehatQuestionnaireResponseRepo)
|
||||||
|
|
||||||
|
// ServiceRequest
|
||||||
|
satuSehatServiceRequestRepo := servicerequest.NewRepository(satusehatClient)
|
||||||
|
satuSehatServiceRequestSvc := servicerequest.NewService(satuSehatServiceRequestRepo)
|
||||||
|
|
||||||
|
// Specimen
|
||||||
|
satuSehatSpecimenRepo := specimen.NewRepository(satusehatClient)
|
||||||
|
satuSehatSpecimenSvc := specimen.NewService(satuSehatSpecimenRepo)
|
||||||
|
|
||||||
|
// DICOM Studies
|
||||||
|
satuSehatStudiesRepo := studies.NewRepository(satusehatClient)
|
||||||
|
satuSehatStudiesSvc := studies.NewService(satuSehatStudiesRepo)
|
||||||
// 6. Server Orchestration (Dual Protocol & Background Workers)
|
// 6. Server Orchestration (Dual Protocol & Background Workers)
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -236,26 +324,27 @@ func main() {
|
|||||||
Organization: satuSehatOrganizationSvc,
|
Organization: satuSehatOrganizationSvc,
|
||||||
Location: satuSehatLocationSvc,
|
Location: satuSehatLocationSvc,
|
||||||
KFA: satuSehatKfaSvc,
|
KFA: satuSehatKfaSvc,
|
||||||
|
KYC: satuSehatKycSvc,
|
||||||
Encounter: satuSehatEncounterSvc,
|
Encounter: satuSehatEncounterSvc,
|
||||||
Procedure: satuSehatProcedureSvc,
|
Procedure: satuSehatProcedureSvc,
|
||||||
Condition: satuSehatConditionSvc,
|
Condition: satuSehatConditionSvc,
|
||||||
// TODO: Inisialisasi service sisanya di sini saat repo & service usecase lainnya sudah Anda buat, contoh:
|
AllergyIntolerance: satuSehatAllergyIntoleranceSvc,
|
||||||
// AllergyIntolerance: allergySvc,
|
CarePlan: satuSehatCarePlanSvc,
|
||||||
// CarePlan: careplanSvc,
|
ClinicalImpression: satuSehatClinicalImpressionSvc,
|
||||||
// ClinicalImpression: clinicalImpressionSvc,
|
Composition: satuSehatCompositionSvc,
|
||||||
// Composition: compositionSvc,
|
DiagnosticReport: satuSehatDiagnosticReportSvc,
|
||||||
// DiagnosticReport: diagnosticReportSvc,
|
EpisodeOfCare: satuSehatEpisodeOfCareSvc,
|
||||||
// EpisodeOfCare: episodeOfCareSvc,
|
ImagingStudy: satuSehatImagingStudySvc,
|
||||||
// ImagingStudy: imagingStudySvc,
|
Immunization: satuSehatImmunizationSvc,
|
||||||
// Immunization: immunizationSvc,
|
Medication: satuSehatMedicationSvc,
|
||||||
// Medication: medicationSvc,
|
MedicationDispense: satuSehatMedicationDispenseSvc,
|
||||||
// MedicationDispense: medicationDispenseSvc,
|
MedicationRequest: satuSehatMedicationRequestSvc,
|
||||||
// MedicationRequest: medicationRequestSvc,
|
MedicationStatement: satuSehatMedicationStatementSvc,
|
||||||
// MedicationStatement: medicationStatementSvc,
|
Observation: satuSehatObservationSvc,
|
||||||
// Observation: observationSvc,
|
QuestionnaireResponse: satuSehatQuestionnaireResponseSvc,
|
||||||
// QuestionnaireResponse: questionnaireResponseSvc,
|
ServiceRequest: satuSehatServiceRequestSvc,
|
||||||
// ServiceRequest: serviceRequestSvc,
|
Specimen: satuSehatSpecimenSvc,
|
||||||
// Specimen: specimenSvc,
|
DicomStudies: satuSehatStudiesSvc,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ networks:
|
|||||||
external: true
|
external: true
|
||||||
name: service-general_default
|
name: service-general_default
|
||||||
# Shared network for cross-container communication
|
# Shared network for cross-container communication
|
||||||
shared-simrs-network:
|
# shared-simrs-network:
|
||||||
external: true
|
# external: true
|
||||||
name: shared-simrs-network
|
# name: shared-simrs-network
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app-dev:
|
app-dev:
|
||||||
@@ -37,5 +37,5 @@ services:
|
|||||||
# - redis # Redis is managed separately
|
# - redis # Redis is managed separately
|
||||||
networks:
|
networks:
|
||||||
- general-app-network
|
- general-app-network
|
||||||
- shared-simrs-network
|
# - shared-simrs-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -17,7 +17,6 @@ require (
|
|||||||
github.com/gin-contrib/cors v1.7.6
|
github.com/gin-contrib/cors v1.7.6
|
||||||
github.com/go-playground/validator/v10 v10.27.0
|
github.com/go-playground/validator/v10 v10.27.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||||
github.com/golang/protobuf v1.5.4
|
|
||||||
github.com/jmoiron/sqlx v1.4.0
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
github.com/karincake/apem v0.0.18
|
github.com/karincake/apem v0.0.18
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
@@ -33,7 +32,6 @@ require (
|
|||||||
go.mongodb.org/mongo-driver v1.17.6
|
go.mongodb.org/mongo-driver v1.17.6
|
||||||
golang.org/x/time v0.15.0
|
golang.org/x/time v0.15.0
|
||||||
google.golang.org/grpc v1.78.0
|
google.golang.org/grpc v1.78.0
|
||||||
google.golang.org/protobuf v1.36.11
|
|
||||||
gorm.io/driver/mysql v1.6.0
|
gorm.io/driver/mysql v1.6.0
|
||||||
gorm.io/driver/sqlite v1.6.0
|
gorm.io/driver/sqlite v1.6.0
|
||||||
gorm.io/driver/sqlserver v1.6.3
|
gorm.io/driver/sqlserver v1.6.3
|
||||||
@@ -100,6 +98,7 @@ require (
|
|||||||
github.com/prometheus/procfs v0.16.1 // indirect
|
github.com/prometheus/procfs v0.16.1 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
github.com/rs/xid v1.6.0 // indirect
|
github.com/rs/xid v1.6.0 // indirect
|
||||||
|
github.com/suyashkumar/dicom v1.1.0 // indirect
|
||||||
github.com/tinylib/msgp v1.6.1 // indirect
|
github.com/tinylib/msgp v1.6.1 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
@@ -110,11 +109,13 @@ require (
|
|||||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/arch v0.20.0 // indirect
|
golang.org/x/arch v0.20.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect
|
||||||
golang.org/x/mod v0.34.0 // indirect
|
golang.org/x/mod v0.34.0 // indirect
|
||||||
golang.org/x/net v0.52.0 // indirect
|
golang.org/x/net v0.52.0 // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
golang.org/x/tools v0.43.0 // indirect
|
golang.org/x/tools v0.43.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
@@ -240,6 +240,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
|||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/suyashkumar/dicom v1.1.0 h1:AG+N/aQnD+jzkFuFzz2wO401qXI8KnNcYGQgvTBr9LA=
|
||||||
|
github.com/suyashkumar/dicom v1.1.0/go.mod h1:8Yw14x/0r4fXVnutbCJpF3HiLVbgMS1DQ2HpfbDjq8Y=
|
||||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||||
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
|
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
|
||||||
@@ -299,6 +301,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
|
|||||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
|
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4=
|
||||||
|
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ func (s *service) Login(ctx context.Context, req LoginRequest) (*LoginResponse,
|
|||||||
"[email protected]": {"password123", "1001", "admin", "Admin Dummy"},
|
"[email protected]": {"password123", "1001", "admin", "Admin Dummy"},
|
||||||
"[email protected]": {"password123", "1002", "user", "User Dummy"},
|
"[email protected]": {"password123", "1002", "user", "User Dummy"},
|
||||||
"[email protected]": {"password123", "1003", "manager", "Manager Dummy"},
|
"[email protected]": {"password123", "1003", "manager", "Manager Dummy"},
|
||||||
|
"[email protected]": {"password123", "1004", "worker", "Worker Dummy"},
|
||||||
}
|
}
|
||||||
|
|
||||||
user, exists := dummyUsers[req.Email]
|
user, exists := dummyUsers[req.Email]
|
||||||
|
|||||||
@@ -133,6 +133,11 @@ type SatuSehatConfig struct {
|
|||||||
BaseURL string `mapstructure:"base_url"`
|
BaseURL string `mapstructure:"base_url"`
|
||||||
ConsentURL string `mapstructure:"consent_url"`
|
ConsentURL string `mapstructure:"consent_url"`
|
||||||
KFAURL string `mapstructure:"kfa_url"`
|
KFAURL string `mapstructure:"kfa_url"`
|
||||||
|
KYCURL string `mapstructure:"kyc_url"`
|
||||||
|
DicomURL string `mapstructure:"dicom_url"`
|
||||||
|
KYCPublicKeyB64 string `mapstructure:"kyc_public_key_b64"`
|
||||||
|
KYCPrivateKeyB64 string `mapstructure:"kyc_private_key_b64"`
|
||||||
|
WebhookSecret string `mapstructure:"webhook_secret"`
|
||||||
Timeout time.Duration `mapstructure:"timeout"`
|
Timeout time.Duration `mapstructure:"timeout"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,6 +285,11 @@ func LoadConfig() *Config {
|
|||||||
BaseURL: getEnv("SATUSEHAT_BASE_URL", getEnv("BRIDGING_SATUSEHAT_BASE_URL", "https://api-satusehat.kemkes.go.id/fhir-r4/v1")),
|
BaseURL: getEnv("SATUSEHAT_BASE_URL", getEnv("BRIDGING_SATUSEHAT_BASE_URL", "https://api-satusehat.kemkes.go.id/fhir-r4/v1")),
|
||||||
ConsentURL: getEnv("SATUSEHAT_CONSENT_URL", getEnv("BRIDGING_SATUSEHAT_CONSENT_URL", "https://api-satusehat.dto.kemkes.go.id/consent/v1")),
|
ConsentURL: getEnv("SATUSEHAT_CONSENT_URL", getEnv("BRIDGING_SATUSEHAT_CONSENT_URL", "https://api-satusehat.dto.kemkes.go.id/consent/v1")),
|
||||||
KFAURL: getEnv("SATUSEHAT_KFA_URL", getEnv("BRIDGING_SATUSEHAT_KFA_URL", "https://api-satusehat.kemkes.go.id/kfa-v2")),
|
KFAURL: getEnv("SATUSEHAT_KFA_URL", getEnv("BRIDGING_SATUSEHAT_KFA_URL", "https://api-satusehat.kemkes.go.id/kfa-v2")),
|
||||||
|
KYCURL: getEnv("SATUSEHAT_KYC_URL", getEnv("BRIDGING_SATUSEHAT_KYC_URL", "https://api-satusehat.dto.kemkes.go.id/kyc/v1")),
|
||||||
|
DicomURL: getEnv("SATUSEHAT_DICOM_URL", getEnv("BRIDGING_SATUSEHAT_DICOM_URL", "https://api-satusehat.kemkes.go.id/dicom/v1/dicomWeb/studies")),
|
||||||
|
KYCPublicKeyB64: getEnv("KYC_PUBLIC_KEY_B64", ""),
|
||||||
|
KYCPrivateKeyB64: getEnv("KYC_PRIVATE_KEY_B64", ""),
|
||||||
|
WebhookSecret: getEnv("SATUSEHAT_WEBHOOK_SECRET", getEnv("BRIDGING_SATUSEHAT_WEBHOOK_SECRET", "")),
|
||||||
Timeout: parseDuration(getEnv("SATUSEHAT_TIMEOUT", getEnv("BRIDGING_SATUSEHAT_TIMEOUT", "30s"))),
|
Timeout: parseDuration(getEnv("SATUSEHAT_TIMEOUT", getEnv("BRIDGING_SATUSEHAT_TIMEOUT", "30s"))),
|
||||||
},
|
},
|
||||||
Swagger: SwaggerConfig{
|
Swagger: SwaggerConfig{
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package reference
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"service/internal/infrastructure/transport/http/middleware"
|
||||||
|
"service/internal/satusehat/reference/kyc"
|
||||||
|
"service/pkg/crypto"
|
||||||
|
"service/pkg/response"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KYCHandler struct {
|
||||||
|
service kyc.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKYCHandler(service kyc.Service) *KYCHandler {
|
||||||
|
return &KYCHandler{
|
||||||
|
service: service,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateKeys godoc
|
||||||
|
//
|
||||||
|
// @Summary Generate Static RSA Keys for KYC
|
||||||
|
// @Description Menghasilkan pasangan kunci RSA 2048-bit dalam format Base64 untuk disimpan di .env (KYC_PRIVATE_KEY_B64 & KYC_PUBLIC_KEY_B64)
|
||||||
|
// @Tags Satu Sehat - KYC
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Router /satusehat/reference/kyc/generate-keys [get]
|
||||||
|
func (h *KYCHandler) GenerateKeys(c *gin.Context) {
|
||||||
|
privPEM, pubPEM, err := crypto.GenerateRSAKeyPair(2048)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, http.StatusInternalServerError, "Gagal membuat RSA Keypair", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData := map[string]interface{}{
|
||||||
|
"instructions": "Salin nilai di bawah ini dan masukkan ke dalam file .env Anda",
|
||||||
|
"KYC_PRIVATE_KEY_B64": base64.StdEncoding.EncodeToString([]byte(privPEM)),
|
||||||
|
"KYC_PUBLIC_KEY_B64": base64.StdEncoding.EncodeToString([]byte(pubPEM)),
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, http.StatusOK, "Berhasil membuat kunci RSA Statis", responseData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateURL godoc
|
||||||
|
//
|
||||||
|
// @Summary Generate KYC URL
|
||||||
|
// @Description Membuat link validasi KYC Satu Sehat
|
||||||
|
// @Tags Satu Sehat - KYC
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body kyc.GenerateURLRequest true "Payload KYC"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Router /satusehat/reference/kyc/generate-url [post]
|
||||||
|
// @Security BearerAuth
|
||||||
|
func (h *KYCHandler) GenerateURL(c *gin.Context) {
|
||||||
|
var req kyc.GenerateURLRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.Error(c, http.StatusBadRequest, "Format request tidak valid", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Public Key statis otomatis disisipkan oleh layer Service yang membaca dari konfigurasi .env
|
||||||
|
|
||||||
|
data, err := h.service.GenerateURL(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
handleSatuSehatError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData := map[string]interface{}{
|
||||||
|
"kyc_data": data,
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, http.StatusOK, "Berhasil membuat link KYC", responseData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback godoc
|
||||||
|
//
|
||||||
|
// @Summary Webhook/Callback KYC
|
||||||
|
// @Description Menerima notifikasi dari Satu Sehat setelah proses KYC selesai
|
||||||
|
// @Tags Satu Sehat - KYC
|
||||||
|
// @Accept text/plain
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body string true "Payload Webhook Terenkripsi (-----BEGIN ENCRYPTED MESSAGE-----...)"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Router /satusehat/reference/kyc/callback [post]
|
||||||
|
func (h *KYCHandler) Callback(c *gin.Context) {
|
||||||
|
// Ekstrak token dari header Authorization (Format: "Bearer <token>")
|
||||||
|
authHeader := c.GetHeader("Authorization")
|
||||||
|
token := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
|
||||||
|
|
||||||
|
// Payload dari webhook Kemenkes berupa string terenkripsi (bukan JSON langsung)
|
||||||
|
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, http.StatusBadRequest, "Gagal membaca body request", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
encryptedPayload := string(bodyBytes)
|
||||||
|
|
||||||
|
if err := h.service.HandleCallback(c.Request.Context(), encryptedPayload, token); err != nil {
|
||||||
|
handleSatuSehatError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.Success(c, http.StatusOK, "Callback berhasil diproses", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *KYCHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||||
|
group := router.Group("/satusehat/reference/kyc")
|
||||||
|
{
|
||||||
|
group.GET("/generate-keys", h.GenerateKeys)
|
||||||
|
group.POST("/generate-url", h.GenerateURL)
|
||||||
|
// Rate limit khusus untuk endpoint callback: max 2 Request/Detik, Burst Limit 5
|
||||||
|
group.POST("/callback", middleware.MemoryRateLimitMiddleware(2.0, 5), h.Callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -25,7 +25,7 @@ func (h *CarePlanHandler) RegisterRoutes(router *gin.RouterGroup) {
|
|||||||
func (h *CarePlanHandler) Create(c *gin.Context) {
|
func (h *CarePlanHandler) Create(c *gin.Context) {
|
||||||
var req careplan.CarePlanRequest
|
var req careplan.CarePlanRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
|
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", validator.TranslateError(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res, err := h.service.Create(c.Request.Context(), req)
|
res, err := h.service.Create(c.Request.Context(), req)
|
||||||
@@ -51,7 +51,7 @@ func (h *CarePlanHandler) Update(c *gin.Context) {
|
|||||||
func (h *CarePlanHandler) Patch(c *gin.Context) {
|
func (h *CarePlanHandler) Patch(c *gin.Context) {
|
||||||
var req careplan.CarePlanPatchRequest
|
var req careplan.CarePlanPatchRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid patch", validator.TranslateError(err))
|
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", validator.TranslateError(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res, err := h.service.Patch(c.Request.Context(), c.Param("id"), req)
|
res, err := h.service.Patch(c.Request.Context(), c.Param("id"), req)
|
||||||
|
|||||||
+3
-3
@@ -27,7 +27,7 @@ func (h *DiagnosticReportHandler) RegisterRoutes(router *gin.RouterGroup) {
|
|||||||
func (h *DiagnosticReportHandler) Create(c *gin.Context) {
|
func (h *DiagnosticReportHandler) Create(c *gin.Context) {
|
||||||
var req diagnosticreport.DiagnosticReportRequest
|
var req diagnosticreport.DiagnosticReportRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
|
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format payload JSON tidak valid", validator.TranslateError(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res, err := h.service.Create(c.Request.Context(), req)
|
res, err := h.service.Create(c.Request.Context(), req)
|
||||||
@@ -40,7 +40,7 @@ func (h *DiagnosticReportHandler) Create(c *gin.Context) {
|
|||||||
func (h *DiagnosticReportHandler) Update(c *gin.Context) {
|
func (h *DiagnosticReportHandler) Update(c *gin.Context) {
|
||||||
var req diagnosticreport.DiagnosticReportRequest
|
var req diagnosticreport.DiagnosticReportRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
|
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format payload JSON tidak valid", validator.TranslateError(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res, err := h.service.Update(c.Request.Context(), c.Param("id"), req)
|
res, err := h.service.Update(c.Request.Context(), c.Param("id"), req)
|
||||||
@@ -53,7 +53,7 @@ func (h *DiagnosticReportHandler) Update(c *gin.Context) {
|
|||||||
func (h *DiagnosticReportHandler) Patch(c *gin.Context) {
|
func (h *DiagnosticReportHandler) Patch(c *gin.Context) {
|
||||||
var req diagnosticreport.DiagnosticReportPatchRequest
|
var req diagnosticreport.DiagnosticReportPatchRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid patch", validator.TranslateError(err))
|
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format patch payload tidak valid", validator.TranslateError(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res, err := h.service.Patch(c.Request.Context(), c.Param("id"), req)
|
res, err := h.service.Patch(c.Request.Context(), c.Param("id"), req)
|
||||||
|
|||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
package usecase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"service/internal/satusehat/usecase/studies"
|
||||||
|
"service/pkg/response"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DicomStudiesHandler struct {
|
||||||
|
service studies.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDicomStudiesHandler(service studies.Service) *DicomStudiesHandler {
|
||||||
|
return &DicomStudiesHandler{
|
||||||
|
service: service,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *DicomStudiesHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||||
|
// URL akan menjadi: POST /api/v1/satusehat/dicom/studies
|
||||||
|
group := router.Group("/satusehat/dicom/studies")
|
||||||
|
{
|
||||||
|
group.POST("", h.Upload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *DicomStudiesHandler) Upload(c *gin.Context) {
|
||||||
|
// Ambil file dari request FormData dengan key "file"
|
||||||
|
file, err := c.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, http.StatusBadRequest, "File DICOM tidak ditemukan pada form-data key 'file'", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
openedFile, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, http.StatusInternalServerError, "Gagal membuka file DICOM sementara", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer openedFile.Close()
|
||||||
|
|
||||||
|
dicomBytes, err := io.ReadAll(openedFile)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, http.StatusInternalServerError, "Gagal membaca byte dari file DICOM", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.service.UploadDICOM(c.Request.Context(), dicomBytes)
|
||||||
|
if err != nil {
|
||||||
|
// Fungsi ini berasal dari encounter_handler.go di package yang sama
|
||||||
|
handleSatuSehatError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, http.StatusOK, "Berhasil mengunggah file DICOM ke SatuSehat STOW-RS", result.FullResponse)
|
||||||
|
}
|
||||||
+15
-16
@@ -3,7 +3,6 @@ package usecase
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
@@ -29,7 +28,7 @@ func (h *EncounterHandler) RegisterRoutes(router *gin.RouterGroup) {
|
|||||||
group := router.Group("/satusehat/encounter")
|
group := router.Group("/satusehat/encounter")
|
||||||
{
|
{
|
||||||
group.POST("", h.Create)
|
group.POST("", h.Create)
|
||||||
group.POST("/sync/:idxdaftar", h.SyncFromSIMRS)
|
// group.POST("/sync/:idxdaftar", h.SyncFromSIMRS)
|
||||||
group.GET("", h.Search)
|
group.GET("", h.Search)
|
||||||
group.GET("/:id", h.GetByID)
|
group.GET("/:id", h.GetByID)
|
||||||
group.PUT("/:id", h.Update)
|
group.PUT("/:id", h.Update)
|
||||||
@@ -132,18 +131,18 @@ func (h *EncounterHandler) Search(c *gin.Context) {
|
|||||||
response.Success(c, http.StatusOK, "Successfully retrieved Encounters", result.FullResponse)
|
response.Success(c, http.StatusOK, "Successfully retrieved Encounters", result.FullResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *EncounterHandler) SyncFromSIMRS(c *gin.Context) {
|
// func (h *EncounterHandler) SyncFromSIMRS(c *gin.Context) {
|
||||||
idxdaftarStr := c.Param("idxdaftar")
|
// idxdaftarStr := c.Param("idxdaftar")
|
||||||
idxdaftar, err := strconv.ParseInt(idxdaftarStr, 10, 64)
|
// idxdaftar, err := strconv.ParseInt(idxdaftarStr, 10, 64)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format idxdaftar tidak valid", nil)
|
// response.ErrorWithLog(c, err, http.StatusBadRequest, "Format idxdaftar tidak valid", nil)
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
|
||||||
result, err := h.service.SyncFromSIMRS(c.Request.Context(), idxdaftar)
|
// result, err := h.service.SyncFromSIMRS(c.Request.Context(), idxdaftar)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
handleSatuSehatError(c, err)
|
// handleSatuSehatError(c, err)
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
response.Success(c, http.StatusOK, "Successfully synced Encounter from SIMRS", result.FullResponse)
|
// response.Success(c, http.StatusOK, "Successfully synced Encounter from SIMRS", result.FullResponse)
|
||||||
}
|
// }
|
||||||
@@ -32,6 +32,7 @@ type ModuleHandlers struct {
|
|||||||
SatuSehatOrganization *satuSehatRefHttp.OrganizationHandler
|
SatuSehatOrganization *satuSehatRefHttp.OrganizationHandler
|
||||||
SatuSehatLocation *satuSehatRefHttp.LocationHandler
|
SatuSehatLocation *satuSehatRefHttp.LocationHandler
|
||||||
SatuSehatKFA *satuSehatRefHttp.KFAHandler
|
SatuSehatKFA *satuSehatRefHttp.KFAHandler
|
||||||
|
SatuSehatKYC *satuSehatRefHttp.KYCHandler
|
||||||
SSAllergyIntolerance *satuSehatUsecaseHttp.AllergyIntoleranceHandler
|
SSAllergyIntolerance *satuSehatUsecaseHttp.AllergyIntoleranceHandler
|
||||||
SSCarePlan *satuSehatUsecaseHttp.CarePlanHandler
|
SSCarePlan *satuSehatUsecaseHttp.CarePlanHandler
|
||||||
SSClinicalImpression *satuSehatUsecaseHttp.ClinicalImpressionHandler
|
SSClinicalImpression *satuSehatUsecaseHttp.ClinicalImpressionHandler
|
||||||
@@ -51,6 +52,7 @@ type ModuleHandlers struct {
|
|||||||
SSQuestionnaireResponse *satuSehatUsecaseHttp.QuestionnaireResponseHandler
|
SSQuestionnaireResponse *satuSehatUsecaseHttp.QuestionnaireResponseHandler
|
||||||
SSServiceRequest *satuSehatUsecaseHttp.ServiceRequestHandler
|
SSServiceRequest *satuSehatUsecaseHttp.ServiceRequestHandler
|
||||||
SSSpecimen *satuSehatUsecaseHttp.SpecimenHandler
|
SSSpecimen *satuSehatUsecaseHttp.SpecimenHandler
|
||||||
|
SSDicomStudies *satuSehatUsecaseHttp.DicomStudiesHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupRoutes mendaftarkan seluruh endpoint API secara dinamis berdasarkan
|
// SetupRoutes mendaftarkan seluruh endpoint API secara dinamis berdasarkan
|
||||||
@@ -128,7 +130,7 @@ func SetupRoutes(
|
|||||||
|
|
||||||
// Private routes (Membutuhkan Autentikasi)
|
// Private routes (Membutuhkan Autentikasi)
|
||||||
protected := v1.Group("")
|
protected := v1.Group("")
|
||||||
protected.Use(middleware.AuthMiddleware(cfg, cacheManager))
|
protected.Use(middleware.AuthMiddleware(cfg, cacheManager)) // Di-comment sementara untuk proses development/testing
|
||||||
{
|
{
|
||||||
if h.Auth != nil {
|
if h.Auth != nil {
|
||||||
h.Auth.RegisterProtectedRoutes(protected)
|
h.Auth.RegisterProtectedRoutes(protected)
|
||||||
@@ -168,6 +170,9 @@ func SetupRoutes(
|
|||||||
if h.SatuSehatKFA != nil {
|
if h.SatuSehatKFA != nil {
|
||||||
h.SatuSehatKFA.RegisterRoutes(satuSehatGroup)
|
h.SatuSehatKFA.RegisterRoutes(satuSehatGroup)
|
||||||
}
|
}
|
||||||
|
if h.SatuSehatKYC != nil {
|
||||||
|
h.SatuSehatKYC.RegisterRoutes(satuSehatGroup)
|
||||||
|
}
|
||||||
if h.SSAllergyIntolerance != nil {
|
if h.SSAllergyIntolerance != nil {
|
||||||
h.SSAllergyIntolerance.RegisterRoutes(satuSehatGroup)
|
h.SSAllergyIntolerance.RegisterRoutes(satuSehatGroup)
|
||||||
}
|
}
|
||||||
@@ -225,6 +230,9 @@ func SetupRoutes(
|
|||||||
if h.SSSpecimen != nil {
|
if h.SSSpecimen != nil {
|
||||||
h.SSSpecimen.RegisterRoutes(satuSehatGroup)
|
h.SSSpecimen.RegisterRoutes(satuSehatGroup)
|
||||||
}
|
}
|
||||||
|
if h.SSDicomStudies != nil {
|
||||||
|
h.SSDicomStudies.RegisterRoutes(satuSehatGroup)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
rolePermission "service/internal/master/role/permission"
|
rolePermission "service/internal/master/role/permission"
|
||||||
satuSehatAuth "service/internal/satusehat/reference/auth"
|
satuSehatAuth "service/internal/satusehat/reference/auth"
|
||||||
satuSehatKfa "service/internal/satusehat/reference/kfa"
|
satuSehatKfa "service/internal/satusehat/reference/kfa"
|
||||||
|
"service/internal/satusehat/reference/kyc"
|
||||||
satuSehatLocation "service/internal/satusehat/reference/location"
|
satuSehatLocation "service/internal/satusehat/reference/location"
|
||||||
satuSehatOrganization "service/internal/satusehat/reference/organization"
|
satuSehatOrganization "service/internal/satusehat/reference/organization"
|
||||||
satuSehatPatient "service/internal/satusehat/reference/patient"
|
satuSehatPatient "service/internal/satusehat/reference/patient"
|
||||||
@@ -36,6 +37,7 @@ import (
|
|||||||
"service/internal/satusehat/usecase/questionnaireresponse"
|
"service/internal/satusehat/usecase/questionnaireresponse"
|
||||||
"service/internal/satusehat/usecase/servicerequest"
|
"service/internal/satusehat/usecase/servicerequest"
|
||||||
"service/internal/satusehat/usecase/specimen"
|
"service/internal/satusehat/usecase/specimen"
|
||||||
|
"service/internal/satusehat/usecase/studies"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MasterServices menampung kumpulan service untuk domain Master Data
|
// MasterServices menampung kumpulan service untuk domain Master Data
|
||||||
@@ -53,6 +55,7 @@ type SatuSehatServices struct {
|
|||||||
Organization satuSehatOrganization.Service
|
Organization satuSehatOrganization.Service
|
||||||
Location satuSehatLocation.Service
|
Location satuSehatLocation.Service
|
||||||
KFA satuSehatKfa.Service
|
KFA satuSehatKfa.Service
|
||||||
|
KYC kyc.Service
|
||||||
|
|
||||||
// Usecase Services
|
// Usecase Services
|
||||||
AllergyIntolerance allergyintolerance.Service
|
AllergyIntolerance allergyintolerance.Service
|
||||||
@@ -74,6 +77,7 @@ type SatuSehatServices struct {
|
|||||||
QuestionnaireResponse questionnaireresponse.Service
|
QuestionnaireResponse questionnaireresponse.Service
|
||||||
ServiceRequest servicerequest.Service
|
ServiceRequest servicerequest.Service
|
||||||
Specimen specimen.Service
|
Specimen specimen.Service
|
||||||
|
DicomStudies studies.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceRegistry berfungsi sebagai Dependency Injection Container untuk transport HTTP.
|
// ServiceRegistry berfungsi sebagai Dependency Injection Container untuk transport HTTP.
|
||||||
|
|||||||
@@ -104,6 +104,9 @@ func NewHTTPServer(
|
|||||||
if registry.SatuSehat.KFA != nil {
|
if registry.SatuSehat.KFA != nil {
|
||||||
appHandlers.SatuSehatKFA = satuSehatRefHttp.NewKFAHandler(registry.SatuSehat.KFA)
|
appHandlers.SatuSehatKFA = satuSehatRefHttp.NewKFAHandler(registry.SatuSehat.KFA)
|
||||||
}
|
}
|
||||||
|
if registry.SatuSehat.KYC != nil {
|
||||||
|
appHandlers.SatuSehatKYC = satuSehatRefHttp.NewKYCHandler(registry.SatuSehat.KYC)
|
||||||
|
}
|
||||||
|
|
||||||
// Inisialisasi Handlers Modul Satu Sehat Usecase
|
// Inisialisasi Handlers Modul Satu Sehat Usecase
|
||||||
if registry.SatuSehat.AllergyIntolerance != nil {
|
if registry.SatuSehat.AllergyIntolerance != nil {
|
||||||
@@ -163,6 +166,9 @@ func NewHTTPServer(
|
|||||||
if registry.SatuSehat.Specimen != nil {
|
if registry.SatuSehat.Specimen != nil {
|
||||||
appHandlers.SSSpecimen = satuSehatUsecaseHttp.NewSpecimenHandler(registry.SatuSehat.Specimen)
|
appHandlers.SSSpecimen = satuSehatUsecaseHttp.NewSpecimenHandler(registry.SatuSehat.Specimen)
|
||||||
}
|
}
|
||||||
|
if registry.SatuSehat.DicomStudies != nil {
|
||||||
|
appHandlers.SSDicomStudies = satuSehatUsecaseHttp.NewDicomStudiesHandler(registry.SatuSehat.DicomStudies)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inisialisasi Health Handler dengan dependensi yang benar
|
// Inisialisasi Health Handler dengan dependensi yang benar
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/textproto"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -88,6 +90,16 @@ func (c *client) GetAccessToken(ctx context.Context) (map[string]interface{}, er
|
|||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
logger.Default().Error("SatuSehat Auth Error", logger.Int("status", resp.StatusCode), logger.String("response", string(respBody)))
|
logger.Default().Error("SatuSehat Auth Error", logger.Int("status", resp.StatusCode), logger.String("response", string(respBody)))
|
||||||
|
|
||||||
|
// Coba parse response body sebagai FHIR OperationOutcome untuk meneruskan status code asli ke client
|
||||||
|
var outcome map[string]interface{}
|
||||||
|
if err := json.Unmarshal(respBody, &outcome); err == nil && outcome["resourceType"] == "OperationOutcome" {
|
||||||
|
return nil, &ErrorOperationOutcome{
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
Outcome: outcome,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("SatuSehat Auth failed with status %d: %s", resp.StatusCode, string(respBody))
|
return nil, fmt.Errorf("SatuSehat Auth failed with status %d: %s", resp.StatusCode, string(respBody))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,21 +165,35 @@ func (c *client) do(ctx context.Context, baseURL, method, endpoint string, body
|
|||||||
endpointPath := strings.TrimLeft(endpoint, "/")
|
endpointPath := strings.TrimLeft(endpoint, "/")
|
||||||
targetURL := fmt.Sprintf("%s/%s", baseURL, endpointPath)
|
targetURL := fmt.Sprintf("%s/%s", baseURL, endpointPath)
|
||||||
|
|
||||||
// Marshal body terlebih dahulu agar bisa digunakan berulang kali jika butuh retry
|
// Identifikasi dan format body berdasarkan tipenya (string/plaintext atau struct/JSON)
|
||||||
var jsonBody []byte
|
var reqBody []byte
|
||||||
|
var contentType string
|
||||||
|
|
||||||
if body != nil {
|
if body != nil {
|
||||||
|
switch v := body.(type) {
|
||||||
|
case string:
|
||||||
|
// Jika body berupa string murni (seperti encrypted payload KYC), jangan di-marshal JSON
|
||||||
|
reqBody = []byte(v)
|
||||||
|
contentType = "text/plain"
|
||||||
|
default:
|
||||||
var err error
|
var err error
|
||||||
jsonBody, err = json.Marshal(body)
|
reqBody, err = json.Marshal(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to marshal request body: %w", err)
|
return nil, fmt.Errorf("failed to marshal request body: %w", err)
|
||||||
}
|
}
|
||||||
|
if method == "PATCH" {
|
||||||
|
contentType = "application/json-patch+json"
|
||||||
|
} else {
|
||||||
|
contentType = "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function untuk merakit ulang request (sangat berguna untuk skenario Retry)
|
// Helper function untuk merakit ulang request (sangat berguna untuk skenario Retry)
|
||||||
buildReq := func(accessToken string) (*http.Request, error) {
|
buildReq := func(accessToken string) (*http.Request, error) {
|
||||||
var bodyReader io.Reader
|
var bodyReader io.Reader
|
||||||
if jsonBody != nil {
|
if reqBody != nil {
|
||||||
bodyReader = bytes.NewBuffer(jsonBody)
|
bodyReader = bytes.NewBuffer(reqBody)
|
||||||
}
|
}
|
||||||
req, err := http.NewRequestWithContext(ctx, method, targetURL, bodyReader)
|
req, err := http.NewRequestWithContext(ctx, method, targetURL, bodyReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -175,12 +201,8 @@ func (c *client) do(ctx context.Context, baseURL, method, endpoint string, body
|
|||||||
}
|
}
|
||||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
if jsonBody != nil {
|
if reqBody != nil {
|
||||||
if method == "PATCH" {
|
req.Header.Set("Content-Type", contentType)
|
||||||
req.Header.Set("Content-Type", "application/json-patch+json")
|
|
||||||
} else {
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
@@ -272,3 +294,83 @@ func (c *client) DoKFA(ctx context.Context, method, endpoint string, body interf
|
|||||||
func (c *client) DoConsent(ctx context.Context, method, endpoint string, body interface{}) ([]byte, error) {
|
func (c *client) DoConsent(ctx context.Context, method, endpoint string, body interface{}) ([]byte, error) {
|
||||||
return c.do(ctx, c.cfg.ConsentURL, method, endpoint, body)
|
return c.do(ctx, c.cfg.ConsentURL, method, endpoint, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *client) DoKYC(ctx context.Context, method, endpoint string, body interface{}) ([]byte, error) {
|
||||||
|
return c.do(ctx, c.cfg.KYCURL, method, endpoint, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadDICOM mengunggah file biner .dcm langsung ke SatuSehat STOW-RS endpoint
|
||||||
|
func (c *client) UploadDICOM(ctx context.Context, dicomBytes []byte) ([]byte, error) {
|
||||||
|
if !c.cfg.Enabled {
|
||||||
|
return nil, fmt.Errorf("SatuSehat integration is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
authData, err := c.GetAccessToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get SatuSehat access token: %w", err)
|
||||||
|
}
|
||||||
|
token, _ := authData["access_token"].(string)
|
||||||
|
|
||||||
|
// 1. Buat body multipart
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
|
||||||
|
// 2. Buat header part khusus untuk DICOM (Kemenkes mewajibkan application/dicom)
|
||||||
|
h := make(textproto.MIMEHeader)
|
||||||
|
h.Set("Content-Type", "application/dicom")
|
||||||
|
|
||||||
|
part, err := writer.CreatePart(h)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("gagal membuat multipart frame: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Tulis biner DICOM ke dalam part
|
||||||
|
if _, err := part.Write(dicomBytes); err != nil {
|
||||||
|
return nil, fmt.Errorf("gagal menulis biner DICOM: %w", err)
|
||||||
|
}
|
||||||
|
writer.Close()
|
||||||
|
|
||||||
|
// 4. Siapkan HTTP Request
|
||||||
|
// NOTE: Content-Type untuk STOW-RS BUKAN multipart/form-data, TAPI multipart/related
|
||||||
|
contentType := fmt.Sprintf(`multipart/related; type="application/dicom"; boundary=%s`, writer.Boundary())
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", c.cfg.DicomURL, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create DICOM request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
req.Header.Set("Content-Type", contentType)
|
||||||
|
req.Header.Set("Accept", "application/dicom+json") // Format balasan Kemenkes
|
||||||
|
|
||||||
|
logger.Default().Info("🛫 Mengirim DICOM ke SatuSehat", logger.String("target_url", c.cfg.DicomURL))
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("http request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
logger.Default().Error("SatuSehat DICOM API Error",
|
||||||
|
logger.Int("status_code", resp.StatusCode),
|
||||||
|
logger.String("response", string(respBody)),
|
||||||
|
)
|
||||||
|
|
||||||
|
var outcome map[string]interface{}
|
||||||
|
if err := json.Unmarshal(respBody, &outcome); err == nil && outcome["resourceType"] == "OperationOutcome" {
|
||||||
|
return nil, &ErrorOperationOutcome{
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
Outcome: outcome,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("DICOM Upload Error (HTTP %d): %s", resp.StatusCode, string(respBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
return respBody, nil
|
||||||
|
}
|
||||||
@@ -18,6 +18,10 @@ type SatuSehatClient interface {
|
|||||||
DoKFA(ctx context.Context, method, endpoint string, body interface{}) ([]byte, error)
|
DoKFA(ctx context.Context, method, endpoint string, body interface{}) ([]byte, error)
|
||||||
// DoConsent mengirimkan HTTP request ke endpoint Consent (ConsentURL).
|
// DoConsent mengirimkan HTTP request ke endpoint Consent (ConsentURL).
|
||||||
DoConsent(ctx context.Context, method, endpoint string, body interface{}) ([]byte, error)
|
DoConsent(ctx context.Context, method, endpoint string, body interface{}) ([]byte, error)
|
||||||
|
// DoKYC mengirimkan HTTP request ke endpoint KYC (KYCURL).
|
||||||
|
DoKYC(ctx context.Context, method, endpoint string, body interface{}) ([]byte, error)
|
||||||
|
// UploadDICOM mengirimkan file biner .dcm menggunakan protokol STOW-RS (multipart/related).
|
||||||
|
UploadDICOM(ctx context.Context, dicomBytes []byte) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FHIRPayload adalah struktur data fleksibel berbentuk map untuk membangun JSON payload FHIR Satu Sehat.
|
// FHIRPayload adalah struktur data fleksibel berbentuk map untuk membangun JSON payload FHIR Satu Sehat.
|
||||||
|
|||||||
@@ -0,0 +1,461 @@
|
|||||||
|
# 📚 Dokumentasi Lengkap API Satu Sehat (GoPrint Builder)
|
||||||
|
|
||||||
|
**Base URL**: `http://localhost:8080/api/v1/satusehat` *(sesuaikan dengan host dan port environment Anda)*
|
||||||
|
**Otorisasi**: Membutuhkan Header `Authorization: Bearer <token_internal_aplikasi_anda>`
|
||||||
|
|
||||||
|
Dokumen ini memuat panduan lengkap penggunaan endpoint dan struktur **JSON Body (POST)** untuk modul referensi (Master Data) maupun modul usecase (Transaksional Klinis). Aplikasi GoPrint bertindak sebagai *proxy builder* yang akan mengonversi struktur JSON datar (*flat*) Anda menjadi format HL7 FHIR standar Kemenkes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 1. Otorisasi (Auth) Kemenkes
|
||||||
|
|
||||||
|
Aplikasi otomatis mengurus *Access Token* ke Kemenkes. Namun jika dibutuhkan secara manual, Anda bisa menggunakan:
|
||||||
|
|
||||||
|
- `GET /reference/auth/token` : Mendapatkan token aktif dari *cache* (digunakan oleh API internal).
|
||||||
|
- `POST /reference/auth/token/refresh` : Memaksa (*bypass cache*) untuk mengambil token baru.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏢 2. Modul Reference (Master Data)
|
||||||
|
|
||||||
|
Modul ini digunakan untuk meregistrasi entitas dasar. URL menggunakan prefix `/reference/{modul}`.
|
||||||
|
|
||||||
|
### A. Organization (Faskes / Departemen)
|
||||||
|
`POST /reference/organization`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"active": true,
|
||||||
|
"type_code": "prov",
|
||||||
|
"type_display": "Healthcare Provider",
|
||||||
|
"identifier_system": "http://sys-ids.kemkes.go.id/organization/10000004",
|
||||||
|
"identifier_value": "10000004",
|
||||||
|
"name": "RSUP Dr. Cipto Mangunkusumo",
|
||||||
|
"phone": "021-1500135",
|
||||||
|
"email": "[email protected]",
|
||||||
|
"url": "https://www.rscm.co.id/",
|
||||||
|
"address": "Jl. Diponegoro No.71, Kenari, Senen",
|
||||||
|
"city": "Jakarta Pusat",
|
||||||
|
"postal_code": "10430",
|
||||||
|
"country_code": "ID",
|
||||||
|
"part_of_id": "1000000"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### B. Location (Lokasi / Ruangan)
|
||||||
|
`POST /reference/location`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"identifier_system": "http://sys-ids.kemkes.go.id/location/10000004",
|
||||||
|
"identifier_value": "LOK-001",
|
||||||
|
"status": "active",
|
||||||
|
"name": "Poli Penyakit Dalam",
|
||||||
|
"description": "Poliklinik Penyakit Dalam Gedung A",
|
||||||
|
"physical_type_code": "ro",
|
||||||
|
"physical_type_display": "Room",
|
||||||
|
"managing_organization_id": "10000004",
|
||||||
|
"part_of_id": "b017aa54-f1df-4ec2-9d84-8823815d7228"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### C. Patient (Pasien)
|
||||||
|
`POST /reference/patient`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "BUDI SANTOSO",
|
||||||
|
"nik": "3573012345678901",
|
||||||
|
"ihs_number": "P0123456789",
|
||||||
|
"gender": "male",
|
||||||
|
"birth_date": "1990-12-31",
|
||||||
|
"phone": "08123456789",
|
||||||
|
"email": "[email protected]",
|
||||||
|
"is_active": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### D. Practitioner (Tenaga Medis)
|
||||||
|
`POST /reference/practitioner`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nik": "3301234567890123",
|
||||||
|
"name": "Dr. Siti Aminah, Sp.PD",
|
||||||
|
"gender": "female",
|
||||||
|
"birth_date": "1985-05-15",
|
||||||
|
"is_active": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🩺 3. Modul Usecase (Data Klinis)
|
||||||
|
|
||||||
|
Semua endpoint ini langsung menggunakan nama resource, contoh: `POST /encounter` atau `POST /condition`.
|
||||||
|
|
||||||
|
### 1. Encounter (Kunjungan)
|
||||||
|
Mencatat registrasi/kedatangan pasien ke faskes.
|
||||||
|
`POST /encounter`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"encounter_id": "KUNJ-001",
|
||||||
|
"organization_id": "10000004",
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"practitioner_id": "N10000001",
|
||||||
|
"practitioner_name": "Dr. Siti Aminah",
|
||||||
|
"location_id": "b017aa54-f1df-4ec2-9d84-8823815d7228",
|
||||||
|
"location_name": "Poli Umum",
|
||||||
|
"status": "arrived",
|
||||||
|
"class": "AMB",
|
||||||
|
"period_start": "2023-10-12T08:00:00+07:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*(Status valid: `planned`, `arrived`, `in-progress`, `finished`, `cancelled`)*
|
||||||
|
|
||||||
|
### 2. Condition (Diagnosis / Keluhan)
|
||||||
|
Mencatat hasil diagnosis ICD-10 pasien.
|
||||||
|
`POST /condition`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"clinical_status": "active",
|
||||||
|
"category_code": "encounter-diagnosis",
|
||||||
|
"category_display": "Encounter Diagnosis",
|
||||||
|
"code": "J06.9",
|
||||||
|
"display": "Acute upper respiratory infection, unspecified",
|
||||||
|
"onset_date_time": "2023-10-12T08:15:00+07:00",
|
||||||
|
"recorded_date": "2023-10-12T08:20:00+07:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Observation (Tanda Vital / Fisik)
|
||||||
|
Mencatat TTV seperti Nadi, Tensi, Suhu, dll (Standar LOINC).
|
||||||
|
`POST /observation`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "final",
|
||||||
|
"category_code": "vital-signs",
|
||||||
|
"category_display": "Vital Signs",
|
||||||
|
"code": "8867-4",
|
||||||
|
"display": "Heart rate",
|
||||||
|
"value": 85,
|
||||||
|
"unit": "beats/minute",
|
||||||
|
"unit_code": "/min",
|
||||||
|
"effective_date_time": "2023-10-12T08:10:00+07:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Procedure (Tindakan Medis / ICD-9 CM)
|
||||||
|
Mencatat tindakan medis atau operasi yang dilakukan ke pasien.
|
||||||
|
`POST /procedure`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "completed",
|
||||||
|
"category_code": "387713003",
|
||||||
|
"category_display": "Surgical procedure",
|
||||||
|
"code": "373632001",
|
||||||
|
"display": "Minor surgery",
|
||||||
|
"performed_date_time": "2023-10-12T09:30:00+07:00",
|
||||||
|
"practitioner_id": "N10000001",
|
||||||
|
"practitioner_name": "Dr. Siti Aminah"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. AllergyIntolerance (Alergi)
|
||||||
|
Mencatat alergi pasien terhadap obat, makanan, atau lingkungan.
|
||||||
|
`POST /allergyintolerance`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"clinical_status": "active",
|
||||||
|
"verification_status": "confirmed",
|
||||||
|
"category": "medication",
|
||||||
|
"code": "373410000",
|
||||||
|
"display": "Allergy to Penicillin",
|
||||||
|
"criticality": "high",
|
||||||
|
"recorded_date": "2023-10-12T08:10:00+07:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. ClinicalImpression (Kesan Klinis / Triase)
|
||||||
|
Mencatat hasil asesmen klinis awal atau triase.
|
||||||
|
`POST /clinicalimpression`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "completed",
|
||||||
|
"description": "Pasien tampak sadar penuh, tidak ada tanda kegawatdaruratan",
|
||||||
|
"summary": "Observasi ringan",
|
||||||
|
"practitioner_id": "N10000001",
|
||||||
|
"practitioner_name": "Dr. Siti Aminah",
|
||||||
|
"effective_date_time": "2023-10-12T08:15:00+07:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Medication (Master Obat per Kunjungan)
|
||||||
|
Mendefinisikan obat berdasarkan KFA (Kamus Farmasi Alat Kesehatan).
|
||||||
|
`POST /medication`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status_code": "active",
|
||||||
|
"kfa_code": "93000940",
|
||||||
|
"kfa_display": "Paracetamol 500 mg Tablet",
|
||||||
|
"form_code": "BS019",
|
||||||
|
"form_display": "Tablet",
|
||||||
|
"manufacturer_id": "10000004",
|
||||||
|
"batch_number": "BATCH12345",
|
||||||
|
"expiration_date": "2025-12-31T00:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. MedicationRequest (Resep Obat)
|
||||||
|
Permintaan/resep obat dari Dokter ke Apotek.
|
||||||
|
`POST /medicationrequest`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "active",
|
||||||
|
"intent": "order",
|
||||||
|
"medication_id": "medication-uuid-from-kfa",
|
||||||
|
"medication_display": "Paracetamol 500 mg Tablet",
|
||||||
|
"practitioner_id": "N10000001",
|
||||||
|
"authored_on": "2023-10-12T08:45:00+07:00",
|
||||||
|
"dosage_instruction": "Diminum 3 kali sehari setelah makan",
|
||||||
|
"dispense_quantity": 10
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. MedicationDispense (Penyerahan Obat / Apotek)
|
||||||
|
Pencatatan penyerahan obat fisik kepada pasien oleh Apoteker.
|
||||||
|
`POST /medicationdispense`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"medication_request_id": "request-uuid-here",
|
||||||
|
"status": "completed",
|
||||||
|
"medication_id": "medication-uuid-from-kfa",
|
||||||
|
"medication_display": "Paracetamol 500 mg Tablet",
|
||||||
|
"practitioner_id": "N10000002",
|
||||||
|
"practitioner_name": "Apt. Budi, S.Farm",
|
||||||
|
"handed_over_date": "2023-10-12T09:00:00+07:00",
|
||||||
|
"quantity": 10
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10. MedicationStatement (Pernyataan Penggunaan Obat)
|
||||||
|
Riwayat penggunaan obat yang dilaporkan oleh pasien.
|
||||||
|
`POST /medicationstatement`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "active",
|
||||||
|
"medication_id": "medication-uuid-from-kfa",
|
||||||
|
"medication_display": "Paracetamol 500 mg Tablet",
|
||||||
|
"effective_date_time": "2023-10-12T08:00:00+07:00",
|
||||||
|
"dosage_text": "Sering minum paracetamol bebas saat pusing"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11. ServiceRequest (Permintaan Layanan / Lab / Rujukan)
|
||||||
|
Permintaan tindakan lebih lanjut, seperti rujuk poli, rawat inap, atau periksa Lab/Radiologi.
|
||||||
|
`POST /servicerequest`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "active",
|
||||||
|
"intent": "order",
|
||||||
|
"category_code": "108252007",
|
||||||
|
"category_display": "Laboratory procedure",
|
||||||
|
"code": "1044-2",
|
||||||
|
"display": "Darah Rutin",
|
||||||
|
"requester_id": "N10000001",
|
||||||
|
"authored_on": "2023-10-12T08:30:00+07:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12. DiagnosticReport (Hasil Lab / Radiologi)
|
||||||
|
Laporan hasil pengecekan Lab atau Radiologi (penggabungan berbagai Observation).
|
||||||
|
`POST /diagnosticreport`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "final",
|
||||||
|
"category_code": "LAB",
|
||||||
|
"category_display": "Laboratory",
|
||||||
|
"code": "1044-2",
|
||||||
|
"display": "Hasil Darah Rutin",
|
||||||
|
"practitioner_id": "N10000001",
|
||||||
|
"issued_date": "2023-10-12T11:00:00+07:00",
|
||||||
|
"conclusion": "Semua indikator darah dalam batas normal."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 13. Specimen (Sampel Lab)
|
||||||
|
Data pencatatan spesimen (misal: darah, urin) yang diambil dari tubuh pasien.
|
||||||
|
`POST /specimen`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"type_code": "119297000",
|
||||||
|
"type_display": "Blood specimen",
|
||||||
|
"collection_date_time": "2023-10-12T10:00:00+07:00",
|
||||||
|
"collector_id": "N10000001",
|
||||||
|
"received_date_time": "2023-10-12T10:15:00+07:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 14. ImagingStudy (Pencitraan Radiologi)
|
||||||
|
Data meta terkait pemeriksaan Radiologi (X-Ray, MRI, CT-Scan).
|
||||||
|
`POST /imagingstudy`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "available",
|
||||||
|
"started_date_time": "2023-10-12T10:30:00+07:00",
|
||||||
|
"number_of_series": 1,
|
||||||
|
"number_of_instances": 5,
|
||||||
|
"procedure_code": "XR",
|
||||||
|
"procedure_display": "X-Ray Chest"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 15. Immunization (Riwayat Vaksin)
|
||||||
|
Pencatatan pemberian imunisasi/vaksinasi.
|
||||||
|
`POST /immunization`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "completed",
|
||||||
|
"vaccine_code": "J07BM01",
|
||||||
|
"vaccine_display": "COVID-19 Vaccine",
|
||||||
|
"occurrence_date_time": "2023-10-12T09:00:00+07:00",
|
||||||
|
"practitioner_id": "N10000001",
|
||||||
|
"location_id": "b017aa54-f1df-4ec2-9d84-8823815d7228",
|
||||||
|
"dose_quantity": 0.5,
|
||||||
|
"dose_unit": "ml"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 16. CarePlan (Rencana Perawatan)
|
||||||
|
Rencana medis/keperawatan masa depan untuk pasien.
|
||||||
|
`POST /careplan`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "active",
|
||||||
|
"intent": "plan",
|
||||||
|
"title": "Rencana Diet Diabetes",
|
||||||
|
"description": "Pasien harus mematuhi diet rendah gula dan karbohidrat.",
|
||||||
|
"practitioner_id": "N10000001",
|
||||||
|
"created_date": "2023-10-12T10:00:00+07:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 17. EpisodeOfCare (Episode Perawatan)
|
||||||
|
Mengelompokkan serangkaian *Encounter* (kunjungan) yang merujuk pada satu kondisi klinis / episode penyakit yang sama.
|
||||||
|
`POST /episodeofcare`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"organization_id": "10000004",
|
||||||
|
"status": "active",
|
||||||
|
"period_start": "2023-10-01T08:00:00+07:00",
|
||||||
|
"managing_practitioner_id": "N10000001"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 18. Composition (Resume Medis / Dokumen Klinis)
|
||||||
|
Menggabungkan berbagai resource (Diagnosis, Obat, Tindakan) menjadi satu resume / dokumen medis yang utuh secara hukum.
|
||||||
|
`POST /composition`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "final",
|
||||||
|
"type_code": "18842-5",
|
||||||
|
"type_display": "Discharge Summary",
|
||||||
|
"title": "Resume Pulang Pasien Rawat Inap",
|
||||||
|
"date": "2023-10-15T12:00:00+07:00",
|
||||||
|
"author_id": "N10000001",
|
||||||
|
"author_name": "Dr. Siti Aminah"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 19. QuestionnaireResponse (Hasil Kuesioner Medis)
|
||||||
|
Jawaban atas asesmen spesifik (misal: Skrining Nyeri, Skrining Jatuh, Tumbuh Kembang).
|
||||||
|
`POST /questionnaireresponse`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "completed",
|
||||||
|
"questionnaire_url": "http://sys-ids.kemkes.go.id/questionnaire/Nyeri",
|
||||||
|
"authored_date": "2023-10-12T08:05:00+07:00",
|
||||||
|
"author_practitioner_id": "N10000002",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"linkId": "1.1",
|
||||||
|
"text": "Tingkat Nyeri (0-10)",
|
||||||
|
"answer_value_integer": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 4. Standar Method GET, PUT, & PATCH
|
||||||
|
|
||||||
|
Semua modul mendukung operasi standar berikut:
|
||||||
|
|
||||||
|
1. **`GET /{modul}`** (Search)
|
||||||
|
Melakukan pencarian. Parameter URL (*Query String*) disesuaikan per modul (misal `?patient=P012...` atau `?name=Budi`).
|
||||||
|
|
||||||
|
2. **`GET /{modul}/{id}`** (Get By ID)
|
||||||
|
Mengambil detail data utuh (dalam format FHIR) berdasarkan ID kemenkes.
|
||||||
|
|
||||||
|
3. **`PUT /{modul}/{id}`** (Replace)
|
||||||
|
Mengirimkan *payload JSON* penuh persis seperti `POST` untuk me-replace (mengganti) dokumen lama.
|
||||||
|
|
||||||
|
4. **`PATCH /{modul}/{id}`** (Partial Update)
|
||||||
|
Menggunakan standar *JSON Patch (RFC 6902)* untuk mengubah satu/beberapa *field* spesifik tanpa mengubah keseluruhan dokumen.
|
||||||
|
**Contoh Request Body (Array of Objects):**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/status",
|
||||||
|
"value": "finished"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
@@ -0,0 +1,463 @@
|
|||||||
|
# 📚 Dokumentasi Lengkap API Satu Sehat (GoPrint Builder)
|
||||||
|
|
||||||
|
**Base URL**: `http://localhost:8080/api/v1/satusehat` *(sesuaikan dengan host dan port environment Anda)*
|
||||||
|
**Otorisasi**: Membutuhkan Header `Authorization: Bearer <token_internal_aplikasi_anda>`
|
||||||
|
|
||||||
|
Dokumen ini memuat panduan lengkap penggunaan endpoint dan struktur **JSON Body (POST)** untuk modul referensi (Master Data) maupun modul usecase (Transaksional Klinis). Aplikasi GoPrint bertindak sebagai *proxy builder* yang akan mengonversi struktur JSON datar (*flat*) Anda menjadi format HL7 FHIR standar Kemenkes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 1. Otorisasi (Auth) Kemenkes
|
||||||
|
|
||||||
|
Aplikasi otomatis mengurus *Access Token* ke Kemenkes. Namun jika dibutuhkan secara manual, Anda bisa menggunakan:
|
||||||
|
|
||||||
|
- `GET /reference/auth/token` : Mendapatkan token aktif dari *cache* (digunakan oleh API internal).
|
||||||
|
- `POST /reference/auth/token/refresh` : Memaksa (*bypass cache*) untuk mengambil token baru.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏢 2. Modul Reference (Master Data)
|
||||||
|
|
||||||
|
Modul ini digunakan untuk meregistrasi entitas dasar. URL menggunakan prefix `/reference/{modul}`.
|
||||||
|
|
||||||
|
### A. Organization (Faskes / Departemen)
|
||||||
|
`POST /reference/organization`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"active": true,
|
||||||
|
"type_code": "prov",
|
||||||
|
"type_display": "Healthcare Provider",
|
||||||
|
"identifier_system": "http://sys-ids.kemkes.go.id/organization/10000004",
|
||||||
|
"identifier_value": "10000004",
|
||||||
|
"name": "RSUP Dr. Cipto Mangunkusumo",
|
||||||
|
"phone": "021-1500135",
|
||||||
|
"email": "[email protected]",
|
||||||
|
"url": "https://www.rscm.co.id/",
|
||||||
|
"address": "Jl. Diponegoro No.71, Kenari, Senen",
|
||||||
|
"city": "Jakarta Pusat",
|
||||||
|
"postal_code": "10430",
|
||||||
|
"country_code": "ID",
|
||||||
|
"part_of_id": "1000000"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### B. Location (Lokasi / Ruangan)
|
||||||
|
`POST /reference/location`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"identifier_system": "http://sys-ids.kemkes.go.id/location/10000004",
|
||||||
|
"identifier_value": "LOK-001",
|
||||||
|
"status": "active",
|
||||||
|
"name": "Poli Penyakit Dalam",
|
||||||
|
"description": "Poliklinik Penyakit Dalam Gedung A",
|
||||||
|
"physical_type_code": "ro",
|
||||||
|
"physical_type_display": "Room",
|
||||||
|
"managing_organization_id": "10000004",
|
||||||
|
"part_of_id": "b017aa54-f1df-4ec2-9d84-8823815d7228"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### C. Patient (Pasien)
|
||||||
|
`POST /reference/patient`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "BUDI SANTOSO",
|
||||||
|
"nik": "3573012345678901",
|
||||||
|
"ihs_number": "P0123456789",
|
||||||
|
"gender": "male",
|
||||||
|
"birth_date": "1990-12-31",
|
||||||
|
"phone": "08123456789",
|
||||||
|
"email": "[email protected]",
|
||||||
|
"is_active": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### D. Practitioner (Tenaga Medis)
|
||||||
|
`POST /reference/practitioner`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nik": "3301234567890123",
|
||||||
|
"name": "Dr. Siti Aminah, Sp.PD",
|
||||||
|
"gender": "female",
|
||||||
|
"birth_date": "1985-05-15",
|
||||||
|
"is_active": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🩺 3. Modul Usecase (Data Klinis)
|
||||||
|
|
||||||
|
Semua endpoint ini langsung menggunakan nama resource, contoh: `POST /encounter` atau `POST /condition`.
|
||||||
|
|
||||||
|
### 1. Encounter (Kunjungan)
|
||||||
|
Mencatat registrasi/kedatangan pasien ke faskes.
|
||||||
|
`POST /encounter`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"encounter_id": "KUNJ-001",
|
||||||
|
"organization_id": "10000004",
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"practitioner_id": "N10000001",
|
||||||
|
"practitioner_name": "Dr. Siti Aminah",
|
||||||
|
"location_id": "b017aa54-f1df-4ec2-9d84-8823815d7228",
|
||||||
|
"location_name": "Poli Umum",
|
||||||
|
"status": "arrived",
|
||||||
|
"class": "AMB",
|
||||||
|
"period_start": "2023-10-12T08:00:00+07:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*(Status valid: `planned`, `arrived`, `in-progress`, `finished`, `cancelled`)*
|
||||||
|
|
||||||
|
### 2. Condition (Diagnosis / Keluhan)
|
||||||
|
Mencatat hasil diagnosis ICD-10 pasien.
|
||||||
|
`POST /condition`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"clinical_status": "active",
|
||||||
|
"category_code": "encounter-diagnosis",
|
||||||
|
"category_display": "Encounter Diagnosis",
|
||||||
|
"code": "J06.9",
|
||||||
|
"display": "Acute upper respiratory infection, unspecified",
|
||||||
|
"onset_date_time": "2023-10-12T08:15:00+07:00",
|
||||||
|
"recorded_date": "2023-10-12T08:20:00+07:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Observation (Tanda Vital / Fisik)
|
||||||
|
Mencatat TTV seperti Nadi, Tensi, Suhu, dll (Standar LOINC).
|
||||||
|
`POST /observation`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "final",
|
||||||
|
"category_code": "vital-signs",
|
||||||
|
"category_display": "Vital Signs",
|
||||||
|
"code": "8867-4",
|
||||||
|
"display": "Heart rate",
|
||||||
|
"value": 85,
|
||||||
|
"unit": "beats/minute",
|
||||||
|
"unit_code": "/min",
|
||||||
|
"effective_date_time": "2023-10-12T08:10:00+07:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Procedure (Tindakan Medis / ICD-9 CM)
|
||||||
|
Mencatat tindakan medis atau operasi yang dilakukan ke pasien.
|
||||||
|
`POST /procedure`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "completed",
|
||||||
|
"category_code": "387713003",
|
||||||
|
"category_display": "Surgical procedure",
|
||||||
|
"code": "373632001",
|
||||||
|
"display": "Minor surgery",
|
||||||
|
"performed_date_time": "2023-10-12T09:30:00+07:00",
|
||||||
|
"practitioner_id": "N10000001",
|
||||||
|
"practitioner_name": "Dr. Siti Aminah"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. AllergyIntolerance (Alergi)
|
||||||
|
Mencatat alergi pasien terhadap obat, makanan, atau lingkungan.
|
||||||
|
`POST /allergyintolerance`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"clinical_status": "active",
|
||||||
|
"verification_status": "confirmed",
|
||||||
|
"category": "medication",
|
||||||
|
"code": "373410000",
|
||||||
|
"display": "Allergy to Penicillin",
|
||||||
|
"criticality": "high",
|
||||||
|
"recorded_date": "2023-10-12T08:10:00+07:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. ClinicalImpression (Kesan Klinis / Triase)
|
||||||
|
Mencatat hasil asesmen klinis awal atau triase.
|
||||||
|
`POST /clinicalimpression`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "completed",
|
||||||
|
"description": "Pasien tampak sadar penuh, tidak ada tanda kegawatdaruratan",
|
||||||
|
"summary": "Observasi ringan",
|
||||||
|
"practitioner_id": "N10000001",
|
||||||
|
"practitioner_name": "Dr. Siti Aminah",
|
||||||
|
"effective_date_time": "2023-10-12T08:15:00+07:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Medication (Master Obat per Kunjungan)
|
||||||
|
Mendefinisikan obat berdasarkan KFA (Kamus Farmasi Alat Kesehatan).
|
||||||
|
`POST /medication`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status_code": "active",
|
||||||
|
"kfa_code": "93000940",
|
||||||
|
"kfa_display": "Paracetamol 500 mg Tablet",
|
||||||
|
"form_code": "BS019",
|
||||||
|
"form_display": "Tablet",
|
||||||
|
"manufacturer_id": "10000004",
|
||||||
|
"batch_number": "BATCH12345",
|
||||||
|
"expiration_date": "2025-12-31T00:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. MedicationRequest (Resep Obat)
|
||||||
|
Permintaan/resep obat dari Dokter ke Apotek.
|
||||||
|
`POST /medicationrequest`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "active",
|
||||||
|
"intent": "order",
|
||||||
|
"medication_id": "medication-uuid-from-kfa",
|
||||||
|
"medication_display": "Paracetamol 500 mg Tablet",
|
||||||
|
"practitioner_id": "N10000001",
|
||||||
|
"authored_on": "2023-10-12T08:45:00+07:00",
|
||||||
|
"dosage_instruction": "Diminum 3 kali sehari setelah makan",
|
||||||
|
"dispense_quantity": 10
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. MedicationDispense (Penyerahan Obat / Apotek)
|
||||||
|
Pencatatan penyerahan obat fisik kepada pasien oleh Apoteker.
|
||||||
|
`POST /medicationdispense`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"medication_request_id": "request-uuid-here",
|
||||||
|
"status": "completed",
|
||||||
|
"medication_id": "medication-uuid-from-kfa",
|
||||||
|
"medication_display": "Paracetamol 500 mg Tablet",
|
||||||
|
"practitioner_id": "N10000002",
|
||||||
|
"practitioner_name": "Apt. Budi, S.Farm",
|
||||||
|
"handed_over_date": "2023-10-12T09:00:00+07:00",
|
||||||
|
"quantity": 10
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10. MedicationStatement (Pernyataan Penggunaan Obat)
|
||||||
|
Riwayat penggunaan obat yang dilaporkan oleh pasien.
|
||||||
|
`POST /medicationstatement`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "active",
|
||||||
|
"medication_id": "medication-uuid-from-kfa",
|
||||||
|
"medication_display": "Paracetamol 500 mg Tablet",
|
||||||
|
"effective_date_time": "2023-10-12T08:00:00+07:00",
|
||||||
|
"dosage_text": "Sering minum paracetamol bebas saat pusing"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11. ServiceRequest (Permintaan Layanan / Lab / Rujukan)
|
||||||
|
Permintaan tindakan lebih lanjut, seperti rujuk poli, rawat inap, atau periksa Lab/Radiologi.
|
||||||
|
`POST /servicerequest`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "active",
|
||||||
|
"intent": "order",
|
||||||
|
"category_code": "108252007",
|
||||||
|
"category_display": "Laboratory procedure",
|
||||||
|
"code": "1044-2",
|
||||||
|
"display": "Darah Rutin",
|
||||||
|
"requester_id": "N10000001",
|
||||||
|
"authored_on": "2023-10-12T08:30:00+07:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12. DiagnosticReport (Hasil Lab / Radiologi)
|
||||||
|
Laporan hasil pengecekan Lab atau Radiologi (penggabungan berbagai Observation).
|
||||||
|
`POST /diagnosticreport`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "final",
|
||||||
|
"category_code": "LAB",
|
||||||
|
"category_display": "Laboratory",
|
||||||
|
"code": "1044-2",
|
||||||
|
"display": "Hasil Darah Rutin",
|
||||||
|
"practitioner_id": "N10000001",
|
||||||
|
"issued_date": "2023-10-12T11:00:00+07:00",
|
||||||
|
"conclusion": "Semua indikator darah dalam batas normal."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 13. Specimen (Sampel Lab)
|
||||||
|
Data pencatatan spesimen (misal: darah, urin) yang diambil dari tubuh pasien.
|
||||||
|
`POST /specimen`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"type_code": "119297000",
|
||||||
|
"type_display": "Blood specimen",
|
||||||
|
"collection_date_time": "2023-10-12T10:00:00+07:00",
|
||||||
|
"collector_id": "N10000001",
|
||||||
|
"received_date_time": "2023-10-12T10:15:00+07:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 14. ImagingStudy (Pencitraan Radiologi)
|
||||||
|
Data meta terkait pemeriksaan Radiologi (X-Ray, MRI, CT-Scan).
|
||||||
|
`POST /imagingstudy`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"organization_id": "10000004",
|
||||||
|
"accession_number": "USG-SEN25-011815",
|
||||||
|
"service_request_id": "c8d9f768-2237-4da5-b0ad-9cd5e6087a9a",
|
||||||
|
"patient_id": "P20395354720",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "available",
|
||||||
|
"started": "2025-06-10T11:41:46+07:00",
|
||||||
|
"number_of_series": 1,
|
||||||
|
"number_of_instances": 5,
|
||||||
|
"procedure_code": "CT",
|
||||||
|
"procedure_display": "CT Scan",
|
||||||
|
"description": "Keterangan hasil pemeriksaan"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 15. Immunization (Riwayat Vaksin)
|
||||||
|
Pencatatan pemberian imunisasi/vaksinasi.
|
||||||
|
`POST /immunization`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "completed",
|
||||||
|
"vaccine_code": "J07BM01",
|
||||||
|
"vaccine_display": "COVID-19 Vaccine",
|
||||||
|
"occurrence_date_time": "2023-10-12T09:00:00+07:00",
|
||||||
|
"practitioner_id": "N10000001",
|
||||||
|
"location_id": "b017aa54-f1df-4ec2-9d84-8823815d7228",
|
||||||
|
"dose_quantity": 0.5,
|
||||||
|
"dose_unit": "ml"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 16. CarePlan (Rencana Perawatan)
|
||||||
|
Rencana medis/keperawatan masa depan untuk pasien.
|
||||||
|
`POST /careplan`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "active",
|
||||||
|
"intent": "plan",
|
||||||
|
"title": "Rencana Diet Diabetes",
|
||||||
|
"description": "Pasien harus mematuhi diet rendah gula dan karbohidrat.",
|
||||||
|
"practitioner_id": "N10000001",
|
||||||
|
"created_date": "2023-10-12T10:00:00+07:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 17. EpisodeOfCare (Episode Perawatan)
|
||||||
|
Mengelompokkan serangkaian *Encounter* (kunjungan) yang merujuk pada satu kondisi klinis / episode penyakit yang sama.
|
||||||
|
`POST /episodeofcare`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"organization_id": "10000004",
|
||||||
|
"status": "active",
|
||||||
|
"period_start": "2023-10-01T08:00:00+07:00",
|
||||||
|
"managing_practitioner_id": "N10000001"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 18. Composition (Resume Medis / Dokumen Klinis)
|
||||||
|
Menggabungkan berbagai resource (Diagnosis, Obat, Tindakan) menjadi satu resume / dokumen medis yang utuh secara hukum.
|
||||||
|
`POST /composition`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "final",
|
||||||
|
"title": "Resume Pulang Pasien Rawat Inap",
|
||||||
|
"date": "2023-10-15T12:00:00Z",
|
||||||
|
"practitioner_id": "N10000001",
|
||||||
|
"practitioner_name": "Dr. Siti Aminah"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 19. QuestionnaireResponse (Hasil Kuesioner Medis)
|
||||||
|
Jawaban atas asesmen spesifik (misal: Skrining Nyeri, Skrining Jatuh, Tumbuh Kembang).
|
||||||
|
`POST /questionnaireresponse`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"patient_id": "P0123456789",
|
||||||
|
"patient_name": "Budi Santoso",
|
||||||
|
"encounter_id": "5fa23d13-0943-4318-b295-eb1ecfa7384a",
|
||||||
|
"status": "completed",
|
||||||
|
"questionnaire_url": "http://sys-ids.kemkes.go.id/questionnaire/Nyeri",
|
||||||
|
"authored_date": "2023-10-12T08:05:00+07:00",
|
||||||
|
"author_practitioner_id": "N10000002",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"linkId": "1.1",
|
||||||
|
"text": "Tingkat Nyeri (0-10)",
|
||||||
|
"answer_value_integer": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 4. Standar Method GET, PUT, & PATCH
|
||||||
|
|
||||||
|
Semua modul mendukung operasi standar berikut:
|
||||||
|
|
||||||
|
1. **`GET /{modul}`** (Search)
|
||||||
|
Melakukan pencarian. Parameter URL (*Query String*) disesuaikan per modul (misal `?patient=P012...` atau `?name=Budi`).
|
||||||
|
|
||||||
|
2. **`GET /{modul}/{id}`** (Get By ID)
|
||||||
|
Mengambil detail data utuh (dalam format FHIR) berdasarkan ID kemenkes.
|
||||||
|
|
||||||
|
3. **`PUT /{modul}/{id}`** (Replace)
|
||||||
|
Mengirimkan *payload JSON* penuh persis seperti `POST` untuk me-replace (mengganti) dokumen lama.
|
||||||
|
|
||||||
|
4. **`PATCH /{modul}/{id}`** (Partial Update)
|
||||||
|
Menggunakan standar *JSON Patch (RFC 6902)* untuk mengubah satu/beberapa *field* spesifik tanpa mengubah keseluruhan dokumen.
|
||||||
|
**Contoh Request Body (Array of Objects):**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/status",
|
||||||
|
"value": "finished"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
// ReferenceDTO merepresentasikan referensi standar ke resource FHIR lain.
|
||||||
|
type ReferenceDTO struct {
|
||||||
|
Reference string `json:"reference,omitempty"`
|
||||||
|
Display string `json:"display,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CodeableConceptDTO merepresentasikan konsep kode HL7/SNOMED/LOINC.
|
||||||
|
type CodeableConceptDTO struct {
|
||||||
|
System string `json:"system,omitempty"`
|
||||||
|
Code string `json:"code,omitempty"`
|
||||||
|
Display string `json:"display,omitempty"`
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuantityDTO merepresentasikan besaran nilai beserta unit pengukurannya.
|
||||||
|
type QuantityDTO struct {
|
||||||
|
Value float64 `json:"value,omitempty"`
|
||||||
|
Unit string `json:"unit,omitempty"`
|
||||||
|
System string `json:"system,omitempty"`
|
||||||
|
Code string `json:"code,omitempty"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
-- Tabel Utama Produk KFA
|
||||||
|
CREATE TABLE kfa_products (
|
||||||
|
kfa_code VARCHAR(50) PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
display_name VARCHAR(255),
|
||||||
|
nama_dagang VARCHAR(255),
|
||||||
|
active BOOLEAN DEFAULT true,
|
||||||
|
state VARCHAR(50),
|
||||||
|
generik BOOLEAN,
|
||||||
|
|
||||||
|
-- Klasifikasi & Atribut Dasar (Disimpan sebagai JSONB untuk efisiensi)
|
||||||
|
dosage_form JSONB, -- { "code": "BS066", "name": "Tablet" }
|
||||||
|
farmalkes_type JSONB, -- { "code": "medicine", "group": "farmasi", "name": "Obat" }
|
||||||
|
uom JSONB, -- { "name": "Tablet" }
|
||||||
|
rute_pemberian JSONB, -- { "code": "O", "name": "Oral" }
|
||||||
|
controlled_drug JSONB, -- { "code": "3", "name": "Obat Keras" }
|
||||||
|
|
||||||
|
-- Harga & Metrik Fisik
|
||||||
|
fix_price NUMERIC(12, 2),
|
||||||
|
het_price NUMERIC(12, 2),
|
||||||
|
dose_per_unit NUMERIC(10, 2),
|
||||||
|
net_weight NUMERIC(10, 2),
|
||||||
|
net_weight_uom_name VARCHAR(20),
|
||||||
|
volume NUMERIC(10, 2),
|
||||||
|
volume_uom_name VARCHAR(20),
|
||||||
|
score_tkdn NUMERIC(5, 2),
|
||||||
|
rxterm INT,
|
||||||
|
|
||||||
|
-- Data Legal & Manufaktur
|
||||||
|
manufacturer VARCHAR(255),
|
||||||
|
registrar VARCHAR(255),
|
||||||
|
nie VARCHAR(100),
|
||||||
|
farmalkes_hscode VARCHAR(100),
|
||||||
|
kode_lkpp VARCHAR(100),
|
||||||
|
tayang_lkpp BOOLEAN,
|
||||||
|
|
||||||
|
-- Teks Panjang (HTML String)
|
||||||
|
description TEXT,
|
||||||
|
indication TEXT,
|
||||||
|
side_effect TEXT,
|
||||||
|
warning TEXT,
|
||||||
|
|
||||||
|
-- Kompleks/Nested JSONB (Data yang jarang di-join tapi sering dibaca)
|
||||||
|
atc_info JSONB, -- Menggabungkan atc_l1 sampai atc_l5 & atc_ddd
|
||||||
|
fornas JSONB, -- Struktur dokumen fornas
|
||||||
|
dosage_usage JSONB, -- Array of dosage usage objects
|
||||||
|
tags JSONB, -- Array of tags [{code, name}]
|
||||||
|
identifier_ids JSONB, -- Array of identifiers [{code, name, source_name}]
|
||||||
|
product_template JSONB, -- Data template referensi KFA
|
||||||
|
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indeks untuk pencarian teks yang cepat
|
||||||
|
CREATE INDEX idx_kfa_products_name ON kfa_products(name);
|
||||||
|
CREATE INDEX idx_kfa_products_kode_lkpp ON kfa_products(kode_lkpp);
|
||||||
|
-- Indeks GIN untuk pencarian dalam JSONB (misal mencari produk berdasarkan tags)
|
||||||
|
-- CREATE INDEX idx_kfa_products_tags ON kfa_products USING GIN (tags); -- Catatan: Buka komentar ini HANYA jika Anda menggunakan PostgreSQL
|
||||||
|
|
||||||
|
-- Tabel Relasi: Zat Aktif (Penting untuk dibuat tabel terpisah agar bisa query "Obat dengan kandungan X")
|
||||||
|
CREATE TABLE kfa_active_ingredients (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
product_kfa_code VARCHAR(50) REFERENCES kfa_products(kfa_code) ON DELETE CASCADE,
|
||||||
|
kfa_code VARCHAR(50),
|
||||||
|
zat_aktif VARCHAR(255) NOT NULL,
|
||||||
|
kekuatan_zat_aktif VARCHAR(100),
|
||||||
|
active BOOLEAN DEFAULT true,
|
||||||
|
state VARCHAR(50),
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_ingredient_zat_aktif ON kfa_active_ingredients(zat_aktif);
|
||||||
|
|
||||||
|
-- Tabel Relasi: Varian Kemasan
|
||||||
|
CREATE TABLE kfa_packaging (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
product_kfa_code VARCHAR(50) REFERENCES kfa_products(kfa_code) ON DELETE CASCADE,
|
||||||
|
kfa_code VARCHAR(50),
|
||||||
|
name VARCHAR(255),
|
||||||
|
pack_price NUMERIC(12, 2),
|
||||||
|
qty INT,
|
||||||
|
uom_id VARCHAR(50)
|
||||||
|
);
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package kyc
|
||||||
|
|
||||||
|
// GenerateURLRequest merepresentasikan payload untuk membuat link validasi KYC
|
||||||
|
type GenerateURLRequest struct {
|
||||||
|
AgentName string `json:"agent_name" binding:"required"`
|
||||||
|
AgentNIK string `json:"agent_nik" binding:"required,len=16"`
|
||||||
|
PublicKey string `json:"public_key"` // Diisi otomatis oleh backend
|
||||||
|
}
|
||||||
|
|
||||||
|
// CallbackRequest merepresentasikan payload webhook/callback dari Satu Sehat setelah KYC selesai.
|
||||||
|
type CallbackRequest struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data map[string]interface{} `json:"data"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package kyc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/crypto"
|
||||||
|
"service/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repository interface {
|
||||||
|
// Menerima privKeyPEM untuk keperluan decrypt hasil response
|
||||||
|
GenerateURL(ctx context.Context, payload GenerateURLRequest, privKeyPEM string) (*satusehat.FHIRResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type repository struct {
|
||||||
|
client satusehat.SatuSehatClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepository(client satusehat.SatuSehatClient) Repository {
|
||||||
|
return &repository{client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hiddenCtx membungkus context untuk mencegah *HTTP client*
|
||||||
|
// melakukan type-assertion ke *gin.Context dan melakukan auto-print error.
|
||||||
|
type hiddenCtx struct {
|
||||||
|
context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) GenerateURL(ctx context.Context, payload GenerateURLRequest, privKeyPEM string) (*satusehat.FHIRResponse, error) {
|
||||||
|
// 1. Marshal JSON asli
|
||||||
|
jsonPayload, _ := json.Marshal(payload)
|
||||||
|
|
||||||
|
// 2. Enkripsi JSON Payload menggunakan fungsi AES/RSA yang baru dibuat
|
||||||
|
encryptedStringPayload, err := crypto.EncryptSatuSehatPayload(jsonPayload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.InternalError().Message("Gagal mengenkripsi payload").Cause(err).Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Endpoint ini mengarah ke /generate-url dengan method POST
|
||||||
|
// NOTE: Pastikan r.client.DoKYC mendukung pengiriman string dan otomatis
|
||||||
|
// memberikan HTTP Header Content-Type: text/plain
|
||||||
|
respData, err := r.client.DoKYC(hiddenCtx{ctx}, "POST", "/generate-url", encryptedStringPayload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.InternalError().
|
||||||
|
Message("Failed to generate KYC URL to SatuSehat").
|
||||||
|
Cause(err).
|
||||||
|
Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Dekripsi response dari Kemenkes menggunakan Private Key kita
|
||||||
|
decryptedData, decErr := crypto.DecryptSatuSehatPayload(string(respData), privKeyPEM)
|
||||||
|
if decErr != nil {
|
||||||
|
// Terkadang jika validasi NIK gagal, Kemenkes membalas JSON plaintext berisi {"metadata": {"code":"400"...}}
|
||||||
|
// Jika gagal decrypt, kita asumsikan Kemenkes mengirim plaintext error, kita parse langsung.
|
||||||
|
decryptedData = respData
|
||||||
|
}
|
||||||
|
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := json.Unmarshal(decryptedData, &result); err != nil {
|
||||||
|
return nil, errors.InternalError().
|
||||||
|
Message("Failed to parse KYC response").
|
||||||
|
Cause(err).
|
||||||
|
Metadata("raw_response", string(decryptedData)).
|
||||||
|
Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &satusehat.FHIRResponse{
|
||||||
|
FullResponse: result,
|
||||||
|
RawResponse: respData,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package kyc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"service/internal/infrastructure/config"
|
||||||
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/crypto"
|
||||||
|
"service/pkg/errors"
|
||||||
|
"service/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
GenerateURL(ctx context.Context, req GenerateURLRequest) (*satusehat.FHIRResponse, error)
|
||||||
|
HandleCallback(ctx context.Context, encryptedPayload string, token string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
repo Repository
|
||||||
|
cfg config.SatuSehatConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(repo Repository, cfg config.SatuSehatConfig) Service {
|
||||||
|
return &service{repo: repo, cfg: cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) GenerateURL(ctx context.Context, req GenerateURLRequest) (*satusehat.FHIRResponse, error) {
|
||||||
|
// Gunakan public key statis dari konfigurasi (disimpan dalam format Base64 di .env)
|
||||||
|
pubKeyBytes, err := base64.StdEncoding.DecodeString(s.cfg.KYCPublicKeyB64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.InternalError().Message("Gagal mendecode Public Key dari konfigurasi").Cause(err).Build()
|
||||||
|
}
|
||||||
|
req.PublicKey = string(pubKeyBytes)
|
||||||
|
|
||||||
|
// Decode private key untuk mendekripsi balasan generate URL dari Kemenkes
|
||||||
|
privKeyBytes, err := base64.StdEncoding.DecodeString(s.cfg.KYCPrivateKeyB64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.InternalError().Message("Gagal mendecode Private Key dari konfigurasi").Cause(err).Build()
|
||||||
|
}
|
||||||
|
privKeyPEM := string(privKeyBytes)
|
||||||
|
|
||||||
|
// Lempar privKeyPEM ke repo agar Kemenkes response dapat didekripsi
|
||||||
|
return s.repo.GenerateURL(ctx, req, privKeyPEM)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) HandleCallback(ctx context.Context, encryptedPayload string, token string) error {
|
||||||
|
// Validasi Security Token dari Kemenkes
|
||||||
|
expectedToken := s.cfg.WebhookSecret
|
||||||
|
if expectedToken == "" {
|
||||||
|
logger.Default().Warn("SATUSEHAT_WEBHOOK_SECRET belum dikonfigurasi. Validasi token webhook dilewati.")
|
||||||
|
} else if token != expectedToken {
|
||||||
|
logger.Default().Warn("Webhook KYC gagal divalidasi: Token tidak cocok", logger.String("received_token", token))
|
||||||
|
return errors.UnauthorizedError().Message("Invalid security webhook token").Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Dapatkan Private Key statis Anda dari config/env var
|
||||||
|
privKeyBytes, err := base64.StdEncoding.DecodeString(s.cfg.KYCPrivateKeyB64)
|
||||||
|
if err != nil {
|
||||||
|
return errors.InternalError().Message("Gagal mendecode Private Key dari konfigurasi").Cause(err).Build()
|
||||||
|
}
|
||||||
|
privKeyPEM := string(privKeyBytes)
|
||||||
|
|
||||||
|
// 2. Dekripsi payload yang masuk
|
||||||
|
decryptedBytes, err := crypto.DecryptSatuSehatPayload(encryptedPayload, privKeyPEM)
|
||||||
|
if err != nil {
|
||||||
|
return errors.InternalError().Message("Gagal mendekripsi payload webhook dari SatuSehat").Cause(err).Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Unmarshal ke bentuk Struct
|
||||||
|
var payload CallbackRequest
|
||||||
|
if err := json.Unmarshal(decryptedBytes, &payload); err != nil {
|
||||||
|
return errors.InternalError().Message("Gagal memparsing JSON hasil dekripsi").Cause(err).Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Tambahkan logika update ke database lokal (misal: tabel user/agent) berdasarkan hasil KYC.
|
||||||
|
// Saat ini, kita log payload yang diterima dari Kemenkes.
|
||||||
|
logger.Default().Info("Menerima Webhook KYC dari SatuSehat", logger.Any("payload", payload))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,17 +1,25 @@
|
|||||||
package allergyintolerance
|
package allergyintolerance
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service/internal/satusehat/common"
|
||||||
|
)
|
||||||
|
|
||||||
type AllergyIntoleranceRequest struct {
|
type AllergyIntoleranceRequest struct {
|
||||||
PatientID string `json:"patient_id" binding:"required"`
|
OrganizationID string `json:"organization_id,omitempty"`
|
||||||
PatientName string `json:"patient_name" binding:"required"`
|
AllergyID string `json:"allergy_id,omitempty"`
|
||||||
EncounterID string `json:"encounter_id" binding:"required"`
|
|
||||||
ClinicalStatus string `json:"clinical_status" binding:"required,oneof=active inactive resolved"`
|
ClinicalStatus string `json:"clinical_status" binding:"required,oneof=active inactive resolved"`
|
||||||
VerificationStatus string `json:"verification_status" binding:"required,oneof=unconfirmed presumed confirmed refuted entered-in-error"`
|
VerificationStatus string `json:"verification_status" binding:"required,oneof=unconfirmed presumed confirmed refuted entered-in-error"`
|
||||||
Category string `json:"category" binding:"required,oneof=food medication environment biologic"`
|
Category []string `json:"category,omitempty"`
|
||||||
Code string `json:"code" binding:"required"` // SNOMED CT Code
|
Code *common.CodeableConceptDTO `json:"code" binding:"required"`
|
||||||
Display string `json:"display" binding:"required"` // Display Name
|
PatientID string `json:"patient_id" binding:"required"`
|
||||||
RecordedDate time.Time `json:"recorded_date" binding:"required"`
|
PatientName string `json:"patient_name,omitempty"`
|
||||||
|
EncounterID string `json:"encounter_id" binding:"required"`
|
||||||
|
EncounterDisplay string `json:"encounter_display,omitempty"`
|
||||||
|
RecordedDate *time.Time `json:"recorded_date" binding:"required"`
|
||||||
|
RecorderID string `json:"recorder_id,omitempty"`
|
||||||
|
RecorderDisplay string `json:"recorder_display,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AllergyIntolerancePatchRequest []map[string]interface{}
|
type AllergyIntolerancePatchRequest []map[string]interface{}
|
||||||
@@ -1,42 +1,115 @@
|
|||||||
package allergyintolerance
|
package allergyintolerance
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var verificationStatusDisplayMap = map[string]string{
|
||||||
|
"unconfirmed": "Unconfirmed",
|
||||||
|
"presumed": "Presumed",
|
||||||
|
"confirmed": "Confirmed",
|
||||||
|
"refuted": "Refuted",
|
||||||
|
"entered-in-error": "Entered in Error",
|
||||||
|
}
|
||||||
|
|
||||||
func MapRequestToFHIR(req AllergyIntoleranceRequest) satusehat.FHIRPayload {
|
func MapRequestToFHIR(req AllergyIntoleranceRequest) satusehat.FHIRPayload {
|
||||||
return satusehat.NewFHIRPayload("AllergyIntolerance").
|
payload := satusehat.NewFHIRPayload("AllergyIntolerance")
|
||||||
Set("clinicalStatus", map[string]interface{}{
|
|
||||||
|
orgID := os.Getenv("SATUSEHAT_ORG_ID")
|
||||||
|
if orgID == "" {
|
||||||
|
orgID = req.OrganizationID
|
||||||
|
}
|
||||||
|
|
||||||
|
if orgID != "" && req.AllergyID != "" {
|
||||||
|
payload.Set("identifier", []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"system": "http://sys-ids.kemkes.go.id/allergy/" + orgID,
|
||||||
|
"use": "official",
|
||||||
|
"value": req.AllergyID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ClinicalStatus != "" {
|
||||||
|
payload.Set("clinicalStatus", map[string]interface{}{
|
||||||
"coding": []map[string]interface{}{
|
"coding": []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical",
|
"system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical",
|
||||||
"code": req.ClinicalStatus,
|
"code": req.ClinicalStatus,
|
||||||
|
"display": strings.ToUpper(req.ClinicalStatus[:1]) + req.ClinicalStatus[1:],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}).
|
})
|
||||||
Set("verificationStatus", map[string]interface{}{
|
}
|
||||||
|
|
||||||
|
if req.VerificationStatus != "" {
|
||||||
|
display, ok := verificationStatusDisplayMap[req.VerificationStatus]
|
||||||
|
if !ok {
|
||||||
|
display = strings.ToUpper(req.VerificationStatus[:1]) + req.VerificationStatus[1:]
|
||||||
|
}
|
||||||
|
payload.Set("verificationStatus", map[string]interface{}{
|
||||||
"coding": []map[string]interface{}{
|
"coding": []map[string]interface{}{
|
||||||
{
|
{"system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-verification", "code": req.VerificationStatus, "display": display},
|
||||||
"system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-verification",
|
|
||||||
"code": req.VerificationStatus,
|
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
}).
|
}
|
||||||
Set("category", []string{req.Category}).
|
|
||||||
Set("code", map[string]interface{}{
|
if len(req.Category) > 0 {
|
||||||
"coding": []map[string]interface{}{
|
payload.Set("category", req.Category)
|
||||||
{
|
}
|
||||||
"system": "http://snomed.info/sct",
|
|
||||||
"code": req.Code,
|
if req.Code != nil {
|
||||||
"display": req.Display,
|
sys := req.Code.System
|
||||||
},
|
if sys == "" {
|
||||||
},
|
sys = "http://snomed.info/sct" // Default to SNOMED
|
||||||
}).
|
}
|
||||||
Set("patient", map[string]interface{}{
|
coding := map[string]interface{}{
|
||||||
"reference": "Patient/" + req.PatientID,
|
"system": sys,
|
||||||
"display": req.PatientName,
|
"code": req.Code.Code,
|
||||||
}).
|
}
|
||||||
Set("recordedDate", req.RecordedDate.Format(time.RFC3339))
|
if req.Code.Display != "" {
|
||||||
|
coding["display"] = req.Code.Display
|
||||||
|
}
|
||||||
|
codeConcept := map[string]interface{}{
|
||||||
|
"coding": []map[string]interface{}{coding},
|
||||||
|
}
|
||||||
|
if req.Code.Text != "" {
|
||||||
|
codeConcept["text"] = req.Code.Text
|
||||||
|
}
|
||||||
|
payload.Set("code", codeConcept)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.PatientID != "" {
|
||||||
|
subject := map[string]interface{}{"reference": "Patient/" + req.PatientID}
|
||||||
|
if req.PatientName != "" {
|
||||||
|
subject["display"] = req.PatientName
|
||||||
|
}
|
||||||
|
payload.Set("patient", subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.EncounterID != "" {
|
||||||
|
encounter := map[string]interface{}{"reference": "Encounter/" + req.EncounterID}
|
||||||
|
if req.EncounterDisplay != "" {
|
||||||
|
encounter["display"] = req.EncounterDisplay
|
||||||
|
}
|
||||||
|
payload.Set("encounter", encounter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.RecordedDate != nil && !req.RecordedDate.IsZero() {
|
||||||
|
payload.Set("recordedDate", req.RecordedDate.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.RecorderID != "" {
|
||||||
|
recorder := map[string]interface{}{"reference": "Practitioner/" + req.RecorderID}
|
||||||
|
if req.RecorderDisplay != "" {
|
||||||
|
recorder["display"] = req.RecorderDisplay
|
||||||
|
}
|
||||||
|
payload.Set("recorder", recorder)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
@@ -19,15 +20,24 @@ type repository struct{ client satusehat.SatuSehatClient }
|
|||||||
|
|
||||||
func NewRepository(client satusehat.SatuSehatClient) Repository { return &repository{client: client} }
|
func NewRepository(client satusehat.SatuSehatClient) Repository { return &repository{client: client} }
|
||||||
|
|
||||||
|
type hiddenCtx struct {
|
||||||
|
context.Context
|
||||||
|
}
|
||||||
|
|
||||||
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
resp, err := r.client.DoRequest(ctx, method, endpoint, req)
|
resp, err := r.client.DoRequest(hiddenCtx{ctx}, method, endpoint, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.InternalError().Message("Failed to execute request to SatuSehat").Cause(err).Build()
|
||||||
}
|
}
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
if err := json.Unmarshal(resp, &result); err != nil {
|
if err := json.Unmarshal(resp, &result); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
return nil, errors.InternalError().Message("Failed to parse SatuSehat response").Cause(err).Metadata("raw_response", string(resp)).Build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if resourceType, ok := result["resourceType"].(string); ok && resourceType == "OperationOutcome" {
|
||||||
|
return nil, errors.ParseSatuSehatError(result)
|
||||||
|
}
|
||||||
|
|
||||||
var resourceID string
|
var resourceID string
|
||||||
if id, ok := result["id"].(string); ok {
|
if id, ok := result["id"].(string); ok {
|
||||||
resourceID = id
|
resourceID = id
|
||||||
|
|||||||
@@ -25,12 +25,23 @@ func (s *service) Update(ctx context.Context, id string, req AllergyIntoleranceR
|
|||||||
if id == "" {
|
if id == "" {
|
||||||
return nil, errors.NewValidationError().Message("ID is required").Build()
|
return nil, errors.NewValidationError().Message("ID is required").Build()
|
||||||
}
|
}
|
||||||
return s.repo.Update(ctx, id, MapRequestToFHIR(req).Set("id", id))
|
payload := MapRequestToFHIR(req)
|
||||||
|
payload.Set("id", id)
|
||||||
|
return s.repo.Update(ctx, id, payload)
|
||||||
}
|
}
|
||||||
func (s *service) Patch(ctx context.Context, id string, req AllergyIntolerancePatchRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Patch(ctx context.Context, id string, req AllergyIntolerancePatchRequest) (*satusehat.FHIRResponse, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("AllergyIntolerance ID is required").Build()
|
||||||
|
}
|
||||||
|
if len(req) == 0 {
|
||||||
|
return nil, errors.NewValidationError().Message("Patch payload cannot be empty").Build()
|
||||||
|
}
|
||||||
return s.repo.Patch(ctx, id, req)
|
return s.repo.Patch(ctx, id, req)
|
||||||
}
|
}
|
||||||
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("AllergyIntolerance ID is required").Build()
|
||||||
|
}
|
||||||
return s.repo.GetByID(ctx, id)
|
return s.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
package careplan
|
package careplan
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service/internal/satusehat/common"
|
||||||
|
)
|
||||||
|
|
||||||
type CarePlanRequest struct {
|
type CarePlanRequest struct {
|
||||||
PatientID string `json:"patient_id" binding:"required"`
|
OrganizationID string `json:"organization_id,omitempty"`
|
||||||
PatientName string `json:"patient_name" binding:"required"`
|
CarePlanID string `json:"care_plan_id,omitempty"`
|
||||||
EncounterID string `json:"encounter_id" binding:"required"`
|
|
||||||
PractitionerID string `json:"practitioner_id" binding:"required"`
|
|
||||||
PractitionerName string `json:"practitioner_name" binding:"required"`
|
|
||||||
Status string `json:"status" binding:"required,oneof=draft active on-hold revoked completed entered-in-error unknown"`
|
Status string `json:"status" binding:"required,oneof=draft active on-hold revoked completed entered-in-error unknown"`
|
||||||
Intent string `json:"intent" binding:"required,oneof=proposal plan order option"`
|
Intent string `json:"intent" binding:"required,oneof=proposal plan order option"`
|
||||||
Title string `json:"title" binding:"required"`
|
Category []common.CodeableConceptDTO `json:"category,omitempty"`
|
||||||
Description string `json:"description" binding:"required"`
|
Title string `json:"title,omitempty"`
|
||||||
CreatedDate time.Time `json:"created_date" binding:"required"`
|
Description string `json:"description,omitempty"`
|
||||||
|
PatientID string `json:"patient_id" binding:"required"`
|
||||||
|
PatientDisplay string `json:"patient_display,omitempty"`
|
||||||
|
EncounterID string `json:"encounter_id" binding:"required"`
|
||||||
|
EncounterDisplay string `json:"encounter_display,omitempty"`
|
||||||
|
CreatedDate *time.Time `json:"created_date,omitempty"`
|
||||||
|
AuthorID string `json:"author_id,omitempty"` // cth: "N10000001"
|
||||||
|
AuthorDisplay string `json:"author_display,omitempty"`
|
||||||
|
GoalIDs []string `json:"goal_ids,omitempty"` // Akan direferensikan sebagai "Goal/{id}"
|
||||||
}
|
}
|
||||||
|
|
||||||
type CarePlanPatchRequest []map[string]interface{}
|
type CarePlanPatchRequest []map[string]interface{}
|
||||||
@@ -1,24 +1,87 @@
|
|||||||
package careplan
|
package careplan
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MapRequestToFHIR(req CarePlanRequest) satusehat.FHIRPayload {
|
func MapRequestToFHIR(req CarePlanRequest) satusehat.FHIRPayload {
|
||||||
return satusehat.NewFHIRPayload("CarePlan").
|
payload := satusehat.NewFHIRPayload("CarePlan")
|
||||||
Set("status", req.Status).
|
|
||||||
Set("intent", req.Intent).
|
orgID := os.Getenv("SATUSEHAT_ORG_ID")
|
||||||
Set("title", req.Title).
|
if orgID == "" {
|
||||||
Set("description", req.Description).
|
orgID = req.OrganizationID
|
||||||
Set("subject", map[string]interface{}{
|
}
|
||||||
"reference": "Patient/" + req.PatientID,
|
|
||||||
"display": req.PatientName,
|
if orgID != "" && req.CarePlanID != "" {
|
||||||
}).
|
payload.Set("identifier", []map[string]interface{}{
|
||||||
Set("encounter", map[string]interface{}{"reference": "Encounter/" + req.EncounterID}).
|
{"system": "http://sys-ids.kemkes.go.id/careplan/" + orgID, "use": "official", "value": req.CarePlanID},
|
||||||
Set("author", map[string]interface{}{
|
})
|
||||||
"reference": "Practitioner/" + req.PractitionerID,
|
}
|
||||||
"display": req.PractitionerName,
|
|
||||||
}).
|
if req.Status != "" {
|
||||||
Set("created", req.CreatedDate.Format(time.RFC3339))
|
payload.Set("status", req.Status)
|
||||||
|
}
|
||||||
|
if req.Intent != "" {
|
||||||
|
payload.Set("intent", req.Intent)
|
||||||
|
}
|
||||||
|
if len(req.Category) > 0 {
|
||||||
|
var categories []map[string]interface{}
|
||||||
|
for _, cat := range req.Category {
|
||||||
|
sys := cat.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://snomed.info/sct" // Default value jika kosong
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": cat.Code}
|
||||||
|
if cat.Display != "" {
|
||||||
|
coding["display"] = cat.Display
|
||||||
|
}
|
||||||
|
concept := map[string]interface{}{"coding": []map[string]interface{}{coding}}
|
||||||
|
if cat.Text != "" {
|
||||||
|
concept["text"] = cat.Text
|
||||||
|
}
|
||||||
|
categories = append(categories, concept)
|
||||||
|
}
|
||||||
|
payload.Set("category", categories)
|
||||||
|
}
|
||||||
|
if req.Title != "" {
|
||||||
|
payload.Set("title", req.Title)
|
||||||
|
}
|
||||||
|
if req.Description != "" {
|
||||||
|
payload.Set("description", req.Description)
|
||||||
|
}
|
||||||
|
if req.PatientID != "" {
|
||||||
|
subject := map[string]interface{}{"reference": "Patient/" + req.PatientID}
|
||||||
|
if req.PatientDisplay != "" {
|
||||||
|
subject["display"] = req.PatientDisplay
|
||||||
|
}
|
||||||
|
payload.Set("subject", subject)
|
||||||
|
}
|
||||||
|
if req.EncounterID != "" {
|
||||||
|
encounter := map[string]interface{}{"reference": "Encounter/" + req.EncounterID}
|
||||||
|
if req.EncounterDisplay != "" {
|
||||||
|
encounter["display"] = req.EncounterDisplay
|
||||||
|
}
|
||||||
|
payload.Set("encounter", encounter)
|
||||||
|
}
|
||||||
|
if req.AuthorID != "" {
|
||||||
|
author := map[string]interface{}{"reference": "Practitioner/" + req.AuthorID}
|
||||||
|
if req.AuthorDisplay != "" {
|
||||||
|
author["display"] = req.AuthorDisplay
|
||||||
|
}
|
||||||
|
payload.Set("author", author)
|
||||||
|
}
|
||||||
|
if req.CreatedDate != nil && !req.CreatedDate.IsZero() {
|
||||||
|
payload.Set("created", req.CreatedDate.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
if len(req.GoalIDs) > 0 {
|
||||||
|
var goals []map[string]interface{}
|
||||||
|
for _, id := range req.GoalIDs {
|
||||||
|
goals = append(goals, map[string]interface{}{"reference": "Goal/" + id})
|
||||||
|
}
|
||||||
|
payload.Set("goal", goals)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
@@ -19,14 +20,22 @@ type repository struct{ client satusehat.SatuSehatClient }
|
|||||||
|
|
||||||
func NewRepository(client satusehat.SatuSehatClient) Repository { return &repository{client: client} }
|
func NewRepository(client satusehat.SatuSehatClient) Repository { return &repository{client: client} }
|
||||||
|
|
||||||
|
type hiddenCtx struct {
|
||||||
|
context.Context
|
||||||
|
}
|
||||||
|
|
||||||
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
resp, err := r.client.DoRequest(ctx, method, endpoint, req)
|
resp, err := r.client.DoRequest(hiddenCtx{ctx}, method, endpoint, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.InternalError().Message("Failed to execute request to SatuSehat").Cause(err).Build()
|
||||||
}
|
}
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
if err := json.Unmarshal(resp, &result); err != nil {
|
if err := json.Unmarshal(resp, &result); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
return nil, errors.InternalError().Message("Failed to parse SatuSehat response").Cause(err).Metadata("raw_response", string(resp)).Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
if resourceType, ok := result["resourceType"].(string); ok && resourceType == "OperationOutcome" {
|
||||||
|
return nil, errors.ParseSatuSehatError(result)
|
||||||
}
|
}
|
||||||
var resourceID string
|
var resourceID string
|
||||||
if id, ok := result["id"].(string); ok {
|
if id, ok := result["id"].(string); ok {
|
||||||
|
|||||||
@@ -24,12 +24,23 @@ func (s *service) Update(ctx context.Context, id string, req CarePlanRequest) (*
|
|||||||
if id == "" {
|
if id == "" {
|
||||||
return nil, errors.NewValidationError().Message("ID is required").Build()
|
return nil, errors.NewValidationError().Message("ID is required").Build()
|
||||||
}
|
}
|
||||||
return s.repo.Update(ctx, id, MapRequestToFHIR(req).Set("id", id))
|
payload := MapRequestToFHIR(req)
|
||||||
|
payload.Set("id", id)
|
||||||
|
return s.repo.Update(ctx, id, payload)
|
||||||
}
|
}
|
||||||
func (s *service) Patch(ctx context.Context, id string, req CarePlanPatchRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Patch(ctx context.Context, id string, req CarePlanPatchRequest) (*satusehat.FHIRResponse, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("CarePlan ID is required").Build()
|
||||||
|
}
|
||||||
|
if len(req) == 0 {
|
||||||
|
return nil, errors.NewValidationError().Message("Patch payload cannot be empty").Build()
|
||||||
|
}
|
||||||
return s.repo.Patch(ctx, id, req)
|
return s.repo.Patch(ctx, id, req)
|
||||||
}
|
}
|
||||||
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("CarePlan ID is required").Build()
|
||||||
|
}
|
||||||
return s.repo.GetByID(ctx, id)
|
return s.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
||||||
|
|||||||
@@ -1,17 +1,40 @@
|
|||||||
package clinicalimpression
|
package clinicalimpression
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service/internal/satusehat/common"
|
||||||
|
)
|
||||||
|
|
||||||
type ClinicalImpressionRequest struct {
|
type ClinicalImpressionRequest struct {
|
||||||
PatientID string `json:"patient_id" binding:"required"`
|
OrganizationID string `json:"organization_id,omitempty"`
|
||||||
PatientName string `json:"patient_name" binding:"required"`
|
ClinicalImpressionID string `json:"clinical_impression_id,omitempty"`
|
||||||
EncounterID string `json:"encounter_id" binding:"required"`
|
|
||||||
PractitionerID string `json:"practitioner_id" binding:"required"`
|
|
||||||
PractitionerName string `json:"practitioner_name" binding:"required"`
|
|
||||||
Status string `json:"status" binding:"required,oneof=in-progress completed entered-in-error"`
|
Status string `json:"status" binding:"required,oneof=in-progress completed entered-in-error"`
|
||||||
Date time.Time `json:"date" binding:"required"`
|
Code *common.CodeableConceptDTO `json:"code,omitempty"`
|
||||||
Summary string `json:"summary" binding:"required"`
|
Description string `json:"description,omitempty"`
|
||||||
Description string `json:"description" binding:"required"`
|
PatientID string `json:"patient_id" binding:"required"`
|
||||||
|
PatientDisplay string `json:"patient_display,omitempty"`
|
||||||
|
EncounterID string `json:"encounter_id" binding:"required"`
|
||||||
|
EncounterDisplay string `json:"encounter_display,omitempty"`
|
||||||
|
EffectiveDateTime *time.Time `json:"effective_datetime,omitempty"`
|
||||||
|
Date *time.Time `json:"date,omitempty"`
|
||||||
|
AssessorID string `json:"assessor_id,omitempty"`
|
||||||
|
AssessorDisplay string `json:"assessor_display,omitempty"`
|
||||||
|
Problem []common.ReferenceDTO `json:"problem,omitempty"`
|
||||||
|
Investigations []InvestigationDTO `json:"investigations,omitempty"`
|
||||||
|
Summary string `json:"summary,omitempty"`
|
||||||
|
Findings []FindingDTO `json:"findings,omitempty"`
|
||||||
|
Prognosis []*common.CodeableConceptDTO `json:"prognosis,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InvestigationDTO struct {
|
||||||
|
Code *common.CodeableConceptDTO `json:"code,omitempty"`
|
||||||
|
Item []common.ReferenceDTO `json:"item,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FindingDTO struct {
|
||||||
|
ItemCodeableConcept *common.CodeableConceptDTO `json:"item_codeable_concept,omitempty"`
|
||||||
|
ItemReference *common.ReferenceDTO `json:"item_reference,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClinicalImpressionPatchRequest []map[string]interface{}
|
type ClinicalImpressionPatchRequest []map[string]interface{}
|
||||||
@@ -1,23 +1,186 @@
|
|||||||
package clinicalimpression
|
package clinicalimpression
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MapRequestToFHIR(req ClinicalImpressionRequest) satusehat.FHIRPayload {
|
func MapRequestToFHIR(req ClinicalImpressionRequest) satusehat.FHIRPayload {
|
||||||
return satusehat.NewFHIRPayload("ClinicalImpression").
|
payload := satusehat.NewFHIRPayload("ClinicalImpression")
|
||||||
Set("status", req.Status).
|
|
||||||
Set("description", req.Description).
|
orgID := os.Getenv("SATUSEHAT_ORG_ID")
|
||||||
Set("summary", req.Summary).
|
if orgID == "" {
|
||||||
Set("subject", map[string]interface{}{
|
orgID = req.OrganizationID
|
||||||
"reference": "Patient/" + req.PatientID,
|
}
|
||||||
"display": req.PatientName,
|
|
||||||
}).
|
if orgID != "" && req.ClinicalImpressionID != "" {
|
||||||
Set("encounter", map[string]interface{}{"reference": "Encounter/" + req.EncounterID}).
|
payload.Set("identifier", []map[string]interface{}{
|
||||||
Set("assessor", map[string]interface{}{
|
{
|
||||||
"reference": "Practitioner/" + req.PractitionerID,
|
"system": "http://sys-ids.kemkes.go.id/clinicalimpression/" + orgID,
|
||||||
"display": req.PractitionerName,
|
"use": "official",
|
||||||
}).
|
"value": req.ClinicalImpressionID,
|
||||||
Set("date", req.Date.Format(time.RFC3339))
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Status != "" {
|
||||||
|
payload.Set("status", req.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Code != nil {
|
||||||
|
sys := req.Code.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://snomed.info/sct"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": req.Code.Code}
|
||||||
|
if req.Code.Display != "" {
|
||||||
|
coding["display"] = req.Code.Display
|
||||||
|
}
|
||||||
|
payload.Set("code", map[string]interface{}{
|
||||||
|
"coding": []map[string]interface{}{
|
||||||
|
coding,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Description != "" {
|
||||||
|
payload.Set("description", req.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.PatientID != "" {
|
||||||
|
subject := map[string]interface{}{"reference": "Patient/" + req.PatientID}
|
||||||
|
if req.PatientDisplay != "" {
|
||||||
|
subject["display"] = req.PatientDisplay
|
||||||
|
}
|
||||||
|
payload.Set("subject", subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.EncounterID != "" {
|
||||||
|
encounter := map[string]interface{}{"reference": "Encounter/" + req.EncounterID}
|
||||||
|
if req.EncounterDisplay != "" {
|
||||||
|
encounter["display"] = req.EncounterDisplay
|
||||||
|
}
|
||||||
|
payload.Set("encounter", encounter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.EffectiveDateTime != nil && !req.EffectiveDateTime.IsZero() {
|
||||||
|
payload.Set("effectiveDateTime", req.EffectiveDateTime.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Date != nil && !req.Date.IsZero() {
|
||||||
|
payload.Set("date", req.Date.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.AssessorID != "" {
|
||||||
|
assessor := map[string]interface{}{"reference": "Practitioner/" + req.AssessorID}
|
||||||
|
if req.AssessorDisplay != "" {
|
||||||
|
assessor["display"] = req.AssessorDisplay
|
||||||
|
}
|
||||||
|
payload.Set("assessor", assessor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Problem) > 0 {
|
||||||
|
var problems []map[string]interface{}
|
||||||
|
for _, p := range req.Problem {
|
||||||
|
problem := map[string]interface{}{"reference": p.Reference}
|
||||||
|
if p.Display != "" {
|
||||||
|
problem["display"] = p.Display
|
||||||
|
}
|
||||||
|
problems = append(problems, problem)
|
||||||
|
}
|
||||||
|
payload.Set("problem", problems)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Investigations) > 0 {
|
||||||
|
var investigations []map[string]interface{}
|
||||||
|
for _, inv := range req.Investigations {
|
||||||
|
invObject := map[string]interface{}{}
|
||||||
|
if inv.Code != nil {
|
||||||
|
codeConcept := map[string]interface{}{}
|
||||||
|
sys := inv.Code.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://snomed.info/sct"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": inv.Code.Code}
|
||||||
|
if inv.Code.Display != "" {
|
||||||
|
coding["display"] = inv.Code.Display
|
||||||
|
}
|
||||||
|
codeConcept["coding"] = []map[string]interface{}{coding}
|
||||||
|
if inv.Code.Text != "" {
|
||||||
|
codeConcept["text"] = inv.Code.Text
|
||||||
|
}
|
||||||
|
invObject["code"] = codeConcept
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(inv.Item) > 0 {
|
||||||
|
var items []map[string]interface{}
|
||||||
|
for _, item := range inv.Item {
|
||||||
|
ref := map[string]interface{}{"reference": item.Reference}
|
||||||
|
if item.Display != "" {
|
||||||
|
ref["display"] = item.Display
|
||||||
|
}
|
||||||
|
items = append(items, ref)
|
||||||
|
}
|
||||||
|
invObject["item"] = items
|
||||||
|
}
|
||||||
|
investigations = append(investigations, invObject)
|
||||||
|
}
|
||||||
|
payload.Set("investigation", investigations)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Summary != "" {
|
||||||
|
payload.Set("summary", req.Summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Findings) > 0 {
|
||||||
|
var findings []map[string]interface{}
|
||||||
|
for _, f := range req.Findings {
|
||||||
|
findingObject := map[string]interface{}{}
|
||||||
|
if f.ItemCodeableConcept != nil {
|
||||||
|
sys := f.ItemCodeableConcept.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://hl7.org/fhir/sid/icd-10"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": f.ItemCodeableConcept.Code}
|
||||||
|
if f.ItemCodeableConcept.Display != "" {
|
||||||
|
coding["display"] = f.ItemCodeableConcept.Display
|
||||||
|
}
|
||||||
|
findingObject["itemCodeableConcept"] = map[string]interface{}{
|
||||||
|
"coding": []map[string]interface{}{coding},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if f.ItemReference != nil {
|
||||||
|
ref := map[string]interface{}{"reference": f.ItemReference.Reference}
|
||||||
|
if f.ItemReference.Display != "" {
|
||||||
|
ref["display"] = f.ItemReference.Display
|
||||||
|
}
|
||||||
|
findingObject["itemReference"] = ref
|
||||||
|
}
|
||||||
|
if len(findingObject) > 0 {
|
||||||
|
findings = append(findings, findingObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payload.Set("finding", findings)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Prognosis) > 0 {
|
||||||
|
var prognosis []map[string]interface{}
|
||||||
|
for _, p := range req.Prognosis {
|
||||||
|
if p != nil {
|
||||||
|
sys := p.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://snomed.info/sct"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": p.Code}
|
||||||
|
if p.Display != "" {
|
||||||
|
coding["display"] = p.Display
|
||||||
|
}
|
||||||
|
prognosis = append(prognosis, map[string]interface{}{"coding": []map[string]interface{}{coding}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payload.Set("prognosisCodeableConcept", prognosis)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
@@ -19,14 +20,21 @@ type repository struct{ client satusehat.SatuSehatClient }
|
|||||||
|
|
||||||
func NewRepository(client satusehat.SatuSehatClient) Repository { return &repository{client: client} }
|
func NewRepository(client satusehat.SatuSehatClient) Repository { return &repository{client: client} }
|
||||||
|
|
||||||
|
type hiddenCtx struct {
|
||||||
|
context.Context
|
||||||
|
}
|
||||||
|
|
||||||
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
resp, err := r.client.DoRequest(ctx, method, endpoint, req)
|
resp, err := r.client.DoRequest(hiddenCtx{ctx}, method, endpoint, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.InternalError().Message("Failed to execute request to SatuSehat").Cause(err).Build()
|
||||||
}
|
}
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
if err := json.Unmarshal(resp, &result); err != nil {
|
if err := json.Unmarshal(resp, &result); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
return nil, errors.InternalError().Message("Failed to parse SatuSehat response").Cause(err).Metadata("raw_response", string(resp)).Build()
|
||||||
|
}
|
||||||
|
if resourceType, ok := result["resourceType"].(string); ok && resourceType == "OperationOutcome" {
|
||||||
|
return nil, errors.ParseSatuSehatError(result)
|
||||||
}
|
}
|
||||||
var resourceID string
|
var resourceID string
|
||||||
if id, ok := result["id"].(string); ok {
|
if id, ok := result["id"].(string); ok {
|
||||||
|
|||||||
@@ -24,12 +24,23 @@ func (s *service) Update(ctx context.Context, id string, req ClinicalImpressionR
|
|||||||
if id == "" {
|
if id == "" {
|
||||||
return nil, errors.NewValidationError().Message("ID is required").Build()
|
return nil, errors.NewValidationError().Message("ID is required").Build()
|
||||||
}
|
}
|
||||||
return s.repo.Update(ctx, id, MapRequestToFHIR(req).Set("id", id))
|
payload := MapRequestToFHIR(req)
|
||||||
|
payload.Set("id", id)
|
||||||
|
return s.repo.Update(ctx, id, payload)
|
||||||
}
|
}
|
||||||
func (s *service) Patch(ctx context.Context, id string, req ClinicalImpressionPatchRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Patch(ctx context.Context, id string, req ClinicalImpressionPatchRequest) (*satusehat.FHIRResponse, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("ID is required").Build()
|
||||||
|
}
|
||||||
|
if len(req) == 0 {
|
||||||
|
return nil, errors.NewValidationError().Message("Patch payload cannot be empty").Build()
|
||||||
|
}
|
||||||
return s.repo.Patch(ctx, id, req)
|
return s.repo.Patch(ctx, id, req)
|
||||||
}
|
}
|
||||||
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("ID is required").Build()
|
||||||
|
}
|
||||||
return s.repo.GetByID(ctx, id)
|
return s.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
||||||
|
|||||||
@@ -1,17 +1,37 @@
|
|||||||
package composition
|
package composition
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service/internal/satusehat/common"
|
||||||
|
)
|
||||||
|
|
||||||
type CompositionRequest struct {
|
type CompositionRequest struct {
|
||||||
|
OrganizationID string `json:"organization_id,omitempty"`
|
||||||
|
CompositionID string `json:"composition_id,omitempty"`
|
||||||
|
Status string `json:"status" binding:"required,oneof=preliminary final amended entered-in-error"`
|
||||||
|
Type *common.CodeableConceptDTO `json:"type" binding:"required"`
|
||||||
|
Category []*common.CodeableConceptDTO `json:"category,omitempty"`
|
||||||
PatientID string `json:"patient_id" binding:"required"`
|
PatientID string `json:"patient_id" binding:"required"`
|
||||||
PatientName string `json:"patient_name" binding:"required"`
|
PatientDisplay string `json:"patient_display,omitempty"`
|
||||||
EncounterID string `json:"encounter_id" binding:"required"`
|
EncounterID string `json:"encounter_id" binding:"required"`
|
||||||
PractitionerID string `json:"practitioner_id" binding:"required"`
|
EncounterDisplay string `json:"encounter_display,omitempty"`
|
||||||
PractitionerName string `json:"practitioner_name" binding:"required"`
|
Date *time.Time `json:"date" binding:"required"`
|
||||||
|
Author []common.ReferenceDTO `json:"author" binding:"required,min=1"`
|
||||||
Title string `json:"title" binding:"required"`
|
Title string `json:"title" binding:"required"`
|
||||||
Status string `json:"status" binding:"required,oneof=preliminary final amended entered-in-error"` // preliminary, final, amended, entered-in-error
|
Sections []SectionDTO `json:"sections,omitempty"`
|
||||||
Date time.Time `json:"date" binding:"required"`
|
}
|
||||||
|
|
||||||
|
type SectionDTO struct {
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Code *common.CodeableConceptDTO `json:"code,omitempty"`
|
||||||
|
Text *NarrativeDTO `json:"text,omitempty"`
|
||||||
|
Entries []common.ReferenceDTO `json:"entries,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NarrativeDTO struct {
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
Div string `json:"div,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompositionPatchRequest merepresentasikan payload operasi JSON Patch.
|
|
||||||
type CompositionPatchRequest []map[string]interface{}
|
type CompositionPatchRequest []map[string]interface{}
|
||||||
@@ -1,38 +1,145 @@
|
|||||||
package composition
|
package composition
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MapRequestToFHIR(req CompositionRequest) satusehat.FHIRPayload {
|
func MapRequestToFHIR(req CompositionRequest) satusehat.FHIRPayload {
|
||||||
payload := satusehat.NewFHIRPayload("Composition").
|
payload := satusehat.NewFHIRPayload("Composition")
|
||||||
Set("status", req.Status).
|
|
||||||
Set("type", map[string]interface{}{
|
|
||||||
"coding": []map[string]interface{}{
|
|
||||||
{
|
|
||||||
"system": "http://loinc.org",
|
|
||||||
"code": "11503-0", // Example: Medical records
|
|
||||||
"display": "Medical records",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).
|
|
||||||
Set("subject", map[string]interface{}{
|
|
||||||
"reference": "Patient/" + req.PatientID,
|
|
||||||
"display": req.PatientName,
|
|
||||||
}).
|
|
||||||
Set("encounter", map[string]interface{}{
|
|
||||||
"reference": "Encounter/" + req.EncounterID,
|
|
||||||
}).
|
|
||||||
Set("date", req.Date.Format(time.RFC3339)).
|
|
||||||
Set("title", req.Title)
|
|
||||||
|
|
||||||
if req.PractitionerID != "" {
|
orgID := os.Getenv("SATUSEHAT_ORG_ID")
|
||||||
payload.Append("author", map[string]interface{}{
|
if orgID == "" {
|
||||||
"reference": "Practitioner/" + req.PractitionerID,
|
orgID = req.OrganizationID
|
||||||
"display": req.PractitionerName,
|
}
|
||||||
|
|
||||||
|
if orgID != "" && req.CompositionID != "" {
|
||||||
|
payload.Set("identifier", map[string]interface{}{
|
||||||
|
"system": "http://sys-ids.kemkes.go.id/composition/" + orgID,
|
||||||
|
"value": req.CompositionID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
status := req.Status
|
||||||
|
if req.Status != "" {
|
||||||
|
status = req.Status
|
||||||
|
} else {
|
||||||
|
status = "final"
|
||||||
|
}
|
||||||
|
payload.Set("status", status)
|
||||||
|
|
||||||
|
if req.Type != nil {
|
||||||
|
sys := req.Type.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://loinc.org"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": req.Type.Code}
|
||||||
|
if req.Type.Display != "" {
|
||||||
|
coding["display"] = req.Type.Display
|
||||||
|
}
|
||||||
|
typeConcept := map[string]interface{}{"coding": []map[string]interface{}{coding}}
|
||||||
|
if req.Type.Text != "" {
|
||||||
|
typeConcept["text"] = req.Type.Text
|
||||||
|
}
|
||||||
|
payload.Set("type", typeConcept)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Category) > 0 {
|
||||||
|
var categories []map[string]interface{}
|
||||||
|
for _, c := range req.Category {
|
||||||
|
if c != nil {
|
||||||
|
sys := c.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://loinc.org"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": c.Code}
|
||||||
|
if c.Display != "" {
|
||||||
|
coding["display"] = c.Display
|
||||||
|
}
|
||||||
|
categories = append(categories, map[string]interface{}{"coding": []map[string]interface{}{coding}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payload.Set("category", categories)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.PatientID != "" {
|
||||||
|
subj := map[string]interface{}{"reference": "Patient/" + req.PatientID}
|
||||||
|
if req.PatientDisplay != "" {
|
||||||
|
subj["display"] = req.PatientDisplay
|
||||||
|
}
|
||||||
|
payload.Set("subject", subj)
|
||||||
|
}
|
||||||
|
if req.EncounterID != "" {
|
||||||
|
enc := map[string]interface{}{"reference": "Encounter/" + req.EncounterID}
|
||||||
|
if req.EncounterDisplay != "" {
|
||||||
|
enc["display"] = req.EncounterDisplay
|
||||||
|
}
|
||||||
|
payload.Set("encounter", enc)
|
||||||
|
}
|
||||||
|
if req.Date != nil && !req.Date.IsZero() {
|
||||||
|
payload.Set("date", req.Date.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
if req.Title != "" {
|
||||||
|
payload.Set("title", req.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Author) > 0 {
|
||||||
|
var authors []map[string]interface{}
|
||||||
|
for _, a := range req.Author {
|
||||||
|
author := map[string]interface{}{"reference": a.Reference}
|
||||||
|
if a.Display != "" {
|
||||||
|
author["display"] = a.Display
|
||||||
|
}
|
||||||
|
authors = append(authors, author)
|
||||||
|
}
|
||||||
|
payload.Set("author", authors)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Sections) > 0 {
|
||||||
|
var sections []map[string]interface{}
|
||||||
|
for _, s := range req.Sections {
|
||||||
|
sec := map[string]interface{}{}
|
||||||
|
if s.Title != "" {
|
||||||
|
sec["title"] = s.Title
|
||||||
|
}
|
||||||
|
if s.Code != nil {
|
||||||
|
sys := s.Code.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://loinc.org"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": s.Code.Code}
|
||||||
|
if s.Code.Display != "" {
|
||||||
|
coding["display"] = s.Code.Display
|
||||||
|
}
|
||||||
|
sec["code"] = map[string]interface{}{"coding": []map[string]interface{}{coding}}
|
||||||
|
}
|
||||||
|
if s.Text != nil {
|
||||||
|
status := s.Text.Status
|
||||||
|
if status == "" {
|
||||||
|
status = "generated"
|
||||||
|
}
|
||||||
|
sec["text"] = map[string]interface{}{
|
||||||
|
"status": status,
|
||||||
|
"div": s.Text.Div,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(s.Entries) > 0 {
|
||||||
|
var entries []map[string]interface{}
|
||||||
|
for _, e := range s.Entries {
|
||||||
|
entry := map[string]interface{}{"reference": e.Reference}
|
||||||
|
if e.Display != "" {
|
||||||
|
entry["display"] = e.Display
|
||||||
|
}
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
sec["entry"] = entries
|
||||||
|
}
|
||||||
|
sections = append(sections, sec)
|
||||||
|
}
|
||||||
|
payload.Set("section", sections)
|
||||||
|
}
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
@@ -25,22 +26,26 @@ func NewRepository(client satusehat.SatuSehatClient) Repository {
|
|||||||
return &repository{client: client}
|
return &repository{client: client}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
type hiddenCtx struct {
|
||||||
resp, err := r.client.DoRequest(ctx, method, endpoint, req)
|
context.Context
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
|
resp, err := r.client.DoRequest(hiddenCtx{ctx}, method, endpoint, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.InternalError().Message("Failed to execute request to SatuSehat").Cause(err).Build()
|
||||||
|
}
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
if err := json.Unmarshal(resp, &result); err != nil {
|
if err := json.Unmarshal(resp, &result); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
return nil, errors.InternalError().Message("Failed to parse SatuSehat response").Cause(err).Metadata("raw_response", string(resp)).Build()
|
||||||
|
}
|
||||||
|
if resourceType, ok := result["resourceType"].(string); ok && resourceType == "OperationOutcome" {
|
||||||
|
return nil, errors.ParseSatuSehatError(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
var resourceID string
|
var resourceID string
|
||||||
if id, ok := result["id"].(string); ok {
|
if id, ok := result["id"].(string); ok {
|
||||||
resourceID = id
|
resourceID = id
|
||||||
}
|
}
|
||||||
|
|
||||||
return &satusehat.FHIRResponse{
|
return &satusehat.FHIRResponse{
|
||||||
ID: resourceID,
|
ID: resourceID,
|
||||||
FullResponse: result,
|
FullResponse: result,
|
||||||
|
|||||||
@@ -25,17 +25,14 @@ func NewService(repo Repository) Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Create(ctx context.Context, req CompositionRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Create(ctx context.Context, req CompositionRequest) (*satusehat.FHIRResponse, error) {
|
||||||
fhirPayload := MapRequestToFHIR(req)
|
return s.repo.Create(ctx, MapRequestToFHIR(req))
|
||||||
return s.repo.Create(ctx, fhirPayload)
|
|
||||||
}
|
}
|
||||||
func (s *service) Update(ctx context.Context, id string, req CompositionRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Update(ctx context.Context, id string, req CompositionRequest) (*satusehat.FHIRResponse, error) {
|
||||||
if id == "" {
|
if id == "" {
|
||||||
return nil, errors.NewValidationError().Message("Composition ID is required").Build()
|
return nil, errors.NewValidationError().Message("Composition ID is required").Build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fhirPayload := MapRequestToFHIR(req)
|
fhirPayload := MapRequestToFHIR(req)
|
||||||
fhirPayload.Set("id", id)
|
fhirPayload.Set("id", id)
|
||||||
|
|
||||||
return s.repo.Update(ctx, id, fhirPayload)
|
return s.repo.Update(ctx, id, fhirPayload)
|
||||||
}
|
}
|
||||||
func (s *service) Patch(ctx context.Context, id string, req CompositionPatchRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Patch(ctx context.Context, id string, req CompositionPatchRequest) (*satusehat.FHIRResponse, error) {
|
||||||
@@ -48,6 +45,9 @@ func (s *service) Patch(ctx context.Context, id string, req CompositionPatchRequ
|
|||||||
return s.repo.Patch(ctx, id, req)
|
return s.repo.Patch(ctx, id, req)
|
||||||
}
|
}
|
||||||
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("Composition ID is required").Build()
|
||||||
|
}
|
||||||
return s.repo.GetByID(ctx, id)
|
return s.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
package condition
|
package condition
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service/internal/satusehat/common"
|
||||||
|
)
|
||||||
|
|
||||||
type ConditionRequest struct {
|
type ConditionRequest struct {
|
||||||
|
OrganizationID string `json:"organization_id,omitempty"`
|
||||||
|
ConditionID string `json:"condition_id,omitempty"`
|
||||||
PatientID string `json:"patient_id" binding:"required"`
|
PatientID string `json:"patient_id" binding:"required"`
|
||||||
PatientName string `json:"patient_name" binding:"required"`
|
PatientName string `json:"patient_name,omitempty"`
|
||||||
EncounterID string `json:"encounter_id" binding:"required"`
|
EncounterID string `json:"encounter_id" binding:"required"`
|
||||||
ClinicalStatus string `json:"clinical_status" binding:"required,oneof=active recurrence relapse inactive remission resolved"` // active, recurrence, relapse, inactive, remission, resolved
|
ClinicalStatus string `json:"clinical_status" binding:"required,oneof=active recurrence relapse inactive remission resolved"`
|
||||||
CategoryCode string `json:"category_code" binding:"required"` // problem-list-item, encounter-diagnosis
|
Category *common.CodeableConceptDTO `json:"category,omitempty"`
|
||||||
CategoryDisplay string `json:"category_display" binding:"required"`
|
Code *common.CodeableConceptDTO `json:"code,omitempty" binding:"required"`
|
||||||
Code string `json:"code" binding:"required"` // SNOMED CT / ICD-10 code
|
OnsetDateTime *time.Time `json:"onset_date_time,omitempty"`
|
||||||
Display string `json:"display" binding:"required"` // Diagnosis text
|
RecordedDate *time.Time `json:"recorded_date,omitempty"`
|
||||||
OnsetDateTime time.Time `json:"onset_date_time" binding:"required"`
|
|
||||||
RecordedDate time.Time `json:"recorded_date" binding:"required"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConditionPatchRequest []map[string]interface{}
|
type ConditionPatchRequest []map[string]interface{}
|
||||||
@@ -1,50 +1,91 @@
|
|||||||
package condition
|
package condition
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MapRequestToFHIR(req ConditionRequest) satusehat.FHIRPayload {
|
func MapRequestToFHIR(req ConditionRequest) satusehat.FHIRPayload {
|
||||||
payload := satusehat.NewFHIRPayload("Condition").
|
payload := satusehat.NewFHIRPayload("Condition")
|
||||||
Set("clinicalStatus", map[string]interface{}{
|
|
||||||
|
orgID := os.Getenv("SATUSEHAT_ORG_ID")
|
||||||
|
if orgID == "" {
|
||||||
|
orgID = req.OrganizationID
|
||||||
|
}
|
||||||
|
|
||||||
|
if orgID != "" && req.ConditionID != "" {
|
||||||
|
payload.Set("identifier", []map[string]interface{}{
|
||||||
|
{"system": "http://sys-ids.kemkes.go.id/condition/" + orgID, "use": "official", "value": req.ConditionID},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ClinicalStatus != "" {
|
||||||
|
payload.Set("clinicalStatus", map[string]interface{}{
|
||||||
"coding": []map[string]interface{}{
|
"coding": []map[string]interface{}{
|
||||||
{
|
{"system": "http://terminology.hl7.org/CodeSystem/condition-clinical", "code": req.ClinicalStatus},
|
||||||
"system": "http://terminology.hl7.org/CodeSystem/condition-clinical",
|
|
||||||
"code": req.ClinicalStatus,
|
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
}).
|
}
|
||||||
Set("category", []map[string]interface{}{
|
if req.Category != nil {
|
||||||
|
sys := req.Category.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://terminology.hl7.org/CodeSystem/condition-category"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{
|
||||||
|
"system": sys,
|
||||||
|
"code": req.Category.Code,
|
||||||
|
}
|
||||||
|
if req.Category.Display != "" {
|
||||||
|
coding["display"] = req.Category.Display
|
||||||
|
}
|
||||||
|
payload.Set("category", []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"coding": []map[string]interface{}{
|
"coding": []map[string]interface{}{
|
||||||
{
|
coding,
|
||||||
"system": "http://terminology.hl7.org/CodeSystem/condition-category",
|
|
||||||
"code": req.CategoryCode,
|
|
||||||
"display": req.CategoryDisplay,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
}).
|
}
|
||||||
Set("code", map[string]interface{}{
|
if req.Code != nil {
|
||||||
|
sys := req.Code.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://snomed.info/sct" // Default value jika tidak diset
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{
|
||||||
|
"system": sys,
|
||||||
|
"code": req.Code.Code,
|
||||||
|
}
|
||||||
|
if req.Code.Display != "" {
|
||||||
|
coding["display"] = req.Code.Display
|
||||||
|
}
|
||||||
|
codeConcept := map[string]interface{}{
|
||||||
"coding": []map[string]interface{}{
|
"coding": []map[string]interface{}{
|
||||||
{
|
coding,
|
||||||
"system": "http://snomed.info/sct",
|
|
||||||
"code": req.Code,
|
|
||||||
"display": req.Display,
|
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
}).
|
if req.Code.Text != "" {
|
||||||
Set("subject", map[string]interface{}{
|
codeConcept["text"] = req.Code.Text
|
||||||
"reference": "Patient/" + req.PatientID,
|
}
|
||||||
"display": req.PatientName,
|
payload.Set("code", codeConcept)
|
||||||
}).
|
}
|
||||||
Set("encounter", map[string]interface{}{
|
if req.PatientID != "" {
|
||||||
"reference": "Encounter/" + req.EncounterID,
|
subject := map[string]interface{}{"reference": "Patient/" + req.PatientID}
|
||||||
}).
|
if req.PatientName != "" {
|
||||||
Set("onsetDateTime", req.OnsetDateTime.Format(time.RFC3339)).
|
subject["display"] = req.PatientName
|
||||||
Set("recordedDate", req.RecordedDate.Format(time.RFC3339))
|
}
|
||||||
|
payload.Set("subject", subject)
|
||||||
|
}
|
||||||
|
if req.EncounterID != "" {
|
||||||
|
payload.Set("encounter", map[string]interface{}{"reference": "Encounter/" + req.EncounterID})
|
||||||
|
}
|
||||||
|
if req.OnsetDateTime != nil && !req.OnsetDateTime.IsZero() {
|
||||||
|
payload.Set("onsetDateTime", req.OnsetDateTime.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
if req.RecordedDate != nil && !req.RecordedDate.IsZero() {
|
||||||
|
payload.Set("recordedDate", req.RecordedDate.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
@@ -24,15 +25,23 @@ func NewRepository(client satusehat.SatuSehatClient) Repository {
|
|||||||
return &repository{client: client}
|
return &repository{client: client}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type hiddenCtx struct {
|
||||||
|
context.Context
|
||||||
|
}
|
||||||
|
|
||||||
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
resp, err := r.client.DoRequest(ctx, method, endpoint, req)
|
resp, err := r.client.DoRequest(hiddenCtx{ctx}, method, endpoint, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.InternalError().Message("Failed to execute request to SatuSehat").Cause(err).Build()
|
||||||
}
|
}
|
||||||
|
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
if err := json.Unmarshal(resp, &result); err != nil {
|
if err := json.Unmarshal(resp, &result); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
return nil, errors.InternalError().Message("Failed to parse SatuSehat response").Cause(err).Metadata("raw_response", string(resp)).Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
if resourceType, ok := result["resourceType"].(string); ok && resourceType == "OperationOutcome" {
|
||||||
|
return nil, errors.ParseSatuSehatError(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
var resourceID string
|
var resourceID string
|
||||||
@@ -40,11 +49,7 @@ func (r *repository) executeRequest(ctx context.Context, method, endpoint string
|
|||||||
resourceID = id
|
resourceID = id
|
||||||
}
|
}
|
||||||
|
|
||||||
return &satusehat.FHIRResponse{
|
return &satusehat.FHIRResponse{ID: resourceID, FullResponse: result, RawResponse: resp}, nil
|
||||||
ID: resourceID,
|
|
||||||
FullResponse: result,
|
|
||||||
RawResponse: resp,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *repository) Create(ctx context.Context, payload interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) Create(ctx context.Context, payload interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
|
|||||||
@@ -57,6 +57,5 @@ func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRRespon
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
||||||
// Tambahkan validasi untuk query params jika diperlukan
|
|
||||||
return s.repo.Search(ctx, queryParams)
|
return s.repo.Search(ctx, queryParams)
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,28 @@
|
|||||||
package diagnosticreport
|
package diagnosticreport
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service/internal/satusehat/common"
|
||||||
|
)
|
||||||
|
|
||||||
type DiagnosticReportRequest struct {
|
type DiagnosticReportRequest struct {
|
||||||
PatientID string `json:"patient_id" binding:"required"`
|
OrganizationID string `json:"organization_id,omitempty"`
|
||||||
PatientName string `json:"patient_name" binding:"required"`
|
DiagnosticID string `json:"diagnostic_id,omitempty"` // e.g. "5234342"
|
||||||
EncounterID string `json:"encounter_id" binding:"required"`
|
|
||||||
PractitionerID string `json:"practitioner_id" binding:"required"`
|
|
||||||
PractitionerName string `json:"practitioner_name" binding:"required"`
|
|
||||||
Status string `json:"status" binding:"required,oneof=registered partial preliminary final amended corrected appended cancelled entered-in-error unknown"`
|
Status string `json:"status" binding:"required,oneof=registered partial preliminary final amended corrected appended cancelled entered-in-error unknown"`
|
||||||
CategoryCode string `json:"category_code" binding:"required"` // e.g. LAB
|
Category []*common.CodeableConceptDTO `json:"category,omitempty"`
|
||||||
CategoryDisplay string `json:"category_display" binding:"required"`
|
Code *common.CodeableConceptDTO `json:"code" binding:"required"`
|
||||||
Code string `json:"code" binding:"required"` // LOINC code
|
PatientID string `json:"patient_id" binding:"required"`
|
||||||
Display string `json:"display" binding:"required"`
|
EncounterID string `json:"encounter_id" binding:"required"`
|
||||||
Issued time.Time `json:"issued" binding:"required"`
|
EffectiveDateTime *time.Time `json:"effective_datetime,omitempty"`
|
||||||
Conclusion string `json:"conclusion" binding:"required"`
|
Issued *time.Time `json:"issued,omitempty"`
|
||||||
|
Performer []common.ReferenceDTO `json:"performer,omitempty"`
|
||||||
|
Result []common.ReferenceDTO `json:"result,omitempty"`
|
||||||
|
Specimen []common.ReferenceDTO `json:"specimen,omitempty"`
|
||||||
|
BasedOn []common.ReferenceDTO `json:"based_on,omitempty"`
|
||||||
|
ImagingStudy []common.ReferenceDTO `json:"imaging_study,omitempty"`
|
||||||
|
ConclusionCode []*common.CodeableConceptDTO `json:"conclusion_code,omitempty"`
|
||||||
|
Conclusion string `json:"conclusion,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DiagnosticReportPatchRequest []map[string]interface{}
|
type DiagnosticReportPatchRequest []map[string]interface{}
|
||||||
@@ -1,41 +1,169 @@
|
|||||||
package diagnosticreport
|
package diagnosticreport
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"service/internal/interfaces/satusehat"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"service/internal/interfaces/satusehat"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MapRequestToFHIR(req DiagnosticReportRequest) satusehat.FHIRPayload {
|
func MapRequestToFHIR(req DiagnosticReportRequest) satusehat.FHIRPayload {
|
||||||
return satusehat.NewFHIRPayload("DiagnosticReport").
|
payload := satusehat.NewFHIRPayload("DiagnosticReport")
|
||||||
Set("status", req.Status).
|
|
||||||
Set("category", []map[string]interface{}{
|
orgID := os.Getenv("SATUSEHAT_ORG_ID")
|
||||||
|
if orgID == "" {
|
||||||
|
orgID = req.OrganizationID
|
||||||
|
}
|
||||||
|
|
||||||
|
if orgID != "" && req.DiagnosticID != "" {
|
||||||
|
payload.Set("identifier", []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"coding": []map[string]interface{}{
|
"system": "http://sys-ids.kemkes.go.id/diagnostic/" + orgID + "/lab",
|
||||||
{
|
"use": "official",
|
||||||
"system": "http://terminology.hl7.org/CodeSystem/v2-0074",
|
"value": req.DiagnosticID,
|
||||||
"code": req.CategoryCode,
|
|
||||||
"display": req.CategoryDisplay,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).
|
|
||||||
Set("code", map[string]interface{}{
|
|
||||||
"coding": []map[string]interface{}{
|
|
||||||
{
|
|
||||||
"system": "http://loinc.org",
|
|
||||||
"code": req.Code,
|
|
||||||
"display": req.Display,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}).
|
})
|
||||||
Set("subject", map[string]interface{}{
|
}
|
||||||
|
|
||||||
|
if req.Status != "" {
|
||||||
|
payload.Set("status", req.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Category) > 0 {
|
||||||
|
var categories []map[string]interface{}
|
||||||
|
for _, c := range req.Category {
|
||||||
|
if c != nil {
|
||||||
|
sys := c.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://terminology.hl7.org/CodeSystem/v2-0074"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": c.Code}
|
||||||
|
if c.Display != "" {
|
||||||
|
coding["display"] = c.Display
|
||||||
|
}
|
||||||
|
categories = append(categories, map[string]interface{}{"coding": []map[string]interface{}{coding}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payload.Set("category", categories)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Code != nil {
|
||||||
|
sys := req.Code.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://loinc.org"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": req.Code.Code}
|
||||||
|
if req.Code.Display != "" {
|
||||||
|
coding["display"] = req.Code.Display
|
||||||
|
}
|
||||||
|
codeConcept := map[string]interface{}{"coding": []map[string]interface{}{coding}}
|
||||||
|
if req.Code.Text != "" {
|
||||||
|
codeConcept["text"] = req.Code.Text
|
||||||
|
}
|
||||||
|
payload.Set("code", codeConcept)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.PatientID != "" {
|
||||||
|
payload.Set("subject", map[string]interface{}{
|
||||||
"reference": "Patient/" + req.PatientID,
|
"reference": "Patient/" + req.PatientID,
|
||||||
"display": req.PatientName,
|
})
|
||||||
}).
|
}
|
||||||
Set("encounter", map[string]interface{}{"reference": "Encounter/" + req.EncounterID}).
|
|
||||||
Set("performer", []map[string]interface{}{
|
if req.EncounterID != "" {
|
||||||
{"reference": "Practitioner/" + req.PractitionerID, "display": req.PractitionerName},
|
payload.Set("encounter", map[string]interface{}{
|
||||||
}).
|
"reference": "Encounter/" + req.EncounterID,
|
||||||
Set("issued", req.Issued.Format(time.RFC3339)).
|
})
|
||||||
Set("conclusion", req.Conclusion)
|
}
|
||||||
|
|
||||||
|
if req.EffectiveDateTime != nil && !req.EffectiveDateTime.IsZero() {
|
||||||
|
payload.Set("effectiveDateTime", req.EffectiveDateTime.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Issued != nil && !req.Issued.IsZero() {
|
||||||
|
payload.Set("issued", req.Issued.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Performer) > 0 {
|
||||||
|
var performers []map[string]interface{}
|
||||||
|
for _, p := range req.Performer {
|
||||||
|
perf := map[string]interface{}{"reference": p.Reference}
|
||||||
|
if p.Display != "" {
|
||||||
|
perf["display"] = p.Display
|
||||||
|
}
|
||||||
|
performers = append(performers, perf)
|
||||||
|
}
|
||||||
|
payload.Set("performer", performers)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Result) > 0 {
|
||||||
|
var results []map[string]interface{}
|
||||||
|
for _, r := range req.Result {
|
||||||
|
res := map[string]interface{}{"reference": r.Reference}
|
||||||
|
if r.Display != "" {
|
||||||
|
res["display"] = r.Display
|
||||||
|
}
|
||||||
|
results = append(results, res)
|
||||||
|
}
|
||||||
|
payload.Set("result", results)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Specimen) > 0 {
|
||||||
|
var specimens []map[string]interface{}
|
||||||
|
for _, s := range req.Specimen {
|
||||||
|
spec := map[string]interface{}{"reference": s.Reference}
|
||||||
|
if s.Display != "" {
|
||||||
|
spec["display"] = s.Display
|
||||||
|
}
|
||||||
|
specimens = append(specimens, spec)
|
||||||
|
}
|
||||||
|
payload.Set("specimen", specimens)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.BasedOn) > 0 {
|
||||||
|
var basedOns []map[string]interface{}
|
||||||
|
for _, b := range req.BasedOn {
|
||||||
|
bo := map[string]interface{}{"reference": b.Reference}
|
||||||
|
if b.Display != "" {
|
||||||
|
bo["display"] = b.Display
|
||||||
|
}
|
||||||
|
basedOns = append(basedOns, bo)
|
||||||
|
}
|
||||||
|
payload.Set("basedOn", basedOns)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.ImagingStudy) > 0 {
|
||||||
|
var imagingStudies []map[string]interface{}
|
||||||
|
for _, i := range req.ImagingStudy {
|
||||||
|
is := map[string]interface{}{"reference": i.Reference}
|
||||||
|
if i.Display != "" {
|
||||||
|
is["display"] = i.Display
|
||||||
|
}
|
||||||
|
imagingStudies = append(imagingStudies, is)
|
||||||
|
}
|
||||||
|
payload.Set("imagingStudy", imagingStudies)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.ConclusionCode) > 0 {
|
||||||
|
var concCodes []map[string]interface{}
|
||||||
|
for _, c := range req.ConclusionCode {
|
||||||
|
if c != nil {
|
||||||
|
sys := c.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://snomed.info/sct"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": c.Code}
|
||||||
|
if c.Display != "" {
|
||||||
|
coding["display"] = c.Display
|
||||||
|
}
|
||||||
|
concCodes = append(concCodes, map[string]interface{}{"coding": []map[string]interface{}{coding}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payload.Set("conclusionCode", concCodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Conclusion != "" {
|
||||||
|
payload.Set("conclusion", req.Conclusion)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
@@ -19,14 +20,21 @@ type repository struct{ client satusehat.SatuSehatClient }
|
|||||||
|
|
||||||
func NewRepository(client satusehat.SatuSehatClient) Repository { return &repository{client: client} }
|
func NewRepository(client satusehat.SatuSehatClient) Repository { return &repository{client: client} }
|
||||||
|
|
||||||
|
type hiddenCtx struct {
|
||||||
|
context.Context
|
||||||
|
}
|
||||||
|
|
||||||
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
resp, err := r.client.DoRequest(ctx, method, endpoint, req)
|
resp, err := r.client.DoRequest(hiddenCtx{ctx}, method, endpoint, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.InternalError().Message("Failed to execute request to SatuSehat").Cause(err).Build()
|
||||||
}
|
}
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
if err := json.Unmarshal(resp, &result); err != nil {
|
if err := json.Unmarshal(resp, &result); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
return nil, errors.InternalError().Message("Failed to parse SatuSehat response").Cause(err).Metadata("raw_response", string(resp)).Build()
|
||||||
|
}
|
||||||
|
if resourceType, ok := result["resourceType"].(string); ok && resourceType == "OperationOutcome" {
|
||||||
|
return nil, errors.ParseSatuSehatError(result)
|
||||||
}
|
}
|
||||||
var resourceID string
|
var resourceID string
|
||||||
if id, ok := result["id"].(string); ok {
|
if id, ok := result["id"].(string); ok {
|
||||||
|
|||||||
@@ -24,12 +24,23 @@ func (s *service) Update(ctx context.Context, id string, req DiagnosticReportReq
|
|||||||
if id == "" {
|
if id == "" {
|
||||||
return nil, errors.NewValidationError().Message("ID is required").Build()
|
return nil, errors.NewValidationError().Message("ID is required").Build()
|
||||||
}
|
}
|
||||||
return s.repo.Update(ctx, id, MapRequestToFHIR(req).Set("id", id))
|
payload := MapRequestToFHIR(req)
|
||||||
|
payload.Set("id", id)
|
||||||
|
return s.repo.Update(ctx, id, payload)
|
||||||
}
|
}
|
||||||
func (s *service) Patch(ctx context.Context, id string, req DiagnosticReportPatchRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Patch(ctx context.Context, id string, req DiagnosticReportPatchRequest) (*satusehat.FHIRResponse, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("ID is required").Build()
|
||||||
|
}
|
||||||
|
if len(req) == 0 {
|
||||||
|
return nil, errors.NewValidationError().Message("Patch payload cannot be empty").Build()
|
||||||
|
}
|
||||||
return s.repo.Patch(ctx, id, req)
|
return s.repo.Patch(ctx, id, req)
|
||||||
}
|
}
|
||||||
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("ID is required").Build()
|
||||||
|
}
|
||||||
return s.repo.GetByID(ctx, id)
|
return s.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
||||||
|
|||||||
@@ -1,44 +1,40 @@
|
|||||||
package encounter
|
package encounter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"service/internal/satusehat/common"
|
||||||
"time"
|
"service/pkg/utils/custom"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EncounterRequest merepresentasikan data input untuk membuat atau memperbarui Encounter.
|
|
||||||
type EncounterRequest struct {
|
type EncounterRequest struct {
|
||||||
EncounterID string `json:"encounter_id" binding:"required"`
|
OrganizationID string `json:"organization_id,omitempty"`
|
||||||
OrganizationID string `json:"organization_id" binding:"required"`
|
EncounterID string `json:"encounter_id,omitempty"`
|
||||||
EpisodeOfCareID string `json:"episode_of_care_id,omitempty"`
|
Status string `json:"status" binding:"required,oneof=planned arrived triaged in-progress onleave finished cancelled entered-in-error unknown"`
|
||||||
PatientID string `json:"patient_id" binding:"required"`
|
Class string `json:"class" binding:"required,oneof=AMB EMER IMP"`
|
||||||
PatientName string `json:"patient_name" binding:"required"`
|
Subject *common.ReferenceDTO `json:"subject" binding:"required"`
|
||||||
PractitionerID string `json:"practitioner_id" binding:"required"`
|
Participant []common.ReferenceDTO `json:"participant,omitempty"`
|
||||||
PractitionerName string `json:"practitioner_name" binding:"required"`
|
Location []LocationDTO `json:"location,omitempty"`
|
||||||
LocationID string `json:"location_id" binding:"required"`
|
PeriodStart *custom.CustomTime `json:"period_start" binding:"required"`
|
||||||
LocationName string `json:"location_name" binding:"required"`
|
PeriodEnd *custom.CustomTime `json:"period_end,omitempty"`
|
||||||
Status string `json:"status" binding:"required,oneof=arrived in-progress finished cancelled"` // arrived, in-progress, finished, cancelled
|
Diagnosis []DiagnosisDTO `json:"diagnosis,omitempty"`
|
||||||
Class string `json:"class" binding:"required,oneof=AMB IMP EMER"` // AMB (ambulatory), IMP (inpatient), EMER (emergency)
|
StatusHistory []StatusHistoryDTO `json:"status_history,omitempty"`
|
||||||
PeriodStart time.Time `json:"period_start" binding:"required"`
|
}
|
||||||
PeriodEnd *time.Time `json:"period_end,omitempty"`
|
|
||||||
|
type LocationDTO struct {
|
||||||
|
Location *common.ReferenceDTO `json:"location,omitempty"`
|
||||||
|
PeriodStart *custom.CustomTime `json:"period_start,omitempty"`
|
||||||
|
PeriodEnd *custom.CustomTime `json:"period_end,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiagnosisDTO struct {
|
||||||
|
Condition *common.ReferenceDTO `json:"condition,omitempty"`
|
||||||
|
Use *common.CodeableConceptDTO `json:"use,omitempty"`
|
||||||
|
Rank int `json:"rank,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatusHistoryDTO struct {
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
PeriodStart *custom.CustomTime `json:"period_start,omitempty"`
|
||||||
|
PeriodEnd *custom.CustomTime `json:"period_end,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncounterPatchRequest merepresentasikan payload operasi JSON Patch.
|
|
||||||
type EncounterPatchRequest []map[string]interface{}
|
type EncounterPatchRequest []map[string]interface{}
|
||||||
|
|
||||||
// PendaftaranDB merepresentasikan baris data dari tabel t_pendaftaran
|
|
||||||
type PendaftaranDB struct {
|
|
||||||
IdxDaftar int64 `db:"idxdaftar"`
|
|
||||||
NoMR sql.NullString `db:"nomr"`
|
|
||||||
JamReg sql.NullTime `db:"jamreg"`
|
|
||||||
MasukPoly sql.NullTime `db:"masukpoly"`
|
|
||||||
KeluarPoly sql.NullTime `db:"keluarpoly"`
|
|
||||||
Batal sql.NullString `db:"batal"`
|
|
||||||
KdPoly sql.NullInt64 `db:"kdpoly"`
|
|
||||||
PoliNameHFIS sql.NullString `db:"poli_name_hfis"`
|
|
||||||
DokterIDHFIS sql.NullString `db:"dokter_id_hfis"`
|
|
||||||
DokterNameHFIS sql.NullString `db:"dokter_name_hfis"`
|
|
||||||
PasienNIK sql.NullString `db:"pasien_nik"`
|
|
||||||
DokterNIK sql.NullString `db:"dokter_nik"`
|
|
||||||
PatientIHS sql.NullString `db:"patient_ihs"`
|
|
||||||
PractitionerIHS sql.NullString `db:"practitioner_ihs"`
|
|
||||||
}
|
|
||||||
@@ -1,141 +1,177 @@
|
|||||||
package encounter
|
package encounter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MapRequestToFHIR mengubah EncounterRequest (DTO internal) menjadi objek Payload FHIR.
|
|
||||||
// Penggunaan FHIRPayload (.Set dan .Append) akan memudahkan penulisan dan maintenance JSON yang kompleks.
|
|
||||||
func MapRequestToFHIR(req EncounterRequest) satusehat.FHIRPayload {
|
func MapRequestToFHIR(req EncounterRequest) satusehat.FHIRPayload {
|
||||||
payload := satusehat.NewFHIRPayload("Encounter").
|
orgID := os.Getenv("SATUSEHAT_ORG_ID")
|
||||||
Set("status", req.Status).
|
if orgID == "" {
|
||||||
Set("class", map[string]interface{}{
|
orgID = req.OrganizationID
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := satusehat.NewFHIRPayload("Encounter")
|
||||||
|
|
||||||
|
if orgID != "" && req.EncounterID != "" {
|
||||||
|
payload.Set("identifier", []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"system": "http://sys-ids.kemkes.go.id/encounter/" + orgID,
|
||||||
|
"value": req.EncounterID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Status != "" {
|
||||||
|
payload.Set("status", req.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Class != "" {
|
||||||
|
classDisplay := "ambulatory"
|
||||||
|
switch req.Class {
|
||||||
|
case "EMER":
|
||||||
|
classDisplay = "emergency"
|
||||||
|
case "IMP":
|
||||||
|
classDisplay = "inpatient encounter"
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.Set("class", map[string]interface{}{
|
||||||
"system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
|
"system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
|
||||||
"code": req.Class,
|
"code": req.Class,
|
||||||
"display": getClassDisplay(req.Class),
|
"display": classDisplay,
|
||||||
}).
|
|
||||||
Set("subject", map[string]interface{}{
|
|
||||||
"reference": "Patient/" + req.PatientID,
|
|
||||||
"display": req.PatientName,
|
|
||||||
})
|
|
||||||
|
|
||||||
if req.OrganizationID != "" {
|
|
||||||
payload.Set("serviceProvider", map[string]interface{}{
|
|
||||||
"reference": "Organization/" + req.OrganizationID,
|
|
||||||
})
|
|
||||||
|
|
||||||
if req.EncounterID != "" {
|
|
||||||
payload.Append("identifier", map[string]interface{}{
|
|
||||||
"system": "http://sys-ids.kemkes.go.id/encounter/" + req.OrganizationID,
|
|
||||||
"value": req.EncounterID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.EpisodeOfCareID != "" {
|
|
||||||
payload.Append("episodeOfCare", map[string]interface{}{
|
|
||||||
"reference": "EpisodeOfCare/" + req.EpisodeOfCareID,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.PractitionerID != "" {
|
if req.Subject != nil {
|
||||||
payload.Append("participant", map[string]interface{}{
|
subj := map[string]interface{}{"reference": req.Subject.Reference}
|
||||||
|
if req.Subject.Display != "" {
|
||||||
|
subj["display"] = req.Subject.Display
|
||||||
|
}
|
||||||
|
payload.Set("subject", subj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Participant) > 0 {
|
||||||
|
var participants []map[string]interface{}
|
||||||
|
for _, p := range req.Participant {
|
||||||
|
ind := map[string]interface{}{"reference": p.Reference}
|
||||||
|
if p.Display != "" {
|
||||||
|
ind["display"] = p.Display
|
||||||
|
}
|
||||||
|
participants = append(participants, map[string]interface{}{
|
||||||
"type": []map[string]interface{}{
|
"type": []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"coding": []map[string]interface{}{
|
"coding": []map[string]interface{}{
|
||||||
{
|
{"system": "http://terminology.hl7.org/CodeSystem/v3-ParticipationType", "code": "ATND", "display": "attender"},
|
||||||
"system": "http://terminology.hl7.org/CodeSystem/v3-ParticipationType",
|
|
||||||
"code": "ATND",
|
|
||||||
"display": "attender",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
"individual": ind,
|
||||||
"individual": map[string]interface{}{
|
|
||||||
"reference": "Practitioner/" + req.PractitionerID,
|
|
||||||
"display": req.PractitionerName,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
payload.Set("participant", participants)
|
||||||
if req.LocationID != "" {
|
|
||||||
payload.Append("location", map[string]interface{}{
|
|
||||||
"location": map[string]interface{}{
|
|
||||||
"reference": "Location/" + req.LocationID,
|
|
||||||
"display": req.LocationName,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
period := map[string]interface{}{
|
if len(req.Location) > 0 {
|
||||||
"start": req.PeriodStart.Format(time.RFC3339),
|
var locations []map[string]interface{}
|
||||||
|
for _, l := range req.Location {
|
||||||
|
if l.Location != nil {
|
||||||
|
locRef := map[string]interface{}{"reference": l.Location.Reference}
|
||||||
|
if l.Location.Display != "" {
|
||||||
|
locRef["display"] = l.Location.Display
|
||||||
}
|
}
|
||||||
if req.PeriodEnd != nil {
|
locEntry := map[string]interface{}{"location": locRef}
|
||||||
|
period := map[string]interface{}{}
|
||||||
|
if l.PeriodStart != nil && !l.PeriodStart.IsZero() {
|
||||||
|
period["start"] = l.PeriodStart.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
if l.PeriodEnd != nil && !l.PeriodEnd.IsZero() {
|
||||||
|
period["end"] = l.PeriodEnd.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
if len(period) > 0 {
|
||||||
|
locEntry["period"] = period
|
||||||
|
}
|
||||||
|
locations = append(locations, locEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payload.Set("location", locations)
|
||||||
|
}
|
||||||
|
|
||||||
|
period := map[string]interface{}{}
|
||||||
|
if req.PeriodStart != nil && !req.PeriodStart.IsZero() {
|
||||||
|
period["start"] = req.PeriodStart.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
if req.PeriodEnd != nil && !req.PeriodEnd.IsZero() {
|
||||||
period["end"] = req.PeriodEnd.Format(time.RFC3339)
|
period["end"] = req.PeriodEnd.Format(time.RFC3339)
|
||||||
}
|
}
|
||||||
|
if len(period) > 0 {
|
||||||
payload.Set("period", period)
|
payload.Set("period", period)
|
||||||
|
}
|
||||||
|
|
||||||
payload.Append("statusHistory", map[string]interface{}{
|
if len(req.Diagnosis) > 0 {
|
||||||
|
var diagnoses []map[string]interface{}
|
||||||
|
for _, d := range req.Diagnosis {
|
||||||
|
diag := map[string]interface{}{}
|
||||||
|
if d.Condition != nil {
|
||||||
|
cond := map[string]interface{}{"reference": d.Condition.Reference}
|
||||||
|
if d.Condition.Display != "" {
|
||||||
|
cond["display"] = d.Condition.Display
|
||||||
|
}
|
||||||
|
diag["condition"] = cond
|
||||||
|
}
|
||||||
|
if d.Use != nil {
|
||||||
|
sys := d.Use.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://terminology.hl7.org/CodeSystem/diagnosis-role"
|
||||||
|
}
|
||||||
|
useCoding := map[string]interface{}{"system": sys, "code": d.Use.Code}
|
||||||
|
if d.Use.Display != "" {
|
||||||
|
useCoding["display"] = d.Use.Display
|
||||||
|
}
|
||||||
|
diag["use"] = map[string]interface{}{"coding": []map[string]interface{}{useCoding}}
|
||||||
|
}
|
||||||
|
if d.Rank > 0 {
|
||||||
|
diag["rank"] = d.Rank
|
||||||
|
}
|
||||||
|
diagnoses = append(diagnoses, diag)
|
||||||
|
}
|
||||||
|
payload.Set("diagnosis", diagnoses)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.StatusHistory) > 0 {
|
||||||
|
var statusHistories []map[string]interface{}
|
||||||
|
for _, sh := range req.StatusHistory {
|
||||||
|
shEntry := map[string]interface{}{"status": sh.Status}
|
||||||
|
shp := map[string]interface{}{}
|
||||||
|
if sh.PeriodStart != nil && !sh.PeriodStart.IsZero() {
|
||||||
|
shp["start"] = sh.PeriodStart.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
if sh.PeriodEnd != nil && !sh.PeriodEnd.IsZero() {
|
||||||
|
shp["end"] = sh.PeriodEnd.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
if len(shp) > 0 {
|
||||||
|
shEntry["period"] = shp
|
||||||
|
}
|
||||||
|
statusHistories = append(statusHistories, shEntry)
|
||||||
|
}
|
||||||
|
payload.Set("statusHistory", statusHistories)
|
||||||
|
} else if req.Status != "" && req.PeriodStart != nil && !req.PeriodStart.IsZero() {
|
||||||
|
payload.Set("statusHistory", []map[string]interface{}{
|
||||||
|
{
|
||||||
"status": req.Status,
|
"status": req.Status,
|
||||||
"period": period,
|
"period": map[string]interface{}{
|
||||||
|
"start": req.PeriodStart.Format(time.RFC3339),
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if orgID != "" {
|
||||||
|
payload.Set("serviceProvider", map[string]interface{}{
|
||||||
|
"reference": "Organization/" + orgID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
// MapPendaftaranToRequest mengubah data database t_pendaftaran menjadi EncounterRequest
|
|
||||||
func MapPendaftaranToRequest(dbData PendaftaranDB, organizationID string, patientIHS string, practitionerIHS string) EncounterRequest {
|
|
||||||
// Penentuan status encounter
|
|
||||||
status := "arrived"
|
|
||||||
if dbData.Batal.Valid && dbData.Batal.String == "Y" {
|
|
||||||
status = "cancelled"
|
|
||||||
} else if dbData.KeluarPoly.Valid {
|
|
||||||
status = "finished"
|
|
||||||
} else if dbData.MasukPoly.Valid {
|
|
||||||
status = "in-progress"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Waktu mulai
|
|
||||||
periodStart := time.Now()
|
|
||||||
if dbData.MasukPoly.Valid {
|
|
||||||
periodStart = dbData.MasukPoly.Time
|
|
||||||
} else if dbData.JamReg.Valid {
|
|
||||||
periodStart = dbData.JamReg.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
var periodEnd *time.Time
|
|
||||||
if dbData.KeluarPoly.Valid {
|
|
||||||
periodEnd = &dbData.KeluarPoly.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
return EncounterRequest{
|
|
||||||
EncounterID: fmt.Sprintf("%d", dbData.IdxDaftar),
|
|
||||||
OrganizationID: organizationID,
|
|
||||||
PatientID: patientIHS, // Hasil pemetaan/pencarian IHS
|
|
||||||
PatientName: "Pasien " + dbData.NoMR.String, // Fallback Name
|
|
||||||
PractitionerID: practitionerIHS, // Hasil pemetaan/pencarian IHS
|
|
||||||
PractitionerName: dbData.DokterNameHFIS.String,
|
|
||||||
LocationID: fmt.Sprintf("%d", dbData.KdPoly.Int64), // Catatan: Anda harus me-mapping ini ke IHS Location ID
|
|
||||||
LocationName: dbData.PoliNameHFIS.String,
|
|
||||||
Status: status,
|
|
||||||
Class: "AMB", // Default: Rawat Jalan (Ambulatory)
|
|
||||||
PeriodStart: periodStart,
|
|
||||||
PeriodEnd: periodEnd,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getClassDisplay(code string) string {
|
|
||||||
switch code {
|
|
||||||
case "AMB":
|
|
||||||
return "ambulatory"
|
|
||||||
case "IMP":
|
|
||||||
return "inpatient encounter"
|
|
||||||
case "EMER":
|
|
||||||
return "emergency"
|
|
||||||
default:
|
|
||||||
return "ambulatory"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,12 +2,12 @@ package encounter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"service/internal/infrastructure/database"
|
"service/internal/infrastructure/database"
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
@@ -16,7 +16,6 @@ type Repository interface {
|
|||||||
Patch(ctx context.Context, id string, payload EncounterPatchRequest) (*satusehat.FHIRResponse, error)
|
Patch(ctx context.Context, id string, payload EncounterPatchRequest) (*satusehat.FHIRResponse, error)
|
||||||
GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error)
|
GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error)
|
||||||
Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error)
|
Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error)
|
||||||
GetPendaftaranByID(ctx context.Context, idxdaftar int64) (*PendaftaranDB, error)
|
|
||||||
SearchPatientByNIK(ctx context.Context, nik string) (*satusehat.FHIRResponse, error)
|
SearchPatientByNIK(ctx context.Context, nik string) (*satusehat.FHIRResponse, error)
|
||||||
SearchPractitionerByNIK(ctx context.Context, nik string) (*satusehat.FHIRResponse, error)
|
SearchPractitionerByNIK(ctx context.Context, nik string) (*satusehat.FHIRResponse, error)
|
||||||
}
|
}
|
||||||
@@ -30,10 +29,14 @@ func NewRepository(client satusehat.SatuSehatClient, db database.Service) Reposi
|
|||||||
return &repository{client: client, db: db}
|
return &repository{client: client, db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type hiddenCtx struct {
|
||||||
|
context.Context
|
||||||
|
}
|
||||||
|
|
||||||
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, payload interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, payload interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
resp, err := r.client.DoRequest(ctx, method, endpoint, payload)
|
resp, err := r.client.DoRequest(hiddenCtx{ctx}, method, endpoint, payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.InternalError().Message("Failed to execute request to SatuSehat").Cause(err).Build()
|
||||||
}
|
}
|
||||||
return r.parseAndProcessResponse(resp)
|
return r.parseAndProcessResponse(resp)
|
||||||
}
|
}
|
||||||
@@ -41,23 +44,16 @@ func (r *repository) executeRequest(ctx context.Context, method, endpoint string
|
|||||||
func (r *repository) parseAndProcessResponse(data []byte) (*satusehat.FHIRResponse, error) {
|
func (r *repository) parseAndProcessResponse(data []byte) (*satusehat.FHIRResponse, error) {
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
if err := json.Unmarshal(data, &result); err != nil {
|
if err := json.Unmarshal(data, &result); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
return nil, errors.InternalError().Message("Failed to parse SatuSehat response").Cause(err).Metadata("raw_response", string(data)).Build()
|
||||||
|
}
|
||||||
|
if resourceType, ok := result["resourceType"].(string); ok && resourceType == "OperationOutcome" {
|
||||||
|
return nil, errors.ParseSatuSehatError(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ekstrak ID dari resource jika ada
|
|
||||||
var resourceID string
|
var resourceID string
|
||||||
if id, ok := result["id"].(string); ok {
|
if id, ok := result["id"].(string); ok {
|
||||||
resourceID = id
|
resourceID = id
|
||||||
}
|
}
|
||||||
|
return &satusehat.FHIRResponse{ID: resourceID, FullResponse: result, RawResponse: data}, nil
|
||||||
// Buat response terstruktur
|
|
||||||
fhirResponse := &satusehat.FHIRResponse{
|
|
||||||
ID: resourceID,
|
|
||||||
FullResponse: result,
|
|
||||||
RawResponse: data, // Simpan data mentah untuk logging atau audit
|
|
||||||
}
|
|
||||||
|
|
||||||
return fhirResponse, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *repository) Create(ctx context.Context, payload interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) Create(ctx context.Context, payload interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
@@ -65,66 +61,25 @@ func (r *repository) Create(ctx context.Context, payload interface{}) (*satuseha
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *repository) Update(ctx context.Context, id string, payload interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) Update(ctx context.Context, id string, payload interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
endpoint := fmt.Sprintf("/Encounter/%s", id)
|
return r.executeRequest(ctx, "PUT", fmt.Sprintf("/Encounter/%s", id), payload)
|
||||||
return r.executeRequest(ctx, "PUT", endpoint, payload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *repository) Patch(ctx context.Context, id string, payload EncounterPatchRequest) (*satusehat.FHIRResponse, error) {
|
func (r *repository) Patch(ctx context.Context, id string, payload EncounterPatchRequest) (*satusehat.FHIRResponse, error) {
|
||||||
endpoint := fmt.Sprintf("/Encounter/%s", id)
|
return r.executeRequest(ctx, "PATCH", fmt.Sprintf("/Encounter/%s", id), payload)
|
||||||
return r.executeRequest(ctx, "PATCH", endpoint, payload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *repository) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
func (r *repository) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
||||||
endpoint := fmt.Sprintf("/Encounter/%s", id)
|
return r.executeRequest(ctx, "GET", fmt.Sprintf("/Encounter/%s", id), nil)
|
||||||
return r.executeRequest(ctx, "GET", endpoint, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *repository) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
func (r *repository) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
||||||
endpoint := fmt.Sprintf("/Encounter?%s", queryParams.Encode())
|
return r.executeRequest(ctx, "GET", fmt.Sprintf("/Encounter?%s", queryParams.Encode()), nil)
|
||||||
return r.executeRequest(ctx, "GET", endpoint, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *repository) SearchPatientByNIK(ctx context.Context, nik string) (*satusehat.FHIRResponse, error) {
|
func (r *repository) SearchPatientByNIK(ctx context.Context, nik string) (*satusehat.FHIRResponse, error) {
|
||||||
endpoint := fmt.Sprintf("/Patient?identifier=https://fhir.kemkes.go.id/id/nik|%s", nik)
|
return r.executeRequest(ctx, "GET", fmt.Sprintf("/Patient?identifier=https://fhir.kemkes.go.id/id/nik|%s", nik), nil)
|
||||||
return r.executeRequest(ctx, "GET", endpoint, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *repository) SearchPractitionerByNIK(ctx context.Context, nik string) (*satusehat.FHIRResponse, error) {
|
func (r *repository) SearchPractitionerByNIK(ctx context.Context, nik string) (*satusehat.FHIRResponse, error) {
|
||||||
endpoint := fmt.Sprintf("/Practitioner?identifier=https://fhir.kemkes.go.id/id/nik|%s", nik)
|
return r.executeRequest(ctx, "GET", fmt.Sprintf("/Practitioner?identifier=https://fhir.kemkes.go.id/id/nik|%s", nik), nil)
|
||||||
return r.executeRequest(ctx, "GET", endpoint, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *repository) GetPendaftaranByID(ctx context.Context, idxdaftar int64) (*PendaftaranDB, error) {
|
|
||||||
// Ambil koneksi database internal
|
|
||||||
db, err := r.db.GetReadDB("default")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var p PendaftaranDB
|
|
||||||
query := `
|
|
||||||
SELECT p.idxdaftar, p.nomr, p.jamreg, p.masukpoly, p.keluarpoly, p.batal,
|
|
||||||
p.kdpoly, p.poli_name_hfis, p.dokter_id_hfis, p.dokter_name_hfis,
|
|
||||||
COALESCE(NULLIF(mp.noktp_baru, ''), mp.noktp) AS pasien_nik,
|
|
||||||
dp.nik AS dokter_nik,
|
|
||||||
dpas."Nomor_satusehat" AS patient_ihs,
|
|
||||||
dp."Kode_satusehat" AS practitioner_ihs
|
|
||||||
FROM public.t_pendaftaran p
|
|
||||||
LEFT JOIN public.m_pasien mp ON p.nomr = mp.nomr
|
|
||||||
LEFT JOIN public.data_pasien dpas ON p.nomr = dpas."Nomor_rekamedik"
|
|
||||||
LEFT JOIN public.data_pegawai dp ON p.kddokter = dp."Kode_DPJP"
|
|
||||||
WHERE p.idxdaftar = $1`
|
|
||||||
|
|
||||||
err = db.QueryRowContext(ctx, query, idxdaftar).Scan(
|
|
||||||
&p.IdxDaftar, &p.NoMR, &p.JamReg, &p.MasukPoly, &p.KeluarPoly, &p.Batal,
|
|
||||||
&p.KdPoly, &p.PoliNameHFIS, &p.DokterIDHFIS, &p.DokterNameHFIS,
|
|
||||||
&p.PasienNIK, &p.DokterNIK, &p.PatientIHS, &p.PractitionerIHS,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return nil, fmt.Errorf("pendaftaran dengan idxdaftar %d tidak ditemukan", idxdaftar)
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &p, nil
|
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,7 @@ package encounter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
"service/pkg/errors"
|
"service/pkg/errors"
|
||||||
)
|
)
|
||||||
@@ -16,31 +13,25 @@ type Service interface {
|
|||||||
Patch(ctx context.Context, id string, req EncounterPatchRequest) (*satusehat.FHIRResponse, error)
|
Patch(ctx context.Context, id string, req EncounterPatchRequest) (*satusehat.FHIRResponse, error)
|
||||||
GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error)
|
GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error)
|
||||||
Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error)
|
Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error)
|
||||||
SyncFromSIMRS(ctx context.Context, idxdaftar int64) (*satusehat.FHIRResponse, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type service struct {
|
type service struct{ repo Repository }
|
||||||
repo Repository
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewService(repo Repository) Service {
|
func NewService(repo Repository) Service {
|
||||||
return &service{repo: repo}
|
return &service{repo: repo}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Create(ctx context.Context, req EncounterRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Create(ctx context.Context, req EncounterRequest) (*satusehat.FHIRResponse, error) {
|
||||||
fhirPayload := MapRequestToFHIR(req)
|
return s.repo.Create(ctx, MapRequestToFHIR(req))
|
||||||
return s.repo.Create(ctx, fhirPayload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Update(ctx context.Context, id string, req EncounterRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Update(ctx context.Context, id string, req EncounterRequest) (*satusehat.FHIRResponse, error) {
|
||||||
if id == "" {
|
if id == "" {
|
||||||
return nil, errors.NewValidationError().Message("Encounter ID is required").Build()
|
return nil, errors.NewValidationError().Message("Encounter ID is required").Build()
|
||||||
}
|
}
|
||||||
|
payload := MapRequestToFHIR(req)
|
||||||
fhirPayload := MapRequestToFHIR(req)
|
payload.Set("id", id)
|
||||||
fhirPayload.Set("id", id) // Untuk Update (PUT), parameter ID di payload diwajibkan oleh standar API Kemenkes
|
return s.repo.Update(ctx, id, payload)
|
||||||
|
|
||||||
return s.repo.Update(ctx, id, fhirPayload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Patch(ctx context.Context, id string, req EncounterPatchRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Patch(ctx context.Context, id string, req EncounterPatchRequest) (*satusehat.FHIRResponse, error) {
|
||||||
@@ -61,62 +52,5 @@ func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRRespon
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
||||||
// Bisa ditambahkan validasi query params wajib di sini jika perlu
|
|
||||||
return s.repo.Search(ctx, queryParams)
|
return s.repo.Search(ctx, queryParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) SyncFromSIMRS(ctx context.Context, idxdaftar int64) (*satusehat.FHIRResponse, error) {
|
|
||||||
// 1. Ambil data dari database SIMRS
|
|
||||||
pendaftaran, err := s.repo.GetPendaftaranByID(ctx, idxdaftar)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("gagal mengambil data pendaftaran: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Ambil Organization ID RS Anda (Bisa dari environment variable atau parameter config)
|
|
||||||
orgID := os.Getenv("SATUSEHAT_ORGANIZATION_ID") // Sesuaikan bila ada package config terpisah
|
|
||||||
|
|
||||||
// 3. Persiapkan Patient ID (IHS) dengan fallback pencarian ke NIK
|
|
||||||
patientIHS := pendaftaran.PatientIHS.String
|
|
||||||
if patientIHS == "" && pendaftaran.PasienNIK.Valid && pendaftaran.PasienNIK.String != "" {
|
|
||||||
resp, err := s.repo.SearchPatientByNIK(ctx, pendaftaran.PasienNIK.String)
|
|
||||||
if err == nil && resp != nil {
|
|
||||||
patientIHS = extractIDFromBundle(resp.RawResponse)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if patientIHS == "" { // Fallback terakhir (jaga-jaga agar mapping tak panic)
|
|
||||||
patientIHS = pendaftaran.NoMR.String
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Persiapkan Practitioner ID (IHS) dengan fallback pencarian ke NIK
|
|
||||||
practitionerIHS := pendaftaran.PractitionerIHS.String
|
|
||||||
if practitionerIHS == "" && pendaftaran.DokterNIK.Valid && pendaftaran.DokterNIK.String != "" {
|
|
||||||
resp, err := s.repo.SearchPractitionerByNIK(ctx, pendaftaran.DokterNIK.String)
|
|
||||||
if err == nil && resp != nil {
|
|
||||||
practitionerIHS = extractIDFromBundle(resp.RawResponse)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if practitionerIHS == "" { // Fallback terakhir
|
|
||||||
practitionerIHS = pendaftaran.DokterIDHFIS.String
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Mapping data database ke struktur Request Encounter API
|
|
||||||
req := MapPendaftaranToRequest(*pendaftaran, orgID, patientIHS, practitionerIHS)
|
|
||||||
|
|
||||||
// 6. Proses Create/Kirim ke Satu Sehat menggunakan fungsi Create yang sudah ada
|
|
||||||
return s.Create(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractIDFromBundle mem-parsing raw response FHIR Bundle untuk mengambil ID resource pertama
|
|
||||||
func extractIDFromBundle(data []byte) string {
|
|
||||||
var bundle struct {
|
|
||||||
Entry []struct {
|
|
||||||
Resource struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
} `json:"resource"`
|
|
||||||
} `json:"entry"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(data, &bundle); err == nil && len(bundle.Entry) > 0 {
|
|
||||||
return bundle.Entry[0].Resource.ID
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,36 @@
|
|||||||
package episodeofcare
|
package episodeofcare
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service/internal/satusehat/common"
|
||||||
|
)
|
||||||
|
|
||||||
type EpisodeOfCareRequest struct {
|
type EpisodeOfCareRequest struct {
|
||||||
|
OrganizationID string `json:"organization_id,omitempty"`
|
||||||
|
EpisodeOfCareID string `json:"episode_of_care_id,omitempty"` // cth: "EOC12345"
|
||||||
|
Status string `json:"status" binding:"required,oneof=planned waitlist active onhold finished cancelled entered-in-error"`
|
||||||
|
StatusHistory []StatusHistoryDTO `json:"status_history,omitempty"`
|
||||||
|
Type []*common.CodeableConceptDTO `json:"type,omitempty"`
|
||||||
|
Diagnosis []DiagnosisDTO `json:"diagnosis,omitempty"`
|
||||||
PatientID string `json:"patient_id" binding:"required"`
|
PatientID string `json:"patient_id" binding:"required"`
|
||||||
PatientName string `json:"patient_name" binding:"required"`
|
PatientName string `json:"patient_name,omitempty"`
|
||||||
OrganizationID string `json:"organization_id" binding:"required"`
|
ManagingOrganization string `json:"managing_organization,omitempty"`
|
||||||
Status string `json:"status" binding:"required,oneof=planned waitlist active onhold finished cancelled entered-in-error"` // planned, waitlist, active, onhold, finished, cancelled, entered-in-error
|
PeriodStart *time.Time `json:"period_start,omitempty"`
|
||||||
PeriodStart time.Time `json:"period_start" binding:"required"`
|
PeriodEnd *time.Time `json:"period_end,omitempty"`
|
||||||
|
CareManager *common.ReferenceDTO `json:"care_manager,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatusHistoryDTO struct {
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
PeriodStart *time.Time `json:"period_start,omitempty"`
|
||||||
PeriodEnd *time.Time `json:"period_end,omitempty"`
|
PeriodEnd *time.Time `json:"period_end,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DiagnosisDTO struct {
|
||||||
|
Condition *common.ReferenceDTO `json:"condition,omitempty"`
|
||||||
|
Role *common.CodeableConceptDTO `json:"role,omitempty"`
|
||||||
|
Rank int `json:"rank,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type EpisodeOfCarePatchRequest []map[string]interface{}
|
type EpisodeOfCarePatchRequest []map[string]interface{}
|
||||||
@@ -1,30 +1,141 @@
|
|||||||
package episodeofcare
|
package episodeofcare
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MapRequestToFHIR(req EpisodeOfCareRequest) satusehat.FHIRPayload {
|
func MapRequestToFHIR(req EpisodeOfCareRequest) satusehat.FHIRPayload {
|
||||||
payload := satusehat.NewFHIRPayload("EpisodeOfCare").
|
payload := satusehat.NewFHIRPayload("EpisodeOfCare")
|
||||||
Set("status", req.Status).
|
|
||||||
Set("patient", map[string]interface{}{
|
|
||||||
"reference": "Patient/" + req.PatientID,
|
|
||||||
"display": req.PatientName,
|
|
||||||
})
|
|
||||||
|
|
||||||
if req.OrganizationID != "" {
|
orgID := os.Getenv("SATUSEHAT_ORG_ID")
|
||||||
payload.Set("managingOrganization", map[string]interface{}{
|
if orgID == "" {
|
||||||
"reference": "Organization/" + req.OrganizationID,
|
orgID = req.OrganizationID
|
||||||
|
}
|
||||||
|
|
||||||
|
if orgID != "" && req.EpisodeOfCareID != "" {
|
||||||
|
payload.Set("identifier", []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"system": "http://sys-ids.kemkes.go.id/episode-of-care/" + orgID,
|
||||||
|
"value": req.EpisodeOfCareID,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.Status != "" {
|
||||||
|
payload.Set("status", req.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.StatusHistory) > 0 {
|
||||||
|
var statusHistory []map[string]interface{}
|
||||||
|
for _, sh := range req.StatusHistory {
|
||||||
|
historyItem := map[string]interface{}{"status": sh.Status}
|
||||||
|
period := map[string]interface{}{}
|
||||||
|
if sh.PeriodStart != nil && !sh.PeriodStart.IsZero() {
|
||||||
|
period["start"] = sh.PeriodStart.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
if sh.PeriodEnd != nil && !sh.PeriodEnd.IsZero() {
|
||||||
|
period["end"] = sh.PeriodEnd.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
if len(period) > 0 {
|
||||||
|
historyItem["period"] = period
|
||||||
|
}
|
||||||
|
statusHistory = append(statusHistory, historyItem)
|
||||||
|
}
|
||||||
|
payload.Set("statusHistory", statusHistory)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Type) > 0 {
|
||||||
|
var types []map[string]interface{}
|
||||||
|
for _, t := range req.Type {
|
||||||
|
if t != nil {
|
||||||
|
sys := t.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://terminology.kemkes.go.id/CodeSystem/episodeofcare-type"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": t.Code}
|
||||||
|
if t.Display != "" {
|
||||||
|
coding["display"] = t.Display
|
||||||
|
}
|
||||||
|
typeConcept := map[string]interface{}{"coding": []map[string]interface{}{coding}}
|
||||||
|
if t.Text != "" {
|
||||||
|
typeConcept["text"] = t.Text
|
||||||
|
}
|
||||||
|
types = append(types, typeConcept)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payload.Set("type", types)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Diagnosis) > 0 {
|
||||||
|
var diagnoses []map[string]interface{}
|
||||||
|
for _, d := range req.Diagnosis {
|
||||||
|
diagnosisItem := map[string]interface{}{}
|
||||||
|
|
||||||
|
if d.Condition != nil {
|
||||||
|
condition := map[string]interface{}{"reference": d.Condition.Reference}
|
||||||
|
if d.Condition.Display != "" {
|
||||||
|
condition["display"] = d.Condition.Display
|
||||||
|
}
|
||||||
|
diagnosisItem["condition"] = condition
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Role != nil {
|
||||||
|
sys := d.Role.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://terminology.hl7.org/CodeSystem/diagnosis-role"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": d.Role.Code}
|
||||||
|
if d.Role.Display != "" {
|
||||||
|
coding["display"] = d.Role.Display
|
||||||
|
}
|
||||||
|
diagnosisItem["role"] = map[string]interface{}{"coding": []map[string]interface{}{coding}}
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Rank > 0 {
|
||||||
|
diagnosisItem["rank"] = d.Rank
|
||||||
|
}
|
||||||
|
|
||||||
|
diagnoses = append(diagnoses, diagnosisItem)
|
||||||
|
}
|
||||||
|
payload.Set("diagnosis", diagnoses)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.PatientID != "" {
|
||||||
|
subject := map[string]interface{}{"reference": "Patient/" + req.PatientID}
|
||||||
|
if req.PatientName != "" {
|
||||||
|
subject["display"] = req.PatientName
|
||||||
|
}
|
||||||
|
payload.Set("patient", subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
manageOrg := orgID
|
||||||
|
if req.ManagingOrganization != "" {
|
||||||
|
manageOrg = req.ManagingOrganization
|
||||||
|
}
|
||||||
|
if manageOrg != "" {
|
||||||
|
payload.Set("managingOrganization", map[string]interface{}{
|
||||||
|
"reference": "Organization/" + manageOrg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.PeriodStart != nil && !req.PeriodStart.IsZero() {
|
||||||
period := map[string]interface{}{"start": req.PeriodStart.Format(time.RFC3339)}
|
period := map[string]interface{}{"start": req.PeriodStart.Format(time.RFC3339)}
|
||||||
if req.PeriodEnd != nil {
|
if req.PeriodEnd != nil && !req.PeriodEnd.IsZero() {
|
||||||
period["end"] = req.PeriodEnd.Format(time.RFC3339)
|
period["end"] = req.PeriodEnd.Format(time.RFC3339)
|
||||||
}
|
}
|
||||||
payload.Set("period", period)
|
payload.Set("period", period)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.CareManager != nil {
|
||||||
|
cm := map[string]interface{}{"reference": req.CareManager.Reference}
|
||||||
|
if req.CareManager.Display != "" {
|
||||||
|
cm["display"] = req.CareManager.Display
|
||||||
|
}
|
||||||
|
payload.Set("careManager", cm)
|
||||||
|
}
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Repository mendefinisikan kontrak untuk operasi penyimpanan data EpisodeOfCare.
|
// Repository mendefinisikan kontrak untuk operasi penyimpanan data EpisodeOfCare.
|
||||||
@@ -26,15 +27,22 @@ func NewRepository(client satusehat.SatuSehatClient) Repository {
|
|||||||
return &repository{client: client}
|
return &repository{client: client}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type hiddenCtx struct {
|
||||||
|
context.Context
|
||||||
|
}
|
||||||
|
|
||||||
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
resp, err := r.client.DoRequest(ctx, method, endpoint, req)
|
resp, err := r.client.DoRequest(hiddenCtx{ctx}, method, endpoint, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.InternalError().Message("Failed to execute request to SatuSehat").Cause(err).Build()
|
||||||
}
|
}
|
||||||
|
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
if err := json.Unmarshal(resp, &result); err != nil {
|
if err := json.Unmarshal(resp, &result); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
return nil, errors.InternalError().Message("Failed to parse SatuSehat response").Cause(err).Metadata("raw_response", string(resp)).Build()
|
||||||
|
}
|
||||||
|
if resourceType, ok := result["resourceType"].(string); ok && resourceType == "OperationOutcome" {
|
||||||
|
return nil, errors.ParseSatuSehatError(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
var resourceID string
|
var resourceID string
|
||||||
@@ -42,11 +50,7 @@ func (r *repository) executeRequest(ctx context.Context, method, endpoint string
|
|||||||
resourceID = id
|
resourceID = id
|
||||||
}
|
}
|
||||||
|
|
||||||
return &satusehat.FHIRResponse{
|
return &satusehat.FHIRResponse{ID: resourceID, FullResponse: result, RawResponse: resp}, nil
|
||||||
ID: resourceID,
|
|
||||||
FullResponse: result,
|
|
||||||
RawResponse: resp,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *repository) Create(ctx context.Context, payload interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) Create(ctx context.Context, payload interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
|
|||||||
@@ -26,19 +26,16 @@ func NewService(repo Repository) Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Create(ctx context.Context, req EpisodeOfCareRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Create(ctx context.Context, req EpisodeOfCareRequest) (*satusehat.FHIRResponse, error) {
|
||||||
fhirPayload := MapRequestToFHIR(req)
|
return s.repo.Create(ctx, MapRequestToFHIR(req))
|
||||||
return s.repo.Create(ctx, fhirPayload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Update(ctx context.Context, id string, req EpisodeOfCareRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Update(ctx context.Context, id string, req EpisodeOfCareRequest) (*satusehat.FHIRResponse, error) {
|
||||||
if id == "" {
|
if id == "" {
|
||||||
return nil, errors.NewValidationError().Message("EpisodeOfCare ID is required").Build()
|
return nil, errors.NewValidationError().Message("EpisodeOfCare ID is required").Build()
|
||||||
}
|
}
|
||||||
|
payload := MapRequestToFHIR(req)
|
||||||
fhirPayload := MapRequestToFHIR(req)
|
payload.Set("id", id)
|
||||||
fhirPayload.Set("id", id)
|
return s.repo.Update(ctx, id, payload)
|
||||||
|
|
||||||
return s.repo.Update(ctx, id, fhirPayload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Patch(ctx context.Context, id string, req EpisodeOfCarePatchRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Patch(ctx context.Context, id string, req EpisodeOfCarePatchRequest) (*satusehat.FHIRResponse, error) {
|
||||||
|
|||||||
@@ -3,18 +3,21 @@ package imagingstudy
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type ImagingStudyRequest struct {
|
type ImagingStudyRequest struct {
|
||||||
PatientID string `json:"patient_id"`
|
OrganizationID string `json:"organization_id,omitempty"`
|
||||||
PatientName string `json:"patient_name"`
|
AccessionNumber string `json:"accession_number,omitempty"`
|
||||||
EncounterID string `json:"encounter_id"`
|
ServiceRequestID string `json:"service_request_id,omitempty"`
|
||||||
PractitionerID string `json:"practitioner_id"`
|
PatientID string `json:"patient_id,omitempty"`
|
||||||
PractitionerName string `json:"practitioner_name"`
|
PatientName string `json:"patient_name,omitempty"`
|
||||||
Status string `json:"status"` // registered, available, cancelled, entered-in-error, unknown
|
EncounterID string `json:"encounter_id,omitempty"`
|
||||||
Started time.Time `json:"started"`
|
PractitionerID string `json:"practitioner_id,omitempty"`
|
||||||
NumberOfSeries int `json:"number_of_series"`
|
PractitionerName string `json:"practitioner_name,omitempty"`
|
||||||
NumberOfInstances int `json:"number_of_instances"`
|
Status string `json:"status,omitempty"` // registered, available, cancelled, entered-in-error, unknown
|
||||||
ProcedureCode string `json:"procedure_code"` // SNOMED CT
|
Started time.Time `json:"started,omitempty"`
|
||||||
ProcedureDisplay string `json:"procedure_display"`
|
NumberOfSeries *int `json:"number_of_series,omitempty"`
|
||||||
Description string `json:"description"`
|
NumberOfInstances *int `json:"number_of_instances,omitempty"`
|
||||||
|
ProcedureCode string `json:"procedure_code,omitempty"` // SNOMED CT
|
||||||
|
ProcedureDisplay string `json:"procedure_display,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImagingStudyPatchRequest []map[string]interface{}
|
type ImagingStudyPatchRequest []map[string]interface{}
|
||||||
@@ -1,39 +1,103 @@
|
|||||||
package imagingstudy
|
package imagingstudy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MapRequestToFHIR(req ImagingStudyRequest) satusehat.FHIRPayload {
|
func MapRequestToFHIR(req ImagingStudyRequest) satusehat.FHIRPayload {
|
||||||
payload := satusehat.NewFHIRPayload("ImagingStudy").
|
orgID := os.Getenv("SATUSEHAT_ORG_ID")
|
||||||
Set("status", req.Status).
|
if orgID == "" {
|
||||||
Set("subject", map[string]interface{}{
|
orgID = req.OrganizationID
|
||||||
"reference": "Patient/" + req.PatientID,
|
}
|
||||||
"display": req.PatientName,
|
|
||||||
}).
|
|
||||||
Set("encounter", map[string]interface{}{
|
|
||||||
"reference": "Encounter/" + req.EncounterID,
|
|
||||||
}).
|
|
||||||
Set("started", req.Started.Format(time.RFC3339)).
|
|
||||||
Set("numberOfSeries", req.NumberOfSeries).
|
|
||||||
Set("numberOfInstances", req.NumberOfInstances).
|
|
||||||
Set("description", req.Description)
|
|
||||||
|
|
||||||
if req.ProcedureCode != "" {
|
payload := satusehat.NewFHIRPayload("ImagingStudy")
|
||||||
payload.Set("procedureCode", []map[string]interface{}{
|
|
||||||
|
if req.Status != "" {
|
||||||
|
payload.Set("status", req.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.PatientID != "" {
|
||||||
|
subject := map[string]interface{}{
|
||||||
|
"reference": "Patient/" + req.PatientID,
|
||||||
|
}
|
||||||
|
if req.PatientName != "" {
|
||||||
|
subject["display"] = req.PatientName
|
||||||
|
}
|
||||||
|
payload.Set("subject", subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !req.Started.IsZero() {
|
||||||
|
payload.Set("started", req.Started.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
if orgID != "" && req.AccessionNumber != "" {
|
||||||
|
payload.Set("identifier", []map[string]interface{}{
|
||||||
{
|
{
|
||||||
|
"use": "official",
|
||||||
|
"type": map[string]interface{}{
|
||||||
"coding": []map[string]interface{}{
|
"coding": []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"system": "http://snomed.info/sct",
|
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
|
||||||
"code": req.ProcedureCode,
|
"code": "ACSN",
|
||||||
"display": req.ProcedureDisplay,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"system": "http://sys-ids.kemkes.go.id/acsn/" + orgID,
|
||||||
|
"value": req.AccessionNumber,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.ServiceRequestID != "" {
|
||||||
|
payload.Set("basedOn", []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"reference": "ServiceRequest/" + req.ServiceRequestID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ProcedureCode != "" {
|
||||||
|
modality := map[string]interface{}{
|
||||||
|
"system": "http://dicom.nema.org/resources/ontology/DCM",
|
||||||
|
"code": req.ProcedureCode,
|
||||||
|
}
|
||||||
|
if req.ProcedureDisplay != "" {
|
||||||
|
modality["display"] = req.ProcedureDisplay
|
||||||
|
}
|
||||||
|
payload.Set("modality", []map[string]interface{}{modality})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menambahkan Practitioner ke kolom `interpreter` jika disediakan
|
||||||
|
if req.PractitionerID != "" {
|
||||||
|
interpreter := map[string]interface{}{
|
||||||
|
"reference": "Practitioner/" + req.PractitionerID,
|
||||||
|
}
|
||||||
|
if req.PractitionerName != "" {
|
||||||
|
interpreter["display"] = req.PractitionerName
|
||||||
|
}
|
||||||
|
payload.Set("interpreter", []map[string]interface{}{interpreter})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.EncounterID != "" {
|
||||||
|
payload.Set("encounter", map[string]interface{}{
|
||||||
|
"reference": "Encounter/" + req.EncounterID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.NumberOfSeries != nil {
|
||||||
|
payload.Set("numberOfSeries", *req.NumberOfSeries)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.NumberOfInstances != nil {
|
||||||
|
payload.Set("numberOfInstances", *req.NumberOfInstances)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Description != "" {
|
||||||
|
payload.Set("description", req.Description)
|
||||||
|
}
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Repository mendefinisikan kontrak untuk operasi penyimpanan data ImagingStudy.
|
// Repository mendefinisikan kontrak untuk operasi penyimpanan data ImagingStudy.
|
||||||
@@ -26,15 +27,33 @@ func NewRepository(client satusehat.SatuSehatClient) Repository {
|
|||||||
return &repository{client: client}
|
return &repository{client: client}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hiddenCtx membungkus context untuk mencegah *HTTP client*
|
||||||
|
// melakukan type-assertion ke *gin.Context dan melakukan auto-print error.
|
||||||
|
type hiddenCtx struct {
|
||||||
|
context.Context
|
||||||
|
}
|
||||||
|
|
||||||
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
resp, err := r.client.DoRequest(ctx, method, endpoint, req)
|
resp, err := r.client.DoRequest(hiddenCtx{ctx}, method, endpoint, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.InternalError().
|
||||||
|
Message("Failed to execute request to SatuSehat").
|
||||||
|
Cause(err).
|
||||||
|
Build()
|
||||||
}
|
}
|
||||||
|
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
if err := json.Unmarshal(resp, &result); err != nil {
|
if err := json.Unmarshal(resp, &result); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
return nil, errors.InternalError().
|
||||||
|
Message("Failed to parse SatuSehat response").
|
||||||
|
Cause(err).
|
||||||
|
Metadata("raw_response", string(resp)).
|
||||||
|
Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tangkap respons OperationOutcome dan serahkan ke Parser terpusat
|
||||||
|
if resourceType, ok := result["resourceType"].(string); ok && resourceType == "OperationOutcome" {
|
||||||
|
return nil, errors.ParseSatuSehatError(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
var resourceID string
|
var resourceID string
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
package immunization
|
package immunization
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service/internal/satusehat/common"
|
||||||
|
)
|
||||||
|
|
||||||
type ImmunizationRequest struct {
|
type ImmunizationRequest struct {
|
||||||
PatientID string `json:"patient_id" binding:"required"`
|
OrganizationID string `json:"organization_id,omitempty"`
|
||||||
PatientName string `json:"patient_name" binding:"required"`
|
ImmunizationID string `json:"immunization_id,omitempty"`
|
||||||
EncounterID string `json:"encounter_id" binding:"required"`
|
|
||||||
Status string `json:"status" binding:"required,oneof=completed entered-in-error not-done"`
|
Status string `json:"status" binding:"required,oneof=completed entered-in-error not-done"`
|
||||||
VaccineCode string `json:"vaccine_code" binding:"required"` // KFA Code for Vaccine
|
VaccineCode *common.CodeableConceptDTO `json:"vaccine_code" binding:"required"`
|
||||||
VaccineDisplay string `json:"vaccine_display" binding:"required"`
|
PatientID string `json:"patient_id" binding:"required"`
|
||||||
OccurrenceDateTime time.Time `json:"occurrence_date_time" binding:"required"`
|
PatientName string `json:"patient_name,omitempty"`
|
||||||
PrimarySource bool `json:"primary_source"`
|
EncounterID string `json:"encounter_id" binding:"required"`
|
||||||
|
EncounterDisplay string `json:"encounter_display,omitempty"`
|
||||||
|
OccurrenceDateTime *time.Time `json:"occurrence_date_time,omitempty"`
|
||||||
|
PrimarySource *bool `json:"primary_source,omitempty"`
|
||||||
LotNumber string `json:"lot_number,omitempty"`
|
LotNumber string `json:"lot_number,omitempty"`
|
||||||
PractitionerID string `json:"practitioner_id" binding:"required"`
|
Performer []common.ReferenceDTO `json:"performer,omitempty"`
|
||||||
PractitionerName string `json:"practitioner_name" binding:"required"`
|
Location *common.ReferenceDTO `json:"location,omitempty"`
|
||||||
|
DoseQuantity *common.QuantityDTO `json:"dose_quantity,omitempty"`
|
||||||
|
Route *common.CodeableConceptDTO `json:"route,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImmunizationPatchRequest []map[string]interface{}
|
type ImmunizationPatchRequest []map[string]interface{}
|
||||||
@@ -1,33 +1,126 @@
|
|||||||
package immunization
|
package immunization
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"service/internal/interfaces/satusehat"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"service/internal/interfaces/satusehat"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MapRequestToFHIR(req ImmunizationRequest) satusehat.FHIRPayload {
|
func MapRequestToFHIR(req ImmunizationRequest) satusehat.FHIRPayload {
|
||||||
payload := satusehat.NewFHIRPayload("Immunization").
|
payload := satusehat.NewFHIRPayload("Immunization")
|
||||||
Set("status", req.Status).
|
|
||||||
Set("vaccineCode", map[string]interface{}{
|
orgID := os.Getenv("SATUSEHAT_ORG_ID")
|
||||||
"coding": []map[string]interface{}{
|
if orgID == "" {
|
||||||
{"system": "http://sys-ids.kemkes.go.id/kfa", "code": req.VaccineCode, "display": req.VaccineDisplay},
|
orgID = req.OrganizationID
|
||||||
},
|
}
|
||||||
}).
|
|
||||||
Set("patient", map[string]interface{}{"reference": "Patient/" + req.PatientID, "display": req.PatientName}).
|
if orgID != "" && req.ImmunizationID != "" {
|
||||||
Set("encounter", map[string]interface{}{"reference": "Encounter/" + req.EncounterID}).
|
payload.Set("identifier", []map[string]interface{}{
|
||||||
Set("occurrenceDateTime", req.OccurrenceDateTime.Format(time.RFC3339)).
|
|
||||||
Set("primarySource", req.PrimarySource).
|
|
||||||
Set("performer", []map[string]interface{}{
|
|
||||||
{
|
{
|
||||||
"actor": map[string]interface{}{
|
"system": "http://sys-ids.kemkes.go.id/immunization/" + orgID,
|
||||||
"reference": "Practitioner/" + req.PractitionerID,
|
"use": "official",
|
||||||
"display": req.PractitionerName,
|
"value": req.ImmunizationID,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Status != "" {
|
||||||
|
payload.Set("status", req.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.VaccineCode != nil {
|
||||||
|
sys := req.VaccineCode.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://sys-ids.kemkes.go.id/kfa"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": req.VaccineCode.Code}
|
||||||
|
if req.VaccineCode.Display != "" {
|
||||||
|
coding["display"] = req.VaccineCode.Display
|
||||||
|
}
|
||||||
|
vaccineConcept := map[string]interface{}{"coding": []map[string]interface{}{coding}}
|
||||||
|
if req.VaccineCode.Text != "" {
|
||||||
|
vaccineConcept["text"] = req.VaccineCode.Text
|
||||||
|
}
|
||||||
|
payload.Set("vaccineCode", vaccineConcept)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.PatientID != "" {
|
||||||
|
subj := map[string]interface{}{"reference": "Patient/" + req.PatientID}
|
||||||
|
if req.PatientName != "" {
|
||||||
|
subj["display"] = req.PatientName
|
||||||
|
}
|
||||||
|
payload.Set("patient", subj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.EncounterID != "" {
|
||||||
|
enc := map[string]interface{}{"reference": "Encounter/" + req.EncounterID}
|
||||||
|
if req.EncounterDisplay != "" {
|
||||||
|
enc["display"] = req.EncounterDisplay
|
||||||
|
}
|
||||||
|
payload.Set("encounter", enc)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.OccurrenceDateTime != nil && !req.OccurrenceDateTime.IsZero() {
|
||||||
|
payload.Set("occurrenceDateTime", req.OccurrenceDateTime.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.PrimarySource != nil {
|
||||||
|
payload.Set("primarySource", *req.PrimarySource)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Location != nil {
|
||||||
|
loc := map[string]interface{}{"reference": req.Location.Reference}
|
||||||
|
if req.Location.Display != "" {
|
||||||
|
loc["display"] = req.Location.Display
|
||||||
|
}
|
||||||
|
payload.Set("location", loc)
|
||||||
|
}
|
||||||
|
|
||||||
if req.LotNumber != "" {
|
if req.LotNumber != "" {
|
||||||
payload.Set("lotNumber", req.LotNumber)
|
payload.Set("lotNumber", req.LotNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(req.Performer) > 0 {
|
||||||
|
var performers []map[string]interface{}
|
||||||
|
for _, p := range req.Performer {
|
||||||
|
actor := map[string]interface{}{"reference": p.Reference}
|
||||||
|
if p.Display != "" {
|
||||||
|
actor["display"] = p.Display
|
||||||
|
}
|
||||||
|
performers = append(performers, map[string]interface{}{"actor": actor})
|
||||||
|
}
|
||||||
|
payload.Set("performer", performers)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.DoseQuantity != nil {
|
||||||
|
sys := req.DoseQuantity.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://terminology.hl7.org/CodeSystem/v3-orderableDrugForm"
|
||||||
|
}
|
||||||
|
payload.Set("doseQuantity", map[string]interface{}{
|
||||||
|
"value": req.DoseQuantity.Value,
|
||||||
|
"unit": req.DoseQuantity.Unit,
|
||||||
|
"system": sys,
|
||||||
|
"code": req.DoseQuantity.Code,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Route != nil {
|
||||||
|
sys := req.Route.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://www.whocc.no/atc"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": req.Route.Code}
|
||||||
|
if req.Route.Display != "" {
|
||||||
|
coding["display"] = req.Route.Display
|
||||||
|
}
|
||||||
|
routeConcept := map[string]interface{}{"coding": []map[string]interface{}{coding}}
|
||||||
|
if req.Route.Text != "" {
|
||||||
|
routeConcept["text"] = req.Route.Text
|
||||||
|
}
|
||||||
|
payload.Set("route", routeConcept)
|
||||||
|
}
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
@@ -19,14 +20,21 @@ type repository struct{ client satusehat.SatuSehatClient }
|
|||||||
|
|
||||||
func NewRepository(client satusehat.SatuSehatClient) Repository { return &repository{client: client} }
|
func NewRepository(client satusehat.SatuSehatClient) Repository { return &repository{client: client} }
|
||||||
|
|
||||||
|
type hiddenCtx struct {
|
||||||
|
context.Context
|
||||||
|
}
|
||||||
|
|
||||||
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
resp, err := r.client.DoRequest(ctx, method, endpoint, req)
|
resp, err := r.client.DoRequest(hiddenCtx{ctx}, method, endpoint, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.InternalError().Message("Failed to execute request to SatuSehat").Cause(err).Build()
|
||||||
}
|
}
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
if err := json.Unmarshal(resp, &result); err != nil {
|
if err := json.Unmarshal(resp, &result); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
return nil, errors.InternalError().Message("Failed to parse SatuSehat response").Cause(err).Metadata("raw_response", string(resp)).Build()
|
||||||
|
}
|
||||||
|
if resourceType, ok := result["resourceType"].(string); ok && resourceType == "OperationOutcome" {
|
||||||
|
return nil, errors.ParseSatuSehatError(result)
|
||||||
}
|
}
|
||||||
var resourceID string
|
var resourceID string
|
||||||
if id, ok := result["id"].(string); ok {
|
if id, ok := result["id"].(string); ok {
|
||||||
|
|||||||
@@ -24,12 +24,23 @@ func (s *service) Update(ctx context.Context, id string, req ImmunizationRequest
|
|||||||
if id == "" {
|
if id == "" {
|
||||||
return nil, errors.NewValidationError().Message("ID is required").Build()
|
return nil, errors.NewValidationError().Message("ID is required").Build()
|
||||||
}
|
}
|
||||||
return s.repo.Update(ctx, id, MapRequestToFHIR(req).Set("id", id))
|
payload := MapRequestToFHIR(req)
|
||||||
|
payload.Set("id", id)
|
||||||
|
return s.repo.Update(ctx, id, payload)
|
||||||
}
|
}
|
||||||
func (s *service) Patch(ctx context.Context, id string, req ImmunizationPatchRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Patch(ctx context.Context, id string, req ImmunizationPatchRequest) (*satusehat.FHIRResponse, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("ID is required").Build()
|
||||||
|
}
|
||||||
|
if len(req) == 0 {
|
||||||
|
return nil, errors.NewValidationError().Message("Patch payload cannot be empty").Build()
|
||||||
|
}
|
||||||
return s.repo.Patch(ctx, id, req)
|
return s.repo.Patch(ctx, id, req)
|
||||||
}
|
}
|
||||||
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("ID is required").Build()
|
||||||
|
}
|
||||||
return s.repo.GetByID(ctx, id)
|
return s.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package medication
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type MedicationRequest struct {
|
type MedicationRequest struct {
|
||||||
|
MedicationID string `json:"medication_id,omitempty"`
|
||||||
|
OrganizationID string `json:"organization_id,omitempty"`
|
||||||
StatusCode string `json:"status_code" binding:"required,oneof=active inactive entered-in-error"`
|
StatusCode string `json:"status_code" binding:"required,oneof=active inactive entered-in-error"`
|
||||||
KfaCode string `json:"kfa_code" binding:"required"`
|
KfaCode string `json:"kfa_code" binding:"required"`
|
||||||
KfaDisplay string `json:"kfa_display" binding:"required"`
|
KfaDisplay string `json:"kfa_display,omitempty"`
|
||||||
FormCode string `json:"form_code,omitempty"`
|
FormCode string `json:"form_code,omitempty"`
|
||||||
FormDisplay string `json:"form_display,omitempty"`
|
FormDisplay string `json:"form_display,omitempty"`
|
||||||
ManufacturerID string `json:"manufacturer_id,omitempty"` // Reference ke ID Organization (Pabrik/Distributor)
|
ManufacturerID string `json:"manufacturer_id,omitempty"` // Reference ke ID Organization (Pabrik/Distributor)
|
||||||
|
|||||||
@@ -1,33 +1,78 @@
|
|||||||
package medication
|
package medication
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MapRequestToFHIR(req MedicationRequest) satusehat.FHIRPayload {
|
func MapRequestToFHIR(req MedicationRequest) satusehat.FHIRPayload {
|
||||||
payload := satusehat.NewFHIRPayload("Medication").
|
payload := satusehat.NewFHIRPayload("Medication")
|
||||||
Set("status", req.StatusCode).
|
|
||||||
Set("code", map[string]interface{}{
|
orgID := os.Getenv("SATUSEHAT_ORG_ID")
|
||||||
|
if orgID == "" {
|
||||||
|
orgID = req.OrganizationID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Meta Profile secara otomatis
|
||||||
|
payload.Set("meta", map[string]interface{}{
|
||||||
|
"profile": []string{"https://fhir.kemkes.go.id/r4/StructureDefinition/Medication"},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set Extension MedicationType (Non-compound) secara otomatis
|
||||||
|
payload.Set("extension", []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"url": "https://fhir.kemkes.go.id/r4/StructureDefinition/MedicationType",
|
||||||
|
"valueCodeableConcept": map[string]interface{}{
|
||||||
"coding": []map[string]interface{}{
|
"coding": []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"system": "http://sys-ids.kemkes.go.id/kfa",
|
"system": "http://terminology.kemkes.go.id/CodeSystem/medication-type",
|
||||||
"code": req.KfaCode,
|
"code": "NC",
|
||||||
"display": req.KfaDisplay,
|
"display": "Non-compound",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if req.FormCode != "" {
|
if orgID != "" && req.MedicationID != "" {
|
||||||
payload.Set("form", map[string]interface{}{
|
payload.Set("identifier", []map[string]interface{}{
|
||||||
"coding": []map[string]interface{}{
|
|
||||||
{
|
{
|
||||||
|
"system": "http://sys-ids.kemkes.go.id/medication/" + orgID,
|
||||||
|
"use": "official",
|
||||||
|
"value": req.MedicationID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.StatusCode != "" {
|
||||||
|
payload.Set("status", req.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.KfaCode != "" {
|
||||||
|
coding := map[string]interface{}{
|
||||||
|
"system": "http://sys-ids.kemkes.go.id/kfa",
|
||||||
|
"code": req.KfaCode,
|
||||||
|
}
|
||||||
|
if req.KfaDisplay != "" {
|
||||||
|
coding["display"] = req.KfaDisplay
|
||||||
|
}
|
||||||
|
payload.Set("code", map[string]interface{}{
|
||||||
|
"coding": []map[string]interface{}{coding},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.FormCode != "" {
|
||||||
|
coding := map[string]interface{}{
|
||||||
"system": "http://terminology.kemkes.go.id/CodeSystem/medication-form",
|
"system": "http://terminology.kemkes.go.id/CodeSystem/medication-form",
|
||||||
"code": req.FormCode,
|
"code": req.FormCode,
|
||||||
"display": req.FormDisplay,
|
}
|
||||||
},
|
if req.FormDisplay != "" {
|
||||||
},
|
coding["display"] = req.FormDisplay
|
||||||
|
}
|
||||||
|
payload.Set("form", map[string]interface{}{
|
||||||
|
"coding": []map[string]interface{}{coding},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,11 +82,15 @@ func MapRequestToFHIR(req MedicationRequest) satusehat.FHIRPayload {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.BatchNumber != "" && req.ExpirationDate != nil {
|
if req.BatchNumber != "" || req.ExpirationDate != nil {
|
||||||
payload.Set("batch", map[string]interface{}{
|
batch := map[string]interface{}{}
|
||||||
"lotNumber": req.BatchNumber,
|
if req.BatchNumber != "" {
|
||||||
"expirationDate": req.ExpirationDate.Format(time.RFC3339),
|
batch["lotNumber"] = req.BatchNumber
|
||||||
})
|
}
|
||||||
|
if req.ExpirationDate != nil {
|
||||||
|
batch["expirationDate"] = req.ExpirationDate.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
payload.Set("batch", batch)
|
||||||
}
|
}
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
@@ -25,15 +26,22 @@ func NewRepository(client satusehat.SatuSehatClient) Repository {
|
|||||||
return &repository{client: client}
|
return &repository{client: client}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type hiddenCtx struct {
|
||||||
|
context.Context
|
||||||
|
}
|
||||||
|
|
||||||
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
resp, err := r.client.DoRequest(ctx, method, endpoint, req)
|
resp, err := r.client.DoRequest(hiddenCtx{ctx}, method, endpoint, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.InternalError().Message("Failed to execute request to SatuSehat").Cause(err).Build()
|
||||||
}
|
}
|
||||||
|
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
if err := json.Unmarshal(resp, &result); err != nil {
|
if err := json.Unmarshal(resp, &result); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
return nil, errors.InternalError().Message("Failed to parse SatuSehat response").Cause(err).Metadata("raw_response", string(resp)).Build()
|
||||||
|
}
|
||||||
|
if resourceType, ok := result["resourceType"].(string); ok && resourceType == "OperationOutcome" {
|
||||||
|
return nil, errors.ParseSatuSehatError(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
var resourceID string
|
var resourceID string
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ func (s *service) Patch(ctx context.Context, id string, req MedicationPatchReque
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("Medication ID is required").Build()
|
||||||
|
}
|
||||||
return s.repo.GetByID(ctx, id)
|
return s.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,22 +3,32 @@ package medicationdispense
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type MedicationDispenseRequest struct {
|
type MedicationDispenseRequest struct {
|
||||||
|
OrganizationID string `json:"organization_id,omitempty"`
|
||||||
PatientID string `json:"patient_id" binding:"required"`
|
PatientID string `json:"patient_id" binding:"required"`
|
||||||
PatientName string `json:"patient_name" binding:"required"`
|
PatientName string `json:"patient_name,omitempty"`
|
||||||
EncounterID string `json:"encounter_id" binding:"required"`
|
EncounterID string `json:"encounter_id" binding:"required"`
|
||||||
MedicationID string `json:"medication_id" binding:"required"`
|
MedicationID string `json:"medication_id" binding:"required"`
|
||||||
MedicationDisplay string `json:"medication_display" binding:"required"`
|
MedicationDisplay string `json:"medication_display,omitempty"`
|
||||||
PractitionerID string `json:"practitioner_id" binding:"required"`
|
PractitionerID string `json:"practitioner_id" binding:"required"`
|
||||||
PractitionerName string `json:"practitioner_name" binding:"required"`
|
PractitionerName string `json:"practitioner_name,omitempty"`
|
||||||
LocationID string `json:"location_id" binding:"required"`
|
LocationID string `json:"location_id" binding:"required"`
|
||||||
LocationName string `json:"location_name" binding:"required"`
|
LocationName string `json:"location_name,omitempty"`
|
||||||
PrescriptionID string `json:"prescription_id" binding:"required"` // Reference to MedicationRequest
|
PrescriptionID string `json:"prescription_id" binding:"required"` // Digunakan untuk Prescription Identifier
|
||||||
|
PrescriptionItemID string `json:"prescription_item_id,omitempty"` // Digunakan untuk Identifier item
|
||||||
Status string `json:"status" binding:"required,oneof=preparation in-progress cancelled on-hold completed entered-in-error stopped declined unknown"`
|
Status string `json:"status" binding:"required,oneof=preparation in-progress cancelled on-hold completed entered-in-error stopped declined unknown"`
|
||||||
|
Category string `json:"category,omitempty" binding:"omitempty,oneof=outpatient inpatient community discharge"`
|
||||||
PreparedDate time.Time `json:"prepared_date" binding:"required"`
|
PreparedDate time.Time `json:"prepared_date" binding:"required"`
|
||||||
HandedOverDate time.Time `json:"handed_over_date" binding:"required"`
|
HandedOverDate time.Time `json:"handed_over_date" binding:"required"`
|
||||||
DispenseValue float64 `json:"dispense_value" binding:"required"`
|
QuantityValue float64 `json:"quantity_value" binding:"required"`
|
||||||
DispenseUnit string `json:"dispense_unit" binding:"required"`
|
QuantityUnit string `json:"quantity_unit" binding:"required"` // e.g. TAB
|
||||||
DosagePatientInstr string `json:"dosage_patient_instruction"`
|
DaysSupplyValue int `json:"days_supply_value,omitempty"`
|
||||||
|
DosageText string `json:"dosage_text,omitempty"`
|
||||||
|
TimingFrequency int `json:"timing_frequency,omitempty"`
|
||||||
|
TimingPeriod int `json:"timing_period,omitempty"`
|
||||||
|
TimingPeriodUnit string `json:"timing_period_unit,omitempty"` // e.g. d, h
|
||||||
|
DoseQuantityValue float64 `json:"dose_quantity_value,omitempty"`
|
||||||
|
DoseQuantityUnit string `json:"dose_quantity_unit,omitempty"` // e.g. TAB
|
||||||
|
MedicationRequestID string `json:"medication_request_id" binding:"required"` //Digunakan untuk authorizingPrescriptionID
|
||||||
}
|
}
|
||||||
|
|
||||||
type MedicationDispensePatchRequest []map[string]interface{}
|
type MedicationDispensePatchRequest []map[string]interface{}
|
||||||
@@ -1,51 +1,171 @@
|
|||||||
package medicationdispense
|
package medicationdispense
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MapRequestToFHIR(req MedicationDispenseRequest) satusehat.FHIRPayload {
|
func MapRequestToFHIR(req MedicationDispenseRequest) satusehat.FHIRPayload {
|
||||||
payload := satusehat.NewFHIRPayload("MedicationDispense").
|
payload := satusehat.NewFHIRPayload("MedicationDispense")
|
||||||
Set("status", req.Status).
|
|
||||||
Set("category", map[string]interface{}{
|
orgID := os.Getenv("SATUSEHAT_ORG_ID")
|
||||||
|
if orgID == "" {
|
||||||
|
orgID = req.OrganizationID
|
||||||
|
}
|
||||||
|
|
||||||
|
if orgID != "" && req.PrescriptionID != "" {
|
||||||
|
identifiers := []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"system": "http://sys-ids.kemkes.go.id/prescription/" + orgID,
|
||||||
|
"use": "official",
|
||||||
|
"value": req.PrescriptionID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if req.PrescriptionItemID != "" {
|
||||||
|
identifiers = append(identifiers, map[string]interface{}{
|
||||||
|
"system": "http://sys-ids.kemkes.go.id/prescription-item/" + orgID,
|
||||||
|
"use": "official",
|
||||||
|
"value": req.PrescriptionItemID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
payload.Set("identifier", identifiers)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Status != "" {
|
||||||
|
payload.Set("status", req.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryCode := "outpatient"
|
||||||
|
categoryDisplay := "Outpatient"
|
||||||
|
if req.Category != "" {
|
||||||
|
categoryCode = req.Category
|
||||||
|
switch req.Category {
|
||||||
|
case "inpatient":
|
||||||
|
categoryDisplay = "Inpatient"
|
||||||
|
case "community":
|
||||||
|
categoryDisplay = "Community"
|
||||||
|
case "discharge":
|
||||||
|
categoryDisplay = "Discharge"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payload.Set("category", map[string]interface{}{
|
||||||
"coding": []map[string]interface{}{
|
"coding": []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"system": "http://terminology.hl7.org/fhir/CodeSystem/medicationdispense-category",
|
"system": "http://terminology.hl7.org/fhir/CodeSystem/medicationdispense-category",
|
||||||
"code": "outpatient",
|
"code": categoryCode,
|
||||||
"display": "Outpatient",
|
"display": categoryDisplay,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}).
|
})
|
||||||
Set("medicationReference", map[string]interface{}{
|
|
||||||
"reference": "Medication/" + req.MedicationID,
|
if req.MedicationID != "" {
|
||||||
"display": req.MedicationDisplay,
|
medRef := map[string]interface{}{"reference": "Medication/" + req.MedicationID}
|
||||||
}).
|
if req.MedicationDisplay != "" {
|
||||||
Set("subject", map[string]interface{}{
|
medRef["display"] = req.MedicationDisplay
|
||||||
"reference": "Patient/" + req.PatientID,
|
}
|
||||||
"display": req.PatientName,
|
payload.Set("medicationReference", medRef)
|
||||||
}).
|
}
|
||||||
Set("context", map[string]interface{}{
|
|
||||||
"reference": "Encounter/" + req.EncounterID,
|
if req.PatientID != "" {
|
||||||
}).
|
subj := map[string]interface{}{"reference": "Patient/" + req.PatientID}
|
||||||
Set("performer", []map[string]interface{}{
|
if req.PatientName != "" {
|
||||||
|
subj["display"] = req.PatientName
|
||||||
|
}
|
||||||
|
payload.Set("subject", subj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.EncounterID != "" {
|
||||||
|
payload.Set("context", map[string]interface{}{"reference": "Encounter/" + req.EncounterID})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.PractitionerID != "" {
|
||||||
|
actor := map[string]interface{}{"reference": "Practitioner/" + req.PractitionerID}
|
||||||
|
if req.PractitionerName != "" {
|
||||||
|
actor["display"] = req.PractitionerName
|
||||||
|
}
|
||||||
|
payload.Set("performer", []map[string]interface{}{{"actor": actor}})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.LocationID != "" {
|
||||||
|
loc := map[string]interface{}{"reference": "Location/" + req.LocationID}
|
||||||
|
if req.LocationName != "" {
|
||||||
|
loc["display"] = req.LocationName
|
||||||
|
}
|
||||||
|
payload.Set("location", loc)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.MedicationRequestID != "" {
|
||||||
|
payload.Set("authorizingPrescription", []map[string]interface{}{{"reference": "MedicationRequest/" + req.MedicationRequestID}})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.QuantityValue != 0 || req.QuantityUnit != "" {
|
||||||
|
qty := map[string]interface{}{
|
||||||
|
"system": "http://terminology.hl7.org/CodeSystem/v3-orderableDrugForm",
|
||||||
|
}
|
||||||
|
if req.QuantityValue != 0 {
|
||||||
|
qty["value"] = req.QuantityValue
|
||||||
|
}
|
||||||
|
if req.QuantityUnit != "" {
|
||||||
|
qty["code"] = req.QuantityUnit
|
||||||
|
}
|
||||||
|
payload.Set("quantity", qty)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.DaysSupplyValue != 0 {
|
||||||
|
payload.Set("daysSupply", map[string]interface{}{
|
||||||
|
"value": req.DaysSupplyValue,
|
||||||
|
"unit": "Day",
|
||||||
|
"system": "http://unitsofmeasure.org",
|
||||||
|
"code": "d",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !req.PreparedDate.IsZero() {
|
||||||
|
payload.Set("whenPrepared", req.PreparedDate.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
if !req.HandedOverDate.IsZero() {
|
||||||
|
payload.Set("whenHandedOver", req.HandedOverDate.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.DosageText != "" || req.TimingFrequency != 0 || req.DoseQuantityValue != 0 {
|
||||||
|
dose := map[string]interface{}{"sequence": 1}
|
||||||
|
if req.DosageText != "" {
|
||||||
|
dose["text"] = req.DosageText
|
||||||
|
}
|
||||||
|
if req.TimingFrequency != 0 && req.TimingPeriod != 0 && req.TimingPeriodUnit != "" {
|
||||||
|
dose["timing"] = map[string]interface{}{
|
||||||
|
"repeat": map[string]interface{}{
|
||||||
|
"frequency": req.TimingFrequency,
|
||||||
|
"period": req.TimingPeriod,
|
||||||
|
"periodUnit": req.TimingPeriodUnit,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.DoseQuantityValue != 0 && req.DoseQuantityUnit != "" {
|
||||||
|
dose["doseAndRate"] = []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"actor": map[string]interface{}{
|
"type": map[string]interface{}{
|
||||||
"reference": "Practitioner/" + req.PractitionerID,
|
"coding": []map[string]interface{}{
|
||||||
"display": req.PractitionerName,
|
{
|
||||||
|
"system": "http://terminology.hl7.org/CodeSystem/dose-rate-type",
|
||||||
|
"code": "ordered",
|
||||||
|
"display": "Ordered",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}).
|
},
|
||||||
Set("location", map[string]interface{}{
|
"doseQuantity": map[string]interface{}{
|
||||||
"reference": "Location/" + req.LocationID,
|
"value": req.DoseQuantityValue,
|
||||||
"display": req.LocationName,
|
"unit": req.DoseQuantityUnit,
|
||||||
}).
|
"system": "http://terminology.hl7.org/CodeSystem/v3-orderableDrugForm",
|
||||||
Set("authorizingPrescription", []map[string]interface{}{
|
"code": req.DoseQuantityUnit,
|
||||||
{"reference": "MedicationRequest/" + req.PrescriptionID},
|
},
|
||||||
}).
|
},
|
||||||
Set("whenPrepared", req.PreparedDate.Format(time.RFC3339)).
|
}
|
||||||
Set("whenHandedOver", req.HandedOverDate.Format(time.RFC3339))
|
}
|
||||||
|
payload.Set("dosageInstruction", []map[string]interface{}{dose})
|
||||||
|
}
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
@@ -25,14 +26,21 @@ func NewRepository(client satusehat.SatuSehatClient) Repository {
|
|||||||
return &repository{client: client}
|
return &repository{client: client}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type hiddenCtx struct {
|
||||||
|
context.Context
|
||||||
|
}
|
||||||
|
|
||||||
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
resp, err := r.client.DoRequest(ctx, method, endpoint, req)
|
resp, err := r.client.DoRequest(hiddenCtx{ctx}, method, endpoint, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.InternalError().Message("Failed to execute request to SatuSehat").Cause(err).Build()
|
||||||
}
|
}
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
if err := json.Unmarshal(resp, &result); err != nil {
|
if err := json.Unmarshal(resp, &result); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
return nil, errors.InternalError().Message("Failed to parse SatuSehat response").Cause(err).Metadata("raw_response", string(resp)).Build()
|
||||||
|
}
|
||||||
|
if resourceType, ok := result["resourceType"].(string); ok && resourceType == "OperationOutcome" {
|
||||||
|
return nil, errors.ParseSatuSehatError(result)
|
||||||
}
|
}
|
||||||
var resourceID string
|
var resourceID string
|
||||||
if id, ok := result["id"].(string); ok {
|
if id, ok := result["id"].(string); ok {
|
||||||
|
|||||||
@@ -36,9 +36,18 @@ func (s *service) Update(ctx context.Context, id string, req MedicationDispenseR
|
|||||||
return s.repo.Update(ctx, id, payload)
|
return s.repo.Update(ctx, id, payload)
|
||||||
}
|
}
|
||||||
func (s *service) Patch(ctx context.Context, id string, req MedicationDispensePatchRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Patch(ctx context.Context, id string, req MedicationDispensePatchRequest) (*satusehat.FHIRResponse, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("MedicationDispense ID is required").Build()
|
||||||
|
}
|
||||||
|
if len(req) == 0 {
|
||||||
|
return nil, errors.NewValidationError().Message("Patch payload cannot be empty").Build()
|
||||||
|
}
|
||||||
return s.repo.Patch(ctx, id, req)
|
return s.repo.Patch(ctx, id, req)
|
||||||
}
|
}
|
||||||
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("MedicationDispense ID is required").Build()
|
||||||
|
}
|
||||||
return s.repo.GetByID(ctx, id)
|
return s.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
||||||
|
|||||||
@@ -3,21 +3,41 @@ package medicationrequest
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type MedicationRequestRequest struct {
|
type MedicationRequestRequest struct {
|
||||||
|
MedicationRequestID string `json:"medicationrequest_id,omitempty"` // Contoh: No Resep / ID Resep
|
||||||
|
PrescriptionItemID string `json:"prescription_item_id,omitempty"`
|
||||||
|
OrganizationID string `json:"organization_id,omitempty"`
|
||||||
|
Category string `json:"category,omitempty" binding:"omitempty,oneof=outpatient inpatient community discharge"`
|
||||||
|
Priority string `json:"priority,omitempty" binding:"omitempty,oneof=routine urgent asap stat"`
|
||||||
PatientID string `json:"patient_id" binding:"required"`
|
PatientID string `json:"patient_id" binding:"required"`
|
||||||
PatientName string `json:"patient_name" binding:"required"`
|
PatientName string `json:"patient_name,omitempty"`
|
||||||
EncounterID string `json:"encounter_id" binding:"required"`
|
EncounterID string `json:"encounter_id" binding:"required"`
|
||||||
PractitionerID string `json:"practitioner_id" binding:"required"`
|
PractitionerID string `json:"practitioner_id" binding:"required"`
|
||||||
PractitionerName string `json:"practitioner_name" binding:"required"`
|
PractitionerName string `json:"practitioner_name,omitempty"`
|
||||||
MedicationID string `json:"medication_id" binding:"required"` // Reference to Medication resource
|
MedicationID string `json:"medication_id" binding:"required"` // Reference to Medication resource
|
||||||
MedicationDisplay string `json:"medication_display" binding:"required"`
|
MedicationDisplay string `json:"medication_display,omitempty"`
|
||||||
|
ReasonCode string `json:"reason_code,omitempty"`
|
||||||
|
ReasonDisplay string `json:"reason_display,omitempty"`
|
||||||
|
CourseOfTherapyCode string `json:"course_of_therapy_code,omitempty"`
|
||||||
|
CourseOfTherapyDisplay string `json:"course_of_therapy_display,omitempty"`
|
||||||
Status string `json:"status" binding:"required,oneof=active on-hold cancelled completed entered-in-error stopped draft unknown"`
|
Status string `json:"status" binding:"required,oneof=active on-hold cancelled completed entered-in-error stopped draft unknown"`
|
||||||
Intent string `json:"intent" binding:"required,oneof=proposal plan order original-order reflex-order filler-order instance-order option"`
|
Intent string `json:"intent" binding:"required,oneof=proposal plan order original-order reflex-order filler-order instance-order option"`
|
||||||
AuthoredOn time.Time `json:"authored_on" binding:"required"`
|
AuthoredOn time.Time `json:"authored_on" binding:"required"`
|
||||||
DosageText string `json:"dosage_text" binding:"required"`
|
DosageText string `json:"dosage_text,omitempty"`
|
||||||
|
AdditionalInstruction string `json:"additional_instruction,omitempty"`
|
||||||
PatientInstr string `json:"patient_instruction"`
|
PatientInstr string `json:"patient_instruction"`
|
||||||
DispenseValue float64 `json:"dispense_value" binding:"required"`
|
TimingFrequency int `json:"timing_frequency,omitempty"`
|
||||||
DispenseUnit string `json:"dispense_unit" binding:"required"`
|
TimingPeriod int `json:"timing_period,omitempty"`
|
||||||
SupplyDuration int `json:"supply_duration" binding:"required"` // in days
|
TimingPeriodUnit string `json:"timing_period_unit,omitempty"` // d (days), h (hours), etc
|
||||||
|
RouteCode string `json:"route_code,omitempty"` // e.g. O (Oral)
|
||||||
|
RouteDisplay string `json:"route_display,omitempty"`
|
||||||
|
DoseQuantityValue float64 `json:"dose_quantity_value,omitempty"`
|
||||||
|
DoseQuantityUnit string `json:"dose_quantity_unit,omitempty"` // TAB, CAP, etc
|
||||||
|
DispenseInterval int `json:"dispense_interval,omitempty"`
|
||||||
|
DispenseValue float64 `json:"dispense_value,omitempty"`
|
||||||
|
DispenseUnit string `json:"dispense_unit,omitempty"`
|
||||||
|
SupplyDuration int `json:"supply_duration,omitempty"` // in days
|
||||||
|
ValidityPeriodStart *time.Time `json:"validity_period_start,omitempty"`
|
||||||
|
ValidityPeriodEnd *time.Time `json:"validity_period_end,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MedicationRequestPatchRequest []map[string]interface{}
|
type MedicationRequestPatchRequest []map[string]interface{}
|
||||||
@@ -1,60 +1,245 @@
|
|||||||
package medicationrequest
|
package medicationrequest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MapRequestToFHIR(req MedicationRequestRequest) satusehat.FHIRPayload {
|
func MapRequestToFHIR(req MedicationRequestRequest) satusehat.FHIRPayload {
|
||||||
payload := satusehat.NewFHIRPayload("MedicationRequest").
|
payload := satusehat.NewFHIRPayload("MedicationRequest")
|
||||||
Set("status", req.Status).
|
|
||||||
Set("intent", req.Intent).
|
orgID := os.Getenv("SATUSEHAT_ORG_ID")
|
||||||
Set("category", []map[string]interface{}{
|
if orgID == "" {
|
||||||
|
orgID = req.OrganizationID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Meta Profile
|
||||||
|
payload.Set("meta", map[string]interface{}{
|
||||||
|
"profile": []string{"https://fhir.kemkes.go.id/r4/StructureDefinition/MedicationRequest"},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set Identifier (ID Resep)
|
||||||
|
if orgID != "" && req.MedicationRequestID != "" {
|
||||||
|
identifiers := []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"system": "http://sys-ids.kemkes.go.id/prescription/" + orgID,
|
||||||
|
"use": "official",
|
||||||
|
"value": req.MedicationRequestID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if req.PrescriptionItemID != "" {
|
||||||
|
identifiers = append(identifiers, map[string]interface{}{
|
||||||
|
"system": "http://sys-ids.kemkes.go.id/prescription-item/" + orgID,
|
||||||
|
"use": "official",
|
||||||
|
"value": req.PrescriptionItemID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
payload.Set("identifier", identifiers)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Status != "" {
|
||||||
|
payload.Set("status", req.Status)
|
||||||
|
}
|
||||||
|
if req.Intent != "" {
|
||||||
|
payload.Set("intent", req.Intent)
|
||||||
|
}
|
||||||
|
if req.Priority != "" {
|
||||||
|
payload.Set("priority", req.Priority)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Penanganan Category dinamis dengan default 'outpatient'
|
||||||
|
categoryCode := "outpatient"
|
||||||
|
categoryDisplay := "Outpatient"
|
||||||
|
if req.Category != "" {
|
||||||
|
categoryCode = req.Category
|
||||||
|
switch req.Category {
|
||||||
|
case "inpatient":
|
||||||
|
categoryDisplay = "Inpatient"
|
||||||
|
case "community":
|
||||||
|
categoryDisplay = "Community"
|
||||||
|
case "discharge":
|
||||||
|
categoryDisplay = "Discharge"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payload.Set("category", []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"coding": []map[string]interface{}{
|
"coding": []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"system": "http://terminology.hl7.org/CodeSystem/medicationrequest-category",
|
"system": "http://terminology.hl7.org/CodeSystem/medicationrequest-category",
|
||||||
"code": "outpatient",
|
"code": categoryCode,
|
||||||
"display": "Outpatient",
|
"display": categoryDisplay,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}).
|
})
|
||||||
Set("medicationReference", map[string]interface{}{
|
|
||||||
"reference": "Medication/" + req.MedicationID,
|
if req.MedicationID != "" {
|
||||||
"display": req.MedicationDisplay,
|
medRef := map[string]interface{}{"reference": "Medication/" + req.MedicationID}
|
||||||
}).
|
if req.MedicationDisplay != "" {
|
||||||
Set("subject", map[string]interface{}{
|
medRef["display"] = req.MedicationDisplay
|
||||||
"reference": "Patient/" + req.PatientID,
|
}
|
||||||
"display": req.PatientName,
|
payload.Set("medicationReference", medRef)
|
||||||
}).
|
}
|
||||||
Set("encounter", map[string]interface{}{
|
|
||||||
"reference": "Encounter/" + req.EncounterID,
|
if req.ReasonCode != "" {
|
||||||
}).
|
reasonCoding := map[string]interface{}{
|
||||||
Set("authoredOn", req.AuthoredOn.Format(time.RFC3339)).
|
"system": "http://hl7.org/fhir/sid/icd-10",
|
||||||
Set("requester", map[string]interface{}{
|
"code": req.ReasonCode,
|
||||||
"reference": "Practitioner/" + req.PractitionerID,
|
}
|
||||||
"display": req.PractitionerName,
|
if req.ReasonDisplay != "" {
|
||||||
}).
|
reasonCoding["display"] = req.ReasonDisplay
|
||||||
Set("dosageInstruction", []map[string]interface{}{
|
}
|
||||||
|
payload.Set("reasonCode", []map[string]interface{}{
|
||||||
|
{"coding": []map[string]interface{}{reasonCoding}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.CourseOfTherapyCode != "" {
|
||||||
|
courseCoding := map[string]interface{}{
|
||||||
|
"system": "http://terminology.hl7.org/CodeSystem/medicationrequest-course-of-therapy",
|
||||||
|
"code": req.CourseOfTherapyCode,
|
||||||
|
}
|
||||||
|
if req.CourseOfTherapyDisplay != "" {
|
||||||
|
courseCoding["display"] = req.CourseOfTherapyDisplay
|
||||||
|
}
|
||||||
|
payload.Set("courseOfTherapyType", map[string]interface{}{
|
||||||
|
"coding": []map[string]interface{}{courseCoding},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.PatientID != "" {
|
||||||
|
subj := map[string]interface{}{"reference": "Patient/" + req.PatientID}
|
||||||
|
if req.PatientName != "" {
|
||||||
|
subj["display"] = req.PatientName
|
||||||
|
}
|
||||||
|
payload.Set("subject", subj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.EncounterID != "" {
|
||||||
|
payload.Set("encounter", map[string]interface{}{"reference": "Encounter/" + req.EncounterID})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !req.AuthoredOn.IsZero() {
|
||||||
|
payload.Set("authoredOn", req.AuthoredOn.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.PractitionerID != "" {
|
||||||
|
reqRef := map[string]interface{}{"reference": "Practitioner/" + req.PractitionerID}
|
||||||
|
if req.PractitionerName != "" {
|
||||||
|
reqRef["display"] = req.PractitionerName
|
||||||
|
}
|
||||||
|
payload.Set("requester", reqRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.DosageText != "" || req.PatientInstr != "" || req.DoseQuantityValue != 0 || req.TimingFrequency != 0 {
|
||||||
|
dose := map[string]interface{}{"sequence": 1}
|
||||||
|
if req.DosageText != "" {
|
||||||
|
dose["text"] = req.DosageText
|
||||||
|
}
|
||||||
|
if req.AdditionalInstruction != "" {
|
||||||
|
dose["additionalInstruction"] = []map[string]interface{}{
|
||||||
|
{"text": req.AdditionalInstruction},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.PatientInstr != "" {
|
||||||
|
dose["patientInstruction"] = req.PatientInstr
|
||||||
|
}
|
||||||
|
if req.TimingFrequency != 0 && req.TimingPeriod != 0 && req.TimingPeriodUnit != "" {
|
||||||
|
dose["timing"] = map[string]interface{}{
|
||||||
|
"repeat": map[string]interface{}{
|
||||||
|
"frequency": req.TimingFrequency,
|
||||||
|
"period": req.TimingPeriod,
|
||||||
|
"periodUnit": req.TimingPeriodUnit,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.RouteCode != "" {
|
||||||
|
routeCoding := map[string]interface{}{
|
||||||
|
"system": "http://www.whocc.no/atc",
|
||||||
|
"code": req.RouteCode,
|
||||||
|
}
|
||||||
|
if req.RouteDisplay != "" {
|
||||||
|
routeCoding["display"] = req.RouteDisplay
|
||||||
|
}
|
||||||
|
dose["route"] = map[string]interface{}{
|
||||||
|
"coding": []map[string]interface{}{routeCoding},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.DoseQuantityValue != 0 && req.DoseQuantityUnit != "" {
|
||||||
|
dose["doseAndRate"] = []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"sequence": 1,
|
"type": map[string]interface{}{
|
||||||
"text": req.DosageText,
|
"coding": []map[string]interface{}{
|
||||||
"patientInstruction": req.PatientInstr,
|
{
|
||||||
|
"system": "http://terminology.hl7.org/CodeSystem/dose-rate-type",
|
||||||
|
"code": "ordered",
|
||||||
|
"display": "Ordered",
|
||||||
},
|
},
|
||||||
}).
|
},
|
||||||
Set("dispenseRequest", map[string]interface{}{
|
},
|
||||||
"quantity": map[string]interface{}{
|
"doseQuantity": map[string]interface{}{
|
||||||
|
"value": req.DoseQuantityValue,
|
||||||
|
"unit": req.DoseQuantityUnit,
|
||||||
|
"system": "http://terminology.hl7.org/CodeSystem/v3-orderableDrugForm",
|
||||||
|
"code": req.DoseQuantityUnit,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payload.Set("dosageInstruction", []map[string]interface{}{dose})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.DispenseValue != 0 || req.SupplyDuration != 0 || req.DispenseInterval != 0 {
|
||||||
|
dispenseReq := map[string]interface{}{}
|
||||||
|
if req.DispenseInterval != 0 {
|
||||||
|
dispenseReq["dispenseInterval"] = map[string]interface{}{
|
||||||
|
"value": req.DispenseInterval,
|
||||||
|
"unit": "days",
|
||||||
|
"system": "http://unitsofmeasure.org",
|
||||||
|
"code": "d",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.ValidityPeriodStart != nil || req.ValidityPeriodEnd != nil {
|
||||||
|
validity := map[string]interface{}{}
|
||||||
|
if req.ValidityPeriodStart != nil {
|
||||||
|
validity["start"] = req.ValidityPeriodStart.Format("2006-01-02")
|
||||||
|
}
|
||||||
|
if req.ValidityPeriodEnd != nil {
|
||||||
|
validity["end"] = req.ValidityPeriodEnd.Format("2006-01-02")
|
||||||
|
}
|
||||||
|
dispenseReq["validityPeriod"] = validity
|
||||||
|
}
|
||||||
|
|
||||||
|
dispenseReq["numberOfRepeatsAllowed"] = 0
|
||||||
|
|
||||||
|
if req.DispenseValue != 0 {
|
||||||
|
qty := map[string]interface{}{
|
||||||
"value": req.DispenseValue,
|
"value": req.DispenseValue,
|
||||||
"code": req.DispenseUnit,
|
"system": "http://terminology.hl7.org/CodeSystem/v3-orderableDrugForm",
|
||||||
},
|
}
|
||||||
"expectedSupplyDuration": map[string]interface{}{
|
if req.DispenseUnit != "" {
|
||||||
|
qty["unit"] = req.DispenseUnit
|
||||||
|
qty["code"] = req.DispenseUnit
|
||||||
|
}
|
||||||
|
dispenseReq["quantity"] = qty
|
||||||
|
}
|
||||||
|
if req.SupplyDuration != 0 {
|
||||||
|
dispenseReq["expectedSupplyDuration"] = map[string]interface{}{
|
||||||
"value": req.SupplyDuration,
|
"value": req.SupplyDuration,
|
||||||
"unit": "days",
|
"unit": "days",
|
||||||
"system": "http://unitsofmeasure.org",
|
"system": "http://unitsofmeasure.org",
|
||||||
"code": "d",
|
"code": "d",
|
||||||
},
|
}
|
||||||
})
|
}
|
||||||
|
if orgID != "" {
|
||||||
|
dispenseReq["performer"] = map[string]interface{}{
|
||||||
|
"reference": "Organization/" + orgID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payload.Set("dispenseRequest", dispenseReq)
|
||||||
|
}
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
@@ -25,14 +26,21 @@ func NewRepository(client satusehat.SatuSehatClient) Repository {
|
|||||||
return &repository{client: client}
|
return &repository{client: client}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type hiddenCtx struct {
|
||||||
|
context.Context
|
||||||
|
}
|
||||||
|
|
||||||
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
resp, err := r.client.DoRequest(ctx, method, endpoint, req)
|
resp, err := r.client.DoRequest(hiddenCtx{ctx}, method, endpoint, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.InternalError().Message("Failed to execute request to SatuSehat").Cause(err).Build()
|
||||||
}
|
}
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
if err := json.Unmarshal(resp, &result); err != nil {
|
if err := json.Unmarshal(resp, &result); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
return nil, errors.InternalError().Message("Failed to parse SatuSehat response").Cause(err).Metadata("raw_response", string(resp)).Build()
|
||||||
|
}
|
||||||
|
if resourceType, ok := result["resourceType"].(string); ok && resourceType == "OperationOutcome" {
|
||||||
|
return nil, errors.ParseSatuSehatError(result)
|
||||||
}
|
}
|
||||||
var resourceID string
|
var resourceID string
|
||||||
if id, ok := result["id"].(string); ok {
|
if id, ok := result["id"].(string); ok {
|
||||||
|
|||||||
@@ -36,9 +36,18 @@ func (s *service) Update(ctx context.Context, id string, req MedicationRequestRe
|
|||||||
return s.repo.Update(ctx, id, payload)
|
return s.repo.Update(ctx, id, payload)
|
||||||
}
|
}
|
||||||
func (s *service) Patch(ctx context.Context, id string, req MedicationRequestPatchRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Patch(ctx context.Context, id string, req MedicationRequestPatchRequest) (*satusehat.FHIRResponse, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("MedicationRequest ID is required").Build()
|
||||||
|
}
|
||||||
|
if len(req) == 0 {
|
||||||
|
return nil, errors.NewValidationError().Message("Patch payload cannot be empty").Build()
|
||||||
|
}
|
||||||
return s.repo.Patch(ctx, id, req)
|
return s.repo.Patch(ctx, id, req)
|
||||||
}
|
}
|
||||||
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("MedicationRequest ID is required").Build()
|
||||||
|
}
|
||||||
return s.repo.GetByID(ctx, id)
|
return s.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
||||||
|
|||||||
@@ -1,18 +1,35 @@
|
|||||||
package medicationstatement
|
package medicationstatement
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service/internal/satusehat/common"
|
||||||
|
)
|
||||||
|
|
||||||
type MedicationStatementRequest struct {
|
type MedicationStatementRequest struct {
|
||||||
PatientID string `json:"patient_id" binding:"required"`
|
OrganizationID string `json:"organization_id,omitempty"`
|
||||||
PatientName string `json:"patient_name" binding:"required"`
|
StatementID string `json:"statement_id,omitempty"`
|
||||||
EncounterID string `json:"encounter_id" binding:"required"`
|
|
||||||
Status string `json:"status" binding:"required,oneof=active completed entered-in-error intended stopped on-hold unknown not-taken"`
|
Status string `json:"status" binding:"required,oneof=active completed entered-in-error intended stopped on-hold unknown not-taken"`
|
||||||
CategoryCode string `json:"category_code" binding:"required,oneof=inpatient outpatient community"`
|
Category *common.CodeableConceptDTO `json:"category,omitempty"`
|
||||||
MedicationCode string `json:"medication_code" binding:"required"` // KFA Code
|
Medication *common.CodeableConceptDTO `json:"medication" binding:"required"`
|
||||||
MedicationDisplay string `json:"medication_display" binding:"required"` // KFA Name
|
Subject *common.ReferenceDTO `json:"subject" binding:"required"`
|
||||||
EffectiveDateTime time.Time `json:"effective_date_time" binding:"required"`
|
Context *common.ReferenceDTO `json:"context,omitempty"` // Encounter
|
||||||
DateAsserted time.Time `json:"date_asserted" binding:"required"`
|
EffectiveDateTime *time.Time `json:"effective_date_time,omitempty"`
|
||||||
DosageText string `json:"dosage_text" binding:"required"`
|
DateAsserted *time.Time `json:"date_asserted,omitempty"`
|
||||||
|
InformationSource *common.ReferenceDTO `json:"information_source,omitempty"`
|
||||||
|
Dosage []DosageDTO `json:"dosage,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DosageDTO struct {
|
||||||
|
Sequence int `json:"sequence,omitempty"`
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
PatientInstruction string `json:"patient_instruction,omitempty"`
|
||||||
|
Route *common.CodeableConceptDTO `json:"route,omitempty"`
|
||||||
|
DoseAndRate []DoseAndRateDTO `json:"dose_and_rate,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DoseAndRateDTO struct {
|
||||||
|
DoseQuantity *common.QuantityDTO `json:"dose_quantity,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MedicationStatementPatchRequest []map[string]interface{}
|
type MedicationStatementPatchRequest []map[string]interface{}
|
||||||
@@ -1,50 +1,141 @@
|
|||||||
package medicationstatement
|
package medicationstatement
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MapRequestToFHIR(req MedicationStatementRequest) satusehat.FHIRPayload {
|
func MapRequestToFHIR(req MedicationStatementRequest) satusehat.FHIRPayload {
|
||||||
payload := satusehat.NewFHIRPayload("MedicationStatement").
|
payload := satusehat.NewFHIRPayload("MedicationStatement")
|
||||||
Set("status", req.Status).
|
|
||||||
Set("category", map[string]interface{}{
|
orgID := os.Getenv("SATUSEHAT_ORG_ID")
|
||||||
"coding": []map[string]interface{}{
|
if orgID == "" {
|
||||||
|
orgID = req.OrganizationID
|
||||||
|
}
|
||||||
|
|
||||||
|
if orgID != "" && req.StatementID != "" {
|
||||||
|
payload.Set("identifier", []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"system": "http://terminology.hl7.org/CodeSystem/medication-statement-category",
|
"system": "http://sys-ids.kemkes.go.id/medicationstatement/" + orgID,
|
||||||
"code": req.CategoryCode,
|
"use": "official",
|
||||||
"display": req.CategoryCode,
|
"value": req.StatementID,
|
||||||
},
|
|
||||||
},
|
|
||||||
}).
|
|
||||||
Set("medicationCodeableConcept", map[string]interface{}{
|
|
||||||
"coding": []map[string]interface{}{
|
|
||||||
{
|
|
||||||
"system": "http://sys-ids.kemkes.go.id/kfa",
|
|
||||||
"code": req.MedicationCode,
|
|
||||||
"display": req.MedicationDisplay,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).
|
|
||||||
Set("subject", map[string]interface{}{
|
|
||||||
"reference": "Patient/" + req.PatientID,
|
|
||||||
"display": req.PatientName,
|
|
||||||
}).
|
|
||||||
Set("context", map[string]interface{}{
|
|
||||||
"reference": "Encounter/" + req.EncounterID,
|
|
||||||
}).
|
|
||||||
Set("informationSource", map[string]interface{}{
|
|
||||||
"reference": "Patient/" + req.PatientID,
|
|
||||||
"display": req.PatientName,
|
|
||||||
}).
|
|
||||||
Set("effectiveDateTime", req.EffectiveDateTime.Format(time.RFC3339)).
|
|
||||||
Set("dateAsserted", req.DateAsserted.Format(time.RFC3339)).
|
|
||||||
Set("dosage", []map[string]interface{}{
|
|
||||||
{
|
|
||||||
"text": req.DosageText,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Status != "" {
|
||||||
|
payload.Set("status", req.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Category != nil {
|
||||||
|
sys := req.Category.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://terminology.hl7.org/CodeSystem/medication-statement-category"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": req.Category.Code}
|
||||||
|
if req.Category.Display != "" {
|
||||||
|
coding["display"] = req.Category.Display
|
||||||
|
}
|
||||||
|
payload.Set("category", map[string]interface{}{
|
||||||
|
"coding": []map[string]interface{}{coding},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Medication != nil {
|
||||||
|
sys := req.Medication.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://sys-ids.kemkes.go.id/kfa"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": req.Medication.Code}
|
||||||
|
if req.Medication.Display != "" {
|
||||||
|
coding["display"] = req.Medication.Display
|
||||||
|
}
|
||||||
|
medConcept := map[string]interface{}{"coding": []map[string]interface{}{coding}}
|
||||||
|
if req.Medication.Text != "" {
|
||||||
|
medConcept["text"] = req.Medication.Text
|
||||||
|
}
|
||||||
|
payload.Set("medicationCodeableConcept", medConcept)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Subject != nil {
|
||||||
|
subject := map[string]interface{}{"reference": req.Subject.Reference}
|
||||||
|
if req.Subject.Display != "" {
|
||||||
|
subject["display"] = req.Subject.Display
|
||||||
|
}
|
||||||
|
payload.Set("subject", subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Context != nil {
|
||||||
|
context := map[string]interface{}{"reference": req.Context.Reference}
|
||||||
|
if req.Context.Display != "" {
|
||||||
|
context["display"] = req.Context.Display
|
||||||
|
}
|
||||||
|
payload.Set("context", context)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.EffectiveDateTime != nil && !req.EffectiveDateTime.IsZero() {
|
||||||
|
payload.Set("effectiveDateTime", req.EffectiveDateTime.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.DateAsserted != nil && !req.DateAsserted.IsZero() {
|
||||||
|
payload.Set("dateAsserted", req.DateAsserted.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.InformationSource != nil {
|
||||||
|
infoSource := map[string]interface{}{"reference": req.InformationSource.Reference}
|
||||||
|
if req.InformationSource.Display != "" {
|
||||||
|
infoSource["display"] = req.InformationSource.Display
|
||||||
|
}
|
||||||
|
payload.Set("informationSource", infoSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Dosage) > 0 {
|
||||||
|
var dosages []map[string]interface{}
|
||||||
|
for _, d := range req.Dosage {
|
||||||
|
dosageObj := map[string]interface{}{}
|
||||||
|
if d.Sequence > 0 {
|
||||||
|
dosageObj["sequence"] = d.Sequence
|
||||||
|
}
|
||||||
|
if d.Text != "" {
|
||||||
|
dosageObj["text"] = d.Text
|
||||||
|
}
|
||||||
|
if d.PatientInstruction != "" {
|
||||||
|
dosageObj["patientInstruction"] = d.PatientInstruction
|
||||||
|
}
|
||||||
|
if d.Route != nil {
|
||||||
|
sys := d.Route.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://www.whocc.no/atc"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": d.Route.Code}
|
||||||
|
if d.Route.Display != "" {
|
||||||
|
coding["display"] = d.Route.Display
|
||||||
|
}
|
||||||
|
dosageObj["route"] = map[string]interface{}{"coding": []map[string]interface{}{coding}}
|
||||||
|
}
|
||||||
|
if len(d.DoseAndRate) > 0 {
|
||||||
|
var doseAndRates []map[string]interface{}
|
||||||
|
for _, dar := range d.DoseAndRate {
|
||||||
|
if dar.DoseQuantity != nil {
|
||||||
|
dq := dar.DoseQuantity
|
||||||
|
sys := dq.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://terminology.hl7.org/CodeSystem/v3-orderableDrugForm"
|
||||||
|
}
|
||||||
|
doseQuantity := map[string]interface{}{"value": dq.Value, "unit": dq.Unit, "system": sys, "code": dq.Code}
|
||||||
|
doseAndRates = append(doseAndRates, map[string]interface{}{"doseQuantity": doseQuantity})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(doseAndRates) > 0 {
|
||||||
|
dosageObj["doseAndRate"] = doseAndRates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dosages = append(dosages, dosageObj)
|
||||||
|
}
|
||||||
|
payload.Set("dosage", dosages)
|
||||||
|
}
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
@@ -25,14 +26,21 @@ func NewRepository(client satusehat.SatuSehatClient) Repository {
|
|||||||
return &repository{client: client}
|
return &repository{client: client}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type hiddenCtx struct {
|
||||||
|
context.Context
|
||||||
|
}
|
||||||
|
|
||||||
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
resp, err := r.client.DoRequest(ctx, method, endpoint, req)
|
resp, err := r.client.DoRequest(hiddenCtx{ctx}, method, endpoint, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.InternalError().Message("Failed to execute request to SatuSehat").Cause(err).Build()
|
||||||
}
|
}
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
if err := json.Unmarshal(resp, &result); err != nil {
|
if err := json.Unmarshal(resp, &result); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
return nil, errors.InternalError().Message("Failed to parse SatuSehat response").Cause(err).Metadata("raw_response", string(resp)).Build()
|
||||||
|
}
|
||||||
|
if resourceType, ok := result["resourceType"].(string); ok && resourceType == "OperationOutcome" {
|
||||||
|
return nil, errors.ParseSatuSehatError(result)
|
||||||
}
|
}
|
||||||
var resourceID string
|
var resourceID string
|
||||||
if id, ok := result["id"].(string); ok {
|
if id, ok := result["id"].(string); ok {
|
||||||
|
|||||||
@@ -36,9 +36,18 @@ func (s *service) Update(ctx context.Context, id string, req MedicationStatement
|
|||||||
return s.repo.Update(ctx, id, payload)
|
return s.repo.Update(ctx, id, payload)
|
||||||
}
|
}
|
||||||
func (s *service) Patch(ctx context.Context, id string, req MedicationStatementPatchRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Patch(ctx context.Context, id string, req MedicationStatementPatchRequest) (*satusehat.FHIRResponse, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("MedicationStatement ID is required").Build()
|
||||||
|
}
|
||||||
|
if len(req) == 0 {
|
||||||
|
return nil, errors.NewValidationError().Message("Patch payload cannot be empty").Build()
|
||||||
|
}
|
||||||
return s.repo.Patch(ctx, id, req)
|
return s.repo.Patch(ctx, id, req)
|
||||||
}
|
}
|
||||||
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("MedicationStatement ID is required").Build()
|
||||||
|
}
|
||||||
return s.repo.GetByID(ctx, id)
|
return s.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
||||||
|
|||||||
@@ -1,20 +1,48 @@
|
|||||||
package observation
|
package observation
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service/internal/satusehat/common"
|
||||||
|
)
|
||||||
|
|
||||||
type ObservationRequest struct {
|
type ObservationRequest struct {
|
||||||
PatientID string `json:"patient_id" binding:"required"`
|
OrganizationID string `json:"organization_id,omitempty"`
|
||||||
PatientName string `json:"patient_name" binding:"required"`
|
ObservationID string `json:"observation_id,omitempty"`
|
||||||
EncounterID string `json:"encounter_id" binding:"required"`
|
Status string `json:"status" binding:"required,oneof=registered preliminary final amended corrected cancelled entered-in-error unknown"`
|
||||||
Status string `json:"status" binding:"required,oneof=registered preliminary final amended"` // registered, preliminary, final, amended
|
Category []*common.CodeableConceptDTO `json:"category" binding:"required,min=1"`
|
||||||
CategoryCode string `json:"category_code" binding:"required"` // vital-signs, laboratory
|
Code *common.CodeableConceptDTO `json:"code" binding:"required"`
|
||||||
CategoryDisplay string `json:"category_display" binding:"required"`
|
Subject *common.ReferenceDTO `json:"subject" binding:"required"` // Patient
|
||||||
Code string `json:"code" binding:"required"` // LOINC code (e.g., 8867-4 for Heart rate)
|
Encounter *common.ReferenceDTO `json:"encounter,omitempty"`
|
||||||
Display string `json:"display" binding:"required"` // Heart rate
|
EffectiveDateTime *time.Time `json:"effective_datetime,omitempty"`
|
||||||
Value float64 `json:"value" binding:"required"`
|
Issued *time.Time `json:"issued,omitempty"`
|
||||||
Unit string `json:"unit" binding:"required"`
|
Performer []common.ReferenceDTO `json:"performer,omitempty"`
|
||||||
UnitCode string `json:"unit_code" binding:"required"` // UCUM code (e.g., /min)
|
BasedOn []common.ReferenceDTO `json:"based_on,omitempty"`
|
||||||
EffectiveDateTime time.Time `json:"effective_date_time" binding:"required"`
|
PartOf []common.ReferenceDTO `json:"part_of,omitempty"`
|
||||||
|
DerivedFrom []common.ReferenceDTO `json:"derived_from,omitempty"`
|
||||||
|
Specimen *common.ReferenceDTO `json:"specimen,omitempty"`
|
||||||
|
BodySite *common.CodeableConceptDTO `json:"body_site,omitempty"`
|
||||||
|
ValueQuantity *common.QuantityDTO `json:"value_quantity,omitempty"`
|
||||||
|
ValueCodeableConcept *common.CodeableConceptDTO `json:"value_codeable_concept,omitempty"`
|
||||||
|
ValueString string `json:"value_string,omitempty"`
|
||||||
|
ValueBoolean *bool `json:"value_boolean,omitempty"`
|
||||||
|
Interpretation []*common.CodeableConceptDTO `json:"interpretation,omitempty"`
|
||||||
|
ReferenceRange []ReferenceRangeDTO `json:"reference_range,omitempty"`
|
||||||
|
Components []ComponentDTO `json:"components,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReferenceRangeDTO struct {
|
||||||
|
Low *common.QuantityDTO `json:"low,omitempty"`
|
||||||
|
High *common.QuantityDTO `json:"high,omitempty"`
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComponentDTO struct {
|
||||||
|
Code *common.CodeableConceptDTO `json:"code,omitempty"`
|
||||||
|
ValueQuantity *common.QuantityDTO `json:"value_quantity,omitempty"`
|
||||||
|
ValueCodeableConcept *common.CodeableConceptDTO `json:"value_codeable_concept,omitempty"`
|
||||||
|
ValueString string `json:"value_string,omitempty"`
|
||||||
|
ValueBoolean *bool `json:"value_boolean,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ObservationPatchRequest []map[string]interface{}
|
type ObservationPatchRequest []map[string]interface{}
|
||||||
@@ -1,48 +1,258 @@
|
|||||||
package observation
|
package observation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MapRequestToFHIR(req ObservationRequest) satusehat.FHIRPayload {
|
func MapRequestToFHIR(req ObservationRequest) satusehat.FHIRPayload {
|
||||||
payload := satusehat.NewFHIRPayload("Observation").
|
payload := satusehat.NewFHIRPayload("Observation")
|
||||||
Set("status", req.Status).
|
|
||||||
Set("category", []map[string]interface{}{
|
orgID := os.Getenv("SATUSEHAT_ORG_ID")
|
||||||
|
if orgID == "" {
|
||||||
|
orgID = req.OrganizationID
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ObservationID != "" {
|
||||||
|
payload.Set("identifier", []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"coding": []map[string]interface{}{
|
"system": "http://sys-ids.kemkes.go.id/observation/" + orgID,
|
||||||
{
|
"value": req.ObservationID,
|
||||||
"system": "http://terminology.hl7.org/CodeSystem/observation-category",
|
|
||||||
"code": req.CategoryCode,
|
|
||||||
"display": req.CategoryDisplay,
|
|
||||||
},
|
},
|
||||||
},
|
|
||||||
},
|
|
||||||
}).
|
|
||||||
Set("code", map[string]interface{}{
|
|
||||||
"coding": []map[string]interface{}{
|
|
||||||
{
|
|
||||||
"system": "http://loinc.org",
|
|
||||||
"code": req.Code,
|
|
||||||
"display": req.Display,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).
|
|
||||||
Set("subject", map[string]interface{}{
|
|
||||||
"reference": "Patient/" + req.PatientID,
|
|
||||||
"display": req.PatientName,
|
|
||||||
}).
|
|
||||||
Set("encounter", map[string]interface{}{
|
|
||||||
"reference": "Encounter/" + req.EncounterID,
|
|
||||||
}).
|
|
||||||
Set("effectiveDateTime", req.EffectiveDateTime.Format(time.RFC3339)).
|
|
||||||
Set("valueQuantity", map[string]interface{}{
|
|
||||||
"value": req.Value,
|
|
||||||
"unit": req.Unit,
|
|
||||||
"system": "http://unitsofmeasure.org",
|
|
||||||
"code": req.UnitCode,
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
status := req.Status
|
||||||
|
if status == "" {
|
||||||
|
status = "final"
|
||||||
|
}
|
||||||
|
payload.Set("status", status)
|
||||||
|
|
||||||
|
if len(req.Category) > 0 {
|
||||||
|
var categories []map[string]interface{}
|
||||||
|
for _, c := range req.Category {
|
||||||
|
if c != nil {
|
||||||
|
sys := c.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://terminology.hl7.org/CodeSystem/observation-category"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": c.Code}
|
||||||
|
if c.Display != "" {
|
||||||
|
coding["display"] = c.Display
|
||||||
|
}
|
||||||
|
categories = append(categories, map[string]interface{}{"coding": []map[string]interface{}{coding}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payload.Set("category", categories)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Code != nil {
|
||||||
|
sys := req.Code.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://loinc.org"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": req.Code.Code}
|
||||||
|
if req.Code.Display != "" {
|
||||||
|
coding["display"] = req.Code.Display
|
||||||
|
}
|
||||||
|
codeConcept := map[string]interface{}{"coding": []map[string]interface{}{coding}}
|
||||||
|
if req.Code.Text != "" {
|
||||||
|
codeConcept["text"] = req.Code.Text
|
||||||
|
}
|
||||||
|
payload.Set("code", codeConcept)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Subject != nil {
|
||||||
|
subj := map[string]interface{}{"reference": req.Subject.Reference}
|
||||||
|
if req.Subject.Display != "" {
|
||||||
|
subj["display"] = req.Subject.Display
|
||||||
|
}
|
||||||
|
payload.Set("subject", subj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Encounter != nil {
|
||||||
|
enc := map[string]interface{}{"reference": req.Encounter.Reference}
|
||||||
|
if req.Encounter.Display != "" {
|
||||||
|
enc["display"] = req.Encounter.Display
|
||||||
|
}
|
||||||
|
payload.Set("encounter", enc)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.EffectiveDateTime != nil && !req.EffectiveDateTime.IsZero() {
|
||||||
|
payload.Set("effectiveDateTime", req.EffectiveDateTime.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
if req.Issued != nil && !req.Issued.IsZero() {
|
||||||
|
payload.Set("issued", req.Issued.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Performer) > 0 {
|
||||||
|
var performers []map[string]interface{}
|
||||||
|
for _, p := range req.Performer {
|
||||||
|
perf := map[string]interface{}{"reference": p.Reference}
|
||||||
|
if p.Display != "" {
|
||||||
|
perf["display"] = p.Display
|
||||||
|
}
|
||||||
|
performers = append(performers, perf)
|
||||||
|
}
|
||||||
|
payload.Set("performer", performers)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.BasedOn) > 0 {
|
||||||
|
var refs []map[string]interface{}
|
||||||
|
for _, r := range req.BasedOn {
|
||||||
|
refs = append(refs, map[string]interface{}{"reference": r.Reference})
|
||||||
|
}
|
||||||
|
payload.Set("basedOn", refs)
|
||||||
|
}
|
||||||
|
if len(req.PartOf) > 0 {
|
||||||
|
var refs []map[string]interface{}
|
||||||
|
for _, r := range req.PartOf {
|
||||||
|
refs = append(refs, map[string]interface{}{"reference": r.Reference})
|
||||||
|
}
|
||||||
|
payload.Set("partOf", refs)
|
||||||
|
}
|
||||||
|
if len(req.DerivedFrom) > 0 {
|
||||||
|
var refs []map[string]interface{}
|
||||||
|
for _, r := range req.DerivedFrom {
|
||||||
|
refs = append(refs, map[string]interface{}{"reference": r.Reference})
|
||||||
|
}
|
||||||
|
payload.Set("derivedFrom", refs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Specimen != nil {
|
||||||
|
spec := map[string]interface{}{"reference": req.Specimen.Reference}
|
||||||
|
if req.Specimen.Display != "" {
|
||||||
|
spec["display"] = req.Specimen.Display
|
||||||
|
}
|
||||||
|
payload.Set("specimen", spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.BodySite != nil {
|
||||||
|
sys := req.BodySite.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://snomed.info/sct"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": req.BodySite.Code}
|
||||||
|
if req.BodySite.Display != "" {
|
||||||
|
coding["display"] = req.BodySite.Display
|
||||||
|
}
|
||||||
|
payload.Set("bodySite", map[string]interface{}{"coding": []map[string]interface{}{coding}})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ValueQuantity != nil {
|
||||||
|
sys := req.ValueQuantity.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://unitsofmeasure.org"
|
||||||
|
}
|
||||||
|
payload.Set("valueQuantity", map[string]interface{}{
|
||||||
|
"value": req.ValueQuantity.Value,
|
||||||
|
"unit": req.ValueQuantity.Unit,
|
||||||
|
"system": sys,
|
||||||
|
"code": req.ValueQuantity.Code,
|
||||||
|
})
|
||||||
|
} else if req.ValueCodeableConcept != nil {
|
||||||
|
sys := req.ValueCodeableConcept.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://snomed.info/sct"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": req.ValueCodeableConcept.Code}
|
||||||
|
if req.ValueCodeableConcept.Display != "" {
|
||||||
|
coding["display"] = req.ValueCodeableConcept.Display
|
||||||
|
}
|
||||||
|
payload.Set("valueCodeableConcept", map[string]interface{}{"coding": []map[string]interface{}{coding}})
|
||||||
|
} else if req.ValueString != "" {
|
||||||
|
payload.Set("valueString", req.ValueString)
|
||||||
|
} else if req.ValueBoolean != nil {
|
||||||
|
payload.Set("valueBoolean", *req.ValueBoolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Interpretation) > 0 {
|
||||||
|
var interps []map[string]interface{}
|
||||||
|
for _, interp := range req.Interpretation {
|
||||||
|
if interp != nil {
|
||||||
|
sys := interp.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": interp.Code}
|
||||||
|
if interp.Display != "" {
|
||||||
|
coding["display"] = interp.Display
|
||||||
|
}
|
||||||
|
interps = append(interps, map[string]interface{}{"coding": []map[string]interface{}{coding}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payload.Set("interpretation", interps)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.ReferenceRange) > 0 {
|
||||||
|
var ranges []map[string]interface{}
|
||||||
|
for _, r := range req.ReferenceRange {
|
||||||
|
rng := map[string]interface{}{}
|
||||||
|
if r.Low != nil {
|
||||||
|
sys := r.Low.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://unitsofmeasure.org"
|
||||||
|
}
|
||||||
|
rng["low"] = map[string]interface{}{"value": r.Low.Value, "unit": r.Low.Unit, "system": sys, "code": r.Low.Code}
|
||||||
|
}
|
||||||
|
if r.High != nil {
|
||||||
|
sys := r.High.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://unitsofmeasure.org"
|
||||||
|
}
|
||||||
|
rng["high"] = map[string]interface{}{"value": r.High.Value, "unit": r.High.Unit, "system": sys, "code": r.High.Code}
|
||||||
|
}
|
||||||
|
if r.Text != "" {
|
||||||
|
rng["text"] = r.Text
|
||||||
|
}
|
||||||
|
ranges = append(ranges, rng)
|
||||||
|
}
|
||||||
|
payload.Set("referenceRange", ranges)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Components) > 0 {
|
||||||
|
var components []map[string]interface{}
|
||||||
|
for _, comp := range req.Components {
|
||||||
|
c := make(map[string]interface{})
|
||||||
|
if comp.Code != nil {
|
||||||
|
sys := comp.Code.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://loinc.org"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": comp.Code.Code}
|
||||||
|
if comp.Code.Display != "" {
|
||||||
|
coding["display"] = comp.Code.Display
|
||||||
|
}
|
||||||
|
c["code"] = map[string]interface{}{"coding": []map[string]interface{}{coding}}
|
||||||
|
}
|
||||||
|
|
||||||
|
if comp.ValueQuantity != nil {
|
||||||
|
sysVQ := comp.ValueQuantity.System
|
||||||
|
if sysVQ == "" {
|
||||||
|
sysVQ = "http://unitsofmeasure.org"
|
||||||
|
}
|
||||||
|
c["valueQuantity"] = map[string]interface{}{"value": comp.ValueQuantity.Value, "unit": comp.ValueQuantity.Unit, "system": sysVQ, "code": comp.ValueQuantity.Code}
|
||||||
|
} else if comp.ValueCodeableConcept != nil {
|
||||||
|
sysVCC := comp.ValueCodeableConcept.System
|
||||||
|
if sysVCC == "" {
|
||||||
|
sysVCC = "http://snomed.info/sct"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sysVCC, "code": comp.ValueCodeableConcept.Code}
|
||||||
|
if comp.ValueCodeableConcept.Display != "" {
|
||||||
|
coding["display"] = comp.ValueCodeableConcept.Display
|
||||||
|
}
|
||||||
|
c["valueCodeableConcept"] = map[string]interface{}{"coding": []map[string]interface{}{coding}}
|
||||||
|
} else if comp.ValueString != "" {
|
||||||
|
c["valueString"] = comp.ValueString
|
||||||
|
} else if comp.ValueBoolean != nil {
|
||||||
|
c["valueBoolean"] = *comp.ValueBoolean
|
||||||
|
}
|
||||||
|
components = append(components, c)
|
||||||
|
}
|
||||||
|
payload.Set("component", components)
|
||||||
|
}
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
@@ -24,15 +25,23 @@ func NewRepository(client satusehat.SatuSehatClient) Repository {
|
|||||||
return &repository{client: client}
|
return &repository{client: client}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type hiddenCtx struct {
|
||||||
|
context.Context
|
||||||
|
}
|
||||||
|
|
||||||
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
resp, err := r.client.DoRequest(ctx, method, endpoint, req)
|
resp, err := r.client.DoRequest(hiddenCtx{ctx}, method, endpoint, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.InternalError().Message("Failed to execute request to SatuSehat").Cause(err).Build()
|
||||||
}
|
}
|
||||||
|
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
if err := json.Unmarshal(resp, &result); err != nil {
|
if err := json.Unmarshal(resp, &result); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
return nil, errors.InternalError().Message("Failed to parse SatuSehat response").Cause(err).Metadata("raw_response", string(resp)).Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
if resourceType, ok := result["resourceType"].(string); ok && resourceType == "OperationOutcome" {
|
||||||
|
return nil, errors.ParseSatuSehatError(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
var resourceID string
|
var resourceID string
|
||||||
@@ -40,11 +49,7 @@ func (r *repository) executeRequest(ctx context.Context, method, endpoint string
|
|||||||
resourceID = id
|
resourceID = id
|
||||||
}
|
}
|
||||||
|
|
||||||
return &satusehat.FHIRResponse{
|
return &satusehat.FHIRResponse{ID: resourceID, FullResponse: result, RawResponse: resp}, nil
|
||||||
ID: resourceID,
|
|
||||||
FullResponse: result,
|
|
||||||
RawResponse: resp,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *repository) Create(ctx context.Context, payload interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) Create(ctx context.Context, payload interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
|
|||||||
@@ -24,19 +24,16 @@ func NewService(repo Repository) Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Create(ctx context.Context, req ObservationRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Create(ctx context.Context, req ObservationRequest) (*satusehat.FHIRResponse, error) {
|
||||||
fhirPayload := MapRequestToFHIR(req)
|
return s.repo.Create(ctx, MapRequestToFHIR(req))
|
||||||
return s.repo.Create(ctx, fhirPayload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Update(ctx context.Context, id string, req ObservationRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Update(ctx context.Context, id string, req ObservationRequest) (*satusehat.FHIRResponse, error) {
|
||||||
if id == "" {
|
if id == "" {
|
||||||
return nil, errors.NewValidationError().Message("Observation ID is required").Build()
|
return nil, errors.NewValidationError().Message("Observation ID is required").Build()
|
||||||
}
|
}
|
||||||
|
payload := MapRequestToFHIR(req)
|
||||||
fhirPayload := MapRequestToFHIR(req)
|
payload.Set("id", id)
|
||||||
fhirPayload.Set("id", id)
|
return s.repo.Update(ctx, id, payload)
|
||||||
|
|
||||||
return s.repo.Update(ctx, id, fhirPayload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Patch(ctx context.Context, id string, req ObservationPatchRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Patch(ctx context.Context, id string, req ObservationPatchRequest) (*satusehat.FHIRResponse, error) {
|
||||||
@@ -57,6 +54,5 @@ func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRRespon
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
||||||
// Tambahkan validasi untuk query params jika diperlukan
|
|
||||||
return s.repo.Search(ctx, queryParams)
|
return s.repo.Search(ctx, queryParams)
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,26 @@
|
|||||||
package procedure
|
package procedure
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service/internal/satusehat/common"
|
||||||
|
)
|
||||||
|
|
||||||
type ProcedureRequest struct {
|
type ProcedureRequest struct {
|
||||||
PatientID string `json:"patient_id" binding:"required"`
|
OrganizationID string `json:"organization_id,omitempty"`
|
||||||
PatientName string `json:"patient_name" binding:"required"`
|
ProcedureID string `json:"procedure_id,omitempty"`
|
||||||
EncounterID string `json:"encounter_id" binding:"required"`
|
Status string `json:"status" binding:"required,oneof=preparation in-progress not-done on-hold stopped completed entered-in-error unknown"`
|
||||||
Status string `json:"status" binding:"required,oneof=preparation in-progress not-done on-hold stopped completed entered-in-error unknown"` // preparation, in-progress, not-done, on-hold, stopped, completed, entered-in-error, unknown
|
Category []*common.CodeableConceptDTO `json:"category,omitempty"`
|
||||||
CategoryCode string `json:"category_code" binding:"required"`
|
Code *common.CodeableConceptDTO `json:"code" binding:"required"`
|
||||||
CategoryDisplay string `json:"category_display" binding:"required"`
|
Subject *common.ReferenceDTO `json:"subject" binding:"required"`
|
||||||
Code string `json:"code" binding:"required"` // SNOMED-CT or ICD9CM code
|
Encounter *common.ReferenceDTO `json:"encounter,omitempty"`
|
||||||
Display string `json:"display" binding:"required"` // Procedure name
|
PerformedDateTime *time.Time `json:"performed_date_time,omitempty"`
|
||||||
PractitionerID string `json:"practitioner_id" binding:"required"`
|
PerformedStart *time.Time `json:"performed_start,omitempty"`
|
||||||
PractitionerName string `json:"practitioner_name" binding:"required"`
|
PerformedEnd *time.Time `json:"performed_end,omitempty"`
|
||||||
PerformedDateTime time.Time `json:"performed_date_time" binding:"required"`
|
Performer []common.ReferenceDTO `json:"performer,omitempty"`
|
||||||
|
ReasonCode []*common.CodeableConceptDTO `json:"reason_code,omitempty"`
|
||||||
|
BodySite []*common.CodeableConceptDTO `json:"body_site,omitempty"`
|
||||||
|
Note []string `json:"note,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProcedurePatchRequest []map[string]interface{}
|
type ProcedurePatchRequest []map[string]interface{}
|
||||||
@@ -1,49 +1,146 @@
|
|||||||
package procedure
|
package procedure
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MapRequestToFHIR(req ProcedureRequest) satusehat.FHIRPayload {
|
func MapRequestToFHIR(req ProcedureRequest) satusehat.FHIRPayload {
|
||||||
payload := satusehat.NewFHIRPayload("Procedure").
|
payload := satusehat.NewFHIRPayload("Procedure")
|
||||||
Set("status", req.Status).
|
|
||||||
Set("category", map[string]interface{}{
|
|
||||||
"coding": []map[string]interface{}{
|
|
||||||
{
|
|
||||||
"system": "http://snomed.info/sct",
|
|
||||||
"code": req.CategoryCode,
|
|
||||||
"display": req.CategoryDisplay,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).
|
|
||||||
Set("code", map[string]interface{}{
|
|
||||||
"coding": []map[string]interface{}{
|
|
||||||
{
|
|
||||||
"system": "http://snomed.info/sct",
|
|
||||||
"code": req.Code,
|
|
||||||
"display": req.Display,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).
|
|
||||||
Set("subject", map[string]interface{}{
|
|
||||||
"reference": "Patient/" + req.PatientID,
|
|
||||||
"display": req.PatientName,
|
|
||||||
}).
|
|
||||||
Set("encounter", map[string]interface{}{
|
|
||||||
"reference": "Encounter/" + req.EncounterID,
|
|
||||||
}).
|
|
||||||
Set("performedDateTime", req.PerformedDateTime.Format(time.RFC3339))
|
|
||||||
|
|
||||||
if req.PractitionerID != "" {
|
orgID := os.Getenv("SATUSEHAT_ORG_ID")
|
||||||
payload.Append("performer", map[string]interface{}{
|
if orgID == "" {
|
||||||
"actor": map[string]interface{}{
|
orgID = req.OrganizationID
|
||||||
"reference": "Practitioner/" + req.PractitionerID,
|
}
|
||||||
"display": req.PractitionerName,
|
|
||||||
|
if orgID != "" && req.ProcedureID != "" {
|
||||||
|
payload.Set("identifier", []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"system": "http://sys-ids.kemkes.go.id/procedure/" + orgID,
|
||||||
|
"value": req.ProcedureID,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.Status != "" {
|
||||||
|
payload.Set("status", req.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Category) > 0 && req.Category[0] != nil {
|
||||||
|
sys := req.Category[0].System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://snomed.info/sct"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": req.Category[0].Code}
|
||||||
|
if req.Category[0].Display != "" {
|
||||||
|
coding["display"] = req.Category[0].Display
|
||||||
|
}
|
||||||
|
catObj := map[string]interface{}{"coding": []map[string]interface{}{coding}}
|
||||||
|
if req.Category[0].Text != "" {
|
||||||
|
catObj["text"] = req.Category[0].Text
|
||||||
|
}
|
||||||
|
payload.Set("category", catObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Code != nil {
|
||||||
|
sys := req.Code.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://snomed.info/sct"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": req.Code.Code}
|
||||||
|
if req.Code.Display != "" {
|
||||||
|
coding["display"] = req.Code.Display
|
||||||
|
}
|
||||||
|
codeObj := map[string]interface{}{"coding": []map[string]interface{}{coding}}
|
||||||
|
if req.Code.Text != "" {
|
||||||
|
codeObj["text"] = req.Code.Text
|
||||||
|
}
|
||||||
|
payload.Set("code", codeObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Subject != nil {
|
||||||
|
subj := map[string]interface{}{"reference": req.Subject.Reference}
|
||||||
|
if req.Subject.Display != "" {
|
||||||
|
subj["display"] = req.Subject.Display
|
||||||
|
}
|
||||||
|
payload.Set("subject", subj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Encounter != nil {
|
||||||
|
enc := map[string]interface{}{"reference": req.Encounter.Reference}
|
||||||
|
if req.Encounter.Display != "" {
|
||||||
|
enc["display"] = req.Encounter.Display
|
||||||
|
}
|
||||||
|
payload.Set("encounter", enc)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.PerformedStart != nil && !req.PerformedStart.IsZero() {
|
||||||
|
period := map[string]interface{}{"start": req.PerformedStart.Format(time.RFC3339)}
|
||||||
|
if req.PerformedEnd != nil && !req.PerformedEnd.IsZero() {
|
||||||
|
period["end"] = req.PerformedEnd.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
payload.Set("performedPeriod", period)
|
||||||
|
} else if req.PerformedDateTime != nil && !req.PerformedDateTime.IsZero() {
|
||||||
|
payload.Set("performedDateTime", req.PerformedDateTime.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Performer) > 0 {
|
||||||
|
var performers []map[string]interface{}
|
||||||
|
for _, p := range req.Performer {
|
||||||
|
actor := map[string]interface{}{"reference": p.Reference}
|
||||||
|
if p.Display != "" {
|
||||||
|
actor["display"] = p.Display
|
||||||
|
}
|
||||||
|
performers = append(performers, map[string]interface{}{"actor": actor})
|
||||||
|
}
|
||||||
|
payload.Set("performer", performers)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.ReasonCode) > 0 {
|
||||||
|
var reasons []map[string]interface{}
|
||||||
|
for _, r := range req.ReasonCode {
|
||||||
|
if r != nil {
|
||||||
|
sys := r.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://hl7.org/fhir/sid/icd-10"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": r.Code}
|
||||||
|
if r.Display != "" {
|
||||||
|
coding["display"] = r.Display
|
||||||
|
}
|
||||||
|
reasons = append(reasons, map[string]interface{}{"coding": []map[string]interface{}{coding}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payload.Set("reasonCode", reasons)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.BodySite) > 0 {
|
||||||
|
var bodySites []map[string]interface{}
|
||||||
|
for _, b := range req.BodySite {
|
||||||
|
if b != nil {
|
||||||
|
sys := b.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://snomed.info/sct"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": b.Code}
|
||||||
|
if b.Display != "" {
|
||||||
|
coding["display"] = b.Display
|
||||||
|
}
|
||||||
|
bodySites = append(bodySites, map[string]interface{}{"coding": []map[string]interface{}{coding}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payload.Set("bodySite", bodySites)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Note) > 0 {
|
||||||
|
var notes []map[string]interface{}
|
||||||
|
for _, text := range req.Note {
|
||||||
|
notes = append(notes, map[string]interface{}{"text": text})
|
||||||
|
}
|
||||||
|
payload.Set("note", notes)
|
||||||
|
}
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
@@ -24,15 +25,23 @@ func NewRepository(client satusehat.SatuSehatClient) Repository {
|
|||||||
return &repository{client: client}
|
return &repository{client: client}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type hiddenCtx struct {
|
||||||
|
context.Context
|
||||||
|
}
|
||||||
|
|
||||||
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
resp, err := r.client.DoRequest(ctx, method, endpoint, req)
|
resp, err := r.client.DoRequest(hiddenCtx{ctx}, method, endpoint, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.InternalError().Message("Failed to execute request to SatuSehat").Cause(err).Build()
|
||||||
}
|
}
|
||||||
|
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
if err := json.Unmarshal(resp, &result); err != nil {
|
if err := json.Unmarshal(resp, &result); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
return nil, errors.InternalError().Message("Failed to parse SatuSehat response").Cause(err).Metadata("raw_response", string(resp)).Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
if resourceType, ok := result["resourceType"].(string); ok && resourceType == "OperationOutcome" {
|
||||||
|
return nil, errors.ParseSatuSehatError(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
var resourceID string
|
var resourceID string
|
||||||
@@ -40,11 +49,7 @@ func (r *repository) executeRequest(ctx context.Context, method, endpoint string
|
|||||||
resourceID = id
|
resourceID = id
|
||||||
}
|
}
|
||||||
|
|
||||||
return &satusehat.FHIRResponse{
|
return &satusehat.FHIRResponse{ID: resourceID, FullResponse: result, RawResponse: resp}, nil
|
||||||
ID: resourceID,
|
|
||||||
FullResponse: result,
|
|
||||||
RawResponse: resp,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *repository) Create(ctx context.Context, payload interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) Create(ctx context.Context, payload interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
@@ -52,21 +57,17 @@ func (r *repository) Create(ctx context.Context, payload interface{}) (*satuseha
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *repository) Update(ctx context.Context, id string, payload interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) Update(ctx context.Context, id string, payload interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
endpoint := fmt.Sprintf("/Procedure/%s", id)
|
return r.executeRequest(ctx, "PUT", fmt.Sprintf("/Procedure/%s", id), payload)
|
||||||
return r.executeRequest(ctx, "PUT", endpoint, payload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *repository) Patch(ctx context.Context, id string, payload ProcedurePatchRequest) (*satusehat.FHIRResponse, error) {
|
func (r *repository) Patch(ctx context.Context, id string, payload ProcedurePatchRequest) (*satusehat.FHIRResponse, error) {
|
||||||
endpoint := fmt.Sprintf("/Procedure/%s", id)
|
return r.executeRequest(ctx, "PATCH", fmt.Sprintf("/Procedure/%s", id), payload)
|
||||||
return r.executeRequest(ctx, "PATCH", endpoint, payload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *repository) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
func (r *repository) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
||||||
endpoint := fmt.Sprintf("/Procedure/%s", id)
|
return r.executeRequest(ctx, "GET", fmt.Sprintf("/Procedure/%s", id), nil)
|
||||||
return r.executeRequest(ctx, "GET", endpoint, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *repository) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
func (r *repository) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
||||||
endpoint := fmt.Sprintf("/Procedure?%s", queryParams.Encode())
|
return r.executeRequest(ctx, "GET", fmt.Sprintf("/Procedure?%s", queryParams.Encode()), nil)
|
||||||
return r.executeRequest(ctx, "GET", endpoint, nil)
|
|
||||||
}
|
}
|
||||||
@@ -24,19 +24,16 @@ func NewService(repo Repository) Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Create(ctx context.Context, req ProcedureRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Create(ctx context.Context, req ProcedureRequest) (*satusehat.FHIRResponse, error) {
|
||||||
fhirPayload := MapRequestToFHIR(req)
|
return s.repo.Create(ctx, MapRequestToFHIR(req))
|
||||||
return s.repo.Create(ctx, fhirPayload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Update(ctx context.Context, id string, req ProcedureRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Update(ctx context.Context, id string, req ProcedureRequest) (*satusehat.FHIRResponse, error) {
|
||||||
if id == "" {
|
if id == "" {
|
||||||
return nil, errors.NewValidationError().Message("Procedure ID is required").Build()
|
return nil, errors.NewValidationError().Message("Procedure ID is required").Build()
|
||||||
}
|
}
|
||||||
|
payload := MapRequestToFHIR(req)
|
||||||
fhirPayload := MapRequestToFHIR(req)
|
payload.Set("id", id)
|
||||||
fhirPayload.Set("id", id)
|
return s.repo.Update(ctx, id, payload)
|
||||||
|
|
||||||
return s.repo.Update(ctx, id, fhirPayload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Patch(ctx context.Context, id string, req ProcedurePatchRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Patch(ctx context.Context, id string, req ProcedurePatchRequest) (*satusehat.FHIRResponse, error) {
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
package questionnaireresponse
|
package questionnaireresponse
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service/internal/satusehat/common"
|
||||||
|
)
|
||||||
|
|
||||||
type QuestionnaireResponseRequest struct {
|
type QuestionnaireResponseRequest struct {
|
||||||
QuestionnaireURL string `json:"questionnaire_url" binding:"required"` // e.g. "https://fhir.kemkes.go.id/Questionnaire/Q0007"
|
OrganizationID string `json:"organization_id,omitempty"`
|
||||||
|
QuestionnaireResponseID string `json:"questionnaire_response_id,omitempty"`
|
||||||
|
QuestionnaireURL string `json:"questionnaire_url" binding:"required"` // e.g., "https://fhir.kemkes.go.id/Questionnaire/Q0007"
|
||||||
Status string `json:"status" binding:"required,oneof=in-progress completed amended entered-in-error stopped"`
|
Status string `json:"status" binding:"required,oneof=in-progress completed amended entered-in-error stopped"`
|
||||||
PatientID string `json:"patient_id" binding:"required"`
|
Subject *common.ReferenceDTO `json:"subject" binding:"required"` // Patient
|
||||||
PatientName string `json:"patient_name" binding:"required"`
|
Encounter *common.ReferenceDTO `json:"encounter,omitempty"`
|
||||||
EncounterID string `json:"encounter_id" binding:"required"`
|
Authored *time.Time `json:"authored" binding:"required"`
|
||||||
AuthoredDate time.Time `json:"authored_date" binding:"required"`
|
Author *common.ReferenceDTO `json:"author,omitempty"`
|
||||||
AuthorID string `json:"author_id" binding:"required"`
|
Source *common.ReferenceDTO `json:"source,omitempty"`
|
||||||
AuthorName string `json:"author_name" binding:"required"`
|
Items []map[string]interface{} `json:"item" binding:"required,min=1"` // Raw FHIR formatted items
|
||||||
Items []map[string]interface{} `json:"items" binding:"required"` // Raw FHIR formatted items to handle high nesting flexibility
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type QuestionnaireResponsePatchRequest []map[string]interface{}
|
type QuestionnaireResponsePatchRequest []map[string]interface{}
|
||||||
@@ -1,28 +1,74 @@
|
|||||||
package questionnaireresponse
|
package questionnaireresponse
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MapRequestToFHIR(req QuestionnaireResponseRequest) satusehat.FHIRPayload {
|
func MapRequestToFHIR(req QuestionnaireResponseRequest) satusehat.FHIRPayload {
|
||||||
payload := satusehat.NewFHIRPayload("QuestionnaireResponse").
|
payload := satusehat.NewFHIRPayload("QuestionnaireResponse")
|
||||||
Set("questionnaire", req.QuestionnaireURL).
|
|
||||||
Set("status", req.Status).
|
orgID := os.Getenv("SATUSEHAT_ORG_ID")
|
||||||
Set("subject", map[string]interface{}{
|
if orgID == "" {
|
||||||
"reference": "Patient/" + req.PatientID,
|
orgID = req.OrganizationID
|
||||||
"display": req.PatientName,
|
}
|
||||||
}).
|
|
||||||
Set("encounter", map[string]interface{}{
|
if orgID != "" && req.QuestionnaireResponseID != "" {
|
||||||
"reference": "Encounter/" + req.EncounterID,
|
// Catatan: Identifier QuestionnaireResponse adalah single object, bukan array
|
||||||
}).
|
payload.Set("identifier", map[string]interface{}{
|
||||||
Set("authored", req.AuthoredDate.Format(time.RFC3339)).
|
"system": "http://sys-ids.kemkes.go.id/QuestionnaireResponse/" + orgID,
|
||||||
Set("author", map[string]interface{}{
|
"value": req.QuestionnaireResponseID,
|
||||||
"reference": "Practitioner/" + req.AuthorID,
|
})
|
||||||
"display": req.AuthorName,
|
}
|
||||||
}).
|
|
||||||
Set("item", req.Items)
|
if req.QuestionnaireURL != "" {
|
||||||
|
payload.Set("questionnaire", req.QuestionnaireURL)
|
||||||
|
}
|
||||||
|
if req.Status != "" {
|
||||||
|
payload.Set("status", req.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Subject != nil {
|
||||||
|
subject := map[string]interface{}{"reference": req.Subject.Reference}
|
||||||
|
if req.Subject.Display != "" {
|
||||||
|
subject["display"] = req.Subject.Display
|
||||||
|
}
|
||||||
|
payload.Set("subject", subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Encounter != nil {
|
||||||
|
encounter := map[string]interface{}{"reference": req.Encounter.Reference}
|
||||||
|
if req.Encounter.Display != "" {
|
||||||
|
encounter["display"] = req.Encounter.Display
|
||||||
|
}
|
||||||
|
payload.Set("encounter", encounter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Authored != nil && !req.Authored.IsZero() {
|
||||||
|
payload.Set("authored", req.Authored.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Author != nil {
|
||||||
|
author := map[string]interface{}{"reference": req.Author.Reference}
|
||||||
|
if req.Author.Display != "" {
|
||||||
|
author["display"] = req.Author.Display
|
||||||
|
}
|
||||||
|
payload.Set("author", author)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Source != nil {
|
||||||
|
source := map[string]interface{}{"reference": req.Source.Reference}
|
||||||
|
if req.Source.Display != "" {
|
||||||
|
source["display"] = req.Source.Display
|
||||||
|
}
|
||||||
|
payload.Set("source", source)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Items) > 0 {
|
||||||
|
payload.Set("item", req.Items)
|
||||||
|
}
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
@@ -25,14 +26,21 @@ func NewRepository(client satusehat.SatuSehatClient) Repository {
|
|||||||
return &repository{client: client}
|
return &repository{client: client}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type hiddenCtx struct {
|
||||||
|
context.Context
|
||||||
|
}
|
||||||
|
|
||||||
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
resp, err := r.client.DoRequest(ctx, method, endpoint, req)
|
resp, err := r.client.DoRequest(hiddenCtx{ctx}, method, endpoint, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.InternalError().Message("Failed to execute request to SatuSehat").Cause(err).Build()
|
||||||
}
|
}
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
if err := json.Unmarshal(resp, &result); err != nil {
|
if err := json.Unmarshal(resp, &result); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
return nil, errors.InternalError().Message("Failed to parse SatuSehat response").Cause(err).Metadata("raw_response", string(resp)).Build()
|
||||||
|
}
|
||||||
|
if resourceType, ok := result["resourceType"].(string); ok && resourceType == "OperationOutcome" {
|
||||||
|
return nil, errors.ParseSatuSehatError(result)
|
||||||
}
|
}
|
||||||
var resourceID string
|
var resourceID string
|
||||||
if id, ok := result["id"].(string); ok {
|
if id, ok := result["id"].(string); ok {
|
||||||
|
|||||||
@@ -36,9 +36,18 @@ func (s *service) Update(ctx context.Context, id string, req QuestionnaireRespon
|
|||||||
return s.repo.Update(ctx, id, payload)
|
return s.repo.Update(ctx, id, payload)
|
||||||
}
|
}
|
||||||
func (s *service) Patch(ctx context.Context, id string, req QuestionnaireResponsePatchRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Patch(ctx context.Context, id string, req QuestionnaireResponsePatchRequest) (*satusehat.FHIRResponse, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("QuestionnaireResponse ID is required").Build()
|
||||||
|
}
|
||||||
|
if len(req) == 0 {
|
||||||
|
return nil, errors.NewValidationError().Message("Patch payload cannot be empty").Build()
|
||||||
|
}
|
||||||
return s.repo.Patch(ctx, id, req)
|
return s.repo.Patch(ctx, id, req)
|
||||||
}
|
}
|
||||||
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("QuestionnaireResponse ID is required").Build()
|
||||||
|
}
|
||||||
return s.repo.GetByID(ctx, id)
|
return s.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
||||||
|
|||||||
@@ -3,16 +3,18 @@ package servicerequest
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type ServiceRequestRequest struct {
|
type ServiceRequestRequest struct {
|
||||||
PatientID string `json:"patient_id" binding:"required"`
|
PatientID string `json:"patient_id,omitempty"`
|
||||||
PatientName string `json:"patient_name" binding:"required"`
|
PatientName string `json:"patient_name,omitempty"`
|
||||||
EncounterID string `json:"encounter_id" binding:"required"`
|
EncounterID string `json:"encounter_id,omitempty"`
|
||||||
RequesterID string `json:"requester_id" binding:"required"`
|
RequesterID string `json:"requester_id,omitempty"`
|
||||||
RequesterName string `json:"requester_name" binding:"required"`
|
PerformerID string `json:"performer_id,omitempty"`
|
||||||
Status string `json:"status" binding:"required,oneof=draft active on-hold revoked completed entered-in-error unknown"` // draft, active, on-hold, revoked, completed, entered-in-error, unknown
|
RequesterName string `json:"requester_name,omitempty"`
|
||||||
Intent string `json:"intent" binding:"required,oneof=proposal plan directive order original-order reflex-order filler-order instance-order option"` // proposal, plan, directive, order, original-order, reflex-order, filler-order, instance-order, option
|
PerformerName string `json:"performer_name,omitempty"`
|
||||||
Code string `json:"code" binding:"required"`
|
Status string `json:"status,omitempty" binding:"omitempty,oneof=draft active on-hold revoked completed entered-in-error unknown"`
|
||||||
Display string `json:"display" binding:"required"`
|
Intent string `json:"intent,omitempty" binding:"omitempty,oneof=proposal plan directive order original-order reflex-order filler-order instance-order option"`
|
||||||
AuthoredOn time.Time `json:"authored_on" binding:"required"`
|
Code string `json:"code,omitempty"`
|
||||||
|
Display string `json:"display,omitempty"`
|
||||||
|
AuthoredOn time.Time `json:"authored_on,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServiceRequestPatchRequest []map[string]interface{}
|
type ServiceRequestPatchRequest []map[string]interface{}
|
||||||
@@ -7,32 +7,66 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func MapRequestToFHIR(req ServiceRequestRequest) satusehat.FHIRPayload {
|
func MapRequestToFHIR(req ServiceRequestRequest) satusehat.FHIRPayload {
|
||||||
payload := satusehat.NewFHIRPayload("ServiceRequest").
|
payload := satusehat.NewFHIRPayload("ServiceRequest")
|
||||||
Set("status", req.Status).
|
|
||||||
Set("intent", req.Intent).
|
if req.Status != "" {
|
||||||
Set("code", map[string]interface{}{
|
payload.Set("status", req.Status)
|
||||||
"coding": []map[string]interface{}{
|
}
|
||||||
{
|
|
||||||
|
if req.Intent != "" {
|
||||||
|
payload.Set("intent", req.Intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Code != "" {
|
||||||
|
coding := map[string]interface{}{
|
||||||
"system": "http://snomed.info/sct",
|
"system": "http://snomed.info/sct",
|
||||||
"code": req.Code,
|
"code": req.Code,
|
||||||
"display": req.Display,
|
}
|
||||||
},
|
if req.Display != "" {
|
||||||
},
|
coding["display"] = req.Display
|
||||||
}).
|
}
|
||||||
Set("subject", map[string]interface{}{
|
payload.Set("code", map[string]interface{}{
|
||||||
|
"coding": []map[string]interface{}{coding},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.PatientID != "" {
|
||||||
|
subject := map[string]interface{}{
|
||||||
"reference": "Patient/" + req.PatientID,
|
"reference": "Patient/" + req.PatientID,
|
||||||
"display": req.PatientName,
|
}
|
||||||
}).
|
if req.PatientName != "" {
|
||||||
Set("encounter", map[string]interface{}{
|
subject["display"] = req.PatientName
|
||||||
|
}
|
||||||
|
payload.Set("subject", subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.EncounterID != "" {
|
||||||
|
payload.Set("encounter", map[string]interface{}{
|
||||||
"reference": "Encounter/" + req.EncounterID,
|
"reference": "Encounter/" + req.EncounterID,
|
||||||
}).
|
})
|
||||||
Set("authoredOn", req.AuthoredOn.Format(time.RFC3339))
|
}
|
||||||
|
|
||||||
|
if !req.AuthoredOn.IsZero() {
|
||||||
|
payload.Set("authoredOn", req.AuthoredOn.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
if req.PerformerID != "" {
|
||||||
|
performer := map[string]interface{}{
|
||||||
|
"reference": "Practitioner/" + req.PerformerID,
|
||||||
|
}
|
||||||
|
if req.PerformerName != "" {
|
||||||
|
performer["display"] = req.PerformerName
|
||||||
|
}
|
||||||
|
payload.Set("performer", []map[string]interface{}{performer})
|
||||||
|
}
|
||||||
|
|
||||||
if req.RequesterID != "" {
|
if req.RequesterID != "" {
|
||||||
payload.Set("requester", map[string]interface{}{
|
requester := map[string]interface{}{
|
||||||
"reference": "Practitioner/" + req.RequesterID,
|
"reference": "Practitioner/" + req.RequesterID,
|
||||||
"display": req.RequesterName,
|
}
|
||||||
})
|
if req.RequesterName != "" {
|
||||||
|
requester["display"] = req.RequesterName
|
||||||
|
}
|
||||||
|
payload.Set("requester", requester)
|
||||||
}
|
}
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Repository mendefinisikan kontrak untuk operasi penyimpanan data ServiceRequest.
|
// Repository mendefinisikan kontrak untuk operasi penyimpanan data ServiceRequest.
|
||||||
@@ -26,15 +27,22 @@ func NewRepository(client satusehat.SatuSehatClient) Repository {
|
|||||||
return &repository{client: client}
|
return &repository{client: client}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type hiddenCtx struct {
|
||||||
|
context.Context
|
||||||
|
}
|
||||||
|
|
||||||
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
resp, err := r.client.DoRequest(ctx, method, endpoint, req)
|
resp, err := r.client.DoRequest(hiddenCtx{ctx}, method, endpoint, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.InternalError().Message("Failed to execute request to SatuSehat").Cause(err).Build()
|
||||||
}
|
}
|
||||||
|
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
if err := json.Unmarshal(resp, &result); err != nil {
|
if err := json.Unmarshal(resp, &result); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
return nil, errors.InternalError().Message("Failed to parse SatuSehat response").Cause(err).Metadata("raw_response", string(resp)).Build()
|
||||||
|
}
|
||||||
|
if resourceType, ok := result["resourceType"].(string); ok && resourceType == "OperationOutcome" {
|
||||||
|
return nil, errors.ParseSatuSehatError(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
var resourceID string
|
var resourceID string
|
||||||
|
|||||||
@@ -1,16 +1,36 @@
|
|||||||
package specimen
|
package specimen
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service/internal/satusehat/common"
|
||||||
|
)
|
||||||
|
|
||||||
type SpecimenRequest struct {
|
type SpecimenRequest struct {
|
||||||
PatientID string `json:"patient_id" binding:"required"`
|
OrganizationID string `json:"organization_id,omitempty"`
|
||||||
PatientName string `json:"patient_name" binding:"required"`
|
SpecimenID string `json:"specimen_id,omitempty"`
|
||||||
Status string `json:"status" binding:"required,oneof=available unavailable unsatisfactory entered-in-error"`
|
Status string `json:"status" binding:"required,oneof=available,unavailable,unsatisfactory,entered-in-error"`
|
||||||
TypeCode string `json:"type_code" binding:"required"` // SNOMED CT Code (e.g., 119297000 for Blood)
|
Type *common.CodeableConceptDTO `json:"type" binding:"required"`
|
||||||
TypeDisplay string `json:"type_display" binding:"required"`
|
Subject *common.ReferenceDTO `json:"subject" binding:"required"`
|
||||||
CollectedDateTime time.Time `json:"collected_date_time" binding:"required"`
|
Collection *CollectionDTO `json:"collection,omitempty"`
|
||||||
CollectorID string `json:"collector_id" binding:"required"`
|
ReceivedDateTime *time.Time `json:"received_date_time,omitempty"`
|
||||||
CollectorName string `json:"collector_name" binding:"required"`
|
Processing []ProcessingDTO `json:"processing,omitempty"`
|
||||||
|
Conditions []string `json:"conditions,omitempty"`
|
||||||
|
Request []common.ReferenceDTO `json:"request,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CollectionDTO struct {
|
||||||
|
CollectedDateTime *time.Time `json:"collected_date_time,omitempty"`
|
||||||
|
Collector *common.ReferenceDTO `json:"collector,omitempty"`
|
||||||
|
Quantity *common.QuantityDTO `json:"quantity,omitempty"`
|
||||||
|
Method *common.CodeableConceptDTO `json:"method,omitempty"`
|
||||||
|
BodySite *common.CodeableConceptDTO `json:"body_site,omitempty"`
|
||||||
|
FastingStatus *common.CodeableConceptDTO `json:"fasting_status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProcessingDTO struct {
|
||||||
|
Procedure *common.CodeableConceptDTO `json:"procedure,omitempty"`
|
||||||
|
TimeDateTime *time.Time `json:"time_datetime,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SpecimenPatchRequest []map[string]interface{}
|
type SpecimenPatchRequest []map[string]interface{}
|
||||||
@@ -1,27 +1,166 @@
|
|||||||
package specimen
|
package specimen
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"service/internal/interfaces/satusehat"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"service/internal/interfaces/satusehat"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MapRequestToFHIR(req SpecimenRequest) satusehat.FHIRPayload {
|
func MapRequestToFHIR(req SpecimenRequest) satusehat.FHIRPayload {
|
||||||
return satusehat.NewFHIRPayload("Specimen").
|
payload := satusehat.NewFHIRPayload("Specimen")
|
||||||
Set("status", req.Status).
|
|
||||||
Set("type", map[string]interface{}{
|
orgID := os.Getenv("SATUSEHAT_ORG_ID")
|
||||||
"coding": []map[string]interface{}{
|
if orgID == "" {
|
||||||
{"system": "http://snomed.info/sct", "code": req.TypeCode, "display": req.TypeDisplay},
|
orgID = req.OrganizationID
|
||||||
|
}
|
||||||
|
|
||||||
|
if orgID != "" && req.SpecimenID != "" {
|
||||||
|
payload.Set("identifier", []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"system": "http://sys-ids.kemkes.go.id/specimen/" + orgID,
|
||||||
|
"value": req.SpecimenID,
|
||||||
|
"assigner": map[string]interface{}{
|
||||||
|
"reference": "Organization/" + orgID,
|
||||||
},
|
},
|
||||||
}).
|
|
||||||
Set("subject", map[string]interface{}{
|
|
||||||
"reference": "Patient/" + req.PatientID,
|
|
||||||
"display": req.PatientName,
|
|
||||||
}).
|
|
||||||
Set("collection", map[string]interface{}{
|
|
||||||
"collectedDateTime": req.CollectedDateTime.Format(time.RFC3339),
|
|
||||||
"collector": map[string]interface{}{
|
|
||||||
"reference": "Practitioner/" + req.CollectorID,
|
|
||||||
"display": req.CollectorName,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.Status != "" {
|
||||||
|
payload.Set("status", req.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Type != nil {
|
||||||
|
sys := req.Type.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://snomed.info/sct"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": req.Type.Code}
|
||||||
|
if req.Type.Display != "" {
|
||||||
|
coding["display"] = req.Type.Display
|
||||||
|
}
|
||||||
|
payload.Set("type", map[string]interface{}{"coding": []map[string]interface{}{coding}})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Collection != nil {
|
||||||
|
coll := map[string]interface{}{}
|
||||||
|
if req.Collection.Collector != nil {
|
||||||
|
collector := map[string]interface{}{"reference": req.Collection.Collector.Reference}
|
||||||
|
if req.Collection.Collector.Display != "" {
|
||||||
|
collector["display"] = req.Collection.Collector.Display
|
||||||
|
}
|
||||||
|
coll["collector"] = collector
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Collection.CollectedDateTime != nil && !req.Collection.CollectedDateTime.IsZero() {
|
||||||
|
coll["collectedDateTime"] = req.Collection.CollectedDateTime.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Collection.Quantity != nil {
|
||||||
|
sys := req.Collection.Quantity.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://unitsofmeasure.org"
|
||||||
|
}
|
||||||
|
coll["quantity"] = map[string]interface{}{
|
||||||
|
"value": req.Collection.Quantity.Value,
|
||||||
|
"unit": req.Collection.Quantity.Unit,
|
||||||
|
"system": sys,
|
||||||
|
"code": req.Collection.Quantity.Code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Collection.Method != nil {
|
||||||
|
sys := req.Collection.Method.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://snomed.info/sct"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": req.Collection.Method.Code}
|
||||||
|
if req.Collection.Method.Display != "" {
|
||||||
|
coding["display"] = req.Collection.Method.Display
|
||||||
|
}
|
||||||
|
coll["method"] = map[string]interface{}{"coding": []map[string]interface{}{coding}}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Collection.BodySite != nil {
|
||||||
|
sys := req.Collection.BodySite.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://snomed.info/sct"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": req.Collection.BodySite.Code}
|
||||||
|
if req.Collection.BodySite.Display != "" {
|
||||||
|
coding["display"] = req.Collection.BodySite.Display
|
||||||
|
}
|
||||||
|
coll["bodySite"] = map[string]interface{}{"coding": []map[string]interface{}{coding}}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Collection.FastingStatus != nil {
|
||||||
|
sys := req.Collection.FastingStatus.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://terminology.hl7.org/CodeSystem/v2-0916"
|
||||||
|
}
|
||||||
|
coding := map[string]interface{}{"system": sys, "code": req.Collection.FastingStatus.Code}
|
||||||
|
if req.Collection.FastingStatus.Display != "" {
|
||||||
|
coding["display"] = req.Collection.FastingStatus.Display
|
||||||
|
}
|
||||||
|
coll["fastingStatusCodeableConcept"] = map[string]interface{}{"coding": []map[string]interface{}{coding}}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(coll) > 0 {
|
||||||
|
payload.Set("collection", coll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Processing) > 0 {
|
||||||
|
var processings []map[string]interface{}
|
||||||
|
for _, proc := range req.Processing {
|
||||||
|
p := map[string]interface{}{}
|
||||||
|
if proc.Procedure != nil {
|
||||||
|
sys := proc.Procedure.System
|
||||||
|
if sys == "" {
|
||||||
|
sys = "http://snomed.info/sct"
|
||||||
|
}
|
||||||
|
p["procedure"] = map[string]interface{}{
|
||||||
|
"coding": []map[string]interface{}{
|
||||||
|
{"system": sys, "code": proc.Procedure.Code, "display": proc.Procedure.Display},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if proc.TimeDateTime != nil && !proc.TimeDateTime.IsZero() {
|
||||||
|
p["timeDateTime"] = proc.TimeDateTime.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
processings = append(processings, p)
|
||||||
|
}
|
||||||
|
payload.Set("processing", processings)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Conditions) > 0 {
|
||||||
|
var conditions []map[string]interface{}
|
||||||
|
for _, c := range req.Conditions {
|
||||||
|
conditions = append(conditions, map[string]interface{}{"text": c})
|
||||||
|
}
|
||||||
|
payload.Set("condition", conditions)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Subject != nil {
|
||||||
|
subject := map[string]interface{}{"reference": req.Subject.Reference}
|
||||||
|
if req.Subject.Display != "" {
|
||||||
|
subject["display"] = req.Subject.Display
|
||||||
|
}
|
||||||
|
payload.Set("subject", subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Request) > 0 {
|
||||||
|
var requests []map[string]interface{}
|
||||||
|
for _, r := range req.Request {
|
||||||
|
requests = append(requests, map[string]interface{}{"reference": r.Reference})
|
||||||
|
}
|
||||||
|
payload.Set("request", requests)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ReceivedDateTime != nil && !req.ReceivedDateTime.IsZero() {
|
||||||
|
payload.Set("receivedTime", req.ReceivedDateTime.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"service/internal/interfaces/satusehat"
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
@@ -19,14 +20,21 @@ type repository struct{ client satusehat.SatuSehatClient }
|
|||||||
|
|
||||||
func NewRepository(client satusehat.SatuSehatClient) Repository { return &repository{client: client} }
|
func NewRepository(client satusehat.SatuSehatClient) Repository { return &repository{client: client} }
|
||||||
|
|
||||||
|
type hiddenCtx struct {
|
||||||
|
context.Context
|
||||||
|
}
|
||||||
|
|
||||||
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
func (r *repository) executeRequest(ctx context.Context, method, endpoint string, req interface{}) (*satusehat.FHIRResponse, error) {
|
||||||
resp, err := r.client.DoRequest(ctx, method, endpoint, req)
|
resp, err := r.client.DoRequest(hiddenCtx{ctx}, method, endpoint, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.InternalError().Message("Failed to execute request to SatuSehat").Cause(err).Build()
|
||||||
}
|
}
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
if err := json.Unmarshal(resp, &result); err != nil {
|
if err := json.Unmarshal(resp, &result); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
return nil, errors.InternalError().Message("Failed to parse SatuSehat response").Cause(err).Metadata("raw_response", string(resp)).Build()
|
||||||
|
}
|
||||||
|
if resourceType, ok := result["resourceType"].(string); ok && resourceType == "OperationOutcome" {
|
||||||
|
return nil, errors.ParseSatuSehatError(result)
|
||||||
}
|
}
|
||||||
var resourceID string
|
var resourceID string
|
||||||
if id, ok := result["id"].(string); ok {
|
if id, ok := result["id"].(string); ok {
|
||||||
|
|||||||
@@ -22,14 +22,25 @@ func (s *service) Create(ctx context.Context, req SpecimenRequest) (*satusehat.F
|
|||||||
}
|
}
|
||||||
func (s *service) Update(ctx context.Context, id string, req SpecimenRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Update(ctx context.Context, id string, req SpecimenRequest) (*satusehat.FHIRResponse, error) {
|
||||||
if id == "" {
|
if id == "" {
|
||||||
return nil, errors.NewValidationError().Message("ID is required").Build()
|
return nil, errors.NewValidationError().Message("Specimen ID is required").Build()
|
||||||
}
|
}
|
||||||
return s.repo.Update(ctx, id, MapRequestToFHIR(req).Set("id", id))
|
payload := MapRequestToFHIR(req)
|
||||||
|
payload.Set("id", id)
|
||||||
|
return s.repo.Update(ctx, id, payload)
|
||||||
}
|
}
|
||||||
func (s *service) Patch(ctx context.Context, id string, req SpecimenPatchRequest) (*satusehat.FHIRResponse, error) {
|
func (s *service) Patch(ctx context.Context, id string, req SpecimenPatchRequest) (*satusehat.FHIRResponse, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("Specimen ID is required").Build()
|
||||||
|
}
|
||||||
|
if len(req) == 0 {
|
||||||
|
return nil, errors.NewValidationError().Message("Patch payload cannot be empty").Build()
|
||||||
|
}
|
||||||
return s.repo.Patch(ctx, id, req)
|
return s.repo.Patch(ctx, id, req)
|
||||||
}
|
}
|
||||||
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
func (s *service) GetByID(ctx context.Context, id string) (*satusehat.FHIRResponse, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("Specimen ID is required").Build()
|
||||||
|
}
|
||||||
return s.repo.GetByID(ctx, id)
|
return s.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
func (s *service) Search(ctx context.Context, queryParams url.Values) (*satusehat.FHIRResponse, error) {
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package studies
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repository interface {
|
||||||
|
UploadDICOM(ctx context.Context, dicomBytes []byte) (*satusehat.FHIRResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type repository struct {
|
||||||
|
client satusehat.SatuSehatClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepository(client satusehat.SatuSehatClient) Repository {
|
||||||
|
return &repository{client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) UploadDICOM(ctx context.Context, dicomBytes []byte) (*satusehat.FHIRResponse, error) {
|
||||||
|
respData, err := r.client.UploadDICOM(ctx, dicomBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := json.Unmarshal(respData, &result); err != nil {
|
||||||
|
return nil, errors.InternalError().Message("Gagal memparsing response DICOM dari SatuSehat").Cause(err).Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &satusehat.FHIRResponse{
|
||||||
|
FullResponse: result,
|
||||||
|
RawResponse: respData,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package studies
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"service/internal/interfaces/satusehat"
|
||||||
|
"service/pkg/errors"
|
||||||
|
"service/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
UploadDICOM(ctx context.Context, dicomBytes []byte) (*satusehat.FHIRResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
repo Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(repo Repository) Service {
|
||||||
|
return &service{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) UploadDICOM(ctx context.Context, dicomBytes []byte) (*satusehat.FHIRResponse, error) {
|
||||||
|
if len(dicomBytes) == 0 {
|
||||||
|
return nil, errors.NewValidationError().Message("File DICOM kosong").Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Default().Info("Mempersiapkan upload DICOM", logger.Int("file_size_bytes", len(dicomBytes)))
|
||||||
|
|
||||||
|
resp, err := s.repo.UploadDICOM(ctx, dicomBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.InternalError().Message("Gagal meneruskan file DICOM ke SatuSehat").Cause(err).Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
Loaded 100 of 127 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in New Issue
Block a user