Compare commits

..

64 Commits

Author SHA1 Message Date
Antonio Manca
c2530030a8 Merge pull request #396 from Kitzanos/feature/GEPAFINBE-6429-prod
Cherry-pick (Fixed ranking issue)
2026-04-16 07:16:25 +02:00
rajesh
c9534187af Fixed ranking issue 2026-04-15 17:30:43 +05:30
Antonio Manca
5e849d76d1 Merge pull request #395 from Kitzanos/pdf-amount-issue-prod
Cherry-pick (PDF Amount Issue)
2026-04-14 10:22:07 +02:00
rajesh
5a24549de3 Fixed PDF calculation issue 2026-04-14 13:43:56 +05:30
Rinaldo
9b321e700d Merge pull request #394 from Kitzanos/feature/GEPAFINBE-6425-prod
Cherry-pick (Fixed application status issue)
2026-04-13 14:23:52 +02:00
rajesh
7176ec27eb Resolved conflict 2026-04-13 17:50:02 +05:30
rajesh
5aa342bd05 Resolved conflict 2026-04-13 17:44:49 +05:30
Rinaldo
eef2d621f8 Merge pull request #393 from Kitzanos/develop
Sync Master with Develop (09/04/2026)
2026-04-09 14:42:24 +02:00
rajesh
a5606c1018 Updated system email templates and role permissions 2026-04-09 18:07:57 +05:30
rajesh
67bf81e5ae Merge branch 'master' of https://github.com/Kitzanos/GEPAFIN-BE into develop 2026-04-09 16:22:42 +05:30
Rinaldo
3c8f2b0c84 Merge pull request #392 from Kitzanos/feature/GEPAFINBE-6412-prod
Cherry-pick (Updated mailgun credentials)
2026-04-09 09:38:28 +02:00
rajesh
b644707f85 Resolved conflict 2026-04-09 12:58:10 +05:30
Rinaldo
acf2e4f73c Merge pull request #390 from Kitzanos/amendment-start-date-prod
Cherry-pick ( Updated Amendment Start Date Flow)
2026-03-17 12:23:42 +01:00
rajesh
63f8bd4a85 Resolved conflict 2026-03-17 15:48:47 +05:30
rajesh
b225b9f042 Resolved conflict 2026-03-17 15:37:50 +05:30
Rinaldo
1e703e4a40 Merge pull request #389 from Kitzanos/email-reject-prod
Cherry-pick ( Handled Email Rejection)
2026-03-16 16:21:48 +01:00
rajesh
d0b05aae11 Updated code 2026-03-16 20:07:27 +05:30
rajesh
94c86ebacd Updated amendment rejection email flow and handled for special amendment 2026-03-16 20:04:56 +05:30
rajesh
3410f6b45f Handled rejected email rollback cases 2026-03-16 19:59:59 +05:30
Rinaldo
9536935d02 Merge pull request #388 from Kitzanos/feature/GEPAFINBE-6233/6268-prod
Cherry-pick ( GEPAFINBE- 6233 & GEPAFINBE-6268)
Gestione Aziende Costituende
Possibilità di rifiutare pec
2026-03-13 07:39:40 +01:00
rajesh
bdb7b7d885 Updated the update company API 2026-03-13 11:42:27 +05:30
rajesh
00080d6a6c Created new endpoint to update VAT number of company 2026-03-13 11:41:47 +05:30
rajesh
5fe2291a3e Created new endpoint to reject emails by director 2026-03-13 11:41:09 +05:30
rajesh
e2c73db9f7 Updated code 2026-03-13 11:40:26 +05:30
rajesh
f0f904dabe Managed company creation 2026-03-13 11:39:55 +05:30
Rinaldo
7c564f999e Merge pull request #387 from Kitzanos/signed-document-id-prod
Cherry-pick ( Added documentAttachmentId in Application Evaluation Response)
2026-03-09 09:28:50 +01:00
rajesh
2c6a64f408 Added documentAttachmentId in response of evaluation 2026-03-09 13:49:35 +05:30
Rinaldo
78b15a0ea0 Merge pull request #386 from Kitzanos/external-document-prod
Cherry-pick ( Fixed upload document to external system issue)
2026-03-05 13:02:27 +01:00
rajesh
0669d2c928 Fixed upload document external system issue 2026-03-05 16:56:04 +05:30
Rinaldo
43047b5cc9 Merge pull request #385 from Kitzanos/handled-non-existent-user-prod
Cherry-pick ( Handled non-existent user login error)
2026-03-05 11:34:34 +01:00
rajesh
aa34bf698e Handled non-existent user login error 2026-03-05 15:45:41 +05:30
Rinaldo
eb5e0f73e2 Merge pull request #384 from Kitzanos/fix-ndg-save-issue-prod
Cherry-pick ( Fixed NDG issue)
2026-03-03 07:56:59 +01:00
rajesh
aa7ac4fef2 Fixed NDG issue 2026-03-03 12:19:44 +05:30
Antonio Manca
cadc4639c5 Merge pull request #383 from Kitzanos/ndg-null-check-prod
Cherry-pick ( Applied null checks in NDG flow)
2026-03-02 14:55:08 +01:00
rajesh
255ee536e1 Applied null check 2026-03-02 18:03:40 +05:30
Rinaldo
6d21d46523 Merge pull request #382 from Kitzanos/task-blue-tongue-prod
Cherry-pick( Implemented email sending flow for type BLUE TONGUE)
2026-02-23 12:43:43 +01:00
rajesh
ccc6ac2c8f Updated application status setting flow 2026-02-23 17:09:13 +05:30
rajesh
b65b3eb47c Updated code 2026-02-23 17:08:19 +05:30
rajesh
f0c15b928e Resolved conflict 2026-02-23 17:06:54 +05:30
Rinaldo
8b72e372a1 Merge pull request #380 from Kitzanos/fixed-doc-issue-mail
Cherry-pick ( Fixed document issue in mail sending API)
2026-02-20 12:29:46 +01:00
rajesh
05f74b3728 Fixed document issue in mail sending API 2026-02-20 16:49:14 +05:30
Rinaldo
3347624d1d Merge pull request #379 from Kitzanos/calculation-removal-prod
Cherry-pick (Removed calculation logic from the evaluation form)
2026-02-16 16:44:04 +01:00
rajesh
7a1f3dd1e8 Removed calculation logic from the evaluation form 2026-02-16 21:10:34 +05:30
Rinaldo
3671d83d25 Merge pull request #378 from Kitzanos/enabled-director-profile-prod
Cherry-pick (Enabled the mail functionality for director profile)
2026-02-12 10:39:54 +01:00
rajesh
ce0320ced6 Enabled the mail functionality for director profile 2026-02-12 14:51:32 +05:30
Rinaldo
b69b4ace36 Merge pull request #377 from Kitzanos/removed-appointment-check
Cherry-pick ( Removed condition of APPOINTMENT status)
2026-02-03 11:15:05 +01:00
rajesh
c56ff7658c Removed condition of APPOINTMENT status 2026-02-03 15:26:55 +05:30
Rinaldo
158e48a2f2 Merge pull request #376 from Kitzanos/ndg-password-update
Updated NDG password
2026-01-28 16:10:43 +01:00
rajesh
b75dcf6532 Updated code 2026-01-28 20:37:24 +05:30
rajesh
0d0aafe31b Updated password of NDG appointment 2026-01-28 20:36:36 +05:30
Antonio Manca
6f3d3ac4b2 Merge pull request #375 from Kitzanos/company-document-prod
Cherry-pick (Enabled permission for accessing company documents)
2025-11-20 10:50:19 +01:00
rajesh
551c4b04df Enabled company document for director 2025-11-20 15:14:34 +05:30
rajesh
6d1b8dc041 Allowed permission to instructor for accessing company document API 2025-11-20 15:14:00 +05:30
Rinaldo
97a992083b Merge pull request #374 from Kitzanos/disable-director-profile-prod
Cherry-pick (Disabled director profile feature)
2025-11-19 13:07:17 +01:00
rajesh
26e7430f93 Disabled director profile feature 2025-11-19 17:31:01 +05:30
Rinaldo
bf086741f7 Merge pull request #373 from Kitzanos/fixed-email-hub-issue-prod
Cherry-pick (FIxed email issue)
2025-11-19 12:17:51 +01:00
rajesh
763aa8ab09 Fixed issue of email 2025-11-19 16:40:13 +05:30
Rinaldo
26d9cf46bb Merge pull request #372 from Kitzanos/feature/GEPAFINBE-6158-prod
Cherry-pick (GEPAFINBE-6158 (Managed company documents for application))
2025-11-19 08:51:53 +01:00
rajesh
e69c6ba8e0 Updated code for company document 2025-11-19 13:18:25 +05:30
rajesh
b52b82d8de Added companyId in evaluation response 2025-11-19 12:41:20 +05:30
rajesh
d8538b0b35 Disabled evaluation scheduler and increased evaluation duration 2025-11-19 12:40:47 +05:30
rajesh
feca1e86bf Fixed issue in company document get endpoint 2025-11-19 12:28:11 +05:30
rajesh
e8a59ce277 Resolved conflicts 2025-11-19 12:26:34 +05:30
Rinaldo
277663639d Merge pull request #371 from Kitzanos/develop
Sync Master with Develop (17/11/2025)
2025-11-17 11:20:10 +01:00
10 changed files with 651 additions and 23 deletions

View File

@@ -1164,15 +1164,12 @@ public class ApplicationAmendmentRequestDao {
applicationEvaluationRepository.save(existingApplicationAmendment.getApplicationEvaluationEntity()); applicationEvaluationRepository.save(existingApplicationAmendment.getApplicationEvaluationEntity());
log.info("Updated ApplicationEvaluation status to OPEN for ID: {}", existingApplicationEvaluationEntity.getId()); log.info("Updated ApplicationEvaluation status to OPEN for ID: {}", existingApplicationEvaluationEntity.getId());
String previousApplicationStatus = application.getPreviousStatus(); if(Boolean.FALSE.equals(existingApplicationAmendment.getType().equals(ApplicationAmendmentRequestTypeEnum.SPECIAL.getValue()))){
if (previousApplicationStatus != null) { application.setStatus(application.getPreviousStatus());
application.setStatus(previousApplicationStatus); applicationRepository.save(application);
} else if (ApplicationAmendmentRequestTypeEnum.SPECIAL.getValue().equals(existingApplicationAmendment.getType())) { loggingUtil.addVersionHistory(
application.setStatus(ApplicationStatusTypeEnum.ADMISSIBLE.getValue()); VersionHistoryRequest.builder().request(request).actionType(VersionActionTypeEnum.UPDATE).oldData(oldApplicationEntityData).newData(application).build());
log.warn("Special amendment close: previousStatus was null for applicationId={}, defaulting to ADMISSIBLE", application.getId());
} }
applicationRepository.save(application);
log.info("Updated Application status to previous state for Application ID: {}", application.getId());
existingApplicationAmendment.getApplicationEvaluationEntity().getAssignedApplicationsEntity().setStatus(AssignedApplicationEnum.OPEN.getValue()); existingApplicationAmendment.getApplicationEvaluationEntity().getAssignedApplicationsEntity().setStatus(AssignedApplicationEnum.OPEN.getValue());
@@ -1193,9 +1190,6 @@ public class ApplicationAmendmentRequestDao {
loggingUtil.addVersionHistory(VersionHistoryRequest.builder().request(request).actionType(VersionActionTypeEnum.UPDATE).oldData(oldApplicationEvaluationEntity) loggingUtil.addVersionHistory(VersionHistoryRequest.builder().request(request).actionType(VersionActionTypeEnum.UPDATE).oldData(oldApplicationEvaluationEntity)
.newData(existingApplicationEvaluationEntity).build()); .newData(existingApplicationEvaluationEntity).build());
/** This code is responsible for adding a version history log for the "Update Application status" operation. **/
loggingUtil.addVersionHistory(
VersionHistoryRequest.builder().request(request).actionType(VersionActionTypeEnum.UPDATE).oldData(oldApplicationEntityData).newData(application).build());
/** This code is responsible for adding a version history log for the "Update assigned application " operation. **/ /** This code is responsible for adding a version history log for the "Update assigned application " operation. **/
loggingUtil.addVersionHistory(VersionHistoryRequest.builder().request(request).actionType(VersionActionTypeEnum.UPDATE).oldData(oldAssignedApplicationData) loggingUtil.addVersionHistory(VersionHistoryRequest.builder().request(request).actionType(VersionActionTypeEnum.UPDATE).oldData(oldAssignedApplicationData)

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

@@ -481,7 +481,7 @@ public class PdfDao {
try { try {
if (Boolean.FALSE.equals(StringUtils.isEmpty(fieldValue))) { if (Boolean.FALSE.equals(StringUtils.isEmpty(fieldValue))) {
// Use Locale.ITALY to parse the number with the Italian format (comma as decimal separator) // Use Locale.ITALY to parse the number with the Italian format (comma as decimal separator)
NumberFormat format = NumberFormat.getInstance(Locale.ENGLISH); NumberFormat format = NumberFormat.getInstance(Locale.ITALY);
Number number = format.parse(fieldValue); // Parse the fieldValue as a number Number number = format.parse(fieldValue); // Parse the fieldValue as a number
double numericValue = number.doubleValue(); // Convert the parsed number to double double numericValue = number.doubleValue(); // Convert the parsed number to double

View File

@@ -15,6 +15,7 @@ import net.gepafin.tendermanagement.enums.StatusTypeEnum;
import net.gepafin.tendermanagement.model.response.EmailLogResponse; import net.gepafin.tendermanagement.model.response.EmailLogResponse;
import net.gepafin.tendermanagement.model.response.PecEmailLogResponse; import net.gepafin.tendermanagement.model.response.PecEmailLogResponse;
import net.gepafin.tendermanagement.model.response.PecMailResponse; import net.gepafin.tendermanagement.model.response.PecMailResponse;
import net.gepafin.tendermanagement.repositories.*;
import net.gepafin.tendermanagement.enums.ApplicationAmendmentRequestEnum; import net.gepafin.tendermanagement.enums.ApplicationAmendmentRequestEnum;
import net.gepafin.tendermanagement.repositories.ApplicationAmendmentRequestRepository; import net.gepafin.tendermanagement.repositories.ApplicationAmendmentRequestRepository;
import net.gepafin.tendermanagement.repositories.ApplicationEvaluationRepository; import net.gepafin.tendermanagement.repositories.ApplicationEvaluationRepository;
@@ -281,7 +282,7 @@ public class PecMailDao {
for(EmailLogEntity emailLogEntity:emailLogs) { for(EmailLogEntity emailLogEntity:emailLogs) {
PecEmailLogResponse pecEmailLogResponse = createPecEmailLogResponse(userActionId, emailLogEntity, callName); PecEmailLogResponse pecEmailLogResponse = createPecEmailLogResponse(userActionId, emailLogEntity, callName);
pecEmailLogResponses.add(pecEmailLogResponse); pecEmailLogResponses.add(pecEmailLogResponse);
} }
return pecEmailLogResponses; return pecEmailLogResponses;
} }

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

@@ -24,8 +24,8 @@ import org.springframework.stereotype.Component;
import net.gepafin.tendermanagement.entities.ApplicationAmendmentRequestEntity; import net.gepafin.tendermanagement.entities.ApplicationAmendmentRequestEntity;
import net.gepafin.tendermanagement.entities.ApplicationEvaluationEntity; import net.gepafin.tendermanagement.entities.ApplicationEvaluationEntity;
import net.gepafin.tendermanagement.enums.ApplicationAmendmentRequestEnum; import net.gepafin.tendermanagement.enums.ApplicationAmendmentRequestEnum;
import net.gepafin.tendermanagement.enums.ApplicationAmendmentRequestTypeEnum;
import net.gepafin.tendermanagement.enums.ApplicationEvaluationStatusTypeEnum; import net.gepafin.tendermanagement.enums.ApplicationEvaluationStatusTypeEnum;
import net.gepafin.tendermanagement.enums.ApplicationStatusTypeEnum;
import net.gepafin.tendermanagement.enums.AssignedApplicationEnum; import net.gepafin.tendermanagement.enums.AssignedApplicationEnum;
import net.gepafin.tendermanagement.enums.NotificationTypeEnum; import net.gepafin.tendermanagement.enums.NotificationTypeEnum;
import net.gepafin.tendermanagement.enums.UserActionContextEnum; import net.gepafin.tendermanagement.enums.UserActionContextEnum;
@@ -129,9 +129,22 @@ public class ApplicationAmendmentScheduler {
// Update AssignedApplicationsEntity status // Update AssignedApplicationsEntity status
updateAssignedApplicationStatus(evaluation.getAssignedApplicationsEntity()); updateAssignedApplicationStatus(evaluation.getAssignedApplicationsEntity());
// Update ApplicationEntity status // Skip application status restore only when every amendment expired *in this run* for this
updateApplicationStatus(evaluation.getAssignedApplicationsEntity().getApplication()); // evaluation is SPECIAL. (Do not use all amendments on the eval — a closed non-SPECIAL
// record from the past would wrongly force a previousStatus restore after special expiry.)
List<ApplicationAmendmentRequestEntity> expiredThisRunForEval = amendmentRequests.stream()
.filter(req -> req.getApplicationEvaluationEntity().getId().equals(evaluation.getId()))
.toList();
boolean skipApplicationStatusRestore = !expiredThisRunForEval.isEmpty()
&& expiredThisRunForEval.stream()
.allMatch(req -> ApplicationAmendmentRequestTypeEnum.SPECIAL.getValue().equals(req.getType()));
if (!skipApplicationStatusRestore) {
updateApplicationStatus(evaluation.getAssignedApplicationsEntity().getApplication());
} else {
log.info(
"Amendment expiration: skipping Application status restore (all amendments expired this run are SPECIAL), evaluationId={}",
evaluation.getId());
}
log.info("Updated EndDate and suspension days for ApplicationEvaluation with ID: {}", log.info("Updated EndDate and suspension days for ApplicationEvaluation with ID: {}",
evaluation.getId()); evaluation.getId());

View File

@@ -3240,4 +3240,49 @@
<changeSet id="09-04-2026_RK_115923" author="Rajesh Khore"> <changeSet id="09-04-2026_RK_115923" author="Rajesh Khore">
<sqlFile dbms="postgresql" path="db/dump/update_email_config_for_mailgun_09_04_2026.sql" /> <sqlFile dbms="postgresql" path="db/dump/update_email_config_for_mailgun_09_04_2026.sql" />
</changeSet> </changeSet>
<changeSet id="09-04-2026_RK_143457" author="Rajesh Khore">
<sqlFile dbms="postgresql" path="db/dump/update_system_email_template_09_04_2026.sql" />
</changeSet>
<changeSet id="09-04-2026_RK_174653" author="Rajesh Khore">
<update tableName="role">
<column name="permissions" value="MANAGE_SUBSEQUENT_PHASES"/>
<where>role_type='ROLE_GEPAFIN_OPERATOR'</where>
</update>
<update tableName="role">
<column name="permissions" value="VIEW_CALLS,APPLY_CALLS"/>
<where>role_type='ROLE_BENEFICIARY'</where>
</update>
<update tableName="role">
<column name="permissions" value="EVALUATE_APPLICATIONS,MANAGE_PRELIMINARY_RELIEF"/>
<where>role_type='ROLE_PRE_INSTRUCTOR'</where>
</update>
<update tableName="role">
<column name="permissions" value="EVALUATE_APPLICATIONS,MANAGE_PRELIMINARY_RELIEF,ASSIGED_APPLICATION"/>
<where>role_type='ROLE_INSTRUCTOR_MANAGER'</where>
</update>
<update tableName="role">
<column name="permissions" value="VIEW_CONFIDI_CALLS,APPLY_CONFIDI_CALLS"/>
<where>role_type='ROLE_CONFIDI'</where>
</update>
<update tableName="role">
<column name="permissions" value="ROOT_MANAGE_APPL_VIEW_DELETED,ROOT_MANAGE_APPL_DELETE_CONFIRM"/>
<where>role_type='ROLE_DIRECTOR'</where>
</update>
<update tableName="role">
<column name="permissions" value="MANAGE_TENDERS,MANAGE_USERS,ROOT_MANAGE_NDG,ROOT_MANAGE_APPL_STATUS,ROOT_MANAGE_AMENDMENT_REOPEN,ROOT_MANAGE_AMENDMENT_EXTEND,ROOT_MANAGE_APPL_VIEW_DELETED,ROOT_MANAGE_APPL_DELETE,ROOT_MANAGE_PEC_SEND,ROOT_MANAGE_VIEW_LOG,ROOT_MANAGE_EMAIL_TEMPLATES,ROOT_MANAGE_CALL_COPY"/>
<where>role_type='ROLE_SUPER_ADMIN'</where>
</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> </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;

View File

@@ -0,0 +1,202 @@
-- 1
UPDATE gepafin_schema.system_email_template
SET template_name = 'Application submission template to beneficiary and company',
"type" = 'APPLICATION_SUBMISSION_TO_USER_AND_COMPANY',
html_content = '<html>
<body style="font-family: Arial, sans-serif; color: #333; line-height: 1.6;">
<div style="padding: 20px; border: 1px solid #ddd; border-radius: 8px; max-width: 600px; margin: auto;">
<p>Buongiorno,</p>
<p>
Si comunica che, in riferimento alla domanda di concessione di
Finanziamento agevolato a valere sul Fondo prestiti
<strong>{{call_name}}</strong> di cui all''oggetto, la stessa è stata
regolarmente acquisita ed è stata registrata con Protocollo n.
<strong>{{protocol_number}}</strong> del <strong>{{date}}</strong> alle
<strong>{{time}}</strong>.
</p>
<p>Distinti Saluti,</p>
<p>
<strong>{{email_signature}}</strong>
</p>
</div>
</body>
</html>',
subject = 'BANDO {{call_name}} - Domanda di concessione di finanziamento agevolato {{company_name}}'
WHERE email_scenario = 'APPLICATION_SUBMITTED'
AND type = 'APPLICATION_SUBMISSION_TO_USER_AND_COMPANY'
AND hub_id IS NULL;
-- 2
UPDATE gepafin_schema.system_email_template
SET template_name='Application submission template to gepafin',
"type"='APPLICATION_SUBMISSION_TO_GEPAFIN',
html_content='<html>
<body style="font-family: Arial, sans-serif; color: #333; line-height: 1.6;">
<div style="padding: 20px; border: 1px solid #ddd; border-radius: 8px; max-width: 600px; margin: auto;">
<p>
In riferimento alla domanda di concessione di Finanziamento agevolato a valere sul Fondo prestiti
<strong>{{call_name}}</strong> di cui all''oggetto, la stessa è stata regolarmente acquisita ed è stata
registrata con Protocollo n. <strong>{{protocol_number}}</strong> del <strong>{{date}}</strong> alle <strong>{{time}}</strong>.
</p>
<p>Distinti Saluti,</p>
<p>
<strong>{{email_signature}}</strong>
</p>
</div>
</body>
</html>',
subject='BANDO {{call_name}} - Domanda di concessione di finanziamento agevolato {{company_name}}'
WHERE email_scenario='APPLICATION_SUBMITTED'
AND type='APPLICATION_SUBMISSION_TO_GEPAFIN'
AND hub_id IS NULL;
-- 3
UPDATE gepafin_schema.system_email_template
SET html_content='<html>
<body style="font-family: Arial, sans-serif; color: #000; line-height: 1.6;">
<div style="padding: 20px; border: 1px solid #ddd; border-radius: 8px; max-width: 600px; margin: auto;">
<p><strong>RICHIESTA INTEGRAZIONE DOCUMENTALE</strong></p>
<p>Buongiorno,</p>
<p>In riferimento alla domanda di concessione di Finanziamento agevolato a valere sul Fondo prestiti
<strong>{{call_name}}</strong> di cui al <strong>Protocollo n. {{protocol_number}} del
{{protocol_date}} e {{protocol_time}}</strong>, alla luce dell''attività istruttoria svolta,
segnaliamo quanto segue:
</p>
<ul>
{{form_dataInput}}
</ul>
<p>{{note}}</p>
<p>Vi invitiamo a fornire quanto sopra richiesto integrando la documentazione sia caricandola all''interno dello sportello
online <a href="{{platform_link}}">{{platform_link}}</a> che inviandola a mezzo PEC all''indirizzo
bandi.gepafin@legalmail.it entro e <strong>non oltre {{response_days}} giorni</strong> dal ricevimento della presente comunicazione,
precisando che, in caso di mancata ricezione nei termini indicati, saremo costretti a non prendere in considerazione la Vostra richiesta di finanziamento.
</p>
<p>Vi informiamo che per la ricezione della PEC farà fede la ricevuta di avvenuta consegna che attesterà il buon esito
dell''invio. La documentazione trasmessa e le informazioni fornite saranno processate dall''istruttore assegnatario della pratica.
</p>
<p>Distinti Saluti,</p>
<p><strong>{{email_signature}}</strong></p>
</div>
</body>
</html>'
WHERE email_scenario='APPLICATION_AMENDMENT_REQUESTED'
AND hub_id IS NULL;
-- 4
UPDATE gepafin_schema.system_email_template
SET subject='BANDO {{call_name}} - Domanda di finanziamento non ammessa {{company_name}}'
WHERE email_scenario='APPLICATION_AMENDMENT_EXPIRED';
-- 5
UPDATE gepafin_schema.system_email_template
SET subject='BANDO {{call_name}} Esito positivo istruttoria di ammissibilità {{company_name}}'
WHERE email_scenario='APPLICATION_ADMISSIBLE';
-- 6
UPDATE gepafin_schema.system_email_template
SET html_content='<html> <body style="font-family: Arial, sans-serif; color: #000; line-height: 1.6;">
<div style="padding: 20px; border: 1px solid #ddd; border-radius: 8px; max-width: 600px; margin: auto;">
<p>Buongiorno,</p>
<p>Si comunica che, in riferimento alla domanda a valere sul bando “<strong>{{call_name}}</strong>” di cui al
<strong>Protocollo n. {{protocol_number}} del {{protocol_date}} alle {{protocol_time}}</strong>,
</p>
<p>
{{tipo_inammissibilita}}.</p>
<p>Le motivazioni sono le seguenti: <strong>{{form_text}}</strong></p>
<p>Vi ricordiamo che i Beneficiari che hanno presentato richieste valutate non ammissibili entro 10 giorni dalla data di ricevimento della presente potranno finoltrare richiesta di chiarimenti e/o osservazioni alla scrivente Società ai sensi e per gli effetti dell''art.10 bis della L.241/1990 e s.m.i.</p>
<p>Distinti Saluti,</p>
<p><strong>{{email_signature}}</strong></p>
</div>
</body>
</html>'
WHERE email_scenario='APPLICATION_REJECTED'
AND hub_id IS NULL;
-- 7
UPDATE gepafin_schema.system_email_template
SET html_content='<html>
<body style="font-family: Arial, sans-serif; color: #000; line-height: 1.6;">
<div style="padding: 20px; border: 1px solid #ddd; border-radius: 8px; max-width: 600px; margin: auto;">
<p><strong>PROMEMORIA PER LA PRESENTAZIONE DELL''Soccorso Istruttorio</strong></p>
<p>Buongiorno,</p>
<p>Questo è un promemoria per completare la presentazione dell''Soccorso Istruttorio entro il termine specificato. Di seguito i dettagli:</p>
<ul>
<li><strong>Amendment ID:</strong> {{amendment_id}}</li>
<li><strong>Data di Scadenza:</strong> {{amendment_due_date}}</li>
</ul>
<p>Si prega di assicurarsi che l''Soccorso Istruttorio venga presentato entro la data di scadenza per evitare ritardi. Inviare l''Soccorso Istruttorio tramite la piattaforma online <a href="{{platform_link}}">{{platform_link}}</a> </p>
<p>Distinti saluti,</p>
<p><strong>{{email_signature}}</strong></p>
</div>
</body>
</html>'
WHERE email_scenario='APPLICATION_AMENDMENT_REMINDER';
-- 8
UPDATE gepafin_schema.system_email_template
SET template_name='Welcome Email for New Confidi User'
WHERE email_scenario='USER_CREATION' AND type='USER_ONBOARDING_CONFIDI';
-- 9
UPDATE gepafin_schema.system_email_template
SET template_name='Password Reset Link Email (Italian)'
WHERE email_scenario='PASSWORD_RESET_REQUEST';
-- 10 (hub specific)
UPDATE gepafin_schema.system_email_template
SET html_content='<html>
<body style="font-family: Arial, sans-serif; color: #000; line-height: 1.6;">
<div style="padding: 20px; border: 1px solid #ddd; border-radius: 8px; max-width: 600px; margin: auto;">
<p><strong>RICHIESTA INTEGRAZIONE DOCUMENTALE</strong></p>
<p>Buongiorno,</p>
<p>In riferimento alla domanda di concessione di Finanziamento agevolato a valere sul Bando
"<strong>{{call_name}}</strong>" di cui al <strong>Protocollo n. {{protocol_number}} del
{{protocol_date}} e {{protocol_time}}</strong>, alla luce dell''attività istruttoria svolta,
segnaliamo quanto segue:
</p>
{{note}}
<p>Vi invitiamo a fornire quanto sopra richiesto integrando la documentazione caricandola all''interno dello sportello
online <a href="{{platform_link}}">{{platform_link}}</a> entro e <strong>non oltre {{response_days}} giorni</strong> dal ricevimento della presente comunicazione,
precisando che, in caso di mancata ricezione nei termini indicati, saremo costretti a non prendere in considerazione la Vostra richiesta di finanziamento.
</p>
<p>La documentazione trasmessa e le informazioni fornite saranno processate dall''istruttore assegnatario della pratica.
</p>
<p>Distinti Saluti,</p>
<p><strong>{{email_signature}}</strong></p>
</div>
</body>
</html>'
WHERE email_scenario='APPLICATION_AMENDMENT_REQUESTED' AND hub_id=2;
-- 11 (hub specific reject)
UPDATE gepafin_schema.system_email_template
SET subject='BANDO {{call_name}} Esito negativo istruttoria di ammissibilità {{company_name}}'
WHERE email_scenario='APPLICATION_REJECTED' AND hub_id=2;
-- 12
UPDATE gepafin_schema.system_email_template
SET template_name='Welcome Email for New Bandi User'
WHERE email_scenario='USER_CREATION' AND type='USER_ONBOARDING_BANDI';
-- 13
UPDATE gepafin_schema.system_email_template
SET template_name='Application submission failure notification'
WHERE email_scenario='APPLICATION_SUBMISSION_FAILURE';
-- 14
UPDATE gepafin_schema.system_email_template
SET subject='BANDO "{{call_name}}" Esito negativo della valutazione tecnica {{company_name}}'
WHERE email_scenario='APPLICATION_TECHNICAL_EVALUATION_REJECTED';
-- 15
UPDATE gepafin_schema.system_email_template
SET subject='Comunicazione esito valutazione tecnica ed economico-finanziaria Avviso {{call_name}} '
WHERE email_scenario='SPECIAL_APPLICATION_AMENDMENT_REQUESTED';
-- 16
UPDATE gepafin_schema.system_email_template
SET subject='Comunicazione esito valutazione tecnica ed economico-finanziaria Avviso {{call_name}} '
WHERE email_scenario='SPECIAL_APPLICATION_AMENDMENT_REQUESTED_BLUE_TONGUE';

View File

@@ -434,4 +434,4 @@ amount.field.not.provided= Si prega di fornire i campi obbligatori per l'importo
vat.or.tax.code.required=<EFBFBD> obbligatorio il numero di partita IVA o il codice fiscale. vat.or.tax.code.required=<EFBFBD> obbligatorio il numero di partita IVA o il codice fiscale.
provide.valid.vat.number=Inserisci un numero di partita IVA valido per procedere con NDG. provide.valid.vat.number=Inserisci un numero di partita IVA valido per procedere con NDG.
application.status.transition.restricted=Questa modifica di stato non <20> disponibile tramite questa operazione. application.status.transition.restricted=Questa modifica di stato non <20> disponibile tramite questa operazione.
application.registry.segment.invalid=Valore del segmento non valido. application.registry.segment.invalid=Valore del segmento non valido.