Merge pull request #396 from Kitzanos/feature/GEPAFINBE-6429-prod

Cherry-pick (Fixed ranking issue)
This commit is contained in:
Antonio Manca
2026-04-16 07:16:25 +02:00
committed by GitHub
4 changed files with 381 additions and 5 deletions

View File

@@ -1173,26 +1173,57 @@ public class ApplicationDao {
validateRankingActionRequest(rankingActionType, manualRanking); validateRankingActionRequest(rankingActionType, manualRanking);
ApplicationEntity oldApplicationEntity = Utils.getClonedEntityForData(applicationEntity); ApplicationEntity oldApplicationEntity = Utils.getClonedEntityForData(applicationEntity);
if (rankingActionType == null) { if (rankingActionType == null) {
compactManualRanksAfterLeavingReposition(applicationEntity);
applicationEntity.setRankingActionType(null); applicationEntity.setRankingActionType(null);
applicationEntity.setManualRanking(null); applicationEntity.setManualRanking(null);
} else { } else {
if (rankingActionType == ApplicationRankingActionTypeEnum.REPOSITION if (rankingActionType == ApplicationRankingActionTypeEnum.REPOSITION) {
&& applicationRepository.existsByCallIdAndManualRankingAndIsDeletedFalseAndIdNot( shiftOtherManualRanksForReposition(applicationEntity, manualRanking);
applicationEntity.getCall().getId(), manualRanking, applicationEntity.getId())) { } else if (rankingActionType == ApplicationRankingActionTypeEnum.REMOVE) {
throw new CustomValidationException(Status.BAD_REQUEST, compactManualRanksAfterLeavingReposition(applicationEntity);
Translator.toLocale(GepafinConstant.APPLICATION_RANKING_ACTION_INVALID));
} }
applicationEntity.setRankingActionType(rankingActionType.getValue()); applicationEntity.setRankingActionType(rankingActionType.getValue());
applicationEntity.setManualRanking( applicationEntity.setManualRanking(
rankingActionType == ApplicationRankingActionTypeEnum.REPOSITION ? manualRanking : null); rankingActionType == ApplicationRankingActionTypeEnum.REPOSITION ? manualRanking : null);
} }
applicationEntity = applicationRepository.save(applicationEntity); applicationEntity = applicationRepository.save(applicationEntity);
normalizeDenseManualRanksForCall(applicationEntity.getCall().getId());
applicationEntity = applicationRepository.findById(applicationEntity.getId()).orElse(applicationEntity);
loggingUtil.addVersionHistory( loggingUtil.addVersionHistory(
VersionHistoryRequest.builder().request(request).actionType(VersionActionTypeEnum.UPDATE) VersionHistoryRequest.builder().request(request).actionType(VersionActionTypeEnum.UPDATE)
.oldData(oldApplicationEntity).newData(applicationEntity).build()); .oldData(oldApplicationEntity).newData(applicationEntity).build());
return getApplicationResponse(applicationEntity); return getApplicationResponse(applicationEntity);
} }
/**
* Keeps stored manual ranks in {@code 1..N} for the call (same count as repositioned rows with a manual value),
* preserving relative order. Prevents gaps or values above the pool size (e.g. 2,3,4,5 with four apps → 1,2,3,4).
*/
private void normalizeDenseManualRanksForCall(Long callId) {
String reposition = ApplicationRankingActionTypeEnum.REPOSITION.getValue();
List<ApplicationEntity> repositioned = applicationRepository.findByCallIdAndIsDeletedFalse(callId).stream()
.filter(a -> ApplicationStatusTypeEnum.APPROVED.getValue().equals(a.getStatus()))
.filter(a -> reposition.equalsIgnoreCase(StringUtils.trimToEmpty(a.getRankingActionType())))
.filter(a -> a.getManualRanking() != null)
.sorted(Comparator.comparing(ApplicationEntity::getManualRanking).thenComparing(ApplicationEntity::getId))
.collect(Collectors.toList());
if (repositioned.isEmpty()) {
return;
}
boolean changed = false;
long slot = 1L;
for (ApplicationEntity a : repositioned) {
if (!Objects.equals(a.getManualRanking(), slot)) {
a.setManualRanking(slot);
changed = true;
}
slot++;
}
if (changed) {
applicationRepository.saveAll(repositioned);
}
}
public CallRankingSummaryResponse getApplicationRanking(Long callId, public CallRankingSummaryResponse getApplicationRanking(Long callId,
List<ApplicationRankingActionTypeEnum> rankingActionTypes) { List<ApplicationRankingActionTypeEnum> rankingActionTypes) {
CallEntity call = callRepository.findById(callId) CallEntity call = callRepository.findById(callId)
@@ -1269,6 +1300,48 @@ public class ApplicationDao {
} }
} }
/**
* When an application stops using manual REPOSITION (cleared or REMOVE), every other manual rank above its
* old slot shifts down by one so ranks stay dense (e.g. after removing rank 1, 2 and 3 become 1 and 2).
*/
private void compactManualRanksAfterLeavingReposition(ApplicationEntity applicationEntity) {
String repositionType = ApplicationRankingActionTypeEnum.REPOSITION.getValue();
if (!repositionType.equalsIgnoreCase(StringUtils.trimToEmpty(applicationEntity.getRankingActionType()))) {
return;
}
Long releasedRank = applicationEntity.getManualRanking();
if (releasedRank == null) {
return;
}
applicationRepository.compactRepositionedManualRankingAfterSlotFreed(
applicationEntity.getCall().getId(),
applicationEntity.getId(),
releasedRank,
repositionType);
}
private void shiftOtherManualRanksForReposition(ApplicationEntity applicationEntity, Long newRank) {
Long callId = applicationEntity.getCall().getId();
Long applicationId = applicationEntity.getId();
String repositionType = ApplicationRankingActionTypeEnum.REPOSITION.getValue();
Long oldRank = applicationEntity.getManualRanking();
if (Objects.equals(oldRank, newRank)) {
return;
}
if (oldRank == null) {
applicationRepository.bumpRepositionedManualRankingFromRankInclusive(callId, applicationId, newRank,
repositionType);
return;
}
if (newRank < oldRank) {
applicationRepository.bumpRepositionedManualRankingUpInHalfOpenInterval(callId, applicationId, newRank,
oldRank, repositionType);
} else {
applicationRepository.bumpRepositionedManualRankingDownInHalfOpenInterval(callId, applicationId, oldRank,
newRank, repositionType);
}
}
/** /**
* Sets all non-terminal amendments, the application evaluation (if any), and the assigned application row to CLOSE. * Sets all non-terminal amendments, the application evaluation (if any), and the assigned application row to CLOSE.
*/ */

View File

@@ -185,4 +185,67 @@ public interface ApplicationRepository extends JpaRepository<ApplicationEntity,
boolean existsByCallIdAndManualRankingAndIsDeletedFalseAndIdNot(Long callId, Long manualRanking, Long id); boolean existsByCallIdAndManualRankingAndIsDeletedFalseAndIdNot(Long callId, Long manualRanking, Long id);
/**
* Increments manual_ranking by 1 for other REPOSITION rows in the call with rank &gt;= fromRank (insert / move up).
*/
@Modifying
@Transactional
@Query("UPDATE ApplicationEntity a SET a.manualRanking = a.manualRanking + 1 "
+ "WHERE a.call.id = :callId AND a.isDeleted = false "
+ "AND a.rankingActionType = :repositionType "
+ "AND a.manualRanking IS NOT NULL AND a.id <> :applicationId "
+ "AND a.manualRanking >= :fromRank")
int bumpRepositionedManualRankingFromRankInclusive(@Param("callId") Long callId,
@Param("applicationId") Long applicationId,
@Param("fromRank") Long fromRank,
@Param("repositionType") String repositionType);
/**
* Increments manual_ranking by 1 for ranks in [lowRank, highRank) — used when moving this application to a better (smaller) rank.
*/
@Modifying
@Transactional
@Query("UPDATE ApplicationEntity a SET a.manualRanking = a.manualRanking + 1 "
+ "WHERE a.call.id = :callId AND a.isDeleted = false "
+ "AND a.rankingActionType = :repositionType "
+ "AND a.manualRanking IS NOT NULL AND a.id <> :applicationId "
+ "AND a.manualRanking >= :lowRank AND a.manualRanking < :highRank")
int bumpRepositionedManualRankingUpInHalfOpenInterval(@Param("callId") Long callId,
@Param("applicationId") Long applicationId,
@Param("lowRank") Long lowRank,
@Param("highRank") Long highRank,
@Param("repositionType") String repositionType);
/**
* Decrements manual_ranking by 1 for ranks in (lowRank, highRank] — used when moving this application to a worse (larger) rank.
*/
@Modifying
@Transactional
@Query("UPDATE ApplicationEntity a SET a.manualRanking = a.manualRanking - 1 "
+ "WHERE a.call.id = :callId AND a.isDeleted = false "
+ "AND a.rankingActionType = :repositionType "
+ "AND a.manualRanking IS NOT NULL AND a.id <> :applicationId "
+ "AND a.manualRanking > :lowRank AND a.manualRanking <= :highRank")
int bumpRepositionedManualRankingDownInHalfOpenInterval(@Param("callId") Long callId,
@Param("applicationId") Long applicationId,
@Param("lowRank") Long lowRank,
@Param("highRank") Long highRank,
@Param("repositionType") String repositionType);
/**
* After this application leaves manual REPOSITION, closes the gap: every other REPOSITION row with rank
* strictly greater than {@code releasedRank} moves down by 1.
*/
@Modifying
@Transactional
@Query("UPDATE ApplicationEntity a SET a.manualRanking = a.manualRanking - 1 "
+ "WHERE a.call.id = :callId AND a.isDeleted = false "
+ "AND a.rankingActionType = :repositionType "
+ "AND a.manualRanking IS NOT NULL AND a.id <> :applicationId "
+ "AND a.manualRanking > :releasedRank")
int compactRepositionedManualRankingAfterSlotFreed(@Param("callId") Long callId,
@Param("applicationId") Long applicationId,
@Param("releasedRank") Long releasedRank,
@Param("repositionType") String repositionType);
} }

View File

@@ -3282,4 +3282,7 @@
</update> </update>
</changeSet> </changeSet>
<changeSet id="15-04-2026_RK_153423" author="Rajesh Khore">
<sqlFile dbms="postgresql" path="db/dump/update_application_ranking_view_15_04_2026.sql"/>
</changeSet>
</databaseChangeLog> </databaseChangeLog>

View File

@@ -0,0 +1,237 @@
DROP VIEW IF EXISTS application_ranking_view;
CREATE OR REPLACE VIEW application_ranking_view AS
WITH evaluation_scores AS (
SELECT
ae.application_id,
COALESCE(
SUM(
COALESCE(NULLIF(TRIM(score_item ->> 'score'), ''), '0')::numeric
),
0
) AS total_score
FROM application_evaluation ae
LEFT JOIN LATERAL jsonb_array_elements(
CASE
WHEN ae.criteria IS NULL OR BTRIM(ae.criteria) = '' THEN '[]'::jsonb
ELSE ae.criteria::jsonb
END
) score_item ON TRUE
WHERE ae.is_deleted = false
GROUP BY ae.application_id
),
approved_applications AS (
SELECT
a.id AS application_id,
a.call_id,
a.company_id,
a.user_id,
a.status AS status,
a.submission_date,
a.amount_requested,
a.amount_accepted,
a.manual_ranking,
a.ranking_action_type,
a.ndg,
a.pec_email,
c.ranking_type,
p.created_date AS protocol_datetime,
p.protocol_number AS protocol_number,
COALESCE(a.amount_accepted, a.amount_requested, 0) AS amount,
COALESCE(es.total_score, 0) AS total_score
FROM application a
JOIN call c ON c.id = a.call_id
AND (c.is_deleted = false OR c.is_deleted IS NULL)
AND NULLIF(BTRIM(c.ranking_type), '') IS NOT NULL
LEFT JOIN protocol p ON p.id = a.protocol_number
LEFT JOIN evaluation_scores es ON es.application_id = a.id
WHERE (a.is_deleted = false OR a.is_deleted IS NULL)
AND a.status = 'APPROVED'
AND COALESCE(a.ranking_action_type, '') <> 'EXCLUDE'
),
ranking_core AS (
SELECT *
FROM approved_applications
WHERE COALESCE(ranking_action_type, '') <> 'REMOVE'
),
remove_apps AS (
SELECT *
FROM approved_applications
WHERE COALESCE(ranking_action_type, '') = 'REMOVE'
),
natural_ranked AS (
SELECT
rc.*,
ROW_NUMBER() OVER (
PARTITION BY rc.call_id
ORDER BY
CASE WHEN rc.ranking_type = 'SCORE' THEN rc.total_score END DESC NULLS LAST,
CASE WHEN rc.ranking_type = 'PROTOCOL_DATE_TIME' THEN rc.protocol_datetime END ASC NULLS LAST,
rc.protocol_datetime ASC NULLS LAST,
rc.application_id ASC
) AS natural_rank
FROM ranking_core rc
),
call_limits AS (
SELECT
nr.call_id,
COUNT(*)::bigint AS app_cnt,
COALESCE(
MAX(
CASE
WHEN COALESCE(nr.ranking_action_type, '') = 'REPOSITION'
AND nr.manual_ranking IS NOT NULL
THEN nr.manual_ranking
END
),
0
) AS max_manual
FROM natural_ranked nr
GROUP BY nr.call_id
),
free_ranks AS (
SELECT
cl.call_id,
gs.slot::bigint AS listing_rank,
ROW_NUMBER() OVER (PARTITION BY cl.call_id ORDER BY gs.slot) AS fill_order
FROM call_limits cl
CROSS JOIN LATERAL generate_series(
1,
GREATEST(cl.app_cnt::int, cl.max_manual::int)
) AS gs(slot)
WHERE NOT EXISTS (
SELECT 1
FROM natural_ranked mr
WHERE mr.call_id = cl.call_id
AND COALESCE(mr.ranking_action_type, '') = 'REPOSITION'
AND mr.manual_ranking IS NOT NULL
AND mr.manual_ranking = gs.slot
)
),
non_manual_ordered AS (
SELECT
nr.*,
ROW_NUMBER() OVER (
PARTITION BY nr.call_id
ORDER BY
CASE WHEN nr.ranking_type = 'SCORE' THEN nr.total_score END DESC NULLS LAST,
CASE WHEN nr.ranking_type = 'PROTOCOL_DATE_TIME' THEN nr.protocol_datetime END ASC NULLS LAST,
nr.protocol_datetime ASC NULLS LAST,
nr.application_id ASC
) AS fill_order
FROM natural_ranked nr
WHERE COALESCE(nr.ranking_action_type, '') <> 'REPOSITION'
OR nr.manual_ranking IS NULL
),
manual_part AS (
SELECT
mr.call_id,
mr.application_id,
mr.ranking_action_type,
mr.total_score,
mr.user_id,
mr.status,
mr.submission_date,
mr.protocol_datetime,
mr.protocol_number,
mr.ndg,
mr.amount_accepted,
mr.pec_email,
mr.manual_ranking,
mr.ranking_type,
mr.manual_ranking AS listing_rank
FROM natural_ranked mr
WHERE COALESCE(mr.ranking_action_type, '') = 'REPOSITION'
AND mr.manual_ranking IS NOT NULL
),
non_manual_part AS (
SELECT
nmo.call_id,
nmo.application_id,
nmo.ranking_action_type,
nmo.total_score,
nmo.user_id,
nmo.status,
nmo.submission_date,
nmo.protocol_datetime,
nmo.protocol_number,
nmo.ndg,
nmo.amount_accepted,
nmo.pec_email,
nmo.manual_ranking,
nmo.ranking_type,
fr.listing_rank
FROM non_manual_ordered nmo
JOIN free_ranks fr
ON fr.call_id = nmo.call_id
AND fr.fill_order = nmo.fill_order
),
ranked_pool AS (
SELECT * FROM manual_part
UNION ALL
SELECT * FROM non_manual_part
),
pool_max AS (
SELECT call_id, COALESCE(MAX(listing_rank), 0) AS max_lr
FROM ranked_pool
GROUP BY call_id
),
remove_part AS (
SELECT
ra.call_id,
ra.application_id,
ra.ranking_action_type,
ra.total_score,
ra.user_id,
ra.status,
ra.submission_date,
ra.protocol_datetime,
ra.protocol_number,
ra.ndg,
ra.amount_accepted,
ra.pec_email,
ra.manual_ranking,
ra.ranking_type,
COALESCE(pm.max_lr, 0)
+ ROW_NUMBER() OVER (
PARTITION BY ra.call_id
ORDER BY ra.protocol_datetime ASC NULLS LAST, ra.application_id
) AS listing_rank
FROM remove_apps ra
LEFT JOIN pool_max pm ON pm.call_id = ra.call_id
)
SELECT
rp.listing_rank,
rp.application_id,
rp.call_id,
rp.ranking_action_type,
rp.total_score,
rp.user_id,
rp.status,
rp.submission_date,
rp.protocol_datetime,
rp.protocol_number,
rp.ndg,
rp.amount_accepted,
rp.pec_email,
rp.manual_ranking,
rp.ranking_type
FROM ranked_pool rp
UNION ALL
SELECT
rv.listing_rank,
rv.application_id,
rv.call_id,
rv.ranking_action_type,
rv.total_score,
rv.user_id,
rv.status,
rv.submission_date,
rv.protocol_datetime,
rv.protocol_number,
rv.ndg,
rv.amount_accepted,
rv.pec_email,
rv.manual_ranking,
rv.ranking_type
FROM remove_part rv order by listing_rank asc;