Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PAGOPA-2104] feat: Add new recovery by IUV API #106

Merged
merged 16 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import it.gov.pagopa.wispconverter.service.RecoveryService;
import it.gov.pagopa.wispconverter.util.Constants;
import it.gov.pagopa.wispconverter.util.ErrorUtil;
import jakarta.validation.constraints.Pattern;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
Expand Down Expand Up @@ -57,4 +58,33 @@ public ResponseEntity<RecoveryReceiptResponse> recoverReceiptKOForCreditorInstit
}
}


@Operation(summary = "Execute IUV reconciliation for certain creditor institution.", description = "Execute reconciliation of all IUVs for certain creditor institution, sending RT for close payment.", security = {@SecurityRequirement(name = "ApiKey")}, tags = {"Recovery"})
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Started reconciling IUV with explicit RT send")
})
@PostMapping(value = "/{creditor_institution}/rpt/{iuv}/receipt-ko")
public ResponseEntity<String> recoverReceiptKOForCreditorInstitutionAndIUV(@Pattern(regexp = "[a-zA-Z0-9_-]{1,100}") @PathVariable("creditor_institution") String ci,
@Pattern(regexp = "[a-zA-Z0-9_-]{1,100}") @PathVariable("iuv") String iuv,
@Pattern(regexp = "[a-zA-Z0-9_-]{1,10}") @QueryParam("date_from") String dateFrom,
@Pattern(regexp = "[a-zA-Z0-9_-]{1,10}") @QueryParam("date_to") String dateTo) {
try {
log.info("Invoking API operation recoverReceiptKOForCreditorInstitution - args: {} {} {} {}", ci, iuv, dateFrom, dateTo);
Dismissed Show dismissed Hide dismissed

boolean recovered = recoveryService.recoverReceiptKO(ci, iuv, dateFrom, dateTo);
if(recovered)
return ResponseEntity.ok(String.format("RPT with CI %s and IUV %s recovered via API", ci, iuv));
github-advanced-security[bot] marked this conversation as resolved.
Dismissed
Show resolved Hide resolved
else return ResponseEntity.ok(String.format("RPT with CI %s and IUV %s could not be recovered via API", ci, iuv));
github-advanced-security[bot] marked this conversation as resolved.
Dismissed
Show resolved Hide resolved
} catch (Exception ex) {
String operationId = MDC.get(Constants.MDC_OPERATION_ID);
log.error(String.format("GenericException: operation-id=[%s]", operationId != null ? operationId : "n/a"), ex);
AppException appException = new AppException(ex, AppErrorCodeMessageEnum.ERROR, ex.getMessage());
ErrorResponse errorResponse = errorUtil.forAppException(appException);
log.error("Failed API operation recoverReceiptKOForCreditorInstitution - error: {}", errorResponse);
throw ex;
} finally {
log.info("Successful API operation recoverReceiptKOForCreditorInstitution");
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ public class RecoveryReceiptPaymentResponse {

private String iuv;
private String ccp;
}
private String ci;
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
package it.gov.pagopa.wispconverter.service;

import it.gov.pagopa.wispconverter.controller.ReceiptController;
import it.gov.pagopa.wispconverter.controller.model.RecoveryReceiptPaymentResponse;
import it.gov.pagopa.wispconverter.controller.model.RecoveryReceiptResponse;
import it.gov.pagopa.wispconverter.exception.AppErrorCodeMessageEnum;
import it.gov.pagopa.wispconverter.exception.AppException;
import it.gov.pagopa.wispconverter.repository.CacheRepository;
import it.gov.pagopa.wispconverter.repository.RTRepository;
import it.gov.pagopa.wispconverter.repository.ReEventRepository;
import it.gov.pagopa.wispconverter.repository.model.RTEntity;
import it.gov.pagopa.wispconverter.repository.model.ReEventEntity;
import it.gov.pagopa.wispconverter.repository.model.enumz.InternalStepStatus;
import it.gov.pagopa.wispconverter.service.model.ReceiptDto;
import it.gov.pagopa.wispconverter.service.model.re.ReEventDto;
import it.gov.pagopa.wispconverter.util.Constants;
import it.gov.pagopa.wispconverter.util.MDCUtil;
Expand All @@ -28,55 +25,71 @@
import java.time.format.DateTimeFormatter;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;

@Service
@Slf4j
@RequiredArgsConstructor
public class RecoveryService {

private static final String EVENT_TYPE_FOR_RECEIPTKO_SEARCH = "GENERATED_CACHE_ABOUT_RPT_FOR_RT_GENERATION";
public static final String EVENT_TYPE_FOR_RECEIPTKO_SEARCH = "GENERATED_CACHE_ABOUT_RPT_FOR_RT_GENERATION";

private static final String RPT_ACCETTATA_NODO = "RPT_ACCETTATA_NODO";

public static final String RPT_PARCHEGGIATA_NODO = "RPT_PARCHEGGIATA_NODO";

private static final String STATUS_RT_SEND_SUCCESS = "RT_SEND_SUCCESS";

private static final String RECOVERY_VALID_START_DATE = "2024-09-03";

private static final List<String> BUSINESS_PROCESSES = List.of("receipt-ok", "receipt-ko", "ecommerce-hang-timeout-trigger");

private final ReceiptController receiptController;
private final ReceiptService receiptService;

private final RTRepository rtRepository;

private final ReEventRepository reEventRepository;

private final CacheRepository cacheRepository;

private final ReService reService;

@Value("${wisp-converter.cached-requestid-mapping.ttl.minutes}")
private Long requestIDMappingTTL;
private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";

private static final String DATE_FORMAT_DAY = "yyyy-MM-dd";

@Value("${wisp-converter.cached-requestid-mapping.ttl.minutes:1440}")
public Long requestIDMappingTTL;

@Value("${wisp-converter.recovery.receipt-generation.wait-time.minutes:60}")
private Long receiptGenerationWaitTime;
public Long receiptGenerationWaitTime;

public RecoveryReceiptResponse recoverReceiptKOForCreditorInstitution(String creditorInstitution, String dateFrom, String dateTo) {
public boolean recoverReceiptKO(String creditorInstitution, String iuv, String dateFrom, String dateTo) {
cap-ang marked this conversation as resolved.
Show resolved Hide resolved
checkDateValidity(dateFrom, dateTo);
List<ReEventEntity> reEvents = reEventRepository.findByIuvAndOrganizationId(dateFrom, dateTo, iuv, creditorInstitution)
.stream()
.sorted(Comparator.comparing(ReEventEntity::getInsertedTimestamp))
.toList();

MDCUtil.setSessionDataInfo("recovery-receipt-ko");
LocalDate lowerLimit = LocalDate.parse(RECOVERY_VALID_START_DATE, DateTimeFormatter.ISO_LOCAL_DATE);
if (LocalDate.parse(dateFrom, DateTimeFormatter.ISO_LOCAL_DATE).isBefore(lowerLimit)) {
throw new AppException(AppErrorCodeMessageEnum.ERROR, String.format("The lower bound cannot be lower than [%s]", RECOVERY_VALID_START_DATE));
}
Set<String> interruptStatusSet = getWISPInterruptStatusSet();

LocalDate now = LocalDate.now();
LocalDate parse = LocalDate.parse(dateTo, DateTimeFormatter.ISO_LOCAL_DATE);
if (parse.isAfter(now)) {
throw new AppException(AppErrorCodeMessageEnum.ERROR, String.format("The upper bound cannot be higher than [%s]", now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))));
if(!reEvents.isEmpty()) {
ReEventEntity lastEvent = reEvents.get(0);
if(interruptStatusSet.contains(lastEvent.getStatus()))
this.recoverReceiptKO(creditorInstitution, iuv, lastEvent.getSessionId(), lastEvent.getCcp(), dateFrom, dateTo);
}

return true;
}

public RecoveryReceiptResponse recoverReceiptKOForCreditorInstitution(String creditorInstitution, String dateFrom, String dateTo) {
MDCUtil.setSessionDataInfo("recovery-receipt-ko");

checkDateValidity(dateFrom, dateTo);

LocalDate parse = LocalDate.parse(dateTo, DateTimeFormatter.ISO_LOCAL_DATE);
String dateToRefactored;
if (now.isEqual(parse)) {

if (LocalDate.now().isEqual(parse)) {
ZonedDateTime nowMinusMinutes = ZonedDateTime.now(ZoneOffset.UTC).minusMinutes(receiptGenerationWaitTime);
dateToRefactored = nowMinusMinutes.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
dateToRefactored = nowMinusMinutes.format(DateTimeFormatter.ofPattern(DATE_FORMAT));
log.info("Upper bound forced to {}", dateToRefactored);
} else {
dateToRefactored = dateTo + " 23:59:59";
Expand All @@ -85,12 +98,13 @@ public RecoveryReceiptResponse recoverReceiptKOForCreditorInstitution(String cre

List<RTEntity> receiptRTs = rtRepository.findByOrganizationId(creditorInstitution, dateFrom, dateToRefactored);
List<RecoveryReceiptPaymentResponse> paymentsToReconcile = receiptRTs.stream().map(entity -> RecoveryReceiptPaymentResponse.builder()
.iuv(entity.getIuv())
.ccp(entity.getCcp())
.build())
.toList();
.iuv(entity.getIuv())
.ccp(entity.getCcp())
.ci(entity.getIdDominio())
.build())
.toList();

CompletableFuture<Boolean> executeRecovery = recoverReceiptKOAsync(dateFrom, dateTo, creditorInstitution, paymentsToReconcile);
CompletableFuture<Boolean> executeRecovery = recoverReceiptKOAsync(dateFrom, dateTo, paymentsToReconcile);
executeRecovery
.thenAccept(value -> log.debug("Reconciliation for creditor institution [{}] in date range [{}-{}] completed!", creditorInstitution, dateFrom, dateTo))
.exceptionally(e -> {
Expand All @@ -99,68 +113,82 @@ public RecoveryReceiptResponse recoverReceiptKOForCreditorInstitution(String cre
});

return RecoveryReceiptResponse.builder()
.payments(paymentsToReconcile)
.build();
.payments(paymentsToReconcile)
.build();
}

private CompletableFuture<Boolean> recoverReceiptKOAsync(String dateFrom, String dateTo, String creditorInstitution, List<RecoveryReceiptPaymentResponse> paymentsToReconcile) {
private CompletableFuture<Boolean> recoverReceiptKOAsync(String dateFrom, String dateTo, List<RecoveryReceiptPaymentResponse> paymentsToReconcile) {
return CompletableFuture.supplyAsync(() -> recoverReceiptKOByRecoveryPayment(dateFrom, dateTo, paymentsToReconcile));
}

return CompletableFuture.supplyAsync(() -> {
private boolean recoverReceiptKOByRecoveryPayment(String dateFrom, String dateTo, List<RecoveryReceiptPaymentResponse> paymentsToReconcile) {

for (RecoveryReceiptPaymentResponse payment : paymentsToReconcile) {
for (RecoveryReceiptPaymentResponse payment : paymentsToReconcile) {
String iuv = payment.getIuv();
String ccp = payment.getCcp();
String ci = payment.getCi();

String iuv = payment.getIuv();
String ccp = payment.getCcp();
try {
List<ReEventEntity> reEvents = reEventRepository.findByIuvAndOrganizationId(dateFrom, dateTo, iuv, ci);

try {
List<ReEventEntity> reEvents = reEventRepository.findByIuvAndOrganizationId(dateFrom, dateTo, iuv, creditorInstitution);
List<ReEventEntity> filteredEvents = reEvents.stream()
.filter(event -> EVENT_TYPE_FOR_RECEIPTKO_SEARCH.equals(event.getStatus()))
.filter(event -> ccp.equals(event.getCcp()))
.sorted(Comparator.comparing(ReEventEntity::getInsertedTimestamp))
.toList();

List<ReEventEntity> filteredEvents = reEvents.stream()
.filter(event -> EVENT_TYPE_FOR_RECEIPTKO_SEARCH.equals(event.getStatus()))
.filter(event -> ccp.equals(event.getCcp()))
.sorted(Comparator.comparing(ReEventEntity::getInsertedTimestamp))
.toList();
int numberOfEvents = filteredEvents.size();
if (numberOfEvents > 0) {
ReEventEntity event = filteredEvents.get(numberOfEvents - 1);
String sessionId = event.getSessionId();

int numberOfEvents = filteredEvents.size();
if (numberOfEvents > 0) {
log.info("[RECOVERY-MISSING-RT] Recovery with receipt-ko for ci = {}, iuv = {}, ccp = {}, sessionId = {}", ci, iuv, ccp, sessionId);
this.recoverReceiptKO(ci, iuv, ccp, sessionId, dateFrom, dateTo);
}
} catch (Exception e) {
generateRE(Constants.PAA_INVIA_RT, "Failure", InternalStepStatus.RT_END_RECONCILIATION_PROCESS, ci, iuv, ccp, null);
throw new AppException(e, AppErrorCodeMessageEnum.ERROR, e.getMessage());
}
}

ReEventEntity event = filteredEvents.get(numberOfEvents - 1);
String noticeNumber = event.getNoticeNumber();
String sessionId = event.getSessionId();
return true;
}

// search by sessionId, then filter by status=RT_SEND_SUCCESS. If there is zero, then proceed
List<ReEventEntity> reEventsRT = reEventRepository.findBySessionIdAndStatus(dateFrom, dateTo, sessionId, STATUS_RT_SEND_SUCCESS);
// check if there is a successful RT submission, if there isn't prepare cached data and send receipt-ko
public void recoverReceiptKO(String ci, String iuv, String ccp, String sessionId, String dateFrom, String dateTo) {
// search by sessionId, then filter by status=RT_SEND_SUCCESS. If there is zero, then proceed
List<ReEventEntity> reEventsRT = reEventRepository.findBySessionIdAndStatus(dateFrom, dateTo, sessionId, STATUS_RT_SEND_SUCCESS);

if (reEventsRT.isEmpty()) {
String navToIuvMapping = String.format(DecouplerService.MAP_CACHING_KEY_TEMPLATE, creditorInstitution, noticeNumber);
String iuvToSessionIdMapping = String.format(DecouplerService.CACHING_KEY_TEMPLATE, creditorInstitution, iuv);
this.cacheRepository.insert(navToIuvMapping, iuvToSessionIdMapping, this.requestIDMappingTTL);
this.cacheRepository.insert(iuvToSessionIdMapping, sessionId, this.requestIDMappingTTL);
if (reEventsRT.isEmpty()) {
MDC.put(Constants.MDC_BUSINESS_PROCESS, "receipt-ko");

MDC.put(Constants.MDC_BUSINESS_PROCESS, "receipt-ko");
generateRE(Constants.PAA_INVIA_RT, null, InternalStepStatus.RT_START_RECONCILIATION_PROCESS, creditorInstitution, iuv, noticeNumber, ccp, sessionId);
String receiptKoRequest = ReceiptDto.builder()
.fiscalCode(creditorInstitution)
.noticeNumber(noticeNumber)
.build()
.toString();
this.receiptController.receiptKo(receiptKoRequest);
generateRE(Constants.PAA_INVIA_RT, "Success", InternalStepStatus.RT_END_RECONCILIATION_PROCESS, creditorInstitution, iuv, noticeNumber, ccp, sessionId);
MDC.remove(Constants.MDC_BUSINESS_PROCESS);
}
}
generateRE(Constants.PAA_INVIA_RT, null, InternalStepStatus.RT_START_RECONCILIATION_PROCESS, ci, iuv, ccp, sessionId);

} catch (Exception e) {
generateRE(Constants.PAA_INVIA_RT, "Failure", InternalStepStatus.RT_END_RECONCILIATION_PROCESS, creditorInstitution, iuv, null, ccp, null);
throw new AppException(e, AppErrorCodeMessageEnum.ERROR, e.getMessage());
}
try {
this.receiptService.sendRTKoFromSessionId(sessionId, InternalStepStatus.NEGATIVE_RT_TRY_TO_SEND_TO_CREDITOR_INSTITUTION);
} catch (Exception e) {
generateRE(Constants.PAA_INVIA_RT, "Failure", InternalStepStatus.RT_END_RECONCILIATION_PROCESS, ci, iuv, ccp, sessionId);
throw new AppException(e, AppErrorCodeMessageEnum.ERROR, e.getMessage());
}
generateRE(Constants.PAA_INVIA_RT, "Success", InternalStepStatus.RT_END_RECONCILIATION_PROCESS, ci, iuv, ccp, sessionId);
MDC.remove(Constants.MDC_BUSINESS_PROCESS);
}
}

private void checkDateValidity(String dateFrom, String dateTo) {
LocalDate lowerLimit = LocalDate.parse(RECOVERY_VALID_START_DATE, DateTimeFormatter.ISO_LOCAL_DATE);
if (LocalDate.parse(dateFrom, DateTimeFormatter.ISO_LOCAL_DATE).isBefore(lowerLimit)) {
throw new AppException(AppErrorCodeMessageEnum.ERROR, String.format("The lower bound cannot be lower than [%s]", RECOVERY_VALID_START_DATE));
}

return true;
});
LocalDate today = LocalDate.now();
LocalDate parse = LocalDate.parse(dateTo, DateTimeFormatter.ISO_LOCAL_DATE);
if (parse.isAfter(today)) {
throw new AppException(AppErrorCodeMessageEnum.ERROR, String.format("The upper bound cannot be higher than [%s]", today.format(DateTimeFormatter.ofPattern(DATE_FORMAT_DAY))));
}
}

private void generateRE(String primitive, String operationStatus, InternalStepStatus status, String domainId, String iuv, String noticeNumber, String ccp, String sessionId) {
private void generateRE(String primitive, String operationStatus, InternalStepStatus status, String domainId, String iuv, String ccp, String sessionId) {

// setting data in MDC for next use
ReEventDto reEvent = ReUtil.getREBuilder()
Expand All @@ -171,8 +199,20 @@ private void generateRE(String primitive, String operationStatus, InternalStepSt
.domainId(domainId)
.iuv(iuv)
.ccp(ccp)
.noticeNumber(noticeNumber)
.noticeNumber(null)
.build();
reService.addRe(reEvent);
}
}

private static Set<String> getWISPInterruptStatusSet() {
return Set.of(
RPT_ACCETTATA_NODO,
RPT_PARCHEGGIATA_NODO,
"GENERATED_NAV_FOR_NEW_PAYMENT_POSITION",
"CREATED_NEW_PAYMENT_POSITION_IN_GPD",
"GENERATED_CACHE_ABOUT_RPT_FOR_DECOUPLER",
EVENT_TYPE_FOR_RECEIPTKO_SEARCH,
"SAVED_RPT_IN_CART_RECEIVED_REDIRECT_URL_FROM_CHECKOUT"
);
}
}
Loading
Loading