Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
28 changes: 24 additions & 4 deletions src/main/java/it/govpay/console/pendenza/PendenzaController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package it.govpay.console.pendenza;

import java.time.LocalDate;
import java.util.List;
import java.util.Set;

Expand All @@ -15,6 +16,7 @@
import it.govpay.console.model.PendenzaExpand;
import it.govpay.console.model.RicevutaSummary;
import it.govpay.console.model.Soggetto;
import it.govpay.console.model.StatoPendenza;
import it.govpay.console.ricevuta.RicevutaService;
import it.govpay.console.soggetto.InformazioniDebitoreService;
import it.govpay.console.web.ListQueryValidator;
Expand All @@ -26,7 +28,9 @@ public class PendenzaController implements PendenzeApi {

private static final Set<String> LIST_PENDENZE_QUERY_PARAMS = Set.of(
"page", "limit", "sort", "total", "cursor",
"idPendenza", "numeroAvviso", "idDominio", "identificativoDebitore");
"idPendenza", "numeroAvviso", "idDominio", "identificativoDebitore",
"stato", "dataDa", "dataA", "iuv", "direzione", "divisione",
"idA2A", "idTipoPendenza");

private static final Set<String> GET_PENDENZA_QUERY_PARAMS = Set.of("expand");

Expand Down Expand Up @@ -66,14 +70,22 @@ public ResponseEntity<ListPendenze200Response> listPendenze(Integer page,
String idPendenza,
String numeroAvviso,
String idDominio,
String identificativoDebitore) {
String identificativoDebitore,
StatoPendenza stato,
LocalDate dataDa,
LocalDate dataA,
String iuv,
String direzione,
String divisione,
String idA2A,
List<String> idTipoPendenza) {
ListQueryValidator.rejectUnsupported(currentRequest, LIST_PENDENZE_QUERY_PARAMS);
// cursor mode attivo se ?cursor=... e' presente nella query string,
// anche con valore vuoto ("prima pagina cursor-mode", scope G issue #9).
boolean cursorMode = ListQueryValidator.isCursorMode(currentRequest);
if (cursorMode) {
ListQueryValidator.rejectCursorIncompatible(currentRequest,
"dataOraUltimoAggiornamento DESC, id DESC");
"dataCreazione DESC, id DESC");
}
PendenzaListQuery query = new PendenzaListQuery(
page == null ? 1 : page,
Expand All @@ -84,7 +96,15 @@ public ResponseEntity<ListPendenze200Response> listPendenze(Integer page,
idPendenza,
numeroAvviso,
idDominio,
identificativoDebitore);
identificativoDebitore,
stato,
dataDa,
dataA,
iuv,
direzione,
divisione,
idA2A,
idTipoPendenza);
return ResponseEntity.ok(service.list(query, currentRequest));
}

Expand Down
15 changes: 14 additions & 1 deletion src/main/java/it/govpay/console/pendenza/PendenzaListQuery.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package it.govpay.console.pendenza;

import java.time.LocalDate;
import java.util.List;

import it.govpay.console.model.StatoPendenza;

public record PendenzaListQuery(
int page,
int limit,
Expand All @@ -9,5 +14,13 @@ public record PendenzaListQuery(
String idPendenza,
String numeroAvviso,
String idDominio,
String identificativoDebitore) {
String identificativoDebitore,
StatoPendenza stato,
LocalDate dataDa,
LocalDate dataA,
String iuv,
String direzione,
String divisione,
String idA2A,
List<String> idTipoPendenza) {
}
21 changes: 5 additions & 16 deletions src/main/java/it/govpay/console/pendenza/PendenzaMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -90,22 +90,11 @@ StatoPendenza mapStato(String statoV1, OffsetDateTime dataScadenza) {
}

private static StatoPendenza baseMapStato(String statoV1) {
// V1 usa varianti femminili/maschili; normalizziamo prima di mappare.
String normalized = statoV1.trim().toUpperCase();
return switch (normalized) {
case "ESEGUITA", "ESEGUITO", "PAGATA", "PAGATO" -> StatoPendenza.PAGATA;
case "NON_ESEGUITA", "NON_ESEGUITO", "NON_PAGATA", "NON_PAGATO" -> StatoPendenza.NON_PAGATA;
case "ESEGUITA_PARZIALE", "ESEGUITO_PARZIALE", "PAGATA_PARZIALE", "PAGATO_PARZIALE" ->
StatoPendenza.PAGATA_PARZIALE;
case "INCASSATA", "INCASSATO", "RICONCILIATA", "RICONCILIATO" -> StatoPendenza.RICONCILIATA;
case "ANNULLATA", "ANNULLATO" -> StatoPendenza.ANNULLATA;
case "SCADUTA", "SCADUTO" -> StatoPendenza.SCADUTA;
case "ANOMALA", "ANOMALO" -> StatoPendenza.ANOMALA;
default -> {
log.warn("Stato versamento V1 sconosciuto, mappato a ANOMALA: {}", statoV1);
yield StatoPendenza.ANOMALA;
}
};
StatoPendenza mapped = StatoVersamentoMapping.baseMap(statoV1);
if (mapped == StatoPendenza.ANOMALA && !StatoVersamentoMapping.isRiconosciuto(statoV1)) {
log.warn("Stato versamento V1 sconosciuto, mappato a ANOMALA: {}", statoV1);
}
return mapped;
}

static DominioRef mapDominio(Dominio dominio) {
Expand Down
63 changes: 57 additions & 6 deletions src/main/java/it/govpay/console/pendenza/PendenzaService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package it.govpay.console.pendenza;

import java.time.Clock;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.HashMap;
Expand Down Expand Up @@ -50,11 +51,14 @@ public class PendenzaService {

public static final String AZIONE_AUDIT_RICERCA = "PENDENZE_RICERCA_PER_DEBITORE";

private static final int MAX_ID_TIPO_PENDENZA = 50;

private final VersamentoRepository repository;
private final PendenzaMapper mapper;
private final PendenzaLinksBuilder linksBuilder;
private final CurrentOperatorService currentOperatorService;
private final AuditService auditService;
private final Clock clock;

@PersistenceContext
private EntityManager entityManager;
Expand All @@ -63,12 +67,14 @@ public PendenzaService(VersamentoRepository repository,
PendenzaMapper mapper,
PendenzaLinksBuilder linksBuilder,
CurrentOperatorService currentOperatorService,
AuditService auditService) {
AuditService auditService,
Clock clock) {
this.repository = repository;
this.mapper = mapper;
this.linksBuilder = linksBuilder;
this.currentOperatorService = currentOperatorService;
this.auditService = auditService;
this.clock = clock;
}

@Transactional(readOnly = true)
Expand Down Expand Up @@ -99,19 +105,35 @@ public Pendenza get(String idA2A, String idPendenza, Set<PendenzaExpand> expand)
@Transactional(readOnly = true)
public ListPendenze200Response list(PendenzaListQuery query, HttpServletRequest request) {
OperatoreCorrente operatore = currentOperatorService.get();
log.debug("listPendenze filtri[idPendenza={}, numeroAvviso={}, idDominio={}, identificativoDebitore={}], "
+ "page={}, limit={}, sort={}, total={}, cursor={}, operatore={}",
log.debug("listPendenze filtri[idPendenza={}, numeroAvviso={}, idDominio={}, identificativoDebitore={}, "
+ "stato={}, dataDa={}, dataA={}, iuv={}, direzione={}, divisione={}, idA2A={}, "
+ "idTipoPendenza={}], page={}, limit={}, sort={}, total={}, cursor={}, operatore={}",
query.idPendenza(), query.numeroAvviso(), query.idDominio(), query.identificativoDebitore(),
query.stato(), query.dataDa(), query.dataA(), query.iuv(), query.direzione(), query.divisione(),
query.idA2A(), query.idTipoPendenza(),
query.page(), query.limit(), query.sort(), query.total(),
query.cursor() != null, operatore.principal());

if (query.dataDa() != null && query.dataA() != null && query.dataDa().isAfter(query.dataA())) {
throw new BadRequestException("'dataDa' non puo' essere successiva a 'dataA'.");
}
List<String> idTipoPendenza = normalizeIdTipoPendenza(query.idTipoPendenza());

// Spring Data JPA 4.x: Specification.allOf rifiuta null. Filtriamo i predicati assenti.
Specification<Versamento> spec = Specification.allOf(
java.util.stream.Stream.of(
PendenzaSpecifications.idPendenzaPartial(query.idPendenza()),
PendenzaSpecifications.numeroAvvisoExact(query.numeroAvviso()),
PendenzaSpecifications.idDominioExact(query.idDominio()),
PendenzaSpecifications.identificativoDebitoreExact(query.identificativoDebitore()),
PendenzaSpecifications.statoExact(query.stato(), OffsetDateTime.now(clock)),
PendenzaSpecifications.dataCreazioneDa(query.dataDa()),
PendenzaSpecifications.dataCreazioneA(query.dataA()),
PendenzaSpecifications.iuvExact(query.iuv()),
PendenzaSpecifications.direzioneExact(query.direzione()),
PendenzaSpecifications.divisioneExact(query.divisione()),
PendenzaSpecifications.idA2AExact(query.idA2A()),
PendenzaSpecifications.idTipoPendenzaIn(idTipoPendenza),
PendenzaSpecifications.visibiliPerOperatore(operatore))
.filter(java.util.Objects::nonNull)
.toList());
Expand Down Expand Up @@ -146,6 +168,28 @@ public ListPendenze200Response list(PendenzaListQuery query, HttpServletRequest
return response;
}

/**
* {@code null} se il parametro non e' presente; altrimenti rimuove i valori
* vuoti (elementi CSV consecutivi, es. {@code idTipoPendenza=,,}) e valida
* i vincoli dell'OpenAPI: lista risultante non vuota, al massimo
* {@value #MAX_ID_TIPO_PENDENZA} elementi.
*/
private List<String> normalizeIdTipoPendenza(List<String> raw) {
if (raw == null) {
return null;
}
List<String> normalized = raw.stream().filter(v -> v != null && !v.isBlank()).toList();
if (normalized.isEmpty()) {
throw new BadRequestException(
"'idTipoPendenza' non puo' essere vuoto o composto solo da separatori.");
}
if (normalized.size() > MAX_ID_TIPO_PENDENZA) {
throw new BadRequestException(
"'idTipoPendenza' supporta al massimo " + MAX_ID_TIPO_PENDENZA + " elementi.");
}
return normalized;
}

private List<Versamento> listOffsetMode(Specification<Versamento> spec,
PendenzaListQuery query,
ListPendenze200Response response) {
Expand Down Expand Up @@ -180,13 +224,20 @@ private List<Versamento> listOffsetMode(Specification<Versamento> spec,

/**
* Modalita' cursor (keyset): ordina per
* {@code (dataOraUltimoAggiornamento DESC, id DESC)} e filtra con
* {@code (dataCreazione DESC, id DESC)} e filtra con
* {@code WHERE data < :ts OR (data = :ts AND id < :id)}. Carica {@code limit+1}
* righe per determinare {@code hasNext}.
*
* <p>Se il cursor e' vuoto (caso "prima pagina cursor mode", attivato da
* {@code ?cursor=} senza valore), il filtro keyset viene omesso e si
* usano solo l'ordinamento e il limit.
*
* <p>Verifica indici (issue #66, non applicata: lo schema di {@code versamenti}
* e' condiviso col core, la migrazione va concordata a parte). Sul DDL V1
* reale esiste solo {@code idx_vrs_data_creaz(data_creazione DESC)}, a singola
* colonna: non copre il tiebreak su {@code id} di questa query. Proposta:
* {@code CREATE INDEX idx_vrs_data_creaz_id ON versamenti (data_creazione DESC, id DESC);}
* (sostituirebbe {@code idx_vrs_data_creaz}, che ne e' un prefisso).
*/
private List<Versamento> listCursorMode(Specification<Versamento> spec,
PendenzaListQuery query,
Expand All @@ -200,7 +251,7 @@ private List<Versamento> listCursorMode(Specification<Versamento> spec,

if (hasNext && !rows.isEmpty()) {
Versamento last = rows.get(rows.size() - 1);
response.setNextCursor(CursorCodec.encode(last.getDataOraUltimoAggiornamento(), last.getId()));
response.setNextCursor(CursorCodec.encode(last.getDataCreazione(), last.getId()));
}
return rows;
}
Expand All @@ -213,7 +264,7 @@ private List<Versamento> findByCursor(Specification<Versamento> spec,
Root<Versamento> root = q.from(Versamento.class);

Predicate specPredicate = spec.toPredicate(root, q, cb);
Path<OffsetDateTime> dataPath = root.get("dataOraUltimoAggiornamento");
Path<OffsetDateTime> dataPath = root.get("dataCreazione");
Path<Long> idPath = root.get("id");

Predicate where;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,22 @@
public final class PendenzaSortParser {

/**
* Campi sortable allineati a V1 ({@code ListaPendenzeDTO}: {@code dataCaricamento},
* Campi sortable allineati a V1 ({@code ListaPendenzeDTO}: {@code dataCreazione},
* {@code dataValidita}, {@code dataScadenza}, {@code stato}) piu'
* {@code dataUltimoAggiornamento} aggiunto come default dall'issue #9.
* {@code dataCreazione} (rinominata da {@code dataCaricamento}, issue #66)
* e' anche il default e la chiave dell'ordinamento cursor.
* Le chiavi della map sono i nomi pubblici (query param), i valori i nomi
* dei campi entity JPA per la query.
*/
private static final Map<String, String> WHITELIST = Map.of(
"dataUltimoAggiornamento", "dataOraUltimoAggiornamento",
"dataCaricamento", "dataCreazione",
"dataCreazione", "dataCreazione",
"dataValidita", "dataValidita",
"dataScadenza", "dataScadenza",
"stato", "statoVersamento");

public static final String DEFAULT_SORT_RAW = "-dataUltimoAggiornamento";
public static final String DEFAULT_SORT_RAW = "-dataCreazione";

private PendenzaSortParser() {
}
Expand Down
Loading
Loading