"""Tests for material/serial sanitization in ADE detail PDF parsing.""" from __future__ import annotations import unittest from sfda_parser.sfda_scraper.detail_pdf import ( DeviceRow, _clean_material_value, _clean_serial_value, _is_likely_date_value, _is_likely_prose, _paired_rows_from_summary, _parse_summary_kv, _sanitize_device_row, ) class TestMaterialCleaning(unittest.TestCase): def test_strips_product_concerned_prefix(self): self.assertEqual( _clean_material_value("of the product concerned: TU2000 TRYTABLE"), "TU2000 TRYTABLE", ) def test_strips_trade_name_product_concerned_prefix(self): self.assertEqual( _clean_material_value("Trade name of the product concerned: TU2000 TRYTABLE"), "TU2000 TRYTABLE", ) def test_parse_summary_trade_name_line(self): lines = ["Trade name of the product concerned: TU2000 TRYTABLE"] summary = _parse_summary_kv(lines) self.assertEqual(summary.get("material"), "TU2000 TRYTABLE") class TestSerialDateRejection(unittest.TestCase): def test_detects_day_month_year_dates(self): self.assertTrue(_is_likely_date_value("21-APR-2025")) self.assertTrue(_is_likely_date_value("21/APR/2025")) def test_allows_real_serial_numbers(self): self.assertFalse(_is_likely_date_value("24B0729")) self.assertFalse(_is_likely_date_value("TSMICS1")) def test_clean_serial_rejects_dates(self): self.assertEqual(_clean_serial_value("21-APR-2025"), "") self.assertEqual(_clean_serial_value("ABC12345"), "ABC12345") def test_sanitize_device_row_clears_date_serial(self): row = DeviceRow( material="Pump", material_description="", catalog_number="", udi="", serial_no="21-APR-2025", gstn="", batch="", ) cleaned = _sanitize_device_row(row) self.assertEqual(cleaned.serial_no, "") class TestPairedUdiSn(unittest.TestCase): def test_pairs_udi_and_sn_by_position(self): udi_values = ( "04020012, 04020014, 04020017, 04020018, 04020026, 04020021, " "04020009, 04020010, 04020011, 04020001, 04020025, 04020013, " "04020003, 04020005, 04020016, 04020023, 04020028, 04020008, " "04020020, 04020019, 04020015, 04020024, 04020022, 04020006, " "04020027, 04020029, 04020030, 04020031" ) sn_values = ( "32090012, 32090014, 32090017, 32090018, 32090026, 32090021, " "32090009, 32090010, 32090011, TR05617, 32090025, 32090013, " "32090003, 302090005, 32090016, 32090023, 32090028, 32090008, " "32090020, 32090019, 32090015, 32090024, 32090022, 302090006, " "32090027, 32090029, 32090030, 32090031" ) summary = { "material": "TU2000 TRYTABLE", "udi": udi_values, "serial_no": sn_values, } rows = _paired_rows_from_summary(summary) self.assertEqual(len(rows), 28) self.assertEqual(rows[0].udi, "04020012") self.assertEqual(rows[0].serial_no, "32090012") self.assertEqual(rows[1].udi, "04020014") self.assertEqual(rows[1].serial_no, "32090014") self.assertEqual(rows[9].udi, "04020001") self.assertEqual(rows[9].serial_no, "TR05617") self.assertEqual(rows[27].udi, "04020031") self.assertEqual(rows[27].serial_no, "32090031") self.assertEqual(rows[0].material, "TU2000 TRYTABLE") def test_parse_summary_kv_stops_before_narrative(self): lines = [ "Trade name of the product concerned: TU2000 TRYTABLE", "UDI-DI: 04020012, 04020014", "SN: 32090012, 32090014", "Dear Customer", "The manufacturer has identified an error in the user manual.", ] summary = _parse_summary_kv(lines) self.assertEqual(summary.get("udi"), "04020012, 04020014") self.assertEqual(summary.get("serial_no"), "32090012, 32090014") self.assertNotIn("Dear Customer", summary.get("serial_no", "")) def test_parse_summary_kv_multiline_udi_sn_lists(self): lines = [ "Trade name of the product concerned: TU2000 TRYTABLE", "UDI-DI: 04020012, 04020014, 04020017, 04020018, 04020026, 04020021, 04020009,", "04020010, 04020011, 04020001, 04020025, 04020013, 04020003, 04020005, 04020016,", "04020023, 04020028, 04020008, 04020020, 04020019, 04020015, 04020024, 04020022,", "04020006, 04020027, 04020029, 04020030, 04020031", "SN: 32090012, 32090014, 32090017, 32090018, 32090026, 32090021, 32090009, 32090010,", "32090011, TR05617, 32090025, 32090013, 32090003, 302090005, 32090016, 32090023,", "32090028, 32090008, 32090020, 32090019, 32090015, 32090024, 32090022, 302090006,", "32090027, 32090029, 32090030, 32090031", "Manufactured: 18/07/2024 – 17/03/2026", "Dear Customer,", ] summary = _parse_summary_kv(lines) rows = _paired_rows_from_summary(summary) self.assertEqual(len(rows), 28) self.assertEqual(rows[9].udi, "04020001") self.assertEqual(rows[9].serial_no, "TR05617") class TestProseRejection(unittest.TestCase): def test_detects_narrative_fragments(self): self.assertTrue(_is_likely_prose("Dear Customer")) self.assertTrue( _is_likely_prose( "The manufacturer has identified an error in the user manual." ) ) self.assertTrue(_is_likely_prose("Manufactured: 18/07/2024 – 17/03/2026")) def test_clean_serial_rejects_prose(self): self.assertEqual(_clean_serial_value("Dear Customer"), "") self.assertEqual(_clean_serial_value("32090012"), "32090012") def test_allows_long_comma_separated_identifier_lists(self): line = ( "0012508285, 0012517045, 0012526102, 0012526103, 0012614786, 0012614787, " "0012623740, 0012623741, 0012642959, 0012659419" ) self.assertFalse(_is_likely_prose(line)) def test_clean_serial_keeps_comma_separated_lot_list(self): value = "24B0729, 25B0770, 25H0171, 26B0542" self.assertEqual(_clean_serial_value(value), value) class TestSerialLabelParsing(unittest.TestCase): def test_parse_sn_label_from_text(self): lines = [ "UDI-DI: 01234567890123", "SN: ABC12345", "Date: 21-APR-2025", ] summary = _parse_summary_kv(lines) self.assertEqual(summary.get("udi"), "01234567890123") self.assertEqual(summary.get("serial_no"), "ABC12345") def test_sn_label_ignores_date_values(self): lines = ["SN: 21-APR-2025"] summary = _parse_summary_kv(lines) self.assertNotIn("serial_no", summary) if __name__ == "__main__": unittest.main()