From c579a4e7e220f771bf9490bb797c4bba92f44cc0 Mon Sep 17 00:00:00 2001 From: Kenneth Lee Date: Tue, 10 Dec 2024 17:03:08 +0000 Subject: [PATCH 1/3] CAPT-2040 normalize names for payroll csv export --- app/models/payroll/name_normalizer.rb | 39 ++++++++++++++ app/models/payroll/payment_csv_row.rb | 12 +++++ spec/models/payroll/name_normalizer_spec.rb | 57 +++++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 app/models/payroll/name_normalizer.rb create mode 100644 spec/models/payroll/name_normalizer_spec.rb diff --git a/app/models/payroll/name_normalizer.rb b/app/models/payroll/name_normalizer.rb new file mode 100644 index 0000000000..1498179dad --- /dev/null +++ b/app/models/payroll/name_normalizer.rb @@ -0,0 +1,39 @@ +module Payroll + class NameNormalizer + def self.normalize(name) + new(name).normalize + end + + def initialize(name) + @name = name + end + + def normalize + return nil if @name.nil? + + @name + .then { |n| remove_typical_disallowed_chars(n) } + .then { |n| transliterate(n) } + .then { |n| final_cleanup(n) } + end + + private + + # Things we allow from `NameFormatValidator` and typically found in names + # but Payroll provider doesn't accept + # Also handles curly apostrophe commonly found + def remove_typical_disallowed_chars(name) + name.gsub(/[,.;\-'‘’\s]/, "") + end + + # Attempt to replace things like `è` with `e` + def transliterate(name) + I18n.transliterate(name) + end + + # Just remove anything missed that aren't allowed by Payroll + def final_cleanup(name) + name.gsub(/[^A-Za-z]/, "") + end + end +end diff --git a/app/models/payroll/payment_csv_row.rb b/app/models/payroll/payment_csv_row.rb index cf34456281..52b17c29f6 100644 --- a/app/models/payroll/payment_csv_row.rb +++ b/app/models/payroll/payment_csv_row.rb @@ -27,6 +27,18 @@ def to_a private + def first_name + NameNormalizer.normalize(model.first_name) + end + + def middle_name + NameNormalizer.normalize(model.middle_name) + end + + def surname + NameNormalizer.normalize(model.surname) + end + def title TITLE end diff --git a/spec/models/payroll/name_normalizer_spec.rb b/spec/models/payroll/name_normalizer_spec.rb new file mode 100644 index 0000000000..5d87f8c87d --- /dev/null +++ b/spec/models/payroll/name_normalizer_spec.rb @@ -0,0 +1,57 @@ +require "rails_helper" + +RSpec.describe Payroll::NameNormalizer do + describe ".normalize" do + subject { described_class.normalize(name) } + + context "nil" do + let(:name) { nil } + it { is_expected.to be_nil } + end + + context "empty string" do + let(:name) { "" } + it { is_expected.to eq "" } + end + + context "blank string" do + let(:name) { " " } + it { is_expected.to eq "" } + end + + context "name with nothing to change" do + let(:name) { "John" } + it { is_expected.to eq "John" } + end + + context "name curly quotes" do + let(:name) { "O’Something" } + it { is_expected.to eq "OSomething" } + end + + context "name accents and spaces" do + let(:name) { "Óscar Hernández" } + it { is_expected.to eq "OscarHernandez" } + end + + context "name with multiple spaces" do + let(:name) { "Chan Chiu Bruce" } + it { is_expected.to eq "ChanChiuBruce" } + end + + context "name with emojis spaces" do + let(:name) { "Thumbs 👍 Up " } + it { is_expected.to eq "ThumbsUp" } + end + + context "name with semi-colon" do + let(:name) { "Samuel;" } + it { is_expected.to eq "Samuel" } + end + + context "name that has it all" do + let(:name) { "Jámes', Ryan, O’Hughes 👍" } + it { is_expected.to eq "JamesRyanOHughes" } + end + end +end From 0b2dcb01c9c5e34f2c4e086cb0df7a5c1cff71ed Mon Sep 17 00:00:00 2001 From: Kenneth Lee Date: Thu, 2 Jan 2025 14:20:39 +0000 Subject: [PATCH 2/3] Remove redundant function --- app/models/payroll/name_normalizer.rb | 11 ++--------- spec/models/payroll/name_normalizer_spec.rb | 12 +++++++++++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/models/payroll/name_normalizer.rb b/app/models/payroll/name_normalizer.rb index 1498179dad..94b867bdd6 100644 --- a/app/models/payroll/name_normalizer.rb +++ b/app/models/payroll/name_normalizer.rb @@ -12,26 +12,19 @@ def normalize return nil if @name.nil? @name - .then { |n| remove_typical_disallowed_chars(n) } .then { |n| transliterate(n) } .then { |n| final_cleanup(n) } end private - # Things we allow from `NameFormatValidator` and typically found in names - # but Payroll provider doesn't accept - # Also handles curly apostrophe commonly found - def remove_typical_disallowed_chars(name) - name.gsub(/[,.;\-'‘’\s]/, "") - end - + # Transliterates UTF-8 characters to ASCII # Attempt to replace things like `è` with `e` def transliterate(name) I18n.transliterate(name) end - # Just remove anything missed that aren't allowed by Payroll + # Just remove anything that isn't allowed by Payroll def final_cleanup(name) name.gsub(/[^A-Za-z]/, "") end diff --git a/spec/models/payroll/name_normalizer_spec.rb b/spec/models/payroll/name_normalizer_spec.rb index 5d87f8c87d..4ee92952f0 100644 --- a/spec/models/payroll/name_normalizer_spec.rb +++ b/spec/models/payroll/name_normalizer_spec.rb @@ -49,8 +49,18 @@ it { is_expected.to eq "Samuel" } end + context "name with hyphen" do + let(:name) { "Double-Barrelled" } + it { is_expected.to eq "DoubleBarrelled" } + end + + context "name with a period" do + let(:name) { "John. Smith" } + it { is_expected.to eq "JohnSmith" } + end + context "name that has it all" do - let(:name) { "Jámes', Ryan, O’Hughes 👍" } + let(:name) { "Jámes'. Ryan, O’Hughes 👍" } it { is_expected.to eq "JamesRyanOHughes" } end end From b42f829b6b0a980d9ccae25887bd9745b52e95dd Mon Sep 17 00:00:00 2001 From: Kenneth Lee Date: Thu, 2 Jan 2025 15:11:49 +0000 Subject: [PATCH 3/3] CAPT-2040 normalize banking name --- app/models/payroll/banking_name_normalizer.rb | 34 +++++++++++ app/models/payroll/payment_csv_row.rb | 2 +- .../payroll/banking_name_normalizer_spec.rb | 57 +++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 app/models/payroll/banking_name_normalizer.rb create mode 100644 spec/models/payroll/banking_name_normalizer_spec.rb diff --git a/app/models/payroll/banking_name_normalizer.rb b/app/models/payroll/banking_name_normalizer.rb new file mode 100644 index 0000000000..310bd401cc --- /dev/null +++ b/app/models/payroll/banking_name_normalizer.rb @@ -0,0 +1,34 @@ +module Payroll + class BankingNameNormalizer + def self.normalize(name) + new(name).normalize + end + + def initialize(name) + @name = name + end + + def normalize + return nil if @name.nil? + + @name + .then { |n| strip(n) } + .then { |n| final_cleanup(n) } + end + + private + + # CAPT-2088 - if we strip spaces in form objects this would't be needed + # Payroll requires this can't have leading spaces + def strip(name) + name.strip + end + + # Just remove anything that isn't allowed by Payroll + # BankDetailsForm::BANKING_NAME_REGEX_FILTER is actually stricter than what is allowed here + # So there is no need to worry about UTF-8 issues + def final_cleanup(name) + name.gsub(/[^A-Za-z0-9 &'()*,-.\/]/, "") + end + end +end diff --git a/app/models/payroll/payment_csv_row.rb b/app/models/payroll/payment_csv_row.rb index 52b17c29f6..7acae9dfd9 100644 --- a/app/models/payroll/payment_csv_row.rb +++ b/app/models/payroll/payment_csv_row.rb @@ -124,7 +124,7 @@ def student_loan_plan end def banking_name - model.banking_name + BankingNameNormalizer.normalize(model.banking_name) end def bank_sort_code diff --git a/spec/models/payroll/banking_name_normalizer_spec.rb b/spec/models/payroll/banking_name_normalizer_spec.rb new file mode 100644 index 0000000000..45491650c9 --- /dev/null +++ b/spec/models/payroll/banking_name_normalizer_spec.rb @@ -0,0 +1,57 @@ +require "rails_helper" + +RSpec.describe Payroll::BankingNameNormalizer do + describe ".normalize" do + subject { described_class.normalize(name) } + + context "nil" do + let(:name) { nil } + it { is_expected.to be_nil } + end + + context "empty string" do + let(:name) { "" } + it { is_expected.to eq "" } + end + + context "blank string" do + let(:name) { " " } + it { is_expected.to eq "" } + end + + context "name with nothing to change" do + let(:name) { "John" } + it { is_expected.to eq "John" } + end + + context "name with leading and trailing spaces" do + let(:name) { " John Doe " } + it { is_expected.to eq "John Doe" } + end + + context "name with spaces" do + let(:name) { "Oscar Hernandez" } + it { is_expected.to eq "Oscar Hernandez" } + end + + context "name with multiple spaces" do + let(:name) { "Chan Chiu Bruce" } + it { is_expected.to eq "Chan Chiu Bruce" } + end + + context "name with allowed characters" do + let(:name) { "John &'()*,-./ Doe" } + it { is_expected.to eq "John &'()*,-./ Doe" } + end + + context "name with disallowed characters" do + let(:name) { "John &'()*,-./+`%£^ Doe" } + it { is_expected.to eq "John &'()*,-./ Doe" } + end + + context "name that has it all" do + let(:name) { " John &'()*,-./+`%£^ Doe " } + it { is_expected.to eq "John &'()*,-./ Doe" } + end + end +end