Merge pull request #396 from Kitzanos/feature/GEPAFINBE-6429-prod
Cherry-pick (Fixed ranking issue)
This commit is contained in:
@@ -1173,26 +1173,57 @@ public class ApplicationDao {
|
||||
validateRankingActionRequest(rankingActionType, manualRanking);
|
||||
ApplicationEntity oldApplicationEntity = Utils.getClonedEntityForData(applicationEntity);
|
||||
if (rankingActionType == null) {
|
||||
compactManualRanksAfterLeavingReposition(applicationEntity);
|
||||
applicationEntity.setRankingActionType(null);
|
||||
applicationEntity.setManualRanking(null);
|
||||
} else {
|
||||
if (rankingActionType == ApplicationRankingActionTypeEnum.REPOSITION
|
||||
&& applicationRepository.existsByCallIdAndManualRankingAndIsDeletedFalseAndIdNot(
|
||||
applicationEntity.getCall().getId(), manualRanking, applicationEntity.getId())) {
|
||||
throw new CustomValidationException(Status.BAD_REQUEST,
|
||||
Translator.toLocale(GepafinConstant.APPLICATION_RANKING_ACTION_INVALID));
|
||||
if (rankingActionType == ApplicationRankingActionTypeEnum.REPOSITION) {
|
||||
shiftOtherManualRanksForReposition(applicationEntity, manualRanking);
|
||||
} else if (rankingActionType == ApplicationRankingActionTypeEnum.REMOVE) {
|
||||
compactManualRanksAfterLeavingReposition(applicationEntity);
|
||||
}
|
||||
applicationEntity.setRankingActionType(rankingActionType.getValue());
|
||||
applicationEntity.setManualRanking(
|
||||
rankingActionType == ApplicationRankingActionTypeEnum.REPOSITION ? manualRanking : null);
|
||||
}
|
||||
applicationEntity = applicationRepository.save(applicationEntity);
|
||||
normalizeDenseManualRanksForCall(applicationEntity.getCall().getId());
|
||||
applicationEntity = applicationRepository.findById(applicationEntity.getId()).orElse(applicationEntity);
|
||||
loggingUtil.addVersionHistory(
|
||||
VersionHistoryRequest.builder().request(request).actionType(VersionActionTypeEnum.UPDATE)
|
||||
.oldData(oldApplicationEntity).newData(applicationEntity).build());
|
||||
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,
|
||||
List<ApplicationRankingActionTypeEnum> rankingActionTypes) {
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -185,4 +185,67 @@ public interface ApplicationRepository extends JpaRepository<ApplicationEntity,
|
||||
|
||||
boolean existsByCallIdAndManualRankingAndIsDeletedFalseAndIdNot(Long callId, Long manualRanking, Long id);
|
||||
|
||||
/**
|
||||
* Increments manual_ranking by 1 for other REPOSITION rows in the call with rank >= 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);
|
||||
|
||||
}
|
||||
|
||||
@@ -3282,4 +3282,7 @@
|
||||
</update>
|
||||
</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>
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user