Files
stack/dev/vendor/bcda-app/bcda/api/v3/api.go
kert 7e53d9360a chore(bcda): vendor CMSgov/bcda-app + bcda.cms.gov v3 artefacts sparsely, flatten the v3 data dictionary into crosswalk seeds, weekly drift workflow
dev/vendor/MANIFEST.toml pins bcda-app@7decdcc (v3 handler + tests, request
validation middleware, OperationOutcome writer, v3 Postman collection, gzip
fixtures, sandbox config, LICENSE) and the bcda.cms.gov v3 data dictionary,
v2 dictionary and official v3 sample ndjson. vendor_bcda_app.py --verify |
--check | --sync (stdlib only); the bcda-vendor workflow runs --check weekly
and files an issue on drift — nothing auto-commits, a payload change upstream
is something a human reads before the flattener is re-pointed.

gen_bcda_v3_crosswalk.py writes dev/seeds/bcda_v3_data_dictionary.csv
(fhirPath, IDR sourceColumn, ccwMapping, cclfMapping per v3 field),
bcda_v3_cclf_crosswalk.csv (293 CCLF elements -> v3 field) and
bcda_v3_cclf_absent.csv (52 CCLF fields v3 does not carry); the CCLF sheet
has label/name swapped on data rows and is normalised. Tests assert the
committed seeds match the vendored workbook.

refs #1139
2026-10-25 13:08:05 -04:00

377 lines
11 KiB
Go

package v3
import (
"database/sql"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/CMSgov/bcda-app/bcda/api"
"github.com/CMSgov/bcda-app/bcda/constants"
"github.com/CMSgov/bcda-app/bcda/fhir"
"github.com/CMSgov/bcda-app/bcda/fhir/r4"
"github.com/CMSgov/bcda-app/bcda/service"
"github.com/CMSgov/bcda-app/bcda/servicemux"
"github.com/CMSgov/bcda-app/conf"
"github.com/CMSgov/bcda-app/log"
pgxv5Pool "github.com/jackc/pgx/v5/pgxpool"
)
type ApiV3 struct {
handler *api.Handler
db *sql.DB
}
func NewApiV3(db *sql.DB, pool *pgxv5Pool.Pool) *ApiV3 {
resources, ok := service.GetClaimTypesMap([]string{
"Patient",
"Coverage",
"ExplanationOfBenefit",
}...)
if !ok {
panic("Failed to configure resource DataTypes")
} else {
h := api.NewHandler(resources, constants.BFDV3Path, constants.V3Version, db, pool)
return &ApiV3{handler: h, db: db}
}
}
// NOTE: The below are all just copies of v3/api
/*
swagger:route GET /api/v3/Patient/$export bulkDatav3 bulkPatientRequestv3
Start FHIR R4 data export for all supported resource types.
Initiates a job to collect data from the Blue Button API for your ACO. Supported resource types are Patient, Coverage, and ExplanationOfBenefit.
Produces:
- application/fhir+json
Security:
bearer_token:
Responses:
202: BulkRequestResponse
400: badRequestResponse
401: invalidCredentials
429: tooManyRequestsResponse
500: errorResponse
*/
func (a ApiV3) BulkPatientRequest(w http.ResponseWriter, r *http.Request) {
a.handler.BulkPatientRequest(w, r)
}
/*
swagger:route GET /api/v3/Group/{groupId}/$export bulkDatav3 bulkGroupRequestv3
# Start FHIR R4 data export (for the specified group identifier) for all supported resource types
Initiates a job to collect data from the Blue Button API for your ACO. The only Group identifier supported by the system are `all` and `runout`.
The `all` identifier returns data for the group of all patients attributed to the requesting ACO. If used when specifying `_since`: all claims data which has been updated since the specified date will be returned for beneficiaries which have been attributed to the ACO since before the specified date; and all historical claims data will be returned for beneficiaries which have been newly attributed to the ACO since the specified date.
The `runout` identifier returns claims runouts data.
Produces:
- application/fhir+json
Security:
bearer_token:
Responses:
202: BulkRequestResponse
400: badRequestResponse
401: invalidCredentials
429: tooManyRequestsResponse
500: errorResponse
*/
func (a ApiV3) BulkGroupRequest(w http.ResponseWriter, r *http.Request) {
a.handler.BulkGroupRequest(w, r)
}
/*
swagger:route GET /api/v3/jobs/{jobId} jobv3 jobStatusv3
# Get job status
Returns the current status of an export job.
Produces:
- application/fhir+json
Schemes: http, https
Security:
bearer_token:
Responses:
202: jobStatusResponse
200: completedJobResponse
400: badRequestResponse
401: invalidCredentials
404: notFoundResponse
410: goneResponse
500: errorResponse
*/
func (a ApiV3) JobStatus(w http.ResponseWriter, r *http.Request) {
a.handler.JobStatus(w, r)
}
/*
swagger:route GET /api/v3/jobs jobv3 jobsStatusv3
# Get jobs statuses
Returns the current statuses of export jobs. Supported status types are Completed, Archived, Expired, Failed, FailedExpired,
In Progress, Pending, Cancelled, and CancelledExpired. If no status(s) is provided, all jobs will be returned.
Note on job status to fhir task resource status mapping:
Due to the fhir task status field having a smaller set of values, the following statuses will be set to different fhir values in the response
Archived, Expired -> Completed
FailedExpired -> Failed
Pending -> In Progress
CancelledExpired -> Cancelled
Though the status name has been remapped the response will still only contain jobs pertaining to the provided job status in the request.
Produces:
- application/fhir+json
Schemes: http, https
Security:
bearer_token:
Responses:
200: jobsStatusResponse
400: badRequestResponse
401: invalidCredentials
404: notFoundResponse
410: goneResponse
500: errorResponse
*/
func (a ApiV3) JobsStatus(w http.ResponseWriter, r *http.Request) {
a.handler.JobsStatus(w, r)
}
/*
swagger:route DELETE /api/v3/jobs/{jobId} jobv3 deleteJobv3
# Cancel a job
Cancels a currently running job.
Produces:
- application/fhir+json
Schemes: http, https
Security:
bearer_token:
Responses:
202: deleteJobResponse
400: badRequestResponse
401: invalidCredentials
404: notFoundResponse
410: goneResponse
500: errorResponse
*/
func (a ApiV3) DeleteJob(w http.ResponseWriter, r *http.Request) {
a.handler.DeleteJob(w, r)
}
/*
swagger:route GET /api/v3/attribution_status attributionStatusv3 attributionStatus
# Get attribution status
Returns the status of the latest ingestion for attribution and claims runout files. The response will contain the Type to identify which ingestion and a Timestamp for the last time it was updated.
Produces:
- application/json
Schemes: http, https
Security:
bearer_token:
Responses:
200: AttributionFileStatusResponse
404: notFoundResponse
*/
func (a ApiV3) AttributionStatus(w http.ResponseWriter, r *http.Request) {
a.handler.AttributionStatus(w, r)
}
/*
swagger:route GET /api/v3/metadata metadatav3 metadata
# Get metadata
Returns metadata about the API.
Produces:
- application/fhir+json
Schemes: http, https
Responses:
200: MetadataResponse
*/
func (a ApiV3) Metadata(w http.ResponseWriter, r *http.Request) {
dt := time.Now()
bbServer := conf.GetEnv("BB_SERVER_LOCATION")
scheme := "http"
if servicemux.IsHTTPS(r) {
scheme = "https"
}
baseURL := fmt.Sprintf("%s://%s", scheme, r.Host)
statement := &r4.CapabilityStatement{
ResourceType: "CapabilityStatement",
Status: r4.PublicationStatusActive,
Date: dt.UTC().Format("2006-01-02T15:04:05Z"),
Publisher: constants.PublisherName,
Kind: r4.CapabilityStatementKindInstance,
Instantiates: []string{
bbServer + "/v3/fhir/metadata",
"http://hl7.org/fhir/uv/bulkdata/CapabilityStatement/bulk-data",
},
Software: r4.Software{
Name: constants.SoftwareName,
Version: constants.Version,
ReleaseDate: dt.UTC().Format("2006-01-02T15:04:05Z"),
},
Implementation: r4.Implementation{
Description: constants.SoftwareDescription,
Url: baseURL,
},
FhirVersion: "4.0.1",
Format: []string{
constants.JsonContentType,
constants.FHIRJsonContentType,
},
Rest: []r4.CapabilityStatementRest{
{
Mode: r4.RestfulCapabilityModeServer,
Security: &r4.Security{
Cors: true,
Service: []r4.CodeableConcept{
{
Coding: []r4.Coding{
{
Display: "OAuth",
Code: "OAuth",
System: constants.RestfulSecurityServiceSystem,
},
},
Text: "OAuth",
},
},
Extension: []r4.Extension{
{
Url: constants.SmartOAuthURIsExtensionURL,
Extension: []r4.Extension{
{
Url: "token",
ValueUri: baseURL + "/auth/token",
},
},
},
},
},
Interaction: []r4.Interaction{
{
Code: r4.SystemRestfulInteractionBatch,
},
{
Code: r4.SystemRestfulInteractionSearchSystem,
},
},
Resource: []r4.RestResource{
{
Type: r4.ResourceTypeCodePatient,
Operation: []r4.RestOperation{
{
Name: "export",
Definition: "http://hl7.org/fhir/uv/bulkdata/OperationDefinition/patient-export",
Documentation: "By default, the patient $export will return ExplanationOfBenefit resources with a meta.tag with a system of '" + constants.BFDSystemTypeURL + "' and code of either NationalClaimsHistory or DDPS. In order to return ExplanationOfBenefit resources with other system types (like SharedSystem), use the _typeFilter parameter.",
},
},
SearchParam: []r4.SearchParam{
restResourceSearchParam("_since", r4.SearchParamTypeDate, "Return resources updated after the date provided for existing and newly attributed enrollees."),
restResourceSearchParam("_type", r4.SearchParamTypeString, "Comma-delimited list of FHIR resource types to include in the export. By default, all supported resource types are returned."),
restResourceSearchParam("_typeFilter", r4.SearchParamTypeString, "Use a URL-encoded FHIR subquery to further-refine patient export results."),
},
},
{
Type: r4.ResourceTypeCodeGroup,
Operation: []r4.RestOperation{
{
Name: "export",
Definition: "http://hl7.org/fhir/uv/bulkdata/OperationDefinition/group-export",
Documentation: "By default, the group $export will return ExplanationOfBenefit resources with a meta.tag with a system of '" + constants.BFDSystemTypeURL + "' and code of either NationalClaimsHistory or DDPS. In order to return ExplanationOfBenefit resources with other system types (like SharedSystem), use the _typeFilter parameter.",
},
},
SearchParam: []r4.SearchParam{
restResourceSearchParam("_since", r4.SearchParamTypeDate, "Return resources updated after the date provided for existing enrollees and all resources for newly attributed enrollees."),
restResourceSearchParam("_type", r4.SearchParamTypeString, "Comma-delimited list of FHIR resource types to include in the export. By default, all supported resource types are returned."),
restResourceSearchParam("_typeFilter", r4.SearchParamTypeString, "Use a URL-encoded FHIR subquery to further-refine group export results."),
},
},
{
Type: r4.ResourceTypeCodeExplanationOfBenefit,
SearchParam: []r4.SearchParam{
restResourceSearchParam(string(fhir.TypeFilterParamTag), r4.SearchParamTypeToken, "Filter ExplanationOfBenefit by the meta.tag element. Pass full token as <system>|<code>. Supported codes in the '"+constants.BFDSystemTypeURL+"' system are: 'SharedSystem', 'NationalClaimsHistory', and 'DDPS'. By Default, only NationalClaimsHistory and DDPS claims will be returned."),
restResourceSearchParam(string(fhir.TypeFilterParamOutcome), r4.SearchParamTypeToken, "Filter ExplanationOfBenefit by the outcome element. Supported values: 'partial' and 'complete'."),
restResourceSearchParam(string(fhir.TypeFilterParamServiceDate), r4.SearchParamTypeDate, "Filter ExplanationOfBenefit based on the claim's service date. The service date is the date that the care occurred within a billable period. This is a FHIR date param format (ex. `gt2026-01-14`)"),
},
},
},
},
},
}
b, err := json.Marshal(statement)
if err != nil {
log.API.WithField("resp_status", http.StatusInternalServerError).Errorf("Failed to marshal Capability Statement %s", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set(constants.ContentType, constants.JsonContentType)
if _, err = w.Write(b); err != nil { // #nosec G705
log.API.WithField("resp_status", http.StatusInternalServerError).Errorf("Failed to write data %s", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func restResourceSearchParam(n string, t r4.SearchParamType, d string) r4.SearchParam {
p := r4.SearchParam{
Name: n,
Type: t,
Documentation: d,
}
return p
}