Repository: fabiocaccamo/python-codicefiscale

Issue:
Issue with Incorrect Validation of Valid 'Codice Fiscale'
**Python version**
3.11.2

**Package version**
0.9.1

**Current behavior (bug description)**
I've encountered an issue with the library's validation function, where it incorrectly marks a certain Italian "codice fiscale" as invalid. The "codice fiscale" in question is validated correctly on the official website of the Italian "Agenzia delle Entrate."

**Expected behavior**
The library should validate this pattern as it aligns with the format deemed valid by the "Agenzia delle Entrate".


<!-- POLAR PLEDGE BADGE START -->
## Upvote & Fund

- We're using [Polar.sh](https://polar.sh/fabiocaccamo) so you can upvote and help fund this issue.
- We receive the funding once the issue is completed & confirmed by you.
- Thank you in advance for helping prioritize & fund our backlog.

<a href="https://polar.sh/fabiocaccamo/python-codicefiscale/issues/162">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/fabiocaccamo/python-codicefiscale/issues/162/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/fabiocaccamo/python-codicefiscale/issues/162/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->


Discussion:

Patch:
diff --git a/codicefiscale/codicefiscale.py b/codicefiscale/codicefiscale.py
index 52bd403..42e2c44 100644
--- a/codicefiscale/codicefiscale.py
+++ b/codicefiscale/codicefiscale.py
@@ -2,7 +2,7 @@ from __future__ import annotations
 
 import re
 import string
-from datetime import datetime
+from datetime import datetime, timedelta
 from itertools import combinations
 from typing import Any, Literal, Pattern
 
@@ -116,6 +116,7 @@ def _get_date(
     if not date:
         return None
     if isinstance(date, datetime):
+        date = date.replace(tzinfo=None)
         return date
     date_slug = slugify(date)
     date_parts = date_slug.split("-")[:3]
@@ -133,6 +134,7 @@ def _get_date(
             date_slug,
             parserinfo=date_parser.parserinfo(**date_parser_options),
         )
+        date_obj = date_obj.replace(tzinfo=None)
         return date_obj
     except ValueError:
         return None
@@ -141,7 +143,7 @@ def _get_date(
 def _get_birthplace(
     birthplace: str,
     birthdate: datetime | str | None = None,
-) -> dict[str, dict[str, bool | datetime | str | list[str]]] | None | None:
+) -> dict[str, dict[str, Any]] | None:
     birthplace_slug = slugify(birthplace)
     birthplace_code = birthplace_slug.upper()
     birthplaces_options = _DATA["municipalities"].get(
@@ -160,15 +162,42 @@ def _get_birthplace(
     if not birthdate_date:
         return birthplaces_options[0].copy()
 
+    # search birthplace that has been created before / deleted after birthdate
     for birthplace_option in birthplaces_options:
         date_created = _get_date(birthplace_option["date_created"]) or datetime.min
-        date_created = date_created.replace(tzinfo=None)
         date_deleted = _get_date(birthplace_option["date_deleted"]) or datetime.max
-        date_deleted = date_deleted.replace(tzinfo=None)
         # print(birthdate_date, date_created, date_deleted)
         if birthdate_date >= date_created and birthdate_date <= date_deleted:
             return birthplace_option.copy()
 
+    return _get_birthplace_fallback(birthplaces_options, birthdate_date)
+
+
+def _get_birthplace_fallback(
+    birthplaces_options: list[dict[str, Any]],
+    birthdate_date: datetime,
+) -> dict[str, dict[str, Any]] | None:
+    # avoid wrong birthplace code error when birthdate falls in
+    # missing date-range in the data-source even if birthplace code is valid
+    if len(birthplaces_options) > 1:
+        for index in range(len(birthplaces_options) - 1):
+            birthplace_option = birthplaces_options[index]
+            birthplace_option_next = birthplaces_options[(index + 1)]
+            date_deleted = _get_date(birthplace_option["date_deleted"])
+            date_created = _get_date(birthplace_option_next["date_created"])
+            if date_deleted and date_created:
+                if birthdate_date >= date_deleted and date_deleted <= date_created:
+                    # the birthdate is in between a deleted munipality and a created one
+                    # if the deleted one has a very short active time delta,
+                    # it means that probably the deleted_at value is wrong
+                    date_created = _get_date(birthplace_option["date_created"])
+                    date_deleted = _get_date(birthplace_option["date_deleted"])
+                    if date_created and date_deleted:
+                        date_delta = date_deleted - date_created
+                        if date_delta <= timedelta(days=1):
+                            return birthplace_option.copy()
+                    return birthplace_option_next.copy()
+
     return None
 
 
diff --git a/codicefiscale/data/municipalities-patch.json b/codicefiscale/data/municipalities-patch.json
index 5c506ca..32960f8 100644
--- a/codicefiscale/data/municipalities-patch.json
+++ b/codicefiscale/data/municipalities-patch.json
@@ -1,72 +1,2 @@
 [
-    {
-        "active": false,
-        "code": "F383",
-        "date_created": "1928-05-12T00:00:00",
-        "date_deleted": "1991-12-09T00:00:00",
-        "name": "Monserrato",
-        "name_alt": "",
-        "name_alt_trans": "",
-        "name_slugs": [
-            "monserrato"
-        ],
-        "name_trans": "Monserrato",
-        "province": "CA"
-    },
-    {
-        "active": false,
-        "code": "F469",
-        "date_created": "1929-06-02T00:00:00",
-        "date_deleted": "1946-07-01T00:00:00",
-        "name": "Montechiaro D'Acqui",
-        "name_alt": "",
-        "name_alt_trans": "",
-        "name_slugs": [
-            "montechiaro-d-acqui"
-        ],
-        "name_trans": "Montechiaro D'Acqui",
-        "province": "AL"
-    },
-    {
-        "active": false,
-        "code": "G133",
-        "date_created": "1928-11-28T00:00:00",
-        "date_deleted": "1948-04-07T00:00:00",
-        "name": "Ortacesus",
-        "name_alt": "",
-        "name_alt_trans": "",
-        "name_slugs": [
-            "ortacesus"
-        ],
-        "name_trans": "Ortacesus",
-        "province": "CA"
-    },
-    {
-        "active": false,
-        "code": "H059",
-        "date_created": "1929-03-26T00:00:00",
-        "date_deleted": "1947-03-29T00:00:00",
-        "name": "Priero",
-        "name_alt": "",
-        "name_alt_trans": "",
-        "name_slugs": [
-            "priero"
-        ],
-        "name_trans": "Priero",
-        "province": "CN"
-    },
-    {
-        "active": false,
-        "code": "L513",
-        "date_created": "1927-11-04T00:00:00",
-        "date_deleted": "1946-02-27T00:00:00",
-        "name": "Ussaramanna",
-        "name_alt": "",
-        "name_alt_trans": "",
-        "name_slugs": [
-            "ussaramanna"
-        ],
-        "name_trans": "Ussaramanna",
-        "province": "CA"
-    }
 ]
\ No newline at end of file
diff --git a/codicefiscale/data/municipalities.json b/codicefiscale/data/municipalities.json
index 3b38bbc..43fe8ea 100644
--- a/codicefiscale/data/municipalities.json
+++ b/codicefiscale/data/municipalities.json
@@ -3382,7 +3382,7 @@
         "active": false,
         "code": "A134",
         "date_created": "1961-03-09T00:00:00",
-        "date_deleted": "2023-12-12T00:00:00",
+        "date_deleted": "2023-11-17T00:00:00",
         "name": "Albaredo Arnaboldi",
         "name_alt": "",
         "name_alt_trans": "",
@@ -85886,7 +85886,7 @@
         "date_created": "2009-01-29T00:00:00",
         "date_deleted": "",
         "name": "Duino Aurisina-Devin Nabre\u017eina",
-        "name_alt": "Comune Di Duino Aurisina-Obcina Devin Nabre\u017eina",
+        "name_alt": "Comune Di Duino Aurisina-Ob\u010dina Devin Nabre\u017eina",
         "name_alt_trans": "Comune Di Duino Aurisina-Obcina Devin Nabrezina",
         "name_slugs": [
             "comune-di-duino-aurisina-obcina-devin-nabrezina",
@@ -139098,20 +139098,6 @@
         "name_trans": "Monserrato",
         "province": "CA"
     },
-    {
-        "active": false,
-        "code": "F383",
-        "date_created": "1928-05-12T00:00:00",
-        "date_deleted": "1991-12-09T00:00:00",
-        "name": "Monserrato",
-        "name_alt": "",
-        "name_alt_trans": "",
-        "name_slugs": [
-            "monserrato"
-        ],
-        "name_trans": "Monserrato",
-        "province": "CA"
-    },
     {
         "active": false,
         "code": "F384",
@@ -142276,20 +142262,6 @@
         "name_trans": "Montechiaro D'Acqui",
         "province": "AL"
     },
-    {
-        "active": false,
-        "code": "F469",
-        "date_created": "1929-06-02T00:00:00",
-        "date_deleted": "1946-07-01T00:00:00",
-        "name": "Montechiaro D'Acqui",
-        "name_alt": "",
-        "name_alt_trans": "",
-        "name_slugs": [
-            "montechiaro-d-acqui"
-        ],
-        "name_trans": "Montechiaro D'Acqui",
-        "province": "AL"
-    },
     {
         "active": false,
         "code": "F468",
@@ -158611,20 +158583,6 @@
         "name_trans": "Ortacesus",
         "province": "SU"
     },
-    {
-        "active": false,
-        "code": "G133",
-        "date_created": "1928-11-28T00:00:00",
-        "date_deleted": "1948-04-07T00:00:00",
-        "name": "Ortacesus",
-        "name_alt": "",
-        "name_alt_trans": "",
-        "name_slugs": [
-            "ortacesus"
-        ],
-        "name_trans": "Ortacesus",
-        "province": "CA"
-    },
     {
         "active": false,
         "code": "G135",
@@ -183423,20 +183381,6 @@
         "name_trans": "Priero",
         "province": "CN"
     },
-    {
-        "active": false,
-        "code": "H059",
-        "date_created": "1929-03-26T00:00:00",
-        "date_deleted": "1947-03-29T00:00:00",
-        "name": "Priero",
-        "name_alt": "",
-        "name_alt_trans": "",
-        "name_slugs": [
-            "priero"
-        ],
-        "name_trans": "Priero",
-        "province": "CN"
-    },
     {
         "active": false,
         "code": "H060",
@@ -204419,7 +204363,7 @@
         "date_created": "1923-02-08T00:00:00",
         "date_deleted": "",
         "name": "San Dorligo Della Valle-Dolina",
-        "name_alt": "Comune Di San Dorligo Della Valle-Obcina Dolina",
+        "name_alt": "Comune Di San Dorligo Della Valle-Ob\u010dina Dolina",
         "name_alt_trans": "Comune Di San Dorligo Della Valle-Obcina Dolina",
         "name_slugs": [
             "comune-di-san-dorligo-della-valle-obcina-dolina",
@@ -204984,10 +204928,9 @@
         "date_created": "2009-01-29T00:00:00",
         "date_deleted": "",
         "name": "San Floriano Del Collio-\u00c5 Teverjan",
-        "name_alt": "Comune Di San Floriano Del Collio-Obcina \u00c5 Teverjan",
+        "name_alt": "Comune Di San Floriano Del Collio-Ob\u010dina \u0160teverjan",
         "name_alt_trans": "Comune Di San Floriano Del Collio-Obcina Steverjan",
         "name_slugs": [
-            "comune-di-san-floriano-del-collio-obcina-a-teverjan",
             "comune-di-san-floriano-del-collio-obcina-steverjan",
             "san-floriano-del-collio-a-teverjan",
             "san-floriano-del-collio-steverjan"
@@ -251869,20 +251812,6 @@
         "name_trans": "Ussaramanna",
         "province": "SU"
     },
-    {
-        "active": false,
-        "code": "L513",
-        "date_created": "1927-11-04T00:00:00",
-        "date_deleted": "1946-02-27T00:00:00",
-        "name": "Ussaramanna",
-        "name_alt": "",
-        "name_alt_trans": "",
-        "name_slugs": [
-            "ussaramanna"
-        ],
-        "name_trans": "Ussaramanna",
-        "province": "CA"
-    },
     {
         "active": false,
         "code": "L514",
diff --git a/pyproject.toml b/pyproject.toml
index 9440411..b478993 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -83,12 +83,10 @@ ignore_missing_imports = true
 strict = true
 
 [tool.ruff]
-ignore = []
 line-length = 88
-select = ["B", "B9", "C", "E", "F", "W"]
-
-[tool.ruff.mccabe]
-max-complexity = 10
+lint.ignore = []
+lint.mccabe.max-complexity = 10
+lint.select = ["B", "B9", "C", "E", "F", "W"]
 
 [tool.setuptools.packages.find]
 include = ["codicefiscale*"]


Test patch:
diff --git a/tests/test_issue_0162.py b/tests/test_issue_0162.py
new file mode 100644
index 0000000..c45ca5d
--- /dev/null
+++ b/tests/test_issue_0162.py
@@ -0,0 +1,46 @@
+import datetime
+import unittest
+
+from codicefiscale import codicefiscale
+
+
+class Issue0162TestCase(unittest.TestCase):
+    def test_issue_0162(self):
+        """
+        Wrong birthplace code error (missing date-range in the data-source).
+        """
+        self.maxDiff = None
+        code = "DFLNTN42T20B860H"
+        self.assertTrue(codicefiscale.is_valid(code))
+        code_data = codicefiscale.decode(code)
+        code_data.pop("omocodes")
+        # print(code_data)
+        expected_code_data = {
+            "code": "DFLNTN42T20B860H",
+            "gender": "M",
+            "birthdate": datetime.datetime(1942, 12, 20, 0, 0),
+            "birthplace": {
+                "active": False,
+                "code": "B860",
+                "date_created": "1927-01-12T00:00:00",
+                "date_deleted": "1928-06-27T00:00:00",
+                "name": "Casagiove",
+                "name_alt": "",
+                "name_alt_trans": "",
+                "name_slugs": ["casagiove"],
+                "name_trans": "Casagiove",
+                "province": "NA",
+            },
+            "raw": {
+                "code": "DFLNTN42T20B860H",
+                "lastname": "DFL",
+                "firstname": "NTN",
+                "birthdate": "42T20",
+                "birthdate_year": "42",
+                "birthdate_month": "T",
+                "birthdate_day": "20",
+                "birthplace": "B860",
+                "cin": "H",
+            },
+        }
+        self.assertEqual(code_data, expected_code_data)