SuspiciousIdentityService.java

/*
 * Copyright (c) 2002-2024, 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.identitystore.modules.quality.service;

import fr.paris.lutece.plugins.identitystore.business.application.ClientApplication;
import fr.paris.lutece.plugins.identitystore.business.duplicates.suspicions.SuspiciousIdentity;
import fr.paris.lutece.plugins.identitystore.business.duplicates.suspicions.SuspiciousIdentityHome;
import fr.paris.lutece.plugins.identitystore.business.identity.Identity;
import fr.paris.lutece.plugins.identitystore.business.rules.duplicate.DuplicateRule;
import fr.paris.lutece.plugins.identitystore.modules.quality.rs.SuspiciousIdentityMapper;
import fr.paris.lutece.plugins.identitystore.service.listeners.IdentityStoreNotifyListenerService;
import fr.paris.lutece.plugins.identitystore.service.user.InternalUserService;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.Page;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.RequestAuthor;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.ResponseStatusType;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.crud.SuspiciousIdentityChangeRequest;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.crud.SuspiciousIdentityDto;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.crud.SuspiciousIdentityExcludeRequest;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.crud.SuspiciousIdentitySearchRequest;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.history.IdentityChangeType;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.lock.SuspiciousIdentityLockRequest;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.util.Constants;
import fr.paris.lutece.plugins.identitystore.web.exception.IdentityStoreException;
import fr.paris.lutece.plugins.identitystore.web.exception.RequestFormatException;
import fr.paris.lutece.plugins.identitystore.web.exception.ResourceNotFoundException;
import fr.paris.lutece.portal.service.security.AccessLogService;
import fr.paris.lutece.portal.service.security.AccessLoggerConstants;
import fr.paris.lutece.util.sql.TransactionManager;
import org.apache.commons.lang3.tuple.Pair;

import java.sql.Timestamp;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class SuspiciousIdentityService
{
    // EVENTS FOR ACCESS LOGGING
    private static final String CREATE_SUSPICIOUS_IDENTITY_EVENT_CODE = "CREATE_SUSPICIOUS_IDENTITY";
    private static final String SEARCH_SUSPICIOUS_IDENTITY_EVENT_CODE = "SEARCH_SUSPICIOUS_IDENTITY";
    private static final String LOCK_SUSPICIOUS_IDENTITY_EVENT_CODE = "LOCK_SUSPICIOUS_IDENTITY";
    private static final String UNLOCK_SUSPICIOUS_IDENTITY_EVENT_CODE = "UNLOCK_SUSPICIOUS_IDENTITY";
    private static final String EXCLUDE_SUSPICIOUS_IDENTITY_EVENT_CODE = "EXCLUDE_SUSPICIOUS_IDENTITY";
    private static final String ACCESS_SUSPICIOUS_IDENTITY_LOCK_STATUS = "ACCESS_SUSPICIOUS_IDENTITY_LOCK_STATUS";
    private static final String SPECIFIC_ORIGIN = "BO";

    // SERVICES
    private final IdentityStoreNotifyListenerService _identityStoreNotifyListenerService = IdentityStoreNotifyListenerService.instance( );
    private final InternalUserService _internalUserService = InternalUserService.getInstance( );
    private static SuspiciousIdentityService _instance;

    public static SuspiciousIdentityService instance( )
    {
        if ( _instance == null )
        {
            _instance = new SuspiciousIdentityService( );
        }
        return _instance;
    }

    /**
     * Creates a new {@link SuspiciousIdentity} according to the given parameters
     *
     * @param request
     *            the {@link SuspiciousIdentityChangeRequest} holding the parameters of the suspicious identity change request
     * @param identity
     *            the {@link Identity} wanted to be marked as suspicious
     * @param duplicateRule
     *            the {@link DuplicateRule} used to mark the suspicious identity
     * @param clientCode
     *            code of the {@link ClientApplication} requesting the change
     * @param author
     *            the author
     * @return the created {@link SuspiciousIdentityDto}
     * @throws IdentityStoreException
     *             in case of error
     */
    public SuspiciousIdentityDto create( final SuspiciousIdentityChangeRequest request, final Identity identity, final DuplicateRule duplicateRule,
            final String clientCode, final RequestAuthor author ) throws IdentityStoreException
    {
        TransactionManager.beginTransaction( null );
        try
        {
            final SuspiciousIdentity suspiciousIdentity = new SuspiciousIdentity( );

            suspiciousIdentity.setDuplicateRuleCode( duplicateRule.getCode( ) );
            suspiciousIdentity.setIdDuplicateRule( duplicateRule.getId( ) );
            suspiciousIdentity.setCustomerId( identity.getCustomerId( ) );
            suspiciousIdentity.setCreationDate( Timestamp.from( Instant.now( ) ) );
            suspiciousIdentity.setLastUpdateDate( identity.getLastUpdateDate( ) );

            SuspiciousIdentityHome.create( suspiciousIdentity );

            TransactionManager.commitTransaction( null );

            final Map<String, String> metadata = new HashMap<>( request.getSuspiciousIdentity( ).getMetadata( ) );
            metadata.put( Constants.METADATA_DUPLICATE_RULE_CODE, duplicateRule.getCode( ) );
            _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.MARKED_SUSPICIOUS, identity,
                    ResponseStatusType.SUCCESS.name( ), ResponseStatusType.SUCCESS.name( ), author, clientCode, metadata );

            AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_CREATE, CREATE_SUSPICIOUS_IDENTITY_EVENT_CODE,
                    _internalUserService.getApiUser( author, clientCode ), request, SPECIFIC_ORIGIN );

            return SuspiciousIdentityMapper.toDto( suspiciousIdentity );
        }
        catch( final Exception e )
        {
            TransactionManager.rollBack( null );
            throw new IdentityStoreException( e.getMessage( ), Constants.PROPERTY_REST_ERROR_DURING_TREATMENT );
        }
    }

    public Pair<List<SuspiciousIdentityDto>, Page> search( final SuspiciousIdentitySearchRequest request, final String clientCode, final RequestAuthor author )
            throws IdentityStoreException
    {
        final List<SuspiciousIdentity> fullSuspiciousList = SuspiciousIdentityHome.getSuspiciousIdentitysList( request.getRuleCode( ), request.getAttributes( ),
                request.getMax( ), request.getRulePriority( ) );

        if ( fullSuspiciousList == null || fullSuspiciousList.isEmpty( ) )
        {
            throw new ResourceNotFoundException( "No suspicious identity found", Constants.PROPERTY_REST_ERROR_NO_SUSPICIOUS_IDENTITY_FOUND );
        }

        final List<SuspiciousIdentityDto> suspiciousIdentitiesToReturn;
        final Page pagination;
        if ( request.getPage( ) != null && request.getSize( ) != null )
        {
            final int totalRecords = fullSuspiciousList.size( );
            final int totalPages = (int) Math.ceil( (double) totalRecords / request.getSize( ) );

            if ( totalPages > 0 && request.getPage( ) > totalPages )
            {
                throw new RequestFormatException( "Pagination index should not exceed total number of pages.", Constants.PROPERTY_REST_PAGINATION_END_ERROR );
            }

            final int start = ( request.getPage( ) - 1 ) * request.getSize( );
            final int end = Math.min( start + request.getSize( ), totalRecords );
            suspiciousIdentitiesToReturn = fullSuspiciousList.subList( start, end ).stream( ).map( SuspiciousIdentityMapper::toDto )
                    .collect( Collectors.toList( ) );

            pagination = new Page( );
            pagination.setTotalPages( totalPages );
            pagination.setTotalRecords( totalRecords );
            pagination.setCurrentPage( request.getPage( ) );
            if( totalPages > 0 )
            {
                pagination.setNextPage( request.getPage() == totalPages ? null : request.getPage() + 1 );
                pagination.setPreviousPage( request.getPage() > 1 ? request.getPage() - 1 : null );
            }
        }
        else
        {
            suspiciousIdentitiesToReturn = fullSuspiciousList.stream( ).map( SuspiciousIdentityMapper::toDto ).collect( Collectors.toList( ) );
            pagination = null;
        }

        for ( final SuspiciousIdentityDto suspiciousIdentity : suspiciousIdentitiesToReturn )
        {
            AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_READ, SEARCH_SUSPICIOUS_IDENTITY_EVENT_CODE,
                    _internalUserService.getApiUser( author, clientCode ), suspiciousIdentity, SPECIFIC_ORIGIN );
        }
        return Pair.of( suspiciousIdentitiesToReturn, pagination );
    }

    public boolean lock( final SuspiciousIdentityLockRequest request, final SuspiciousIdentity suspiciousIdentity, final String strClientCode,
            final RequestAuthor author ) throws IdentityStoreException
    {
        TransactionManager.beginTransaction( null );
        try
        {
            final boolean locked = SuspiciousIdentityHome.manageLock( suspiciousIdentity.getCustomerId(), author.getName( ), author.getType( ).name( ), request.isLocked( ) );
            TransactionManager.commitTransaction( null );
            AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY,
                    request.isLocked( ) ? LOCK_SUSPICIOUS_IDENTITY_EVENT_CODE : UNLOCK_SUSPICIOUS_IDENTITY_EVENT_CODE,
                    _internalUserService.getApiUser( author, strClientCode ), request, SPECIFIC_ORIGIN );
            return locked;
        }
        catch( final Exception e )
        {
            TransactionManager.rollBack( null );
            throw new IdentityStoreException( e.getMessage( ), Constants.PROPERTY_REST_ERROR_DURING_TREATMENT );
        }
    }

    public void exclude( final SuspiciousIdentityExcludeRequest request, final Identity firstIdentity, final Identity secondIdentity, final String clientCode,
            final RequestAuthor author ) throws IdentityStoreException
    {
        TransactionManager.beginTransaction( null );
        try
        {
            // flag the 2 identities: manage the list of identities to exclude (supposed to be a field at the identity level)
            SuspiciousIdentityHome.exclude( firstIdentity.getCustomerId( ), secondIdentity.getCustomerId( ), author.getType( ).name( ), author.getName( ) );

            TransactionManager.commitTransaction( null );

            // First identity history
            final Map<String, String> metadata = new HashMap<>( );
            metadata.put( Constants.METADATA_EXCLUDED_CUID_KEY, secondIdentity.getCustomerId( ) );
            _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.EXCLUDED, firstIdentity, ResponseStatusType.SUCCESS.name( ),
                    ResponseStatusType.SUCCESS.name( ), author, clientCode, metadata );

            // Second identity history
            final Map<String, String> metadata2 = new HashMap<>( );
            metadata2.put( Constants.METADATA_EXCLUDED_CUID_KEY, firstIdentity.getCustomerId( ) );
            _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.EXCLUDED, secondIdentity, ResponseStatusType.SUCCESS.name( ),
                    ResponseStatusType.SUCCESS.name( ), author, clientCode, metadata2 );

            AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, EXCLUDE_SUSPICIOUS_IDENTITY_EVENT_CODE,
                    _internalUserService.getApiUser( author, clientCode ), request, SPECIFIC_ORIGIN );
        }
        catch( final Exception e )
        {
            TransactionManager.rollBack( null );
            throw new IdentityStoreException( e.getMessage( ), Constants.PROPERTY_REST_ERROR_DURING_TREATMENT );
        }
    }

    public void cancelExclusion( final SuspiciousIdentityExcludeRequest request, final Identity firstIdentity, final Identity secondIdentity,
            final String clientCode, final RequestAuthor author ) throws IdentityStoreException
    {
        TransactionManager.beginTransaction( null );
        try
        {
            // remove the exclusion
            SuspiciousIdentityHome.removeExcludedIdentities( firstIdentity.getCustomerId( ), secondIdentity.getCustomerId( ) );

            TransactionManager.commitTransaction( null );

            // First identity history
            final Map<String, String> metadata = new HashMap<>( );
            metadata.put( Constants.METADATA_EXCLUDED_CUID_KEY, secondIdentity.getCustomerId( ) );
            _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.EXCLUSION_CANCELLED, firstIdentity,
                    ResponseStatusType.SUCCESS.name( ), ResponseStatusType.SUCCESS.name( ), author, clientCode, metadata );

            // Second identity history
            final Map<String, String> metadata2 = new HashMap<>( );
            metadata2.put( Constants.METADATA_EXCLUDED_CUID_KEY, firstIdentity.getCustomerId( ) );
            _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.EXCLUSION_CANCELLED, secondIdentity,
                    ResponseStatusType.SUCCESS.name( ), ResponseStatusType.SUCCESS.name( ), author, clientCode, metadata2 );

            AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, EXCLUDE_SUSPICIOUS_IDENTITY_EVENT_CODE,
                    _internalUserService.getApiUser( author, clientCode ), request, SPECIFIC_ORIGIN );
        }
        catch( final Exception e )
        {
            TransactionManager.rollBack( null );
            throw new IdentityStoreException( e.getMessage( ), Constants.PROPERTY_REST_ERROR_DURING_TREATMENT );
        }
    }

    public boolean hasSuspicious( final List<String> customerIds )
    {
        return SuspiciousIdentityHome.hasSuspicious( customerIds );
    }

    public List<SuspiciousIdentityDto> getSuspiciousIdentity( final List<String> customerIds )
    {
        final List<SuspiciousIdentity> suspiciousIdentities = SuspiciousIdentityHome.selectByCustomerIDs(customerIds);
        return suspiciousIdentities.stream( ).map( SuspiciousIdentityMapper::toDto ).collect( Collectors.toList( ) );
    }
}