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/name_normalizer.rb b/app/models/payroll/name_normalizer.rb new file mode 100644 index 0000000000..94b867bdd6 --- /dev/null +++ b/app/models/payroll/name_normalizer.rb @@ -0,0 +1,32 @@ +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| transliterate(n) } + .then { |n| final_cleanup(n) } + end + + private + + # Transliterates UTF-8 characters to ASCII + # Attempt to replace things like `è` with `e` + def transliterate(name) + I18n.transliterate(name) + end + + # Just remove anything that isn'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..7acae9dfd9 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 @@ -112,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 diff --git a/spec/models/payroll/name_normalizer_spec.rb b/spec/models/payroll/name_normalizer_spec.rb new file mode 100644 index 0000000000..4ee92952f0 --- /dev/null +++ b/spec/models/payroll/name_normalizer_spec.rb @@ -0,0 +1,67 @@ +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 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 👍" } + it { is_expected.to eq "JamesRyanOHughes" } + end + end +end