Skip to content

Commit db02e1e

Browse files
BCDA-9021: Add _typeFilter Param to Demo (#1143)
## 🎫 Ticket https://jira.cms.gov/browse/BCDA-9021 ## πŸ›  Changes This PR: * updates the BCDA `/demo` API to point to BFD V2. (temporary until v3 is released) * adds the _typeFilter parameter for the `/demo` API. (while we are pointing at BFD v2, only the service-date subquery on the EOB resource will work) ## ℹ️ Context For Connectathon, we want to get a demo api up and running for people to test against. This way we can preview BFD V3 and the _typeFilter parameter ## πŸ§ͺ Validation ### Test 1: /demo with _typefilter Make an $export request to the `/demo` API and include the _typeFilter parameter for service-date (example: `/api/demo/Patient/$export?_typeFilter=ExplanationOfBenefit%3Fservice-date%3Dgt2001-07-01`) - When you pull the EOB file, make sure only EOBs from the specified date range are included (ex above: after July 1, 2001) ### Test 2: demo without _typefilter Make an $export request to the `/demo` API and do not include any _typeFilter param: - When you pull the EOB file, there should not be any extra filtering ### Test 3: demo with invalid _typefilter Make an $export request to the `/demo` API and include an invalid _typeFilter parameter - ([examples of bad query parameters](https://github.com/CMSgov/bcda-app/blob/2b8a9aace01f9f563ab0ef3c3a7a3a521c60fdc9/bcda/web/middleware/validation_test.go#L62-L67)): - `_typeFilter=MedicationRequest%%3Fstatus%%3Dactive` - invalid resource type - `_typeFilter=ExplanationOfBenefit%%3Fservice-dateactive` - invalid parameter/value - ` _typeFilter=ExplanationOfBenefit%%3Fstatus%%3Dactive` - invalid subquery parameter - When you make the $export request, you should see an OperationOutcome response with an appropriate error ### Test 4: v2 with (ignored) _typefilter [*regression*] Make an $export request to the `/v2` API and include the _typeFilter parameter for service-date (example: `/api/demo/Patient/$export?_typeFilter=ExplanationOfBenefit%3Fservice-date%3Dgt2001-07-01`) - When you pull the EOB file, there should not be any extra filtering
1 parent dcdc186 commit db02e1e

File tree

11 files changed

+102
-24
lines changed

11 files changed

+102
-24
lines changed

β€Žbcda/api/requests.goβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,7 @@ func (h *Handler) bulkRequest(w http.ResponseWriter, r *http.Request, reqType co
582582
ComplexDataRequestType: complexDataRequestType,
583583
ResourceTypes: resourceTypes,
584584
Since: rp.Since,
585+
TypeFilter: rp.TypeFilter,
585586
CreationTime: time.Now(),
586587
ClaimsDate: timeConstraints.ClaimsDate,
587588
OptOutDate: timeConstraints.OptOutDate,

β€Žbcda/api/v3/api.goβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ func Metadata(w http.ResponseWriter, r *http.Request) {
291291
Publisher: &fhirdatatypes.String{Value: "Centers for Medicare & Medicaid Services"},
292292
Kind: &fhircapabilitystatement.CapabilityStatement_KindCode{Value: fhircodes.CapabilityStatementKindCode_INSTANCE},
293293
Instantiates: []*fhirdatatypes.Canonical{
294-
{Value: bbServer + "/v3/fhir/metadata"},
294+
{Value: bbServer + constants.BFDV3Path + "/metadata"},
295295
{Value: "http://hl7.org/fhir/uv/bulkdata/CapabilityStatement/bulk-data"},
296296
},
297297
Software: &fhircapabilitystatement.CapabilityStatement_Software{

β€Žbcda/client/bluebutton.goβ€Ž

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ func (bbc *BlueButtonClient) GetExplanationOfBenefit(jobData worker_types.JobEnq
226226

227227
updateParamWithServiceDate(&params, claimsWindow)
228228
updateParamWithLastUpdated(&params, jobData.Since, jobData.TransactionTime)
229+
updateParamWithTypeFilter(&params, jobData.TypeFilter)
229230

230231
u, err := bbc.getURL("ExplanationOfBenefit", params)
231232
if err != nil {
@@ -423,6 +424,12 @@ func updateParamWithLastUpdated(params *url.Values, since string, transactionTim
423424
}
424425
}
425426

427+
func updateParamWithTypeFilter(params *url.Values, typeFilter [][]string) {
428+
for _, paramPair := range typeFilter {
429+
params.Add(paramPair[0], paramPair[1])
430+
}
431+
}
432+
426433
func updateParamsWithClaimsDefaults(params *url.Values, mbi string) {
427434
params.Set("excludeSAMHSA", "true")
428435
params.Set("includeTaxNumbers", "true")

β€Žbcda/client/bluebutton_test.goβ€Ž

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ func (s *BBRequestTestSuite) TearDownAllSuite() {
365365
func (s *BBRequestTestSuite) TestValidateRequest() {
366366
old := conf.GetEnv("BB_CLIENT_PAGE_SIZE")
367367
jobDataNoSince := worker_types.JobEnqueueArgs{ID: 1, CMSID: "A0000", Since: "", TransactionTime: now}
368+
jobDataWithTypeFilter := worker_types.JobEnqueueArgs{ID: 1, CMSID: "A0000", Since: "gt2020-02-14", TypeFilter: [][]string{{"service-date", "gt2022-06-26"}}, TransactionTime: now}
368369
defer conf.SetEnv(s.T(), "BB_CLIENT_PAGE_SIZE", old)
369370
conf.SetEnv(s.T(), "BB_CLIENT_PAGE_SIZE", "0") // Need to ensure that requests do not have the _count parameter
370371

@@ -743,6 +744,25 @@ func (s *BBRequestTestSuite) TestValidateRequest() {
743744
hasClaimRequiredURLEncodedBody,
744745
},
745746
},
747+
{
748+
"GetExplanationOfBenefitWithTypeFilterServiceDate",
749+
func(bbClient *client.BlueButtonClient) (interface{}, error) {
750+
return bbClient.GetExplanationOfBenefit(jobDataWithTypeFilter, "patient1", client.ClaimsWindow{})
751+
},
752+
func(t *testing.T, payload interface{}) {
753+
result, ok := payload.(*fhirModels.Bundle)
754+
assert.True(t, ok)
755+
assert.NotEmpty(t, result.Entries)
756+
},
757+
[]func(*testing.T, *http.Request){
758+
sinceChecker,
759+
nowChecker,
760+
excludeSAMHSAChecker,
761+
ServiceDateChecker,
762+
hasDefaultRequestHeaders,
763+
hasBulkRequestHeaders,
764+
},
765+
},
746766
}
747767

748768
for _, tt := range tests {
@@ -860,6 +880,9 @@ func nowChecker(t *testing.T, req *http.Request) {
860880
func noServiceDateChecker(t *testing.T, req *http.Request) {
861881
assert.Empty(t, req.URL.Query()[constants.TestSvcDate])
862882
}
883+
func ServiceDateChecker(t *testing.T, req *http.Request) {
884+
assert.Contains(t, req.URL.String(), "service-date=gt2022-06-26")
885+
}
863886
func serviceDateUpperBoundChecker(t *testing.T, req *http.Request) {
864887
// We expect that service date only contains YYYY-MM-DD
865888
assert.Contains(t, req.URL.Query()[constants.TestSvcDate], fmt.Sprintf("le%s", claimsDate.UpperBound.Format(constants.TestSvcDateResult)))

β€Žbcda/constants/constants.goβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const CCLF8FileNum = int(8)
4949

5050
const BFDV1Path = "/v1/fhir"
5151
const BFDV2Path = "/v2/fhir"
52-
const BFDV3Path = "/v3/fhir"
52+
const BFDV3Path = "/v2/fhir" // TODO: V3
5353
const V3Version = "demo"
5454

5555
const GetExistingBenes = "GetExistingBenes"

β€Žbcda/service/service.goβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ func (s *service) createQueueJobs(ctx context.Context, args worker_types.Prepare
320320
BeneficiaryIDs: jobIDs,
321321
ResourceType: rt,
322322
Since: sinceArg,
323+
TypeFilter: args.TypeFilter,
323324
TransactionID: ctx.Value(middleware.CtxTransactionKey).(string),
324325
TransactionTime: transactionTime,
325326
BBBasePath: args.BFDPath,

β€Žbcda/web/middleware/validation.goβ€Ž

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"context"
55
"fmt"
66
"net/http"
7+
"net/url"
78
"regexp"
9+
"slices"
810
"strings"
911
"time"
1012

@@ -24,7 +26,7 @@ type RequestParameters struct {
2426
ResourceTypes []string
2527
Version string // e.g. v1, v2
2628
RequestURL string
27-
// TypeFilter string
29+
TypeFilter [][]string
2830
}
2931

3032
// const BBSystemURL = "https://bluebutton.cms.gov/fhir/CodeSystem/Adjudication-Status"
@@ -126,26 +128,60 @@ func ValidateRequestURL(next http.Handler) http.Handler {
126128
rp.ResourceTypes = resourceTypes
127129
}
128130

129-
// TODO: V3 _typeFilter
130-
// params, ok = r.URL.Query()["_typeFilter"]
131-
// if ok {
132-
// allowedQueryParams := []string{
133-
// "ExplanationOfBenefit?",
134-
// "ExplanationOfBenefit?tag=PartiallyAdjudicated",
135-
// "ExplanationOfBenefit?_tag=Adjudicated",
136-
// "ExplanationOfBenefit?_tag=PartiallyAdjudicated,Adjudicated",
137-
// "ExplanationOfBenefit?_source=FISS",
138-
// "ExplanationOfBenefit?_source=MCS",
139-
// }
140-
// if utils.ContainsString(allowedQueryParams, params[0]) {
141-
// rp.TypeFilter = params[0]
142-
// } else {
143-
// errMsg := fmt.Sprintf("invalid _typeFilter (or currently unimplemented query) %s", params[0])
144-
// log.API.Error(errMsg)
145-
// rw.Exception(r.Context(), w, http.StatusBadRequest, responseutils.RequestErr, errMsg)
146-
// return
147-
// }
148-
// }
131+
// validate _typeFilter params
132+
params, ok = r.URL.Query()["_typeFilter"]
133+
if (version == "demo") && ok {
134+
var typeFilterParams [][]string
135+
for _, subQuery := range params {
136+
137+
// The subquery is url-encoded. So we will first decode so we can parse it
138+
decodedQuery, err := url.QueryUnescape(subQuery)
139+
if err != nil {
140+
errMsg := fmt.Sprintf("failed to unescape %s", subQuery)
141+
log.API.Error(errMsg)
142+
rw.Exception(r.Context(), w, http.StatusBadRequest, responseutils.RequestErr, errMsg)
143+
return
144+
}
145+
146+
// Expected format is: <resourceType>?<paramList>
147+
resourceType, queryParams, ok := strings.Cut(decodedQuery, "?")
148+
if !ok {
149+
errMsg := fmt.Sprintf("missing question mark %s", decodedQuery)
150+
log.API.Error(errMsg)
151+
rw.Exception(r.Context(), w, http.StatusBadRequest, responseutils.RequestErr, errMsg)
152+
}
153+
154+
// Right now, we are only accepting ExplanationOfBenefit subqueries
155+
if resourceType != "ExplanationOfBenefit" {
156+
errMsg := fmt.Sprintf("Invalid _typeFilter Resource Type (Only EOBs valid): %s", resourceType)
157+
log.API.Error(errMsg)
158+
rw.Exception(r.Context(), w, http.StatusBadRequest, responseutils.RequestErr, errMsg)
159+
}
160+
161+
// Loop through the param list from the subquery
162+
paramAry := strings.Split(queryParams, "&")
163+
for _, paramPair := range paramAry {
164+
165+
paramName, paramValue, ok := strings.Cut(paramPair, "=")
166+
if !ok {
167+
errMsg := fmt.Sprintf("Invalid _typeFilter parameter/value: %s", paramPair)
168+
log.API.Error(errMsg)
169+
rw.Exception(r.Context(), w, http.StatusBadRequest, responseutils.RequestErr, errMsg)
170+
}
171+
172+
if slices.Contains([]string{"service-date", "_tag", "_profile"}, paramName) {
173+
typeFilterParams = append(typeFilterParams, []string{paramName, paramValue})
174+
} else {
175+
errMsg := fmt.Sprintf("Invalid _typeFilter subquery parameter: %s", paramName)
176+
log.API.Error(errMsg)
177+
rw.Exception(r.Context(), w, http.StatusBadRequest, responseutils.RequestErr, errMsg)
178+
return
179+
}
180+
}
181+
182+
rp.TypeFilter = typeFilterParams
183+
}
184+
}
149185

150186
ctx := SetRequestParamsCtx(r.Context(), rp)
151187

β€Žbcda/web/middleware/validation_test.goβ€Ž

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func TestValidRequestURL(t *testing.T) {
2727

2828
now := time.Now().Add(-24 * time.Hour).Round(time.Millisecond)
2929
req, err := http.NewRequest("GET",
30-
fmt.Sprintf("/api/v1/Patient/$export?_type=Patient&_since=%s&_outputFormat=ndjson",
30+
fmt.Sprintf("/api/v1/Patient/$export?_type=Patient&_since=%s&_outputFormat=ndjson&_typeFilter=ExplanationOfBenefit%%3Fservice-date%%3Dgt2001-04-01",
3131
now.Format(time.RFC3339Nano)),
3232
nil)
3333
assert.NoError(t, err)
@@ -46,6 +46,7 @@ func TestValidRequestURL(t *testing.T) {
4646
func TestInvalidRequestURL(t *testing.T) {
4747

4848
base := "/api/v1/Patient/$export?"
49+
baseV3 := constants.V3Path + "Patient/$export?"
4950
tests := []struct {
5051
name string
5152
url string
@@ -59,6 +60,12 @@ func TestInvalidRequestURL(t *testing.T) {
5960
"Date must be a date that has already passed"},
6061
{"repeatedType", fmt.Sprintf("%s_type=Patient,Patient", base), "Repeated resource type Patient"},
6162
{"noVersion", "/api/Patient$export", "cannot retrieve version"},
63+
{"invalidTypeFilterResourceType", fmt.Sprintf("%s_typeFilter=MedicationRequest%%3Fstatus%%3Dactive", baseV3),
64+
"Invalid _typeFilter Resource Type (Only EOBs valid): MedicationRequest"},
65+
{"invalidTypeFilterSubquery", fmt.Sprintf("%s_typeFilter=ExplanationOfBenefit%%3Fservice-dateactive", baseV3),
66+
"Invalid _typeFilter parameter/value: service-dateactive"},
67+
{"invalidTypeFilterSubqueryParam", fmt.Sprintf("%s_typeFilter=ExplanationOfBenefit%%3Fstatus%%3Dactive", baseV3),
68+
"Invalid _typeFilter subquery parameter: status"},
6269
}
6370

6471
for _, tt := range tests {

β€Žbcdaworker/queueing/worker_prepare.goβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ func (p *PrepareJobWorker) prepareExportJobs(ctx context.Context, args worker_ty
122122
ID: id,
123123
ACOID: args.Job.ACOID.String(),
124124
Since: args.Since.String(),
125+
TypeFilter: args.TypeFilter,
125126
TransactionTime: time.Now(),
126127
CMSID: args.CMSID,
127128
}

β€Žbcdaworker/queueing/worker_types/prepare_types.goβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type PrepareJobArgs struct {
2121
ComplexDataRequestType string
2222
ResourceTypes []string
2323
Since time.Time
24+
TypeFilter [][]string
2425
CreationTime time.Time
2526
ClaimsDate time.Time
2627
OptOutDate time.Time

0 commit comments

Comments
Β (0)