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.duplicates.suspicions.SuspiciousIdentityLockedException;
import fr.paris.lutece.plugins.identitystore.business.identity.Identity;
import fr.paris.lutece.plugins.identitystore.business.identity.IdentityHome;
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.duplicate.DuplicateRuleService;
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.RequestAuthor;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.crud.SuspiciousIdentityChangeRequest;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.crud.SuspiciousIdentityChangeResponse;
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.SuspiciousIdentityExcludeResponse;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.crud.SuspiciousIdentitySearchRequest;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.crud.SuspiciousIdentitySearchResponse;
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.dto.lock.SuspiciousIdentityLockResponse;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.util.Constants;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.util.ResponseStatusFactory;
import fr.paris.lutece.plugins.identitystore.web.exception.IdentityNotFoundException;
import fr.paris.lutece.plugins.identitystore.web.exception.IdentityStoreException;
import fr.paris.lutece.portal.service.security.AccessLogService;
import fr.paris.lutece.portal.service.security.AccessLoggerConstants;
import fr.paris.lutece.portal.service.util.AppPropertiesService;
import fr.paris.lutece.util.sql.TransactionManager;

import java.sql.Timestamp;
import java.time.Instant;
import java.util.Collections;
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 SPECIFIC_ORIGIN = "BO";

    // SERVICES
    private final String externalDeclarationRuleCode = AppPropertiesService.getProperty( "identitystore-quality.external.duplicates.rule.code" );
    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 {@link SuspiciousIdentityChangeRequest}
     *
     * @param request
     *            the {@link SuspiciousIdentityChangeRequest} holding the parameters of the suspicious identity change request
     * @param clientCode
     *            code of the {@link ClientApplication} requesting the change
     * @param response
     *            the {@link SuspiciousIdentityChangeResponse} holding the status of the execution of the request
     * @return the created {@link SuspiciousIdentity}
     * @throws IdentityStoreException
     *             in case of error
     */
    public void create( final SuspiciousIdentityChangeRequest request, final String clientCode, final RequestAuthor author,
            final SuspiciousIdentityChangeResponse response )
    {
        // TODO check if the application has the right to create a suspicious identity
        /*
         * if ( !_serviceContractService.canCreateSuspiciousIdentity( clientCode ) ) { response.setStatus( IdentityChangeStatus.FAILURE ); response.setMessage(
         * "The client application is not authorized to create an identity." ); return null; }
         */
        TransactionManager.beginTransaction( null );
        boolean marked = false;
        try
        {
            final SuspiciousIdentity suspiciousIdentity = new SuspiciousIdentity( );
            final String requestRuleCode = request.getSuspiciousIdentity( ).getDuplicationRuleCode( );
            final String ruleCode = requestRuleCode != null ? requestRuleCode : externalDeclarationRuleCode;

            final Identity identity = IdentityHome.findByCustomerId( request.getSuspiciousIdentity( ).getCustomerId( ) );
            if ( identity == null )
            {
                response.setStatus( ResponseStatusFactory.notFound( ).setMessageKey( Constants.PROPERTY_REST_ERROR_IDENTITY_NOT_FOUND ) );
            }
            else
            {
                final DuplicateRule duplicateRule = DuplicateRuleService.instance( ).get( ruleCode );
                suspiciousIdentity.setDuplicateRuleCode( ruleCode );
                suspiciousIdentity.setIdDuplicateRule( duplicateRule.getId( ) );
                suspiciousIdentity.setCustomerId( request.getSuspiciousIdentity( ).getCustomerId( ) );
                suspiciousIdentity.setCreationDate( Timestamp.from( Instant.now( ) ) );
                suspiciousIdentity.setLastUpdateDate( identity.getLastUpdateDate( ) );

                SuspiciousIdentityHome.create( suspiciousIdentity );

                response.setSuspiciousIdentity( SuspiciousIdentityMapper.toDto( suspiciousIdentity ) );
                response.setStatus( ResponseStatusFactory.success( ).setMessageKey( Constants.PROPERTY_REST_INFO_SUCCESSFUL_OPERATION ) );
                marked = true;
            }
            TransactionManager.commitTransaction( null );
            if ( marked )
            {
                final Map<String, String> metadata = new HashMap<>( request.getSuspiciousIdentity( ).getMetadata( ) );
                metadata.put( Constants.METADATA_DUPLICATE_RULE_CODE, ruleCode );
                _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.MARKED_SUSPICIOUS, identity,
                        response.getStatus( ).getType( ).name( ), response.getStatus( ).getMessage( ), author, clientCode, metadata );
            }
            AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_CREATE, CREATE_SUSPICIOUS_IDENTITY_EVENT_CODE,
                    _internalUserService.getApiUser( author, clientCode ), request, SPECIFIC_ORIGIN );
        }
        catch( Exception e )
        {
            TransactionManager.rollBack( null );
            response.setStatus(
                    ResponseStatusFactory.failure( ).setMessage( e.getMessage( ) ).setMessageKey( Constants.PROPERTY_REST_ERROR_DURING_TREATMENT ) );
        }
    }

    public void search( final SuspiciousIdentitySearchRequest request, String clientCode, final RequestAuthor author,
            final SuspiciousIdentitySearchResponse response ) throws IdentityStoreException
    {
        // TODO check if the application has the right to search a suspicious identity
        final List<SuspiciousIdentity> suspiciousIdentitysList = SuspiciousIdentityHome.getSuspiciousIdentitysList( request.getRuleCode( ),
                request.getAttributes( ), request.getMax( ), request.getRulePriority( ) );

        if ( suspiciousIdentitysList == null || suspiciousIdentitysList.isEmpty( ) )
        {
            response.setStatus( ResponseStatusFactory.noResult( ).setMessageKey( Constants.PROPERTY_REST_ERROR_NO_SUSPICIOUS_IDENTITY_FOUND ) );
            response.setSuspiciousIdentities( Collections.emptyList( ) );
        }
        else
        {
            response.setStatus( ResponseStatusFactory.ok( ).setMessageKey( Constants.PROPERTY_REST_INFO_SUCCESSFUL_OPERATION ) );
            response.setSuspiciousIdentities( suspiciousIdentitysList.stream( ).map( SuspiciousIdentityMapper::toDto ).collect( Collectors.toList( ) ) );
        }
        for ( final SuspiciousIdentityDto suspiciousIdentity : response.getSuspiciousIdentities( ) )
        {
            AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_READ, SEARCH_SUSPICIOUS_IDENTITY_EVENT_CODE,
                    _internalUserService.getApiUser( author, clientCode ), suspiciousIdentity, SPECIFIC_ORIGIN );
        }
    }

    public void lock( SuspiciousIdentityLockRequest request, String strClientCode, final RequestAuthor author, SuspiciousIdentityLockResponse response )
    {
        TransactionManager.beginTransaction( null );
        try
        {
            final boolean locked = SuspiciousIdentityHome.manageLock( request.getCustomerId( ), author.getName( ), author.getType( ).name( ),
                    request.isLocked( ) );
            response.setLocked( locked );
            response.setStatus( ResponseStatusFactory.success( ).setMessageKey( Constants.PROPERTY_REST_INFO_SUCCESSFUL_OPERATION ) );
            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 );
        }
        catch( final SuspiciousIdentityLockedException e )
        {
            response.setLocked( false );
            response.setStatus(
                    ResponseStatusFactory.conflict( ).setMessage( e.getMessage( ) ).setMessageKey( Constants.PROPERTY_REST_ERROR_UNAUTHORIZED_OPERATION ) );
            TransactionManager.rollBack( null );
        }
        catch( final IdentityNotFoundException e )
        {
            response.setLocked( false );
            response.setStatus(
                    ResponseStatusFactory.notFound( ).setMessage( e.getMessage( ) ).setMessageKey( Constants.PROPERTY_REST_ERROR_IDENTITY_NOT_FOUND ) );
            TransactionManager.rollBack( null );
        }
        catch( final Exception e )
        {
            response.setLocked( false );
            response.setStatus(
                    ResponseStatusFactory.failure( ).setMessage( e.getMessage( ) ).setMessageKey( Constants.PROPERTY_REST_ERROR_DURING_TREATMENT ) );
            TransactionManager.rollBack( null );
        }
    }

    public void exclude( final SuspiciousIdentityExcludeRequest request, final String clientCode, final RequestAuthor author,
            final SuspiciousIdentityExcludeResponse response )
    {
        final Identity firstIdentity = IdentityHome.findByCustomerId( request.getIdentityCuid1( ) );
        final Identity secondIdentity = IdentityHome.findByCustomerId( request.getIdentityCuid2( ) );

        if ( firstIdentity == null )
        {
            response.setStatus( ResponseStatusFactory.notFound( ).setMessage( "Cannot find identity with cuid " + request.getIdentityCuid1( ) )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_IDENTITY_NOT_FOUND ) );
            return;
        }

        if ( secondIdentity == null )
        {
            response.setStatus( ResponseStatusFactory.notFound( ).setMessage( "Cannot find identity with cuid " + request.getIdentityCuid2( ) )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_IDENTITY_NOT_FOUND ) );
            return;
        }

        if ( SuspiciousIdentityHome.excluded( request.getIdentityCuid1( ), request.getIdentityCuid2( ) ) )
        {
            response.setStatus( ResponseStatusFactory.conflict( ).setMessage( "Identities are already excluded from duplicate suspicions." )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_ALREADY_EXCLUDED ) );
            return;
        }

        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( request.getIdentityCuid1( ), request.getIdentityCuid2( ), author.getType( ).name( ), author.getName( ) );

            response.setStatus( ResponseStatusFactory.success( ).setMessage( "Identities excluded from duplicate suspicions." )
                    .setMessageKey( Constants.PROPERTY_REST_INFO_SUCCESSFUL_OPERATION ) );
            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,
                    response.getStatus( ).getType( ).name( ), response.getStatus( ).getMessage( ), 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,
                    response.getStatus( ).getType( ).name( ), response.getStatus( ).getMessage( ), author, clientCode, metadata2 );

            AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, EXCLUDE_SUSPICIOUS_IDENTITY_EVENT_CODE,
                    _internalUserService.getApiUser( author, clientCode ), request, SPECIFIC_ORIGIN );
        }
        catch( Exception e )
        {
            TransactionManager.rollBack( null );
            response.setStatus(
                    ResponseStatusFactory.failure( ).setMessage( e.getMessage( ) ).setMessageKey( Constants.PROPERTY_REST_ERROR_DURING_TREATMENT ) );
        }
    }

    public void cancelExclusion( final SuspiciousIdentityExcludeRequest request, final String clientCode, final RequestAuthor author,
            final SuspiciousIdentityExcludeResponse response )
    {
        final Identity firstIdentity = IdentityHome.findByCustomerId( request.getIdentityCuid1( ) );
        final Identity secondIdentity = IdentityHome.findByCustomerId( request.getIdentityCuid2( ) );

        if ( firstIdentity == null )
        {
            response.setStatus( ResponseStatusFactory.notFound( ).setMessage( "Cannot find identity with cuid " + request.getIdentityCuid1( ) )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_IDENTITY_NOT_FOUND ) );
            return;
        }

        if ( secondIdentity == null )
        {
            response.setStatus( ResponseStatusFactory.notFound( ).setMessage( "Cannot find identity with cuid " + request.getIdentityCuid2( ) )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_IDENTITY_NOT_FOUND ) );
            return;
        }

        if ( !SuspiciousIdentityHome.excluded( request.getIdentityCuid1( ), request.getIdentityCuid2( ) ) )
        {
            response.setStatus( ResponseStatusFactory.conflict( ).setMessage( "Identities are not excluded from duplicate suspicions." )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_NOT_EXCLUDED ) );
            return;
        }

        TransactionManager.beginTransaction( null );
        try
        {
            // remove the exclusion
            SuspiciousIdentityHome.removeExcludedIdentities( request.getIdentityCuid1( ), request.getIdentityCuid2( ) );
            response.setStatus( ResponseStatusFactory.success( ).setMessage( "Identities exclusion has been cancelled." )
                    .setMessageKey( Constants.PROPERTY_REST_INFO_SUCCESSFUL_OPERATION ) );
            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,
                    response.getStatus( ).getType( ).name( ), response.getStatus( ).getMessage( ), 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,
                    response.getStatus( ).getType( ).name( ), response.getStatus( ).getMessage( ), author, clientCode, metadata2 );

            AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, EXCLUDE_SUSPICIOUS_IDENTITY_EVENT_CODE,
                    _internalUserService.getApiUser( author, clientCode ), request, SPECIFIC_ORIGIN );
        }
        catch( Exception e )
        {
            TransactionManager.rollBack( null );
            response.setStatus(
                    ResponseStatusFactory.failure( ).setMessage( e.getMessage( ) ).setMessageKey( 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( ) );
    }
}