3. Implement the Patient Resource Provider

The generated PatientProvider validates requests and checks access, but then returns HTTP 501 Not Implemented. On this page, you replace the two stubs with a real read and search. The patient data comes from the Mobile MVI Service, which looks up patients in the VA Master Veteran Index (MVI):

  • GET [base]/Patient/10115V737384 reads the patient whose ICN is 10115V737384. The ICN is the logical id of your Patient resources.

  • GET [base]/Patient?identifier=urn:oid:2.16.840.1.113883.4.349|10115V737384 searches for the patient by ICN.

Both call the Mobile MVI Service’s GET /mvi/v1/patients/ICN/{icn}/mvi/person endpoint and map the Person it returns to a FHIR Patient.

In your local cluster, WireMock stands in for the Mobile MVI Service. WireMock is a server that returns prepared responses (stubs) for the requests it receives. Your stubs are loaded when WireMock starts, so the data is available to curl, Postman, and the integration tests alike.

Add WireMock to the Local Deployment

The dependencies that are deployed along with your service are listed in kubernetes/dev-with-dependencies/kustomization.yaml. Add the WireMock component before ../components/dev. Order matters: your components/dev adds the stubs to the WireMock deployment, so WireMock must already be part of the configuration.

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
...
components:
...
- https://coderepo.mobilehealth.va.gov/scm/dhsss/wiremock.git//kubernetes?ref=v3.13.2&timeout=120s
- ../components/dev

Add the MVI Stubs

Create the directory kubernetes/components/dev/wiremock-data/mvi and add the four stub files below. Each stub has a request that WireMock matches against incoming requests and the response it returns. urlPath matches the path only, so requests with query parameters, such as ?senderId=200CIH, match too.

version.json answers the health check of the Mobile MVI Service. You will see why below.

{
  "request": {
    "method": "GET",
    "urlPath": "/mvi/v1/version"
  },
  "response": {
    "status": 200,
    "body": "1.53.0"
  }
}

person-10115V737384.json returns the demographics of PATIENT FIFTEEN, a synthetic test patient:

{
  "request": {
    "method": "GET",
    "urlPath": "/mvi/v1/patients/ICN/10115V737384/mvi/person"
  },
  "response": {
    "status": 200,
    "headers": {
      "content-type": "application/json"
    },
    "jsonBody": {
      "lastName": "FIFTEEN",
      "firstName": "PATIENT",
      "birthTime": "19530401",
      "gender": "M",
      "telecom": "(123)234-5678",
      "address": {
        "street": "1111 Any Street",
        "city": "Any Town",
        "state": "WV",
        "zipCode": "99998-3414",
        "country": "USA"
      },
      "identifiers": [
        {
          "idType": "NI",
          "idStatus": "PERMANENT",
          "uniqueId": "10115V737384",
          "assigningAuthority": "ICN",
          "assigningCode": "200M"
        },
        {
          "idType": "PI",
          "idStatus": "ACTIVE",
          "uniqueId": "69",
          "assigningAuthority": "dfn-500",
          "assigningCode": "500"
        }
      ]
    }
  }
}

person-10132V467385.json returns PATIENT THIRTYTWO. Copy the previous file and change three values: 10115V737384 to 10132V467385 (twice), FIFTEEN to THIRTYTWO, and the dfn-500 identifier 69 to 140.

person-1012845331V153043.json is for a patient that MVI has no record of. The real Mobile MVI Service answers such requests with HTTP 204 No Content, so the stub does too:

{
  "request": {
    "method": "GET",
    "urlPath": "/mvi/v1/patients/ICN/1012845331V153043/mvi/person"
  },
  "response": {
    "status": 204
  }
}

Next, make the stubs available to WireMock. In kubernetes/components/dev/kustomization.yaml, generate a ConfigMap from the stub files, and add the WireMock patch right after - path: service.yaml. Leave the other entries under patches: as they are:

...
configMapGenerator:
- name: fhir-starter-service-app-config
  behavior: merge
  envs:
  - overrides.env
- name: mvi-wiremock-mappings
  behavior: create
  files:
  - wiremock-data/mvi/version.json
  - wiremock-data/mvi/person-10115V737384.json
  - wiremock-data/mvi/person-10132V467385.json
  - wiremock-data/mvi/person-1012845331V153043.json
...
patches:
- path: deployment.yaml
- path: service.yaml
- path: wiremock-data/data-wiremock.yaml
...

Then create the patch, kubernetes/components/dev/wiremock-data/data-wiremock.yaml. It mounts the ConfigMap into the WireMock container, in the directory WireMock loads its stubs from:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: wiremock
spec:
  template:
    spec:
      volumes:
      - name: mvi-wiremock-mappings
        configMap:
          name: mvi-wiremock-mappings
      containers:
      - name: wiremock
        volumeMounts:
        - name: mvi-wiremock-mappings
          mountPath: /home/wiremock/mappings/mappings/mvi

Kustomize adds a hash of the contents to the name of a generated ConfigMap, and updates the reference in the patch to match. When you change a stub, the ConfigMap gets a new name, and Kubernetes replaces the WireMock pod with one that loads the new stubs.

skaffold dev doesn’t notice changes to the files in the kubernetes directory. After you change a stub or a kustomization.yaml, stop skaffold dev with Ctrl-C, run ./scripts/localize.sh force, and start skaffold dev again. Without localize.sh force, a fix to a stub you haven’t committed yet isn’t deployed; see The Kubernetes Files Skaffold Deploys.

Add the MVI Client Library

The Mobile MVI Service publishes a client library with the model classes of its API, such as Person. Add a property for its version to the <properties> section of pom.xml, in a new Client Library Dependencies group before the Plugins group, and the dependency at the top of the <dependencies> section:

<properties>
...
    <!-- Client Library Dependencies -->
    <mobile-mvi-client.version>1.53</mobile-mvi-client.version>
...
</properties>

<dependencies>
...
    <!-- Mobile MVI Service client models (Person, PatientIdentifier) -->
    <dependency>
        <groupId>gov.va.mobile.client.openapi</groupId>
        <artifactId>mobile-mvi-client</artifactId>
        <version>${mobile-mvi-client.version}</version>
    </dependency>
...
</dependencies>

You only use the model classes. The requests themselves go through a REST client that the Mobile Framework configures for you.

Register the REST Client

In src/main/resources/application.properties, make two replacements. Replace the three readiness lines (the comment that starts with # Add the health indicators, the commented example, and the readinessState setting) with the first two lines below, and the two lines of the commented Mobile MVI Service example with the last two:

# The service is only ready when the Mobile MVI Service is reachable (health indicator "mvi")
management.endpoint.health.group.readiness.include=readinessState,mvi
...
# Mobile MVI Service: patient demographics and identifiers, used through RestClientManager.getClient("mvi")
va.mobile.restclient.registration.mvi.url=http://mobile-mvi-service-v1:8080/mvi/v1

In src/main/resources/application-dev.properties, replace the four commented lines that start with # Point downstream clients at WireMock with the following, so local deployments use your stubs. You don’t need the health.enabled=false example, because your version.json stub answers the health check:

# Local deployments use the WireMock stubs of the Mobile MVI Service (kubernetes/components/dev/wiremock-data/mvi)
va.mobile.restclient.registration.mvi.url=http://wiremock:8080/mvi/v1

The va.mobile.restclient.registration.<id>.url property is a registration: the Mobile Framework creates a REST client for it, which you get from RestClientManager by its id, mvi. The base file holds the URL of the real service in the cluster, and the dev profile, which local deployments run with, points the client at WireMock.

Each registration also gets a health indicator with the same id. It calls GET <url>/version and is part of the service’s health (/actuator/health on port 8081). That’s why you added the version.json stub. Adding mvi to the readiness group tells Kubernetes to send requests to your service only while the Mobile MVI Service is reachable.

Create the MVI Client

Create a new package named client next to the resource package, and in it a class named MviClient. Copy the following code into MviClient.java:

package gov.va.mobile.fhirstarter.v1.client;

import gov.va.mobile.boot.restclient.RestClientManager;
import gov.va.mobile.mvi.v1.model.Person;
import gov.va.mobile.security.HeaderNames;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestClient;

import java.util.Optional;

/**
 * Client for the Mobile MVI Service, which looks up patients in the VA Master Veteran Index (MVI).
 *
 * <p>The REST client is configured by the {@code va.mobile.restclient.registration.mvi.*} properties. Local
 * deployments call the WireMock stubs in {@code kubernetes/components/dev/wiremock-data/mvi} instead of the real
 * service.
 *
 * @since 1.0
 */
@Slf4j
@Component
public class MviClient {

    /**
     * Registration ID of the Mobile MVI Service REST client ({@code va.mobile.restclient.registration.mvi.*}).
     */
    public static final String REGISTRATION_ID = "mvi";

    /**
     * REST client for the Mobile MVI Service, created by the Mobile Framework from the registration properties.
     */
    private final RestClient restClient;

    /**
     * Creates the client.
     *
     * @param restClientManager provides the REST clients of the registrations in the service properties
     */
    public MviClient(final RestClientManager restClientManager) {
        this.restClient = restClientManager.getClient(REGISTRATION_ID);
    }

    /**
     * Returns the demographics of the patient with the given ICN.
     *
     * <p>The caller's JWT is passed on, because the Mobile MVI Service authorizes every request.
     *
     * @param icn the ICN of the patient
     * @param jwt the JWT of the caller
     * @return the demographics of the patient, or empty if MVI has no record of the patient
     */
    public Optional<Person> getPerson(final String icn, final String jwt) {
        try {
            // The Mobile MVI Service answers 204 No Content when MVI has no record, which results in a null body
            return Optional.ofNullable(restClient.get()
                                                 .uri("/patients/ICN/{icn}/mvi/person", icn)
                                                 .header(HeaderNames.VAMF_JWT_HEADER, jwt)
                                                 .retrieve()
                                                 .body(Person.class));
        } catch (final HttpClientErrorException.NotFound e) {
            log.debug("The Mobile MVI Service has no record of the requested patient");
            return Optional.empty();
        }
    }
}

The constructor gets the RestClient for the mvi registration from RestClientManager. getPerson passes the caller’s JWT on in the x-vamf-jwt header, because the Mobile MVI Service authorizes every request too. When MVI has no record of the patient, the service answers HTTP 204 No Content, which results in a null body; a request that WireMock has no stub for results in HTTP 404, which RestClient throws as HttpClientErrorException.NotFound. Both mean "no record", which the method returns as an empty Optional.

Map MVI Data to a FHIR Patient

Create another package named mapper, and in it a class named PatientMapper:

package gov.va.mobile.fhirstarter.v1.mapper;

import gov.va.mobile.mvi.v1.model.Person;
import gov.va.mobile.security.FhirSystems;
import org.hl7.fhir.r4.model.Address;
import org.hl7.fhir.r4.model.ContactPoint;
import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Patient;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Optional;

/**
 * Maps patient data from the Mobile MVI Service to FHIR resources.
 *
 * @since 1.0
 */
@Component
public class PatientMapper {

    /**
     * MVI gender code of a male patient.
     */
    private static final String MALE = "M";

    /**
     * MVI gender code of a female patient.
     */
    private static final String FEMALE = "F";

    /**
     * Maps the MVI demographics of a patient to a FHIR Patient.
     *
     * <p>The logical id of the Patient is the ICN, so clients can read it with {@code GET [base]/Patient/<icn>}.
     *
     * @param icn the ICN of the patient
     * @param person the demographics of the patient from the Mobile MVI Service
     * @return the Patient resource
     */
    public Patient toPatient(final String icn, final Person person) {
        final Patient patient = new Patient();
        patient.setId(icn);
        patient.addIdentifier()
               .setUse(Identifier.IdentifierUse.OFFICIAL)
               .setSystem(FhirSystems.ICN_SYSTEM)
               .setValue(icn);
        patient.addName(toName(person));
        patient.setGender(toGender(person.getGender()));
        patient.setBirthDateElement(toBirthDate(person.getBirthTime()));
        if (isPresent(person.getTelecom())) {
            patient.addTelecom()
                   .setSystem(ContactPoint.ContactPointSystem.PHONE)
                   .setValue(person.getTelecom());
        }
        Optional.ofNullable(person.getAddress())
                .map(PatientMapper::toAddress)
                .ifPresent(patient::addAddress);
        // MVI also returns the SSN of the patient. Never copy it to the Patient resource.
        return patient;
    }

    /**
     * Maps an MVI gender code to a FHIR administrative gender.
     *
     * @param gender the MVI gender code, {@code M} or {@code F}
     * @return the FHIR gender, {@code UNKNOWN} for any other code, or {@code null} if the code is missing
     */
    static AdministrativeGender toGender(final String gender) {
        if (!isPresent(gender)) {
            return null;
        }
        return switch (gender) {
            case MALE -> AdministrativeGender.MALE;
            case FEMALE -> AdministrativeGender.FEMALE;
            default -> AdministrativeGender.UNKNOWN;
        };
    }

    /**
     * Maps an MVI birth time to a FHIR date.
     *
     * @param birthTime the MVI birth time in the format {@code yyyyMMdd}
     * @return the birth date, or {@code null} if the birth time is missing or invalid
     */
    static DateType toBirthDate(final String birthTime) {
        if (!isPresent(birthTime)) {
            return null;
        }
        try {
            return new DateType(LocalDate.parse(birthTime, DateTimeFormatter.BASIC_ISO_DATE).toString());
        } catch (final DateTimeParseException e) {
            return null;
        }
    }

    /**
     * Maps the MVI name of a patient to the official FHIR name.
     *
     * @param person the demographics of the patient
     * @return the official name
     */
    private static HumanName toName(final Person person) {
        final HumanName name = new HumanName()
                .setUse(HumanName.NameUse.OFFICIAL)
                .setFamily(person.getLastName());
        if (isPresent(person.getFirstName())) {
            name.addGiven(person.getFirstName());
        }
        if (isPresent(person.getMiddleName())) {
            name.addGiven(person.getMiddleName());
        }
        return name;
    }

    /**
     * Maps an MVI address to a FHIR address.
     *
     * @param mviAddress the address from the Mobile MVI Service
     * @return the FHIR address
     */
    private static Address toAddress(final gov.va.mobile.mvi.v1.model.Address mviAddress) {
        final Address address = new Address()
                .setCity(mviAddress.getCity())
                .setState(mviAddress.getState())
                .setPostalCode(mviAddress.getZipCode())
                .setCountry(mviAddress.getCountry());
        if (isPresent(mviAddress.getStreet())) {
            address.addLine(mviAddress.getStreet());
        }
        return address;
    }

    /**
     * Checks whether a value from MVI is present.
     *
     * @param value the value
     * @return {@code true} if the value isn't {@code null} or blank
     */
    private static boolean isPresent(final String value) {
        return value != null && !value.isBlank();
    }
}

Take a moment to look at how the MVI data maps to the FHIR Patient:

  • The logical id of the Patient is the ICN, so a client that found a patient with a search can read it again with GET [base]/Patient/<icn>. The ICN is also the patient’s official identifier, in the ICN system (FhirSystems.ICN_SYSTEM).

  • MVI uses M and F for the gender; FHIR uses the AdministrativeGender codes. MVI sends the birth date in the format yyyyMMdd; FHIR dates look like 1953-04-01.

  • The MVI response also contains the patient’s SSN. The mapper deliberately ignores it. Return only the data that your clients need, and never copy identifiers like the SSN into a response.

  • toGender and toBirthDate are package-private, so the unit tests in the same package can call them directly.

Look at the Access Checks

The generated PatientProvider doesn’t validate the ICN or check the user’s access itself. It calls PatientAccess, in the same resource package, which every provider that returns patient data uses; on page 8, so does the Encounter provider. Open PatientAccess.java:

  • validateIcn has two versions: one for a search parameter (TokenParam), whose system must be the ICN system when it’s present, and one for a plain String, such as the logical id of a read.

  • verifyPatientAccess lets staff users access any patient, and veterans only their own records, identified by the subject (sub) of their JWT.

  • getJwt returns the caller’s JWT, which your service passes on to the Mobile MVI Service.

Update the Provider

Replace the contents of PatientProvider.java with the following. It still calls PatientAccess, now also for the read, and passes the caller’s JWT on to MviClient:

package gov.va.mobile.fhirstarter.v1.resource;

import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import gov.va.mobile.aep.v2.client.web.JwtAudited;
import gov.va.mobile.fhirstarter.v1.client.MviClient;
import gov.va.mobile.fhirstarter.v1.exception.ErrorMessage;
import gov.va.mobile.fhirstarter.v1.mapper.PatientMapper;
import gov.va.mobile.security.UserRoles;
import gov.va.mobile.security.web.jwt.JwtRestricted;
import gov.va.mobile.security.web.jwt.JwtRestrictedGroup;
import lombok.RequiredArgsConstructor;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Patient;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * Patient resource provider, backed by the Mobile MVI Service.
 *
 * <p>Spring beans implementing {@link IResourceProvider} are registered with the FHIR server automatically; see
 * {@code FhirConfig} for registering plain providers and interceptors.
 *
 * <p>Access is role based: {@link JwtRestrictedGroup} admits staff and veteran users. Each operation then validates
 * the ICN and the user's access with {@link PatientAccess}, which results in HTTP 400 for an invalid ICN and HTTP 403
 * when a veteran requests the records of another patient. Operations that return patient data are audited with
 * {@link JwtAudited}.
 *
 * @since 1.0
 */
@Component
@RequiredArgsConstructor
@JwtRestrictedGroup(oneOf = {
        @JwtRestricted(roles = UserRoles.STAFF),
        @JwtRestricted(roles = UserRoles.VETERAN)})
public class PatientProvider implements IResourceProvider {

    /**
     * Client for the Mobile MVI Service.
     */
    private final MviClient mviClient;

    /**
     * Maps MVI data to FHIR resources.
     */
    private final PatientMapper patientMapper;

    /**
     * Returns the FHIR resource type handled by this provider.
     *
     * @return the Patient resource class
     */
    @Override
    public Class<? extends IBaseResource> getResourceType() {
        return Patient.class;
    }

    /**
     * Searches for the Patient with the given ICN, for example {@code GET [base]/Patient?identifier=<system>|10115V737384}.
     *
     * @param requestDetails request details provided by HAPI FHIR
     * @param identifier the patient identifier; the system is optional, but when present it must be the ICN system
     * @return the matching Patient, or an empty list if MVI has no record of the patient; HAPI FHIR returns the list as
     *         a Bundle of type {@code searchset}
     */
    @Search
    @JwtAudited(JwtAudited.AuditOptions.REQUIRED)
    public List<Patient> search(
            final ServletRequestDetails requestDetails,
            @Description(shortDefinition = "The patient's ICN, optionally qualified by the ICN system")
            @RequiredParam(name = Patient.SP_IDENTIFIER)
            final TokenParam identifier) {
        final String icn = PatientAccess.validateIcn(identifier);
        PatientAccess.verifyPatientAccess(requestDetails, icn);
        return mviClient.getPerson(icn, PatientAccess.getJwt(requestDetails))
                        .map(person -> List.of(patientMapper.toPatient(icn, person)))
                        .orElseGet(List::of);
    }

    /**
     * Reads the Patient with the given ICN, for example {@code GET [base]/Patient/10115V737384}.
     *
     * @param requestDetails request details provided by HAPI FHIR
     * @param id the logical id of the Patient, which is the ICN of the patient
     * @return the Patient
     * @throws ResourceNotFoundException if MVI has no record of the patient (HTTP 404)
     */
    @Read
    @JwtAudited(JwtAudited.AuditOptions.REQUIRED)
    public Patient read(final ServletRequestDetails requestDetails, @IdParam final IdType id) {
        final String icn = PatientAccess.validateIcn(id.getIdPart());
        PatientAccess.verifyPatientAccess(requestDetails, icn);
        return mviClient.getPerson(icn, PatientAccess.getJwt(requestDetails))
                        .map(person -> patientMapper.toPatient(icn, person))
                        .orElseThrow(() -> new ResourceNotFoundException(ErrorMessage.PATIENT_NOT_FOUND.getMessage()));
    }

}

Finally, add the message for an unknown patient to exception/ErrorMessage.java, after PATIENT_ACCESS_DENIED:

    /** No patient was found for the requested ICN. */
    PATIENT_NOT_FOUND("No patient was found for the requested ICN"),

Here is what changed in PatientProvider:

  • @RequiredArgsConstructor (Lombok) generates a constructor for the two final fields, so Spring injects MviClient and PatientMapper.

  • search now returns a List<Patient>. HAPI wraps the list in a Bundle of type searchset for you, with a total and a fullUrl for each entry. When MVI has no record of the patient, the search returns an empty Bundle: a search that finds nothing isn’t an error.

  • read gets the logical id as an IdType, not a TokenParam, so it uses the String version of PatientAccess.validateIcn. A read of a patient that doesn’t exist is an error, ResourceNotFoundException, which HAPI returns as HTTP 404. The message leaves out the ICN on purpose: HAPI logs every error response with its message (4xx at WARN, 5xx at ERROR), and logs must never contain patient identifiers.

  • Both methods pass the caller’s JWT to MviClient.

  • @JwtAudited(JwtAudited.AuditOptions.REQUIRED) records an audit event for every request, which every operation that returns patient data must do. In local deployments, the dev profile turns the audit client off (va.mobile.aep.client.enabled=false), so no audit service is needed.

Keep the Unit Tests Passing

skaffold dev compiles your service and runs its unit tests before it deploys, and stops if a test fails. Two things in PatientProviderTest break that now: it creates PatientProvider without arguments, and three of its tests expect HTTP 501. You write the real tests on the next page; for now, make these changes to src/test/java/gov/va/mobile/fhirstarter/v1/resource/PatientProviderTest.java:

  1. Delete the provider field, which creates the provider without arguments, and add the following after the requestDetails field. It gives the provider a mocked MviClient; the next page explains mocks:

        /**
         * The Mobile MVI Service client, mocked so the tests don't need the service.
         */
        @Mock
        private MviClient mviClient;
    
        /**
         * The provider under test.
         */
        private PatientProvider provider;
    
        /**
         * Create the provider with the mocked client before each test.
         */
        @BeforeEach
        void setUp() {
            provider = new PatientProvider(mviClient, new PatientMapper());
        }
  2. Add these imports:

    import gov.va.mobile.fhirstarter.v1.client.MviClient;
    import gov.va.mobile.fhirstarter.v1.mapper.PatientMapper;
    import org.junit.jupiter.api.BeforeEach;
  3. Delete the three tests that expect HTTP 501, search_staff, search_veteranSelfWithoutSystem, and read, and the import of NotImplementedOperationException.

The other tests check the access rules and the ICN validation, which haven’t changed, so they still pass.

Try It

Stop skaffold dev, run ./scripts/localize.sh force, and start skaffold dev --rpc-port 50051 --port-forward=true again, so it deploys WireMock with your stubs. If it doesn’t reach Press Ctrl+C to exit within two minutes, see When Something Goes Wrong. Then read PATIENT FIFTEEN with your staff JWT:

curl -s -H "x-vamf-jwt: $STAFF_JWT" 'http://localhost:8080/fhirstarter/v1/fhir/Patient/10115V737384' | jq

Response:

{
  "resourceType": "Patient",
  "id": "10115V737384",
  "identifier": [
    {
      "use": "official",
      "system": "urn:oid:2.16.840.1.113883.4.349",
      "value": "10115V737384"
    }
  ],
  "name": [
    {
      "use": "official",
      "family": "FIFTEEN",
      "given": [
        "PATIENT"
      ]
    }
  ],
  "telecom": [
    {
      "system": "phone",
      "value": "(123)234-5678"
    }
  ],
  "gender": "male",
  "birthDate": "1953-04-01",
  "address": [
    {
      "line": [
        "1111 Any Street"
      ],
      "city": "Any Town",
      "state": "WV",
      "postalCode": "99998-3414",
      "country": "USA"
    }
  ]
}

Search for PATIENT THIRTYTWO:

curl -s -H "x-vamf-jwt: $STAFF_JWT" 'http://localhost:8080/fhirstarter/v1/fhir/Patient?identifier=urn:oid:2.16.840.1.113883.4.349%7C10132V467385' | jq

The response is a searchset Bundle with "total":1 and one entry, whose fullUrl is http://localhost:8080/fhirstarter/v1/fhir/Patient/10132V467385.

Then try a patient that MVI has no record of:

  • Patient/1012845331V153043 returns HTTP 404:

    {
      "resourceType": "OperationOutcome",
      "issue": [
        {
          "severity": "error",
          "code": "processing",
          "diagnostics": "No patient was found for the requested ICN"
        }
      ]
    }
  • Patient?identifier=1012845331V153043 returns HTTP 200 with an empty Bundle, "type":"searchset","total":0.

Now try the access rules with your veteran JWT, which is for PATIENT FIFTEEN:

  • Patient/10115V737384 with $VETERAN_JWT returns HTTP 200 with PATIENT FIFTEEN, the veteran’s own record.

  • Patient/10132V467385 with $VETERAN_JWT returns HTTP 403, "diagnostics":"Access to the requested patient is not permitted".

Finally, look at the health of your service: curl -s http://localhost:8081/actuator/health. The mvi component is UP, with the URL of the health check and the version that your version.json stub returned.

Commit Your Work

The integration tests that came with the archetype still expect HTTP 501, so the full build would fail. You update them on the next page, so don’t run the full build yet. Commit your work as a checkpoint:

git add .
git commit -m "Implement the Patient read and search with the Mobile MVI Service"

You push after the next page, when the build passes again.

See It in a Real Service

  • cerner-fhir-adapter: src/main/java/gov/va/mobile/cfa/v1/clients/MobileMviClient.java, an MVI client like yours

  • vista-fhir-note-api: src/main/java/gov/va/mobile/vfna/v1/services/MobileMviClient.java, and the WireMock setup in kubernetes/components/dev/wiremock-data/

  • clinical-encounter-service: kubernetes/components/dev/wiremock-data/mappings/mvi/, more MVI stubs for synthetic test patients

You’re Done When

  • The Patient read and search return data from your MVI stubs

  • Reading an unknown patient returns HTTP 404, and searching for it returns an empty Bundle

  • /actuator/health shows the mvi component UP

  • Your work is committed

Continue to 4. Test the Patient Provider to update the tests for the Patient read and search.