diff --git a/backend/openapi.json b/backend/openapi.json
index 55d6433d0..d9675163a 100644
--- a/backend/openapi.json
+++ b/backend/openapi.json
@@ -445,43 +445,43 @@
"fewer_ballots_count": {
"type": "integer",
"format": "int32",
- "description": "Number of fewer counted ballots (\"Er zijn minder stembiljetten geteld. Noteer hoeveel stembiljetten er minder zijn geteld\")",
+ "description": "Number of fewer counted ballots (\"Er zijn minder stembiljetten geteld. Hoeveel stembiljetten zijn er minder geteld\")",
"minimum": 0
},
"more_ballots_count": {
"type": "integer",
"format": "int32",
- "description": "Number of more counted ballots (\"Er zijn méér stembiljetten geteld. Noteer hoeveel stembiljetten er meer zijn geteld\")",
+ "description": "Number of more counted ballots (\"Er zijn méér stembiljetten geteld. Hoeveel stembiljetten zijn er meer geteld?\")",
"minimum": 0
},
"no_explanation_count": {
"type": "integer",
"format": "int32",
- "description": "Number of no explanations (\"Aantal keren dat er geen verklaring is voor het verschil\")",
+ "description": "Number of no explanations (\"Hoe vaak is er geen verklaring voor het verschil?\")",
"minimum": 0
},
"other_explanation_count": {
"type": "integer",
"format": "int32",
- "description": "Number of other explanations (\"Aantal keren dat er een andere verklaring is voor het verschil\")",
+ "description": "Number of other explanations (\"Hoe vaak is er een andere verklaring voor het verschil?\")",
"minimum": 0
},
"too_few_ballots_handed_out_count": {
"type": "integer",
"format": "int32",
- "description": "Number of fewer ballots handed out (\"Aantal keren dat er een stembiljet te weinig is uitgereikt\")",
+ "description": "Number of fewer ballots handed out (\"Hoe vaak is er een stembiljet te weinig uitgereikt?\")",
"minimum": 0
},
"too_many_ballots_handed_out_count": {
"type": "integer",
"format": "int32",
- "description": "Number of more ballots handed out (\"Aantal keren dat er een stembiljet teveel is uitgereikt\")",
+ "description": "Number of more ballots handed out (\"Hoe vaak is er een stembiljet te veel uitgereikt?\")",
"minimum": 0
},
"unreturned_ballots_count": {
"type": "integer",
"format": "int32",
- "description": "Number of unreturned ballots (\"Aantal keren dat een kiezer het stembiljet niet heeft ingeleverd\")",
+ "description": "Number of unreturned ballots (\"Hoe vaak heeft een kiezer het stembiljet niet ingeleverd?\")",
"minimum": 0
}
}
@@ -697,8 +697,9 @@
},
"PollingStationResults": {
"type": "object",
- "description": "PollingStationResults, following the fields in\n\"Model N 10-1. Proces-verbaal van een stembureau\"\n",
+ "description": "PollingStationResults, following the fields in\n\"Model Na 31-2. Proces-verbaal van een gemeentelijk stembureau/stembureau voor het openbaar lichaam\nin een gemeente/openbaar lichaam waar een centrale stemopneming wordt verricht\"\n\"Bijlage 2: uitkomsten per stembureau\"\n utoipa::openapi::OpenApi {
polling_station::PollingStationType,
polling_station::PollingStation,
polling_station::VotersCounts,
+ polling_station::VotersRecounts,
polling_station::VotesCounts,
),
),
diff --git a/backend/src/polling_station/mod.rs b/backend/src/polling_station/mod.rs
index 50a83514d..30656b9b5 100644
--- a/backend/src/polling_station/mod.rs
+++ b/backend/src/polling_station/mod.rs
@@ -177,6 +177,7 @@ mod tests {
async fn test_polling_station_data_entry_valid(pool: SqlitePool) {
let mut request_body = DataEntryRequest {
data: PollingStationResults {
+ recounted: false,
voters_counts: VotersCounts {
poll_card_count: 100, // incorrect
proxy_certificate_count: 1,
@@ -189,6 +190,7 @@ mod tests {
invalid_votes_count: 2,
total_votes_cast_count: 100,
},
+ voters_recounts: None,
differences_counts: DifferencesCounts {
more_ballots_count: 0,
fewer_ballots_count: 0,
diff --git a/backend/src/polling_station/structs.rs b/backend/src/polling_station/structs.rs
index 6e6c737fa..00b717375 100644
--- a/backend/src/polling_station/structs.rs
+++ b/backend/src/polling_station/structs.rs
@@ -69,17 +69,23 @@ impl From for PollingStationType {
}
/// PollingStationResults, following the fields in
-/// "Model N 10-1. Proces-verbaal van een stembureau"
-///
+/// "Model Na 31-2. Proces-verbaal van een gemeentelijk stembureau/stembureau voor het openbaar lichaam
+/// in een gemeente/openbaar lichaam waar een centrale stemopneming wordt verricht"
+/// "Bijlage 2: uitkomsten per stembureau"
+/// ,
+ /// Differences counts ("3. Verschil tussen het aantal toegelaten kiezers en het aantal getelde stembiljetten")
pub differences_counts: DifferencesCounts,
- /// Vote counts for each candidate in each political group ("Aantal stemmen per lijst en kandidaat")
+ /// Vote counts per list and candidate (5. "Aantal stemmen per lijst en kandidaat")
pub political_group_votes: Vec,
}
@@ -90,33 +96,154 @@ impl Validate for PollingStationResults {
validation_results: &mut ValidationResults,
field_name: String,
) -> Result<(), DataError> {
- self.voters_counts.validate(
- election,
- validation_results,
- format!("{field_name}.voters_counts"),
- )?;
self.votes_counts.validate(
election,
validation_results,
format!("{field_name}.votes_counts"),
)?;
- self.political_group_votes.validate(
- election,
- validation_results,
- format!("{field_name}.political_group_votes"),
- )?;
- if identical_counts(&self.voters_counts, &self.votes_counts) {
+ let total_votes_counts = self.votes_counts.total_votes_cast_count;
+ let total_voters_counts: Count;
+
+ // if recounted = true
+ if let Some(voters_recounts) = &self.voters_recounts {
+ total_voters_counts = voters_recounts.total_admitted_voters_recount;
+ voters_recounts.validate(
+ election,
+ validation_results,
+ format!("{field_name}.voters_recounts"),
+ )?;
+
+ // W.210 validate that the numbers in voters_recounts and votes_counts are not the same
+ if identical_voters_recounts_and_votes_counts(voters_recounts, &self.votes_counts) {
+ validation_results.warnings.push(ValidationResult {
+ fields: vec![
+ format!("{field_name}.voters_recounts"),
+ format!("{field_name}.votes_counts"),
+ ],
+ code: ValidationResultCode::EqualInput,
+ });
+ }
+ // if recounted = false
+ } else {
+ total_voters_counts = self.voters_counts.total_admitted_voters_count;
+ self.voters_counts.validate(
+ election,
+ validation_results,
+ format!("{field_name}.voters_counts"),
+ )?;
+
+ // W.209 validate that the numbers in voters_counts and votes_counts are not the same
+ if identical_voters_counts_and_votes_counts(&self.voters_counts, &self.votes_counts) {
+ validation_results.warnings.push(ValidationResult {
+ fields: vec![
+ format!("{field_name}.voters_counts"),
+ format!("{field_name}.votes_counts"),
+ ],
+ code: ValidationResultCode::EqualInput,
+ });
+ }
+ }
+
+ // F.301 (recounted = false) or F.302 (recounted = true)
+ // validate that the difference for more ballots counted is correct
+ if total_voters_counts < total_votes_counts
+ && (total_votes_counts - total_voters_counts
+ != self.differences_counts.more_ballots_count)
+ {
+ validation_results.errors.push(ValidationResult {
+ fields: vec![format!(
+ "{field_name}.differences_counts.more_ballots_count"
+ )],
+ code: ValidationResultCode::IncorrectDifference,
+ });
+ }
+
+ // F.303 (recounted = false) or F.304 (recounted = true)
+ // validate that the difference for fewer ballots counted is correct
+ if total_voters_counts > total_votes_counts
+ && (total_voters_counts - total_votes_counts
+ != self.differences_counts.fewer_ballots_count)
+ {
+ validation_results.errors.push(ValidationResult {
+ fields: vec![format!(
+ "{field_name}.differences_counts.fewer_ballots_count"
+ )],
+ code: ValidationResultCode::IncorrectDifference,
+ });
+ }
+
+ // W.301 validate that only more or fewer ballots counted is filled in when there is a difference in the totals
+ if total_voters_counts != total_votes_counts
+ && (self.differences_counts.more_ballots_count != 0
+ && self.differences_counts.fewer_ballots_count != 0)
+ {
+ validation_results.warnings.push(ValidationResult {
+ fields: vec![
+ format!("{field_name}.differences_counts.more_ballots_count"),
+ format!("{field_name}.differences_counts.fewer_ballots_count"),
+ ],
+ code: ValidationResultCode::ConflictingDifferences,
+ });
+ }
+
+ // W.304 (recounted = false) or W.305 (recounted = true)
+ // validate that no difference should be filled in when there is no difference in the totals
+ if total_voters_counts == total_votes_counts
+ && (self.differences_counts.more_ballots_count != 0
+ || self.differences_counts.fewer_ballots_count != 0)
+ {
+ if self.differences_counts.more_ballots_count != 0 {
+ validation_results.warnings.push(ValidationResult {
+ fields: vec![format!(
+ "{field_name}.differences_counts.more_ballots_count"
+ )],
+ code: ValidationResultCode::NoDifferenceExpected,
+ });
+ }
+ if self.differences_counts.fewer_ballots_count != 0 {
+ validation_results.warnings.push(ValidationResult {
+ fields: vec![format!(
+ "{field_name}.differences_counts.fewer_ballots_count"
+ )],
+ code: ValidationResultCode::NoDifferenceExpected,
+ });
+ }
+ }
+
+ // W.306 validate that no difference specifics should be filled in when there is no difference in the totals
+ if total_voters_counts == total_votes_counts
+ && (self.differences_counts.unreturned_ballots_count != 0
+ || self.differences_counts.too_few_ballots_handed_out_count != 0
+ || self.differences_counts.too_many_ballots_handed_out_count != 0
+ || self.differences_counts.other_explanation_count != 0
+ || self.differences_counts.no_explanation_count != 0)
+ {
validation_results.warnings.push(ValidationResult {
fields: vec![
- format!("{field_name}.voters_counts"),
- format!("{field_name}.votes_counts"),
+ format!("{field_name}.differences_counts.unreturned_ballots_count"),
+ format!("{field_name}.differences_counts.too_few_ballots_handed_out_count"),
+ format!("{field_name}.differences_counts.too_many_ballots_handed_out_count"),
+ format!("{field_name}.differences_counts.other_explanation_count"),
+ format!("{field_name}.differences_counts.no_explanation_count"),
],
- code: ValidationResultCode::EqualInput,
+ code: ValidationResultCode::NoDifferenceExpected,
});
}
- // validate that the total number of valid votes is equal to the sum of all political group totals
+ self.differences_counts.validate(
+ election,
+ validation_results,
+ format!("{field_name}.differences_counts"),
+ )?;
+
+ self.political_group_votes.validate(
+ election,
+ validation_results,
+ format!("{field_name}.political_group_votes"),
+ )?;
+
+ // F.204 validate that the total number of valid votes is equal to the sum of all political group totals
if self.votes_counts.votes_candidates_counts as u64
!= self
.political_group_votes
@@ -171,7 +298,7 @@ pub struct VotersCounts {
/// Check if all voters counts and votes counts are equal to zero.
/// Used in validations where this is an edge case that needs to be handled.
-fn all_zero(voters: &VotersCounts, votes: &VotesCounts) -> bool {
+fn all_zero_voters_counts_and_votes_counts(voters: &VotersCounts, votes: &VotesCounts) -> bool {
voters.poll_card_count == 0
&& voters.proxy_certificate_count == 0
&& voters.voter_card_count == 0
@@ -182,13 +309,12 @@ fn all_zero(voters: &VotersCounts, votes: &VotesCounts) -> bool {
&& votes.total_votes_cast_count == 0
}
-/// Check if the voters counts and votes counts are identical, which should
-/// result in a warning.
+/// Check if the voters counts and votes counts are identical, which should result in a warning.
///
/// This is not implemented as Eq because there is no true equality relation
/// between these two sets of numbers.
-fn identical_counts(voters: &VotersCounts, votes: &VotesCounts) -> bool {
- !all_zero(voters, votes)
+fn identical_voters_counts_and_votes_counts(voters: &VotersCounts, votes: &VotesCounts) -> bool {
+ !all_zero_voters_counts_and_votes_counts(voters, votes)
&& voters.poll_card_count == votes.votes_candidates_counts
&& voters.proxy_certificate_count == votes.blank_votes_count
&& voters.voter_card_count == votes.invalid_votes_count
@@ -224,7 +350,7 @@ impl Validate for VotersCounts {
format!("{field_name}.total_admitted_voters_count"),
)?;
- // validate that total_admitted_voters_count == poll_card_count + proxy_certificate_count + voter_card_count
+ // F.201 validate that total_admitted_voters_count == poll_card_count + proxy_certificate_count + voter_card_count
if self.poll_card_count + self.proxy_certificate_count + self.voter_card_count
!= self.total_admitted_voters_count
{
@@ -289,7 +415,7 @@ impl Validate for VotesCounts {
format!("{field_name}.total_votes_cast_count"),
)?;
- // validate that total_votes_cast_count == votes_candidates_counts + blank_votes_count + invalid_votes_count
+ // F.202 validate that total_votes_cast_count == votes_candidates_counts + blank_votes_count + invalid_votes_count
if self.votes_candidates_counts + self.blank_votes_count + self.invalid_votes_count
!= self.total_votes_cast_count
{
@@ -309,7 +435,7 @@ impl Validate for VotesCounts {
return Ok(());
}
- // validate that number of blank votes is no more than 3%
+ // W.201 validate that number of blank votes is no more than 3%
if above_percentage_threshold(self.blank_votes_count, self.total_votes_cast_count, 3) {
validation_results.warnings.push(ValidationResult {
fields: vec![
@@ -320,7 +446,7 @@ impl Validate for VotesCounts {
});
}
- // validate that number of invalid votes is no more than 3%
+ // W.202 validate that number of invalid votes is no more than 3%
if above_percentage_threshold(self.invalid_votes_count, self.total_votes_cast_count, 3) {
validation_results.warnings.push(ValidationResult {
fields: vec![
@@ -334,33 +460,213 @@ impl Validate for VotesCounts {
}
}
+/// Voters recounts, part of the polling station results.
+#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq, Eq, Hash)]
+pub struct VotersRecounts {
+ /// Number of valid poll cards ("Aantal geldige stempassen")
+ #[schema(value_type = u32)]
+ pub poll_card_recount: Count,
+ /// Number of valid proxy certificates ("Aantal geldige volmachtbewijzen")
+ #[schema(value_type = u32)]
+ pub proxy_certificate_recount: Count,
+ /// Number of valid voter cards ("Aantal geldige kiezerspassen")
+ #[schema(value_type = u32)]
+ pub voter_card_recount: Count,
+ /// Total number of admitted voters ("Totaal aantal toegelaten kiezers")
+ #[schema(value_type = u32)]
+ pub total_admitted_voters_recount: Count,
+}
+
+/// Check if all voters recounts and votes counts are equal to zero.
+/// Used in validations where this is an edge case that needs to be handled.
+fn all_zero_voters_recounts_and_votes_counts(voters: &VotersRecounts, votes: &VotesCounts) -> bool {
+ voters.poll_card_recount == 0
+ && voters.proxy_certificate_recount == 0
+ && voters.voter_card_recount == 0
+ && voters.total_admitted_voters_recount == 0
+ && votes.votes_candidates_counts == 0
+ && votes.blank_votes_count == 0
+ && votes.invalid_votes_count == 0
+ && votes.total_votes_cast_count == 0
+}
+
+/// Check if the voters recounts and votes counts are identical, which should result in a warning.
+///
+/// This is not implemented as Eq because there is no true equality relation
+/// between these two sets of numbers.
+fn identical_voters_recounts_and_votes_counts(
+ voters: &VotersRecounts,
+ votes: &VotesCounts,
+) -> bool {
+ !all_zero_voters_recounts_and_votes_counts(voters, votes)
+ && voters.poll_card_recount == votes.votes_candidates_counts
+ && voters.proxy_certificate_recount == votes.blank_votes_count
+ && voters.voter_card_recount == votes.invalid_votes_count
+ && voters.total_admitted_voters_recount == votes.total_votes_cast_count
+}
+
+impl Validate for VotersRecounts {
+ fn validate(
+ &self,
+ election: &Election,
+ validation_results: &mut ValidationResults,
+ field_name: String,
+ ) -> Result<(), DataError> {
+ // validate all counts
+ self.poll_card_recount.validate(
+ election,
+ validation_results,
+ format!("{field_name}.poll_card_recount"),
+ )?;
+ self.proxy_certificate_recount.validate(
+ election,
+ validation_results,
+ format!("{field_name}.proxy_certificate_recount"),
+ )?;
+ self.voter_card_recount.validate(
+ election,
+ validation_results,
+ format!("{field_name}.voter_card_recount"),
+ )?;
+ self.total_admitted_voters_recount.validate(
+ election,
+ validation_results,
+ format!("{field_name}.total_admitted_voters_recount"),
+ )?;
+
+ // F.203 validate that total_admitted_voters_recount == poll_card_recount + proxy_certificate_recount + voter_card_recount
+ if self.poll_card_recount + self.proxy_certificate_recount + self.voter_card_recount
+ != self.total_admitted_voters_recount
+ {
+ validation_results.errors.push(ValidationResult {
+ fields: vec![
+ format!("{field_name}.total_admitted_voters_recount"),
+ format!("{field_name}.poll_card_recount"),
+ format!("{field_name}.proxy_certificate_recount"),
+ format!("{field_name}.voter_card_recount"),
+ ],
+ code: ValidationResultCode::IncorrectTotal,
+ });
+ }
+ Ok(())
+ }
+}
+
/// Differences counts, part of the polling station results.
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq, Eq, Hash)]
pub struct DifferencesCounts {
- /// Number of more counted ballots ("Er zijn méér stembiljetten geteld. Noteer hoeveel stembiljetten er meer zijn geteld")
+ /// Number of more counted ballots ("Er zijn méér stembiljetten geteld. Hoeveel stembiljetten zijn er meer geteld?")
#[schema(value_type = u32)]
pub more_ballots_count: Count,
- /// Number of fewer counted ballots ("Er zijn minder stembiljetten geteld. Noteer hoeveel stembiljetten er minder zijn geteld")
+ /// Number of fewer counted ballots ("Er zijn minder stembiljetten geteld. Hoeveel stembiljetten zijn er minder geteld")
#[schema(value_type = u32)]
pub fewer_ballots_count: Count,
- /// Number of unreturned ballots ("Aantal keren dat een kiezer het stembiljet niet heeft ingeleverd")
+ /// Number of unreturned ballots ("Hoe vaak heeft een kiezer het stembiljet niet ingeleverd?")
#[schema(value_type = u32)]
pub unreturned_ballots_count: Count,
- /// Number of fewer ballots handed out ("Aantal keren dat er een stembiljet te weinig is uitgereikt")
+ /// Number of fewer ballots handed out ("Hoe vaak is er een stembiljet te weinig uitgereikt?")
#[schema(value_type = u32)]
pub too_few_ballots_handed_out_count: Count,
- /// Number of more ballots handed out ("Aantal keren dat er een stembiljet teveel is uitgereikt")
+ /// Number of more ballots handed out ("Hoe vaak is er een stembiljet te veel uitgereikt?")
#[schema(value_type = u32)]
pub too_many_ballots_handed_out_count: Count,
- /// Number of other explanations ("Aantal keren dat er een andere verklaring is voor het verschil")
+ /// Number of other explanations ("Hoe vaak is er een andere verklaring voor het verschil?")
#[schema(value_type = u32)]
pub other_explanation_count: Count,
- /// Number of no explanations ("Aantal keren dat er geen verklaring is voor het verschil")
+ /// Number of no explanations ("Hoe vaak is er geen verklaring voor het verschil?")
#[schema(value_type = u32)]
pub no_explanation_count: Count,
}
-// TODO: impl Validate for DifferencesCounts
+impl Validate for DifferencesCounts {
+ fn validate(
+ &self,
+ election: &Election,
+ validation_results: &mut ValidationResults,
+ field_name: String,
+ ) -> Result<(), DataError> {
+ // validate all counts
+ self.more_ballots_count.validate(
+ election,
+ validation_results,
+ format!("{field_name}.more_ballots_count"),
+ )?;
+ self.fewer_ballots_count.validate(
+ election,
+ validation_results,
+ format!("{field_name}.fewer_ballots_count"),
+ )?;
+ self.unreturned_ballots_count.validate(
+ election,
+ validation_results,
+ format!("{field_name}.unreturned_ballots_count"),
+ )?;
+ self.too_few_ballots_handed_out_count.validate(
+ election,
+ validation_results,
+ format!("{field_name}.too_few_ballots_handed_out_count"),
+ )?;
+ self.too_many_ballots_handed_out_count.validate(
+ election,
+ validation_results,
+ format!("{field_name}.too_many_ballots_handed_out_count"),
+ )?;
+ self.other_explanation_count.validate(
+ election,
+ validation_results,
+ format!("{field_name}.other_explanation_count"),
+ )?;
+ self.no_explanation_count.validate(
+ election,
+ validation_results,
+ format!("{field_name}.no_explanation_count"),
+ )?;
+
+ // W.302 validate that more_ballots_count == too_many_ballots_handed_out_count + other_explanation_count + no_explanation_count - unreturned_ballots_count - too_few_ballots_handed_out_count
+ if self.more_ballots_count != 0
+ && (i64::from(self.too_many_ballots_handed_out_count)
+ + i64::from(self.other_explanation_count)
+ + i64::from(self.no_explanation_count)
+ - i64::from(self.unreturned_ballots_count)
+ - i64::from(self.too_few_ballots_handed_out_count)
+ != i64::from(self.more_ballots_count))
+ {
+ validation_results.warnings.push(ValidationResult {
+ fields: vec![
+ format!("{field_name}.more_ballots_count"),
+ format!("{field_name}.too_many_ballots_handed_out_count"),
+ format!("{field_name}.other_explanation_count"),
+ format!("{field_name}.no_explanation_count"),
+ format!("{field_name}.unreturned_ballots_count"),
+ format!("{field_name}.too_few_ballots_handed_out_count"),
+ ],
+ code: ValidationResultCode::IncorrectTotal,
+ });
+ }
+ // W.303 validate that fewer_ballots_count == unreturned_ballots_count + too_few_ballots_handed_out_count + other_explanation_count + no_explanation_count
+ if self.fewer_ballots_count != 0
+ && (i64::from(self.unreturned_ballots_count)
+ + i64::from(self.too_few_ballots_handed_out_count)
+ + i64::from(self.other_explanation_count)
+ + i64::from(self.no_explanation_count)
+ - i64::from(self.too_many_ballots_handed_out_count)
+ != i64::from(self.fewer_ballots_count))
+ {
+ validation_results.warnings.push(ValidationResult {
+ fields: vec![
+ format!("{field_name}.fewer_ballots_count"),
+ format!("{field_name}.unreturned_ballots_count"),
+ format!("{field_name}.too_few_ballots_handed_out_count"),
+ format!("{field_name}.too_many_ballots_handed_out_count"),
+ format!("{field_name}.other_explanation_count"),
+ format!("{field_name}.no_explanation_count"),
+ ],
+ code: ValidationResultCode::IncorrectTotal,
+ });
+ }
+ Ok(())
+ }
+}
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq, Eq, Hash)]
pub struct PoliticalGroupVotes {
@@ -436,7 +742,7 @@ impl Validate for PoliticalGroupVotes {
self.total
.validate(election, validation_results, format!("{field_name}.total"))?;
- // validate whether if the total number of votes matches the sum of all candidate votes,
+ // F.401 validate whether the total number of votes matches the sum of all candidate votes,
// cast to u64 to avoid overflow
if self.total as u64
!= self
@@ -480,23 +786,26 @@ mod tests {
use super::*;
#[test]
- fn test_polling_station_results_validation() {
+ fn test_polling_station_results_incorrect_total_and_difference_validation() {
+ // test F.201 incorrect total, F.202 incorrect total and F.301 incorrect difference
let mut validation_results = ValidationResults::default();
let polling_station_results = PollingStationResults {
+ recounted: false,
voters_counts: VotersCounts {
poll_card_count: 1,
proxy_certificate_count: 2,
voter_card_count: 3,
- total_admitted_voters_count: 5, // incorrect total
+ total_admitted_voters_count: 5, // F.201 incorrect total
},
votes_counts: VotesCounts {
- votes_candidates_counts: 5,
- blank_votes_count: 6,
- invalid_votes_count: 7,
- total_votes_cast_count: 20, // incorrect total
+ votes_candidates_counts: 2,
+ blank_votes_count: 1,
+ invalid_votes_count: 4,
+ total_votes_cast_count: 8, // F.202 incorrect total
},
+ voters_recounts: None,
differences_counts: DifferencesCounts {
- more_ballots_count: 0,
+ more_ballots_count: 0, // F.301 incorrect difference
fewer_ballots_count: 0,
unreturned_ballots_count: 0,
too_few_ballots_handed_out_count: 0,
@@ -506,10 +815,95 @@ mod tests {
},
political_group_votes: vec![PoliticalGroupVotes {
number: 1,
- total: 5,
+ total: 2,
+ candidate_votes: vec![CandidateVotes {
+ number: 1,
+ votes: 2,
+ }],
+ }],
+ };
+ let election = election_fixture(&[1]);
+ polling_station_results
+ .validate(
+ &election,
+ &mut validation_results,
+ "polling_station_results".to_string(),
+ )
+ .unwrap();
+ assert_eq!(validation_results.errors.len(), 3);
+ assert_eq!(validation_results.warnings.len(), 0);
+ assert_eq!(
+ validation_results.errors[0].code,
+ ValidationResultCode::IncorrectTotal
+ );
+ assert_eq!(
+ validation_results.errors[0].fields,
+ vec![
+ "polling_station_results.votes_counts.total_votes_cast_count",
+ "polling_station_results.votes_counts.votes_candidates_counts",
+ "polling_station_results.votes_counts.blank_votes_count",
+ "polling_station_results.votes_counts.invalid_votes_count"
+ ]
+ );
+ assert_eq!(
+ validation_results.errors[1].code,
+ ValidationResultCode::IncorrectTotal
+ );
+ assert_eq!(
+ validation_results.errors[1].fields,
+ vec![
+ "polling_station_results.voters_counts.total_admitted_voters_count",
+ "polling_station_results.voters_counts.poll_card_count",
+ "polling_station_results.voters_counts.proxy_certificate_count",
+ "polling_station_results.voters_counts.voter_card_count"
+ ]
+ );
+ assert_eq!(
+ validation_results.errors[2].code,
+ ValidationResultCode::IncorrectDifference
+ );
+ assert_eq!(
+ validation_results.errors[2].fields,
+ vec!["polling_station_results.differences_counts.more_ballots_count"]
+ );
+
+ // test F.201 incorrect total, F.202 incorrect total, F.203 incorrect total and F.302 incorrect difference
+ validation_results = ValidationResults::default();
+ let polling_station_results = PollingStationResults {
+ recounted: true,
+ voters_counts: VotersCounts {
+ poll_card_count: 1,
+ proxy_certificate_count: 2,
+ voter_card_count: 3,
+ total_admitted_voters_count: 5, // F.201 incorrect total
+ },
+ votes_counts: VotesCounts {
+ votes_candidates_counts: 3,
+ blank_votes_count: 2,
+ invalid_votes_count: 1,
+ total_votes_cast_count: 5, // F.202 incorrect total
+ },
+ voters_recounts: Some(VotersRecounts {
+ poll_card_recount: 2,
+ proxy_certificate_recount: 1,
+ voter_card_recount: 4,
+ total_admitted_voters_recount: 8, // F.203 incorrect total
+ }),
+ differences_counts: DifferencesCounts {
+ more_ballots_count: 0,
+ fewer_ballots_count: 0, // F.302 incorrect difference
+ unreturned_ballots_count: 0,
+ too_few_ballots_handed_out_count: 0,
+ too_many_ballots_handed_out_count: 0,
+ other_explanation_count: 0,
+ no_explanation_count: 0,
+ },
+ political_group_votes: vec![PoliticalGroupVotes {
+ number: 1,
+ total: 3,
candidate_votes: vec![CandidateVotes {
number: 1,
- votes: 5,
+ votes: 3,
}],
}],
};
@@ -521,27 +915,277 @@ mod tests {
"polling_station_results".to_string(),
)
.unwrap();
- assert_eq!(validation_results.errors.len(), 2);
+ assert_eq!(validation_results.errors.len(), 3);
assert_eq!(validation_results.warnings.len(), 0);
+ assert_eq!(
+ validation_results.errors[0].code,
+ ValidationResultCode::IncorrectTotal
+ );
+ assert_eq!(
+ validation_results.errors[0].fields,
+ vec![
+ "polling_station_results.votes_counts.total_votes_cast_count",
+ "polling_station_results.votes_counts.votes_candidates_counts",
+ "polling_station_results.votes_counts.blank_votes_count",
+ "polling_station_results.votes_counts.invalid_votes_count"
+ ]
+ );
+ assert_eq!(
+ validation_results.errors[1].code,
+ ValidationResultCode::IncorrectTotal
+ );
+ assert_eq!(
+ validation_results.errors[1].fields,
+ vec![
+ "polling_station_results.voters_recounts.total_admitted_voters_recount",
+ "polling_station_results.voters_recounts.poll_card_recount",
+ "polling_station_results.voters_recounts.proxy_certificate_recount",
+ "polling_station_results.voters_recounts.voter_card_recount"
+ ]
+ );
+ assert_eq!(
+ validation_results.errors[2].code,
+ ValidationResultCode::IncorrectDifference
+ );
+ assert_eq!(
+ validation_results.errors[2].fields,
+ vec!["polling_station_results.differences_counts.fewer_ballots_count"]
+ );
}
#[test]
- fn test_polling_station_identical_counts_validation() {
+ fn test_polling_station_results_wrong_and_no_difference_validation() {
+ // test W.301 conflicting differences
let mut validation_results = ValidationResults::default();
let polling_station_results = PollingStationResults {
+ recounted: false,
+ voters_counts: VotersCounts {
+ poll_card_count: 50,
+ proxy_certificate_count: 2,
+ voter_card_count: 4,
+ total_admitted_voters_count: 56,
+ },
+ votes_counts: VotesCounts {
+ votes_candidates_counts: 50,
+ blank_votes_count: 1,
+ invalid_votes_count: 1,
+ total_votes_cast_count: 52,
+ },
+ voters_recounts: None,
+ differences_counts: DifferencesCounts {
+ more_ballots_count: 4, // W.301 conflicting differences
+ fewer_ballots_count: 4, // W.301 conflicting differences
+ unreturned_ballots_count: 0,
+ too_few_ballots_handed_out_count: 0,
+ too_many_ballots_handed_out_count: 0,
+ other_explanation_count: 2,
+ no_explanation_count: 2,
+ },
+ political_group_votes: vec![PoliticalGroupVotes {
+ number: 1,
+ total: 50,
+ candidate_votes: vec![CandidateVotes {
+ number: 1,
+ votes: 50,
+ }],
+ }],
+ };
+ let election = election_fixture(&[1]);
+ polling_station_results
+ .validate(
+ &election,
+ &mut validation_results,
+ "polling_station_results".to_string(),
+ )
+ .unwrap();
+ assert_eq!(validation_results.errors.len(), 0);
+ assert_eq!(validation_results.warnings.len(), 1);
+ assert_eq!(
+ validation_results.warnings[0].code,
+ ValidationResultCode::ConflictingDifferences
+ );
+ assert_eq!(
+ validation_results.warnings[0].fields,
+ vec![
+ "polling_station_results.differences_counts.more_ballots_count",
+ "polling_station_results.differences_counts.fewer_ballots_count"
+ ]
+ );
+
+ // test W.304 and W.306 no difference expected
+ validation_results = ValidationResults::default();
+ let polling_station_results = PollingStationResults {
+ recounted: false,
+ voters_counts: VotersCounts {
+ poll_card_count: 46,
+ proxy_certificate_count: 2,
+ voter_card_count: 4,
+ total_admitted_voters_count: 52,
+ },
+ votes_counts: VotesCounts {
+ votes_candidates_counts: 50,
+ blank_votes_count: 1,
+ invalid_votes_count: 1,
+ total_votes_cast_count: 52,
+ },
+ voters_recounts: None,
+ differences_counts: DifferencesCounts {
+ more_ballots_count: 4, // W.304 no difference expected
+ fewer_ballots_count: 0,
+ unreturned_ballots_count: 0,
+ too_few_ballots_handed_out_count: 0,
+ too_many_ballots_handed_out_count: 0,
+ other_explanation_count: 2, // W.306 no difference expected
+ no_explanation_count: 2, // W.306 no difference expected
+ },
+ political_group_votes: vec![PoliticalGroupVotes {
+ number: 1,
+ total: 50,
+ candidate_votes: vec![CandidateVotes {
+ number: 1,
+ votes: 50,
+ }],
+ }],
+ };
+ let election = election_fixture(&[1]);
+ polling_station_results
+ .validate(
+ &election,
+ &mut validation_results,
+ "polling_station_results".to_string(),
+ )
+ .unwrap();
+ assert_eq!(validation_results.errors.len(), 0);
+ assert_eq!(validation_results.warnings.len(), 2);
+ assert_eq!(
+ validation_results.warnings[0].code,
+ ValidationResultCode::NoDifferenceExpected
+ );
+ assert_eq!(
+ validation_results.warnings[0].fields,
+ vec!["polling_station_results.differences_counts.more_ballots_count",]
+ );
+ assert_eq!(
+ validation_results.warnings[1].code,
+ ValidationResultCode::NoDifferenceExpected
+ );
+ assert_eq!(
+ validation_results.warnings[1].fields,
+ vec![
+ "polling_station_results.differences_counts.unreturned_ballots_count",
+ "polling_station_results.differences_counts.too_few_ballots_handed_out_count",
+ "polling_station_results.differences_counts.too_many_ballots_handed_out_count",
+ "polling_station_results.differences_counts.other_explanation_count",
+ "polling_station_results.differences_counts.no_explanation_count",
+ ]
+ );
+
+ // test W.305 and W.306 no difference expected and F.204 incorrect total
+ validation_results = ValidationResults::default();
+ let polling_station_results = PollingStationResults {
+ recounted: true,
+ voters_counts: VotersCounts {
+ poll_card_count: 50,
+ proxy_certificate_count: 2,
+ voter_card_count: 4,
+ total_admitted_voters_count: 56,
+ },
+ votes_counts: VotesCounts {
+ votes_candidates_counts: 50,
+ blank_votes_count: 1,
+ invalid_votes_count: 1,
+ total_votes_cast_count: 52,
+ },
+ voters_recounts: Some(VotersRecounts {
+ poll_card_recount: 46,
+ proxy_certificate_recount: 2,
+ voter_card_recount: 4,
+ total_admitted_voters_recount: 52,
+ }),
+ differences_counts: DifferencesCounts {
+ more_ballots_count: 0,
+ fewer_ballots_count: 4, // W.305 no difference expected
+ unreturned_ballots_count: 1, // W.306 no difference expected
+ too_few_ballots_handed_out_count: 1, // W.306 no difference expected
+ too_many_ballots_handed_out_count: 0,
+ other_explanation_count: 1, // W.306 no difference expected
+ no_explanation_count: 1, // W.306 no difference expected
+ },
+ political_group_votes: vec![PoliticalGroupVotes {
+ number: 1,
+ total: 49, // F.204 incorrect total
+ candidate_votes: vec![CandidateVotes {
+ number: 1,
+ votes: 49,
+ }],
+ }],
+ };
+ let election = election_fixture(&[1]);
+ polling_station_results
+ .validate(
+ &election,
+ &mut validation_results,
+ "polling_station_results".to_string(),
+ )
+ .unwrap();
+ assert_eq!(validation_results.errors.len(), 1);
+ assert_eq!(validation_results.warnings.len(), 2);
+ assert_eq!(
+ validation_results.errors[0].code,
+ ValidationResultCode::IncorrectTotal
+ );
+ assert_eq!(
+ validation_results.errors[0].fields,
+ vec![
+ "polling_station_results.votes_counts.votes_candidates_counts",
+ "polling_station_results.political_group_votes"
+ ]
+ );
+ assert_eq!(
+ validation_results.warnings[0].code,
+ ValidationResultCode::NoDifferenceExpected
+ );
+ assert_eq!(
+ validation_results.warnings[0].fields,
+ vec!["polling_station_results.differences_counts.fewer_ballots_count"]
+ );
+ assert_eq!(
+ validation_results.warnings[1].code,
+ ValidationResultCode::NoDifferenceExpected
+ );
+ assert_eq!(
+ validation_results.warnings[1].fields,
+ vec![
+ "polling_station_results.differences_counts.unreturned_ballots_count",
+ "polling_station_results.differences_counts.too_few_ballots_handed_out_count",
+ "polling_station_results.differences_counts.too_many_ballots_handed_out_count",
+ "polling_station_results.differences_counts.other_explanation_count",
+ "polling_station_results.differences_counts.no_explanation_count"
+ ]
+ );
+ }
+
+ #[test]
+ fn test_polling_station_identical_counts_validation() {
+ let mut validation_results = ValidationResults::default();
+ // test W.209 equal input
+ let mut polling_station_results = PollingStationResults {
+ recounted: false,
voters_counts: VotersCounts {
+ // W.209 equal input
poll_card_count: 1000,
proxy_certificate_count: 1,
voter_card_count: 1,
total_admitted_voters_count: 1002,
},
- // same as above
votes_counts: VotesCounts {
+ // W.209 equal input
votes_candidates_counts: 1000,
blank_votes_count: 1,
invalid_votes_count: 1,
total_votes_cast_count: 1002,
},
+ voters_recounts: None,
differences_counts: DifferencesCounts {
more_ballots_count: 0,
fewer_ballots_count: 0,
@@ -574,12 +1218,58 @@ mod tests {
validation_results.warnings[0].code,
ValidationResultCode::EqualInput
);
+ assert_eq!(
+ validation_results.warnings[0].fields,
+ vec![
+ "polling_station_results.voters_counts",
+ "polling_station_results.votes_counts"
+ ]
+ );
+
+ // test W.210 equal input
+ validation_results = ValidationResults::default();
+ polling_station_results.recounted = true;
+ // voters_counts is not equal to votes_counts
+ polling_station_results.voters_counts = VotersCounts {
+ poll_card_count: 998,
+ proxy_certificate_count: 1,
+ voter_card_count: 1,
+ total_admitted_voters_count: 1000,
+ };
+ // voters_recounts is now equal to votes_counts: W.210 equal input
+ polling_station_results.voters_recounts = Some(VotersRecounts {
+ poll_card_recount: 1000,
+ proxy_certificate_recount: 1,
+ voter_card_recount: 1,
+ total_admitted_voters_recount: 1002,
+ });
+ polling_station_results
+ .validate(
+ &election,
+ &mut validation_results,
+ "polling_station_results".to_string(),
+ )
+ .unwrap();
+ assert_eq!(validation_results.errors.len(), 0);
+ assert_eq!(validation_results.warnings.len(), 1);
+ assert_eq!(
+ validation_results.warnings[0].code,
+ ValidationResultCode::EqualInput
+ );
+ assert_eq!(
+ validation_results.warnings[0].fields,
+ vec![
+ "polling_station_results.voters_recounts",
+ "polling_station_results.votes_counts"
+ ]
+ );
}
#[test]
fn test_voters_counts_validation() {
let mut validation_results = ValidationResults::default();
- let voters_counts = VotersCounts {
+ // test out of range
+ let mut voters_counts = VotersCounts {
poll_card_count: 1_000_000_001, // out of range
proxy_certificate_count: 2,
voter_card_count: 3,
@@ -592,17 +1282,65 @@ mod tests {
"voters_counts".to_string(),
);
assert!(res.is_err());
+
+ // test F.201 incorrect total
+ validation_results = ValidationResults::default();
+ voters_counts = VotersCounts {
+ poll_card_count: 5,
+ proxy_certificate_count: 6,
+ voter_card_count: 7,
+ total_admitted_voters_count: 20, // F.201 incorrect total
+ };
+
+ voters_counts
+ .validate(
+ &election,
+ &mut validation_results,
+ "voters_counts".to_string(),
+ )
+ .unwrap();
+ assert_eq!(validation_results.errors.len(), 1);
+ assert_eq!(validation_results.warnings.len(), 0);
+ assert_eq!(
+ validation_results.errors[0].code,
+ ValidationResultCode::IncorrectTotal
+ );
+ assert_eq!(
+ validation_results.errors[0].fields,
+ vec![
+ "voters_counts.total_admitted_voters_count",
+ "voters_counts.poll_card_count",
+ "voters_counts.proxy_certificate_count",
+ "voters_counts.voter_card_count"
+ ]
+ );
}
#[test]
fn test_votes_counts_validation() {
- // test incorrect total
let mut validation_results = ValidationResults::default();
- let votes_counts = VotesCounts {
+ // test out of range
+ let mut votes_counts = VotesCounts {
+ votes_candidates_counts: 1_000_000_001, // out of range
+ blank_votes_count: 2,
+ invalid_votes_count: 3,
+ total_votes_cast_count: 1_000_000_006, // correct but out of range
+ };
+ let election = election_fixture(&[]);
+ let res = votes_counts.validate(
+ &election,
+ &mut validation_results,
+ "votes_counts".to_string(),
+ );
+ assert!(res.is_err());
+
+ // test F.202 incorrect total
+ validation_results = ValidationResults::default();
+ votes_counts = VotesCounts {
votes_candidates_counts: 5,
blank_votes_count: 6,
invalid_votes_count: 7,
- total_votes_cast_count: 20, // incorrect total
+ total_votes_cast_count: 20, // F.202 incorrect total
};
let election = election_fixture(&[]);
votes_counts
@@ -614,12 +1352,25 @@ mod tests {
.unwrap();
assert_eq!(validation_results.errors.len(), 1);
assert_eq!(validation_results.warnings.len(), 0);
+ assert_eq!(
+ validation_results.errors[0].code,
+ ValidationResultCode::IncorrectTotal
+ );
+ assert_eq!(
+ validation_results.errors[0].fields,
+ vec![
+ "votes_counts.total_votes_cast_count",
+ "votes_counts.votes_candidates_counts",
+ "votes_counts.blank_votes_count",
+ "votes_counts.invalid_votes_count"
+ ]
+ );
- // test high number of blank votes
- let mut validation_results = ValidationResults::default();
- let votes_counts = VotesCounts {
+ // test W.201 high number of blank votes
+ validation_results = ValidationResults::default();
+ votes_counts = VotesCounts {
votes_candidates_counts: 100,
- blank_votes_count: 10, // high number of blank votes
+ blank_votes_count: 10, // W.201 above threshold
invalid_votes_count: 1,
total_votes_cast_count: 111,
};
@@ -632,13 +1383,24 @@ mod tests {
.unwrap();
assert_eq!(validation_results.errors.len(), 0);
assert_eq!(validation_results.warnings.len(), 1);
+ assert_eq!(
+ validation_results.warnings[0].code,
+ ValidationResultCode::AboveThreshold
+ );
+ assert_eq!(
+ validation_results.warnings[0].fields,
+ vec![
+ "votes_counts.blank_votes_count",
+ "votes_counts.total_votes_cast_count"
+ ]
+ );
- // test high number of invalid votes
- let mut validation_results = ValidationResults::default();
- let votes_counts = VotesCounts {
+ // test W.202 high number of invalid votes
+ validation_results = ValidationResults::default();
+ votes_counts = VotesCounts {
votes_candidates_counts: 100,
blank_votes_count: 1,
- invalid_votes_count: 10, // high number of invalid votes
+ invalid_votes_count: 10, // W.202 above threshold
total_votes_cast_count: 111,
};
votes_counts
@@ -650,10 +1412,243 @@ mod tests {
.unwrap();
assert_eq!(validation_results.errors.len(), 0);
assert_eq!(validation_results.warnings.len(), 1);
+ assert_eq!(
+ validation_results.warnings[0].code,
+ ValidationResultCode::AboveThreshold
+ );
+ assert_eq!(
+ validation_results.warnings[0].fields,
+ vec![
+ "votes_counts.invalid_votes_count",
+ "votes_counts.total_votes_cast_count"
+ ]
+ );
+ }
+
+ #[test]
+ fn test_voters_recounts_validation() {
+ let mut validation_results = ValidationResults::default();
+ // test out of range
+ let mut voters_recounts = VotersRecounts {
+ poll_card_recount: 1_000_000_001, // out of range
+ proxy_certificate_recount: 2,
+ voter_card_recount: 3,
+ total_admitted_voters_recount: 1_000_000_006, // correct but out of range
+ };
+ let election = election_fixture(&[]);
+ let res = voters_recounts.validate(
+ &election,
+ &mut validation_results,
+ "voters_recounts".to_string(),
+ );
+ assert!(res.is_err());
+
+ // test F.203 incorrect total
+ validation_results = ValidationResults::default();
+ voters_recounts = VotersRecounts {
+ poll_card_recount: 5,
+ proxy_certificate_recount: 6,
+ voter_card_recount: 7,
+ total_admitted_voters_recount: 20, // F.203 incorrect total
+ };
+ let election = election_fixture(&[]);
+ voters_recounts
+ .validate(
+ &election,
+ &mut validation_results,
+ "voters_recounts".to_string(),
+ )
+ .unwrap();
+ assert_eq!(validation_results.errors.len(), 1);
+ assert_eq!(validation_results.warnings.len(), 0);
+ assert_eq!(
+ validation_results.errors[0].code,
+ ValidationResultCode::IncorrectTotal
+ );
+ assert_eq!(
+ validation_results.errors[0].fields,
+ vec![
+ "voters_recounts.total_admitted_voters_recount",
+ "voters_recounts.poll_card_recount",
+ "voters_recounts.proxy_certificate_recount",
+ "voters_recounts.voter_card_recount"
+ ]
+ );
+ }
+
+ #[test]
+ fn test_differences_counts_validation() {
+ let mut validation_results = ValidationResults::default();
+ // test out of range
+ let mut differences_counts = DifferencesCounts {
+ more_ballots_count: 1_000_000_002, // correct but out of range
+ fewer_ballots_count: 0,
+ unreturned_ballots_count: 0,
+ too_few_ballots_handed_out_count: 0,
+ too_many_ballots_handed_out_count: 1,
+ other_explanation_count: 1,
+ no_explanation_count: 1_000_000_000, // out of range
+ };
+ let election = election_fixture(&[]);
+ let res = differences_counts.validate(
+ &election,
+ &mut validation_results,
+ "differences_counts".to_string(),
+ );
+ assert!(res.is_err());
+
+ // test calculation for more_ballots_count does not add up and becomes minus
+ validation_results = ValidationResults::default();
+ differences_counts = DifferencesCounts {
+ more_ballots_count: 1,
+ fewer_ballots_count: 0,
+ unreturned_ballots_count: 2,
+ too_few_ballots_handed_out_count: 0,
+ too_many_ballots_handed_out_count: 1,
+ other_explanation_count: 0,
+ no_explanation_count: 0,
+ };
+ let election = election_fixture(&[]);
+ differences_counts
+ .validate(
+ &election,
+ &mut validation_results,
+ "differences_counts".to_string(),
+ )
+ .unwrap();
+ assert_eq!(validation_results.errors.len(), 0);
+ assert_eq!(validation_results.warnings.len(), 1);
+ assert_eq!(
+ validation_results.warnings[0].code,
+ ValidationResultCode::IncorrectTotal
+ );
+ assert_eq!(
+ validation_results.warnings[0].fields,
+ vec![
+ "differences_counts.more_ballots_count",
+ "differences_counts.too_many_ballots_handed_out_count",
+ "differences_counts.other_explanation_count",
+ "differences_counts.no_explanation_count",
+ "differences_counts.unreturned_ballots_count",
+ "differences_counts.too_few_ballots_handed_out_count"
+ ]
+ );
+
+ // test W.302 incorrect total
+ validation_results = ValidationResults::default();
+ differences_counts = DifferencesCounts {
+ more_ballots_count: 5, // F.302 incorrect total
+ fewer_ballots_count: 0,
+ unreturned_ballots_count: 0,
+ too_few_ballots_handed_out_count: 0,
+ too_many_ballots_handed_out_count: 1,
+ other_explanation_count: 1,
+ no_explanation_count: 1,
+ };
+ let election = election_fixture(&[]);
+ differences_counts
+ .validate(
+ &election,
+ &mut validation_results,
+ "differences_counts".to_string(),
+ )
+ .unwrap();
+ assert_eq!(validation_results.errors.len(), 0);
+ assert_eq!(validation_results.warnings.len(), 1);
+ assert_eq!(
+ validation_results.warnings[0].code,
+ ValidationResultCode::IncorrectTotal
+ );
+ assert_eq!(
+ validation_results.warnings[0].fields,
+ vec![
+ "differences_counts.more_ballots_count",
+ "differences_counts.too_many_ballots_handed_out_count",
+ "differences_counts.other_explanation_count",
+ "differences_counts.no_explanation_count",
+ "differences_counts.unreturned_ballots_count",
+ "differences_counts.too_few_ballots_handed_out_count"
+ ]
+ );
+
+ // test calculation for fewer_ballots_count does not add up and becomes minus
+ validation_results = ValidationResults::default();
+ differences_counts = DifferencesCounts {
+ more_ballots_count: 0,
+ fewer_ballots_count: 1,
+ unreturned_ballots_count: 1,
+ too_few_ballots_handed_out_count: 0,
+ too_many_ballots_handed_out_count: 2,
+ other_explanation_count: 0,
+ no_explanation_count: 0,
+ };
+ let election = election_fixture(&[]);
+ differences_counts
+ .validate(
+ &election,
+ &mut validation_results,
+ "differences_counts".to_string(),
+ )
+ .unwrap();
+ assert_eq!(validation_results.errors.len(), 0);
+ assert_eq!(validation_results.warnings.len(), 1);
+ assert_eq!(
+ validation_results.warnings[0].code,
+ ValidationResultCode::IncorrectTotal
+ );
+ assert_eq!(
+ validation_results.warnings[0].fields,
+ vec![
+ "differences_counts.fewer_ballots_count",
+ "differences_counts.unreturned_ballots_count",
+ "differences_counts.too_few_ballots_handed_out_count",
+ "differences_counts.too_many_ballots_handed_out_count",
+ "differences_counts.other_explanation_count",
+ "differences_counts.no_explanation_count"
+ ]
+ );
+
+ // test W.303 incorrect total
+ validation_results = ValidationResults::default();
+ differences_counts = DifferencesCounts {
+ more_ballots_count: 0,
+ fewer_ballots_count: 5, // W.303 incorrect total
+ unreturned_ballots_count: 1,
+ too_few_ballots_handed_out_count: 1,
+ too_many_ballots_handed_out_count: 0,
+ other_explanation_count: 1,
+ no_explanation_count: 1,
+ };
+ let election = election_fixture(&[]);
+ differences_counts
+ .validate(
+ &election,
+ &mut validation_results,
+ "differences_counts".to_string(),
+ )
+ .unwrap();
+ assert_eq!(validation_results.errors.len(), 0);
+ assert_eq!(validation_results.warnings.len(), 1);
+ assert_eq!(
+ validation_results.warnings[0].code,
+ ValidationResultCode::IncorrectTotal
+ );
+ assert_eq!(
+ validation_results.warnings[0].fields,
+ vec![
+ "differences_counts.fewer_ballots_count",
+ "differences_counts.unreturned_ballots_count",
+ "differences_counts.too_few_ballots_handed_out_count",
+ "differences_counts.too_many_ballots_handed_out_count",
+ "differences_counts.other_explanation_count",
+ "differences_counts.no_explanation_count"
+ ]
+ );
}
#[test]
fn test_political_group_votes_validation() {
+ let mut validation_results = ValidationResults::default();
// create a valid political group votes with two groups and two candidates each
let mut political_group_votes = vec![
PoliticalGroupVotes {
@@ -672,7 +1667,7 @@ mod tests {
},
PoliticalGroupVotes {
number: 2,
- total: 1_000_000_000, // F.01 out of range
+ total: 1_000_000_000, // out of range
candidate_votes: vec![
CandidateVotes {
number: 1,
@@ -680,15 +1675,14 @@ mod tests {
},
CandidateVotes {
number: 2,
- votes: 1_000_000_000, // F.01 out of range
+ votes: 1_000_000_000, // out of range
},
],
},
];
- let election = election_fixture(&[2, 2]);
+ let mut election = election_fixture(&[2, 2]);
// validate out of range number of candidates
- let mut validation_results = ValidationResults::default();
let res = political_group_votes.validate(
&election,
&mut validation_results,
@@ -697,10 +1691,10 @@ mod tests {
assert!(res.is_err());
// validate with correct in range votes for second political group but incorrect total for first political group
+ validation_results = ValidationResults::default();
political_group_votes[1].candidate_votes[1].votes = 20;
political_group_votes[1].total = 20;
political_group_votes[0].total = 20;
- let mut validation_results = ValidationResults::default();
political_group_votes
.validate(
&election,
@@ -714,11 +1708,15 @@ mod tests {
validation_results.errors[0].code,
ValidationResultCode::IncorrectTotal
);
+ assert_eq!(
+ validation_results.errors[0].fields,
+ vec!["political_group_votes[0].total"]
+ );
// validate with incorrect number of candidates for the first political group
+ validation_results = ValidationResults::default();
+ election = election_fixture(&[3, 2]);
political_group_votes[0].total = 25;
- let election = election_fixture(&[3, 2]);
- let mut validation_results = ValidationResults::default();
let res = political_group_votes.validate(
&election,
&mut validation_results,
@@ -727,8 +1725,8 @@ mod tests {
assert!(res.is_err());
// validate with incorrect number of political groups
- let election = election_fixture(&[2, 2, 2]);
- let mut validation_results = ValidationResults::default();
+ validation_results = ValidationResults::default();
+ election = election_fixture(&[2, 2, 2]);
let res = political_group_votes.validate(
&election,
&mut validation_results,
@@ -737,9 +1735,9 @@ mod tests {
assert!(res.is_err());
// validate with correct number of political groups but mixed up numbers
- let election = election_fixture(&[2, 2]);
+ validation_results = ValidationResults::default();
+ election = election_fixture(&[2, 2]);
political_group_votes[0].number = 2;
- let mut validation_results = ValidationResults::default();
let res = political_group_votes.validate(
&election,
&mut validation_results,
diff --git a/backend/src/validation.rs b/backend/src/validation.rs
index 79439692a..91e043aa0 100644
--- a/backend/src/validation.rs
+++ b/backend/src/validation.rs
@@ -35,6 +35,10 @@ pub enum ValidationResultCode {
IncorrectTotal,
AboveThreshold,
EqualInput,
+ MissingRecounts,
+ IncorrectDifference,
+ ConflictingDifferences,
+ NoDifferenceExpected,
}
impl fmt::Display for ValidationResultCode {
@@ -43,6 +47,10 @@ impl fmt::Display for ValidationResultCode {
ValidationResultCode::IncorrectTotal => write!(f, "Incorrect sum"),
ValidationResultCode::AboveThreshold => write!(f, "Above threshold"),
ValidationResultCode::EqualInput => write!(f, "Equal input"),
+ ValidationResultCode::MissingRecounts => write!(f, "Missing recounts"),
+ ValidationResultCode::IncorrectDifference => write!(f, "Incorrect difference"),
+ ValidationResultCode::ConflictingDifferences => write!(f, "Conflicting differences"),
+ ValidationResultCode::NoDifferenceExpected => write!(f, "No difference expected"),
}
}
}
diff --git a/backend/tests/data_entries_integration_test.rs b/backend/tests/data_entries_integration_test.rs
index 782676c0c..4d4aa659d 100644
--- a/backend/tests/data_entries_integration_test.rs
+++ b/backend/tests/data_entries_integration_test.rs
@@ -21,6 +21,7 @@ async fn test_polling_station_data_entry_valid(pool: SqlitePool) {
let request_body = DataEntryRequest {
data: PollingStationResults {
+ recounted: false,
voters_counts: VotersCounts {
poll_card_count: 100,
proxy_certificate_count: 2,
@@ -33,6 +34,7 @@ async fn test_polling_station_data_entry_valid(pool: SqlitePool) {
invalid_votes_count: 1,
total_votes_cast_count: 104,
},
+ voters_recounts: None,
differences_counts: DifferencesCounts {
more_ballots_count: 0,
fewer_ballots_count: 0,
@@ -91,6 +93,7 @@ async fn test_polling_station_data_entry_validation(pool: SqlitePool) {
let request_body = json!({
"data": {
+ "recounted": false,
"voters_counts": {
"poll_card_count": 1,
"proxy_certificate_count": 2,
@@ -103,6 +106,7 @@ async fn test_polling_station_data_entry_validation(pool: SqlitePool) {
"invalid_votes_count": 7,
"total_votes_cast_count": 8
},
+ "voters_recounts": null,
"differences_counts": {
"more_ballots_count": 4,
"fewer_ballots_count": 0,
@@ -153,10 +157,10 @@ async fn test_polling_station_data_entry_validation(pool: SqlitePool) {
assert_eq!(
errors[0].fields,
vec![
- "data.voters_counts.total_admitted_voters_count",
- "data.voters_counts.poll_card_count",
- "data.voters_counts.proxy_certificate_count",
- "data.voters_counts.voter_card_count"
+ "data.votes_counts.total_votes_cast_count",
+ "data.votes_counts.votes_candidates_counts",
+ "data.votes_counts.blank_votes_count",
+ "data.votes_counts.invalid_votes_count"
]
);
// error 2
@@ -164,10 +168,10 @@ async fn test_polling_station_data_entry_validation(pool: SqlitePool) {
assert_eq!(
errors[1].fields,
vec![
- "data.votes_counts.total_votes_cast_count",
- "data.votes_counts.votes_candidates_counts",
- "data.votes_counts.blank_votes_count",
- "data.votes_counts.invalid_votes_count"
+ "data.voters_counts.total_admitted_voters_count",
+ "data.voters_counts.poll_card_count",
+ "data.voters_counts.proxy_certificate_count",
+ "data.voters_counts.voter_card_count"
]
);
// error 3
@@ -219,6 +223,7 @@ async fn test_polling_station_data_entry_only_for_existing(pool: SqlitePool) {
let request_body = DataEntryRequest {
data: PollingStationResults {
+ recounted: false,
voters_counts: VotersCounts {
poll_card_count: 100,
proxy_certificate_count: 2,
@@ -231,6 +236,7 @@ async fn test_polling_station_data_entry_only_for_existing(pool: SqlitePool) {
invalid_votes_count: 1,
total_votes_cast_count: 104,
},
+ voters_recounts: None,
differences_counts: DifferencesCounts {
more_ballots_count: 0,
fewer_ballots_count: 0,
diff --git a/backend/tests/election_integration_test.rs b/backend/tests/election_integration_test.rs
index 20b1e6abf..9306f8fb8 100644
--- a/backend/tests/election_integration_test.rs
+++ b/backend/tests/election_integration_test.rs
@@ -1,14 +1,215 @@
#![cfg(test)]
use reqwest::StatusCode;
+use serde_json::json;
use sqlx::SqlitePool;
-use backend::election::{ElectionDetailsResponse, ElectionListResponse};
-
use crate::utils::serve_api;
+use backend::election::{ElectionDetailsResponse, ElectionListResponse};
+use backend::polling_station::{
+ CandidateVotes, DataEntryRequest, DataEntryResponse, DifferencesCounts, PoliticalGroupVotes,
+ PollingStationResults, VotersCounts, VotesCounts,
+};
+use backend::validation::ValidationResultCode::IncorrectTotal;
+use backend::ErrorResponse;
mod utils;
+#[sqlx::test(fixtures("../fixtures/elections.sql", "../fixtures/polling_stations.sql"))]
+async fn test_polling_station_data_entry_valid(pool: SqlitePool) {
+ let addr = serve_api(pool).await;
+
+ let request_body = DataEntryRequest {
+ data: PollingStationResults {
+ recounted: false,
+ voters_counts: VotersCounts {
+ poll_card_count: 100,
+ proxy_certificate_count: 2,
+ voter_card_count: 2,
+ total_admitted_voters_count: 104,
+ },
+ votes_counts: VotesCounts {
+ votes_candidates_counts: 102,
+ blank_votes_count: 1,
+ invalid_votes_count: 1,
+ total_votes_cast_count: 104,
+ },
+ voters_recounts: None,
+ differences_counts: DifferencesCounts {
+ more_ballots_count: 0,
+ fewer_ballots_count: 0,
+ unreturned_ballots_count: 0,
+ too_few_ballots_handed_out_count: 0,
+ too_many_ballots_handed_out_count: 0,
+ other_explanation_count: 0,
+ no_explanation_count: 0,
+ },
+ political_group_votes: vec![PoliticalGroupVotes {
+ number: 1,
+ total: 102,
+ candidate_votes: vec![
+ CandidateVotes {
+ number: 1,
+ votes: 54,
+ },
+ CandidateVotes {
+ number: 2,
+ votes: 48,
+ },
+ ],
+ }],
+ },
+ };
+
+ let url = format!("http://{addr}/api/polling_stations/1/data_entries/1");
+ let response = reqwest::Client::new()
+ .post(&url)
+ .json(&request_body)
+ .send()
+ .await
+ .unwrap();
+
+ // Ensure the response is what we expect
+ let status = response.status();
+ if status != StatusCode::OK {
+ println!("Response body: {:?}", &response.text().await.unwrap());
+ panic!("Unexpected response status: {:?}", status);
+ }
+ let validation_results: DataEntryResponse = response.json().await.unwrap();
+ assert_eq!(validation_results.validation_results.errors.len(), 0);
+ assert_eq!(validation_results.validation_results.warnings.len(), 0);
+}
+
+#[sqlx::test]
+async fn test_polling_station_data_entry_invalid(pool: SqlitePool) {
+ let addr = serve_api(pool).await;
+
+ let url = format!("http://{addr}/api/polling_stations/1/data_entries/1");
+ let response = reqwest::Client::new()
+ .post(&url)
+ .header("content-type", "application/json")
+ .body(r##"{"data":null}"##)
+ .send()
+ .await
+ .unwrap();
+
+ // Ensure the response is what we expect
+ let status = response.status();
+ let body: ErrorResponse = response.json().await.unwrap();
+ println!("response body: {:?}", &body);
+ assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
+ assert_eq!(
+ body.error,
+ "Failed to deserialize the JSON body into the target type: data: \
+ invalid type: null, expected struct PollingStationResults at line 1 column 12"
+ );
+}
+
+#[sqlx::test(fixtures("../fixtures/elections.sql", "../fixtures/polling_stations.sql"))]
+async fn test_polling_station_data_entry_validation(pool: SqlitePool) {
+ let addr = serve_api(pool).await;
+
+ let request_body = json!({
+ "data": {
+ "recounted": false,
+ "voters_counts": {
+ "poll_card_count": 1,
+ "proxy_certificate_count": 2,
+ "voter_card_count": 3,
+ "total_admitted_voters_count": 4
+ },
+ "votes_counts": {
+ "votes_candidates_counts": 5,
+ "blank_votes_count": 6,
+ "invalid_votes_count": 7,
+ "total_votes_cast_count": 8
+ },
+ "voters_recounts": null,
+ "differences_counts": {
+ "more_ballots_count": 4,
+ "fewer_ballots_count": 0,
+ "unreturned_ballots_count": 0,
+ "too_few_ballots_handed_out_count": 0,
+ "too_many_ballots_handed_out_count": 2,
+ "other_explanation_count": 1,
+ "no_explanation_count": 1,
+ },
+ "political_group_votes": [
+ {
+ "number": 1,
+ "total": 11,
+ "candidate_votes": [
+ {
+ "number": 1,
+ "votes": 6
+ },
+ {
+ "number": 2,
+ "votes": 4
+ }
+ ]
+ }
+ ]
+ }
+ });
+
+ let url = format!("http://{addr}/api/polling_stations/1/data_entries/1");
+ let response = reqwest::Client::new()
+ .post(&url)
+ .json(&request_body)
+ .send()
+ .await
+ .unwrap();
+
+ // Ensure the response is what we expect
+ let status = response.status();
+ if status != StatusCode::OK {
+ println!("response body: {:?}", &response.text().await.unwrap());
+ panic!("Unexpected response status: {:?}", status);
+ }
+ let body: DataEntryResponse = response.json().await.unwrap();
+ let errors = body.validation_results.errors;
+ assert_eq!(errors.len(), 4);
+ // error 1
+ assert_eq!(errors[0].code, IncorrectTotal);
+ assert_eq!(
+ errors[0].fields,
+ vec![
+ "data.votes_counts.total_votes_cast_count",
+ "data.votes_counts.votes_candidates_counts",
+ "data.votes_counts.blank_votes_count",
+ "data.votes_counts.invalid_votes_count"
+ ]
+ );
+ // error 2
+ assert_eq!(errors[1].code, IncorrectTotal);
+ assert_eq!(
+ errors[1].fields,
+ vec![
+ "data.voters_counts.total_admitted_voters_count",
+ "data.voters_counts.poll_card_count",
+ "data.voters_counts.proxy_certificate_count",
+ "data.voters_counts.voter_card_count"
+ ]
+ );
+ // error 3
+ assert_eq!(errors[2].code, IncorrectTotal);
+ assert_eq!(
+ errors[2].fields,
+ vec!["data.political_group_votes[0].total"]
+ );
+ // error 4
+ assert_eq!(errors[3].code, IncorrectTotal);
+ assert_eq!(
+ errors[3].fields,
+ vec![
+ "data.votes_counts.votes_candidates_counts",
+ "data.political_group_votes"
+ ]
+ );
+ assert_eq!(body.validation_results.warnings.len(), 0);
+}
+
#[sqlx::test(fixtures("../fixtures/elections.sql"))]
async fn test_election_list_works(pool: SqlitePool) {
let addr = serve_api(pool).await;
diff --git a/frontend/app/component/form/candidates_votes_form/CandidatesVotesForm.test.tsx b/frontend/app/component/form/candidates_votes_form/CandidatesVotesForm.test.tsx
index 92818a13c..450eae5a8 100644
--- a/frontend/app/component/form/candidates_votes_form/CandidatesVotesForm.test.tsx
+++ b/frontend/app/component/form/candidates_votes_form/CandidatesVotesForm.test.tsx
@@ -25,23 +25,7 @@ const Component = (
const rootRequest: POLLING_STATION_DATA_ENTRY_REQUEST_BODY = {
data: {
- political_group_votes: electionMock.political_groups.map((group) => ({
- number: group.number,
- total: 0,
- candidate_votes: group.candidates.map((candidate) => ({
- number: candidate.number,
- votes: 0,
- })),
- })),
- differences_counts: {
- more_ballots_count: 0,
- fewer_ballots_count: 0,
- unreturned_ballots_count: 0,
- too_few_ballots_handed_out_count: 0,
- too_many_ballots_handed_out_count: 0,
- other_explanation_count: 0,
- no_explanation_count: 0,
- },
+ recounted: false,
voters_counts: {
poll_card_count: 0,
proxy_certificate_count: 0,
@@ -54,6 +38,24 @@ const rootRequest: POLLING_STATION_DATA_ENTRY_REQUEST_BODY = {
invalid_votes_count: 0,
total_votes_cast_count: 0,
},
+ voters_recounts: undefined,
+ differences_counts: {
+ more_ballots_count: 0,
+ fewer_ballots_count: 0,
+ unreturned_ballots_count: 0,
+ too_few_ballots_handed_out_count: 0,
+ too_many_ballots_handed_out_count: 0,
+ other_explanation_count: 0,
+ no_explanation_count: 0,
+ },
+ political_group_votes: electionMock.political_groups.map((group) => ({
+ number: group.number,
+ total: 0,
+ candidate_votes: group.candidates.map((candidate) => ({
+ number: candidate.number,
+ votes: 0,
+ })),
+ })),
},
};
@@ -319,7 +321,7 @@ describe("Test CandidatesVotesForm", () => {
});
describe("CandidatesVotesForm errors", () => {
- test("F.31 IncorrectTotal group total", async () => {
+ test("F.401 IncorrectTotal group total", async () => {
overrideOnce("post", "/api/polling_stations/1/data_entries/1", 200, {
validation_results: {
errors: [
diff --git a/frontend/app/component/form/candidates_votes_form/CandidatesVotesForm.tsx b/frontend/app/component/form/candidates_votes_form/CandidatesVotesForm.tsx
index 7fec2ce16..f5db0949b 100644
--- a/frontend/app/component/form/candidates_votes_form/CandidatesVotesForm.tsx
+++ b/frontend/app/component/form/candidates_votes_form/CandidatesVotesForm.tsx
@@ -64,7 +64,7 @@ export function CandidatesVotesForm({ group }: CandidatesVotesFormProps) {
const errorsAndWarnings = useErrorsAndWarnings(errors, warnings, inputMaskWarnings);
- //const blocker = useBlocker() use const blocker to render confirmation UI.
+ //const blocker = useBlocker() use const blocker to render confirmation UI.
useBlocker(() => {
if (formRef.current && !isCalled) {
const elements = formRef.current.elements;
@@ -86,7 +86,8 @@ export function CandidatesVotesForm({ group }: CandidatesVotesFormProps) {
const hasValidationError = errors.length > 0;
const hasValidationWarning = warnings.length > 0;
- const success = isCalled && !hasValidationError && !hasValidationWarning && !loading;
+ const success =
+ isCalled && !serverError && !hasValidationError && !hasValidationWarning && !loading;
return (
diff --git a/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.test.tsx b/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.test.tsx
index 9da6316f4..82082046f 100644
--- a/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.test.tsx
+++ b/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.test.tsx
@@ -9,40 +9,28 @@ import { getUrlMethodAndBody, overrideOnce, render, screen, userTypeInputs } fro
import {
POLLING_STATION_DATA_ENTRY_REQUEST_BODY,
PollingStationFormController,
+ PollingStationValues,
} from "@kiesraad/api";
import { electionMock, pollingStationMock } from "@kiesraad/api-mocks";
import { VotersAndVotesForm } from "./VotersAndVotesForm";
-const Component = (
-
-
-
-);
+function renderForm(defaultValues: Partial = {}) {
+ return render(
+
+
+ ,
+ );
+}
const rootRequest: POLLING_STATION_DATA_ENTRY_REQUEST_BODY = {
data: {
- political_group_votes: electionMock.political_groups.map((group) => ({
- number: group.number,
- total: 0,
- candidate_votes: group.candidates.map((candidate) => ({
- number: candidate.number,
- votes: 0,
- })),
- })),
- differences_counts: {
- more_ballots_count: 0,
- fewer_ballots_count: 0,
- unreturned_ballots_count: 0,
- too_few_ballots_handed_out_count: 0,
- too_many_ballots_handed_out_count: 0,
- other_explanation_count: 0,
- no_explanation_count: 0,
- },
+ recounted: false,
voters_counts: {
poll_card_count: 0,
proxy_certificate_count: 0,
@@ -55,6 +43,24 @@ const rootRequest: POLLING_STATION_DATA_ENTRY_REQUEST_BODY = {
invalid_votes_count: 0,
total_votes_cast_count: 0,
},
+ voters_recounts: undefined,
+ differences_counts: {
+ more_ballots_count: 0,
+ fewer_ballots_count: 0,
+ unreturned_ballots_count: 0,
+ too_few_ballots_handed_out_count: 0,
+ too_many_ballots_handed_out_count: 0,
+ other_explanation_count: 0,
+ no_explanation_count: 0,
+ },
+ political_group_votes: electionMock.political_groups.map((group) => ({
+ number: group.number,
+ total: 0,
+ candidate_votes: group.candidates.map((candidate) => ({
+ number: candidate.number,
+ votes: 0,
+ })),
+ })),
},
};
@@ -67,7 +73,7 @@ describe("Test VotersAndVotesForm", () => {
test("hitting enter key does not result in api call", async () => {
const user = userEvent.setup();
- render(Component);
+ renderForm();
const spy = vi.spyOn(global, "fetch");
const pollCards = await screen.findByTestId("poll_card_count");
@@ -86,7 +92,7 @@ describe("Test VotersAndVotesForm", () => {
const user = userEvent.setup();
- render(Component);
+ renderForm();
const pollCards = await screen.findByTestId("poll_card_count");
expect(pollCards).toHaveFocus();
@@ -173,7 +179,7 @@ describe("Test VotersAndVotesForm", () => {
const user = userEvent.setup();
- render(Component);
+ renderForm();
const spy = vi.spyOn(global, "fetch");
await userTypeInputs(user, {
@@ -202,7 +208,7 @@ describe("Test VotersAndVotesForm", () => {
const user = userEvent.setup();
- render(Component);
+ renderForm();
const submitButton = await screen.findByRole("button", { name: "Volgende" });
await user.click(submitButton);
@@ -221,7 +227,7 @@ describe("Test VotersAndVotesForm", () => {
const user = userEvent.setup();
- render(Component);
+ renderForm();
const submitButton = await screen.findByRole("button", { name: "Volgende" });
await user.click(submitButton);
@@ -234,7 +240,7 @@ describe("Test VotersAndVotesForm", () => {
});
describe("VotersAndVotesForm errors", () => {
- test("F.11 IncorrectTotal Voters", async () => {
+ test("F.201 IncorrectTotal Voters counts", async () => {
overrideOnce("post", "/api/polling_stations/1/data_entries/1", 200, {
validation_results: {
errors: [
@@ -254,7 +260,7 @@ describe("Test VotersAndVotesForm", () => {
const user = userEvent.setup();
- render(Component);
+ renderForm();
// We await the first element to appear, so we know the page is loaded
await user.type(await screen.findByTestId("poll_card_count"), "1");
@@ -271,7 +277,7 @@ describe("Test VotersAndVotesForm", () => {
expect(screen.queryByTestId("server-feedback-error")).toBeNull();
});
- test("F.12 IncorrectTotal Votes", async () => {
+ test("F.202 IncorrectTotal Votes counts", async () => {
overrideOnce("post", "/api/polling_stations/1/data_entries/1", 200, {
validation_results: {
errors: [
@@ -284,10 +290,6 @@ describe("Test VotersAndVotesForm", () => {
],
code: "IncorrectTotal",
},
- {
- fields: ["data.votes_counts.votes_candidates_counts", "data.political_group_votes"],
- code: "IncorrectTotal",
- },
],
warnings: [],
},
@@ -295,7 +297,7 @@ describe("Test VotersAndVotesForm", () => {
const user = userEvent.setup();
- render(Component);
+ renderForm();
// We await the first element to appear, so we know the page is loaded
await user.type(await screen.findByTestId("votes_candidates_counts"), "1");
@@ -312,6 +314,42 @@ describe("Test VotersAndVotesForm", () => {
expect(screen.queryByTestId("server-feedback-error")).toBeNull();
});
+ test("F.203 IncorrectTotal Voters recounts", async () => {
+ overrideOnce("post", "/api/polling_stations/1/data_entries/1", 200, {
+ validation_results: {
+ errors: [
+ {
+ fields: [
+ "data.voters_recounts.total_admitted_voters_recount",
+ "data.voters_recounts.poll_card_recount",
+ "data.voters_recounts.proxy_certificate_recount",
+ "data.voters_recounts.voter_card_recount",
+ ],
+ code: "IncorrectTotal",
+ },
+ ],
+ warnings: [],
+ },
+ });
+
+ const user = userEvent.setup();
+
+ renderForm({ recounted: true });
+
+ await user.type(screen.getByTestId("poll_card_recount"), "1");
+ await user.type(screen.getByTestId("proxy_certificate_recount"), "1");
+ await user.type(screen.getByTestId("voter_card_recount"), "1");
+ await user.type(screen.getByTestId("total_admitted_voters_recount"), "4");
+
+ const submitButton = screen.getByRole("button", { name: "Volgende" });
+ await user.click(submitButton);
+
+ const feedbackError = await screen.findByTestId("feedback-error");
+ expect(feedbackError).toHaveTextContent(/^IncorrectTotal$/);
+ expect(screen.queryByTestId("feedback-warning")).toBeNull();
+ expect(screen.queryByTestId("server-feedback-error")).toBeNull();
+ });
+
test("Error with non-existing fields is not displayed", async () => {
overrideOnce("post", "/api/polling_stations/1/data_entries/1", 200, {
validation_results: {
@@ -330,7 +368,7 @@ describe("Test VotersAndVotesForm", () => {
const user = userEvent.setup();
- render(Component);
+ renderForm();
// Since the component does not allow to input values for non-existing fields,
// not inputting any values and just clicking the submit button.
@@ -345,7 +383,7 @@ describe("Test VotersAndVotesForm", () => {
});
describe("VotersAndVotesForm warnings", () => {
- test("W.21 AboveThreshold blank votes", async () => {
+ test("W.201 AboveThreshold blank votes", async () => {
overrideOnce("post", "/api/polling_stations/1/data_entries/1", 200, {
validation_results: {
errors: [],
@@ -363,7 +401,7 @@ describe("Test VotersAndVotesForm", () => {
const user = userEvent.setup();
- render(Component);
+ renderForm();
// We await the first element to appear, so we know the page is loaded
await user.type(await screen.findByTestId("votes_candidates_counts"), "0");
@@ -380,7 +418,7 @@ describe("Test VotersAndVotesForm", () => {
expect(screen.queryByTestId("feedback-error")).toBeNull();
});
- test("W.22 AboveThreshold invalid votes", async () => {
+ test("W.202 AboveThreshold invalid votes", async () => {
overrideOnce("post", "/api/polling_stations/1/data_entries/1", 200, {
validation_results: {
errors: [],
@@ -398,7 +436,7 @@ describe("Test VotersAndVotesForm", () => {
const user = userEvent.setup();
- render(Component);
+ renderForm();
// We await the first element to appear, so we know the page is loaded
await user.type(await screen.findByTestId("votes_candidates_counts"), "0");
@@ -415,15 +453,10 @@ describe("Test VotersAndVotesForm", () => {
expect(screen.queryByTestId("feedback-error")).toBeNull();
});
- test("W.27 EqualInput voters and votes", async () => {
+ test("W.209 EqualInput voters counts and votes counts", async () => {
overrideOnce("post", "/api/polling_stations/1/data_entries/1", 200, {
validation_results: {
- errors: [
- {
- fields: ["data.votes_counts.votes_candidates_counts", "data.political_group_votes"],
- code: "IncorrectTotal",
- },
- ],
+ errors: [],
warnings: [
{
fields: ["data.voters_counts", "data.votes_counts"],
@@ -435,7 +468,7 @@ describe("Test VotersAndVotesForm", () => {
const user = userEvent.setup();
- render(Component);
+ renderForm();
// We await the first element to appear, so we know the page is loaded
await user.type(await screen.findByTestId("poll_card_count"), "1");
@@ -456,5 +489,41 @@ describe("Test VotersAndVotesForm", () => {
expect(screen.queryByTestId("feedback-server-error")).toBeNull();
expect(screen.queryByTestId("feedback-error")).toBeNull();
});
+
+ test("W.210 EqualInput voters recounts and votes counts", async () => {
+ overrideOnce("post", "/api/polling_stations/1/data_entries/1", 200, {
+ validation_results: {
+ errors: [],
+ warnings: [
+ {
+ fields: ["data.voters_recounts", "data.votes_counts"],
+ code: "EqualInput",
+ },
+ ],
+ },
+ });
+
+ const user = userEvent.setup();
+
+ renderForm({ recounted: true });
+
+ await user.type(screen.getByTestId("votes_candidates_counts"), "1");
+ await user.type(screen.getByTestId("blank_votes_count"), "0");
+ await user.type(screen.getByTestId("invalid_votes_count"), "0");
+ await user.type(screen.getByTestId("total_votes_cast_count"), "1");
+
+ await user.type(screen.getByTestId("poll_card_recount"), "1");
+ await user.type(screen.getByTestId("proxy_certificate_recount"), "0");
+ await user.type(screen.getByTestId("voter_card_recount"), "0");
+ await user.type(screen.getByTestId("total_admitted_voters_recount"), "1");
+
+ const submitButton = screen.getByRole("button", { name: "Volgende" });
+ await user.click(submitButton);
+
+ const feedbackWarning = await screen.findByTestId("feedback-warning");
+ expect(feedbackWarning).toHaveTextContent(/^EqualInput$/);
+ expect(screen.queryByTestId("feedback-server-error")).toBeNull();
+ expect(screen.queryByTestId("feedback-error")).toBeNull();
+ });
});
});
diff --git a/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx b/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx
index 97ea49bb2..e88fd6e78 100644
--- a/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx
+++ b/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx
@@ -14,6 +14,10 @@ interface FormElements extends HTMLFormControlsCollection {
blank_votes_count: HTMLInputElement;
invalid_votes_count: HTMLInputElement;
total_votes_cast_count: HTMLInputElement;
+ poll_card_recount: HTMLInputElement;
+ proxy_certificate_recount: HTMLInputElement;
+ voter_card_recount: HTMLInputElement;
+ total_admitted_voters_recount: HTMLInputElement;
}
interface VotersAndVotesFormElement extends HTMLFormElement {
@@ -40,6 +44,7 @@ export function VotersAndVotesForm() {
serverError,
isCalled,
setTemporaryCache,
+ recounted,
} = useVotersAndVotes();
useTooltip({
@@ -48,7 +53,7 @@ export function VotersAndVotesForm() {
const getValues = React.useCallback(
(elements: VotersAndVotesFormElement["elements"]): VotersAndVotesValues => {
- return {
+ const values: VotersAndVotesValues = {
voters_counts: {
poll_card_count: deformat(elements.poll_card_count.value),
proxy_certificate_count: deformat(elements.proxy_certificate_count.value),
@@ -61,17 +66,24 @@ export function VotersAndVotesForm() {
invalid_votes_count: deformat(elements.invalid_votes_count.value),
total_votes_cast_count: deformat(elements.total_votes_cast_count.value),
},
+ voters_recounts: undefined,
};
+ if (recounted) {
+ values.voters_recounts = {
+ poll_card_recount: deformat(elements.poll_card_recount.value),
+ proxy_certificate_recount: deformat(elements.proxy_certificate_recount.value),
+ voter_card_recount: deformat(elements.voter_card_recount.value),
+ total_admitted_voters_recount: deformat(elements.total_admitted_voters_recount.value),
+ };
+ }
+ return values;
},
- [deformat],
+ [deformat, recounted],
);
- function handleSubmit(event: React.FormEvent) {
- event.preventDefault();
- const elements = event.currentTarget.elements;
- setSectionValues(getValues(elements));
- }
- //const blocker = useBlocker() use const blocker to render confirmation UI.
+ const errorsAndWarnings = useErrorsAndWarnings(errors, warnings, inputMaskWarnings);
+
+ //const blocker = useBlocker() use const blocker to render confirmation UI.
useBlocker(() => {
if (formRef.current && !isCalled) {
const elements = formRef.current.elements as VotersAndVotesFormElement["elements"];
@@ -84,7 +96,11 @@ export function VotersAndVotesForm() {
return false;
});
- const errorsAndWarnings = useErrorsAndWarnings(errors, warnings, inputMaskWarnings);
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault();
+ const elements = event.currentTarget.elements;
+ setSectionValues(getValues(elements));
+ }
React.useEffect(() => {
if (isCalled) {
@@ -94,7 +110,8 @@ export function VotersAndVotesForm() {
const hasValidationError = errors.length > 0;
const hasValidationWarning = warnings.length > 0;
- const success = isCalled && !hasValidationError && !hasValidationWarning && !loading;
+ const success =
+ isCalled && !serverError && !hasValidationError && !hasValidationWarning && !loading;
return (