AppointmentService.java

/*
 * Copyright (c) 2002-2022, City of Paris
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice
 *     and the following disclaimer.
 *
 *  2. Redistributions in binary form must reproduce the above copyright notice
 *     and the following disclaimer in the documentation and/or other materials
 *     provided with the distribution.
 *
 *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
 *     contributors may be used to endorse or promote products derived from
 *     this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * License 1.0
 */
package fr.paris.lutece.plugins.appointment.service;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.StringJoiner;
import java.util.stream.Collectors;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;

import fr.paris.lutece.plugins.appointment.business.appointment.Appointment;
import fr.paris.lutece.plugins.appointment.business.appointment.AppointmentHome;
import fr.paris.lutece.plugins.appointment.business.appointment.AppointmentSlot;
import fr.paris.lutece.plugins.appointment.business.form.Form;
import fr.paris.lutece.plugins.appointment.business.slot.Slot;
import fr.paris.lutece.plugins.appointment.business.user.User;
import fr.paris.lutece.plugins.appointment.business.user.UserHome;
import fr.paris.lutece.plugins.appointment.service.listeners.AppointmentListenerManager;
import fr.paris.lutece.plugins.appointment.service.listeners.SlotListenerManager;
import fr.paris.lutece.plugins.appointment.web.dto.AppointmentDTO;
import fr.paris.lutece.plugins.appointment.web.dto.AppointmentFilterDTO;
import fr.paris.lutece.plugins.genericattributes.business.Field;
import fr.paris.lutece.plugins.genericattributes.business.Response;
import fr.paris.lutece.plugins.genericattributes.business.ResponseHome;
import fr.paris.lutece.plugins.genericattributes.service.entrytype.IEntryTypeService;
import fr.paris.lutece.portal.business.user.AdminUser;
import fr.paris.lutece.portal.business.user.AdminUserHome;
import fr.paris.lutece.portal.service.util.AppException;
import fr.paris.lutece.portal.service.util.AppLogService;
import fr.paris.lutece.portal.service.util.AppPropertiesService;
import fr.paris.lutece.portal.service.util.CryptoService;
import fr.paris.lutece.portal.service.workflow.WorkflowService;
import fr.paris.lutece.util.sql.TransactionManager;

/**
 * Service class for an appointment
 * 
 * @author Laurent Payen
 *
 */
public final class AppointmentService
{

    private static final String PROPERTY_REF_ENCRYPTION_ALGORITHM = "appointment.refEncryptionAlgorithm";
    private static final String CONSTANT_SHA256 = "SHA-256";
    private static final String PROPERTY_REF_SIZE_RANDOM_PART = "appointment.refSizeRandomPart";
    private static final String CONSTANT_SEPARATOR = "$";
    /**
     * Get the number of characters of the random part of appointment reference
     */
    private static final int CONSTANT_REF_SIZE_RANDOM_PART = 5;

    /**
     * Private constructor - this class does not need to be instantiated
     */
    private AppointmentService( )
    {
    }

    /**
     * Find all the appointments of the slots given in parameter
     * 
     * @param listSlot
     *            the list of slots
     * @return a list of the appointments on these slots
     */
    public static List<Appointment> findListAppointmentByListSlot( List<Slot> listSlot )
    {
        List<Integer> listIdSlot = listSlot.stream( ).map( Slot::getIdSlot ).collect( Collectors.toList( ) );

        return AppointmentHome.findByListIdSlot( listIdSlot );
    }

    /**
     * Find the appointments of a slot
     * 
     * @param nIdSlot
     *            the slot Id
     * @return the appointments of the slot
     */
    public static List<Appointment> findListAppointmentBySlot( int nIdSlot )
    {
        return AppointmentHome.findByIdSlot( nIdSlot );
    }

    /**
     * Find the appointments of a user
     * 
     * @param nIdUser
     *            the user Id
     * @return the appointment of the user
     */
    public static List<Appointment> findListAppointmentByUserId( int nIdUser )
    {
        return AppointmentHome.findByIdUser( nIdUser );

    }

    /**
     * Find the appointments of a user by guid
     * 
     * @param nIdUser
     *            the user guid
     * @return the appointment of the guid
     */
    public static List<Appointment> findListAppointmentByUserGuid( String strGuidUser )
    {
        List<Appointment> listAppointment = AppointmentHome.findByGuidUser( strGuidUser );
        for ( Appointment appointment : listAppointment )
        {
            List<Slot> listSlot = SlotService.findListSlotByIdAppointment( appointment.getIdAppointment( ) );
            appointment.setSlot( listSlot );

        }
        return listAppointment;

    }

    /**
     * Find the appointments by form
     * 
     * @param nIdForm
     *            the form Id
     * 
     * @return the appointments that matches the criteria
     */
    public static List<Appointment> findListAppointmentByIdForm( int nIdForm )
    {
        return AppointmentHome.findByIdForm( nIdForm );
    }

    /**
     * Build and create in database an appointment from the dto
     * 
     * @param appointmentDTO
     *            the appointment dto
     * @param user
     *            the user
     * @param slot
     *            the slot
     * @return the appointment created
     */
    static Appointment buildAndCreateAppointment( AppointmentDTO appointmentDTO, User user )
    {
        Appointment appointment = new Appointment( );
        if ( appointmentDTO.getIdAppointment( ) != 0 )
        {
            appointment = AppointmentService.findAppointmentById( appointmentDTO.getIdAppointment( ) );
        }
        if ( appointmentDTO.getIdAdminUser( ) != 0 )
        {
            appointment.setIdAdminUser( appointmentDTO.getIdAdminUser( ) );
        }
        if ( appointmentDTO.getAdminUserCreate( ) != null )
        {
            appointment.setAdminUserCreate( appointmentDTO.getAdminUserCreate( ) );
        }
        appointment.setListAppointmentSlot( appointmentDTO.getListAppointmentSlot( ) );
        appointment.setNbPlaces( appointmentDTO.getNbBookedSeats( ) );
        appointment.setIdUser( user.getIdUser( ) );
        appointment.setIsSurbooked( appointmentDTO.getIsSurbooked( ) );

        if ( appointment.getIdAppointment( ) == 0 )
        {
            appointment = AppointmentHome.create( appointment );
            String strEmailLastNameFirstName = new StringJoiner( StringUtils.SPACE ).add( user.getEmail( ) ).add( CONSTANT_SEPARATOR )
                    .add( user.getLastName( ) ).add( CONSTANT_SEPARATOR ).add( user.getFirstName( ) ).toString( );
            String strReference = appointment.getIdAppointment( ) + CryptoService
                    .encrypt( appointment.getIdAppointment( ) + strEmailLastNameFirstName,
                            AppPropertiesService.getProperty( PROPERTY_REF_ENCRYPTION_ALGORITHM, CONSTANT_SHA256 ) )
                    .substring( 0, AppPropertiesService.getPropertyInt( PROPERTY_REF_SIZE_RANDOM_PART, CONSTANT_REF_SIZE_RANDOM_PART ) );

            Form form = FormService.findFormLightByPrimaryKey( appointmentDTO.getIdForm( ) );
            if ( StringUtils.isNotEmpty( form.getReference( ) ) )
            {
                strReference = form.getReference( ) + strReference;
            }
            appointment.setReference( strReference );
            appointment = AppointmentHome.update( appointment );
        }
        else
        {
            AppointmentHome.updateAppointmentDate( appointment );

        }
        return appointment;
    }

    /**
     * Find an appointment by its primary key
     * 
     * @param nIdAppointment
     *            the appointment Id
     * @return the appointment
     */
    public static Appointment findAppointmentById( int nIdAppointment )
    {
        return AppointmentHome.findByPrimaryKey( nIdAppointment );
    }

    /**
     * Find an appointment by its reference
     * 
     * @param strReference
     *            the appointment Reference
     * @return the appointment
     */
    public static Appointment findAppointmentByReference( String strReference )
    {
        return AppointmentHome.findByReference( strReference );
    }

    /**
     * Find a list of appointments matching the filter
     * 
     * @param appointmentFilter
     *            the filter
     * @return a list of appointments
     */
    public static List<AppointmentDTO> findListAppointmentsDTOByFilter( AppointmentFilterDTO appointmentFilter )
    {
        List<AppointmentDTO> listAppointmentsDTO = new ArrayList<>( );
        for ( Appointment appointment : AppointmentHome.findByFilter( appointmentFilter ) )
        {
            listAppointmentsDTO.add( buildAppointmentDTO( appointment ) );
        }
        return listAppointmentsDTO;
    }

    /**
     * Find a list of appointments by id category and mail
     * 
     * @param nIdCategory
     *            the id category
     * @param mail
     *            the mail
     * @return list of appointments
     */
    public static List<AppointmentDTO> findAppointmentByMailAndCategory( int nIdCategory, String mail )
    {
        List<AppointmentDTO> listAppointmentsDTO = new ArrayList<>( );
        for ( Appointment appointment : AppointmentHome.findByMailAndCategory( nIdCategory, mail ) )
        {
            listAppointmentsDTO.add( buildAppointmentDTO( appointment ) );
        }
        return listAppointmentsDTO;
    }

    /**
     * Find a list of appointments matching the filter
     * 
     * @param appointmentFilter
     * @return a list of appointments
     */
    public static List<Appointment> findListAppointmentsByFilter( AppointmentFilterDTO appointmentFilter )
    {
        return AppointmentHome.findByFilter( appointmentFilter );
    }

    /**
     * Find a list of appointments ids matching the filter
     *
     * @param appointmentFilter
     *         the filter
     * @return a list of appointments
     */
    public static List<Integer> findListAppointmentsIdsByFilter( AppointmentFilterDTO appointmentFilter )
    {
        return AppointmentHome.findIdsByFilter( appointmentFilter );
    }

    /**
     * Build an appointment dto from an appointment business object
     * 
     * @param appointment
     *            the appointment business object
     * @return the appointment DTO
     */
    private static AppointmentDTO buildAppointmentDTO( Appointment appointment )
    {
        AppointmentDTO appointmentDTO = new AppointmentDTO( );
        appointmentDTO.setIdForm( appointment.getSlot( ).get( 0 ).getIdForm( ) );
        appointmentDTO.setIdUser( appointment.getIdUser( ) );
        appointmentDTO.setListAppointmentSlot( appointment.getListAppointmentSlot( ) );
        appointmentDTO.setIdAppointment( appointment.getIdAppointment( ) );
        appointmentDTO.setFirstName( appointment.getUser( ).getFirstName( ) );
        appointmentDTO.setLastName( appointment.getUser( ).getLastName( ) );
        appointmentDTO.setEmail( appointment.getUser( ).getEmail( ) );
        appointmentDTO.setPhoneNumber( appointment.getUser( ).getPhoneNumber( ) );
        appointmentDTO.setGuid( appointment.getUser( ).getGuid( ) );
        appointmentDTO.setReference( appointment.getReference( ) );
        LocalDateTime startingDateTime = AppointmentUtilities.getStartingDateTime( appointment );
        LocalDateTime endingDateTime = AppointmentUtilities.getEndingDateTime( appointment );
        appointmentDTO.setStartingDateTime( startingDateTime );
        appointmentDTO.setEndingDateTime( endingDateTime );
        appointmentDTO.setDateOfTheAppointment( startingDateTime.toLocalDate( ).format( Utilities.getFormatter( ) ) );
        appointmentDTO.setStartingTime( startingDateTime.toLocalTime( ) );
        appointmentDTO.setEndingTime( endingDateTime.toLocalTime( ) );
        appointmentDTO.setIsCancelled( appointment.getIsCancelled( ) );
        appointmentDTO.setNbBookedSeats( appointment.getNbPlaces( ) );
        for ( Slot slt : appointment.getSlot( ) )
        {

            SlotService.addDateAndTimeToSlot( slt );
        }
        appointmentDTO.setSlot( appointment.getSlot( ) );
        appointmentDTO.setUser( appointment.getUser( ) );
        if ( appointment.getIdAdminUser( ) != 0 )
        {
            AdminUser adminUser = AdminUserHome.findByPrimaryKey( appointment.getIdAdminUser( ) );
            if ( adminUser != null )
            {
                appointmentDTO.setIdAdminUser( adminUser.getUserId());
                appointmentDTO.setAdminUser(
                        new StringBuilder( adminUser.getFirstName( ) + org.apache.commons.lang3.StringUtils.SPACE + adminUser.getLastName( ) ).toString( ) );
            }
        }
        else
        {
            appointmentDTO.setAdminUser( StringUtils.EMPTY );
        }
        appointmentDTO.setAdminUserCreate( appointment.getAdminUserCreate( ) );
        appointmentDTO.setDateAppointmentTaken( appointment.getDateAppointmentTaken( ) );
        return appointmentDTO;
    }

    /**
     * Fill the appointment data transfer object with complementary appointment responses
     * 
     * @param appointmentDto
     *            The appointmentDTO object
     */
    public static void addAppointmentResponses( AppointmentDTO appointmentDto )
    {
        // Load the response list in the DTO
        appointmentDto.setListResponse( AppointmentResponseService.findListResponse( appointmentDto.getIdAppointment( ) ) );
    }

    /**
     * Delete an appointment (and update the number of remaining places of the related slot)
     * 
     * @param nIdAppointment
     *            the id of the appointment to delete
     * @throws Exception
     *             the exception
     */
    public static void deleteAppointment( int nIdAppointment )
    {
        TransactionManager.beginTransaction( AppointmentPlugin.getPlugin( ) );
        try
        {
            Appointment appointmentToDelete = AppointmentHome.findByPrimaryKey( nIdAppointment );
            deleteWorkflowResource( nIdAppointment );
            if ( !appointmentToDelete.getIsCancelled( ) )
            {
                for ( AppointmentSlot appSlot : appointmentToDelete.getListAppointmentSlot( ) )
                {
                    // Need to update the nb remaining places of the related slot
                    SlotSafeService.updateRemaningPlacesWithAppointmentMovedDeletedOrCanceled( appSlot.getNbPlaces( ), appSlot.getIdSlot( ) );
                }

            }
            // Need to delete also the responses linked to this appointment
            AppointmentResponseService.removeResponsesByIdAppointment( nIdAppointment );
            AppointmentService.deleteAppointment( appointmentToDelete );
            UserHome.delete( appointmentToDelete.getIdUser( ) );
            TransactionManager.commitTransaction( AppointmentPlugin.getPlugin( ) );
            AppointmentListenerManager.notifyListenersAppointmentRemoval( nIdAppointment );
            for ( AppointmentSlot appSlot : appointmentToDelete.getListAppointmentSlot( ) )
            {
                SlotListenerManager.notifyListenersSlotChange( appSlot.getIdSlot( ) );
            }
        }
        catch( Exception e )
        {
            TransactionManager.rollBack( AppointmentPlugin.getPlugin( ) );
            AppLogService.error( "Error delete appointment " + e.getMessage( ), e );
            throw new AppException( e.getMessage( ), e );
        }
    }

    private static void deleteWorkflowResource( int nIdAppointment )
    {
        if ( WorkflowService.getInstance( ).isAvailable( ) )
        {
            try
            {
                WorkflowService.getInstance( ).doRemoveWorkFlowResource( nIdAppointment, Appointment.APPOINTMENT_RESOURCE_TYPE );
            }
            catch( Exception e )
            {
                AppLogService.error( "Error Workflow", e );
            }
        }
    }

    /**
     * Delete an appointment
     * 
     * @param appointment
     *            the appointment to delete
     */
    private static void deleteAppointment( Appointment appointment )
    {
        AppointmentHome.delete( appointment.getIdAppointment( ) );
    }

    /**
     * Build an appointment DTO from the id of an appointment business object
     * 
     * @param nIdAppointment
     *            the id of the appointment
     * @return the appointment DTO
     */
    public static AppointmentDTO buildAppointmentDTOFromIdAppointment( int nIdAppointment )
    {
        Appointment appointment = AppointmentService.findAppointmentById( nIdAppointment );
        User user = UserService.findUserById( appointment.getIdUser( ) );
        List<Slot> listSlot = SlotService.findListSlotByIdAppointment( appointment.getIdAppointment( ) );
        appointment.setSlot( listSlot );
        appointment.setUser( user );
        return buildAppointmentDTO( appointment );
    }

    /**
     * Build an appointment DTO from the reference of an appointment business object
     * 
     * @param refAppointment
     *            the reference of the appointment
     * @return the appointment DTO, or null if the appointment wasn't found
     */
    public static AppointmentDTO buildAppointmentDTOFromRefAppointment( String refAppointment )
    {
        Appointment appointment = AppointmentService.findAppointmentByReference( refAppointment );

        if ( appointment == null )
        {
        	return null;
        }
        User user = UserService.findUserById( appointment.getIdUser( ) );
        List<Slot> listSlot = SlotService.findListSlotByIdAppointment( appointment.getIdAppointment( ) );
        appointment.setSlot( listSlot );
        appointment.setUser( user );

        return buildAppointmentDTO( appointment );
    }

    /**
     * Update an appointment DTO in database
     * 
     * @param nIdappointment
     *            the id appointment
     * @param user
     *            the user
     * @param listResponse
     *            the listResponse
     * @param deleteBoOnly
     *            if the action is Bo only
     */
    public static void updateAppointmentDTO( int nIdappointment, User user, List<Response> listResponse, boolean deleteBoOnly )
    {
        UserHome.update( user );
        AppointmentResponseService.removeUpdatableResponsesOnly( nIdappointment, deleteBoOnly );
        if ( CollectionUtils.isNotEmpty( listResponse ) )
        {
            for ( Response response : listResponse )
            {
                Field updatableField = response.getEntry( ).getFieldByCode( IEntryTypeService.FIELD_IS_UPDATABLE );
                // In case the Response is not updatable, make sure that no new Response is created for it
                if( updatableField != null && !Boolean.valueOf( updatableField.getValue( ) ) )
                {
                    // Do nothing with this Response and process the next one
                    continue;
                }
                ResponseHome.create( response );
                AppointmentResponseService.insertAppointmentResponse( nIdappointment, response.getIdResponse( ) );
            }
        }
        AppointmentListenerManager.notifyListenersAppointmentUpdated( nIdappointment );
    }

    /**
     * Update an appointment in database
     * 
     * @param appointment
     *            the appointment to update
     */
    public static void updateAppointment( Appointment appointment )
    {
        boolean statusUpdated = false;
        // Get the old appointment in db
        Appointment oldAppointment = AppointmentService.findAppointmentById( appointment.getIdAppointment( ) );
        // If the update concerns a cancellation of the appointment
        TransactionManager.beginTransaction( AppointmentPlugin.getPlugin( ) );
        try
        {
            if ( !oldAppointment.getIsCancelled( ) && appointment.getIsCancelled( ) )
            {
                for ( AppointmentSlot appSlot : oldAppointment.getListAppointmentSlot( ) )
                {
                    // Need to update the nb remaining places of the related slot
                    SlotSafeService.updateRemaningPlacesWithAppointmentMovedDeletedOrCanceled( appSlot.getNbPlaces( ), appSlot.getIdSlot( ) );
                }
                statusUpdated = true;
            }
            else
                if ( oldAppointment.getIsCancelled( ) && !appointment.getIsCancelled( ) )
                {
                    for ( AppointmentSlot appSlot : oldAppointment.getListAppointmentSlot( ) )
                    {
                        // Need to update the nb remaining places of the related slot
                        SlotSafeService.updateRemaningPlacesWithAppointmentReactivated( appSlot.getNbPlaces( ), appSlot.getIdSlot( ) );
                    }
                    statusUpdated = true;
                }
            AppointmentHome.update( appointment );
            TransactionManager.commitTransaction( AppointmentPlugin.getPlugin( ) );
            AppointmentListenerManager.notifyListenersAppointmentUpdated( appointment.getIdAppointment( ) );
            if ( statusUpdated )
            {
                for ( AppointmentSlot appSlot : oldAppointment.getListAppointmentSlot( ) )
                {
                    SlotListenerManager.notifyListenersSlotChange( appSlot.getIdSlot( ) );
                }
            }
        }
        catch( Exception e )
        {
            TransactionManager.rollBack( AppointmentPlugin.getPlugin( ) );
            AppLogService.error( "Error update appointment " + e.getMessage( ), e );
            throw new AppException( e.getMessage( ), e );
        }

    }

    /**
     * Save an appointment in database
     * 
     * @param appointmentDTO
     *            the appointment dto
     * @return the id of the appointment saved
     * @throws Exception
     */
    public static int saveAppointment( AppointmentDTO appointmentDTO )
    {
        return SlotSafeService.saveAppointment( appointmentDTO, null );

    }

    /**
     * Save an appointment in database
     * 
     * @param appointmentDTO
     *            the appointment dto
     * @return the id of the appointment saved
     * @throws Exception
     */
    public static int saveAppointment( AppointmentDTO appointmentDTO, HttpServletRequest request )
    {
        return SlotSafeService.saveAppointment( appointmentDTO, request );

    }

    public static void buildListAppointmentSlot( AppointmentDTO appointmentDTO )
    {

        List<AppointmentSlot> listApptSlot = new ArrayList<>( );
        int nIdAppointment = appointmentDTO.getIdAppointment( );
        int nNumberPlace = appointmentDTO.getNbBookedSeats( );
        List<Slot> listSlot = appointmentDTO.getSlot( );

        if ( listSlot.size( ) > 1 )
        {

            nNumberPlace = 1;
            listSlot.sort( ( slot1, slot2 ) -> slot1.getStartingDateTime( ).compareTo( slot2.getStartingDateTime( ) ) );
        }
        for ( Slot slot : listSlot )
        {

            AppointmentSlot apptSlot = new AppointmentSlot( );
            apptSlot.setIdAppointment( nIdAppointment );
            apptSlot.setIdSlot( slot.getIdSlot( ) );

            apptSlot.setNbPlaces( nNumberPlace );
            listApptSlot.add( apptSlot );
        }
        appointmentDTO.setListAppointmentSlot( listApptSlot );
    }

}