ClaimService.java

package uk.gov.dhsc.htbhf.claimant.service.claim;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.javers.core.Javers;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import uk.gov.dhsc.htbhf.claimant.converter.ClaimToClaimResponseDTOConverter;
import uk.gov.dhsc.htbhf.claimant.entitlement.VoucherEntitlement;
import uk.gov.dhsc.htbhf.claimant.entity.Claim;
import uk.gov.dhsc.htbhf.claimant.message.payload.EmailType;
import uk.gov.dhsc.htbhf.claimant.message.payload.LetterType;
import uk.gov.dhsc.htbhf.claimant.message.payload.TextType;
import uk.gov.dhsc.htbhf.claimant.model.ClaimResponseDTO;
import uk.gov.dhsc.htbhf.claimant.model.ClaimStatus;
import uk.gov.dhsc.htbhf.claimant.model.VerificationResult;
import uk.gov.dhsc.htbhf.claimant.model.eligibility.EligibilityAndEntitlementDecision;
import uk.gov.dhsc.htbhf.claimant.reporting.ClaimAction;
import uk.gov.dhsc.htbhf.claimant.repository.ClaimRepository;
import uk.gov.dhsc.htbhf.claimant.service.ClaimMessageSender;
import uk.gov.dhsc.htbhf.claimant.service.ClaimRequest;
import uk.gov.dhsc.htbhf.claimant.service.ClaimResult;
import uk.gov.dhsc.htbhf.claimant.service.EligibilityAndEntitlementService;
import uk.gov.dhsc.htbhf.claimant.service.audit.EventAuditor;
import uk.gov.dhsc.htbhf.claimant.service.audit.NewClaimEvent;
import uk.gov.dhsc.htbhf.dwp.model.VerificationOutcome;
import uk.gov.dhsc.htbhf.eligibility.model.CombinedIdentityAndEligibilityResponse;
import uk.gov.dhsc.htbhf.eligibility.model.EligibilityStatus;
import uk.gov.dhsc.htbhf.logging.event.FailureEvent;
import uk.gov.dhsc.htbhf.reference.ReferenceGenerator;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import static java.util.Collections.emptyList;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import static org.springframework.util.CollectionUtils.isEmpty;
import static uk.gov.dhsc.htbhf.claimant.factory.VerificationResultFactory.buildVerificationResult;
import static uk.gov.dhsc.htbhf.claimant.model.eligibility.EligibilityAndEntitlementDecision.buildWithStatus;

@Service
@Slf4j
@RequiredArgsConstructor
@SuppressWarnings({"PMD.TooManyMethods", "PMD.ExcessiveImports"})
public class ClaimService {

    private final ClaimRepository claimRepository;
    private final EligibilityAndEntitlementService eligibilityAndEntitlementService;
    private final EventAuditor eventAuditor;
    private final ClaimMessageSender claimMessageSender;
    private final Javers javers;
    private final ClaimToClaimResponseDTOConverter claimToClaimResponseDTOConverter;

    @Value("${claim-reference.size:10}")
    private int claimReferenceSize;

    private static final Map<EligibilityStatus, ClaimStatus> STATUS_MAP = Map.of(
            EligibilityStatus.ELIGIBLE, ClaimStatus.NEW,
            EligibilityStatus.PENDING, ClaimStatus.PENDING,
            EligibilityStatus.NO_MATCH, ClaimStatus.REJECTED,
            EligibilityStatus.ERROR, ClaimStatus.ERROR,
            EligibilityStatus.DUPLICATE, ClaimStatus.REJECTED,
            EligibilityStatus.INELIGIBLE, ClaimStatus.REJECTED
    );

    public ClaimResult createClaim(ClaimRequest claimRequest, String user) {
        try {
            EligibilityAndEntitlementDecision decision = eligibilityAndEntitlementService.evaluateNewClaimant(claimRequest.getClaimant(),
                    claimRequest.getEligibilityOverride());
            CombinedIdentityAndEligibilityResponse identityAndEligibilityResponse = decision.getIdentityAndEligibilityResponse();

            if (decision.getEligibilityStatus() == EligibilityStatus.DUPLICATE) {
                Claim claim = createDuplicateClaim(claimRequest, decision, user);
                claimMessageSender.sendReportClaimMessage(claim, identityAndEligibilityResponse, ClaimAction.REJECTED);
                return ClaimResult.withNoEntitlement(claim);
            }

            VerificationResult verificationResult = buildVerificationResult(claimRequest.getClaimant(), identityAndEligibilityResponse);
            Claim claim = createNewClaim(claimRequest, decision, verificationResult, user);
            if (claim.getClaimStatus() == ClaimStatus.NEW) {
                sendMessagesForNewClaim(decision, identityAndEligibilityResponse, claim);
                VoucherEntitlement weeklyEntitlement = decision.getVoucherEntitlement().getFirstVoucherEntitlementForCycle();
                return ClaimResult.withEntitlement(claim, weeklyEntitlement, verificationResult);
            }

            if (StringUtils.isNotEmpty(claim.getClaimant().getNino())) {
                sendMessagesForRejectedClaimIfNinoPresent(identityAndEligibilityResponse, verificationResult, claim);
            }
            return ClaimResult.withNoEntitlement(claim, verificationResult);
        } catch (RuntimeException e) {
            handleFailedClaim(claimRequest, user, e);
            throw e;
        }
    }

    public List<ClaimResponseDTO> findClaims() {
        List<Claim> claims = claimRepository.findAll();
        return claimToClaimResponseDTOConverter.convert(claims);
    }

    private void sendMessagesForNewClaim(EligibilityAndEntitlementDecision decision,
                                         CombinedIdentityAndEligibilityResponse identityAndEligibilityResponse,
                                         Claim claim) {
        if (identityAndEligibilityResponse.getEmailAddressMatch() == VerificationOutcome.MATCHED) {
            EmailType emailType = registeredChildrenContainAllDeclaredChildren(identityAndEligibilityResponse, claim)
                    ? EmailType.INSTANT_SUCCESS
                    : EmailType.INSTANT_SUCCESS_PARTIAL_CHILDREN_MATCH;
            claimMessageSender.sendInstantSuccessEmail(claim, decision, emailType);
        } else if (identityAndEligibilityResponse.getMobilePhoneMatch() == VerificationOutcome.MATCHED) {
            claimMessageSender.sendInstantSuccessText(claim, decision, TextType.INSTANT_SUCCESS_TEXT);
        } else {
            sendMessagesForPhoneOrEmailMismatch(claim, decision, identityAndEligibilityResponse);
        }

        claimMessageSender.sendNewCardMessage(claim, decision);
        claimMessageSender.sendReportClaimMessage(claim, identityAndEligibilityResponse, ClaimAction.NEW);
    }

    private void sendMessagesForPhoneOrEmailMismatch(Claim claim,
                                                     EligibilityAndEntitlementDecision decision,
                                                     CombinedIdentityAndEligibilityResponse identityAndEligibilityResponse) {
        if (StringUtils.isNotEmpty(claim.getClaimant().getEmailAddress())) {
            claimMessageSender.sendDecisionPendingEmailMessage(claim);
        }
        LetterType letterType = registeredChildrenContainAllDeclaredChildren(identityAndEligibilityResponse, claim)
                ? LetterType.APPLICATION_SUCCESS_CHILDREN_MATCH
                : LetterType.APPLICATION_SUCCESS_CHILDREN_MISMATCH;
        claimMessageSender.sendLetterWithAddressAndPaymentFieldsMessage(claim, decision, letterType);
    }

    private void sendMessagesForRejectedClaimIfNinoPresent(CombinedIdentityAndEligibilityResponse identityAndEligibilityResponse,
                                                           VerificationResult verificationResult,
                                                           Claim claim) {
        if (verificationResult.isAddressMismatch()) {
            if (StringUtils.isNotEmpty(claim.getClaimant().getEmailAddress())) {
                claimMessageSender.sendDecisionPendingEmailMessage(claim);
            }
            claimMessageSender.sendLetterWithAddressOnlyMessage(claim, LetterType.UPDATE_YOUR_ADDRESS);
        }
        claimMessageSender.sendReportClaimMessage(claim, identityAndEligibilityResponse, ClaimAction.REJECTED);
    }

    public void updateCurrentIdentityAndEligibilityResponse(Claim claim, CombinedIdentityAndEligibilityResponse response) {
        claim.setCurrentIdentityAndEligibilityResponse(response);
        claimRepository.save(claim);
    }

    private void handleFailedClaim(ClaimRequest claimRequest, String user, RuntimeException e) {
        EligibilityAndEntitlementDecision decision = buildWithStatus(EligibilityStatus.ERROR);
        Claim claim = buildClaim(ClaimStatus.ERROR, claimRequest, decision);
        NewClaimEvent newClaimEvent = new NewClaimEvent(claim, user);
        FailureEvent failureEvent = FailureEvent.builder()
                .failureDescription("Unable to create (or update) claim")
                .failedEvent(newClaimEvent)
                .exception(e)
                .build();
        eventAuditor.auditFailedEvent(failureEvent);
        claimRepository.save(claim);
        javers.commit(user, claim);
    }

    private Claim createDuplicateClaim(ClaimRequest claimRequest, EligibilityAndEntitlementDecision decision, String user) {
        return buildAndSaveClaim(ClaimStatus.REJECTED, claimRequest, decision, user);
    }

    private Claim createNewClaim(ClaimRequest claimRequest, EligibilityAndEntitlementDecision decision, VerificationResult verificationResult, String user) {
        ClaimStatus claimStatus = getClaimStatus(decision.getEligibilityStatus(), verificationResult);
        return buildAndSaveClaim(claimStatus, claimRequest, decision, user);
    }

    private Claim buildAndSaveClaim(ClaimStatus claimStatus, ClaimRequest claimRequest, EligibilityAndEntitlementDecision decision, String user) {
        Claim claim = buildClaim(claimStatus, claimRequest, decision);
        log.info("Saving new claim: {} with status {} and reference {}", claim.getId(), claim.getEligibilityStatus(), claim.getReference());
        claimRepository.save(claim);
        javers.commit(user, claim);
        eventAuditor.auditNewClaim(claim, user);
        return claim;
    }

    @SuppressFBWarnings(value = "SECMD5",
            justification = "Using a hash of the device fingerprint to identify multiple claims from the same device, not for encryption")
    private Claim buildClaim(ClaimStatus claimStatus, ClaimRequest claimRequest, EligibilityAndEntitlementDecision decision) {
        LocalDateTime currentDateTime = LocalDateTime.now();
        Map<String, Object> deviceFingerprint = claimRequest.getDeviceFingerprint();
        String fingerprintHash = isEmpty(deviceFingerprint) ? null : DigestUtils.md5Hex(deviceFingerprint.toString());
        String reference = checkAndReturnUniqueReferenceForClaim();
        return Claim.builder()
                .dwpHouseholdIdentifier(decision.getDwpHouseholdIdentifier())
                .hmrcHouseholdIdentifier(decision.getHmrcHouseholdIdentifier())
                .eligibilityStatus(decision.getEligibilityStatus())
                .eligibilityStatusTimestamp(currentDateTime)
                .claimStatus(claimStatus)
                .claimStatusTimestamp(currentDateTime)
                .claimant(claimRequest.getClaimant())
                .deviceFingerprint(deviceFingerprint)
                .deviceFingerprintHash(fingerprintHash)
                .webUIVersion(claimRequest.getWebUIVersion())
                .initialIdentityAndEligibilityResponse(decision.getIdentityAndEligibilityResponse())
                .currentIdentityAndEligibilityResponse(decision.getIdentityAndEligibilityResponse())
                .eligibilityOverride(claimRequest.getEligibilityOverride())
                .reference(reference)
                .build();
    }

    private String checkAndReturnUniqueReferenceForClaim() {
        String reference;
        boolean isReferenceDuplicate;
        do {
            reference = ReferenceGenerator.generateReference(claimReferenceSize);
            isReferenceDuplicate = false;
            Optional<Claim> claim = claimRepository.findByReference(reference);
            if (claim.isPresent()) {
                isReferenceDuplicate = true;
            }

        } while (isReferenceDuplicate);

        return reference;
    }

    private ClaimStatus getClaimStatus(EligibilityStatus eligibilityStatus, VerificationResult verificationResult) {
        if (verificationResult.isAddressMismatch()) {
            return ClaimStatus.REJECTED;
        } else if (verificationResult.getIsPregnantOrAtLeast1ChildMatched()) {
            return STATUS_MAP.get(eligibilityStatus);
        }
        return ClaimStatus.REJECTED;
    }

    private boolean registeredChildrenContainAllDeclaredChildren(CombinedIdentityAndEligibilityResponse identityAndEligibilityResponse, Claim claim) {
        List<LocalDate> initiallyDeclaredChildrenDob = defaultIfNull(claim.getClaimant().getInitiallyDeclaredChildrenDob(), emptyList());
        List<LocalDate> identityAndEligibilityResponseChildren = defaultIfNull(identityAndEligibilityResponse.getDobOfChildrenUnder4(), emptyList());
        return identityAndEligibilityResponseChildren.containsAll(initiallyDeclaredChildrenDob)
                && identityAndEligibilityResponseChildren.size() >= initiallyDeclaredChildrenDob.size();
    }
}