• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DataBiosphere / consent / #6139

25 Jun 2025 05:44PM UTC coverage: 79.389% (+0.08%) from 79.309%
#6139

push

web-flow
DT-1823: Signing Official Emails (#2572)

Co-authored-by: otchet-broad <111771148+otchet-broad@users.noreply.github.com>

91 of 109 new or added lines in 8 files covered. (83.49%)

10400 of 13100 relevant lines covered (79.39%)

0.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.98
/src/main/java/org/broadinstitute/consent/http/service/VoteService.java
1
package org.broadinstitute.consent.http.service;
2

3
import static java.util.function.Predicate.not;
4

5
import com.google.api.client.http.HttpStatusCodes;
6
import com.google.common.annotations.VisibleForTesting;
7
import com.google.gson.Gson;
8
import com.google.gson.reflect.TypeToken;
9
import com.google.inject.Inject;
10
import freemarker.template.TemplateException;
11
import jakarta.ws.rs.NotFoundException;
12
import java.io.IOException;
13
import java.lang.reflect.Type;
14
import java.sql.SQLException;
15
import java.util.ArrayList;
16
import java.util.Collections;
17
import java.util.Date;
18
import java.util.HashMap;
19
import java.util.HashSet;
20
import java.util.List;
21
import java.util.Map;
22
import java.util.Objects;
23
import java.util.Set;
24
import java.util.stream.Collectors;
25
import org.apache.commons.lang3.StringUtils;
26
import org.apache.commons.validator.routines.EmailValidator;
27
import org.broadinstitute.consent.http.db.DataAccessRequestDAO;
28
import org.broadinstitute.consent.http.db.DatasetDAO;
29
import org.broadinstitute.consent.http.db.ElectionDAO;
30
import org.broadinstitute.consent.http.db.UserDAO;
31
import org.broadinstitute.consent.http.db.VoteDAO;
32
import org.broadinstitute.consent.http.enumeration.DataUseTranslationType;
33
import org.broadinstitute.consent.http.enumeration.ElectionStatus;
34
import org.broadinstitute.consent.http.enumeration.ElectionType;
35
import org.broadinstitute.consent.http.enumeration.UserRoles;
36
import org.broadinstitute.consent.http.enumeration.VoteType;
37
import org.broadinstitute.consent.http.models.Dac;
38
import org.broadinstitute.consent.http.models.DataAccessRequest;
39
import org.broadinstitute.consent.http.models.Dataset;
40
import org.broadinstitute.consent.http.models.Election;
41
import org.broadinstitute.consent.http.models.Study;
42
import org.broadinstitute.consent.http.models.StudyProperty;
43
import org.broadinstitute.consent.http.models.User;
44
import org.broadinstitute.consent.http.models.Vote;
45
import org.broadinstitute.consent.http.models.dataset_registration_v1.builder.DatasetRegistrationSchemaV1Builder;
46
import org.broadinstitute.consent.http.models.dto.DatasetMailDTO;
47
import org.broadinstitute.consent.http.service.dao.VoteServiceDAO;
48
import org.broadinstitute.consent.http.util.ComplianceLogger;
49
import org.broadinstitute.consent.http.util.ConsentLogger;
50
import org.broadinstitute.consent.http.util.gson.GsonUtil;
51
import org.glassfish.jersey.server.ContainerRequest;
52

53
public class VoteService implements ConsentLogger {
54

55
  private final UserDAO userDAO;
56
  private final DataAccessRequestDAO dataAccessRequestDAO;
57
  private final DatasetDAO datasetDAO;
58
  private final ElectionDAO electionDAO;
59
  private final EmailService emailService;
60
  private final ElasticSearchService elasticSearchService;
61
  private final UseRestrictionConverter useRestrictionConverter;
62
  private final VoteDAO voteDAO;
63
  private final VoteServiceDAO voteServiceDAO;
64

65
  @Inject
66
  public VoteService(UserDAO userDAO, DataAccessRequestDAO dataAccessRequestDAO,
67
      DatasetDAO datasetDAO, ElectionDAO electionDAO, EmailService emailService,
68
      ElasticSearchService elasticSearchService, UseRestrictionConverter useRestrictionConverter,
69
      VoteDAO voteDAO, VoteServiceDAO voteServiceDAO) {
1✔
70
    this.userDAO = userDAO;
1✔
71
    this.dataAccessRequestDAO = dataAccessRequestDAO;
1✔
72
    this.datasetDAO = datasetDAO;
1✔
73
    this.electionDAO = electionDAO;
1✔
74
    this.emailService = emailService;
1✔
75
    this.elasticSearchService = elasticSearchService;
1✔
76
    this.useRestrictionConverter = useRestrictionConverter;
1✔
77
    this.voteDAO = voteDAO;
1✔
78
    this.voteServiceDAO = voteServiceDAO;
1✔
79
  }
1✔
80

81
  /**
82
   * @param vote Vote to update
83
   * @return The updated Vote
84
   */
85
  public Vote updateVote(Vote vote) {
86
    validateVote(vote);
1✔
87
    Date now = new Date();
1✔
88
    voteDAO.updateVote(
1✔
89
        vote.getVote(),
1✔
90
        vote.getRationale(),
1✔
91
        Objects.isNull(vote.getUpdateDate()) ? now : vote.getUpdateDate(),
1✔
92
        vote.getVoteId(),
1✔
93
        vote.getIsReminderSent(),
1✔
94
        vote.getElectionId(),
1✔
95
        Objects.isNull(vote.getCreateDate()) ? now : vote.getCreateDate(),
1✔
96
        vote.getHasConcerns()
1✔
97
    );
98
    return voteDAO.findVoteById(vote.getVoteId());
1✔
99
  }
100

101

102
  public Vote updateVote(Vote rec, Integer voteId, String referenceId)
103
      throws IllegalArgumentException {
104
    if (voteDAO.checkVoteById(referenceId, voteId) == null) {
1✔
105
      notFoundException(voteId);
×
106
    }
107
    Vote vote = voteDAO.findVoteById(voteId);
1✔
108
    Date updateDate = rec.getVote() == null ? null : new Date();
1✔
109
    String rationale = StringUtils.isNotEmpty(rec.getRationale()) ? rec.getRationale() : null;
1✔
110
    voteDAO.updateVote(rec.getVote(), rationale, updateDate, voteId, false, vote.getElectionId(),
1✔
111
        vote.getCreateDate(), rec.getHasConcerns());
1✔
112
    return voteDAO.findVoteById(voteId);
1✔
113
  }
114

115
  /**
116
   * Create votes for an election
117
   *
118
   * @param election       The Election
119
   * @param electionType   The Election type
120
   * @param isManualReview Is this a manual review election
121
   * @return List of votes
122
   */
123
  @SuppressWarnings("DuplicatedCode")
124
  public List<Vote> createVotes(Election election, ElectionType electionType,
125
      Boolean isManualReview) {
126
    Dac dac = electionDAO.findDacForElection(election.getElectionId());
1✔
127
    Set<User> users;
128
    if (dac != null) {
1✔
129
      users = userDAO.findUsersEnabledToVoteByDAC(dac.getDacId());
×
130
    } else {
131
      users = userDAO.findNonDacUsersEnabledToVote();
1✔
132
    }
133
    List<Vote> votes = new ArrayList<>();
1✔
134
    if (users != null) {
1✔
135
      for (User user : users) {
1✔
136
        votes.addAll(createVotesForUser(user, election, electionType, isManualReview));
1✔
137
      }
1✔
138
    }
139
    return votes;
1✔
140
  }
141

142
  /**
143
   * Create election votes for a user
144
   *
145
   * @param user           DACUser
146
   * @param election       Election
147
   * @param electionType   ElectionType
148
   * @param isManualReview Is election manual review
149
   * @return List of created votes
150
   */
151
  public List<Vote> createVotesForUser(User user, Election election, ElectionType electionType,
152
      Boolean isManualReview) {
153
    Dac dac = electionDAO.findDacForElection(election.getElectionId());
1✔
154
    List<Vote> votes = new ArrayList<>();
1✔
155
    Integer dacVoteId = voteDAO.insertVote(user.getUserId(), election.getElectionId(),
1✔
156
        VoteType.DAC.getValue());
1✔
157
    votes.add(voteDAO.findVoteById(dacVoteId));
1✔
158
    if (isDacChairPerson(dac, user)) {
1✔
159
      Integer chairVoteId = voteDAO.insertVote(user.getUserId(), election.getElectionId(),
1✔
160
          VoteType.CHAIRPERSON.getValue());
1✔
161
      votes.add(voteDAO.findVoteById(chairVoteId));
1✔
162
      // Requires Chairperson role to create a final and agreement vote in the Data Access case
163
      if (electionType.equals(ElectionType.DATA_ACCESS)) {
1✔
164
        Integer finalVoteId = voteDAO.insertVote(user.getUserId(), election.getElectionId(),
1✔
165
            VoteType.FINAL.getValue());
1✔
166
        votes.add(voteDAO.findVoteById(finalVoteId));
1✔
167
        if (!isManualReview) {
1✔
168
          Integer agreementVoteId = voteDAO.insertVote(user.getUserId(), election.getElectionId(),
1✔
169
              VoteType.AGREEMENT.getValue());
1✔
170
          votes.add(voteDAO.findVoteById(agreementVoteId));
1✔
171
        }
172
      }
173
    }
174
    return votes;
1✔
175
  }
176

177
  public List<Vote> findVotesByIds(List<Integer> voteIds) {
178
    if (voteIds.isEmpty()) {
1✔
179
      return Collections.emptyList();
1✔
180
    }
181
    return voteDAO.findVotesByIds(voteIds);
1✔
182
  }
183

184
  /**
185
   * Delete any votes in Open elections for the specified user in the specified Dac.
186
   *
187
   * @param dac  The Dac we are restricting elections to
188
   * @param user The Dac member we are deleting votes for
189
   */
190
  public void deleteOpenDacVotesForUser(Dac dac, User user) {
191
    List<Integer> openElectionIds = electionDAO.findOpenElectionsByDacId(dac.getDacId()).stream().
×
192
        map(Election::getElectionId).
×
193
        collect(Collectors.toList());
×
194
    if (!openElectionIds.isEmpty()) {
×
195
      List<Integer> openUserVoteIds = voteDAO.findVotesByElectionIds(openElectionIds).stream().
×
196
          filter(v -> v.getUserId().equals(user.getUserId())).
×
197
          map(Vote::getVoteId).
×
198
          collect(Collectors.toList());
×
199
      if (!openUserVoteIds.isEmpty()) {
×
200
        voteDAO.removeVotesByIds(openUserVoteIds);
×
201
      }
202
    }
203
  }
×
204

205
  /**
206
   * Update vote values. 'FINAL' votes impact elections so matching elections marked as
207
   * ElectionStatus.CLOSED as well. Approved 'FINAL' votes trigger an approval email to
208
   * researchers.
209
   *
210
   * @param votes     List of Votes to update
211
   * @param voteValue Value to update the votes to
212
   * @param rationale Value to update the rationales to. Only update if non-null.
213
   * @param user      The user making the update
214
   * @return The updated Vote
215
   * @throws IllegalArgumentException when there are non-open, non-rp elections on any of the votes
216
   */
217
  public List<Vote> updateVotesWithValue(List<Vote> votes, boolean voteValue, String rationale, User user)
218
      throws IllegalArgumentException {
219
    validateVotesCanUpdate(votes);
1✔
220
    try {
221
      List<Vote> updatedVotes = voteServiceDAO.updateVotesWithValue(votes, voteValue, rationale);
1✔
222
      if (voteValue) {
1✔
223
        try {
224
          sendDatasetApprovalNotifications(updatedVotes, user);
1✔
225
        } catch (Exception e) {
×
226
          // We can recover from email errors, log it and don't fail the overall process.
227
          String voteIds = votes.stream().map(Vote::getVoteId).map(Object::toString)
×
228
              .collect(Collectors.joining(","));
×
229
          String message =
×
230
              "Error notifying researchers and custodians for votes: [" + voteIds + "]: "
231
                  + e.getMessage();
×
232
          logException(message, e);
×
233
        }
1✔
234
      }
235
      return updatedVotes;
1✔
236
    } catch (SQLException e) {
×
237
      throw new IllegalArgumentException("Unable to update election votes.");
×
238
    }
239
  }
240

241
  /**
242
   * Review all positive, FINAL votes and send a notification to the researcher and data custodians
243
   * describing the approved access to datasets on their Data Access Request.
244
   *
245
   * @param votes List of Vote objects. In practice, this will be a batch of votes for a group of
246
   *              elections for datasets that all have the same data use restriction in a single
247
   *              DarCollection. This method is flexible enough to send email for any number of
248
   *              unrelated elections in various DarCollections.
249
   * @param user  The user sending approval notifications
250
   */
251
  public void sendDatasetApprovalNotifications(List<Vote> votes, User user) {
252

253
    List<Integer> finalElectionIds = votes.stream()
1✔
254
        .filter(Vote::getVote) // Safety check to ensure we're only emailing for approved election
1✔
255
        .filter(v -> VoteType.FINAL.getValue().equalsIgnoreCase(v.getType()))
1✔
256
        .map(Vote::getElectionId)
1✔
257
        .distinct()
1✔
258
        .collect(Collectors.toList());
1✔
259

260
    List<Election> finalElections = electionDAO.findElectionsByIds(finalElectionIds);
1✔
261

262
    List<String> finalElectionReferenceIds = finalElections.stream()
1✔
263
        .map(Election::getReferenceId)
1✔
264
        .distinct()
1✔
265
        .collect(Collectors.toList());
1✔
266

267
    List<DataAccessRequest> dars = dataAccessRequestDAO.findByReferenceIds(finalElectionReferenceIds);
1✔
268

269

270
    List<Integer> datasetIds = finalElections.stream()
1✔
271
        .map(Election::getDatasetId)
1✔
272
        .collect(Collectors.toList());
1✔
273
    List<Dataset> datasets =
274
        datasetIds.isEmpty() ? List.of() : datasetDAO.findDatasetsByIdList(datasetIds);
1✔
275

276
    try {
277
      elasticSearchService.indexDatasets(datasetIds, user);
1✔
278
    } catch (Exception e) {
×
279
      logException("Error indexing datasets for approved DARs: " + e.getMessage(), e);
×
280
    }
1✔
281

282
    // For each dar, email the researcher summarizing the approved datasets in that dar
283
    dars.forEach(dar -> {
1✔
284
      // Get the datasets in this collection that have been approved
285
      List<Dataset> approvedDatasetsInDar = datasets.stream()
1✔
286
          .filter(d -> dar.getDatasetIds().contains(d.getDatasetId()))
1✔
287
          .toList();
1✔
288

289
      if (!approvedDatasetsInDar.isEmpty()) {
1✔
290
        String darCode = dar.getDarCode();
1✔
291
        User researcher = userDAO.findUserById(dar.getUserId());
1✔
292
        Integer researcherId = researcher.getUserId();
1✔
293
        List<DatasetMailDTO> datasetMailDTOs = approvedDatasetsInDar
1✔
294
            .stream()
1✔
295
            .map(d -> new DatasetMailDTO(d.getName(), d.getDatasetIdentifier()))
1✔
296
            .toList();
1✔
297

298
        // Get all Data Use translations, distinctly in the case that there are several with the same
299
        // data use, and then conjoin them for email display.
300
        String translation = approvedDatasetsInDar.stream()
1✔
301
            .map(dataset -> useRestrictionConverter.translateDataUse(dataset.getDataUse(), DataUseTranslationType.DATASET))
1✔
302
            .distinct()
1✔
303
            .collect(Collectors.joining(";"));
1✔
304

305
        try {
306
          if(dar.getProgressReport()) {
1✔
307
            emailService.sendResearcherProgressReportApproved(darCode, researcherId, datasetMailDTOs,
1✔
308
                translation);
309
          } else {
310
            emailService.sendResearcherDarApproved(darCode, researcherId, datasetMailDTOs,
1✔
311
                translation);
312
          }
313
        } catch (Exception e) {
×
314
          logException("Error sending researcher dar approved email: " + e.getMessage(), e);
×
315
        }
1✔
316
        try {
317
          notifyCustodiansOfApprovedDatasets(approvedDatasetsInDar, researcher, darCode);
×
318
        } catch (Exception e) {
1✔
319
          logException("Error notifying custodians of dar approved email: " + e.getMessage(), e);
1✔
320
        }
×
321
        try {
322
          notifySigningOfficialsOfApprovedDatasets(approvedDatasetsInDar, researcher, dar, darCode, translation);
1✔
NEW
323
        } catch (Exception e) {
×
NEW
324
          logException("Error notifying signing officials of dar approved email: " + e.getMessage(), e);
×
325
        }
1✔
326
      }
327
    });
1✔
328
  }
1✔
329

330
  @VisibleForTesting
331
  protected void notifySigningOfficialsOfApprovedDatasets(List<Dataset> datasets, User researcher,
332
      DataAccessRequest dar, String darCode, String translation)
333
      throws TemplateException, IOException {
334
    if (researcher == null) {
1✔
335
      logWarn(
1✔
336
          "Unable to send new DAR/PR message to Signing Officials: Researcher does not exist: %s".formatted(
1✔
337
              dar.getUserId()));
1✔
338
      return;
1✔
339
    }
340
    if (researcher.getInstitutionId() == null) {
1✔
341
      logWarn(
1✔
342
          "Unable to send new DAR/PR message to Signing Officials: Researcher does not have an institution id: %s".formatted(
1✔
343
              dar.getUserId()));
1✔
344
      return;
1✔
345
    }
346
    List<User> signingOfficials = userDAO.getSOsByInstitution(researcher.getInstitutionId());
1✔
347
    for (User so : signingOfficials) {
1✔
348
      if (dar.getProgressReport()) {
1✔
349
        emailService.sendNewSoProgressReportApprovedEmail(so, darCode, researcher,
1✔
350
            dar.getReferenceId(), datasets, translation);
1✔
351
      } else {
352
        emailService.sendNewSoDARApprovedEmail(so, darCode, researcher, dar.getReferenceId(),
1✔
353
            datasets, translation);
354
      }
355
    }
1✔
356
  }
1✔
357

358
  /**
359
   * Notify all data submitters, custodians, depositors, and owners of a dataset approval.
360
   *
361
   * @param datasets   Requested datasets
362
   * @param researcher The approved researcher
363
   * @param darCode    The DAR Collection Code
364
   * @throws IllegalArgumentException when there are no custodians or depositors to notify
365
   */
366
  protected void notifyCustodiansOfApprovedDatasets(List<Dataset> datasets, User researcher,
367
      String darCode) throws IllegalArgumentException {
368
    Map<User, HashSet<Dataset>> custodianMap = new HashMap<>();
1✔
369

370
    // Find all the data custodians and submitters to notify for each dataset
371
    datasets.forEach(d -> {
1✔
372
      if (Objects.nonNull(d.getStudy())) {
1✔
373
        Study study = d.getStudy();
1✔
374

375
        // Data Submitter (study)
376
        if (Objects.nonNull(study.getCreateUserId())) {
1✔
377
          User submitter = userDAO.findUserById(study.getCreateUserId());
1✔
378
          if (Objects.nonNull(submitter)) {
1✔
379
            custodianMap.putIfAbsent(submitter, new HashSet<>());
1✔
380
            custodianMap.get(submitter).add(d);
1✔
381
          }
382
        }
383

384
        // Data Custodian (study)
385
        if (Objects.nonNull(study.getProperties())) {
1✔
386
          Type listOfStringType = new TypeToken<ArrayList<String>>() {}.getType();
1✔
387
          Gson gson = GsonUtil.gsonBuilderWithAdapters().create();
1✔
388
          Set<StudyProperty> props = study.getProperties();
1✔
389
          List<String> custodianEmails = new ArrayList<>();
1✔
390
          props.stream()
1✔
391
              .filter(p -> p.getKey().equals(DatasetRegistrationSchemaV1Builder.dataCustodianEmail))
1✔
392
              .forEach(p -> {
1✔
393
                String propValue = p.getValue().toString();
1✔
394
                try {
395
                  custodianEmails.addAll(gson.fromJson(propValue, listOfStringType));
1✔
396
                } catch (Exception e) {
×
397
                  logException("Error finding data custodians for study: " + study.getStudyId(), e);
×
398
                }
1✔
399
              });
1✔
400
          if (!custodianEmails.isEmpty()) {
1✔
401
            List<User> custodianUsers = userDAO.findUsersByEmailList(custodianEmails);
1✔
402
            custodianUsers.forEach(s -> {
1✔
403
              custodianMap.putIfAbsent(s, new HashSet<>());
1✔
404
              custodianMap.get(s).add(d);
1✔
405
            });
1✔
406
          }
407
        }
408
      }
409

410
      // Data Submitter (dataset)
411
      if (Objects.nonNull(d.getCreateUserId())) {
1✔
412
        User submitter = userDAO.findUserById(d.getCreateUserId());
1✔
413
        if (Objects.nonNull(submitter)) {
1✔
414
          custodianMap.putIfAbsent(submitter, new HashSet<>());
1✔
415
          custodianMap.get(submitter).add(d);
1✔
416
        }
417
      }
418
    });
1✔
419

420
    // Filter out invalid emails in custodian map
421
    EmailValidator emailValidator = EmailValidator.getInstance();
1✔
422
    Map<User, HashSet<Dataset>> validCustodians = custodianMap.entrySet().stream()
1✔
423
        .filter(e -> e.getKey().getEmail() != null && emailValidator.isValid(e.getKey().getEmail()))
1✔
424
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, HashMap::new));
1✔
425

426
    if (validCustodians.isEmpty()) {
1✔
427
      String identifiers = datasets.stream().map(Dataset::getDatasetIdentifier)
1✔
428
          .collect(Collectors.joining(", "));
1✔
429
      throw new IllegalArgumentException(
1✔
430
          "No submitters, custodians, owners, or depositors found for provided dataset identifiers: "
431
              + identifiers);
432
    }
433
    // For each custodian, notify them of their approved datasets
434
    for (Map.Entry<User, HashSet<Dataset>> entry : validCustodians.entrySet()) {
1✔
435
      List<DatasetMailDTO> datasetMailDTOs = entry.getValue()
1✔
436
          .stream()
1✔
437
          .map(d -> new DatasetMailDTO(d.getName(), d.getDatasetIdentifier()))
1✔
438
          .toList();
1✔
439
      try {
440
        emailService.sendDataCustodianApprovalMessage(
1✔
441
            entry.getKey(),
1✔
442
            darCode,
443
            datasetMailDTOs,
444
            entry.getKey().getDisplayName(),
1✔
445
            researcher.getEmail());
1✔
446
      } catch (Exception e) {
×
447
        logException("Error sending custodian approval email: " + e.getMessage(), e);
×
448
      }
1✔
449
    }
1✔
450
  }
1✔
451

452
  /**
453
   * The Rationale for RP Votes can be updated for any election status. The Rationale for DataAccess
454
   * Votes can only be updated for OPEN elections. Votes for elections of other types are not
455
   * updatable through this method.
456
   *
457
   * @param voteIds   List of vote ids for DataAccess and RP elections
458
   * @param rationale The rationale to update
459
   * @return List of updated votes
460
   * @throws IllegalArgumentException when there are non-open, non-rp elections on any of the votes
461
   */
462
  public List<Vote> updateRationaleByVoteIds(List<Integer> voteIds, String rationale)
463
      throws IllegalArgumentException {
464
    List<Vote> votes = voteDAO.findVotesByIds(voteIds);
1✔
465
    validateVotesCanUpdate(votes);
1✔
466
    voteDAO.updateRationaleByVoteIds(voteIds, rationale);
1✔
467
    return findVotesByIds(voteIds);
1✔
468
  }
469

470
  private void validateVotesCanUpdate(List<Vote> votes) throws IllegalArgumentException {
471
    List<Election> elections = electionDAO.findElectionsByIds(votes.stream()
1✔
472
        .map(Vote::getElectionId)
1✔
473
        .toList());
1✔
474

475
    // If there are any DataAccess elections in a non-open state, throw an error
476
    List<Election> nonOpenAccessElections = elections.stream()
1✔
477
        .filter(election -> election.getElectionType().equals(ElectionType.DATA_ACCESS.getValue()))
1✔
478
        .filter(election -> !election.getStatus().equals(ElectionStatus.OPEN.getValue()))
1✔
479
        .toList();
1✔
480
    if (!nonOpenAccessElections.isEmpty()) {
1✔
481
      throw new IllegalArgumentException(
1✔
482
          "There are non-open Data Access elections for provided votes");
483
    }
484

485
    // If there are non-DataAccess or non-RP elections, throw an error
486
    List<Election> disallowedElections = elections.stream()
1✔
487
        .filter(election -> !election.getElectionType().equals(ElectionType.DATA_ACCESS.getValue()))
1✔
488
        .filter(election -> !election.getElectionType().equals(ElectionType.RP.getValue()))
1✔
489
        .toList();
1✔
490
    if (!disallowedElections.isEmpty()) {
1✔
491
      throw new IllegalArgumentException(
1✔
492
          "There are non-Data Access/RP elections for provided votes");
493
    }
494
  }
1✔
495

496
  private boolean isDacChairPerson(Dac dac, User user) {
497
    if (dac != null) {
1✔
498
      return user.getRoles().
×
499
          stream().
×
500
          anyMatch(userRole -> Objects.nonNull(userRole.getRoleId()) &&
×
501
              Objects.nonNull(userRole.getDacId()) &&
×
502
              userRole.getRoleId().equals(UserRoles.CHAIRPERSON.getRoleId()) &&
×
503
              userRole.getDacId().equals(dac.getDacId()));
×
504
    }
505
    return user.getRoles().
1✔
506
        stream().
1✔
507
        anyMatch(userRole -> Objects.nonNull(userRole.getRoleId()) &&
1✔
508
            userRole.getRoleId().equals(UserRoles.CHAIRPERSON.getRoleId()));
1✔
509
  }
510

511
  /**
512
   * Convenience method to ensure Vote non-nullable values are populated
513
   *
514
   * @param vote The Vote to validate
515
   */
516
  private void validateVote(Vote vote) {
517
    if (Objects.isNull(vote) ||
1✔
518
        Objects.isNull(vote.getVoteId()) ||
1✔
519
        Objects.isNull(vote.getUserId()) ||
1✔
520
        Objects.isNull(vote.getElectionId())) {
1✔
521
      throw new IllegalArgumentException("Invalid vote: " + vote);
×
522
    }
523
    if (Objects.isNull(voteDAO.findVoteById(vote.getVoteId()))) {
1✔
524
      throw new IllegalArgumentException("No vote exists with the id of " + vote.getVoteId());
×
525
    }
526
  }
1✔
527

528
  private void notFoundException(Integer voteId) {
529
    throw new NotFoundException("Could not find vote for specified id. Vote id: " + voteId);
1✔
530
  }
531

532
  public void logDARApprovalOrRejection(User user, List<Vote> updatedVotes,
533
      ContainerRequest request) {
534
    List<Integer> approvedElectionIds = updatedVotes.stream()
1✔
535
        .filter(v -> v.getType().equals(VoteType.FINAL.getValue()))
1✔
536
        .filter(Vote::getVote)
1✔
537
        .map(Vote::getElectionId)
1✔
538
        .toList();
1✔
539
    List<Integer> approvedDatasetIds = electionDAO.findElectionsByIds(approvedElectionIds).stream()
1✔
540
        .map(Election::getDatasetId).toList();
1✔
541
    List<Dataset> approvedDatasets = datasetDAO.findDatasetsByIdList(approvedDatasetIds);
1✔
542
    ComplianceLogger.logDARApproval(user, approvedDatasets, request,
1✔
543
        HttpStatusCodes.STATUS_CODE_OK);
544

545
    List<Integer> rejectedElectionIds = updatedVotes.stream()
1✔
546
        .filter(v -> v.getType().equals(VoteType.FINAL.getValue()))
1✔
547
        .filter(not(Vote::getVote))
1✔
548
        .map(Vote::getElectionId)
1✔
549
        .toList();
1✔
550
    List<Integer> rejectedDatasetIds = electionDAO.findElectionsByIds(rejectedElectionIds).stream()
1✔
551
        .map(Election::getDatasetId).toList();
1✔
552
    List<Dataset> rejectedDatasets = datasetDAO.findDatasetsByIdList(rejectedDatasetIds);
1✔
553
    ComplianceLogger.logDARRejection(user, rejectedDatasets, request, HttpStatusCodes.STATUS_CODE_OK);
1✔
554
  }
1✔
555

556
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc