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 4 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 @@ -57,4 +57,33 @@
}
}


@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(@PathVariable("creditor_institution") String ci,
@PathVariable("creditor_institution") String iuv,
@QueryParam("date_from") String dateFrom,
@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
Expand Up @@ -25,6 +25,12 @@ public void insert(String key, String value, long ttl, ChronoUnit chronoUnit) {
this.redisSimpleTemplate.opsForValue().set(key, value, Duration.of(ttl, chronoUnit));
}

public void insert(String key, String value, long ttl, ChronoUnit chronoUnit, boolean deleteIfAlreadyExists) {
if(deleteIfAlreadyExists && hasKey(key))
this.delete(key);
this.redisSimpleTemplate.opsForValue().set(key, value, Duration.of(ttl, chronoUnit));
}

public <T> T read(String key, Class<T> clazz) {
T result = null;
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,6 @@ private void generateREForNotGenerableRT(SessionDataDTO sessionData, String iuv,
}

private void generateREForSentRT(SessionDataDTO sessionData, String iuv, String noticeNumber) {

// extract psp on which the payment will be sent
List<RPTContentDTO> rpts = sessionData.getRPTByIUV(iuv);
for (RPTContentDTO rptContent : rpts) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;

@Service
Expand All @@ -36,10 +39,18 @@ public class RecoveryService {

private 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";

private static final String RT_SEND_SUCCESS = "RT_SEND_SUCCESS";

private static final String STATUS_RT_SEND_SUCCESS = "RT_SEND_SUCCESS";

private static final String CREATED_NEW_PAYMENT_POSITION_IN_GPD = "CREATED_NEW_PAYMENT_POSITION_IN_GPD";

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

private static final String MOCK_NOTICE_NUMBER = "348000000000000000";

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

private final ReceiptController receiptController;
Expand All @@ -52,29 +63,58 @@ public class RecoveryService {

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";

@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
if(!areValidDates(dateFrom, dateTo)) {
throw new AppException(AppErrorCodeMessageEnum.ERROR, String.format("The lower bound cannot be lower than [%s], the upper bound cannot be higher than [%s]",
RECOVERY_VALID_START_DATE, LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))));
}

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));
List<ReEventEntity> reEvents = reEventRepository.findByIuvAndOrganizationId(dateFrom, dateTo, iuv, creditorInstitution);
List<ReEventEntity> events = reEvents.stream()
.sorted(Comparator.comparing(ReEventEntity::getInsertedTimestamp))
.toList();

Set<String> noRedirectSet = Set.of("RPT_ACCETTATA_NODO", "RPT_PARCHEGGIATA_NODO");
Set<String> breakAfterRedirectSet = Set.of("GENERATED_NAV_FOR_NEW_PAYMENT_POSITION", "CREATED_NEW_PAYMENT_POSITION_IN_GPD",
"GENERATED_CACHE_ABOUT_RPT_FOR_DECOUPLER", "GENERATED_CACHE_ABOUT_RPT_FOR_RT_GENERATION", "SAVED_RPT_IN_CART_RECEIVED_REDIRECT_URL_FROM_CHECKOUT");

if(!events.isEmpty()) {
ReEventEntity lastEvent = events.get(0);
String status = lastEvent.getStatus();
String noticeNumber;

if(noRedirectSet.contains(status)) {
noticeNumber = MOCK_NOTICE_NUMBER;
} else if(breakAfterRedirectSet.contains(status)) {
noticeNumber = lastEvent.getNoticeNumber();
} else return false;

this.recoverReceiptKO(creditorInstitution, noticeNumber, iuv, lastEvent.getSessionId(), lastEvent.getCcp(), dateFrom, dateTo);
}

return true;
}

public RecoveryReceiptResponse recoverReceiptKOForCreditorInstitution(String creditorInstitution, String dateFrom, String dateTo) {
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(!areValidDates(dateFrom, dateTo)) {
throw new AppException(AppErrorCodeMessageEnum.ERROR, String.format("The lower bound cannot be lower than [%s], the upper bound cannot be higher than [%s]",
RECOVERY_VALID_START_DATE, now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))));
}

LocalDate parse = LocalDate.parse(dateTo, DateTimeFormatter.ISO_LOCAL_DATE);
String dateToRefactored;
if (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 @@ -83,12 +123,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.info("Reconciliation for creditor institution [{}] in date range [{}-{}] completed!", creditorInstitution, dateFrom, dateTo))
.exceptionally(e -> {
Expand All @@ -97,65 +138,90 @@ 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) {

return CompletableFuture.supplyAsync(() -> {

for (RecoveryReceiptPaymentResponse payment : paymentsToReconcile) {
private boolean areValidDates(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));
}

String iuv = payment.getIuv();
String ccp = payment.getCcp();
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"))));
}

try {
List<ReEventEntity> reEvents = reEventRepository.findByIuvAndOrganizationId(dateFrom, dateTo, iuv, creditorInstitution);
return true;
}

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();
private CompletableFuture<Boolean> recoverReceiptKOAsync(String dateFrom, String dateTo, List<RecoveryReceiptPaymentResponse> paymentsToReconcile) {
return CompletableFuture.supplyAsync(() -> recoverReceiptKOByRecoveryPayment(dateFrom, dateTo, paymentsToReconcile));
}

int numberOfEvents = filteredEvents.size();
if (numberOfEvents > 0) {
private boolean recoverReceiptKOByRecoveryPayment(String dateFrom, String dateTo, List<RecoveryReceiptPaymentResponse> paymentsToReconcile) {

ReEventEntity event = filteredEvents.get(numberOfEvents - 1);
String noticeNumber = event.getNoticeNumber();
String sessionId = event.getSessionId();
for (RecoveryReceiptPaymentResponse payment : paymentsToReconcile) {
String iuv = payment.getIuv();
String ccp = payment.getCcp();
String ci = payment.getCi();

// 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);
try {
List<ReEventEntity> reEvents = reEventRepository.findByIuvAndOrganizationId(dateFrom, dateTo, iuv, ci);

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);
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();

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);
}
}
int numberOfEvents = filteredEvents.size();
if (numberOfEvents > 0) {
ReEventEntity event = filteredEvents.get(numberOfEvents - 1);
String noticeNumber = event.getNoticeNumber();
String sessionId = event.getSessionId();

} 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());
log.info("[RECOVERY-MISSING-RT] Recovery with receipt-ko for ci = {}, iuv = {}, ccp = {}, sessionId = {}", ci, iuv, ccp, sessionId);
this.recoverReceiptKO(ci, noticeNumber, iuv, sessionId, ccp, dateFrom, dateTo);
}
} catch (Exception e) {
generateRE(Constants.PAA_INVIA_RT, "Failure", InternalStepStatus.RT_END_RECONCILIATION_PROCESS, ci, iuv, null, ccp, null);
throw new AppException(e, AppErrorCodeMessageEnum.ERROR, e.getMessage());
}
}

return true;
}

return true;
});
// 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 noticeNumber, String iuv, String sessionId, String ccp, 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, ci, noticeNumber);
String iuvToSessionIdMapping = String.format(DecouplerService.CACHING_KEY_TEMPLATE, ci, iuv);
this.cacheRepository.insert(navToIuvMapping, iuvToSessionIdMapping, this.requestIDMappingTTL, ChronoUnit.MINUTES,true);
this.cacheRepository.insert(iuvToSessionIdMapping, sessionId, this.requestIDMappingTTL, ChronoUnit.MINUTES,true);

MDC.put(Constants.MDC_BUSINESS_PROCESS, "receipt-ko");
generateRE(Constants.PAA_INVIA_RT, null, InternalStepStatus.RT_START_RECONCILIATION_PROCESS, ci, iuv, noticeNumber, ccp, sessionId);
String receiptKoRequest = ReceiptDto.builder()
.fiscalCode(ci)
.noticeNumber(noticeNumber)
.build()
.toString();
try {
this.receiptController.receiptKo(receiptKoRequest);
} catch (Exception e) {
generateRE(Constants.PAA_INVIA_RT, "Failure", InternalStepStatus.RT_END_RECONCILIATION_PROCESS, ci, iuv, noticeNumber, ccp, sessionId);
throw new AppException(e, AppErrorCodeMessageEnum.ERROR, e.getMessage());
}
generateRE(Constants.PAA_INVIA_RT, "Success", InternalStepStatus.RT_END_RECONCILIATION_PROCESS, ci, iuv, noticeNumber, ccp, sessionId);
MDC.remove(Constants.MDC_BUSINESS_PROCESS);
}
}

private void generateRE(String primitive, String operationStatus, InternalStepStatus status, String domainId, String iuv, String noticeNumber, String ccp, String sessionId) {
Expand All @@ -173,4 +239,4 @@ private void generateRE(String primitive, String operationStatus, InternalStepSt
.build();
reService.addRe(reEvent);
}
}
}
Loading