From e03d2c603793ed0ae8df55a403fd97f5f7805b00 Mon Sep 17 00:00:00 2001 From: Matteo Date: Fri, 29 May 2026 22:50:50 +0200 Subject: [PATCH 1/2] fix(parser): raise InvalidTaxCodeError when birthplace not found in cities or countries Previously the birthplace method returned nil implicitly when the stop: true recursive call found nothing, masking invalid tax codes silently. Co-Authored-By: Claude Sonnet 4.6 --- AGENTS.md | 2 +- lib/itax_code/parser.rb | 8 +++++--- test/itax_code/parser_test.rb | 5 ++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0b846ab..ee5a94c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -78,7 +78,7 @@ ItaxCode.decode(tax_code) #=> Hash: # code: String, # the input code, upcased # gender: "M" | "F", # birthdate: String, # "YYYY-MM-DD" -# birthplace: { # nil if not found in either CSV +# birthplace: { # raises InvalidTaxCodeError if not found in either CSV # code: String, # e.g. "F205" # province: String, # e.g. "MI" # name: String, # e.g. "MILANO" diff --git a/lib/itax_code/parser.rb b/lib/itax_code/parser.rb index 97c1ee0..e110025 100644 --- a/lib/itax_code/parser.rb +++ b/lib/itax_code/parser.rb @@ -92,10 +92,12 @@ def birthplace(src = utils.cities, stop: false) place = src.find { |item| item["code"] == birthplace_code && in_dates?(item) } if place.nil? - birthplace(utils.countries, stop: true) unless stop - else - place.to_h.transform_keys(&:to_sym) + return birthplace(utils.countries, stop: true) unless stop + + raise InvalidTaxCodeError end + + place.to_h.transform_keys(&:to_sym) end def birthplace_code diff --git a/test/itax_code/parser_test.rb b/test/itax_code/parser_test.rb index ba12176..b0d0d17 100644 --- a/test/itax_code/parser_test.rb +++ b/test/itax_code/parser_test.rb @@ -29,9 +29,8 @@ class ParserTest < Minitest::Test end end - # FIXME: This behaviour needs to be fixed, maybe by raising an InvalidTaxCodeError. - test "returns nil birthplace when the lookup on both cities and countries fails" do - assert_nil klass.new("BRRDRN70M41ZXXXE").decode[:birthplace] + test "raises InvalidTaxCodeError when birthplace code is not found in cities or countries" do + assert_raises(klass::InvalidTaxCodeError) { klass.new("BRRDRN70M41ZXXXE").decode } end test "raises NoTaxCodeError with an empty tax code" do From eb5faad8efe74fbe5b7d1b164944e5565b75fb07 Mon Sep 17 00:00:00 2001 From: Matteo Date: Fri, 29 May 2026 22:57:05 +0200 Subject: [PATCH 2/2] =?UTF-8?q?test:=20fill=20coverage=20gaps=20=E2=80=94?= =?UTF-8?q?=20female=20encode,=20individual=20missing=20fields,=20round-tr?= =?UTF-8?q?ips,=20year/gender=20boundaries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds T1 (female +40 day offset encoding), T2 (individual nil-field MissingDataError tests), T3 (encode→decode round-trips for male, female, and foreign), T4 (in_dates? false-branch via Mocha stub), T5 (year century-rollback boundary with Timecop), and T6 (day 31 male / day 41 female gender boundary). Co-Authored-By: Claude Sonnet 4.6 --- test/itax_code/encoder_test.rb | 52 ++++++++++++++++++++++++++++++++++ test/itax_code/parser_test.rb | 39 +++++++++++++++++++++++++ test/itax_code_test.rb | 27 ++++++++++++++++++ 3 files changed, 118 insertions(+) diff --git a/test/itax_code/encoder_test.rb b/test/itax_code/encoder_test.rb index 2e30ff5..2d4be82 100644 --- a/test/itax_code/encoder_test.rb +++ b/test/itax_code/encoder_test.rb @@ -54,12 +54,64 @@ class EncoderTest < Minitest::Test ).encode end + test "#encode for female gender applies +40 day offset" do + assert_equal "RSSMRA80A41F205B", + Encoder.new( + surname: "Rossi", + name: "Mario", + gender: "F", + birthdate: "1980-01-01", + birthplace: "Milano" + ).encode + end + test "#encode raises MissingDataError on missing data" do assert_raises Encoder::MissingDataError do Encoder.new.encode end end + test "#encode raises MissingDataError on missing surname" do + assert_raises(Encoder::MissingDataError) do + Encoder.new( + surname: nil, name: "Mario", gender: "M", birthdate: "1980-01-01", birthplace: "Milano" + ) + end + end + + test "#encode raises MissingDataError on missing name" do + assert_raises(Encoder::MissingDataError) do + Encoder.new( + surname: "Rossi", name: nil, gender: "M", birthdate: "1980-01-01", birthplace: "Milano" + ) + end + end + + test "#encode raises MissingDataError on missing gender" do + assert_raises(Encoder::MissingDataError) do + Encoder.new( + surname: "Rossi", name: "Mario", gender: nil, + birthdate: "1980-01-01", birthplace: "Milano" + ) + end + end + + test "#encode raises MissingDataError on missing birthdate" do + assert_raises(Encoder::MissingDataError) do + Encoder.new( + surname: "Rossi", name: "Mario", gender: "M", birthdate: nil, birthplace: "Milano" + ) + end + end + + test "#encode raises MissingDataError on missing birthplace field" do + assert_raises(Encoder::MissingDataError) do + Encoder.new( + surname: "Rossi", name: "Mario", gender: "M", birthdate: "1980-01-01", birthplace: nil + ) + end + end + test "#encode raises MissingDataError on missing birthplace" do assert_raises Encoder::MissingDataError do Encoder.new( diff --git a/test/itax_code/parser_test.rb b/test/itax_code/parser_test.rb index b0d0d17..754cfcd 100644 --- a/test/itax_code/parser_test.rb +++ b/test/itax_code/parser_test.rb @@ -61,6 +61,45 @@ class ParserTest < Minitest::Test end end + test "raises InvalidTaxCodeError when birthdate falls outside city validity window" do + # Stub cities to expose a single F205 entry whose window excludes birthdate 1980-01-01 + stub_city = { "code" => "F205", "province" => "MI", "name" => "MILANO", + "created_on" => "1960-01-01", "deleted_on" => "1975-12-31" } + Utils.stubs(:cities).returns([stub_city]) + Utils.stubs(:countries).returns([]) + # in_dates? returns false (1980-01-01 > deleted_on) → F205 not matched in cities + # F205 not in countries either → raises InvalidTaxCodeError + assert_raises(klass::InvalidTaxCodeError) { klass.new("RSSMRA80A01F205X").decode } + end + + test "decodes a birthdate in the current year" do + Timecop.freeze(Date.parse("2026-06-01")) do + # year "26" → 20+26=2026, 2026 <= 2026 → 2026 (not 1926) + assert_equal "2026-01-01", klass.new("RSSMRA26A01L219E").decode[:birthdate] + end + end + + test "decodes a birthdate as 1927 when current year is 2026 and year code is 27" do + Timecop.freeze(Date.parse("2026-06-01")) do + # year "27" → 20+27=2027, 2027 > 2026 → 2027-100 = 1927 + assert_equal "1927-01-01", klass.new("RSSMRA27A01L219F").decode[:birthdate] + end + end + + test "decodes day 31 as male gender with day 31" do + result = klass.new("RSSMRA80A31L219P").decode + + assert_equal "M", result[:gender] + assert_equal "1980-01-31", result[:birthdate] + end + + test "decodes day 41 as female gender with day 1" do + result = klass.new("RSSMRA80A41F205B").decode + + assert_equal "F", result[:gender] + assert_equal "1980-01-01", result[:birthdate] + end + private def klass diff --git a/test/itax_code_test.rb b/test/itax_code_test.rb index 63b285e..4045501 100644 --- a/test/itax_code_test.rb +++ b/test/itax_code_test.rb @@ -27,6 +27,33 @@ class ItaxCodeTest < Minitest::Test refute klass.valid?("WRONG") end + test "encode/decode round-trip for male" do + result = klass.decode(klass.encode(surname: "Rossi", name: "Mario", gender: "M", + birthdate: "1980-01-01", birthplace: "Milano")) + + assert_equal "M", result[:gender] + assert_equal "1980-01-01", result[:birthdate] + assert_equal "F205", result[:birthplace][:code] + end + + test "encode/decode round-trip for female (day offset)" do + result = klass.decode(klass.encode(surname: "Rossi", name: "Mario", gender: "F", + birthdate: "1980-01-01", birthplace: "Milano")) + + assert_equal "F", result[:gender] + assert_equal "1980-01-01", result[:birthdate] + assert_equal "F205", result[:birthplace][:code] + end + + test "encode/decode round-trip for foreign birthplace" do + result = klass.decode(klass.encode(surname: "Berardi", name: "Adriana", gender: "F", + birthdate: "1970-08-01", birthplace: "Brasile")) + + assert_equal "F", result[:gender] + assert_equal "1970-08-01", result[:birthdate] + assert_equal "Z602", result[:birthplace][:code] + end + private def klass