org.openmrs.module.emrapi.adt.AdtServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.openmrs.module.emrapi.adt.AdtServiceImpl.java

Source

/*
 * The contents of this file are subject to the OpenMRS Public License
 * Version 1.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://license.openmrs.org
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations
 * under the License.
 *
 * Copyright (C) OpenMRS, LLC.  All Rights Reserved.
 */

package org.openmrs.module.emrapi.adt;

import org.apache.commons.lang.time.DateUtils;
import org.openmrs.Encounter;
import org.openmrs.EncounterRole;
import org.openmrs.EncounterType;
import org.openmrs.Form;
import org.openmrs.Location;
import org.openmrs.Obs;
import org.openmrs.Order;
import org.openmrs.Patient;
import org.openmrs.PersonAttribute;
import org.openmrs.PersonAttributeType;
import org.openmrs.Provider;
import org.openmrs.User;
import org.openmrs.Visit;
import org.openmrs.api.APIException;
import org.openmrs.api.EncounterService;
import org.openmrs.api.LocationService;
import org.openmrs.api.PatientService;
import org.openmrs.api.ProviderService;
import org.openmrs.api.VisitService;
import org.openmrs.api.context.Context;
import org.openmrs.api.impl.BaseOpenmrsService;
import org.openmrs.module.emrapi.EmrApiConstants;
import org.openmrs.module.emrapi.EmrApiProperties;
import org.openmrs.module.emrapi.adt.exception.ExistingVisitDuringTimePeriodException;
import org.openmrs.module.emrapi.disposition.Disposition;
import org.openmrs.module.emrapi.domainwrapper.DomainWrapperFactory;
import org.openmrs.module.emrapi.merge.PatientMergeAction;
import org.openmrs.module.emrapi.patient.PatientDomainWrapper;
import org.openmrs.module.emrapi.visit.VisitDomainWrapper;
import org.openmrs.serialization.SerializationException;
import org.openmrs.util.OpenmrsUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class AdtServiceImpl extends BaseOpenmrsService implements AdtService {

    private final Logger log = LoggerFactory.getLogger(getClass());

    private EmrApiProperties emrApiProperties;

    private PatientService patientService;

    private EncounterService encounterService;

    private VisitService visitService;

    private ProviderService providerService;

    private LocationService locationService;

    private DomainWrapperFactory domainWrapperFactory;

    private List<PatientMergeAction> patientMergeActions;

    public void setPatientService(PatientService patientService) {
        this.patientService = patientService;
    }

    public void setLocationService(LocationService locationService) {
        this.locationService = locationService;
    }

    public void setEmrApiProperties(EmrApiProperties emrApiProperties) {
        this.emrApiProperties = emrApiProperties;
    }

    public void setEncounterService(EncounterService encounterService) {
        this.encounterService = encounterService;
    }

    public void setVisitService(VisitService visitService) {
        this.visitService = visitService;
    }

    public void setProviderService(ProviderService providerService) {
        this.providerService = providerService;
    }

    public void setDomainWrapperFactory(DomainWrapperFactory domainWrapperFactory) {
        this.domainWrapperFactory = domainWrapperFactory;
    }

    public void setPatientMergeActions(List<PatientMergeAction> patientMergeActions) {
        this.patientMergeActions = patientMergeActions;
    }

    // for testing
    public List<PatientMergeAction> getPatientMergeActions() {
        return patientMergeActions;
    }

    @Override
    public void closeInactiveVisits() {
        List<Visit> openVisits = visitService.getVisits(null, null, null, null, null, null, null, null, null, false,
                false);
        for (Visit visit : openVisits) {
            if (shouldBeClosed(visit)) {
                try {
                    closeAndSaveVisit(visit);
                } catch (Exception ex) {
                    log.warn("Failed to close inactive visit " + visit, ex);
                }
            }
        }
    }

    private boolean shouldBeClosed(Visit visit) {

        if (visit.getStopDatetime() != null) {
            return false; // already closed
        }

        VisitDomainWrapper visitDomainWrapper = domainWrapperFactory.newVisitDomainWrapper(visit);

        if (visitDomainWrapper.isAdmitted() || visitDomainWrapper.isAwaitingAdmission()) {
            return false; // don't close the visit if patient is admitted or waiting admission
        }

        Disposition mostRecentDisposition = visitDomainWrapper.getMostRecentDisposition();
        if (mostRecentDisposition != null && mostRecentDisposition.getKeepsVisitOpen() != null
                && mostRecentDisposition.getKeepsVisitOpen()) {
            return false; // don't close the visit if the most recent disposition is one that keeps visit opens
        }

        Date now = new Date();
        Date mustHaveSomethingAfter = DateUtils.addHours(now, -emrApiProperties.getVisitExpireHours());

        if (OpenmrsUtil.compare(visit.getStartDatetime(), mustHaveSomethingAfter) >= 0) {
            return false;
        }

        if (visit.getEncounters() != null) {
            for (Encounter candidate : visit.getEncounters()) {
                if (OpenmrsUtil.compare(candidate.getEncounterDatetime(), mustHaveSomethingAfter) >= 0) {
                    return false;
                }
            }
        }

        return true;
    }

    @Override
    public boolean visitsOverlap(Visit v1, Visit v2) {
        Location where1 = v1.getLocation();
        Location where2 = v2.getLocation();
        if ((where1 == null && where2 == null) || isSameOrAncestor(where1, where2)
                || isSameOrAncestor(where2, where1)) {
            // "same" location, so check if date ranges overlap (assuming startDatetime is never null)
            return (OpenmrsUtil.compareWithNullAsLatest(v1.getStartDatetime(), v2.getStopDatetime()) <= 0)
                    && (OpenmrsUtil.compareWithNullAsLatest(v2.getStartDatetime(), v1.getStopDatetime()) <= 0);
        }
        return false;
    }

    /**
     * Anything that calls this needs to be @Transactional
     *
     * @param patient
     * @param department
     * @return
     */
    private Visit getActiveVisitHelper(Patient patient, Location department) {
        Date now = new Date();

        List<Visit> candidates = visitService.getVisitsByPatient(patient);
        Visit ret = null;
        for (Visit candidate : candidates) {
            if (isSuitableVisit(candidate, department, now)) {
                ret = candidate;
            }
        }

        return ret;
    }

    @Override
    @Transactional
    public VisitDomainWrapper getActiveVisit(Patient patient, Location location) {
        VisitDomainWrapper visitSummary = null;
        Visit activeVisit = getActiveVisitHelper(patient, location);
        if (activeVisit != null) {
            visitSummary = wrap(activeVisit);
        }
        return visitSummary;
    }

    @Override
    @Transactional
    public void closeAndSaveVisit(Visit visit) {
        visit.setStopDatetime(guessVisitStopDatetime(visit));
        visitService.saveVisit(visit);
    }

    @Override
    @Transactional
    public Visit ensureActiveVisit(Patient patient, Location department) {
        Visit activeVisit = getActiveVisitHelper(patient, department);
        if (activeVisit == null) {
            Date now = new Date();
            activeVisit = buildVisit(patient, department, now);
            visitService.saveVisit(activeVisit);
        }
        return activeVisit;
    }

    @Transactional
    public Visit ensureVisit(Patient patient, Date visitTime, Location department) {
        if (visitTime == null) {
            visitTime = new Date();
        }
        Visit visit = null;
        List<Patient> patientList = Collections.singletonList(patient);

        // visits that have not ended by the encounter date.
        List<Visit> candidates = visitService.getVisits(null, patientList, null, null, null, visitTime, null, null,
                null, true, false);
        if (candidates != null) {
            for (Visit candidate : candidates) {
                if (isSuitableVisit(candidate, department, visitTime)) {
                    return candidate;
                }
            }
        }
        if (visit == null) {
            visit = buildVisit(patient, department, visitTime);
            visitService.saveVisit(visit);
        }
        return visit;
    }

    private Date guessVisitStopDatetime(Visit visit) {
        if (visit.getEncounters() == null || visit.getEncounters().size() == 0) {
            return visit.getStartDatetime();
        }

        Iterator<Encounter> iterator = visit.getEncounters().iterator();
        Encounter latest = iterator.next();
        while (iterator.hasNext()) {
            Encounter candidate = iterator.next();
            if (OpenmrsUtil.compare(candidate.getEncounterDatetime(), latest.getEncounterDatetime()) > 0) {
                latest = candidate;
            }
        }
        return latest.getEncounterDatetime();
    }

    /**
     * This method is synchronized to prevent multiple check-ins in a row at the same location and during the same visit.
     * See #579.
     * 
     * @see org.openmrs.module.emrapi.adt.AdtService#checkInPatient(org.openmrs.Patient, org.openmrs.Location, org.openmrs.Provider, java.util.List, java.util.List, boolean)
     */
    @Override
    @Transactional
    public synchronized Encounter checkInPatient(Patient patient, Location where, Provider checkInClerk,
            List<Obs> obsForCheckInEncounter, List<Order> ordersForCheckInEncounter, boolean newVisit) {
        if (checkInClerk == null) {
            checkInClerk = getProvider(Context.getAuthenticatedUser());
        }

        Visit activeVisit = getActiveVisitHelper(patient, where);

        if (activeVisit != null && newVisit) {
            closeAndSaveVisit(activeVisit);
            activeVisit = null;
        }

        if (activeVisit == null) {
            activeVisit = ensureActiveVisit(patient, where);
        }

        Encounter lastEncounter = getLastEncounter(patient);
        if (lastEncounter != null && activeVisit.equals(lastEncounter.getVisit())
                && emrApiProperties.getCheckInEncounterType().equals(lastEncounter.getEncounterType())
                && where.equals(lastEncounter.getLocation())) {
            log.warn("Patient id:{} tried to check-in twice in a row at id:{} during the same visit",
                    patient.getId(), where.getId());
            return lastEncounter;
        }

        Encounter encounter = buildEncounter(emrApiProperties.getCheckInEncounterType(), patient, where, null,
                new Date(), obsForCheckInEncounter, ordersForCheckInEncounter);
        encounter.addProvider(emrApiProperties.getCheckInClerkEncounterRole(), checkInClerk);
        activeVisit.addEncounter(encounter);
        encounterService.saveEncounter(encounter);
        return encounter;
    }

    // Commenting this out since the feature isn't in use yet, and it refers to payment, which isn't supposed to be in this module
    //    @Override
    //    @Transactional
    //    public Encounter createCheckinInRetrospective(Patient patient, Location location, Provider clerk, Obs paymentReason, Obs paymentAmount, Obs paymentReceipt, Date checkinDate) {
    //        Visit encounterVisit = buildVisit(patient, location, checkinDate);
    //
    //        List<Obs> paymentObservations = new ArrayList<Obs>();
    //        Obs paymentGroup = new Obs();
    //        paymentGroup.setConcept(emrApiProperties.getPaymentConstructConcept());
    //        paymentGroup.addGroupMember(paymentReason);
    //        paymentGroup.addGroupMember(paymentAmount);
    //        paymentGroup.addGroupMember(paymentReceipt);
    //        paymentObservations.add(paymentGroup);
    //        Encounter checkinEncounter = buildEncounter(emrApiProperties.getCheckInEncounterType(), patient, location, checkinDate, paymentObservations, null);
    //        checkinEncounter.addProvider(emrApiProperties.getCheckInClerkEncounterRole(), clerk);
    //        encounterVisit.addEncounter(checkinEncounter);
    //
    //        checkinEncounter = encounterService.saveEncounter(checkinEncounter);
    //
    //        return checkinEncounter;
    //    }

    private Provider getProvider(User accountBelongingToUser) {
        Collection<Provider> candidates = providerService.getProvidersByPerson(accountBelongingToUser.getPerson(),
                false);
        if (candidates.size() == 0) {
            throw new IllegalStateException(
                    "User " + accountBelongingToUser.getUsername() + " does not have a Provider account");
        } else if (candidates.size() > 1) {
            throw new IllegalStateException(
                    "User " + accountBelongingToUser.getUsername() + " has more than one Provider account");
        } else {
            return candidates.iterator().next();
        }
    }

    private Encounter buildEncounter(EncounterType encounterType, Patient patient, Location location, Form form,
            Date when, List<Obs> obsToCreate, List<Order> ordersToCreate) {
        Encounter encounter = new Encounter();
        encounter.setPatient(patient);
        encounter.setEncounterType(encounterType);
        encounter.setLocation(location);
        encounter.setForm(form);
        encounter.setEncounterDatetime(when);
        if (obsToCreate != null) {
            for (Obs obs : obsToCreate) {
                obs.setObsDatetime(new Date());
                encounter.addObs(obs);
            }
        }
        if (ordersToCreate != null) {
            for (Order order : ordersToCreate) {
                encounter.addOrder(order);
            }
        }
        return encounter;
    }

    private Visit buildVisit(Patient patient, Location location, Date when) {
        Visit visit = new Visit();
        visit.setPatient(patient);
        visit.setLocation(getLocationThatSupportsVisits(location));
        visit.setStartDatetime(when);
        visit.setVisitType(emrApiProperties.getAtFacilityVisitType());
        return visit;
    }

    /**
     * Looks at location, and if necessary its ancestors in the location hierarchy, until it finds one tagged with
     * "Visit Location"
     *
     * @param location
     * @return location, or an ancestor
     * @throws IllegalArgumentException if neither location nor its ancestors support visits
     */
    @Override
    public Location getLocationThatSupportsVisits(Location location) {
        if (location == null) {
            throw new IllegalArgumentException("Location does not support visits");
        } else if (location.hasTag(EmrApiConstants.LOCATION_TAG_SUPPORTS_VISITS)) {
            return location;
        } else {
            return getLocationThatSupportsVisits(location.getParentLocation());
        }
    }

    @Override
    @Transactional(readOnly = true)
    public List<Location> getAllLocationsThatSupportVisits() {
        return locationService.getLocationsByTag(emrApiProperties.getSupportsVisitsLocationTag());
    }

    /**
     * @param visit
     * @param location
     * @param when
     * @return true if when falls in the visits timespan AND location is within visit.location
     */
    @Override
    public boolean isSuitableVisit(Visit visit, Location location, Date when) {
        if (OpenmrsUtil.compare(when, visit.getStartDatetime()) < 0) {
            return false;
        }
        if (OpenmrsUtil.compareWithNullAsLatest(when, visit.getStopDatetime()) > 0) {
            return false;
        }
        return isSameOrAncestor(visit.getLocation(), location);
    }

    /**
     * @param a
     * @param b
     * @return true if a.equals(b) or a is an ancestor of b.
     */
    private boolean isSameOrAncestor(Location a, Location b) {
        if (a == null || b == null) {
            return a == null && b == null;
        }
        return a.equals(b) || isSameOrAncestor(a, b.getParentLocation());
    }

    /**
     * @see org.openmrs.module.emrapi.adt.AdtService#getActiveVisits(org.openmrs.Location)
     */
    @Override
    public List<VisitDomainWrapper> getActiveVisits(Location location) {
        if (location == null) {
            throw new IllegalArgumentException("Location is required");
        }
        Set<Location> locations = getChildLocationsRecursively(location, null);
        List<Visit> candidates = visitService.getVisits(null, null, locations, null, null, null, null, null, null,
                false, false);

        List<VisitDomainWrapper> active = new ArrayList<VisitDomainWrapper>();
        for (Visit candidate : candidates) {
            if (itBelongsToARealPatient(candidate)) {
                active.add(wrap(candidate));
            }
        }

        return active;
    }

    @Override
    public List<VisitDomainWrapper> getInpatientVisits(Location visitLocation, Location ward) {

        if (visitLocation == null) {
            throw new IllegalArgumentException("Location is required");
        }
        Set<Location> locations = getChildLocationsRecursively(visitLocation, null);
        List<Visit> candidates = visitService.getVisits(null, null, locations, null, null, null, null, null, null,
                false, false);

        List<VisitDomainWrapper> inpatientVisits = new ArrayList<VisitDomainWrapper>();
        for (Visit candidate : candidates) {
            VisitDomainWrapper visitDomainWrapper = wrap(candidate);
            if (itBelongsToARealPatient(candidate) && visitDomainWrapper.isAdmitted()) {
                if (ward != null) {
                    Encounter latestAdtEncounter = visitDomainWrapper.getLatestAdtEncounter();
                    if (latestAdtEncounter != null
                            && (latestAdtEncounter.getLocation().getId().compareTo(ward.getId()) == 0)) {
                        inpatientVisits.add(visitDomainWrapper);
                    }
                } else {
                    inpatientVisits.add(visitDomainWrapper);
                }
            }
        }

        return inpatientVisits;
    }

    private boolean itBelongsToARealPatient(Visit candidate) {
        Patient patient = candidate.getPatient();
        PatientDomainWrapper domainWrapper = new PatientDomainWrapper(patient, emrApiProperties, null, null, null,
                null);
        return !domainWrapper.isTestPatient();
    }

    @Override
    public Encounter getLastEncounter(Patient patient) {
        // speed this up by implementing it directly in a DAO
        List<Encounter> byPatient = encounterService.getEncountersByPatient(patient);
        if (byPatient.size() == 0) {
            return null;
        } else {
            return byPatient.get(byPatient.size() - 1);
        }
    }

    @Override
    public int getCountOfEncounters(Patient patient) {
        // speed this up by implementing it directly in a DAO
        return encounterService.getEncountersByPatient(patient).size();
    }

    @Override
    public int getCountOfVisits(Patient patient) {
        // speed this up by implementing it directly in a DAO
        return visitService.getVisitsByPatient(patient, true, false).size();
    }

    /**
     * Utility method that returns all child locations and children of its child locations
     * recursively
     *
     * @param location
     * @param foundLocations
     * @return
     */
    private Set<Location> getChildLocationsRecursively(Location location, Set<Location> foundLocations) {
        if (foundLocations == null)
            foundLocations = new LinkedHashSet<Location>();

        foundLocations.add(location);

        if (location.getChildLocations() != null) {
            for (Location l : location.getChildLocations()) {
                foundLocations.add(l);
                getChildLocationsRecursively(l, foundLocations);
            }
        }

        return foundLocations;
    }

    @Transactional
    @Override
    public void mergePatients(Patient preferred, Patient notPreferred) {
        boolean preferredWasUnknown = domainWrapperFactory.newPatientDomainWrapper(preferred).isUnknownPatient();
        boolean notPreferredWasUnknown = domainWrapperFactory.newPatientDomainWrapper(notPreferred)
                .isUnknownPatient();
        if (preferredWasUnknown && !notPreferredWasUnknown) {
            throw new IllegalArgumentException("Cannot merge a permanent record into an unknown one");
        }

        List<Visit> preferredVisits = visitService.getVisitsByPatient(preferred, true, false);
        List<Visit> notPreferredVisits = visitService.getVisitsByPatient(notPreferred, true, false);

        // if the non-preferred patient has any visits that overlap with visits of the preferred patient, we need to merge them together
        for (Visit losing : notPreferredVisits) {
            if (!losing.isVoided()) {
                for (Visit winning : preferredVisits) {
                    if (!winning.isVoided() && visitsOverlap(losing, winning)) {
                        mergeVisits(winning, losing);
                        break;
                    }
                }
            }
        }

        // merging in visits from the non-preferred patient (and extending visit durations) may have caused preferred-patient visits to overlap
        Collections.sort(preferredVisits, new Comparator<Visit>() {
            @Override
            public int compare(Visit left, Visit right) {
                return OpenmrsUtil.compareWithNullAsEarliest(left.getStartDatetime(), right.getStartDatetime());
            }
        });
        for (int i = 0; i < preferredVisits.size(); ++i) {
            Visit visit = preferredVisits.get(i);
            if (!visit.isVoided()) {
                for (int j = i + 1; j < preferredVisits.size(); ++j) {
                    Visit candidate = preferredVisits.get(j);
                    if (!candidate.isVoided() && visitsOverlap(visit, candidate)) {
                        mergeVisits(visit, candidate);
                    }
                }
            }
        }

        if (patientMergeActions != null) {
            for (PatientMergeAction patientMergeAction : patientMergeActions) {
                patientMergeAction.beforeMergingPatients(preferred, notPreferred);
            }
        }

        try {
            patientService.mergePatients(preferred, notPreferred);
            // if we merged an unknown record into a permanent one, remove the unknown flag; if we merged two unknown records, keep it
            if (!preferredWasUnknown) {
                removeAttributeOfUnknownPatient(preferred);
            }
        } catch (SerializationException e) {
            throw new APIException("Unable to merge patients due to serialization error", e);
        }

        if (patientMergeActions != null) {
            for (PatientMergeAction patientMergeAction : patientMergeActions) {
                patientMergeAction.afterMergingPatients(preferred, notPreferred);
            }
        }

    }

    private void removeAttributeOfUnknownPatient(Patient preferred) {
        PersonAttributeType unknownPatientPersonAttributeType = emrApiProperties
                .getUnknownPatientPersonAttributeType();
        PersonAttribute attribute = preferred.getAttribute(unknownPatientPersonAttributeType);
        if (attribute != null) {
            preferred.removeAttribute(attribute);
            patientService.savePatient(preferred);
        }
    }

    public boolean areConsecutiveVisits(List<Integer> visits, Patient patient) {
        if (patient != null && visits != null && (visits.size() > 0)) {
            List<Visit> patientVisits = visitService.getVisitsByPatient(patient, true, false);
            if ((patientVisits != null) && (patientVisits.size() > 0)) {
                ArrayList<Integer> allVisits = new ArrayList<Integer>();
                int j = 0;
                for (Visit visit : patientVisits) {
                    allVisits.add(j++, visit.getId());
                }
                if (allVisits.containsAll(visits)) {
                    //find the index of the first candidate for a consecutive visit
                    int i = allVisits.indexOf(visits.get(0));
                    //make sure there are still more elements in the list than the the number of candidate consecutives
                    if ((allVisits.size() - i) >= visits.size()) {
                        for (Integer candidateVisit : visits) {
                            if (allVisits.get(i).compareTo(candidateVisit) == 0) {
                                i++;
                            } else {
                                return false;
                            }
                        }
                        return true;
                    }
                }
            }
        }
        return false;
    }

    public Visit mergeConsecutiveVisits(List<Integer> visits, Patient patient) {
        if (areConsecutiveVisits(visits, patient)) {
            Visit mergedVisit = visitService.getVisit(visits.get(0));
            if (visits.size() > 1) {
                for (int i = 1; i < visits.size(); i++) {
                    mergedVisit = mergeVisits(mergedVisit, visitService.getVisit(visits.get(i)));
                }
            }
            return mergedVisit;
        }
        return null;
    }

    public Visit mergeVisits(Visit preferred, Visit nonPreferred) {
        // extend date range of winning
        if (OpenmrsUtil.compareWithNullAsEarliest(nonPreferred.getStartDatetime(),
                preferred.getStartDatetime()) < 0) {
            preferred.setStartDatetime(nonPreferred.getStartDatetime());
        }
        if (preferred.getStopDatetime() != null && OpenmrsUtil.compareWithNullAsLatest(preferred.getStopDatetime(),
                nonPreferred.getStopDatetime()) < 0) {
            preferred.setStopDatetime(nonPreferred.getStopDatetime());
        }

        // move encounters from losing into winning
        if (nonPreferred.getEncounters() != null) {
            for (Encounter e : nonPreferred.getEncounters()) {
                e.setPatient(preferred.getPatient());
                preferred.addEncounter(e);
                encounterService.saveEncounter(e);
            }
        }
        nonPreferred.setEncounters(null); // we need to manually the encounters from the non-preferred visit before voiding or all the encounters we just moved will also get voided!

        visitService.voidVisit(nonPreferred, "EMR - Merge Patients: merged into visit " + preferred.getVisitId());
        visitService.saveVisit(preferred);
        return preferred;
    }

    private void addProviders(Encounter encounter, Map<EncounterRole, ? extends Collection<Provider>> providers) {
        for (Map.Entry<EncounterRole, ? extends Collection<Provider>> entry : providers.entrySet()) {
            EncounterRole encounterRole = entry.getKey();
            for (Provider provider : entry.getValue()) {
                encounter.addProvider(encounterRole, provider);
            }
        }
    }

    private boolean hasAny(Map<?, ? extends Collection<?>> providers) {
        if (providers == null) {
            return false;
        }
        for (Collection<?> byType : providers.values()) {
            if (byType != null && byType.size() > 0) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void addPatientMergeAction(PatientMergeAction patientMergeAction) {
        if (this.patientMergeActions == null) {
            this.patientMergeActions = new ArrayList<PatientMergeAction>();
        }
        this.patientMergeActions.add(patientMergeAction);
    }

    @Override
    public void removePatientMergeAction(PatientMergeAction patientMergeAction) {
        if (this.patientMergeActions == null) {
            this.patientMergeActions = new ArrayList<PatientMergeAction>();
        }
        this.patientMergeActions.remove(patientMergeAction);
    }

    @Transactional
    @Override
    public Encounter createAdtEncounterFor(AdtAction action) {
        if (action.getVisit() == null || action.getLocation() == null || !hasAny(action.getProviders())) {
            throw new IllegalArgumentException("Must provide a visit, location, and provider");
        }

        VisitDomainWrapper visit = wrap(action.getVisit());

        action.getType().checkVisitValid(visit);

        Date adtDatetime = action.getActionDatetime();
        if (adtDatetime == null) {
            adtDatetime = new Date();
        }

        visit.errorIfOutsideVisit(adtDatetime, "ADT Datetime outside of visit bounds");

        EncounterType adtEncounterType = action.getType().getEncounterType(emrApiProperties);
        Form adtForm = action.getType().getForm(emrApiProperties);

        Encounter encounter = buildEncounter(adtEncounterType, visit.getVisit().getPatient(), action.getLocation(),
                adtForm, adtDatetime, null, null);
        addProviders(encounter, action.getProviders());

        visit.addEncounter(encounter);
        encounterService.saveEncounter(encounter);
        return encounter;
    }

    @Override
    @Deprecated // use new VisitDomainWrapperFactory instead (this service method has been delegated to use the new factory)
    public VisitDomainWrapper wrap(Visit visit) {
        return domainWrapperFactory.newVisitDomainWrapper(visit);
    }

    @Override
    @Transactional
    public VisitDomainWrapper createRetrospectiveVisit(Patient patient, Location location, Date startDatetime,
            Date stopDatetime) throws ExistingVisitDuringTimePeriodException {

        if (startDatetime.after(new Date())) {
            throw new IllegalArgumentException("emrapi.retrospectiveVisit.startDateCannotBeInFuture");
        }

        if (stopDatetime.after(new Date())) {
            throw new IllegalArgumentException("emrapi.retrospectiveVisit.stopDateCannotBeInFuture");
        }

        if (startDatetime.after(stopDatetime)) {
            throw new IllegalArgumentException("emrapi.retrospectiveVisit.endDateBeforeStartDateMessage");
        }

        if (hasVisitDuring(patient, location, startDatetime, stopDatetime)) {
            throw new ExistingVisitDuringTimePeriodException("emrapi.retrospectiveVisit.patientAlreadyHasVisit");
        }

        Visit visit = buildVisit(patient, location, startDatetime);
        visit.setStopDatetime(stopDatetime);

        return wrap(visitService.saveVisit(visit));
    }

    @Override
    @Transactional(readOnly = true)
    public List<VisitDomainWrapper> getVisits(Patient patient, Location location, Date startDatetime,
            Date endDatetime) {

        List<VisitDomainWrapper> visitDomainWrappers = new ArrayList<VisitDomainWrapper>();

        for (Visit visit : visitService.getVisits(
                Collections.singletonList(emrApiProperties.getAtFacilityVisitType()),
                Collections.singletonList(patient),
                Collections.singletonList(getLocationThatSupportsVisits(location)), null, null, endDatetime,
                startDatetime, null, null, true, false)) {
            visitDomainWrappers.add(wrap(visit));
        }

        return visitDomainWrappers;
    }

    @Override
    @Transactional(readOnly = true)
    public boolean hasVisitDuring(Patient patient, Location location, Date startDatetime, Date stopDatetime) {
        List<VisitDomainWrapper> visits = getVisits(patient, location, startDatetime, stopDatetime);
        return visits == null || visits.size() == 0 ? false : true;
    }

    @Override
    public List<Location> getInpatientLocations() {
        return locationService.getLocationsByTag(emrApiProperties.getSupportsAdmissionLocationTag());
    }

}