Repository: nhsuk/manage-vaccinations-in-schools
Pull request #3409: Accumulate gender on import
Author: login: alistair-white-horne-tng
Labels: name: feature
description: New functionality
Created: 2025-04-15T11:06:41Z | Merged: 2025-04-30T13:23:37Z

During an import, if the existing patient's gender is `nil`, `not_known` or `not_specified`, and the incoming value is different, then Mavis will now automatically accept the new value without staging it and asking for a review.


Diff:
diff --git a/app/models/patient_import_row.rb b/app/models/patient_import_row.rb
index d24e41d32d..8f48f93a65 100644
--- a/app/models/patient_import_row.rb
+++ b/app/models/patient_import_row.rb
@@ -47,6 +47,12 @@ def to_patient
         existing_patient.registration = attributes.delete(:registration)
       end
 
+      if attributes[:gender_code].in?(%w[male female not_specified]) &&
+           !existing_patient.gender_code.in?(%w[male female not_specified]) &&
+           attributes[:gender_code] != existing_patient.gender_code
+        existing_patient.gender_code = attributes.delete(:gender_code)
+      end
+
       if address_postcode.present? &&
            address_postcode.to_postcode != existing_patient.address_postcode
         attributes.merge!(
diff --git a/spec/models/class_import_row_spec.rb b/spec/models/class_import_row_spec.rb
index 3a0aeb7e06..edf6ff4984 100644
--- a/spec/models/class_import_row_spec.rb
+++ b/spec/models/class_import_row_spec.rb
@@ -216,6 +216,62 @@
         end
       end
     end
+
+    context "with an existing patient without gender" do
+      let(:data) { valid_data.merge("CHILD_GENDER" => "male") }
+
+      let!(:existing_patient) do
+        create(
+          :patient,
+          address_postcode: "SW1A 1AA",
+          family_name: "Smith",
+          gender_code: "not_known",
+          given_name: "Jimmy",
+          date_of_birth: Date.new(2010, 1, 1)
+        )
+      end
+
+      it { should eq(existing_patient) }
+
+      it "saves the incoming gender" do
+        expect(patient).to have_attributes(gender_code: "male")
+      end
+
+      it "doesn't stage the gender differences" do
+        expect(patient.pending_changes).to be_empty
+      end
+    end
+
+    context "with an existing patient already with gender" do
+      let(:data) { valid_data.merge("CHILD_GENDER" => "male") }
+
+      let!(:existing_patient) do
+        create(
+          :patient,
+          address_postcode: "SW1A 1AA",
+          family_name: "Smith",
+          gender_code: "female",
+          given_name: "Jimmy",
+          nhs_number: "1234567890",
+          address_line_1: "10 Downing Street",
+          address_line_2: "",
+          address_town: "London",
+          birth_academic_year: 2009,
+          date_of_birth: Date.new(2010, 1, 1),
+          registration: "8AB"
+        )
+      end
+
+      it { should eq(existing_patient) }
+
+      it "does not save the incoming gender" do
+        expect(patient).to have_attributes(gender_code: "female")
+      end
+
+      it "does stage the gender differences" do
+        expect(patient.pending_changes).to include("gender_code" => "male")
+      end
+    end
   end
 
   describe "#to_parent_relationships" do
diff --git a/spec/models/cohort_import_row_spec.rb b/spec/models/cohort_import_row_spec.rb
index 1b0735495b..3228f085d7 100644
--- a/spec/models/cohort_import_row_spec.rb
+++ b/spec/models/cohort_import_row_spec.rb
@@ -221,6 +221,64 @@
         expect(patient.pending_changes).to include("registration" => "8AB")
       end
     end
+
+    context "with an existing patient without gender" do
+      let!(:existing_patient) do
+        create(
+          :patient,
+          address_postcode: "SW1A 1AA",
+          family_name: "Smith",
+          gender_code: "not_known",
+          given_name: "Jimmy",
+          nhs_number: "1234567890",
+          address_line_1: "10 Downing Street",
+          address_line_2: "",
+          address_town: "London",
+          birth_academic_year: 2009,
+          date_of_birth: Date.new(2010, 1, 1),
+          registration: "8AB"
+        )
+      end
+
+      it { should eq(existing_patient) }
+
+      it "saves the incoming gender" do
+        expect(patient).to have_attributes(gender_code: "male")
+      end
+
+      it "doesn't stage the gender differences" do
+        expect(patient.pending_changes).to be_empty
+      end
+    end
+
+    context "with an existing patient already with gender" do
+      let!(:existing_patient) do
+        create(
+          :patient,
+          address_postcode: "SW1A 1AA",
+          family_name: "Smith",
+          gender_code: "female",
+          given_name: "Jimmy",
+          nhs_number: "1234567890",
+          address_line_1: "10 Downing Street",
+          address_line_2: "",
+          address_town: "London",
+          birth_academic_year: 2009,
+          date_of_birth: Date.new(2010, 1, 1),
+          registration: "8AB"
+        )
+      end
+
+      it { should eq(existing_patient) }
+
+      it "does not save the incoming gender" do
+        expect(patient).to have_attributes(gender_code: "female")
+      end
+
+      it "does stage the gender differences" do
+        expect(patient.pending_changes).to include("gender_code" => "male")
+      end
+    end
   end
 
   describe "#to_school_move" do


Reviews:
user: login: thomasleese
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T11:14:06Z
user: login: alistair-white-horne-tng
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T12:19:18Z
user: login: thomasleese
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T14:27:41Z
user: login: misaka
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-16T15:18:50Z
user: login: alistair-white-horne-tng
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T13:36:56Z
user: login: misaka
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T14:48:23Z
user: login: alistair-white-horne-tng
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T15:46:02Z
user: login: thomasleese
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T07:30:49Z
user: login: thomasleese
body: The code looks good, I would suggest the commit messages can be swashed together as I don't think we need the level of granularity in the commit history.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T07:36:43Z
user: login: misaka
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T07:58:48Z
user: login: misaka
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-23T10:17:57Z

Review comments:
diff_hunk: @@ -9,14 +9,24 @@ def stage_changes(attributes)
     new_pending_changes =
       attributes.each_with_object({}) do |(attr, new_value), staged_changes|
         current_value = public_send(attr)
-        staged_changes[attr.to_s] = new_value if new_value != current_value
+
+        if attr == :gender_code && overwrite_gender?(current_value)
+          public_send("#{attr}=", new_value) if new_value != current_value
+        elsif new_value != current_value
+          staged_changes[attr.to_s] = new_value
+        end
path: app/models/concerns/pending_changes_concern.rb
user: login: thomasleese
body: The logic looks good, but I don't think it belongs in the `PendingChangesConcern` class. The class is designed to be generic enough that it can applied to any model, at the moment it's only being used by `Patient` and `VaccinationRecord`, but that doesn't mean it will always be the case.

The gender is specific to the patient, and therefore it may not be appropriate to add this logic to other classes. I would suggest instead that we apply this logic where the `stage_changes` method is being used.

We already have similar logic related to registration, where it's set automatically in class list imports but not in cohort imports. I think we can probably follow a similar approach for gender: https://github.com/nhsuk/manage-vaccinations-in-schools/blob/3d6d91783a30beb355f1bd8d00b674faaf4455f6/app/models/patient_import_row.rb#L46-L48
created_at: 2025-04-15T11:14:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -9,14 +9,24 @@ def stage_changes(attributes)
     new_pending_changes =
       attributes.each_with_object({}) do |(attr, new_value), staged_changes|
         current_value = public_send(attr)
-        staged_changes[attr.to_s] = new_value if new_value != current_value
+
+        if attr == :gender_code && overwrite_gender?(current_value)
+          public_send("#{attr}=", new_value) if new_value != current_value
+        elsif new_value != current_value
+          staged_changes[attr.to_s] = new_value
+        end
path: app/models/concerns/pending_changes_concern.rb
user: login: alistair-white-horne-tng
body: That makes sense. Changed that now
created_at: 2025-04-15T12:19:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -47,6 +47,12 @@ def to_patient
         existing_patient.registration = attributes.delete(:registration)
       end
 
+      if existing_patient.gender_code.nil? ||
path: app/models/patient_import_row.rb
user: login: thomasleese
body: We don't need to check for a `nil` gender code as the column is defined as `not null`:

https://github.com/nhsuk/manage-vaccinations-in-schools/blob/3d6d91783a30beb355f1bd8d00b674faaf4455f6/app/models/patient.rb#L17
created_at: 2025-04-15T14:27:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -145,6 +150,10 @@ def to_parent_relationships(parents, patient)
       end
   end
 
+  def is_gender_known?(gender_code)
+    gender_code.in?(%w[male female])
path: app/models/patient_import_row.rb
user: login: misaka
body: Is there a reason we only check this against `male` and `female`? Our patients model support `not_specified` ...
created_at: 2025-04-16T15:18:50Z
author_association: COLLABORATOR
diff_hunk: @@ -145,6 +150,10 @@ def to_parent_relationships(parents, patient)
       end
   end
 
+  def is_gender_known?(gender_code)
+    gender_code.in?(%w[male female])
path: app/models/patient_import_row.rb
user: login: alistair-white-horne-tng
body: I was under the impression that `male` or `female` should overwrite `not_specified` - is that incorrect?

I suppose I thought that `not_specified` indicated that the person didn't choose an option, rather than actively chose to keep their gender out of Mavis.
created_at: 2025-04-22T13:36:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -145,6 +150,10 @@ def to_parent_relationships(parents, patient)
       end
   end
 
+  def is_gender_known?(gender_code)
+    gender_code.in?(%w[male female])
path: app/models/patient_import_row.rb
user: login: misaka
body: There is a `indeterminate` concept in SystmOne, apparently, and an `unknown`. We had a conversation with Chris from the Hertfordshire SAIS team about how Mavis should export the gender code for SystmOne. He mentioned that SystmOne has both `unknown` and `indeterminate`, which I think map to `not_known` and `not_specified` in Mavis respectively. Despite this mapping, he was of the firm opinion that both `not_known` and `not_specified` in Mavis should export as `unknown` for the SystmOne export:

Chris:
> Indeterminate is the correct term for the mapping, but that should only be used if confirmed as part of the clinician's assessment and I would not expect it to appear unless the patient is aware their medical records still display that outcome. It would be (as far as I'm aware) much preferrable to have both mapped to "Unknown" for "Not Specified" and "Not Known". 

So while there is a difference between `not_known` and `not_specified`, it's not clear to me what's the right thing to do here. I suppose the first question is whether we have to get this right from the get-go, and if we do then what is the right mapping here.
created_at: 2025-04-22T14:48:23Z
author_association: COLLABORATOR
diff_hunk: @@ -145,6 +150,10 @@ def to_parent_relationships(parents, patient)
       end
   end
 
+  def is_gender_known?(gender_code)
+    gender_code.in?(%w[male female])
path: app/models/patient_import_row.rb
user: login: alistair-white-horne-tng
body: Okiedoke, because we're unsure, I'll leave it for manual review, and not get Mavis to auto-accept if `not_specified` is already there.
created_at: 2025-04-22T15:46:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -145,6 +150,10 @@ def to_parent_relationships(parents, patient)
       end
   end
 
+  def is_gender_known?(gender_code)
path: app/models/patient_import_row.rb
user: login: thomasleese
body: Just a minor thing, this method should probably belong below these in the `private` section since it's only to be used within this class and not externally.
created_at: 2025-04-23T07:30:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -145,6 +150,10 @@ def to_parent_relationships(parents, patient)
       end
   end
 
+  def is_gender_known?(gender_code)
+    gender_code.in?(%w[male female])
path: app/models/patient_import_row.rb
user: login: misaka
body: Might be worth adding a comment about this behaviour to the Jira ticket in the testing section to ask testers to have a look and see what they think.
created_at: 2025-04-23T07:58:48Z
author_association: COLLABORATOR

PR comments:
user: login: misaka
created_at: 2025-04-23T07:56:14Z
body: > The code looks good, I would suggest the commit messages can be swashed together as I don't think we need the level of granularity in the commit history.

➕1 for swashing commit messages! 😀 
author_association: COLLABORATOR
user: login: sonarqubecloud[bot]
created_at: 2025-04-28T08:58:15Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=nhsuk_manage-childrens-vaccinations&pullRequest=3409) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=nhsuk_manage-childrens-vaccinations&pullRequest=3409&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=nhsuk_manage-childrens-vaccinations&pullRequest=3409&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=nhsuk_manage-childrens-vaccinations&pullRequest=3409&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=nhsuk_manage-childrens-vaccinations&pullRequest=3409&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=nhsuk_manage-childrens-vaccinations&pullRequest=3409&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=nhsuk_manage-childrens-vaccinations&pullRequest=3409)


author_association: NONE