diff --git a/services/well_inventory_csv.py b/services/well_inventory_csv.py index 049eddea..89ece733 100644 --- a/services/well_inventory_csv.py +++ b/services/well_inventory_csv.py @@ -681,6 +681,22 @@ def _add_csv_row(session: Session, group: Group, model: WellInventoryRow, user) } ) + if ( + model.mp_height is not None + and model.measuring_point_height_ft is not None + and model.mp_height != model.measuring_point_height_ft + ): + raise ValueError( + "Conflicting values for measuring point height: mp_height and measuring_point_height_ft" + ) + + if model.measuring_point_height_ft is not None: + universal_mp_height = model.measuring_point_height_ft + elif model.mp_height is not None: + universal_mp_height = model.mp_height + else: + universal_mp_height = None + data = CreateWell( location_id=loc.id, group_id=group.id, @@ -689,7 +705,7 @@ def _add_csv_row(session: Session, group: Group, model: WellInventoryRow, user) well_depth=model.total_well_depth_ft, well_depth_source=model.depth_source, well_casing_diameter=model.casing_diameter_ft, - measuring_point_height=model.measuring_point_height_ft, + measuring_point_height=universal_mp_height, measuring_point_description=model.measuring_point_description, well_completion_date=model.date_drilled, well_completion_date_source=model.completion_source, @@ -821,6 +837,11 @@ def _add_csv_row(session: Session, group: Group, model: WellInventoryRow, user) session.add(sample) session.flush() + if model.depth_to_water_ft is not None and universal_mp_height is None: + raise ValueError( + "measuring_point_height_ft or mp_height is required when depth_to_water_ft is provided for a non-null observation" + ) + # create Observation # TODO: groundwater_level_reason may be conditionally required for null depth_to_water_ft - handle accordingly observation = Observation( @@ -829,7 +850,7 @@ def _add_csv_row(session: Session, group: Group, model: WellInventoryRow, user) value=model.depth_to_water_ft, unit="ft", observation_datetime=model.measurement_date_time, - measuring_point_height=model.mp_height, + measuring_point_height=universal_mp_height, groundwater_level_reason=( model.level_status.value if hasattr(model.level_status, "value") diff --git a/tests/test_well_inventory.py b/tests/test_well_inventory.py index 08d2573b..fd27e395 100644 --- a/tests/test_well_inventory.py +++ b/tests/test_well_inventory.py @@ -481,7 +481,7 @@ def test_well_inventory_db_contents_with_waterlevels(tmp_path): "sample_method": "Steel-tape measurement", "data_quality": "Water level accurate to within two hundreths of a foot", "water_level_notes": "Attempted measurement", - "mp_height_ft": 2.5, + "mp_height_ft": 3.5, "level_status": "Water level not affected", } ) @@ -529,6 +529,160 @@ def test_well_inventory_db_contents_with_waterlevels(tmp_path): assert observation.sample == sample +def test_measuring_point_height_ft_used_for_thing_and_observation(tmp_path): + """When measuring_point_height_ft is provided it is used for the thing's (MeasuringPointHistory) and observation's measuring_point_height values.""" + row = _minimal_valid_well_inventory_row() + row.update( + { + "measuring_point_height_ft": 3.5, + "water_level_date_time": "2025-02-15T10:30:00", + "depth_to_water_ft": "8", + "sample_method": "Steel-tape measurement", + "data_quality": "Water level accurate to within two hundreths of a foot", + "water_level_notes": "Attempted measurement", + } + ) + + file_path = tmp_path / "well-inventory-blank-depth.csv" + with file_path.open("w", encoding="utf-8", newline="") as f: + writer = csv.DictWriter(f, fieldnames=list(row.keys())) + writer.writeheader() + writer.writerow(row) + + result = well_inventory_csv(file_path) + assert result.exit_code == 0, result.stderr + + with session_ctx() as session: + things = session.query(Thing).all() + observations = session.query(Observation).all() + + assert len(things) == 1 + assert things[0].measuring_point_height == 3.5 + assert len(observations) == 1 + assert observations[0].measuring_point_height == 3.5 + + +def test_mp_height_used_for_thing_and_observation_when_measuring_point_height_ft_blank( + tmp_path, +): + """When depth to water is provided and measuring_point_height_ft is blank the mp_height value should be used for the thing's (MeasuringPointHistory) and observation's measuring_point_height.""" + row = _minimal_valid_well_inventory_row() + row.update( + { + "measuring_point_height_ft": "", + "water_level_date_time": "2025-02-15T10:30:00", + "depth_to_water_ft": "8", + "sample_method": "Steel-tape measurement", + "data_quality": "Water level accurate to within two hundreths of a foot", + "water_level_notes": "Attempted measurement", + "mp_height": 4.0, + } + ) + + file_path = tmp_path / "well-inventory-blank-depth.csv" + with file_path.open("w", encoding="utf-8", newline="") as f: + writer = csv.DictWriter(f, fieldnames=list(row.keys())) + writer.writeheader() + writer.writerow(row) + + result = well_inventory_csv(file_path) + assert result.exit_code == 0, result.stderr + + with session_ctx() as session: + things = session.query(Thing).all() + observations = session.query(Observation).all() + + assert len(things) == 1 + assert things[0].measuring_point_height == 4.0 + assert len(observations) == 1 + assert observations[0].measuring_point_height == 4.0 + + +def test_null_observation_allows_blank_mp_height(tmp_path): + """When depth to water is not provided (ie null), blank measuring_point_height_ft and mp_height fields should be allowed and result in a null measuring_point_height for the observation and no associated measuring point height (MeasuringPointHistory) for the well.""" + row = _minimal_valid_well_inventory_row() + row.update( + { + "measuring_point_height_ft": "", + "water_level_date_time": "2025-02-15T10:30:00", + "depth_to_water_ft": "", + "sample_method": "Steel-tape measurement", + "data_quality": "Water level accurate to within two hundreths of a foot", + "water_level_notes": "Attempted measurement", + } + ) + + file_path = tmp_path / "well-inventory-blank-depth.csv" + with file_path.open("w", encoding="utf-8", newline="") as f: + writer = csv.DictWriter(f, fieldnames=list(row.keys())) + writer.writeheader() + writer.writerow(row) + + result = well_inventory_csv(file_path) + assert result.exit_code == 0, result.stderr + + with session_ctx() as session: + things = session.query(Thing).all() + observations = session.query(Observation).all() + + assert len(things) == 1 + assert things[0].measuring_point_height is None + assert len(observations) == 1 + assert observations[0].measuring_point_height is None + + +def test_conflicting_mp_heights_raises_error(tmp_path): + """ + When both measuring_point_height_ft and mp_height are provided, an inequality (conflict) should raise an error. + """ + row = _minimal_valid_well_inventory_row() + + row.update( + { + "measuring_point_height_ft": 3.5, + "mp_height": 4.0, + } + ) + + file_path = tmp_path / "well-inventory-blank-depth.csv" + with file_path.open("w", encoding="utf-8", newline="") as f: + writer = csv.DictWriter(f, fieldnames=list(row.keys())) + writer.writeheader() + writer.writerow(row) + + result = well_inventory_csv(file_path) + assert result.exit_code == 1, result.stderr + assert ( + result.payload["validation_errors"][0]["error"] + == "Conflicting values for measuring point height: mp_height and measuring_point_height_ft" + ) + + +def test_no_mp_height_raises_error_when_depth_to_water_provided(tmp_path): + row = _minimal_valid_well_inventory_row() + row.update( + { + "water_level_date_time": "2025-02-15T10:30:00", + "measuring_point_height_ft": "", + "mp_height": "", + "depth_to_water_ft": "8", + } + ) + + file_path = tmp_path / "well-inventory-no-mp-height.csv" + with file_path.open("w", encoding="utf-8", newline="") as f: + writer = csv.DictWriter(f, fieldnames=list(row.keys())) + writer.writeheader() + writer.writerow(row) + + result = well_inventory_csv(file_path) + assert result.exit_code == 1, result.stderr + assert ( + result.payload["validation_errors"][0]["error"] + == "measuring_point_height_ft or mp_height is required when depth_to_water_ft is provided for a non-null observation" + ) + + def test_blank_depth_to_water_still_creates_water_level_records(tmp_path): """Blank depth-to-water is treated as missing while preserving the attempted measurement.""" row = _minimal_valid_well_inventory_row() @@ -539,7 +693,7 @@ def test_blank_depth_to_water_still_creates_water_level_records(tmp_path): "sample_method": "Steel-tape measurement", "data_quality": "Water level accurate to within two hundreths of a foot", "water_level_notes": "Attempted measurement", - "mp_height_ft": 2.5, + "mp_height_ft": 3.5, } ) @@ -563,7 +717,7 @@ def test_blank_depth_to_water_still_creates_water_level_records(tmp_path): "2025-02-15T10:30:00Z" ) assert observations[0].value is None - assert observations[0].measuring_point_height == 2.5 + assert observations[0].measuring_point_height == 3.5 def test_rerunning_same_well_inventory_csv_is_idempotent():