diff --git a/go.mod b/go.mod index 555500a2..8258eae3 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/ahmetb/go-linq/v3 v3.2.0 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/brianvoe/gofakeit v3.18.0+incompatible + github.com/chenyahui/gin-cache v1.8.1 github.com/getsentry/sentry-go v0.23.0 github.com/gin-contrib/cors v1.4.0 github.com/gin-gonic/gin v1.9.1 @@ -55,7 +56,6 @@ require ( github.com/bytedance/sonic v1.10.1 // indirect github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/chenyahui/gin-cache v1.8.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect diff --git a/go.sum b/go.sum index 6189031c..3ade1c28 100644 --- a/go.sum +++ b/go.sum @@ -106,8 +106,6 @@ github.com/brianvoe/gofakeit v3.18.0+incompatible h1:wDOmHc9DLG4nRjUVVaxA+CEglKO github.com/brianvoe/gofakeit v3.18.0+incompatible/go.mod h1:kfwdRA90vvNhPutZWfH7WPaDzUjz+CZFqG+rPkOjGOc= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= -github.com/bytedance/sonic v1.10.0 h1:qtNZduETEIWJVIyDl01BeNxur2rW9OwTQ/yBqFRkKEk= -github.com/bytedance/sonic v1.10.0/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc= github.com/bytedance/sonic v1.10.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -175,6 +173,7 @@ github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBd github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= @@ -218,8 +217,6 @@ github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= -github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2raYcGffYWZEjZzM= -github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-playground/validator/v10 v10.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7NLylN+x8TTueE24= github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= @@ -433,21 +430,22 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= -github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= -github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= @@ -630,9 +628,8 @@ go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZE go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc= -golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y= golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -646,8 +643,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -733,8 +728,6 @@ golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -765,8 +758,6 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -833,8 +824,6 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -851,8 +840,6 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1068,6 +1055,7 @@ gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8 gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/clinical/application/common/defaults.go b/pkg/clinical/application/common/defaults.go index c4028920..7349a08b 100644 --- a/pkg/clinical/application/common/defaults.go +++ b/pkg/clinical/application/common/defaults.go @@ -106,6 +106,12 @@ const ( // AddFHIRIDToProgram is the topic where details to update a program's fhir ID will be published to AddFHIRIDToProgram = "program.fhirid.update" + + // LOINCProgressNoteCode defines LOINC progress note terminology code + LOINCProgressNoteCode = "81216-4" + + // LOINCAssessmentPlanCode defines LOINC assessment plan note terminology code + LOINCAssessmentPlanCode = "51847-2" ) // DefaultIdentifier assigns a patient a code to function as their diff --git a/pkg/clinical/application/dto/composition_output.go b/pkg/clinical/application/dto/composition_output.go new file mode 100644 index 00000000..170b89e5 --- /dev/null +++ b/pkg/clinical/application/dto/composition_output.go @@ -0,0 +1,51 @@ +package dto + +import ( + "github.com/savannahghi/scalarutils" +) + +// Composition is a minimal representation of a fhir Composition +type Composition struct { + ID string `json:"id,omitempty"` + Text string `json:"text,omitempty"` + Type CompositionType `json:"type,omitempty"` + Category CompositionCategory `json:"category,omitempty"` + Status CompositionStatusEnum `json:"status,omitempty"` + PatientID string `json:"patientID,omitempty"` + EncounterID string `json:"encounterID,omitempty"` + Date *scalarutils.Date `json:"date"` + Author string `json:"author,omitempty"` +} + +// CompositionEdge is a composition edge +type CompositionEdge struct { + Node Composition + Cursor string +} + +// CompositionConnection is a Composition Connection Type +type CompositionConnection struct { + TotalCount int + Edges []CompositionEdge + PageInfo PageInfo +} + +// CreateConditionConnection creates a connection that follows the GraphQl Cursor Connection Specification +func CreateCompositionConnection(compositions []Composition, pageInfo PageInfo, total int) CompositionConnection { + connection := CompositionConnection{ + TotalCount: total, + Edges: []CompositionEdge{}, + PageInfo: pageInfo, + } + + for _, composition := range compositions { + edge := CompositionEdge{ + Node: composition, + Cursor: composition.ID, + } + + connection.Edges = append(connection.Edges, edge) + } + + return connection +} diff --git a/pkg/clinical/application/dto/enums.go b/pkg/clinical/application/dto/enums.go index 2b3361be..d8985361 100644 --- a/pkg/clinical/application/dto/enums.go +++ b/pkg/clinical/application/dto/enums.go @@ -116,7 +116,7 @@ const ( ConditionStatusUnknown ConditionStatus = "UNKNOWN" ) -// ConditionStatus represents status of a FHIR condition +// ConditionCategory represents status of a FHIR condition type ConditionCategory string const ( @@ -133,3 +133,35 @@ const ( TerminologySourceSNOMEDCT TerminologySource = "SNOMED_CT" TerminologySourceLOINC TerminologySource = "LOINC" ) + +// LOINCCodes represents LOINC assessment codes +type LOINCCodes string + +const ( + LOINCPlanOfCareCode LOINCCodes = "18776-5" + LOINCAssessmentPlanCode LOINCCodes = "51847-2" +) + +// CompositionCategory enum represents category composition attribute +type CompositionCategory string + +const ( + AssessmentAndPlan CompositionCategory = "ASSESSMENT_PLAN" +) + +// Type enum represents type composition attribute +type CompositionType string + +const ( + ProgressNote CompositionType = "PROGRESS_NOTE" +) + +// CompositionStatus enum represents status composition attribute +type CompositionStatusEnum string + +const ( + CompositionStatuEnumPreliminary CompositionStatusEnum = "PRELIMINARY" + CompositionStatuEnumFinal CompositionStatusEnum = "FINAL" + CompositionStatuEnumAmended CompositionStatusEnum = "AMENDED" + CompositionStatuEnumEnteredInErrorPreliminary CompositionStatusEnum = "ENTERED_IN_ERROR" +) diff --git a/pkg/clinical/application/dto/input.go b/pkg/clinical/application/dto/input.go index 0861bb52..62b2428c 100644 --- a/pkg/clinical/application/dto/input.go +++ b/pkg/clinical/application/dto/input.go @@ -101,3 +101,12 @@ type MediaInput struct { EncounterID string `json:"encounterID"` File map[string][]*multipart.FileHeader `form:"file" json:"file"` } + +// CompositionInput models the composition input +type CompositionInput struct { + EncounterID string `json:"encounterID"` + Type CompositionType `json:"type"` + Category CompositionCategory `json:"category"` + Status CompositionStatusEnum `json:"status"` + Note string `json:"note"` +} diff --git a/pkg/clinical/domain/complex_types.go b/pkg/clinical/domain/complex_types.go index 7e1f0b51..a00f9ad5 100644 --- a/pkg/clinical/domain/complex_types.go +++ b/pkg/clinical/domain/complex_types.go @@ -2099,6 +2099,7 @@ type CompositionStatusEnum string const ( // CompositionStatusEnumPreliminary ... CompositionStatusEnumPreliminary CompositionStatusEnum = "preliminary" + CompositionStatusEnumRegistered CompositionStatusEnum = "registered" // CompositionStatusEnumFinal ... CompositionStatusEnumFinal CompositionStatusEnum = "final" // CompositionStatusEnumAmended ... diff --git a/pkg/clinical/domain/composition.go b/pkg/clinical/domain/composition.go index 723674f7..4f43a20a 100644 --- a/pkg/clinical/domain/composition.go +++ b/pkg/clinical/domain/composition.go @@ -223,7 +223,7 @@ type FHIRCompositionSection struct { Title *string `json:"title,omitempty"` // A code identifying the kind of content contained within the section. This must be consistent with the section title. - Code *scalarutils.Code `json:"code,omitempty"` + Code *FHIRCodeableConceptInput `json:"code,omitempty"` // Identifies who is responsible for the information in this section, not necessarily who typed it in. Author []*FHIRReference `json:"author,omitempty"` @@ -259,7 +259,7 @@ type FHIRCompositionSectionInput struct { Title *string `json:"title,omitempty"` // A code identifying the kind of content contained within the section. This must be consistent with the section title. - Code *scalarutils.Code `json:"code,omitempty"` + Code *FHIRCodeableConceptInput `json:"code,omitempty"` // Identifies who is responsible for the information in this section, not necessarily who typed it in. Author []*FHIRReferenceInput `json:"author,omitempty"` diff --git a/pkg/clinical/infrastructure/datastore/cloudhealthcare/mock/fhir_mock.go b/pkg/clinical/infrastructure/datastore/cloudhealthcare/mock/fhir_mock.go index 36ca84a4..23df8fbc 100644 --- a/pkg/clinical/infrastructure/datastore/cloudhealthcare/mock/fhir_mock.go +++ b/pkg/clinical/infrastructure/datastore/cloudhealthcare/mock/fhir_mock.go @@ -6,6 +6,7 @@ import ( "github.com/brianvoe/gofakeit" "github.com/google/uuid" + "github.com/savannahghi/clinical/pkg/clinical/application/common" "github.com/savannahghi/clinical/pkg/clinical/application/dto" "github.com/savannahghi/clinical/pkg/clinical/domain" "github.com/savannahghi/firebasetools" @@ -652,7 +653,103 @@ func NewFHIRMock() *FHIRMock { return &domain.FHIRCompositionRelayConnection{}, nil }, MockCreateFHIRCompositionFn: func(ctx context.Context, input domain.FHIRCompositionInput) (*domain.FHIRCompositionRelayPayload, error) { - return &domain.FHIRCompositionRelayPayload{}, nil + UUID := uuid.New().String() + compositionTitle := gofakeit.Name() + "assessment note" + typeSystem := scalarutils.URI("http://hl7.org/fhir/ValueSet/doc-typecodes") + categorySystem := scalarutils.URI("http://hl7.org/fhir/ValueSet/referenced-item-category") + category := "Assessment + plan" + compositionType := "Progress note" + treatmentPlan := "Treatment Plan" + compositionStatus := "active" + note := scalarutils.Markdown("Fever Fever") + PatientRef := "Patient/" + uuid.NewString() + patientType := "Patient" + organizationRef := "Organization/" + uuid.NewString() + compositionSectionTextStatus := "generated" + + return &domain.FHIRCompositionRelayPayload{ + Resource: &domain.FHIRComposition{ + ID: &UUID, + Text: &domain.FHIRNarrative{}, + Identifier: &domain.FHIRIdentifier{}, + Status: (*domain.CompositionStatusEnum)(&compositionStatus), + Type: &domain.FHIRCodeableConcept{ + ID: new(string), + Coding: []*domain.FHIRCoding{ + { + ID: &UUID, + System: &typeSystem, + Code: scalarutils.Code(string(common.LOINCProgressNoteCode)), + Display: compositionType, + }, + }, + Text: "Progress note", + }, + Category: []*domain.FHIRCodeableConcept{ + { + // ID: new(string), + Coding: []*domain.FHIRCoding{ + { + ID: &UUID, + System: &categorySystem, + Version: new(string), + Code: scalarutils.Code(string(common.LOINCAssessmentPlanCode)), + Display: category, + }, + }, + Text: "Assessment + plan", + }, + }, + Subject: &domain.FHIRReference{ + ID: &UUID, + Reference: &PatientRef, + Type: (*scalarutils.URI)(&patientType), + }, + Encounter: &domain.FHIRReference{ + ID: &UUID, + }, + Date: &scalarutils.Date{ + Year: 2023, + Month: 9, + Day: 25, + }, + Author: []*domain.FHIRReference{ + { + Reference: &organizationRef, + }, + }, + Title: &compositionTitle, + Section: []*domain.FHIRCompositionSection{ + { + ID: &UUID, + Title: &treatmentPlan, + Code: &domain.FHIRCodeableConceptInput{ + ID: new(string), + Coding: []*domain.FHIRCodingInput{ + { + ID: &UUID, + System: &categorySystem, + Version: new(string), + Code: scalarutils.Code(string(common.LOINCAssessmentPlanCode)), + Display: category, + }, + }, + Text: "Assessment + plan", + }, + Author: []*domain.FHIRReference{ + { + Reference: new(string), + }, + }, + Text: &domain.FHIRNarrative{ + ID: &UUID, + Status: (*domain.NarrativeStatusEnum)(&compositionSectionTextStatus), + Div: scalarutils.XHTML(note), + }, + }, + }, + }, + }, nil }, MockUpdateFHIRCompositionFn: func(ctx context.Context, input domain.FHIRCompositionInput) (*domain.FHIRCompositionRelayPayload, error) { return &domain.FHIRCompositionRelayPayload{}, nil diff --git a/pkg/clinical/presentation/graph/clinical.graphql b/pkg/clinical/presentation/graph/clinical.graphql index 46ed53f0..04e51b9f 100644 --- a/pkg/clinical/presentation/graph/clinical.graphql +++ b/pkg/clinical/presentation/graph/clinical.graphql @@ -133,4 +133,7 @@ extend type Mutation { # Allergy Intolerance createAllergyIntolerance(input: AllergyInput!): Allergy + + # Clinical notes(composition) + createComposition(input: CompositionInput!): Composition! } diff --git a/pkg/clinical/presentation/graph/clinical.resolvers.go b/pkg/clinical/presentation/graph/clinical.resolvers.go index c3d8b640..fba9334d 100644 --- a/pkg/clinical/presentation/graph/clinical.resolvers.go +++ b/pkg/clinical/presentation/graph/clinical.resolvers.go @@ -155,6 +155,12 @@ func (r *mutationResolver) CreateAllergyIntolerance(ctx context.Context, input d return r.usecases.CreateAllergyIntolerance(ctx, input) } +// CreateComposition is the resolver for the createComposition field. +func (r *mutationResolver) CreateComposition(ctx context.Context, input dto.CompositionInput) (*dto.Composition, error) { + r.CheckDependencies() + return r.usecases.CreateComposition(ctx, input) +} + // PatientHealthTimeline is the resolver for the patientHealthTimeline field. func (r *queryResolver) PatientHealthTimeline(ctx context.Context, input dto.HealthTimelineInput) (*dto.HealthTimeline, error) { r.CheckDependencies() diff --git a/pkg/clinical/presentation/graph/enums.graphql b/pkg/clinical/presentation/graph/enums.graphql index 4f5c48fb..31665ed2 100644 --- a/pkg/clinical/presentation/graph/enums.graphql +++ b/pkg/clinical/presentation/graph/enums.graphql @@ -69,6 +69,21 @@ enum ConditionStatus { REMISSSION } +enum CompositionStatusEnum { + PRELIMINARY + FINAL + AMENDED + ENTERED_IN_ERROR +} + +enum CompositionCategory { + ASSESSMENT_PLAN +} + +enum CompositionType { + PROGRESS_NOTE +} + enum TerminologySource { ICD10 CIEL diff --git a/pkg/clinical/presentation/graph/generated/generated.go b/pkg/clinical/presentation/graph/generated/generated.go index 76508146..db162d6a 100644 --- a/pkg/clinical/presentation/graph/generated/generated.go +++ b/pkg/clinical/presentation/graph/generated/generated.go @@ -68,6 +68,17 @@ type ComplexityRoot struct { Node func(childComplexity int) int } + Composition struct { + Category func(childComplexity int) int + Date func(childComplexity int) int + EncounterID func(childComplexity int) int + ID func(childComplexity int) int + PatientID func(childComplexity int) int + Status func(childComplexity int) int + Text func(childComplexity int) int + Type func(childComplexity int) int + } + Condition struct { Category func(childComplexity int) int Code func(childComplexity int) int @@ -164,6 +175,7 @@ type ComplexityRoot struct { Mutation struct { CreateAllergyIntolerance func(childComplexity int, input dto.AllergyInput) int + CreateComposition func(childComplexity int, input dto.CompositionInput) int CreateCondition func(childComplexity int, input dto.ConditionInput) int CreateEpisodeOfCare func(childComplexity int, episodeOfCare dto.EpisodeOfCareInput) int CreatePatient func(childComplexity int, input dto.PatientInput) int @@ -316,6 +328,7 @@ type MutationResolver interface { DeletePatient(ctx context.Context, id string) (bool, error) CreateCondition(ctx context.Context, input dto.ConditionInput) (*dto.Condition, error) CreateAllergyIntolerance(ctx context.Context, input dto.AllergyInput) (*dto.Allergy, error) + CreateComposition(ctx context.Context, input dto.CompositionInput) (*dto.Composition, error) } type QueryResolver interface { PatientHealthTimeline(ctx context.Context, input dto.HealthTimelineInput) (*dto.HealthTimeline, error) @@ -441,6 +454,62 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.AllergyEdge.Node(childComplexity), true + case "Composition.category": + if e.complexity.Composition.Category == nil { + break + } + + return e.complexity.Composition.Category(childComplexity), true + + case "Composition.date": + if e.complexity.Composition.Date == nil { + break + } + + return e.complexity.Composition.Date(childComplexity), true + + case "Composition.encounterID": + if e.complexity.Composition.EncounterID == nil { + break + } + + return e.complexity.Composition.EncounterID(childComplexity), true + + case "Composition.id": + if e.complexity.Composition.ID == nil { + break + } + + return e.complexity.Composition.ID(childComplexity), true + + case "Composition.patientID": + if e.complexity.Composition.PatientID == nil { + break + } + + return e.complexity.Composition.PatientID(childComplexity), true + + case "Composition.status": + if e.complexity.Composition.Status == nil { + break + } + + return e.complexity.Composition.Status(childComplexity), true + + case "Composition.text": + if e.complexity.Composition.Text == nil { + break + } + + return e.complexity.Composition.Text(childComplexity), true + + case "Composition.type": + if e.complexity.Composition.Type == nil { + break + } + + return e.complexity.Composition.Type(childComplexity), true + case "Condition.category": if e.complexity.Condition.Category == nil { break @@ -817,6 +886,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.CreateAllergyIntolerance(childComplexity, args["input"].(dto.AllergyInput)), true + case "Mutation.createComposition": + if e.complexity.Mutation.CreateComposition == nil { + break + } + + args, err := ec.field_Mutation_createComposition_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.CreateComposition(childComplexity, args["input"].(dto.CompositionInput)), true + case "Mutation.createCondition": if e.complexity.Mutation.CreateCondition == nil { break @@ -1667,6 +1748,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec := executionContext{rc, e, 0, 0, make(chan graphql.DeferredResult)} inputUnmarshalMap := graphql.BuildUnmarshalerMap( ec.unmarshalInputAllergyInput, + ec.unmarshalInputCompositionInput, ec.unmarshalInputConditionInput, ec.unmarshalInputContactInput, ec.unmarshalInputEncounterInput, @@ -1910,6 +1992,9 @@ extend type Mutation { # Allergy Intolerance createAllergyIntolerance(input: AllergyInput!): Allergy + + # Clinical notes(composition) + createComposition(input: CompositionInput!): Composition! } `, BuiltIn: false}, {Name: "../enums.graphql", Input: `enum EpisodeOfCareStatusEnum { @@ -1983,6 +2068,21 @@ enum ConditionStatus { REMISSSION } +enum CompositionStatusEnum { + PRELIMINARY + FINAL + AMENDED + ENTERED_IN_ERROR +} + +enum CompositionCategory { + ASSESSMENT_PLAN +} + +enum CompositionType { + PROGRESS_NOTE +} + enum TerminologySource { ICD10 CIEL @@ -2085,6 +2185,14 @@ input AllergyInput { reaction: ReactionInput } +input CompositionInput { + type: CompositionType! + status: CompositionStatusEnum! + category: CompositionCategory! + encounterID: String! + note: String! +} + input ReactionInput { code: String system: String @@ -2289,6 +2397,18 @@ type MediaConnection { edges: [MediaEdge] pageInfo: PageInfo } + +type Composition { + id: String! + text: String! + type: CompositionType! + category: CompositionCategory! + status: CompositionStatusEnum! + date: Date + + patientID: String + encounterID: String +} `, BuiltIn: false}, {Name: "../../../../../federation/directives.graphql", Input: ` directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE @@ -2330,6 +2450,21 @@ func (ec *executionContext) field_Mutation_createAllergyIntolerance_args(ctx con return args, nil } +func (ec *executionContext) field_Mutation_createComposition_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 dto.CompositionInput + if tmp, ok := rawArgs["input"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + arg0, err = ec.unmarshalNCompositionInput2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐCompositionInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_createCondition_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -3703,7 +3838,366 @@ func (ec *executionContext) _AllergyEdge_node(ctx context.Context, field graphql }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Node, nil + return obj.Node, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(dto.Allergy) + fc.Result = res + return ec.marshalOAllergy2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐAllergy(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_AllergyEdge_node(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AllergyEdge", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Allergy_id(ctx, field) + case "code": + return ec.fieldContext_Allergy_code(ctx, field) + case "name": + return ec.fieldContext_Allergy_name(ctx, field) + case "system": + return ec.fieldContext_Allergy_system(ctx, field) + case "terminologySource": + return ec.fieldContext_Allergy_terminologySource(ctx, field) + case "encounterID": + return ec.fieldContext_Allergy_encounterID(ctx, field) + case "reaction": + return ec.fieldContext_Allergy_reaction(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Allergy", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _AllergyEdge_cursor(ctx context.Context, field graphql.CollectedField, obj *dto.AllergyEdge) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AllergyEdge_cursor(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Cursor, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalOString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_AllergyEdge_cursor(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AllergyEdge", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Composition_id(ctx context.Context, field graphql.CollectedField, obj *dto.Composition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Composition_id(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Composition_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Composition", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Composition_text(ctx context.Context, field graphql.CollectedField, obj *dto.Composition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Composition_text(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Text, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Composition_text(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Composition", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Composition_type(ctx context.Context, field graphql.CollectedField, obj *dto.Composition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Composition_type(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Type, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(dto.CompositionType) + fc.Result = res + return ec.marshalNCompositionType2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐCompositionType(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Composition_type(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Composition", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type CompositionType does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Composition_category(ctx context.Context, field graphql.CollectedField, obj *dto.Composition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Composition_category(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Category, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(dto.CompositionCategory) + fc.Result = res + return ec.marshalNCompositionCategory2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐCompositionCategory(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Composition_category(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Composition", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type CompositionCategory does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Composition_status(ctx context.Context, field graphql.CollectedField, obj *dto.Composition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Composition_status(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Status, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(dto.CompositionStatusEnum) + fc.Result = res + return ec.marshalNCompositionStatusEnum2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐCompositionStatusEnum(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Composition_status(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Composition", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type CompositionStatusEnum does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Composition_date(ctx context.Context, field graphql.CollectedField, obj *dto.Composition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Composition_date(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Date, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*scalarutils.Date) + fc.Result = res + return ec.marshalODate2ᚖgithubᚗcomᚋsavannahghiᚋscalarutilsᚐDate(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Composition_date(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Composition", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Date does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Composition_patientID(ctx context.Context, field graphql.CollectedField, obj *dto.Composition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Composition_patientID(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.PatientID, nil }) if err != nil { ec.Error(ctx, err) @@ -3712,42 +4206,26 @@ func (ec *executionContext) _AllergyEdge_node(ctx context.Context, field graphql if resTmp == nil { return graphql.Null } - res := resTmp.(dto.Allergy) + res := resTmp.(string) fc.Result = res - return ec.marshalOAllergy2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐAllergy(ctx, field.Selections, res) + return ec.marshalOString2string(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_AllergyEdge_node(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Composition_patientID(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "AllergyEdge", + Object: "Composition", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "id": - return ec.fieldContext_Allergy_id(ctx, field) - case "code": - return ec.fieldContext_Allergy_code(ctx, field) - case "name": - return ec.fieldContext_Allergy_name(ctx, field) - case "system": - return ec.fieldContext_Allergy_system(ctx, field) - case "terminologySource": - return ec.fieldContext_Allergy_terminologySource(ctx, field) - case "encounterID": - return ec.fieldContext_Allergy_encounterID(ctx, field) - case "reaction": - return ec.fieldContext_Allergy_reaction(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type Allergy", field.Name) + return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } -func (ec *executionContext) _AllergyEdge_cursor(ctx context.Context, field graphql.CollectedField, obj *dto.AllergyEdge) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_AllergyEdge_cursor(ctx, field) +func (ec *executionContext) _Composition_encounterID(ctx context.Context, field graphql.CollectedField, obj *dto.Composition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Composition_encounterID(ctx, field) if err != nil { return graphql.Null } @@ -3760,7 +4238,7 @@ func (ec *executionContext) _AllergyEdge_cursor(ctx context.Context, field graph }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Cursor, nil + return obj.EncounterID, nil }) if err != nil { ec.Error(ctx, err) @@ -3774,9 +4252,9 @@ func (ec *executionContext) _AllergyEdge_cursor(ctx context.Context, field graph return ec.marshalOString2string(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_AllergyEdge_cursor(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Composition_encounterID(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "AllergyEdge", + Object: "Composition", Field: field, IsMethod: false, IsResolver: false, @@ -7793,6 +8271,79 @@ func (ec *executionContext) fieldContext_Mutation_createAllergyIntolerance(ctx c return fc, nil } +func (ec *executionContext) _Mutation_createComposition(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_createComposition(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreateComposition(rctx, fc.Args["input"].(dto.CompositionInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*dto.Composition) + fc.Result = res + return ec.marshalNComposition2ᚖgithubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐComposition(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_createComposition(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Composition_id(ctx, field) + case "text": + return ec.fieldContext_Composition_text(ctx, field) + case "type": + return ec.fieldContext_Composition_type(ctx, field) + case "category": + return ec.fieldContext_Composition_category(ctx, field) + case "status": + return ec.fieldContext_Composition_status(ctx, field) + case "date": + return ec.fieldContext_Composition_date(ctx, field) + case "patientID": + return ec.fieldContext_Composition_patientID(ctx, field) + case "encounterID": + return ec.fieldContext_Composition_encounterID(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Composition", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_createComposition_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Observation_id(ctx context.Context, field graphql.CollectedField, obj *dto.Observation) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Observation_id(ctx, field) if err != nil { @@ -12963,6 +13514,71 @@ func (ec *executionContext) unmarshalInputAllergyInput(ctx context.Context, obj return it, nil } +func (ec *executionContext) unmarshalInputCompositionInput(ctx context.Context, obj interface{}) (dto.CompositionInput, error) { + var it dto.CompositionInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"type", "status", "category", "encounterID", "note"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "type": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("type")) + data, err := ec.unmarshalNCompositionType2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐCompositionType(ctx, v) + if err != nil { + return it, err + } + it.Type = data + case "status": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("status")) + data, err := ec.unmarshalNCompositionStatusEnum2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐCompositionStatusEnum(ctx, v) + if err != nil { + return it, err + } + it.Status = data + case "category": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("category")) + data, err := ec.unmarshalNCompositionCategory2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐCompositionCategory(ctx, v) + if err != nil { + return it, err + } + it.Category = data + case "encounterID": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("encounterID")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.EncounterID = data + case "note": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("note")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Note = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputConditionInput(ctx context.Context, obj interface{}) (dto.ConditionInput, error) { var it dto.ConditionInput asMap := map[string]interface{}{} @@ -13692,6 +14308,71 @@ func (ec *executionContext) _AllergyEdge(ctx context.Context, sel ast.SelectionS return out } +var compositionImplementors = []string{"Composition"} + +func (ec *executionContext) _Composition(ctx context.Context, sel ast.SelectionSet, obj *dto.Composition) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, compositionImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Composition") + case "id": + out.Values[i] = ec._Composition_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "text": + out.Values[i] = ec._Composition_text(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "type": + out.Values[i] = ec._Composition_type(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "category": + out.Values[i] = ec._Composition_category(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "status": + out.Values[i] = ec._Composition_status(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "date": + out.Values[i] = ec._Composition_date(ctx, field, obj) + case "patientID": + out.Values[i] = ec._Composition_patientID(ctx, field, obj) + case "encounterID": + out.Values[i] = ec._Composition_encounterID(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var conditionImplementors = []string{"Condition"} func (ec *executionContext) _Condition(ctx context.Context, sel ast.SelectionSet, obj *dto.Condition) graphql.Marshaler { @@ -14495,6 +15176,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_createAllergyIntolerance(ctx, field) }) + case "createComposition": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_createComposition(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -15880,6 +16568,73 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se return res } +func (ec *executionContext) marshalNComposition2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐComposition(ctx context.Context, sel ast.SelectionSet, v dto.Composition) graphql.Marshaler { + return ec._Composition(ctx, sel, &v) +} + +func (ec *executionContext) marshalNComposition2ᚖgithubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐComposition(ctx context.Context, sel ast.SelectionSet, v *dto.Composition) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._Composition(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNCompositionCategory2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐCompositionCategory(ctx context.Context, v interface{}) (dto.CompositionCategory, error) { + tmp, err := graphql.UnmarshalString(v) + res := dto.CompositionCategory(tmp) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNCompositionCategory2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐCompositionCategory(ctx context.Context, sel ast.SelectionSet, v dto.CompositionCategory) graphql.Marshaler { + res := graphql.MarshalString(string(v)) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) unmarshalNCompositionInput2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐCompositionInput(ctx context.Context, v interface{}) (dto.CompositionInput, error) { + res, err := ec.unmarshalInputCompositionInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) unmarshalNCompositionStatusEnum2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐCompositionStatusEnum(ctx context.Context, v interface{}) (dto.CompositionStatusEnum, error) { + tmp, err := graphql.UnmarshalString(v) + res := dto.CompositionStatusEnum(tmp) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNCompositionStatusEnum2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐCompositionStatusEnum(ctx context.Context, sel ast.SelectionSet, v dto.CompositionStatusEnum) graphql.Marshaler { + res := graphql.MarshalString(string(v)) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) unmarshalNCompositionType2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐCompositionType(ctx context.Context, v interface{}) (dto.CompositionType, error) { + tmp, err := graphql.UnmarshalString(v) + res := dto.CompositionType(tmp) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNCompositionType2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐCompositionType(ctx context.Context, sel ast.SelectionSet, v dto.CompositionType) graphql.Marshaler { + res := graphql.MarshalString(string(v)) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + func (ec *executionContext) marshalNCondition2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐCondition(ctx context.Context, sel ast.SelectionSet, v dto.Condition) graphql.Marshaler { return ec._Condition(ctx, sel, &v) } diff --git a/pkg/clinical/presentation/graph/inputs.graphql b/pkg/clinical/presentation/graph/inputs.graphql index 2f7a1c9b..6576950d 100644 --- a/pkg/clinical/presentation/graph/inputs.graphql +++ b/pkg/clinical/presentation/graph/inputs.graphql @@ -66,6 +66,14 @@ input AllergyInput { reaction: ReactionInput } +input CompositionInput { + type: CompositionType! + status: CompositionStatusEnum! + category: CompositionCategory! + encounterID: String! + note: String! +} + input ReactionInput { code: String system: String diff --git a/pkg/clinical/presentation/graph/types.graphql b/pkg/clinical/presentation/graph/types.graphql index 2373e9f0..52d18c95 100644 --- a/pkg/clinical/presentation/graph/types.graphql +++ b/pkg/clinical/presentation/graph/types.graphql @@ -188,3 +188,15 @@ type MediaConnection { edges: [MediaEdge] pageInfo: PageInfo } + +type Composition { + id: String! + text: String! + type: CompositionType! + category: CompositionCategory! + status: CompositionStatusEnum! + date: Date + + patientID: String + encounterID: String +} diff --git a/pkg/clinical/presentation/rest/auth_middleware.go b/pkg/clinical/presentation/rest/auth_middleware.go index 330af30f..c3294dd7 100644 --- a/pkg/clinical/presentation/rest/auth_middleware.go +++ b/pkg/clinical/presentation/rest/auth_middleware.go @@ -96,7 +96,11 @@ type authCheckFn = func( r *http.Request, ) (bool, map[string]string, *authutils.TokenIntrospectionResponse) -// HasValidCachedToken ... +// HasValidCachedToken returns an authentication check function for verifying +// the validity of a token stored in the provided cache store. +// +// Parameters: +// - cacheStore (persist.CacheStore): The cache store used for storing tokens. func HasValidCachedToken(cacheStore persist.CacheStore) authCheckFn { return func(_ context.Context, r *http.Request) (bool, map[string]string, *authutils.TokenIntrospectionResponse) { token, err := firebasetools.ExtractBearerToken(r) @@ -138,7 +142,7 @@ func AuthenticationGinMiddleware(cacheStore persist.CacheStore, cl authutils.Cli tokenResponse = authToken // myCareHub doesn't set expires in - // TODO: Set expires at iin myCareHub introspection + // TODO: Set expires at in myCareHub introspection if !tokenResponse.Expires.IsZero() { err := cacheStore.Set(tokenResponse.Token, *authToken, time.Until(tokenResponse.Expires)) if err != nil { diff --git a/pkg/clinical/presentation/rest/auth_middleware_test.go b/pkg/clinical/presentation/rest/auth_middleware_test.go new file mode 100644 index 00000000..12105c10 --- /dev/null +++ b/pkg/clinical/presentation/rest/auth_middleware_test.go @@ -0,0 +1,94 @@ +package rest + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/chenyahui/gin-cache/persist" + "github.com/savannahghi/authutils" +) + +func TestHasValidCachedToken(t *testing.T) { + type args struct { + cacheStore persist.CacheStore + } + tests := []struct { + name string + args args + }{ + { + name: "happy case: generate cached token", + args: args{ + cacheStore: persist.NewMemoryStore(1 * time.Second), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := HasValidCachedToken(tt.args.cacheStore); got == nil { + t.Errorf("HasValidCachedToken() is nil") + } + }) + } +} + +func TestCachedToken(t *testing.T) { + cacheStore := persist.NewMemoryStore(1 * time.Second) + + testRequestWithStoredToken := httptest.NewRequest(http.MethodGet, "/api/v1/", nil) + testRequestWithStoredToken.Header.Set("Authorization", "Bearer xyz") + cacheStore.Set("xyz", authutils.TokenIntrospectionResponse{}, time.Until(time.Now().Add(1*time.Hour))) + + testRequestWithToken := httptest.NewRequest(http.MethodGet, "/api/v1/", nil) + testRequestWithToken.Header.Set("Authorization", "Bearer zxy") + + testRequestNoToken := httptest.NewRequest(http.MethodGet, "/api/v1/", nil) + + type args struct { + ctx context.Context + r *http.Request + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "happy case: cached token", + args: args{ + ctx: nil, + r: testRequestWithStoredToken, + }, + want: true, + }, + { + name: "sad case: missing bearer token in request", + args: args{ + ctx: nil, + r: testRequestNoToken, + }, + want: false, + }, + { + name: "sad case: missing token in cache", + args: args{ + ctx: nil, + r: testRequestWithToken, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + middleware := HasValidCachedToken(cacheStore) + + got, _, _ := middleware(tt.args.ctx, tt.args.r) + if got != tt.want { + t.Errorf("CachedToken() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/clinical/usecases/clinical/composition.go b/pkg/clinical/usecases/clinical/composition.go new file mode 100644 index 00000000..6077ed6f --- /dev/null +++ b/pkg/clinical/usecases/clinical/composition.go @@ -0,0 +1,165 @@ +package clinical + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/google/uuid" + "github.com/savannahghi/clinical/pkg/clinical/application/common" + "github.com/savannahghi/clinical/pkg/clinical/application/dto" + "github.com/savannahghi/clinical/pkg/clinical/domain" + "github.com/savannahghi/scalarutils" +) + +// CreateComposition creates a new composition +func (c *UseCasesClinicalImpl) CreateComposition(ctx context.Context, input dto.CompositionInput) (*dto.Composition, error) { + encounter, err := c.infrastructure.FHIR.GetFHIREncounter(ctx, input.EncounterID) + if err != nil { + return nil, err + } + + // check encounter status + if encounter.Resource.Status == domain.EncounterStatusEnumFinished { + return nil, fmt.Errorf("cannot record a composition in a finished encounter") + } + + // get patient from encounter + patient, err := c.infrastructure.FHIR.GetFHIRPatient(ctx, *encounter.Resource.Subject.ID) + if err != nil { + return nil, err + } + + identifiers, err := c.infrastructure.BaseExtension.GetTenantIdentifiers(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get tenant identifiers from context: %w", err) + } + + patientRef := fmt.Sprintf("Patient/%s", *patient.Resource.ID) + patientType := "Patient" + compositionTitle := fmt.Sprintf("%s's assessment note", patient.Resource.Name[0].Text) + compositionSectionTextStatus := "generated" + + encounterRef := fmt.Sprintf("Encounter/%s", *encounter.Resource.ID) + encounterType := scalarutils.URI("Encounter") + + organizationRef := fmt.Sprintf("Organization/%s", identifiers.OrganizationID) + + today := time.Now() + + date, err := scalarutils.NewDate(today.Day(), int(today.Month()), today.Year()) + if err != nil { + return nil, err + } + + id := uuid.New().String() + + compositionCategoryConcept, err := c.GetConcept(ctx, dto.TerminologySourceLOINC, common.LOINCAssessmentPlanCode) + if err != nil { + return nil, err + } + + compositionTypeConcept, err := c.GetConcept(ctx, dto.TerminologySourceLOINC, common.LOINCProgressNoteCode) + if err != nil { + return nil, err + } + + status := strings.ToLower(string(input.Status)) + + compositionInput := domain.FHIRCompositionInput{ + ID: &id, + Status: (*domain.CompositionStatusEnum)(&status), + Type: &domain.FHIRCodeableConceptInput{ + Coding: []*domain.FHIRCodingInput{ + { + System: (*scalarutils.URI)(&compositionTypeConcept.URL), + Code: scalarutils.Code(compositionTypeConcept.ID), + Display: compositionTypeConcept.DisplayName, + }, + }, + Text: compositionTypeConcept.DisplayName, + }, + Category: []*domain.FHIRCodeableConceptInput{ + { + ID: &id, + Coding: []*domain.FHIRCodingInput{ + { + System: (*scalarutils.URI)(&compositionCategoryConcept.URL), + Code: scalarutils.Code(compositionCategoryConcept.ID), + Display: compositionCategoryConcept.DisplayName, + }, + }, + Text: compositionCategoryConcept.DisplayName, + }, + }, + Subject: &domain.FHIRReferenceInput{ + ID: patient.Resource.ID, + Reference: &patientRef, + Type: (*scalarutils.URI)(&patientType), + }, + Encounter: &domain.FHIRReferenceInput{ + ID: encounter.Resource.ID, + Reference: &encounterRef, + Display: *encounter.Resource.ID, + Type: &encounterType, + }, + Date: date, + Author: []*domain.FHIRReferenceInput{ + { + Reference: &organizationRef, + }, + }, + Title: &compositionTitle, + Section: []*domain.FHIRCompositionSectionInput{ + { + ID: &id, + Title: &compositionCategoryConcept.DisplayName, + Code: &domain.FHIRCodeableConceptInput{ + ID: new(string), + Coding: []*domain.FHIRCodingInput{ + { + ID: &id, + System: (*scalarutils.URI)(&compositionCategoryConcept.URL), + Code: scalarutils.Code(compositionCategoryConcept.ID), + Display: compositionCategoryConcept.DisplayName, + }, + }, + Text: compositionTypeConcept.DisplayName, + }, + Author: []*domain.FHIRReferenceInput{ + { + Reference: &organizationRef, + }, + }, + Text: &domain.FHIRNarrativeInput{ + ID: new(string), + Status: (*domain.NarrativeStatusEnum)(&compositionSectionTextStatus), + Div: scalarutils.XHTML(input.Note), + }, + }, + }, + } + + composition, err := c.infrastructure.FHIR.CreateFHIRComposition(ctx, compositionInput) + if err != nil { + return nil, err + } + + return mapFHIRCompositionToCompositionDTO(*composition.Resource), nil +} + +func mapFHIRCompositionToCompositionDTO(composition domain.FHIRComposition) *dto.Composition { + output := dto.Composition{ + ID: *composition.ID, + Text: string(composition.Section[0].Text.Div), + Type: dto.CompositionType(composition.Type.Text), + Category: dto.CompositionCategory(composition.Category[0].Text), + Status: dto.CompositionStatusEnum(*composition.Status), + PatientID: *composition.Subject.ID, + EncounterID: *composition.Encounter.ID, + Date: composition.Date, + } + + return &output +} diff --git a/pkg/clinical/usecases/clinical/composition_test.go b/pkg/clinical/usecases/clinical/composition_test.go new file mode 100644 index 00000000..e4f6fb12 --- /dev/null +++ b/pkg/clinical/usecases/clinical/composition_test.go @@ -0,0 +1,302 @@ +package clinical_test + +import ( + "context" + "fmt" + "testing" + + "github.com/brianvoe/gofakeit" + "github.com/google/uuid" + "github.com/savannahghi/clinical/pkg/clinical/application/common" + "github.com/savannahghi/clinical/pkg/clinical/application/dto" + fakeExtMock "github.com/savannahghi/clinical/pkg/clinical/application/extensions/mock" + "github.com/savannahghi/clinical/pkg/clinical/domain" + "github.com/savannahghi/clinical/pkg/clinical/infrastructure" + fakeFHIRMock "github.com/savannahghi/clinical/pkg/clinical/infrastructure/datastore/cloudhealthcare/mock" + fakeOCLMock "github.com/savannahghi/clinical/pkg/clinical/infrastructure/services/openconceptlab/mock" + fakePubSubMock "github.com/savannahghi/clinical/pkg/clinical/infrastructure/services/pubsub/mock" + fakeUploadMock "github.com/savannahghi/clinical/pkg/clinical/infrastructure/services/upload/mock" + clinicalUsecase "github.com/savannahghi/clinical/pkg/clinical/usecases/clinical" + "github.com/savannahghi/scalarutils" +) + +func TestUseCasesClinicalImpl_CreateComposition(t *testing.T) { + type args struct { + ctx context.Context + input dto.CompositionInput + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "happy case: create composition", + args: args{ + ctx: context.Background(), + input: dto.CompositionInput{ + EncounterID: gofakeit.UUID(), + Type: dto.ProgressNote, + Category: dto.AssessmentAndPlan, + Status: "final", + Note: "Patient is deteriorating", + }, + }, + wantErr: false, + }, + { + name: "sad case: error fetching concept", + args: args{ + ctx: context.Background(), + input: dto.CompositionInput{ + EncounterID: gofakeit.UUID(), + Type: dto.ProgressNote, + Category: dto.AssessmentAndPlan, + Status: "final", + Note: "Patient is deteriorating", + }, + }, + wantErr: true, + }, + { + name: "sad case: fail to get encounter", + args: args{ + ctx: context.Background(), + input: dto.CompositionInput{ + EncounterID: gofakeit.UUID(), + Type: dto.ProgressNote, + Category: dto.AssessmentAndPlan, + Status: "final", + Note: "Patient is deteriorating", + }, + }, + wantErr: true, + }, + { + name: "sad case: fail to get patient", + args: args{ + ctx: context.Background(), + input: dto.CompositionInput{ + EncounterID: gofakeit.UUID(), + Type: dto.ProgressNote, + Category: dto.AssessmentAndPlan, + Status: "final", + Note: "Patient is deteriorating", + }, + }, + wantErr: true, + }, + { + name: "sad case: fail to get identifiers", + args: args{ + ctx: context.Background(), + input: dto.CompositionInput{ + EncounterID: gofakeit.UUID(), + Type: dto.ProgressNote, + Category: dto.AssessmentAndPlan, + Status: "final", + Note: "Patient is deteriorating", + }, + }, + wantErr: true, + }, + { + name: "sad case: fail on finished encounter", + args: args{ + ctx: context.Background(), + input: dto.CompositionInput{ + EncounterID: gofakeit.UUID(), + Type: dto.ProgressNote, + Category: dto.AssessmentAndPlan, + Status: "final", + Note: "Patient is deteriorating", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeExt := fakeExtMock.NewFakeBaseExtensionMock() + fakeFHIR := fakeFHIRMock.NewFHIRMock() + fakeOCL := fakeOCLMock.NewFakeOCLMock() + fakePubSub := fakePubSubMock.NewPubSubServiceMock() + + fakeUpload := fakeUploadMock.NewFakeUploadMock() + + infra := infrastructure.NewInfrastructureInteractor(fakeExt, fakeFHIR, fakeOCL, fakeUpload, fakePubSub) + c := clinicalUsecase.NewUseCasesClinicalImpl(infra) + + if tt.name == "happy case: get encounter" { + UUID := uuid.New().String() + compositionTitle := gofakeit.Name() + "assessment note" + typeSystem := scalarutils.URI("http://hl7.org/fhir/ValueSet/doc-typecodes") + categorySystem := scalarutils.URI("http://hl7.org/fhir/ValueSet/referenced-item-category") + category := "Assessment + plan" + compositionType := "Progress note" + treatmentPlan := "Treatment Plan" + compositionStatus := "active" + note := scalarutils.Markdown("Fever Fever") + PatientRef := "Patient/" + uuid.NewString() + patientType := "Patient" + organizationRef := "Organization/" + uuid.NewString() + compositionSectionTextStatus := "generated" + + fakeFHIR.MockCreateFHIRCompositionFn = func(ctx context.Context, input domain.FHIRCompositionInput) (*domain.FHIRCompositionRelayPayload, error) { + return &domain.FHIRCompositionRelayPayload{ + Resource: &domain.FHIRComposition{ + ID: &UUID, + Text: &domain.FHIRNarrative{}, + Identifier: &domain.FHIRIdentifier{}, + Status: (*domain.CompositionStatusEnum)(&compositionStatus), + Type: &domain.FHIRCodeableConcept{ + ID: new(string), + Coding: []*domain.FHIRCoding{ + { + ID: &UUID, + System: &typeSystem, + Code: scalarutils.Code(string(common.LOINCProgressNoteCode)), + Display: compositionType, + }, + }, + Text: "Progress note", + }, + Category: []*domain.FHIRCodeableConcept{ + { + ID: new(string), + Coding: []*domain.FHIRCoding{ + { + ID: &UUID, + System: &categorySystem, + Version: new(string), + Code: scalarutils.Code(string(common.LOINCAssessmentPlanCode)), + Display: category, + }, + }, + Text: "Assessment + plan", + }, + }, + Subject: &domain.FHIRReference{ + ID: &UUID, + Reference: &PatientRef, + Type: (*scalarutils.URI)(&patientType), + }, + Encounter: &domain.FHIRReference{ + ID: &UUID, + }, + Date: &scalarutils.Date{ + Year: 2023, + Month: 9, + Day: 25, + }, + Author: []*domain.FHIRReference{ + { + Reference: &organizationRef, + }, + }, + Title: &compositionTitle, + Section: []*domain.FHIRCompositionSection{ + { + ID: &UUID, + Title: &treatmentPlan, + Code: &domain.FHIRCodeableConceptInput{ + ID: new(string), + Coding: []*domain.FHIRCodingInput{ + { + ID: &UUID, + System: &categorySystem, + Version: new(string), + Code: scalarutils.Code(string(common.LOINCAssessmentPlanCode)), + Display: category, + }, + }, + Text: "Assessment + plan", + }, + Author: []*domain.FHIRReference{ + { + Reference: new(string), + }, + }, + Text: &domain.FHIRNarrative{ + ID: &UUID, + Status: (*domain.NarrativeStatusEnum)(&compositionSectionTextStatus), + Div: scalarutils.XHTML(note), + }, + }, + }, + }, + }, nil + } + } + + if tt.name == "sad case: error fetching concept" { + fakeOCL.MockGetConceptFn = func(ctx context.Context, org string, source string, concept string, includeMappings bool, includeInverseMappings bool) (*domain.Concept, error) { + return nil, fmt.Errorf("failed to get concept") + } + } + + if tt.name == "sad case: fail on finished encounter" { + fakeFHIR.MockGetFHIREncounterFn = func(ctx context.Context, id string) (*domain.FHIREncounterRelayPayload, error) { + UUID := uuid.New().String() + PatientRef := "Patient/" + uuid.NewString() + + return &domain.FHIREncounterRelayPayload{ + Resource: &domain.FHIREncounter{ + ID: &UUID, + Text: &domain.FHIRNarrative{}, + Identifier: []*domain.FHIRIdentifier{}, + Status: domain.EncounterStatusEnum(domain.EncounterStatusEnumFinished), + StatusHistory: []*domain.FHIREncounterStatushistory{}, + Class: domain.FHIRCoding{}, + ClassHistory: []*domain.FHIREncounterClasshistory{}, + Type: []*domain.FHIRCodeableConcept{}, + ServiceType: &domain.FHIRCodeableConcept{}, + Priority: &domain.FHIRCodeableConcept{}, + Subject: &domain.FHIRReference{ + ID: &UUID, + Reference: &PatientRef, + }, + EpisodeOfCare: []*domain.FHIRReference{}, + BasedOn: []*domain.FHIRReference{}, + Participant: []*domain.FHIREncounterParticipant{}, + Appointment: []*domain.FHIRReference{}, + Period: &domain.FHIRPeriod{}, + Length: &domain.FHIRDuration{}, + ReasonReference: []*domain.FHIRReference{}, + Diagnosis: []*domain.FHIREncounterDiagnosis{}, + Account: []*domain.FHIRReference{}, + Hospitalization: &domain.FHIREncounterHospitalization{}, + Location: []*domain.FHIREncounterLocation{}, + ServiceProvider: &domain.FHIRReference{}, + PartOf: &domain.FHIRReference{}, + }, + }, nil + } + } + + if tt.name == "sad case: fail to get encounter" { + fakeFHIR.MockGetFHIREncounterFn = func(ctx context.Context, id string) (*domain.FHIREncounterRelayPayload, error) { + return nil, fmt.Errorf("failed to get concept") + } + } + + if tt.name == "sad case: fail to get patient" { + fakeFHIR.MockGetFHIRPatientFn = func(ctx context.Context, id string) (*domain.FHIRPatientRelayPayload, error) { + return nil, fmt.Errorf("failed to get patient") + } + } + + if tt.name == "sad case: fail to get identifiers" { + fakeExt.MockGetTenantIdentifiersFn = func(ctx context.Context) (*dto.TenantIdentifiers, error) { + return nil, fmt.Errorf("failed to get identifiers") + } + } + + _, err := c.CreateComposition(tt.args.ctx, tt.args.input) + if (err != nil) != tt.wantErr { + t.Errorf("UseCasesClinicalImpl.CreateComposition() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} diff --git a/pkg/clinical/usecases/usecases.go b/pkg/clinical/usecases/usecases.go index 786228d9..9292302f 100644 --- a/pkg/clinical/usecases/usecases.go +++ b/pkg/clinical/usecases/usecases.go @@ -79,6 +79,7 @@ type Clinical interface { UploadMedia(ctx context.Context, encounterID string, file io.Reader, contentType string) (*dto.Media, error) ListPatientMedia(ctx context.Context, patientID string, pagination dto.Pagination) (*dto.MediaConnection, error) + CreateComposition(ctx context.Context, input dto.CompositionInput) (*dto.Composition, error) } // Interactor is an implementation of the usecases interface